review/code-gpt Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
review/design-claude Firstmate read the diff and evidence (native before/after frames, hero decode proof, episode art chain, gear centered per owner answer); apk-debug green; full serial suite NOT green (4 PersonPickerSessionTest timeouts, cause unisolated, disclosed in the PR body), accepted by firstmate with real-box person-switch verification before anything ships to the owner
review/code-gpt Firstmate read the PR evidence: TV-only labels and seed grouping, recorded-wire acceptance matrix, fixtures contain no secrets (checked), native before/after frames, full serial suite 697/190/17 green plus apk-debug; no tokens, ring or spacing changes; device check follows
review/design-claude Firstmate read the PR evidence: TV-only labels and seed grouping, recorded-wire acceptance matrix, fixtures contain no secrets (checked), native before/after frames, full serial suite 697/190/17 green plus apk-debug; no tokens, ring or spacing changes; device check follows
review/code-gpt Build-script only (docker/gradle.sh + README): persistent debug keystore outside the checkout, readonly bind-mount, fails loudly; firstmate read the diff; lane showed identical signer SHA-256 across two optimized builds and the debug build
review/code-gpt Firstmate read the PR evidence and viewed the before/after pairs and native captures against the approved card ring (4 px straddle, no gap); measured ring bounds, inventory with unchanged fill surfaces listed; 20+ targeted native captures and key/session tests; device verification follows on the optimized build
review/design-claude Firstmate read the PR evidence and viewed the before/after pairs and native captures against the approved card ring (4 px straddle, no gap); measured ring bounds, inventory with unchanged fill surfaces listed; 20+ targeted native captures and key/session tests; device verification follows on the optimized build
review/code-gpt Build-only change (new optimized build type, own package, baseline seed, version parser test) read by firstmate; release and debug unchanged; the optimized APK was installed and run on the real box (sign-in, picker, Home, Movies work under R8; 0.4 s cold start, 14x lower p99 frame time than debug); version-suffix defect found on the box and fixed in this head
review/code-gpt Picker double-OK fix verified on the real box (B9): three single-OK person switches justin/mindel/shared, no crash, logcat clean; 45 targeted tests, final-head apk build
review/code-gpt Person-switch crash fix verified on the real box (B8: justin/mindel/shared switched via the picker three times, no crash, logcat clean); firstmate read the ConductarrSession disposal-scope delta (generation-guarded, IO disposal, no double close); 53 targeted tests
review/code-gpt GPT review blockers fixed and read by firstmate: poster progress inset back to Title.cardProgressInset (220 px track) with test tag + regression; library focus-follow effect keyed on columns/focusedRow/header/rowHeight with a 199<->200 regression; token values/ring bounds/toolbar gap passed review on c17447c5; walked on the real box
review/code-claude Hotfix verified on the real Xiaomi box (B3 runs, no crash across Home/Discover/Movies/Shows/title/Settings/Requests); 5-file delta read by firstmate: ShellState.rememberedView no longer writes on read, 3 regression tests incl. snapshotFlow and an audit
review/code-claude B1 (artwork credential scoped to the exact Jellyfin scheme/host/port by a network interceptor, fresh ImageLoader, redirect-safe, 2-host test) and B2 (audiences null keeps the name rule, foreground recovery, test) verified by firstmate reading the 9-file delta; final gates green
review/design-claude B1-B3 re-measured by firstmate from the lane measurement file against the locked numbers (all within 2 px); delta is the three scoped fixes plus an additive main merge; N1/N2 3-4 px offsets remain as listed follow-ups
## Description
- Fixes typo that could prevent restoring a page with artists or album
artists
- Clarified some javadocs
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
I was having trouble playing HDHomeRun channels through Jellyfin in
Wholphin. They started quickly in the official Jellyfin app, but
Wholphin would sometimes spin or fall back when switching channels.
This change includes the live stream ID returned by Jellyfin when
building the Direct Play URL. It also adds a regression test.
### Related issues
Related to #2014 and possibly #1903. My testing was with an HDHomeRun; I
haven’t verified the TVHeadend setups in those reports.
### Testing
Tested on a Google TV Streamer with multiple local channels, FAST
channels, and movie playback. The tested content used Direct Play, and
the HDHomeRun tuners were released after stopping.
All 18 playback tests and the formatting check passed after the
requested test cleanup.
## Screenshots
No UI changes.
## AI or LLM usage
I used OpenAI Codex to help investigate the issue, implement the change
in PlaybackViewModel.kt, add the regression test in
PlaybackViewModelTests.kt, and run validation. Codex also helped draft
this description.
## Description
- From media3 1.11, controllers that aren't trusted get read-only player
commands by default.
- Google Home sends its commands through
com.google.android.apps.mediashell (Google's Cast service on the TV),
which isn't trusted, so play/pause from it was dropped.
- Logcat showed: MediaSessionLegacyStub: Calling play() omitted due to
COMMAND_PLAY_PAUSE not being available
- The change: a MediaSession.Callback (TrustAllControllersCallback in
PlayerFactory.kt) that gives every controller all player commands again.
It uses onConnectAsync and AcceptedResultBuilder(session, controller),
so untrusted controllers keep the restricted session commands.
- It's applied to the video session (createMediaSession) and the music
session (MusicService).
- It probably started with the media3 1.11 bump in #1814.
### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->
- Fixes#2087
### Testing
- JMGO N1S 4K, Google TV, Android 14, ExoPlayer.
- Before the fix: play/pause from Google Home did nothing, and logcat
showed the "omitted" warning.
- After the fix: pause and play from Google Home work. Logcat shows each
command reaching handleSetPlayWhenReady, and the warning is gone.
- The TV remote still works.
- I didn't test music playback; leave that out or test it first.
## Screenshots
- None; no UI changes.
## AI or LLM usage
- Claude Code read the logcat output, found the media3 1.11
untrusted-controller change, and wrote TrustAllControllersCallback plus
the two setCallback(...) calls.
## Description
The Music / Artists view currently appears to use the artist metadata
from individual tracks. While at a track level this makes perfect sense,
it can result in a very noisy and fragmented artists view.
For example, an album by Eminem might contain tracks with artists such
as:
- Eminem
- Eminem feat. 50 Cent
- Eminem ft. Rihanna
- Eminem & Dr. Dre
Each of these seem to appear as a separate artist entry (even though
they are all tracks from the same album by Eminem)
This becomes particularly noticeable in large music libraries, where
featured artists, collaborations, remixes, and slightly different artist
metadata ends up resulting in a large number of sparsely populated or
duplicate / strange looking artist entries.
This PR adds an option / toggle to determine which metadata field is
used to populate the artists view. This option sits neatly within the
current artist view settings e.g. Music > Artists > View options >
Artist source
- Artist (current behavior)
- Album Artist (use the album artist metadata instead)
That Artist setting stays as the default choice (so nobody sees a change
unless they pick it) but now gives the option to filter down only to
Album Artists if desired - this choice is saved per library and applies
immediately.
### Related issues
Closes#1868
### Testing
I've tested it extensively on an Amazon Fire TV Cube Gen 2 (using the
firetv debug variant) the following manual tests were performed:
- The list changes when you pick Album artist and importantly the
selection survives a full app restart
- It was tested on a large library and can confirm only the Album
Artists remained (e.g. only one Eminem entry remained)
Automated checks were run (testFiretvDebugUnitTest & pre-commit) As well
as two new tests being added to the CollectionFolderViewModelTest.kt:
- For letter counting its checking via the album artist endpoint (and
not the standard one) when the source is set as ALBUM_ARTIST
- When the source is changed via the saveViewOptions its triggering a
load from the album artist endpoint
## Screenshots
Please see the screenshots below showing the UI impact / modification:
<img width="4624" height="3472" alt="1"
src="https://github.com/user-attachments/assets/6b53575e-ad15-4f66-b9c0-074657128a1d"
/>
<img width="4624" height="3472" alt="2"
src="https://github.com/user-attachments/assets/528c5766-44d8-43d3-8519-778f5aab7441"
/>
<img width="4624" height="3472" alt="3"
src="https://github.com/user-attachments/assets/5de2fb6d-173d-4ac3-ad1b-e08f27f4dafd"
/>
## AI or LLM usage
The code was written / supported with Claude - the approach (e.g. design
/ storing the choice in ViewOptions) endpoint research etc was done by
myself - I reviewed the approaches / refactoring and than deployed a
debug.apk to my local device to confirm everything was working as
expected.
I've reviewed each line and can explain it / the thinking to approaches
/ logic etc and am comfortable submitting the PR.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
The video player passes false to setAudioAttributes (since #1258 I
think), as ExoPlayer never requests audio focus. This will pass `true`.
### Related issues
#447, #1258.
### Testing
Not tested on a device.
### Screenshots
Not applicable
### AI or LLM usage
None.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
As usual, a PR to fix various smaller bugs in the latest release.
- Fixes not showing the Videos tab on favorites page
- Fixes the Random button not working for photos
- Long nav drawer item names use a marquee scroll if needed
- Fixes a crash when clicking "View More" on a studios rows on the home
page
### Related issues
Fixes#2070Fixes#2069Fixes#2135
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds an option to the change the max bitrate during playback. It
defaults to the global setting and can be changed in the settings (gear
icon) on the playback controls.
It is applied for the entire playback session. So if you start an
episode and change it, will be used for each next up episode. If you
back out of playback and start again, it will reset back to the global
default.
### Related issues
Closes#1382
### Testing
Testing on the emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
**On Amlogic boxes (S905X4 and similar) Dolby Vision Profile 7 does not
play as Dolby Vision.**
The server transcodes it, or, with "Direct play Dolby Vision Profile 7"
switched on by hand, the
decoder answers `NoSupport` and fallback ends in HDR10. Devices with a
native Profile 7 decoder,
such as the Shield, are not affected and nothing changes for them.
This PR converts Profile 7 to 8.1 on the fly. It has two halves and one
decision.
The preference `convertDolbyVisionProfile7` is proto field 6 in the
experimental settings, off by
default. In `WholphinRenderersFactory` the filter comes first in the
list, followed by the HDR10+
masking from #1876.
**What this PR does not do.** The enhancement layer is dropped; that is
the price of the
conversion, which is why devices with a native Profile 7 decoder are
left alone.
`BlockAdditional` (an enhancement layer outside the stream) is not read
— like the rest of
Wholphin today — so such a file stays HDR10, as it is now.
### Related issues
Closes#244
Builds on #1876 (the `BitstreamFilter` list). Related to #1540. @fanni42
asked for exactly this on
an S905X4 in #1876.
### Testing
**Hardware** — a debug build of this branch, Homatics box (Amlogic
S905X4, Android 14) + LG BX,
Jellyfin 12.1.0, a Profile 7 FEL remux with the enhancement layer in
band, played from the start
for 39 seconds. Only the new preference was switched on; manual "Direct
play Dolby Vision
Profile 7" was **off**:
* `DolbyVisionProfile7FilterTest` (14) — a fake rewriter on a direct
buffer: offset ≠ 0,
position/limit untouched, every "bytes stay as they were" branch,
three-byte start codes,
`UnsatisfiedLinkError` tried once;
* `DolbyVisionProfile7ExtractorsFactoryTest` (5, mockk) — only the video
track is rewritten;
* `DolbyVisionProfile7ConversionTest` (6) — codec strings, DRM, `dvh1`.
CI in my fork ran on this branch plus a debug signing commit:
`pre-commit`,
`testDefaultDebugUnitTest` and `assembleDefaultDebug` are all green. The
hardware measurement
above is that build.
## Screenshots
The only UI change is one toggle in the experimental settings.
## AI or LLM usage
The code was written with Claude (Claude Code): the filter, the
extractor wrapper, the condition
and the tests. I reviewed every line and can explain it. It was tested
with the unit tests and
the hardware measurement above. This description was written by me in
Russian and translated
into English with the same assistant.
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Adds information to the debug page about detected HDR format for both
the device and the display.
I also wanted to try out the new `Grid` layout composable! It's much
better than the `SimpleTable`/`TableRow` composables I wrote previously.
### Related issues
There's a few issues/PR for improvements to HDR/DV support, so this will
help with debugging
### Testing
Emulator & nvidia shield on HDR TV
## Screenshots
N/A, just tables of info
## AI or LLM usage
None
## Description
https://github.com/damontecres/jellyfin-plugin-wholphin/pull/3 added a
new endpoint for Seerr configuration in Wholphin server plugin.
This PR adds support for reading that configuration and either
pre-filling the URL or automatically logging in.
Some code was copied from #1418 (thanks @matschi95!), but that PR worked
by inserting a DB record for the plugin provided server. Since the local
DB was preferred, this meant plugin config changes wouldn't be reflected
in the client. This PR instead attempts the connection and only updates
the in-memory connection state, so user switching and app restarts will
re-fetch from the plugin.
If the server plugin is not installed or no Seerr server is configured,
nothing in Wholphin changes.
### Related issues
#598
This PR replaces #1418
### Testing
Emulator with different scenarios
## Screenshots
N/A
## AI or LLM usage
None
## Description
Show album artist under album name on album cards
### Related issues
Fixes#2055
### Testing
Android TV
## Screenshots
<img width="1920" height="1080" alt="pr1-albums-grid-artist-year"
src="https://github.com/user-attachments/assets/d00a8f78-8bf9-41da-8220-5cc7f6862cf5"
/>
### AI or LLM usage
Claude did the code, I reviewed it, then deployed a -debug.apk to my
device to confirm feature live.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## Description
When resetting the watch state or choosing to start from the beginning,
the "from_start" flag isn't passed to VLC. Because of that, VLC doesn't
restart playback from the beginning as expected.
This change adds the flag when needed so VLC starts from the correct
position.
## AI or LLM usage
я использовал Codex
Co-authored-by: osipovgleb <19697793+osipovgleb@users.noreply.github.com>
## Description
Just a small change to AndroidManifest.xml so that Alexa voice commands
work in Wholphin.
"To enable voice command and control to work with your application, add
the permission com.amazon.permission.media.session.voicecommandcontrol
to your application manifest"
See
https://developer.amazon.com/docs/fire-tv/mediasession-api-integration.html
This is required only for Fire OS <14.
### Related issues
I couldn't find any related issues.
Steps to reproduce: Start some media -> Tell Alexa to pause/play
("Alexa, pause", "Alexa, play")
Expected behavior: Media pauses/resumes almost immediately.
Actual behavior: Pausing takes ~10 seconds, resuming doesn't work at all
Environment:
- FireTV Cube Gen 3 running Fire OS 7.7.1.7 (PS7717/5741)
- Wholphin 1.0.9-0-g2d87d526
- Jellyfin server 12.2
### Testing
See environment above.
After the change pause/play works immediately.
## Screenshots
N/A
## AI or LLM usage
I used Claude to analyze the issue and Claude Code to build a debug app
for testing.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
This PR updates the playlist details page to more closely match the look
of a library grid tab. This allows for a few things:
- Customize layout (grid, list, backdrop, etc)
- Show logos
- Random button
- The play all/filter/etc buttons no longer awkwardly show all the time
Basically, a playlist details will default to the list layout with "Show
details" enabled
The library list layout was actually based on the playlist details
layout originally, so this unifies the code. This means a bit less
maintenance and improvements to one page benefits the other.
### Dev note
This PR adds a `CollectionFolderViewScope` to pass down information
about the current collection and layout. This allows for easier
customizing of components. The PR also lifts the card & list item
composable to be parameters of `CollectionFolderViewContent` which,
again, allows for lots of customization.
### Related issues
Related to
https://github.com/damontecres/Wholphin/discussions/2078#discussioncomment-19046988
### Testing
Emulator
## Screenshots
### Before
<img width="1280" height="720" alt="playlist_ui_before Large"
src="https://github.com/user-attachments/assets/1cf8db82-4f53-4f20-a05e-ab937ffa3e3f"
/>
### After
<img width="1280" height="720" alt="playlist_ui_after Large"
src="https://github.com/user-attachments/assets/96ee6c1a-1a20-44dc-b979-0d314539a04a"
/>
## AI or LLM usage
None
## Description
Now that Jellyfin 12 has been out for over six weeks, it's time to start
considering dropping support for Jellyfin 10 & 11 as part of #1640.
This PR adds a warning message on the switch server, switch user, &
preferences pages if the current server version is <12.
### Related issues
See https://github.com/damontecres/Wholphin/discussions/1640 for further
discussion!
### Testing
Emulator w/ faked data
## Screenshots
N/A, reuses the same error message space
## AI or LLM usage
None
## Description
Rename music "Top Rated Unwatched" row to "Top Rated"
### Related issues
Typo.
### Testing
Android TV
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
<img width="1920" height="1080" alt="pr2-music-recommended-top-rated"
src="https://github.com/user-attachments/assets/3978bc9b-a020-4ee7-ae35-feb19e19ebae"
/>
## AI or LLM usage
Claude wrote the code, I reviewed, deploed a -debug.apk to my device and
tested live.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
## Description
PR to add support for a Wholphin server plugin:
https://github.com/damontecres/jellyfin-plugin-wholphin
Note: The plugin is not required to use Wholphin. It adds features the
server admin can use to set default settings like the home page or
eventually Seerr connection info.
### Changes
Wholphin will use the home page settings in order of:
1. Saved on the device locally
2. Saved on the server in the user's profile settings (display
preferences)
3. Provided by `jellyfin-plugin-wholphin`
4. Default fallback: combined continue watching & recently added in
libraries
Under customize home page, you can now see the "source" of the home
settings. This also allows to pick a different source
Finally, instead of the home customization being saved in the background
only locally, if there are any changes, a dialog is shown with options
to save locally, server-side user profile, or discard.
So overall, it's more clear where the home page settings are from and
where the user wants to save them.
### Install
[See instructions
here](https://github.com/damontecres/Wholphin/releases/tag/develop-server-plugin)
### Related issues
Related to #598
### Testing
WIP
## Screenshots
N/A
## AI or LLM usage
None
---------
Co-authored-by: Mario Reisinger <marioreisinger95@gmail.com>
## Description
No longer assume that if a user has "Request 4K" then they must also
have regular "Request". Now the requesting dialogs will handle:
- User has only "Request" (including just "Request Movie" vs "Request
TV")
- User has only "Request 4K"
- User has both
Also fixes an issue where the request button would be disabled if a
previous request was declined.
And also because it helped me with testing this and is useful, the
dialog in settings to remove a Seerr server now also shows the username
in addition to URL.
### Related issues
https://github.com/damontecres/Wholphin/issues/702#issuecomment-6132914691
### Testing
Manually on the emulator with the test cases above. I also tested with
and without "Advanced Requests" permission
## Screenshots
N/A, the dialog change is minor
## AI or LLM usage
None
## Description
The refactor in https://github.com/damontecres/Wholphin/pull/1955
(included in `v1.0.9`) swapped the branches for the experimental "Use
AC3 for surround sound audio" breaking it.
Unfortunately, there isn't a real workaround to get the AC3 behavior
back. With the experimental settings enabled & "Use AC3 for surround
sound audio" _disabled_, surround sound audio will transcode to stereo
AAC. Otherwise if experimental settings are _disabled_ and/or "Use AC3
for surround sound audio" is _enabled_, there's no effect.
### Related issues
Credit to @StefanBS who opened #2090, but closed it before I verified
the bug
Closes#2093
### Testing
Manually via emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Wholphin builds item image URLs without Jellyfin's `tag` query
parameter. `ImageUrlService` already receives the item's `imageTags` and
`backdropTags`, and the low-level `getItemImageUrl` supports `tag`, but
no branch passed it. Today only chapter and now-playing images set it.
Before: `/Items/{id}/Images/Primary?quality=96&fillHeight=229`
After:
`/Items/{id}/Images/Primary?quality=96&fillHeight=229&tag={ImageTags.Primary}`
This is how the official Jellyfin clients request images. jellyfin-web
and the official Jellyfin Android TV app both copy `ImageTags[...]` (and
the parent/series tags) into the image URL.
I also added `ImageUrlServiceTest`, which covers each branch.
### Related issues
I tested each one on the emulator against a stock Jellyfin 12.1 server
(no plugins), comparing `main` with this branch:
- **#366 (Image cache expiration): fixed by this PR.**
- **#1249 (blank tile for a newly added movie) and #1338 (episode cover
never refreshes): already fixed on `main` since v1.0.3, not by this
PR.**
- **#360 (Option to hide spoilers): partly.**
## AI or LLM usage
This change was made with AI assistance (Claude Code). It was used for:
- the code changes in `ImageUrlService` and the call sites listed above
- the new `ImageUrlServiceTest`
- the emulator testing and benchmarks
- drafting this description
The testing is what's described above: unit tests, an Android TV
emulator against Jellyfin 12.1 servers (with Jellyfin Enhanced and
stock), request logs from a proxy, and a check of every tag against the
server's metadata. I've reviewed the change and can explain it.
I'm also a maintainer on Jellyfin Enhanced, which is how I ran into
this. I'm happy to share examples of my own coding work if that helps
with reviewing this.
## Description
This PR overhauls how audio and subtitle track persistence works for
series! Previously, when you chose an audio or subtitle track for an
episode, only the language was saved and used for subsequent episodes.
This meant aspects about why that choice were lost such as a forced or
hearing impaired track.
This PR changes the persistence to include more details about the chosen
track. Additionally, the persistence is saved at both the season and
series levels.
## How it works
Selecting the track uses the first success option:
1. Episode specific - User has explicitly chosen an audio/subtitle track
for this episode
2. Season specific - User has chosen the track for another episode in
this season
3. Series specific - User has chosen the track for another episode in
this series
4. User preferences - User has _not_ chosen a track for the episode,
season, nor series, so determine track based on the preferred audio
language, preferred subtitle language, and subtitle mode
Additionally, the season or series specific choices use a scoring
algorithm to find the best suited track. For example, if the chosen
tracks for an episode in the season had the forced flag, this will be
preferred for subsequent episodes. Same for SDH/hearing impaired or
title, etc. This means that the common case, where a season has
substantially identical tracks, will use the same ones across all
episodes (after choosing for one, otherwise falling back to the series
or user preferences).
Finally, this also means that users that prefer SDH/hearing impaired
subtitles can be prioritized now, though there is no global preference
for this yet.
The previous track language choices are migrated to the new algorithm.
They lack the additional context, but this makes for a seamless
transition.
## UI changes
The context menu for choosing audio or subtitle tracks now displays why
that track was chosen.
### Related issues
Fixes#1795
Maybe fixes#1783
Related to #1136 & #1798Closes#1470
### Testing
Previous unit tests for track selection all pass, plus there are a few
additional unit tests. I've done some testing on the emulator and it
seems to work well. But I think real world testing will be needed.
## AI or LLM usage
None
## Description
This change applies only to subfolders that do not have their own saved
display settings.
When a subfolder has no saved settings, it now inherits the view type
and sort order from the closest ancestor with saved settings. Filter
settings are not inherited.
If the user changes the settings within a subfolder, those settings are
saved for that subfolder and take precedence over the inherited
settings.
### Related issues
Fixes#1768
### Testing
Tested an App Store flavor debug build on the following setup:
- Device: Xiaomi TV A Pro 43
- Jellyfin server version: 12.1
- Library: A hierarchical recording library stored on a NAS
I changed the view type and sort order at the top level of the library
and confirmed that both settings were inherited by untouched subfolders.
## Screenshots
N/A - no UI changes
## AI or LLM usage
Claude was used to write the changes in `CollectionFolderView.kt`.
I performed the testing on a physical Google TV device and verified the
behavior described above.
The commit also includes a `Co-Authored-By: Claude` trailer.
This description was written in Japanese and translated with an LLM.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Description
Adding per-format HDR override preferences (HDR10, HDR10+, Dolby Vision
Profile 5/7/8) to control device compatibility information sent to the
Jellyfin server, so users can force specific formats on or off
independently instead of disabling Dolby Vision as a whole.
Related issues
This is a common problem across the Jellyfin ecosystem, but there isn't
a specific issue for Wholphin. My experience is the issue is FireTV
(model and generation) + TV dependent + media dependent. I tested videos
from
https://kodi.wiki/view/Samples with the same DOVIWithHDR10 flag that
worked while other movies I have with It is a very frustrating user
experience and the current workaround of setting the bitrate really low
to force a transcode is sub-optimal IMO as it lowers the video quality.
This allows the user to maintain full quality while disabling only the
specific format(s) causing problems.
https://github.com/jellyfin/jellyfin-androidtv/issues/4021https://github.com/jellyfin/jellyfin-androidtv/issues/4892https://github.com/nova-video-player/aos-AVP/issues/1503https://www.reddit.com/r/jellyfin/comments/12jqoxh/dolby_vision_video_not_working_on_fire_stick_4k/
Testing
<!-- Describe how this change was tested and on what device(s) -->
FireTV 4K (gen 1) + Vizio
FireTV 4K Max (gen 1) + LG
FireTV 4K Max (gen 2) + Vizio
FireTV (gen 1) + Olevia
AI or LLM usage
Claude code assisted and wrote much of the code in this PR. I've
reviewed it all and run the pre-commit linting and formatting.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
This PR reworks some of the internal handling of changing users and auto
sign in.
1. Most recently used server/user is migrated from `AppPreferences` to
`MostRecentServerProvider`
2. Checking if a user profile is protected occurs at a lower level in
`ServerRepository` so fewer services need to check for this
3. Now always defaults to the most recent server's user list in cases
where the most recent user can't be switched to such as if the profile
is protected, auto signin is disabled, or and error occurred
The default implementation for `MostRecentServerProvider` uses Android's
`SharedPreferences`. `SharedPreferences` was already was being used to
track the most recent server for crash reporting (the crash reporting
process cannot use the Hilt DI). This PR enhances this and adds a better
API for saving/retrieving.
This also removes the last bit of "user specific" data from
`AppPreferences`.
### Related issues
None specific, but there have been a few related to auto sign-in &
intents that were recently fixed. This PR is a more detailed approach to
those same problems.
### Testing
Manually via the emulator. There are also unit tests for most (all?) of
the different settings and state combinations.
## Screenshots
N/A
## AI or LLM usage
None
## Description
When switching users, the current user's home page would start a reload
during the switch. This PR fixes that by comparing the user switch.
The underlying cause is that Room's flow is blunt and emits whenever the
table updates. So [during the user
switch](9b02804075/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt (L117-L118))
the table is updated causing [current user to
emit](9b02804075/app/src/main/java/com/github/damontecres/wholphin/data/ServerRepository.kt (L71))
and the current user flow is updated causing the new user to emit.
### Related issues
N/A
### Testing
Emulator, switching users and switching locales
Can observe that `UserSwitchListener` only trigger once per user switch
instead of twice
## Screenshots
N/A
## AI or LLM usage
None
## Description
On the series overview page, if you play the second or third episode,
then return to the page, focus will move the first episode.
This did not happen in `v1.0.7` and the code for that page was minimally
changed, so I think this was caused by updating the compose version,
maybe the ordering on focus resolution or something.
This small change seems to reliably fix the issue though.
### Related issues
Fixes#2043
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
If a collection is using separated types and there were 2+ empty rows
between types, then the forward composition wouldn't trigger and the
next row would not appear. In practice this means if you have a
Collection that contained Movies and Videos, you might not be able to
scroll down from Movies to Videos.
The fix is to filter out the empty rows before composing.
### Related issues
Fixes#2046
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Collecting minor bug fixes in one PR:
- If error occurs fetching a series, show the error on the UI instead of
loading forever
- Use song's artists on Now Playing page
- Fixes settings wrap around scrolling when an update is available
- Fix backdrop not clearing for items without backdrop
- Fix not picking an ABI-specific APK when updating a regular release
(maybe related to #1966)
- Add "Go To" for album context menus
- Fix handling of next/previous media remote button presses during video
playback
### Related issues
Fixes#1990Fixes#1965
### Testing
Emulator mostly
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes a potential crash on any page that uses `HomePageContent`, such as
home page, recommended tab, & customize home page.
I think this is actually a bug in compose because it shouldn't pass
`Color.Unspecified` into the native color code. But similarly, using
`Color.Unspecified` in `animateColorAsState` doesn't make sense either.
### Related issues
Fixes#1995
### Testing
I was able to reproduce this consistently using the Purple theme on
customize home page by focusing on an empty row. This change doesn't
cause the crash.
## Screenshots
N/A
## AI or LLM usage
None
## Description
Wholphin already ships a baseline profile. AGP merges the profiles from the AndroidX libraries into `assets/dexopt/baseline.prof` on every release build, so a release APK built from `main` already has one at around 15 KB. It just never gets installed on the device.
The reason is in `app/src/main/AndroidManifest.xml`, which removes `androidx.startup.InitializationProvider` completely:
That was added in #372 so WorkManager could be set up on demand through `Configuration.Provider` and `HiltWorkerFactory`. The problem is that `androidx.profileinstaller` registers `ProfileInstallerInitializer` inside the same provider, so removing the provider removes that as well.
### Related issues
#553
### Testing
API 34 AOSP emulator, `defaultRelease` signed with a debug key, clean install each time. With this change, logcat shows the profile being installed.
Not tested on a physical Fire TV device.
## Screenshots
None, there are no UI changes.
## AI or LLM usage
I used Claude for this one.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Adds a new setting to pick a maximum resolution (by height). Any
playback that has a larger resolution will be transcoded down to the
maximum resolution.
Playback equal or less than the maximum resolution will not be affected
and will direct play if otherwise able. This setting is also independent
of the maximum bitrate (ie if max resolution is 4K and max bitrate is
2mbps, playing 4K will transcode).
This setting is pretty much equivalent to the Web UI setting "Maximum
Allowed Video Transcoding Resolution" with "Limit maximum supported
video resolution" enabled.
Note: this setting does not affect which version is chosen by default
### Related issues
Closes#756Closes#1594
### Testing
Manual emulator testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
Updates the home page header & card text when a Season type is shown,
such as in recently added for a TV Show library.
The title or logo for the series is shown with the season name as the
title. The "quick details" has the year of the season. The card shows
the unplayed count of episodes.
### Related issues
Closes#1958
### Testing
Emulator over a few days to see different seasons
## Screenshots
<img width="313" height="153" alt="image"
src="https://github.com/user-attachments/assets/c41e9edd-9f2a-4f62-b76d-e452e83dd8bf"
/>
## AI or LLM usage
None
## Description
Add better error handling for server web socket messages that are
(technically) invalid.
For example, I talked to Niels and some third party plugins don't always
have the required fields. Since the server isn't as strict as the client
SDK, it's fine on the server but will generate an invalid message sent
to the client.
Also listens for server-side user configuration changes via web socket.
### Related issues
Fixes#1979
### Testing
Valid server messages are handled as normal and I did some manual
testing to push invalid JSON into the webSocketApi, but the code is
gnarly and not viable as an actual unit test
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a "Prefer Dolby Vision over HDR10+" switch (off by default) under
Experimental settings. That screen only shows up once "Experimental
settings" is enabled under Advanced, and until then the switch is read
as off: it goes through `experimentalPreferences.get {}`, so nothing is
wrapped and the playback path is untouched.
Most current 4K web releases carry both Dolby Vision (profile 8.1) RPUs
and HDR10+ dynamic metadata in the same HEVC stream. Some MediaTek based
devices (Philips and TCL TVs, some Fire TV models) latch onto the HDR10+
metadata on those files and never engage Dolby Vision, even though a
Dolby Vision decoder is selected.
I isolated this on my Philips 55OLED810 by playing the same episode
three ways, direct play in all three cases:
| | Stream | TV shows |
|---|---|---|
| A | MKV with DV 8.1 + HDR10+ | HDR10+ |
| B | Same MKV with the HDR10+ metadata removed using
[hdr10plus_tool](https://github.com/quietvoid/hdr10plus_tool), DV
untouched | Dolby Vision |
| C | Same stream remuxed to MP4, both kinds of metadata present |
HDR10+ |
The container doesn't matter and the Dolby Vision signaling is picked up
correctly. The only thing the app can do is keep the HDR10+ metadata
away from the decoder.
When the switch is on, and only when a Dolby Vision decoder was actually
selected, the HDR10+ SEI messages (SMPTE ST 2094-40, recognized by their
ITU-T T.35 header `B5 003C 0001`) are masked in each access unit before
it is queued to MediaCodec. The SEI payload type byte is overwritten
with an unused value (254), so the decoder skips the message. That is
one byte rewritten in place per message: the masking filter returns the
size it was given, so the access unit is queued exactly as long as it
came out of the extractor, which is what makes it safe with both the
synchronous and asynchronous MediaCodec adapters. media3 does the same
thing for HAGC metadata in `HevcSeiUtil`; this filter only targets ST
2094-40, so other T.35 SEI such as CEA-708 captions are not affected.
The filter was written from the H.265 spec, no Kodi or media3 code was
copied.
The hook is generic rather than HDR10+ specific, as asked in the thread.
`BitstreamFilteringCodecAdapterFactory` wraps the codec adapter with a
`List<BitstreamFilter>`, and a filter is `(ByteBuffer, offset, size) ->
Int` where the returned `Int` is the new size of the access unit: the
adapter queues the size the chain reports, and hands each filter the
size the previous one reported. A filter which only overwrites bytes
returns the size unchanged, which is what the HDR10+ masking does; a
filter which drops NAL units compacts what it keeps and reports the
shorter size. A reported size which is negative or larger than the one
the filter was given is ignored rather than queued. That is what lets
the Dolby Vision profile 7 to 8.1 conversion of #244 arrive later as one
more filter instead of a second copy of the adapter plumbing.
When the switch is off, the playback path is unchanged. On a device
without a Dolby Vision decoder (or a display without DV support) the
wrapper is never installed, so HDR10+ only devices keep their metadata.
Encrypted samples, codec config buffers and empty buffers are passed
through untouched.
Kodi has an equivalent option for the same reason: xbmc/xbmc@032cbfeb
and xbmc/xbmc@e2c54e7f.
### Related issues
Implements what was requested in #1855. The filter list is what #244
(Dolby Vision profile 7 direct play) needs, so that conversion can be
added as another filter. #1895 implemented the same masking
independently and was closed in favour of this PR; the device profile
part of it was not carried over, see the note under Testing. Upstream:
androidx/media#1895, where the maintainers say the bug is in the device
codecs and that they don't plan a workaround in ExoPlayer itself.
## AI or LLM usage
The implementation (bitstream filter, MediaCodecAdapter wiring, unit
tests) were written with AI assistance (Claude). I reviewed the changes
and validated the behavior myself on a real MediaTek TV (Philips
55OLED810), with the setting on and off, on hybrid and DV-only content.
---------
Co-authored-by: Jycreyn <github.2ilp7@passmail.com>
## Description
Fixes the home page reloading from scratch when the app is resumed
This happened because the `userDto` is updated on app resume and since
it has a last accessed timestamp, it would always different and trigger
a reload.
### Related issues
Bug introduced by #1981
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
When clicking on an episode, the app assumes if there is a season ID for
the episode, then there must be a series ID. However, on my JF 12.0
server, an episode (with no metadata) appeared on my Continue Watching
and it has a season ID (which in turn has a series ID) but no series ID.
I'm not sure if this really a valid situation or if it's a server bug,
but Wholphin shouldn't crash at least. So this PR checks both `seasonId`
and `seriesId` and if either is null, falls back to the generic "Episode
Page". Similarly, if a season has no `seriesId` (again, not sure if a
valid situation), a crash won't occur, however there is no generic
"Season Page", so an error page is shown.
### Related issues
TODO, check JF server issues, maybe fixed in 12.1?
### Testing
Emulator
## Screenshots
N/a
## AI or LLM usage
None
## Description
There is a race condition when switching users between
`HomeViewModel.init` and `HomeSetttingsService.loadCurrentSettings`.
This happens more often if the new user doesn't have a customized home
page. The result is that the previous user's home page settings are used
for the initial load of the home page.
This PR fixes this by collecting the flows for current user & home
settings, waiting until both are valid and refer to the same user, then
fetch the data.
### Related issues
Fixes#1976
Maybe related (and fixes) #1980, but need to investigate more
### Testing
Emulator. It's easy to reproduce by adding a manual `delay(2.seconds)`
at the start of `HomeSetttingsService.loadCurrentSettings`.
## Screenshots
N/A
## AI or LLM usage
None
## Description
Makes the `itemId` parameter optional allowing just passing
`serverId`/`userId` to switch the user and load the app at the home page
for that user.
Dev note: I added a new assert function `assertIs` which asserts that an
object `is` of a type. It also has a contract so the compiler will
smart-cast usages after the `assertIs`. Checking a result type is a very
common pattern in the unit tests, so this assert will simplify a lot of
current & future tests.
### Related issues
Closes https://github.com/damontecres/Wholphin/discussions/1824
### Testing
Emulator and added unit tests for `parseIntent`
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fix two bugs:
- Favorites page didn't query for favorite people, introduced by #1864
- Fixes restoring focus on the search page after clicking a discover
item and going back
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes some devices activating the OS screensaver during playback when
the in-app dim was enabled.
I was not able to reproduce this on Shield 2017/2019, but could easily
on a Fire TV Stick. #1834 added the dim feature and, when enabled, would
not pass the keep screen on flag to the OS during playback. It seems
maybe regular Android TV's screensaver also listened to active media
session to prevent starting but maybe Fire TV OS doesn't. I'm not 100%
sure. This PR changes the logic slightly so that the keep screen on flag
is always set during playback.
### Related issues
Fixes#1894
Related to dim feature from
https://github.com/damontecres/Wholphin/pull/1834
### Testing
Emulator, shield 2019, & fire tv stick 4k
## Screenshots
N/A
## AI or LLM usage
None
## Description
Increases the memory available for CI builds and splits up builds a bit
more
There are no user facing changes
### Related issues
Recently dev builds failed due to OOM
### Testing
https://github.com/damontecres/Wholphin/actions/runs/34710759629
## Screenshots
N/A
## AI or LLM usage
None
## Description
Now that the SDK level is updated, many dependencies can be updated as
well.
This includes a fix from `wholphin-extensions` for a TLS timeout issue
on some devices using MPV
(https://github.com/damontecres/wholphin-extensions/pull/13).
### Related issues
Fixes#1932
### Testing
Just basics on emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Collection of minor bug fixes for `v1.0.7`
- Fixes a possible race condition crash when navigating to the search
page
- Uses friendlier error message when login fails due to parental
restrictions (based on
[`jellyfin-web`](https://github.com/jellyfin/jellyfin-web/blob/v10.11.11/src/controllers/session/login/index.js#L38-L41))
- Don't re-sort search results by name, just by relevance scoring
- Consume the key event when dim is active (ie if the screen is dimmed,
pressing a key will un-dim and not perform another action)
### Related issues
Fixes#1922Fixes#1951
Related to #1848 (search sorting)
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
<!-- By submitting this pull request, you acknowledge that you have read
the [contributing
guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md,
including the AI/LLM policy, and [developer's
guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md)
-->
## Description
Person pages that get a filmography from Seerr had three problems, all
in the same place.
The release date was taken from `firstAirDate` alone — the field TMDB
fills for series — so
movie cards showed no year at all. The library person page capped the
row at 25 cast + 25 crew while
`DiscoverPersonPage` had no cap, because each screen carried its own
copy of the same logic.
And `cast + crew` left the row in no order a viewer can read, with a
title appearing twice when
a person is both cast and crew on it.
This PR:
* maps credit dates as `releaseDate ?: firstAirDate` — the expression
the search result mapping
already uses;
* moves the credits logic into a single `SeerrService.personCredits()`
used by both screens, so
they cannot drift apart again;
* drops the 25 + 25 cap, de-duplicates by TMDB id and sorts by release
date descending, with
undated entries last.
Rows on movie and series pages are untouched: only the
`BaseItemKind.PERSON` branch of
`similar()` changed.
Two things worth your opinion:
* Without the cap, the row for a person with a long filmography is long
— 135 cards for Cameron
Diaz, for example. If you would rather keep a cap, the row could keep
the newest 25 and gain a
`ViewMoreCard` opening the full grid — `Destination.DiscoveredItem` with
a `PERSON` item
already routes to `DiscoverPersonPage`. I am happy to add that in a
second commit.
* The sort is fixed to release date descending. It now lives in one
function, so a different key
or a user-facing choice would be cheap to add later.
### Related issues
Fixes#1873
### Testing
CI in my fork ran the repository's own PR workflow on this branch:
`pre-commit` (ktlint) and
`assembleDefaultDebug` with `testDefaultDebugUnitTest` are both green.
I then installed a debug build of the same code on an NVIDIA Shield TV
Pro 2019 (Android 11),
against Jellyfin 10.11.11 with Jellyseerr, and opened Cameron Diaz from
the cast row of
*Charlie's Angels* before and after the change:
* movie cards show a year now; before, the only two cards in the row
with a year were the two
TV entries;
* the row is ordered by release date descending, movies and series
interleaved, instead of the
API order of all cast followed by all crew;
* the row is no longer cut at 25 + 25 — it now runs back to her 1952
credit, and the entries
TMDB has no date for sit at the very end of the row.
Not tested on any other device; nothing in the change is device
specific.
## Screenshots
Person page of Cameron Diaz, reached from the cast row of *Charlie's
Angels*. The library rows
above the Discover row are the same in every shot and are useful as a
reference: they always had
their years.
Before, start of the Discover row — no year under any card:
<img width="1920" height="1080" alt="pr1873-1-before-start"
src="https://github.com/user-attachments/assets/585d0670-ae39-4214-9004-0c92262991da"
/>
Before, end of the Discover row — the row stops here, and the only two
years in it belong to the
two TV entries:
<img width="1920" height="1080" alt="pr1873-2-before-end"
src="https://github.com/user-attachments/assets/85c58575-2ad0-4369-a103-60f04c8d2350"
/>
After, start of the row — years on the movie cards, ordered by date
descending:
<img width="1920" height="1080" alt="pr1873-3-after-start"
src="https://github.com/user-attachments/assets/804745c5-e1a6-4fe4-93bd-dc6c97a7a876"
/>
After, end of the row — the list now reaches 1952, with the undated
entries last:
<img width="1920" height="1080" alt="pr1873-4-after-end"
src="https://github.com/user-attachments/assets/2c32389c-ec0b-4df5-b9d1-f564ef10d8b3"
/>
## AI or LLM usage
The code was written with Claude Opus 5 (Claude Code): the two mapping
lines, the extracted
`personCredits()` function and the two call sites. I reviewed every line
and can explain it.
This description was written by me in Russian and translated into
English with the same
assistant.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Adds support to remove items from a playlist and move items within a
playlist
The remove option is in the context menu. Moving has buttons on the
right side. The buttons are available if the user has edit permissions
and the playlist is sorted by "Default" (the playlist order). Remove
from playlist option is available if the user has edit permissions.
Also, thumb images are now preferred instead of primary/poster images
since the layout is much wider than tall.
### Related issues
Closes#1881
### Testing
Emulator with both a user with & without permissions to edit
## Screenshots
### With move buttons
<img width="1280" height="720" alt="playlist_buttons Large"
src="https://github.com/user-attachments/assets/ce7880c3-b823-4b69-9ad4-cfc9e7ee5fd5"
/>
### Without move buttons
<img width="1280" height="720" alt="playlist_no_buttons Large"
src="https://github.com/user-attachments/assets/f2027052-4628-4d44-8257-6c2a5e8c3af4"
/>
## AI or LLM usage
None
## Description
Reverts/fixes parts of #1404 for the playback UI. Generally the playback
UI always uses left-to-right for seek bar & media controls even on RTL.
Other buttons or texts will be RTL.
### Related issues
Fixes#1397
### Testing
Emulator w/ forced RTL English
## Screenshots
See
https://github.com/damontecres/Wholphin/issues/1397#issuecomment-5553711593
## AI or LLM usage
None
## Description
Fixes cases where the server has duplicate season IDs causing the season
index search to fail and default to the first season in the list.
### Related issues
Fixes#1837
### Testing
Emulator & unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
Consolidates sending reports/logs to the server into
`ServerReportService` (renamed from `MediaReportService`)
Also ensures a consistent header is used including the build flavor.
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Add scrolling from top to bottom and vice versa. This makes it quicker
to get to Advanced Settings for example.
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Added localized formatting for season/episode numbers across the UI
because it was in English-only in every UI language.
### Related issues
None
### Testing
Tested on a physical Google TV Streamer 4K device. Checked manually
relevant screens while browsing and during playback.
## Screenshots
No frontend changes are visible in English and new translations will be
contributed through Weblate for other languages later, as usual.
## AI or LLM usage
Generated initial code with AI, then manually passed over it while
cleaning and refactoring where needed.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Fixes several issues on the Now Playing page
- Keep controller focus after lyrics are focused
- Fix the queue not visually updating until recomposition
- The first focused queue shown/focused is now what's currently playing
### Related issues
Fixes#1821
### Testing
Emulator
## Screenshots
N/A, just behavior fixes
## AI or LLM usage
None
## Description
This PR fixes how cards are displayed initially when creating the
default home page or loading it from the web config:
- Ensure view options are selected based on the library type (this is
how it's done already when manually adding a row)
- Support more home row types such as photos and music
### Related issues
Fixes https://github.com/damontecres/Wholphin/issues/1838
### Testing
Emulator, resetting to default and loading from web config
## Screenshots
N/A, music cards will be square instead of poster sized
## AI or LLM usage
None
## Description
Adds a random button on library tab/grid pages which navigates to a
random item in the list. This goes to the item's detail page.
The random item is chosen with the filters applied. So you could filter
by unplayed Action movies with 7+ stars and the random button will
select from those movies.
Pressing back restores focus on the random button so it's easy to
quickly retry.
### Related issues
Closes#838
### Testing
Emulator
## Screenshots
<img width="935" height="105" alt="image"
src="https://github.com/user-attachments/assets/170b495c-b47d-41ce-93b5-99831f2064c7"
/>
<img width="936" height="114" alt="image"
src="https://github.com/user-attachments/assets/3efa6a11-ebe8-42d7-b023-354b1e6eda37"
/>
## AI or LLM usage
None
## Description
Updates the compile & target SDKs to 37 (Android 17).
The only required change is to support the new `ACCESS_LOCAL_NETWORK`
permission. This is required for Android 17 devices to auto discover
Jellyfin servers on the local network. Wholphin will prompt for
permission and if granted, use the discover servers feature. If
permission is not granted, the user must enter the server URL manually.
### Related issues
N/A, but several dependency renovate PRs are blocked by this
### Testing
I tested with a regular Android 17 emulator. There are no Android TV 17
images (or probably even devices) available yet.
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR override Coil's built-in network connectivity checker with one
that allows returns true. The built-in one seems to be unreliable on
devices running Fire OS 6 (maybe other versions too) and/or devices
using a VPN such as Tailscale.
See more details here:
https://github.com/damontecres/Wholphin/issues/1800#issuecomment-5432941761
### Related issues
Should fix#1156
Should fix#1751
Should fix#1800
### Testing
Unfortunately, I've never been able to reproduce the original issue on
any of my devices, but hopefully someone who is affected can try this
branch out!
This change doesn't break anything on some emulators (tried Android 7.0
& Android TV 14) or NVIDIA shield
## Screenshots
N/A
## AI or LLM usage
None
## Try it
See instructions here:
https://github.com/damontecres/Wholphin/releases/tag/develop-image-fix
## Description
This PR adds many more data types as tabs to the favorites page.
Additionally, only tabs for the types the user has favorites for will
appear. For example, if you have no favorited music videos, that tab
will not show.
Added types:
- Collections
- TV Channels
- Music albums
- Music artists
- Music videos
- Songs
- Home videos
- Photos
- Photo albums
Dev note: The tabs are implemented using keys instead of indexes which
could be useful for the series overview season tabs. Plus it maybe even
be adapted for the episode list to eliminate the terrible index search
logic. A major difference is that the possible favorites tabs are
known/static list where seasons/episodes are queried & paginated.
### Related issues
Closes#1041Closes#1844
### Testing
Manually on emulator, plus unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
This fixes the brief full screen white flash when the app starts or
reloads.
I bisected to find the cause which was #1653. The newly added `Surface`
uses `MaterialTheme` to set its colors. `MaterialTheme` defaults to a
generic _light_ color scheme. So this PR applies the Wholphin's default
purple theme, which will then be overridden once the app preferences are
loaded.
### Related issues
Mentioned in
https://github.com/damontecres/Wholphin/issues/1576#issuecomment-5376373039
### Testing
Used emulator with "Don't keep activities" and repeatedly opened &
closed the app to see if the white flash visually occurred
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR adds several more data types to search. It also adds a setting
(per use profile) to choose which types to include/exclude when
searching. The combined results now uses the same types instead of a
separate list.
A user can only search for types that they have access to. For example,
they need Live TV access for TV channels/programs or need access to a
movies-type library to search movies.
Added types to search:
- Photos & Photo albums
- Playlists
- People
- TV channels
- Live TV programs
Clicking a TV channel will play it. Clicking a TV program shows the
program dialog allowing for recording or watching live if it's currently
airing.
On the main search page you can also toggle whether to search Seerr or
not. This does not apply to the discover search tab.
Dev note: Some minor refactoring to move setting up/canceling live tv
recordings into a new service so it can be shared between with the live
tv & search pages. Also search related files are moved into the same
package now.
### Related issues
Closes#409Closes#646Closes#1668Closes#1750
### Testing
Emulator using users with different permissions and library access
## Screenshots
<img width="475" height="584" alt="image"
src="https://github.com/user-attachments/assets/ebcff697-07ba-4ddd-8c26-57f147cfa02a"
/>
## AI or LLM usage
None
Updates translations as of 2026-08-21
Thank you to all of the contributors! There is a large update to Danish
and a complete translation for Croatian, so special thanks to the
contributors for these languages!
## Description
When parsing intents, not every path for authentication was checked, so
launching via the "Next Up" OS channel wouldn't check for a profile PIN.
This PR fixes that.
Additionally, the "Next Up" OS channel launch wasn't checking the
correct parameter to ensure the back stack was written properly.
### Related issues
Fixes#1883
### Testing
I manually test on the emulator with these test cases:
- No profile PIN, cold start
- No profile PIN, hot start
- Profile PIN, cold start
- Profile PIN, hot start
- No profile PIN, launch via "Next Up" channel
- No profile PIN, launch via intent
- Profile PIN, launch via "Next Up" channel
- Profile PIN, launch via intent
Also added unit tests for intent-based and regular launching which
covers those cases and more.
## Screenshots
N/A
## AI or LLM usage
None
## Description
Another PR to gather small bug fixes:
- Fix aspect ratio for episodes on person page
- Use the song's artist instead of the album's artist on album page
- Fix how the dim percentage is applied to the OS screensaver dim
percentage (#1834)
### Related issues
Fixes#1851Fixes#1863
### Testing
<!-- Describe how this change was tested and on what device(s) -->
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code
and how you tested it -->
## Description
There were two problems with the experimental "Use AC3 for surround
sound audio" setting (#947):
1. Defaulted to true
2. Access wasn't guarded by enabling experimental settings
This caused unintentional audio transcoding
### Related issues
Fixes#1878
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Slight optimization to remove "child count" for grid pages. Adds back
"primary image aspect ratio" for several queries where its necessary.
Also updates extras cards to use the "primary image aspect ratio"
instead of a hard coded value.
Dev note: some light refactoring is also in this PR including renaming
the item fields lists to better reflect the intended usage in code
### Related issues
Fixes#1843
### Testing
Emulator, I tested many pages and views, but probably need to do "real
world" testing to see if I missed anything
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a new sceensaver setting to add dimming. A larger value is darker
This works with or without the in-app screensaver. If dimming is
enabled, the screen be will be dimmed after inactivity. If both dimming
and the in-app screensaver are enabled, the screensaver will show dimmed
after inactivity.
### Related issues
Closes#1179
### Testing
Emulator
## Screenshots
N/A, it's just darker
## AI or LLM usage
None
## Description
The audio player passed handleAudioFocus = false when setting its audio
attributes, so ExoPlayer never requested audio focus. Music kept playing
and mixed with other audio when a call arrived, another app started
playback, or voice search took focus.
### Related issues
Fixes https://github.com/damontecres/Wholphin/issues/1841
### Testing
1. Play a music track
2. Open YouTube and play a video
## AI or LLM usage
Cursor and Claude Code
## Description
Fixes several issues on the discover pages:
- Fixes sorting by release date for tv shows
- Fixes automatic go-to for movie & tv show grid
- Fixes tv show discover page's go-to using a movie detail page
- Updates the alignment & padding for the headers on different discover
pages to match the reset of the app
- Use media's logo image if available and the setting is enabled
### Related issues
N/A
### Testing
Emulator mostly
## Screenshots
N/A, headers and logos are unchanged, just moved slightly to match
regular content
## AI or LLM usage
None
## Description
When viewing images in a slideshow, this PR enqueues the next image so
it will likely be cached and ready to display when the slideshow
advances.
This works for both automatic and manual scrolling as well as advancing
next or previous images.
### Related issues
Closes#1835
### Testing
Emulator with large photos
## Screenshots
N/A
## AI or LLM usage
None
## Description
I accidentally removed the original fix/workaround from #1089 during a
merge in #1052, so this PR restores the fix.
### Related issues
Fixes#1736
Related to the original fix#1089
### Testing
Emulator with portrait and landscape images
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fix some bugs on discover pages:
- UI fixes for available seasons
- Fix toggle not updating when removing a season from a pending request
- Don't request images if the path is null
- Better error handling when fetching ratings
### Related issues
Follow up to #1816
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes several small bugs and does some minor refactoring:
- Use "Recently Added in XYZ" instead of "Recently recorded" for
recorded tv libraries
- Improve home page query performance by using fewer fields in the query
- When an error occurs connecting to discovered servers, show the error
instead of only logging
- Fix headers sometimes appearing briefly on some pages when navigating
back
- Catch exceptions in socket connections
- Use a larger buffer when downloading updates
- Split up ADB logging for media info
### Related issues
Related to
https://github.com/damontecres/Wholphin/issues/1517#issuecomment-5184316086
Maybe the larger download buffer will help with
https://github.com/damontecres/Wholphin/issues/1696
Will help show the error in #1801
Prevents the crash in #1786
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Add `density` to `configChanges` and handle any changes to density that
occur. Thankfully, Compose handles density changes itself, so only
changes where a density calculation is "remembered" needed to be
updated.
Without this, some devices will recreate `MainActivity` when switching
resolutions for playback. That recreation would cause playback to end
immediately.
### Related issues
Fixes#1809
### Testing
Tested on Onn 4k Pro & nvidia shield
I was able to reproduce on the Onn, but not the Shield, so this might be
device and/or API level specific
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes requesting additional seasons of a show if a previous request has
already been approved
Now if the previous request is already approved/completed/etc, a new
request will be created instead of trying to update the previous one.
Pending requests will still be updated as before instead of requesting a
new one.
### Related issues
Fixes#1702
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an edge case where clicking on an episode to go to Series Overview
wouldn't scroll to the episode if there is a special (season 0) episodes
in the episode list.
Dev note: to simplify things, if the entire dataset is cached, then just
search with `indexOf`. Often the data is cached because 20 seasons or
100 episodes are fetched initially which covers many shows.
### Related issues
Follow up to #1708
### Testing
Emulator and unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
Ensure that a playlist or collection's items are random sorted on the
home page, if applicable.
Previously, the request did sort by random then by sort name resulting
in only sorting by sort name.
However, there are still issues with random sorting in general, see
#1819
### Related issues
Technically fixes#1806
Further bugs in #1819
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
By default, Wholphin plays music with hardware audio offloading enabled.
This delegates audio decoding to the device's hardware DSP, making
playback stability highly dependent on the manufacturer's system audio
implementation. On certain hardware (such as my TCL TVs, e.g., the
Chinese Model Q9L Pro), this implementation causes music playback to
hang indefinitely at 0 seconds.
### Related issues
#1372
### Testing
Tested on a TCL TV (Q9L Pro):
- Some musics playback hangs at 0s with offloading enabled.
- Disabling audio offloading fixes the issues for all medias.
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
<img width="4284" height="5712" alt="IMG_9486"
src="https://github.com/user-attachments/assets/ddf95e68-6ebd-42f9-9565-568de4fbfc42"
/>
## AI or LLM usage
Developed in pair with Gemini and human-reviewed, tested.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
This PR has updates the Now Playing page.
Now only the song's backdrops and the song's artists' backdrops will
show. First, if the song has any backdrops (probably pretty uncommon),
they will be shuffled and shown, then if the song's artists have
backdrop, those are shuffled and shown. The backdrop changes every 2
minutes unless there's <15s left in the song to prevent rapid changes.
Note: album backdrops and album artists not directly linked to the
specific song are not included.
If a song has a lyrics and the user is scrolling through them, if the
user doesn't scroll for 5 seconds, the focus leaves the lyrics and they
scroll to the current line if available.
Dev note: Both backdrop & lyric updates are refactored to use `Flow`s to
trigger lookups
### Related issues
Fixes#1170
### Testing
Emulator, I need to test it longer term
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds an optional boolean parameter `shuffle` that is read for the
playback intent. When true, it will cause the resulting media to be
shuffled if applicable.
Something can be shuffled if the `itemId` is a grouping on media such as
a playlist, series, season, or even library.
### Related issues
Depends on #1756
### Testing
Emulator with the documented command
## Screenshots
N/A
## AI or LLM usage
None
## Description
When testing the new deep link feature from this PR
[#1688](https://github.com/damontecres/Wholphin/pull/1688)
I noticed that the back button would close the app. As in, all routes
are cleared and only the playback is in the nav stack.
I figured a more desirable behavior would be to create a nav stack,
always starting with the home screen, then also inserting the media
item's details page, if available.
To achieve this, I made the return Target.destinations a list of
destinations, and updated the other two uses in IntentService.
Open to implementation tweaks, or scrap it if this isn't how it should
work, haha.
### Testing
Tested deep links, which produced the following nav stacks:
- Movie view
- Home → Movie details
- Movie playback (with and without position)
- Home → Movie details → Playback
- Series view
- Home → Series details
- Season view
- Home → Season details
- Episode view
- Home → Episode details
- Episode playback (with and without position)
- Home → Episode details → Playback
- Trailer/extras playback (no details page available)
- Home → Playback
- TV channel view
- Home → Playback
- TV channel playback
- Home → Playback
## AI or LLM usage
AI was used to generate the initial code, debug and test, then manual
refactoring and cleanup was done.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
The alphabet scroll bar highlighted the letter of an item's *display*
name while the grid sorted by the server's article-stripped `SortName`,
so "The Beatles" sat under **B** but highlighted **T**.
- **Artists:** the Artists tab never requested `SortName`, so the
highlight silently fell back to `Name`. It now asks for `SlimItemFields`
(which already includes `SORT_NAME`), matching the sibling `/Items`
branch. The count-only query used for letter positions passes `fields =
null`, since a `limit = 0` request has no use for them.
- **Genres and studios:** these grids bucketed on the display name
unconditionally. The sort-name fallback is extracted as
`BaseItemDto.alphabetSortName` (`sortName ?: name ?: ""`) and carried
onto the `Genre` and `Studio` grid items, so all three grids now derive
the letter from one value.
### Related issues
https://github.com/damontecres/Wholphin/issues/1787
### Testing
Two unit tests added:
- `CollectionFolderViewModelTest` pins that the artist pager actually
requests `SORT_NAME` — the field whose absence caused the bug.
- `AlphabetSortNameTest` covers both branches of the `alphabetSortName`
fallback.
Manually: select a music library → Artists tab → put the cursor on an
artist starting with "The " and confirm the highlighted letter matches
the grid position. Same check on the genre and studio grids.
## AI or LLM usage
I used Cursor for debugging and implementing the fix, and Claude Code
for a follow-up cleanup pass over the diff.
## Description
Fixes a bug where the external subtitle track would not be activated if
playback is transcoded
Also fixes a bug where external subtitles would cause MPV to incorrectly
switch to transcoding
### Related issues
Fixes#1778
### Testing
Emulator with files that have a variety of external SRT, embedded SRT &
ASS, & embedded PGS
## Screenshots
N/A
## AI or LLM usage
None
## Description
Removes the max height constraints for slider preferences and navigation
drawer preferences so that longer text (possibly in other languages or a
long library name) won't cut off any content
### Related issues
Fixes#1784
### Testing
Emulator with manual long text
## Screenshots
N/A, no significant change except sliders can be slightly taller if
needed
## AI or LLM usage
None
## Description
positionOfLetter counted through /Items no matter which endpoint the
grid was actually paging. A music library's Artists tab pages /Artists,
so the index it returned had counted every album and song before the
letter - an order of magnitude more rows than the artist grid holds -
and every letter jumped to the last row.
Count through the same endpoint createPager builds its pager from:
/Artists for the ARTIST override, /Items for NONE. The artist request is
now built once in createGetArtistsRequest and shared by both, so the two
cannot drift apart.
GetPersonsRequest has neither nameLessThan nor startIndex, so /Persons
cannot be counted up to a letter at all; the PERSON override returns
null, which the call sites already map to the "no jump" sentinel. That
path is unreachable today - the only PERSON tab sorts by
ItemSortBy.DEFAULT, so showLetterButtons is never true there and
AlphabetButtons is never composed.
### Related issues
https://github.com/damontecres/Wholphin/issues/1774
### Testing
Select a music library -> Go to Artist tab -> Select a letter on the
right side alphabet scroll bar
## AI or LLM usage
I used Cursor and Claude Code for planning, implementation and code
review.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
#1654 broke playback for both live TV and recorded programs in `.ts`
format.
This PR fixes both issues! Jellyfin returns `NoCompatibleStream` for
live tv if the source ID is passed in, so that is now skipped for live
tv. `.ts` files may have IDs formatted with a slash instead of colon, so
the logic is updated to handle that plus fallbacks for potentially
unknown future formats.
### Related issues
Fixes#1767
### Testing
Emulator & nvidia shield, plus unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
Another follow up to https://github.com/damontecres/Wholphin/pull/1764 &
#1772
Also adds `lint.xml` to mark the extra translations as just a warning
since they don't cause a problem in the final build.
### Related issues
#1764#1772
### Testing
N/A
## Screenshots
N/A
## AI or LLM usage
None
## Description
Another collection of small individual fixes that don't warrant a full
PR:
- Better error handling for series and collection detail pages
- Better error handling on Discover search tab
- Fix issues when changing view options on card grid without reloading
the page
- Better error handling when changing the refresh rate or resolution
- Fix some nav drawer items disappearing briefly when restarting the app
- Fix that the discover search row would not clear when the query is
cleared
And some UI tweaks:
- Use consistent padding for the different customize home page sections
- Convert the "Continue watching click behavior" setting from a toggle
to choice dialog (the options & behaviors have not changed)
### Related issues
I think the error handling when changing the refresh rate might help
with #1737
### Testing
Emulator mostly
## Screenshots
N/A, UI changes are minor
## AI or LLM usage
None
## Description
Updates `libass-android` to `v0.5.0`. There aren't any changes from
`v0.5.0-beta01`, so this is just a formal release.
### Related issues
Fixes#1554Fixes#1561Fixes#1678
Pr for the beta release: #1752
### Testing
N/A, I tested the beta
## Screenshots
N/A
## AI or LLM usage
None
## Description
Selecting a song from search now resolves to its album's detail page,
scrolled to and focusing that song, instead of sending you to an
"unsupported item type" page. Implemented in the shared
BaseItem.destination() rather than in search specifically, so any caller
that navigates to a song gets the same behavior.
A song with no albumId plays directly while remaining in the search
results page: NavigationManager.navigateTo() intercepts a bare AUDIO
destination and hands it to a new MusicService.playSong(). Handling this
in the single navigation funnel means every caller gets it for free with
no loading-screen flash.
Adds BaseItemDestinationTest covering the new destination() logic.
### Related issues
https://github.com/damontecres/Wholphin/issues/1656
### Testing
Device: Sony Bravia 4K VH21
#### Tests on a search results page, song with albumId:
1. Select a song sends you to album page with cursor on song. Another
press plays it
2. Back button brings you back to search results.
Tested with both songs at the start of an album and songs deep into a
100+ songs album.
#### Tests on a search results page, song with no albumId:
1. Select a songs plays the song while staying in the search results
page. No loading screen.
## AI or LLM usage
I used Cursor and Claude Code for planning, implementation and code
review.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Fixes an incorrect query for an artist's albums (thanks @StefanBS for
reporting & fixing in #1761!)
Additionally, another album row for "Appears on" is added. This is
definitely necessary because Wholphin only has a tab for all artists
which includes artists featured on albums attributed to a different
artist. So those albums will now show in the "Appears on" similar to the
web UI instead of an empty artist page.
Finally, the long-click context menu is added for music videos on an
artist's page.
### Related issues
Fixes#1760
Replaced #1761
### Testing
Emulator, particularly with artists who are not album artists
Also, verified the query fix against the web UI dev console
## Screenshots
N/A
## AI or LLM usage
None, but the fix included from #1761 did use some AI
---------
Co-authored-by: Stefan Bullones <stefan.bullones@gmail.com>
## Description
The TV library page offers the option to sort by date played, however
when selected it reverts to the default (alphabetical). This appears to
be due to the use of `ItemSortBy.DATE_PLAYED` rather than the correct
`ItemSortBy.SERIES_DATE_PLAYED`. See
[BrowseGridFragment](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/ui/browsing/BrowseGridFragment.java#L169)
for the usage in the official Android TV client.
### Related issues
N/A
### Testing
Emulator against personal Jellyfin server.
## Screenshots
N/A
## AI or LLM usage
None
## Description
The bug report was caused by a race condition where the image URL was
built before the grid was fully laid out causing a `fillWidth=0` and the
`remember` would not reset after the `fillWidth` was updated to the
actual value. So this PR fixes the `remember`.
Furthermore, some extra guards are added to only pass the `fillWidth` &
`fillHeight` params if greater than 0.
Finally, the `EpisodeCard` code is deleted because it's height & width
logic was overly complicated and `BannerCardWithTitle` provides better
functionality anyway.
### Related issues
Should fix#1751
Maybe #1156 is also fixed by this
### Testing
Emulator, but I haven't been able to reproduce the race condition, but
the fix makes sense given the bug report
## Screenshots
N/A
## AI or LLM usage
None
## Description
Updates `libass-android` to `0.5.0-beta01`. While this is a beta
release, from my testing it is very stable. It also fixes several
issues, including crashes, as well. So I want to merge into the develop
branch, but it can always been reverted if necessary.
### Related issues
Should fix these issues:
- #1554
- #1561
- #1678
### Testing
On emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
On the switch server page, display error message at the bottom for the
focused server. This includes unsupported server version.
### Related issues
Jellyfin 10.11/12.0 support discussion: #1640
### Testing
Emulator
## Screenshots
<img width="1280" height="720" alt="server_error Large"
src="https://github.com/user-attachments/assets/1227e7e7-a364-48d9-9a77-cb3f78d134a1"
/>
## AI or LLM usage
None
## Description
Makes some minor changes to the playlist details page
- Center playlist name
- Show duration with seconds for songs
- Adjust some padding
Also fixes an issue where focus could be lost when leaving the page
while focused on a playlist item
Dev note: bulk of the change in thie PR is refactoring the `LazyColumn`
of playlist items into its own composable
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A, the changes are very minor
## AI or LLM usage
None
## Description
Adds a third option for d-pad seeking to not show any seek bar which
only shows the skip indicator and not the seek bar along the bottom
### Related issues
Original work to add other mode in #1650Closes#1520
### Testing
Emulator testing
## Screenshots
<img width="546" height="347" alt="image"
src="https://github.com/user-attachments/assets/2872566e-4598-4284-92cb-95a45b307835"
/>
## AI or LLM usage
None
## Description
Adds a setting to allow the user to choose between the current D-Pad
skip functionality, and a new simple trickplay overlay.
The setting is added in Advanced, under the Playback section, called
"D-Pad seek mode". The options are "Skip time" and "Trickplay previews".
I left the default as "Skip time" since that's how it operates now, and
nothing would change for users after this option is added unless they go
and set it.
When "Trickplay previews" is enabled, it shows an simple overlay when
using the D-Pad to seek (without the full overlay open). The new simple
overlay is the same implementation of the components in the full
overlay: seekbar, trickplay images, current playhead time, remaining
time. The components were pulled from the full overlay's inline UI code
into their own reusable components, now used in both the full overlay
and the simple overlay.
#### Notes
I could see there being an adjustment for the overlay hide delay. It's
currently hard coded to 1.5s.
This could be the default method, but it seems like preference is split
as to whether people want this, or the minimalistic skipped seconds only
version.
### Related issues
This is directly in response to this issue, as well as my own desire for
the feature :)
https://github.com/damontecres/Wholphin/discussions/1084
It's also loosely related to this issue, in that it would solve it since
it shows the playhead and remaining times.
https://github.com/damontecres/Wholphin/discussions/1122
### Testing
Built and tested on an Amazon Fire TV Stick 4K Max. I wouldn't
anticipate and compatibility or performance changes since it's a simple
settings/UI change involving already-used UI components.
## Screenshots
<img width="1920" height="1080" alt="wholphin-20260630-155708"
src="https://github.com/user-attachments/assets/52046cc2-7413-4f66-89dd-355464fa0f91"
/>
<img width="1920" height="1080" alt="wholphin-20260630-155723"
src="https://github.com/user-attachments/assets/57a4a271-9e44-4d2a-bea3-ce5c9cdc731a"
/>
<img width="1920" height="1080" alt="wholphin-20260630-153825"
src="https://github.com/user-attachments/assets/2cf664ff-3db5-4a2e-8626-10219946669c"
/>
## AI or LLM usage
AI was used to generate the change, automate some testing, and provide
information surrounding the change. I manually did some refactoring and
cleanup to keep it clean and consistent with the rest of the code
conventions.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
A library's grid page tries to highlight the first letter of the focused
item. But if the first letter was a not in the alphabet string (which
varies by language), a exception was thrown on the main thread causing a
freeze.
This PR ensures only a non-negative index is highlighted on the letter
buttons.
### Related issues
Fixes#1361
Need to create an issue to improve alphabet letter button handling for
non-English alphabets
### Testing
Emulator, I manually passed a non-`[0-9A-Z]` character to replicate the
freeze
## Screenshots
N/A
## AI or LLM usage
None
## Description
Switches to use the Artists API instead of Items API when fetching the
list of artists in a library.
Enables fetching artists, albums, and music videos in a collection.
Also adds titles for the remaining `BaseItemKind` types for future
proofing even if they aren't used anywhere in the app.
### Related issues
Fixes#1733Fixes#1734Fixes#1735
### Testing
Emulator, created a new collection with the above. Also verified the
artist query against the web UI
## Screenshots
N/A
## AI or LLM usage
None
## Description
Changes to searching & downloading subtitles:
- Activating newly downloaded subtitles more reliably
- Better audio index adjustment checks after downloading subtitles
- Sort subtitle downloadable options by whether the file hash matches
first
- Update the UI for subtitle downloadable options
Also, some improvements to the subtitle choice dialog:
- It now loops from top to bottom (and vice versa)
- It now focuses initially on current choice
### Dev notes
This PR now the selected tracks from `state.currentItemPlayback` to
`state.currentPlayback`. `currentItemPlayback` now only reflects the
user's choices saved in the database instead of a combination of DB and
"actual" state. `currentPlayback` stores the actual, resolved track
indexes regardless of how they were derived.
This means the indexes values in `state.currentItemPlayback` and
`state.currentPlayback` may be different. For example, if the user has
chosen "Only forced", `state.currentItemPlayback==-3` but
`state.currentPlayback` might be an actual forced track index.
### Related issues
Fixes#1549Fixes#1718Closes#1723
Fixes bug reported in
https://github.com/damontecres/Wholphin/issues/1498#issuecomment-5019965271
### Testing
Emulator
## Screenshots
Now shows SDH & hash matching
<img width="779" height="280" alt="image"
src="https://github.com/user-attachments/assets/4c33e5af-c9a1-4c9c-8217-6bc2ab3fcd27"
/>
## AI or LLM usage
None
## Description
Changes the chapter cards to use the chapter's image's aspect ratio
instead of forcing the video's aspect ratio. These ratios can be
different sometimes often for "dummy" chapters.
Also, the font size for the chapter name & position is reduced a bit.
### Related issues
N/A
### Testing
Emulator
## Screenshots
<img width="873" height="193" alt="image"
src="https://github.com/user-attachments/assets/e1c0e8a8-2e37-4644-bc99-899e20c4af09"
/>
## AI or LLM usage
None
## Description
- Fixes not scrolling to the last season on series overview if there is
a Specials season
- Apply the "bouncing" fix from #1543 to all applicable card types
- Fix card images not automatically reloading when changing image type
on grid pages
- Get rid of the sticky headers which overlap items on view options
dialogs and use a fixed header as elsewhere in the app
### Related issues
Related to #1708 which didn't introduce the bug, but made it much more
likely to occur
### Testing
Emulator & unit tests
## Screenshots
N/A, the sticky header UI changes are very minor
## AI or LLM usage
None
## Description
Adds parsing for intents to search, view, or play media based on the
action and parameters. This allows for remotely opening Wholphin and
starting playback.
This is considered experimental, so the syntax or behaviors may change!
See the `Intents.md` file in this PR for syntax and examples.
### Related issues
Replaces https://github.com/damontecres/Wholphin/pull/738
Closes https://github.com/damontecres/Wholphin/discussions/1095
### Testing
Emulator with the test commands in the docs
## Screenshots
N/A
## AI or LLM usage
None
## Description
Previously not every plural translation had full coverage and some did
not have the fallback "other" quantity translation. So for some
languages this would cause an error when trying to display the string
depending on the quantity. This PR ensures every plural translation has
the "other" fallback.
Note: the "other" fallback is copied from an existing translation so I'm
sure some will not be grammatically correct!
Dev note: I used a python script to parse and fix the entries, so it
also rewrote some of the unrelated `CDATA` entries.
### Related issues
Fixes#1676
### Testing
On the emulator, I reproduced the error in the issue with the Danish
translation, verified it was fixed after the patch was applied
## Screenshots
N/A
## AI or LLM usage
None
## Description
Gathering several small bug fixes:
- Clear the backdrop when manually reloading the home page
- Convert the "# years old" string resource to plural
- Fix a spelling error in the source English strings
- Ensure #1680 is respected when searching for subtitles to download
- Slightly adjust padding for the header in collection details to match
other headers
### Related issues
Fixes string issues reported in
https://github.com/damontecres/Wholphin/issues/1705#issuecomment-4982909931
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes querying for resume item (continue watching) for Jellyfin 12 so
that whole series are not included. The patch is also compatible with
Jellyfin 10.11.
### Related issues
Fixes#1689
I couldn't find an issue in Jellyfin that mentions this behavior change,
so I'm guessing it was a side effect. But since the old query did ask to
include Series, I don't think it's a bug in the server.
### Testing
Manual API queries & emulator on both Jellyfin 11 & 12-rc2 servers
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds long clicking search results to show the context menu. It's the
same context menu as elsewhere in the app.
### Related issues
Closes#1695
### Testing
Emulator to toggle favorite and watched status on various types
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes & improves the "Add to playlist" dialog:
- Added searching by name
- Playlists are sorted by recently updated
- No longer filters by media type
- Filters based on user's permission for the playlists
The behavior is now consistent with the web UI's list except for the
sort
### Related issues
Should fix#1124
### Testing
Emulator testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
#1614 added filtering by a TV production status. I pulled the enum
values from a comment in Seerr's code which spelled it as `Cancelled`,
but the actual API uses `Canceled`. This PR corrects the spelling.
Also, since this value isn't actually used yet, I've removed it from
`TvDetails` for now.
### Related issues
Fixes#1703
### Testing
Emulator, filter by production status as canceled and open any result
## Screenshots
N/A
## AI or LLM usage
None
## Description
Saves the sort and filter of items within a genre or studio
(Movies->Genres tab->Click genre) across genres or studios. So if you
open "Action" movies and change the sort, the sort will be used for
"Animation" or "Horror" movies.
This is saved separate from the sort/filter on the Library tab.
### Related issues
Closes https://github.com/damontecres/Wholphin/issues/1253
### Testing
Emulator by switching sorts
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an off-by-one error when searching for the right episode (or
season) to scroll to on the series overview page.
While the code works most of the time, on cases where a special appears
_after_ a season is included, the final episode in the season not be
scrolled to. For example, if Special S00E04 comes after Season 2 (which
has 10 episodes), clicking on S02E10 on the home page won't scroll to
that episode on the series overview, instead S02E01 will be focused. And
S00E04 will appear at the end of the list.
This should also be a very, very tiny performance boost since the
"search the whole list" fallback won't be triggered as often. I also
added some comments to explain the function better.
### Related issues
N/A, just noticed it yesterday
### Testing
Emulator in the situation described above
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds settings under the "User profile settings" section which allows for
specifying preferred languages for audio and subtitles and the preferred
subtitle mode.
By default (matching existing Wholphin behavior), the preferred
languages and subtitle mode are derived from the user's profile settings
provided by the server which can be set on the WebUI:
User->Settings->Playback or Subtitles. However, this isn't well
understood and Wholphin doesn't display these values anywhere, so many
users were confused.
The preferred languages and subtitle mode can now be configured from
within Wholphin. Setting these will **not** update the server. The
settings are **only** used locally.
### Related issues
Related to #319Closes#790
### Testing
Emulator manually changing some settings, added additional unit tests to
ensure the overrides are respected when choosing tracks
## Screenshots
### Settings page
<img width="1280" height="720" alt="settings_01 Large"
src="https://github.com/user-attachments/assets/28cc2f6b-3a60-42a4-b268-48695086d566"
/>
### Clicking into "More user profile settings"
<img width="1280" height="720" alt="settings_02 Large"
src="https://github.com/user-attachments/assets/6c3be3aa-c177-4d44-8060-b76d15fb10d7"
/>
### Language choice dialog
Can filter languages by typing in the search box at the top
<img width="1280" height="720" alt="settings_03 Large"
src="https://github.com/user-attachments/assets/c014d0ba-8d5b-4201-a6ee-4876b478148c"
/>
## AI or LLM usage
None
## Description
Adds the duration & ends at time to the next up card
### Related issues
Closes#1664
### Testing
<!-- Describe how this change was tested and on what device(s) -->
## Screenshots
<img width="660" height="241" alt="image"
src="https://github.com/user-attachments/assets/7551843e-4f53-43fc-af2a-3e503ba6d03c"
/>
## AI or LLM usage
None
## Description
Fix applying the subtitle outline width in ExoPlayer for release builds
The outline width field isn't public, so reflection is used to bypass
and modify it. Since proguard/r8 renames all of the classes and fields,
this meant the field was no longer found. The fix is to have proguard
preserve the necessary field names.
### Related issues
Fixes#1681
### Testing
Emulator w/ release build
## Screenshots
N/a
## AI or LLM usage
None
## Description
Updates the logic for selecting tracks which should fix several edge
cases
The new logic groups track types together and then applying the user's
selection to the actual tracks in the file. For example, if the user
choosing the audio track which the server reports as absolute `index=2`
(eg the _third_ track of any type in the file) and audio `index=1` (eg
the _second audio_ track in the file), then the track selection logic
will use the _second audio_ track in the direct played file (after
accounting for id sorting).
Some other changes:
- If direct playing and an unsupported track is selected, the app will
now fall back to transcoding
- If the correct audio/subtitle tracks are not found, the app will now
fall back to transcoding instead of blocking any track change attempts
### TODO
Not necessarily in this PR, but some things need further investigation:
- Handling external audio tracks
- Why does ExoPlayer order the tracks differently sometimes, see
[example
here](https://github.com/damontecres/Wholphin/issues/791#issuecomment-4836470813)
where `0:3` is _after_ `0:4`, but the `MediaSourceInfo` lists them `0:3`
then `0:4`
### Related issues
Maybe fixes#791
Should fix
https://github.com/damontecres/Wholphin/issues/1005#issuecomment-4085440175
Related to the parent issue, #1475
Replaces #845
### Testing
Unit tests mostly, some basic tests on emulator
Needs more "real world" testing
#### Try it out?
You can try out this build by following the [instructions
here](https://github.com/damontecres/Wholphin/releases/tag/develop-track-selection).
If you do, please report your results in this PR!
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds an experimental setting to prefer transcoding audio with more than
2 channels to AC3. Stereo audio does not transcode.
This is basically a workaround for HDMI ARC (not eARC) or optical audio
devices because instead of 2 uncompressed PCM channels, 6 AC3 channels
(ie 5.1) is possible.
An downside of enabling this is that embedded subtitles be burned in
causing a video transcode as well.
### Related issues
Fixes#255
Incorporates some code from #1410
### Testing
Just on the emulator so far to observe if the settings trigger the
transcode or not
## Screenshots
N/A
## AI or LLM usage
None
---------
Co-authored-by: darkflame91 <darkflame91@gmail.com>
## Description
Start testing against Seerr `3.3.0` which has been out for over a month.
The only significant change is that the new "blocklisted" status is
added, but it uses the same code as the previous "deleted" status. So
generally, Seerr `3.2.0` will still work.
This starts work on supporting Seerr's blocklist which, unfortunately,
is expected to be enforced on the client side. For now, blocklisted
items still show, but the request button is disabled and shows
"Unavailable" instead.
Finally, some Seerr parent permissions I missed in #1631 are added.
### Related issues
Related to #1626
### Testing
Emulator testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
Instead of using English-only duration format (e.g. `HHh MMm SSs`) in
various places, I've added a format function that is based on several
new localized resources keys.
That includes:
1. Media runtime in Item view
2. Time remaining in Item view
3. End time in Player view
4. Time passed and time remained in Player view
### Related issues
None
### Testing
I installed the app on my Google TV Streamer, switched to my language,
browsed all item views that I could think of to see that the duration
labels are all localized in my language. I didn't commit those strings,
I will do it through the Weblate after it will get merged. I also played
a few items and saw that the labels there are localized as well.
## Screenshots
It looks the same in English as before, currently no other language has
custom resources for it yet.
## AI or LLM usage
Used Claude Sonnet to detect places that I missed that are related to
this localization change. Also it helped me to get access to my format
function from various places in the code.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Updates several dependencies and cleans up some naming and duplicates.
There are no user facing changes in this PR
### Dev notes
There is a workaround needed to upgrade to Kotlin 2.4.0 to avoid
https://github.com/google/ksp/issues/2964
There are a few more dependencies, but they require increasing the
target SDK to 37, but I don't want to do that yet. API 37 requires
requesting a [new permission for local network
access](https://developer.android.com/privacy-and-security/local-network-permission).
Since that is a critical requirement for Wholphin/Jellyfin, I need to
spend more time reviewing the dev guide to make sure it's handled
correctly.
### Related issues
Replaces #1593
Replaces #1512
Replaces #1465
Replaces part of #1446
### Testing
Unit tests and quick emulator run
## Screenshots
N/A
## AI or LLM usage
None
## Description
Add some improvements related to life cycle handling whenever the app is
resumed:
- Refreshing the server-side user config
- Refreshing the home page automatically
Also 401/403 errors accessing recording folders will be skipped since
this typically means the user lost access while the app was running.
Refreshing the server-side user config (above) will also help with this.
Finally, there's some general code clean up in `MainActivity` to move
non-UI related code out of Compose.
### Related issues
Closes#1647
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This is a targeting fix for a very specific bug: on Fire TV 4k Max
(AFTKRT), and possibly others but untested, ExoPlayer can't play an
HLS/TS stream that has 8-channel (7.1) EAC3 audio. Direct play is not
affected (7.1 EAC3 plays fine), and fewer channels work fine (i.e. 5.1
EAC3) even in an HLS/TS wrapper.
This bug + device is tracked in KnownDefects. If more models are tested
and found to have the same issue, they can be added to the list. Top
candidates are probably AFTKA and AFTKM since they are also in the
models with HEVC DoVi/HDR10+ playback issues and so probably have
similar underlying limitations, but I don't have those devices to test.
The fix is to first allow the app to make a playback request to the
server and receive the stream info Jellyfin has decided to send. Then it
evaluates the settings and stream for the following:
- Playback is using ExoPlayer
- Device is in the modelsWithEac3HlsPassthroughBug list (currently only
AFTKRT)
- Stream is not direct play
- Stream is HLS with MPEG-TS container
- Audio is 8-channel EAC3
We have to use a two-request fix because we don't know what Jellyfin is
going to decide for the stream until we receive it. That way normal
playable streams aren't getting overwritten by the fix when they
shouldn't be. So the only streams that get the two request path applied
is the exact fix for this bug.
To apply post-playback-info fixes (including this one) a helper file was
created PostPlaybackInfoFixes. In the future, if there are more bugs
that can only be determined after the decided stream info is received,
they can be added here. This keeps things logical and maintainable.
### Related issues
The bug was able to be reproduced on multiple items with this exact
scenario by simply playing back the item (making sure ExoPlayer is set
as the default player in settings).
### Testing
Tested on the target device: Amazon Fire TV 4k Max (2nd Gen), model
AFTKRT.
The following media was tested:
- ExoPlayer, multiple titles with HEVC, EAC3 7.1 copied into HLS/TS (all
failed, only processing 1-2fps)
- Direct play of the same titles (works)
- EAC3 5.1, HLS, MPEG-TS (works)
- MPV with failing titles (works)
The following device and settings were tested and had no effect on the
above media results, ruling out external connections and settings:
- HDMI to an LG TV, both connected to a soundbar via ARC, and
disconnected using internal speakers
- HDMI to a Dell computer monitor
- Fire TV Settings > Display & Sound > Audio > Surround Sound > all 4
settings tested (Best Available, PCM, Dolby Digital Plus, Dolby Digital)
## AI or LLM usage
Codex was used to debug the issue, create the initial change, and do
iterative testing on the device to narrow the scope. The code was then
carefully reviewed and I made structural decisions so everything fits
well within the project's established conventions, removing bloat, added
comments and kept it targeted, clear and maintainable.
## Description
Fixes a crash when trying to play content via MPV on release builds.
After #1365, the package name for `MPVLib` changed, so the existing
proguard rule no longer applied. This meant the native code could no
longer find the right class.
### Related issues
Fixes#1633https://github.com/damontecres/wholphin-extensions/pull/9 fixes this
upstream
### Testing
Tested release builds on emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR introduces the concept of "experimental" settings or features.
These may cause instabilities or may need further testing. They may be
changed or removed any time. Also, the developers won't provide lots of
support for them.
The goal is expose some specialized settings that might only apply to a
small subset of users or test out tweaks to playback.
The first experimental feature is a toggle to enable [video
tunneling](https://developer.android.com/media/media3/exoplayer/track-selection#tunneling).
### Related issues
Related to #1283
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
When saving Seerr permissions on the WebUI, if a "parent" permission is
checked, only its bit is flipped and its children's bits are not. So
clients must check if a user has the permission _or_ its parent.
For example, if just "Request 4K" is checked, the flags for "Request 4K
Movies" and "Request 4K TV" will not set explicitly.
### Related issues
Fixes#1630
### Testing
Emulator & unit tests, also manually reviewed API requests/responses on
Seerr's web UI
## Screenshots
N/A
## AI or LLM usage
None
## Description
Uses the newer version of `wholphin-extensions` which now has the Kotlin
code, so this PR removes the MPV Kotlin code from this repo. Also
updates gradle to use providers.
There are no user facing changes
### Dev notes
`wholphin-mpv` can be provided by:
1. [Fetching pre-built
binaries](https://github.com/damontecres/wholphin-extensions#pre-built-binaries)
(Preferred method for devs)
2. Building `wholphin-extensions` from
[source](https://github.com/damontecres/wholphin-extensions#build-from-source)
3. Using `wholphin-mpv-stub` (the new module added in this PR) which
allows for compiling Wholphin, but will throw exceptions if MPV is used
during runtime
### Related issues
Related to #1172
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Gathering some smaller UI/UX changes:
- Default to the user's preferred subtitle language when
searching/downloading subtitles and if unset, fall back to the UI's
language
- Only show >0s runtime on quick details & don't show negative ends at
- On the OS channel row for recently added, only include movies &
episodes
- Only show movies & episodes to "Recently Added" Android OS channel
- Fix context menu for adding songs & albums to the music queue
### Related issues
Related to #1339 & #1484Fixes#1617
Fixes
https://github.com/damontecres/Wholphin/issues/1329#issuecomment-4440684328
### Testing
Just on the emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR moves the separated HDR subtitle style behind a toggle. This
means users must now opt-in to using separated SDR vs HDR subtitle
styles. This is a better UX for users who want to customize the style
but use the same styles for both.
Users who have changed the HDR subtitles previously will have the toggle
enabled automatically. If the user hasn't changed HDR style or their SDR
& HDR styles are the the same, the toggle will be disabled.
Also, fixes a bug where when playback switched between SDR or HDR, the
subtitle styles might not be updated.
### Related issues
Closes#1603
### Testing
Emulator & unit tests for migration
## Screenshots
N/A
## AI or LLM usage
None
## Description
<!-- Describe the changes in detail -->
The clock ignored the device's system 24-hour preference:
getTimeFormatter derived 12/24h purely from the locale. This reads
DateFormat.is24HourFormat and builds the time pattern from it, so the
clock follows the system setting.
### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->
### Testing
<!-- Describe how this change was tested and on what device(s) -->
Tested on the emulator toggling the system 24-hour setting on/off.
assembleDefaultDebug, testDefaultDebugUnitTest and pre-commit (ktlint)
pass.
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code
and how you tested it -->
The analysis and the fix were done by me with AI assistance (Claude). I
wrote the description myself; the English wording was polished with AI.
I understand the code and can maintain it.
## Description
This PR adds sort & filter options for the discover grid pages.
Sort options:
- Popularity
- Release date
- "Community rating" (TMDB user vote)
- Name
Filter options:
- Genre
- Studio/Network
- "Community rating" (TMDB user vote)
- Parental rating
- Production status (TV only)
Also, two more tabs are added to the discover page for browsing movies
and TV. These are essentially the same as clicking "View more" on the
respective rows on the Discover tab.
### Limitations
This PR hardcodes the region used for studio listing and filtering to
"United States". I'll work on a follow up PR to enable users to specify
this.
The sort & filters are not persisted. I think they should be on the two
new tabs, but not elsewhere, but this will be a follow up PR.
### Related issues
Related to #702
Implements some of #1511
Rewrites the filter logic in #1610
### Testing
Emulator, but needs further testing
## Screenshots
<img width="1280" height="720" alt="discover_filters Large"
src="https://github.com/user-attachments/assets/cdefb95c-c4b7-4e06-9bac-b535d372ba03"
/>
## AI or LLM usage
None
## Description
The exact behavior of what happens when playback ends based on the
user's settings would change between versions. This PR defines the
behaviors better and includes unit tests to verify future changes won't
break it again.
### Related issues
Fixes#1548
Related to https://github.com/damontecres/Wholphin/pull/1257
### Testing
Emulator & unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
If subtitles preferences are "Always", this PR will prefer non-forced
tracks even if any forced ones are default tracks.
This is in-line with the web UI from my testing and another review of
the code.
### Related issues
Fixes
https://github.com/damontecres/Wholphin/issues/790#issuecomment-4708705576
### Testing
Unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
If remember tabs is enabled and the current remembered index is somehow
removed, the app would crash.
I think the only way a regular user can encounter this is if the server
removes a live TV recording folder that the user has
selected/remembered. All other tabbed pages use a fixed list.
### Related issues
None
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Fixes showing "null" in the subtitle for some tv episodes (usually
daily shows like Jeopardy)
- Partially fix holding back to return to the previous grid position
(still has focus issues)
- Show/hide tabs on the discover page similar to other pages in the app
- Prevent a warning server-side if a negative playback timestamp is sent
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds option to select the quality profile and/or root folder when
requesting from Seerr.
The user needs "Advanced Requests" permission to use this feature. If
not, the options won't show.
### Related issues
Closes#863
Related to #702
### Testing
Emulator w/ different user permissions and different request
configurations
## Screenshots
<img width="1280" height="720" alt="request_movie Large"
src="https://github.com/user-attachments/assets/2665dcbb-e6c3-4d26-857c-b465edff6a02"
/>
## AI or LLM usage
None
## Description
Fixes a crash when backgrounding/restoring the app on certain grid pages
### Related issues
Fixes https://github.com/damontecres/Wholphin/issues/1598
Replaces #1599
### Testing
Emulator
It's easy to reproduce by going to a library's recommended tab,
scrolling to any row's view more, then pressing home to background the
app
## Screenshots
N/A
## AI or LLM usage
None, but includes a commit from #1599 which did, see the note there
---------
Co-authored-by: Christian Tismer <tismer@stackless.com>
## Description
This PR contains only tests, there are no user facing changes.
Adds tests for playback covering:
- Playing next, previous, or within the queue
- Media segments (intro/credit)
- Playing intros before media
Adds basic unit tests for track selection. This uses a new "test track"
builder which can model various files and build them as represented
(differently) by the server, ExoPlayer, and MPV. In future, this will
allow for testing of more complex files.
Also add unit tests for services covering things like:
- Fetching extras
- Deleting media
- Server/user management & auth
### Related issues
Will help with testing/verifying some issues like #1475, #1548
### Testing
Ran the unit tests!
## Screenshots
N/A
## AI or LLM usage
None
## Description
Switches all usages of `kotlinx.coroutines.Dispatchers` to
`WholphinDispatchers`.
`WholphinDispatchers` provides an easier and more reliable mechanism to
use a test dispatcher in unit tests. For regular app usage,
`WholphinDispatchers` just delegates to `Dispatchers`.
There are no user facing changes
### Related issues
None
### Testing
Basic emulator testing & unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
Ensures that all string & plural resource keys are unique. This should
help make sure all of the strings are shown in weblate's translation.
### Related issues
Related to #1551
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds options to the context menu to play or shuffle items in a series,
season, collection, or playlist. So you can long click a TV show and
play it directly.
Playing a TV show will play the next up episode (same as the play button
on the series detail page).
Playing a season, collection, or playlist will start from the beginning.
Also, removes the green play arrows and uses the same neutral color as
other icons.
### Related issues
Closes#1451Closes#1507
### Testing
Emulator from library pages & series detail pages
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Ensures all parent ratings are shown in the filter drop down
- Fixes focus loss after filtering library and there are no results
### Related issues
Fixes#1535 (along w/ #1529)
Fixes#1534
### Testing
Emulator w/ a UK movie library
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes row bounce using `SeasonCard` when are two exactly two items. This
was done by calculating the y-offset in the `layout` modifier instead of
`offset`. This has the side effect of probably being very slightly more
efficient since only 1 animation state is needed now.
Fixes row bounce on the second row of the home page if the first row is
empty and not shown.
### Related issues
Fixes#1528Fixes#1271
### Testing
Emulator in different situations. Also checked the layout inspector to
make sure the optimizations from #1184 weren't affected
## Screenshots
N/A
## AI or LLM usage
None
## Description
Makes some improvements to the debug page. Also adds some audio device
listing for debugging.
### Related issues
N/A
### Testing
Emulator & shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Moves storing the remembered tabs from app preferences to the app
database. This means deleting the user will remove the remembered tabs
and the app preferences file is smaller and faster to load.
The remembered tabs are migrated automatically when the app is updated.
There are no significant user facing changes in this PR.
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Updates the parental rating filter so it only shows the relevant options
instead of all of them. This is now the same behavior as the web UI.
Additionally, the filter options are cached for a short time so the UI
doesn't need to load them every time the menu is opened.
Dev note: Refactors `FilterUtils` into a service which allows for the
caching
### Related issues
Closes#410
### Testing
Emulator with movie & tv show libraries
## Screenshots
N/A
## AI or LLM usage
None
## Description
Based on feedback from users, many felt it was difficult to 1) tell if
multiple version of an item existed and 2) choose the version they want
So this PR adds a new expandable button next to Play/Resume/Restart to
choose the version. It is a shortcut to the More->Choose Version dialog,
but also serves as a more prominent indicator that there are multiple
versions. The button is only shown if there are 2+ versions to choose
from.
Also adds displaying the bitrate in the choose version dialog just in
case there are identically named versions with the same resolution.
Dev note: I also added some docs for classes & functions related to
choosing a version
### Related issues
Related to #652
### Testing
Emulator
## Screenshots
<img width="1280" height="720" alt="choose_version_button_unfocused
Large"
src="https://github.com/user-attachments/assets/4f6051ef-b6ae-4fad-9d54-3840523350ed"
/>
<img width="1280" height="720" alt="choose_version_button Large"
src="https://github.com/user-attachments/assets/0e8412bc-2d18-4c6c-987a-2f067363ad2c"
/>
## AI or LLM usage
None
## Description
Fixes bugs:
- Follow up to #1487 since the 404 exception can be ignored in some
cases
- Fixes a only-on-main-thread exception when clearing the secure flag in
some cases
### Related issues
Related to #1469
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Minor tweak to prevent the keyboard from losing focus when typing search
queries when "combine search results" is enabled
### Related issues
Fixes#1523
### Testing
Emulator only
## Screenshots
N/A
## AI or LLM usage
None
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Collecting some minor fixes together:
- Adds error handling when submitting a discover/seerr request,
previously there was no visual feedback if the request failed
- Fix MpvPlayer reporting that it was buffering forever
- Fix discover results row on regular search page not being clickable
- Fix focus loss when returning to movie details after playback advanced
enough to switch the button from Play to Resume
- Fix some focus issues when returning to the TV guide page
### Related issues
Related to #1503 for MPV
Fixes#1500Fixes#1508Fixes#1501
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an issue where the wrong nav drawer item would be highlighted in
the "More" section
Previously, displaying the discover nav item was partially handled in
the view model state and partially within the UI code which caused this
bug. This PR moves it all into the view model state.
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes a crash that could occur during the beginning of playback
Also adds better handling for clicking next extremely fast
### Technical details
The playlist objects would be created on the IO thread and it had a
`mutableIntStateOf` to track the current index. This meant that there is
a race conditions for the IO thread to commit the snapshot for the main
thread to see them. If the main thread is faster, it will crash.
To fix that, this PR moves the mutable state in `Playlist` into the
`PlaybackState` flow in the view model. This enures both the background
threads and re-compositions have the right state.
### Related issues
Fixes#1509
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes the inconsistent live tv programs shown on the home page by using
a different API endpoint & filtering.
Also some UI fixes:
- Use the "Watch live" string consistently for the row instead of it
sometimes switching to "Live TV"
- Adjust the "Ends at" in the home page header to show the actual end
time
- Show the channel name in "quick details"
- Show season & episode number, if applicable, in "quick details"
### Related issues
Fixes#1080
### Testing
Nvidia shield w/ fake EPG data
## Screenshots
N/A, just UI fixes, nothing new
## AI or LLM usage
None
## Description
Only show the view more card at the end of row if there are actually
more items (or at the limit).
Also reduces the size of the arrow a bit
### Related issues
Related to #1392
### Testing
Emulator with different max items setting
## Screenshots
N/A
## AI or LLM usage
None
## Description
On a library recommended tab, if you clicked View More, clicking a card
on the grid would directly play the item instead of the regular
behavior.
This PR fixes that and simplifies the code required to support simple
context menus.
### Related issues
Fixes#1480
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
If a home page row returns 404 Not Found, treat it as empty and don't
show it.
It will still show as an error on home row settings, since this page
doesn't hide empty rows.
Also fixes a bug where you can't scroll up if there are 3+ empty rows
before the current row.
### Related issues
Closes#1469
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds more sorting options, notably premier date & ratings, to different
media types.
### Related issues
Closes#1417Closes#1429
Replaces #1420
### Testing
Emulator on movie genre's items and music video libraries
## Screenshots
N/A
## AI or LLM usage
None
## Description
### Bug fixes
- Fixes a race condition crash when configuring subtitle styling before
libmpv is initialized
- Save the clicked item position on grid pages (#1428 & #1462)
- Fix querying the server every 3 seconds for transcode reason if the
video is direct playing
### UI changes
- Makes the title on grid pages slightly smaller
- Makes adjustments to the song list item on now playing so that no
space is used if there's no artist
- Use settings icon instead of triple dots on now playing page
### Related issues
See above
### Testing
Emulator, nvidia shield & google streamer
## Screenshots
### Song list item
Before & after
<img width="199" height="119" alt="image"
src="https://github.com/user-attachments/assets/7fd3636a-d1e4-4b1f-8379-8599068d6309"
/>
## AI or LLM usage
None
## Description
Adds a few enhancements to home page customization:
- Add option to combined continue watching & next up to split it
- Add option to continue watching and next up rows to combine them
- Add option for sorting playlist or collections
- Show suggestions (recently updated) when searching for a playlist or
collection to add
- Better handling for loading vs unsupported types for suggestions
Also, the old toggle for combining continue watching & next up in
advanced settings is removed. Now by default the rows will be combined
unless changed in the home page customization settings.
### Related issues
Closes#340Closes#1383Closes#1350
### Testing
Just on the emulator so far
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Fixes a bug introduced in #1453 which broke the favorite page
- Follow up to #1428 to refactor the collection folder code to hoist
state up another level
- Fixes resuming playback from series details play button
### Related issues
Fixes#1313Fixes#1461
Follow up to #1453 & #1428
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an issue where local trailers wouldn't play (or show in the queue)
if Cinema Mode is enabled and the server has a preroll/intro.
Also fix building the playback queue when "Show next up" is "Never". The
queue is shown, but not automatically played and next item dialog popup
is never shown. But to indicate playback is over, the controller show.
### Related issues
Fixes#1348Fixes#1303
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Prefer using thumb image types for movies when adding to Watch Next
android OS channel. The primary/poster image will be used if there is no
thumb.
### Related issues
Fixes#1425Fixes#1403
### Testing
Emulator, verified a movie was showing its primary image, then ran this
PR
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Fix crash on license info page for release builds (#1437)
- Fix configuring which ratings to show by default (#1407)
- When re-ordering nav drawer items, ensure the one that was just moved
is always visible (#886 )
- Fix app losing focus when switching between list layout types (#1428)
- If the version doesn't have any additional commits, don't show the git
hash info (ie `v0.6.4-0-gabc1234` shows as `v0.6.4`)
- Adds tests for Version comparisons & toString
### Related issues
See above
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Refactors all usages of `LiveData` into flows in keeping with best
practices. This also includes refactoring several `ViewModel`s to
combine multiple `LiveData` into a new single `StateFlow`.
There are no user facing changes
### Related issues
None
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds an option to display libraries in a list layout
The list can be regular showing 6-7 items or compact showing ~11 items.
Can also toggle showing the details on the left side. Movies, tv shows,
& albums have type-specific layouts and other types like videos, music
videos, etc use a generic one similar to movies.
Another change is that showing the backdrop is now a separate toggle
from showing details for both grid & list layouts.
Sorting, filtering, & jumping to a letter all work as expected!
### Related issues
Closes#1101
### Testing
Emulator mostly
## Screenshots
<details><summary>Click for screenshots</summary>
<p>
### TV shows list
<img width="1280" height="720" alt="series_list Large"
src="https://github.com/user-attachments/assets/f8f9b801-e537-4cdf-b992-f2f1259457fa"
/>
### TV shows compact list
<img width="1280" height="720" alt="series_list_compact Large"
src="https://github.com/user-attachments/assets/747e5929-90d7-4d59-a179-bc3210ac01b9"
/>
### Movie list
<img width="1280" height="720" alt="movie_list Large"
src="https://github.com/user-attachments/assets/ee3e1c6c-fb25-438a-a11e-522d30fbaabf"
/>
### Album list
<img width="1280" height="720" alt="music_list Large"
src="https://github.com/user-attachments/assets/9b1f59ec-c237-4ab2-9b51-6cb519f5327c"
/>
</p>
</details>
## AI or LLM usage
None
## Description
Shows a loading indicator and slightly dim the screen when playback is
buffering.
Showing the indicator is delayed in case the buffering doesn't take very
long.
Dev note: the changes to `MpvPlayer` need to be ported to
wholphin-extensions as well.
### Related issues
Closes#1411
### Testing
Emulator
## Screenshots
[buffering.webm](https://github.com/user-attachments/assets/dd848696-d6de-4f68-94e4-901bb5db1185)
## AI or LLM usage
None
## Description
Adds a setting to configure the app's language/locale. It is configured
per user. This settings has no impact on audio or subtitles language
choices.
Only languages Wholphin has translations for can be used. If you would
like to contribute new translations, check out
https://translate.codeberg.org/projects/wholphin/!
Dev notes: This doesn't exactly follow Android's recommendations because
the [guidelines are for
per-app](https://developer.android.com/guide/topics/resources/app-languages)
and not users within the app.
### Related issues
Closes#513Closes#669
### Testing
- Emulator API 34
- Nvidia shield API 30
- Fire TV OS 7 API 28
## Screenshots
<img width="1280" height="720" alt="language Large"
src="https://github.com/user-attachments/assets/bf1c0962-8a36-4012-aed9-eab564fc1114"
/>
## AI or LLM usage
None
## Description
r8 optimizations seemingly cause guava's LocalCache to throw an
AssertationError when evicting an entry. This PR adds a broad keep rule
for the cache package.
The crash is reproducible on a release build with a TV Season that has
25+ episodes with people in them and scrolling through the episodes. It
will crash when focusing on the ~25th episode.
### Related issues
Related to #1437
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Enables minify & resource shrinking for release builds. This should
improve overall performance a bit. It also decreases the apk size by
~15%.
Also updates AGP
### Related issues
N/A
### Testing
Emulator, but needs more testing before a final release
## Screenshots
N/A
## AI or LLM usage
None
## Description
There was an issue if when opening a page with theme music and quickly
changing pages, usually playback, the theme music might play on the new
page. This is because the call to stop the theme music could potentially
happen _before_ the song was loaded or had started playing.
This PR fixes this race condition by tracking the job that's fetching
the theme song and by adding a mutex to ensure the play function
completes before the stop function can begin.
### Related issues
Fixes#938
### Testing
Emulator w/ fake delay to increase the odds of the issue occurring
## Screenshots
N/A
## AI or LLM usage
None
## Description
Don't show future content (episodes, etc) on recently released rows in
recommended tabs or home page rows.
### Related issues
Closes#262
Replaces #1262
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
The original PR #1262 author used claude. Some of that code that was
retained in this PR was reviewed & tested.
---------
Co-authored-by: ADT95 <turner7@outlook.com>
## Description
This should fix the issue where suggestions would sometimes get stuck
loading. My best guess is that they were getting stuck because the app
thought it was still loading, even though the background job was just
waiting.
This should fix that by making each library kick off its own suggestions
job when needed.
### Related issues
Fixes#1065
### Testing
Emulator and nvidia shield
## Screenshots
N/A no ui changes
## AI or LLM usage
Used codex to help diagnosis the bug. Also added unit tests and more
debug logging for the suggestions system using ai. Wrote the fix myself.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Fixes showing the discover search result row
Also adds a Search tab to the discover page. You can already search
discover results from the regular search page, so the new tab is just
for convenience.
### Related issues
Related to #709
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Fix always showing the user list on app cold start even with auto sign
in enabled
- Ignore unknown JSON keys when restoring view options or filters from
the database making them more backward compatible for devs switching
branches
- Don't show a view more card on collection detail page rows, it's not
necessary
### Related issues
First bug was introduced by #1367
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds the standard extras row to the album detail page if there are any
extras
I also tried to get Jellyfin to recognize artist extras, but it did not
work, so this just adds extras for albums
### Related issues
Closes#1409
### Testing
Emulator
## Screenshots
N/A, same extras row
## AI or LLM usage
None
## Description
Adds a "Show voice search button" toggle to the search page's view
options dialog, alongside the existing "Combined search results" toggle.
When disabled, the voice search button is hidden from the search row,
taking it out of the D-pad focus path.
The new field `show_voice_search_button` is added to `SearchPreferences`
(default `true`). The serializer applies the default for new installs,
and a new entry in `AppUpgradeHandler` sets it to `true` for users
upgrading from earlier versions so existing behaviour is preserved.
### Related issues
Discussion: https://github.com/damontecres/Wholphin/discussions/1419
### Testing
Tested on an Android TV (1080p) AVD running API 34:
- Toggle on (default): voice search button visible at the top of the
search row.
- Toggle off: button disappears; only the search text field remains.
- Toggle back on: button reappears.
- Upgrade handler verified by spoofing `version.current.name` to an
older value in SharedPreferences and force-restarting the app; after
`needUpgrade()` fired, the toggle was on as expected.
The upgrade-handler entry currently uses `0.6.4-8-g0` (based on current
`main` HEAD); happy to bump this if you'd prefer a different target
version on merge.
## Screenshots
<img width="1920" height="1080" alt="pr1-01-home"
src="https://github.com/user-attachments/assets/0d5cdad0-5547-44f3-b4a5-5d15f71dddc0"
/>
<img width="1920" height="1080" alt="pr1-02-toggled-off"
src="https://github.com/user-attachments/assets/1e574229-0838-487d-b943-83d394a8193d"
/>
## AI or LLM usage
I used Claude to scaffold the proto/serializer/upgrade-handler/dialog
changes, mirroring the existing `combined_search_results` pattern. I
reviewed each diff before committing and tested the toggle manually on
the emulator, including the upgrade-handler simulation.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
Fixes#1396.
The else branch of `when (nextState)` in PlaybackController added a 32dp
Spacer that left empty space below the controls when there were no
chapters or queue.
Possible follow-up: Pentaphon's idea of a "similar items" row in that
empty slot. Happy to put up a separate PR if useful.
## Testing
Verified on an Android TV (1080p) AVD running API 34 by playing a movie
with no chapters and no next-up item. With the fix, the bottom of the
control overlay sits the expected 8dp from the screen edge (the existing
parent padding). Also rebuilt without the fix and compared back-to-back
to confirm the Spacer was the cause.
The CHAPTERS and QUEUE branches were not modified.
## Screenshot
<img width="1920" height="1080" alt="case1-controls"
src="https://github.com/user-attachments/assets/18f50a87-a373-4f48-a9d4-f3c2a5cb2c00"
/>
## AI or LLM usage
I used Claude to help identify the file containing the bug and to
suggest the fix. I reviewed the diff before committing.
## Description
Adds a setting, Settings->Advanced->"Customize which ratings to
display", which allows for toggling parental, community, or critic
ratings to be displayed in the quick details.
This doesn't apply to discover yet.
### Related issues
Related to #1020
### Testing
Emulator
## Screenshots
### Just community rating
<img width="1280" height="720" alt="ratings Large"
src="https://github.com/user-attachments/assets/d6f9ad8a-6cc5-4680-9d2f-66d16a44b41e"
/>
## AI or LLM usage
None
## Description
This PR adds a new method of protecting a user profile when switching to
it: server login. When enabled, switching to the user requires
authenticating with the server again via Quick Connect or username &
password. This is basically the same as the official client.
### Related issues
Closes#321
Related to #1330 - might cause merge conflicts
Related to #1287 - definitely causes merge conflicts
### Testing
Emulator with various uses cases including auto sign-in, switching
between server login & PIN protection, and removing the protection
## Screenshots
N/A, the login looks identical as adding a new user
## AI or LLM usage
None
## Description
This PR has some UI/UX updates for live tv epg/guide.
- Show program images, if provided by the EPG source
- Show channel name under logo
- Show & set favorite channels
- Adjusts the header to match the rest of the app (font, padding, etc)
Additionally, navigation around the guide has been rewritten. It is more
efficient and feels a bit more smooth to scroll around.
### Related issues
Closes#529
### Testing
Emulator mostly
## Screenshots
<img width="1280" height="720" alt="epg_new Large"
src="https://github.com/user-attachments/assets/c53c01be-9209-4c59-82cb-47f268811d3e"
/>
## AI or LLM usage
None
## Description
Adds an option under Settings->Customize home page->Settings, "Continue
watching/Next up click behavior":
- When disabled (the default), clicking goes to the item's details page
- When enabled, clicking starts playback for the item from its resume
position
This only applies to the continue watching, next up, or combined rows.
All other rows go to the details page no matter what.
### Related issues
Closes#1007
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes a small UI glitch when the subtitle label is too long, it would
slightly increase the high of the labels. This is especially noticeable
on series details if some episodes have the long label and some don't
because the episode row will bounce up and down a few pixels.
Also moves the labels closer together and changes "Disabled" to "None"
when subtitles are turned off.
### Related issues
None
### Testing
Emulator & compose previews
## Screenshots
### Before
The subtitle label is slightly taller than the others
<img width="528" height="36" alt="image"
src="https://github.com/user-attachments/assets/32d322bc-11cc-4238-840c-03ae41b069d3"
/>
### After
The ellipse doesn't look great, but there's limited space and looks
better than being cut off.
<img width="510" height="37" alt="image"
src="https://github.com/user-attachments/assets/40af707c-3121-40c0-a978-60781d9704d6"
/>
## AI or LLM usage
None
## Description
Adds a card at the end of the rows on the home page and recommended tabs
to "View More" which goes to a grid of the items in the row.
The grid will endlessly scroll all of the available items except for a
few cases where the first 1000 are shown because the server does not
(yet) support pagination: genres, studios, people (added in v12), and
latest grouped tv shows.
Dev notes: This PR also rewrites the `RecommendedViewModel` and
consolidates the previous multiple view models into one.
### Related issues
Closes#1371Closes#1035
Similar to #1198
### Testing
Emulator
## Screenshots
N/A, see #1198
## AI or LLM usage
None
## Description
Improves how search results are displayed:
- Reorders search categories to Movies → TV Shows → Episodes →
Collections, surfacing the most commonly searched content types at the
top.
- Adds a new "Combined search results" preference in Advanced settings →
Search. When enabled, shows all results in a single list instead of
separate categories.
- When Seerr is active, adds Library/Discover tabs to switch between
local library results and Seerr discovery results.
- Hides empty result categories instead of showing "No results"
placeholders.
### Screenshots
<img width="1935" height="1100" alt="Screenshot_20260116_113656"
src="https://github.com/user-attachments/assets/1e29aa3a-3ba2-4f86-ab61-fed683490474"
/>
<img width="1934" height="1095" alt="image"
src="https://github.com/user-attachments/assets/8b93fa38-d4c9-4e72-a997-caa82b4949f0"
/>
<img width="1921" height="1094" alt="image"
src="https://github.com/user-attachments/assets/82aed1a5-810c-413f-a266-6d78490a1534"
/>
### AI/LLM usage
Code auditing and PR description drafting with Claude Code. Also used
Claude Code for the regex.
Fixed a bug with libass integration where the background thread
responsible for drawing the ASS subtitles (AssRenderThread) has been
shut down, but ExoPlayer is still actively running and trying to push
video time updates to it. Added fallback for software rendering on older
android versions that can have broken OpenGl implementation.This should
address much of issue #1049.
## Description
Essentially, debug logs showed repeated:
3145 3920 W MessageQueue: Handler (android.os.Handler) {4ba3c72} sending
message to a Handler on a dead thread
3145 3920 W MessageQueue: java.lang.IllegalStateException: Handler
(android.os.Handler) {4ba3c72} sending message to a Handler on a dead
thread
Essentially this was a lifecycle bug where the libass-android thread was
created, but was detaching from the window, shutting down when it does
so. It was still registered to ExoPlayer, but since there was no thread
to send to, it just repeatedly kept failing, never spinning back up to
display any subtitles. It seems just about any ui change can cause this
detachment, but in this case it seems it was right from the moment the
loading spinner shows up (making libass-android fail before video starts
playing).
Fixed by ensuring TextureView's visibility wasn't changed to invisible,
which ends up destroying the canvas that libass-android attaches itself
to. Hid the canvas using graphicsLayer (so the user can't see it, but
it's still alive and active). Also changed resizing logic to prevent
Android from seeing a 0x0 TextureView as "destroyed".
Since I discovered in testing how terribly my TV's OpenGl was handling
this, I added an additional fallback for Android versions < 9. Older
TV's can fallback to software rendering when using libass-android.
### Related issues
#### Issue #1049 :
**Fixes:**
- the issue where libass-android (when enabled) fails to load any
subtitles in ExoPlayer DirectPlay.
**Not Fixed from issue #1049 :**
- Issues around accented characters.
- Any issues surrounding MPV (I have no way of testing, MPV never worked
on any player for me)
- Fallback mode (libass-android disabled) where subtitles aren't
rendering properly
**Added:**
- While testing, discovered older Android versions have trouble with
reliable OpenGl rendering. Thus I added a fallback for older Android
versions to use libass-android in software rendering mode
(AssRenderType.OVERLAY_CANVAS)
### Testing
- Tested and confirmed flawlessly to work on Android 9 TCL Smart TV
(AssRenderType.OVERLAY_CANVAS)
- Tested on Android 14 Chromecast (AssRenderType.OVERLAY_OPEN_GL).
Slightly less perfect (font not always a match, less fancy shadows) but
most ASS/SSA effects work well. May be worth testing other AssRenderType
options to compare features and performance on other machines.
## AI or LLM usage
Google Gemini Pro in tracing down the cause and fine-tuning the fixes.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
1. Fixes when moving from nav drawer to home page content, focus would
sometimes move up a row
2. Fixes when moving up/down between rows, focus would sometimes move to
the later card (usually the last visible)
The second above is fixed by downgrading `tv-material` to
`1.0.1-alpha01`. Switching to
[`1.0.1-beta01`](https://developer.android.com/jetpack/androidx/releases/tv#tv-material-1.1.0-beta01)
or newer causes the bug. Not clear if its a bug in the library or
Wholphin though.
### Related issues
Fixes#1359
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Directly use "Wholphin" when searching for older APKs to clean up.
On debug builds, the `app_name` string is "Wholphin (Debug)" but the APK
file names is always "Wholphin.apk", so I think the debug build won't
clean them up.
### Related issues
Fixes#1368
### Testing
Testing downloading a few times without this change, applied the change,
and clean up worked
## Screenshots
N/A
## AI or LLM usage
None
## Description
This enhances the PIN entry system by allowing users to input digits
using the numbers 0-9. This is useful for those using a keyboard or
universal remote with a number pad.
### Testing
Tested on Nvidia Shield Pro and Onn 4k Pro
## Screenshots
<img width="3840" height="2160" alt="Screenshot_20260429_083041"
src="https://github.com/user-attachments/assets/9e603bda-f1d3-4268-a04c-b077297fdfac"
/>
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Show the public users (ie "Hide this user from login screens" is
unchecked) if the server has any. Previously, all users had to be
explicitly added by username or quick connect.
This PR also refactors the server & user pages to use modern state flows
and deletes some unused code.
### Related issues
Closes#600
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Instead of adding/removing the `SubtitleView` from composition
sometimes, instead toggle its visibility. This is technically slightly
more efficient. There's also a couple other minor tweaks that make the
subtitles style a little more efficient.
This should also help prevent crash caused by possible a race condition
in libass-android
(https://github.com/peerless2012/libass-android/issues/75).
### Related issues
I think this fixes#1322
### Testing
Emulator mostly
## Screenshots
N/A
## AI or LLM usage
None
## Description
Just a lot of dependency bumps. There are no user facing changes.
Switches to the new Android gradle DSL now that the protobuf plugin is
compatible.
### Related issues
None
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes the delete button not showing on the movie detail page. Also fixes
some edge cases where button may not appear when toggling on the media
management setting.
Adds the delete button to album & artist pages. Previously, it was only
in the context menu.
### Related issues
Fixes#1343
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an issue when changing subtitles during playback if you had
previously explicitly chosen subtitles for that item but not chosen an
audio track.
### Related issues
I don't think there is a specific issue for this
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR simply enables the `largeHeap` flag to request more memory from
the OS.
I am a bit embarrassed that I haven't added this before now, but I had
read that using `largeHeap` was a bandaid, not a solution. So I assumed
Wholphin was doing something incorrect memory-wise. But after crawling
through many, many heap dumps to analyze Wholphin's memory usage, and
after further research, enabling this does make sense for handling high
bitrate, high resolution videos. Furthermore, many other Jellyfin
clients and other video apps like Plex and Prime Video have this
enabled.
So since I am pretty confident that Wholphin doesn't have any memory
leaks, I feel good about enabling this.
### Related issues
I believe this will fix many of the bug reports about crashes during 4K
playback, but it's not a cure-all since it is up the device & OS to
allocate the max memory.
Fixes#333Fixes#826Fixes#1189
### Testing
Shield 2019, Fire 4k Max
## Screenshots
N/A
## AI or LLM usage
None
## Description
Preload ~6 rows worth of images on library grid pages. So depending on
device CPU & memory, most cards do not show a placeholder image when
scrolling unless you press and hold up/down to scroll as fast as
possible.
There's a couple other minor optimizations on the grid code as well, but
nothing huge.
### Dev notes
This PR makes the `cacheWindow` for pre-rendering cards applicable to
their images. Previously, images would not be fetched until the card is
laid out within the window. This was to get the width/height to pass to
the server request. This PR instead calculates the card widths when the
grid itself is laid out and then passes this width into the cards'
composables.
### Related issues
Related to #1246
### Testing
Tested with emulator, shield, fire stick 4k max
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a context menu option to choose the player backend or transcoding
to start playback. This allows for temporarily swapping to another
backend without having to updating the settings.
### Related issues
A couple comments have suggested this, but I don't think there's a
specific feature request
### Testing
Emulator
## Screenshots
<img width="1280" height="720" alt="play_with Large"
src="https://github.com/user-attachments/assets/2a75756f-5d7f-45f6-8ea6-a406f3d7f3e1"
/>
## AI or LLM usage
None
## Description
This PR refactors most of the context menu dialog code moving more of
the display into a Composable. This then allows for embedding the
confirm delete or choose version sub-dialogs. This will allow for easier
future expansion of the context menu.
Now instead of the page using a combination of `DiaglogPopup` &
`buildMoreFor` functions, pages create a `ContextMenu` object and pass
it to a composable.
There's no significant user facing changes.
### Related issues
None
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Shows the overlay (watched, progress bar) for extras appearing as single
items. Grouped extras individually show the overlay when clicking into.
If all of the grouped are played, the group is show with the watched
indicator.
Also very slightly optimizations the UI by offloading the
title/subtitle/imageUrl to object creation time instead of when they're
shown on screen.
### Related issues
Closes#1311
### Testing
Shield testing with different extras
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an bug caused when the home page has an empty row before the last
row and there are non-empty rows later, an `IndexOutOfBoundsException`
would be thrown.
Also removes the loading indicator if an unknown error occurs during a
refresh.
### Related issues
Fixes#1314
### Testing
Emulator & shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes the play & shuffle buttons on music library tabs.
They previously attempted to play the audio via the video playback
method. This PR adds the ability to override the buttons and does so for
music libraries.
### Related issues
Fixes#1304
### Testing
Emulator, compared results against the web UI
## Screenshots
N/A
## AI or LLM usage
None
## Description
Moves the five skip segment settings to their own page. So there's now a
single "Skip behavior" that you click to bring up the settings for the
five. I doubt many people change these frequently, so moving them to a
new page de-clutters the settings page.
### Related issues
N/A
### Testing
Emulator
## Screenshots
### Advanced settings
<img width="1280" height="720" alt="single_setting Large"
src="https://github.com/user-attachments/assets/673555d5-109c-4c43-9073-f51572bb949d"
/>
### Skip segment specific settings
<img width="1280" height="720" alt="skip_settings Large"
src="https://github.com/user-attachments/assets/dae9d0bd-7340-4731-b561-b7b4e74d9d4f"
/>
## AI or LLM usage
None
## Description
Fixes some issues when using an external player:
- Send playback started before launching so the last played date is
updated
- Only use VLC's position if >0 since it returns 0 when playback
completes
### Related issues
Follow up to #1256
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Previously the home page always tried to load the "watching rows" (Next
up, Continue watching, or combined) first. This meant that even if the
user's customized home page did not put these rows at the top, they
would always be listed first anyway.
This PR fixes this so rows on the home page are always listed in the
right order.
### Related issues
Fixes#1275
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a row for season level extras on the series overview page below the
people rows. The extras will update when switching between season tabs.
Like elsewhere in the app, an single extra of a type is show inline and
multiple extras of the same type open a grid when clicked.
### Related issues
Closes#504
### Testing
Emulator
## Screenshots
<img width="1280" height="720" alt="season_extras Large"
src="https://github.com/user-attachments/assets/bc62bef4-66c8-4412-9eca-ec0105d0d0fa"
/>
## AI or LLM usage
None
## Description
If the Wholphin OS screensaver starts before Wholphin (the app) is
opened, a default image loader is used. Then when Wholphin is opened, it
will crash when trying to set the app's image loader.
This PR adds configuring the image loader to the screensaver as well. It
is safe to do this multiple times. It's only an error to try to replace
the default image loader.
### Related issues
None
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes a crash if there is a network error when starting the screensaver.
Instead, a black screen with an error message shows. The error message
fades in/out to different parts of the screen. This applies to both
in-app and OS screensavers.
### Related issues
Fixes#1247
### Testing
Emulator w/ simulated errors
## Screenshots
N/A
## AI or LLM usage
None
## Description
Ports two changes in the official jellyfin android TV app here:
- https://github.com/jellyfin/jellyfin-androidtv/pull/5240
- https://github.com/jellyfin/jellyfin-androidtv/pull/5186
The code changes are copied under GPL-2.0 from the above pull requests.
### Related issues
Closes#1260
### Testing
Emulator mostly
## Screenshots
N/A
## AI or LLM usage
None
---------
Co-authored-by: Niels van Velzen <git@ndat.nl>
Co-authored-by: clams4shoes <prestidgeandrew@gmail.com>
## Description
Adds a new playback "backend" to play media in another app such as VLC.
By default, Wholphin will use the system default app. Typically if you
haven't chosen one, a dialog will show to pick which app. You can also
set a specific external app to use in Wholphin independent of the system
default.
Currently, this only support single item playback, ie playing next up
episodes is not supported yet.
Also since there is no standardized way to send resume position,
external subtitle info, etc, Wholphin makes a best effort. VLC,
mpv-android, & MX-Player are specifically tested/supported.
### Related issues
Closes#85
### Testing
Emulator & nvidia shield w/ VLC, mpv-android, & MX-player
## Screenshots
<img width="543" height="407" alt="image"
src="https://github.com/user-attachments/assets/ab37d41e-2909-40ed-b537-191ebb54d979"
/>
## AI or LLM usage
None
## Description
Issue 943 where trickplay images were misaligned with seek preview
mages. I made a post in issues but figured i'd go ahead and make the
pull request.
per gemini:
In SeekPreviewImage.kt, the code calculates which tile in the grid to
display based on the current timestamp:
val x = (tileIndex % trickPlayInfo.tileWidth) // x position within tile
grid
val y = (tileIndex / trickPlayInfo.tileHeight) // y position <-- BUG IS
HERE
To find the correct row (y) in a grid, you are supposed to divide the
current index by the width of the row (the number of columns), not the
height.
Why this breaks: Let's assume a standard trickplay grid that is 10
images wide (tileWidth = 10) and 5 images tall (tileHeight = 5).
If you are looking at the 12th image (tileIndex = 11), it should be on
Row 1 (the second row).
The code calculates y = 11 / 5 = 2. It skips to Row 2!
Because it divides by the smaller height value, the y coordinate
increases much faster than it should. This causes the preview to
visually "jump" down to future rows prematurely, and then snap back as
the x coordinate resets, causing the exact "flashing back and forth" you
saw with the credits and intros.
### Related issue
Fixes#943
### Testing
Tested in Android TV (Android 9) and it does indeed display proper seek
previews using trickplay images.
## AI or LLM usage
Gemini Pro was used to find and fix this bug.
## Description
Adds an option to never show next up. This means when an episode
completes, the app always returns to the previous page instead of
suggesting the next episode.
### Related issues
Closes#1100
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a line to the overview/media info dialog with the date when the
item was last played.
### Related issues
N/A
### Testing
Emulator
## Screenshots
<img width="1280" height="720" alt="last_played Large"
src="https://github.com/user-attachments/assets/1c38141d-1000-43d0-bf28-a0845edfacc2"
/>
## AI or LLM usage
None
## Description
Add better ASS/SSA support to ExoPlayer by using
[`libass-android`](https://github.com/peerless2012/libass-android).
This is enabled with the "Use libass for ASS subtitles" advanced
settings for ExoPlayer. It is enabled by default.
### Related issues
Related to #22
Replaces reverted #569Fixes#1107
## Description
Adds a small glow to the border of the segment skip buttons, ie "Skip
Intro", etc. Since this button appears as focused immediately, there's
no contrast and it can be missed sometimes. So this glowing border draws
the eye a bit better.
Also a small change to reduce the minimum skip forward amount to 5
seconds .
### Related issues
Skip forward: #1203
### Testing
Emulator. But this needs some "real world" testing on a TV, too, so this
may need tweaking
## Screenshots
https://github.com/user-attachments/assets/5488d468-aee4-4448-a823-6326ccb003da
## AI or LLM usage
None
## Description
Updates to use the latest wholphin-extensions which includes
https://github.com/damontecres/wholphin-extensions/pull/2
The crash in #1239 occurs if "AV1 software decoding" is enabled and
seemingly only if the app was installed via APK, including app stores.
### Related issues
Fixes#1239
### Testing
Emulator, nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a variant of Wholphin targeted for the Amazon Fire TV App store.
### Related issues
See #1192 for more details
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Add indicator to mark the chosen version on the choose version dialog
- Add an option for album's context menu to add it to the now playing
queue
- Fix how season cards refresh on the series detail page so the images
don't glitch
### Related issues
Related to #1220
### Testing
Emulator mostly
## Screenshots
The indicator shows which version will play, whether explicitly chosen
by the user or implicitly picked by Wholphin.

## AI or LLM usage
None
## Description
Ignore dependencies from `wholphin-extensions` & the Jellyfin SDK. These
dependencies should be updated manually.
### Related issues
N/A
### Testing
N/A
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds context menu options for "Remove from continue watching" and
"Remove series from next up"
### Remove from continue watching
This marks the item as unplayed so it will not be shown on the Continue
Watching row anymore. It may still show on a Next Up or combined row
though.
### Remove series from next up
This first does the same as "Remove from continue watching" above. But
it also stores the series' ID in your display preferences as removed.
Wholphin will filter out this series from appearing in the Next Up row
until you watch _any_ episode in the series in the future.
This only works for Wholphin clients!
There's a dialog to view which series have been removed and to re-add
them manually. Settings->Customize Home->Settings->View removed next up
### Related issues
Closes#906
### Testing
Mostly emulator testing
## Screenshots
## AI or LLM usage
None
## Description
Adds a tab for Studios (TV networks) to TV libraries. This displays a
grid of all of the studios. Clicking one displays the series produced by
the studio.
Additionally, adds a filter to the library tab to filter by studio. You
can select multiple studios, but its not the best UX since there will
likely be many studios (I have almost 200).
Finally, you can add a home row for studios in a library.
### Related issues
Closes#1002
### Testing
Mostly manual testing via emulator
## Screenshots
### Studios tab for a library

### Filter drop down for studios

### Updates series details
Shows the studio above the genres

## AI or LLM usage
None
## Description
Show the watched indicator and favorite icon on music videos on both the
artist or album detail pages.
### Related issues
Fixes#1206
### Testing
Quick emulator testing
## Screenshots
N/A, same indicators as the rest of the app
## AI or LLM usage
None
## Description
Instead of applying a code patch, this PR uses build time [product
flavors](https://developer.android.com/build/build-variants#product-flavors)
to define the existing default and an app store variant. This is more
robust than maintaining a patch file and making sure it is applied
before building.
The app store variant applies the same code changes (removing install
permission, requiring leanback, & setting `UpdateChecker.ACTIVE`), but
via manifest placeholders/merging and `BuildConfig` entries.
There are no user facing changes.
### Related issues
None
### Testing
Built the variants and verified manifest changes via aapt
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a card at the end of the rows on the discover page/tab to view more
results. When clicking this card, it goes to a grid view of more results
starting focused on the next one.
### Dev notes
This PR modifies `ApiRequestPager` to move most of its functionality
into an abstract class, `RequestPager`. And then add a new concrete
class, `DiscoverRequestPager`, for Seerr API paging. `ApiRequestPager`
remains for Jellyfin paging.
### Related issues
Related to #1035
### Testing
Emulator
## Screenshots


## AI or LLM usage
None
<!-- By submitting this pull request, you acknowledge that you have read
the [contributing
guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md,
including the AI/LLM policy, and [developer's
guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md)
-->
## Description
<!-- Describe the changes in detail -->
Make alpha channel and background change in Banner Card more
recomposition friendly.
Split from #1152
### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->
### Testing
<!-- Describe how this change was tested and on what device(s) -->
Tested on a emulator
It's observed when moving from TV series episode card to EpisodeFooter
and back
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
| Pre | Post |
|--------|--------|
| <img width="395" height="152" alt="Screenshot 2026-04-02 at 08 41 40"
src="https://github.com/user-attachments/assets/b6554425-8af5-4e29-a866-3e4f6a7be473"
/> | <img width="406" height="296" alt="Screenshot 2026-04-02 at 08 44
56"
src="https://github.com/user-attachments/assets/511fc6df-c860-47eb-aec8-57c400dba83c"
/> |
## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code
and how you tested it -->
Gemini in AS was used for repetitive code completion
## Description
Refactors the movie page & view model to use a `StateFlow` instead of
many `LiveData` objects. This is in keeping with best practices.
There are no user facing changes.
### Related issues
None
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
When Wholphin is updated, it will show a short toast message. This works
for both app store and side loaded installs.
You can now click on the "Installed version" in settings to fetch the
release notes for the current version.
### Related issues
Closes#1058
### Testing
Emulator
## Screenshots

## AI or LLM usage
None
## Description
Fixes the list of supported audio types when playing music.
This fixes allows for playback of audio formats like ALAC, PCM/WAV,
Vorbis, etc.
### Related issues
Fixes#1168
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Always show card overlays (ie Movie vs TV Show) for discover/Seerr
cards
- Show card overlays (ie favorite and watched status) for search result
cards
- Restore showing home page refresh indicator
### Related issues
Closes#1141
Closes
https://github.com/damontecres/Wholphin/issues/702#issuecomment-4114775539
### Testing
Emulator
## Screenshots
N/A, no new UIs, just adding existing ones
## AI or LLM usage
None
<!-- By submitting this pull request, you acknowledge that you have read
the [contributing
guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md,
including the AI/LLM policy, and [developer's
guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md)
-->
## Description
<!-- Describe the changes in detail -->
I've look at the app using Layout inspector to see if there are any
recompositions that shouldn't be happening while navigating normally in
the app. I've noticed two issues
1. Every BannerCard was being recomposed on every cursor move, which
added a lot of overhead to home screen navigation
2. Tab row in the Series View was being recomposed while navigating
across episodes in given season.
### Related issues
<!-- If this is a new feature or a change, there must be a discussion in
an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug,
including steps to reproduce -->
Performance on the home screen is lacking, especially on low end
devices, while this isn't a full fix, it will improve the navigation, as
the app will be making significantly less work.
Probably will fix https://github.com/damontecres/Wholphin/issues/1043
### Testing
<!-- Describe how this change was tested and on what device(s) -->
Using Layout Inspector built-in recomposition counter and highlights
it's easy to see components which recompose while they shouldn't. I've
tested it on a 4K TV emulator on my M1 Macbook Pro
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
main:
<img width="402" height="508" alt="image"
src="https://github.com/user-attachments/assets/6aaac6af-50d6-4f21-b465-e78bccf7e985"
/>
my branch:
<img width="401" height="563" alt="image"
src="https://github.com/user-attachments/assets/1034c0e2-9a40-4a05-8a07-f30b2e10301d"
/>
same navigation, on the same content. (down, right x4
## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code
and how you tested it -->
Gemini built in to Android Studio helped me identify non-stable parts of
the code. It also helped me understand and implement fixes in the
HomePage code.
I've took the time to check manually that it didn't alter any behavior,
and verified the fixes using Layout Inspector and recomposition counter
## Description
This PR moves the native components (ffmpeg decoder, av1 decoder, &
`libmpv`) to https://github.com/damontecres/wholphin-extensions. They
can be retrieved as gradle dependencies now.
This allows for contributors to more easily include these extensions
when building Wholphin locally.
There are no user facing changes in this PR.
Note: the kotlin code for interfacing with `libmpv`, ie `MPVLib` &
`MpvPlayer`, is still in this repo for now.
### Related issues
Related to #828
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Don't recursively query collections for the home page rows. This
prevents episodes within tv shows from being shown.
### Related issues
Fixes https://github.com/damontecres/Wholphin/issues/1148Fixes#1147
### Testing
Emulator & API
## Screenshots
N/A
## AI or LLM usage
None
## Description
A brain dump documenting many classes and functions throughout the app.
Definitely does not document everything, but covers most of the major
components. This should be useful for new contributors.
There is a small amount of code clean up too.
There are no user facing changes.
### Related issues
N/A
### Testing
N/A
## Screenshots
N/A
## AI or LLM usage
None
## Description
Ensure the new pause indicator from #1144 is the right color for all
devices.
### Related issues
Related to #1144
### Testing
Emulator & shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Instead of displaying the full playback overlay when pausing via a
button press, just show a short pause image in the center of the screen.
Any button press that will pause triggers this including a remote Pause,
Pause/Play, or one-click pause Enter.
I decided to make this the standard behavior instead of a toggle because
I think if the user intends to do something with the overlay (change
subtitles, audio, etc), it's still straightforward to open the overlay,
click the pause button (which is focused first), and then move to the
desired option versus assuming click-to-pause will open the overlay so
the user can move to the desired option.
### Related issues
Closes#990
### Testing
Emulator
## Screenshots
[pause_osd.webm](https://github.com/user-attachments/assets/8f908164-2a50-43f9-9e13-a463d2914e42)
## AI or LLM usage
None
## Description
Use localized date & time formatting such as using 24 hour clock or
Day-Month-Year date formatting. The localization is based on the
device's locale/language setting. Some pages require reloading if you
switch the locale while Wholphin is running.
Format bitrates using SI 1000 magnitudes instead of 1024. File size
still uses 1024 but formats with "MiB"/"GiB" suffixes now. This aligns
with the server web UI and is more in line with standard practices for
display as well.
Fixes the playback overlay layout being messed up by extremely long
episode names.
Finally, adds jumping by letter using keyboard letter keys on grids if
letter jumping is enabled. I doubt many users have a full keyboard
attached to their TV, but this saves me time using the emulator when I
need to repeatedly open the same item for testing.
### Related issues
Fixes#1132
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Updates the playback overlay so that individual component animate in
more deliberately.
For example, the logo & clock slide in from the top instead of the
middle of the screen. And the trick play preview now expands and shrinks
vertically.
### Related issues
I thought there was an issue about this, but I can't find it
### Testing
Emulator, nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds better error handling when loading home page rows. For example,
this fixes loading errors if a playlist row is configured, but the
playlist is deleted.
Also fixes an issue from #1116 where changing users doesn't go the home
page.
### Related issues
Related to #1124
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Simply fetches the tv show's seasons when returning the details page.
This ensures the unwatched counts are updated.
### Related issues
Mentioned as a bug in
https://github.com/damontecres/Wholphin/issues/767#issuecomment-4071852502
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This fixes some issues restoring the app state after it is killed by the
system, such as to free up memory while the screensaver is showing.
### Dev notes
Reproduce:
1. Start playing media from beginning
2. Pause at 10 minute mark
3. Wait for screensaver to start
4. Press a button
5. Media starts from beginning
Note: It may take a few iterations of 2-4 before the OS destroys the
app.
If the app is killed during playback, ideally Wholphin restores state to
_previous_ page, not playback since restoring playback is complex.
Before this PR, the back stack was maintained by Compose's
`rememberSerializable` and would be restored _after_ the
`PlaybackLifecycleObserver` cleans up the playback destination.
This meant that the app would be restored, from scratch, to the playback
page. The `PlaybackViewModel` would be initialized from scratch as it
was originally called, ie play from beginning.
The fix is to save and restore the back stack outside of Compose.
### Related issues
Fixes#767
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
A few improvements to the ffmpeg decoder & mpv build scripts. Mainly
changes the dependency fetching so the scripts are more idempotent,
though some rebuilds still always occur.
### Related issues
Related to
https://github.com/damontecres/Wholphin/pull/1106#issuecomment-4077678690
### Testing
Local builds
## Screenshots
N/A
## AI or LLM usage
None
## Description
Several small tweaks:
- Always put screensaver logo on left instead of random
- Disable playback speed option if audio is being passed through
- When opening playback settings such as speed or scale, focus on the
current value instead of top of the list; doesn't apply to subtitles so
it's easy to toggle them off
- If the user has a preferred subtitle language, show tracks in that
language first/top
- Add toast message after creating or adding to a playlist
### Related issues
Playback speed audio pass through: #164
### Testing
Emulator & shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes debug install updates from trying to install the release APK and
use debug ones instead.
### Related issues
Fixes#1098
### Testing
Unit tests
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR makes changes to how images are fetched for discover/Seerr
items.
If the discovered item is available on the Jellyfin server, the images
will be fetched from the Jellyfin server.
Then if the Seerr server has image caching enabled, the images will be
fetched from the Seerr server. Otherwise, images are fetched from TMDB .
TMDB was the previous behavior for all images.
### Dev notes
This PR also updates the Seerr server URLs stored in the database, so
it's not backwards compatible.
### Related issues
Fixes#1079
### Testing
Emulator against Jellyseer 2.7.3 & Seerr 3.0.1
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR simplifies refreshing the episode data when returning to the
series overview page and reduces the number of queries.
Also fixes a race condition when refreshing a single item in the an
`ApiRequestPager`.
### Related issues
Related to #767
### Testing
Emulator, but will need more testing during actual usage
## Screenshots
N/A
## AI or LLM usage
None
## Description
During a slideshow of images, the viewpoint would resize to largest
image resolution and images with smaller dimensions would be scaled down
and centered with black bars.
This might be a Coil bug, but I'm not 100% sure yet. This PR works
around the issue by scaling all images to the display resolution instead
of using `Size.ORIGINAL`.
However, we want `Size.ORIGINAL` while zooming so it doesn't pixelate if
the original image's resolution is larger than the display's.
### Related issues
Fixes
https://github.com/damontecres/Wholphin/issues/835#issuecomment-4035005433
### Testing
Emulator with 1920x1080 & 800x1600 images
## Screenshots
N/A
## AI or LLM usage
None
<!-- By submitting this pull request, you acknowledge that you have read
the [contributing
guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md,
including the AI/LLM policy, and [developer's
guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md)
-->
## Description
The formatter for audio codecs did only match the profile of high
definition DTS streams against "DTS:HD" or "DTS:X". At least for DTS:HD
there are also cases where the profile contains "DTS-HD" with "-"
instead of ":".
Made the matcher more generic by allowing both "-"" or ":"" and also
checking if the profile contains "MA" for Master Audio tracks.
This will now correctly display the following high definition DTS types
in the UI:
- DTS:HD
- DTS:HD MA
- DTS:X
**Note:** This is my first public pull request ever, let me know if I
did it wrong :)
### Related issues
Relates to (but does not fix):
https://github.com/damontecres/Wholphin/issues/1057
### Testing
Tested on a Fire TV Cube Gen 3 with multiple files containing DTS HD
audio tracks.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Fixes a race condition saving preferences. It would be triggered if you
have "Remember selected tabs" enabled and either: 1) changed settings
while on a library grid page or 2) changed settings on a page navigated
_from_ a library grid page and then returned to the grid
### Related issues
I think this will fix#1075
### Testing
Emulator following the steps above
## Screenshots
N/A
## AI or LLM usage
None
## Description
A few fixes
- Better crossfade for photo slideshow
- Fix "Go to" in context menu for photos
- Fix certain home video/photo libraries not working for photos
- Fix removing seerr server in some cases
### Related issues
Closes#835Fixes#1069
Addresses
https://github.com/damontecres/Wholphin/issues/634#issuecomment-4018580518
### Testing
Emulator mostly
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a dialog to select individual seasons for a TV show when requesting
it via Discover/Jellyseerr
If a request is already pending, it can be updated. If there's more than
7 seasons, a second submit button is added at the bottom of the dialog
for easier access.
Also, adds a button from Jellyfin series details to its Discover
equivalent and added a request button for partially available series to
request more seasons.
### Related issues
Closes#961
### Testing
Emulator & Jellyseerr 2.7.3
## Screenshots

## AI or LLM usage
None
## Description
Fix a screensaver crash after the last item
Also adds some extra error checking
### Related issues
Fixes#1053
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
Unfortunately, there seem to be issues with switching to
`libass-android`, so reverting those changes so I can cut a release and
troubleshoot later.
Reverts damontecres/Wholphin#569
## Description
Fixes a few issues with the in-app & OS screensavers
- Make sure clock is updating for in-app screensaver
- Fix crash if app is force quit before the OS screensaver starts
### Related issues
Fixes#1045
### Testing
NVIDIA shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Add better ASS/SSA support to ExoPlayer by using
[`libass-android`](https://github.com/peerless2012/libass-android).
This is enabled with the "Use libass for ASS subtitles" advanced
settings for ExoPlayer. It is enabled by default.
### Related issues
Related to #22
## Description
This PR improves the logic used to choose a display mode when refresh
rate and resolution switching are enabled.
As before, matching the refresh rate is prioritized over picking the
resolution. For example, with the display modes of 1080@30 & 720@60 for
a 720@30 video, 1080@30 is used because the frame rate exactly matches.
### Related issues
Fixes#1036
### Testing
Unit tests, added test cases for more situations
## Screenshots
N/A
## AI or LLM usage
None
## Description
A follow up to #1014 to add the delete option in more context menus.
Also improves the "reaction" logic which updates previous pages from
deletions on later pages.
### Related issues
Addresses
https://github.com/damontecres/Wholphin/pull/1014#issuecomment-3987205355
Related to #104
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes some issues with Jellyseerr login. Also makes error message more
clear by showing the URLs that were tested and the error that occurred.
Also fixes an issue where you could not remove a Jellyseerr server if it
was previously configured but can no longer connect.
### Related issues
Fixes#1028
Should fix#1022
Related to #747
### Testing
Emulator, tested adding and removing valid & invalid IPs
## Screenshots
N/A
## AI or LLM usage
None
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
|
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
| action | major | `v6` → `v7` |
---
### Release Notes
<details>
<summary>actions/upload-artifact (actions/upload-artifact)</summary>
###
[`v7`](https://redirect.github.com/actions/upload-artifact/compare/v6...v7)
[Compare
Source](https://redirect.github.com/actions/upload-artifact/compare/v6...v7)
</details>
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/damontecres/Wholphin).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4zNi4yIiwidXBkYXRlZEluVmVyIjoiNDMuMzYuMiIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
## Description
Fixes several small issues throughout the app
- Adds 1440p as a resolution option
- Removes the delay when d-pad seeking on the seek bar & reduces it
slightly when controls are hidden
- Fixes padding one home customize pages
- Adds a title to the search for dialog
### Related issues
Fixes#1017
### Testing
Emulator mostly
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR is mostly a proof-of-concept for setting up automated UI
testing. The tests use Roboelectric so they can run without an emulator.
There is also a sample instrumented test for running on emulators or
devices for future tests.
It adds two basic automated UI tests for adding a server. These are sort
integration tests because they use the actual implementations for the
view model and services like `ServerRepository` and `JellyfinServerDao`.
Also, moves the bulk of `MainActivity`'s compose code into a function
for future tests.
### Related issues
N/A
### Testing
Local testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a setting to enable "Media management" which adds a context menu
item to delete media. This is accessible using either the More button or
long clicking.
You can delete episodes, seasons, series and movies/videos.
If order for the option to appear the current user must have server-side
settings for "Allow media deletion from" enabled for the library and the
in-app setting under Settings->Advanced Settings->"Show media management
options".
### Related issues
Closes#104
### Testing
Emulator
## Screenshots
### Context menu

### Confirmation

## AI or LLM usage
None
## Description
- Aims to further optimize performance for the suggestions fetching
### Related issues
Fixes#833
### Testing
Android Emulator
NVIDIA Shield Pro 2019
## Screenshots
N/A
## AI or LLM usage
AI being used to assist with updating/creating unit tests
## Description
- Adds hold-to-seek acceleration for left/right DPAD input during
playback skip handling.
- Reuses one shared, duration-aware acceleration profile for both
playback DPAD and seekbar skipping.
- Extracts acceleration logic into a shared helper to keep playback and
seekbar behavior consistent.
- Slows the acceleration ramp (about one-third) to improve control
during longer holds.
- Preserves tap-on-release behavior: single taps seek on key release,
while holds seek on repeated key-down without an extra release step.
- Normalizes unknown/unset duration values to use the shortest-content
acceleration profile.
- Added unit coverage for shared seek acceleration math to prevent
accidental regression in ramp thresholds.
### Related issues
Supersedes #846
Incorporates #784Closes#522
### Testing
Tested on Nvidia Shield Pro 2019.
- Quick left/right tap still performs normal skip behavior.
- Holding left/right seeks repeatedly with progressive acceleration.
- Seekbar hold uses the same acceleration profile as playback hold.
- Releasing after a hold does not trigger an extra seek step.
## AI or LLM usage
Codex 5.3 and Claude Opus 4.6 were used for implementation, debugging,
and cross-review. Changes were manually validated on device.
---------
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Changes the animation for showing/hiding the header title/buttons on
grid pages.
This eliminates the awkward jump the grid does to fill the screen.
Instead it smoothly resizes.
### Related issues
Fixes the second item in #221
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Restores corner text for episode cards
- Always show end time on playback overlay instead of it being
controlled by the "Show Clock" setting
- Better logic to determine person's role in media
### Related issues
Fixes#1010Fixes#1004Fixes#975
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Catch network exceptions when querying to populate the nav drawer
- Perform screensaver work on IO thread
- Fix possible crash if screensaver service runs while regular activity
is destroyed
- Fix genre images & backdrop images not loading
This PR also adds an Jellyfin SDK `ApiClient` wrapper to push all
network calls onto the IO thread. And tries to use a more strict
network-on-main policy for debug builds.
### Related issues
Fixes#994Fixes#1001
Should fix#1003
### Testing
Emulator with simulated exception throws
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds both an optional in-app and Android TV OS (DreamService)
screensaver
Has settings for:
- Starting delay, options from 30s to 1 hour
- Duration to show each image, 30s to 2 minutes
- Show clock (separate from app-wide clock)
- Toggle animated zooming
- Set a max age rating to limit what content is shown
- Included media types: movies, tv shows, and/or photos
By default only Movies & TV Shows are included. But if you have like
family albums on your server, you could enable Photos too.
There's also an button to start the screensaver so you can see what it
looks like.
### Related issues
Closes#230
### Testing
Emulator, Onn 4k pro, nvidia shield using both in-app and OS
screensavers
## Screenshots

## AI or LLM usage
None
## Acknowledgements
Some of the skeleton code for setting up the `DreamService` with the
right lifecycle was copied and modified from the [official app's
implementation](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/integration/dream/DreamServiceCompat.kt).
## Description
A few more home page fixes
* Focus issue between settings & preset buttons
* Fix default genre card size for "Wholphin Default" preset
* Cache genre image urls for 2 hours so that they don't change on every
refresh
### Related issues
Related to #399
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| [actions/checkout](https://redirect.github.com/actions/checkout) |
action | major | `v5` → `v6` |
---
### Release Notes
<details>
<summary>actions/checkout (actions/checkout)</summary>
### [`v6`](https://redirect.github.com/actions/checkout/compare/v5...v6)
[Compare
Source](https://redirect.github.com/actions/checkout/compare/v5...v6)
</details>
---
### Configuration
📅 **Schedule**: Branch creation - At any time (no schedule defined),
Automerge - At any time (no schedule defined).
🚦 **Automerge**: Disabled by config. Please merge this manually once you
are satisfied.
♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.
🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.
---
- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/damontecres/Wholphin).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNi41IiwidXBkYXRlZEluVmVyIjoiNDMuMjYuNSIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
## Description
This PR makes it so when you open the playback overlay and move down to
the "Chapters" row, the first focused chapter will be the current one
instead of always the first one.
### Related issues
Closes#695Closes#951
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Play theme songs for collections if available
### Related issues
Closes#711
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Weblate's Android string parser (aka
https://translate.codeberg.org/projects/wholphin/) doesn't support
direct translations of string arrays.
Many of Wholphin's preference options are defined in string arrays and
therefore could not be translated. So this PR implements the [suggested
workaround](https://docs.weblate.org/en/latest/formats/android.html) to
move the string into individual entries and reference them in arrays.
Also moves some hardcoded strings into `strings.xml` so they can be
translated. Note: virtually all error messages are still hardcoded, but
they are only for exceptional cases, so it's probably not a huge
problem.
A minor bug where the backdrop would show briefly after switching users
is fixed.
### Related issues
Fixes#957
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a preset for "Episode thumbnails" which shows episodes using their
Primary image (ie a thumbnail or screenshot of the episode).
The previous "Thumbnails" preset is renamed "Series Thumbs" because it
uses the episode's Series Thumb image.
Also fixes a bug where toggling "Use series" might not update the home
page preview automatically.
### Related issues
Fixes#940
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes clicking on a genre card added to the home page
### Related issues
Fixes#952
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR changes the padding on the SeriesOverview to align it with the
updated layout on the Home, MovieDetails and SeriesDetails pages.
There is also a small change to fix an incorrect spacing on the
SeriesDetails, and start padding added to the ItemRow to bring it into
alignment with other text elements on the page and ensure consistent
spacing from the navdrawer
### Related issues
I believe this is the final PR to close out
[704](https://github.com/damontecres/Wholphin/discussions/704)
### Testing
Tested changes on both the emulator and physical tv
## Screenshots
Home screen showing the ItemRow alignment
<img width="1920" height="1080" alt="home"
src="https://github.com/user-attachments/assets/b338e2b4-2c53-45a1-b532-544263632230"
/>
SeriesOverview
<img width="1920" height="1080" alt="Screenshot_20260221_200450"
src="https://github.com/user-attachments/assets/51710ade-5155-41d9-8b89-0678c71e0c76"
/>
## AI or LLM usage
AI was used to identify the current padding and spacing values
## Description
Just enables showing and playing music videos as if they were regular
videos. Basic metadata is shown as well.
Navigation isn't great because a music video library goes by folders
which don't necessary have the best UI choices.
### Related issues
Related to #267
### Testing
Checked playback on the emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Adding a background for the Settings stickyHeader. This helps with
visibility and prevents overlap with the menu items when scrolling.
### Related issues
In the Settings screen, the header can overlap the text of the menu
items when scrolling down. This is a minor UI bug only and does not
affect functionality at all, but the fix seemed easy and harmless so I
figured I'd give it a go.
### Testing
Tested manually on TCL model 50S434 running Android TV. Tested manually
using AVD emulation on Television (1080p) and Television (4K) preset
devices. Reproduced the problem on all 3 devices and confirmed that my
fix works across all.
## Screenshots
Before:
<img width="1920" height="1080" alt="before-settings-fix"
src="https://github.com/user-attachments/assets/15f9916d-2583-4dc4-8302-cb1f3a395b1a"
/>
After:
<img width="1920" height="1080" alt="after-settings-fix"
src="https://github.com/user-attachments/assets/3f8781aa-7063-4eb4-bf4a-95ff34b9b1a1"
/>
## AI or LLM usage
None
---------
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Several Fire TV devices natively support H264/AVC High 10 Profile Level
5.2 according to [their
specs](https://developer.amazon.com/docs/device-specs/device-specifications-fire-tv-streaming-media-player.html),
but the `MediaCodecInfo` doesn't return this support.
So this PR adds an override for the known devices that support this
which prevents unnecessary transcoding.
### Related issues
Closes#786
### Testing
Verified on a Fire TV 4k Max 2nd Gen
## Screenshots
N/A
## AI or LLM usage
None
## Description
Show Jellyseerr connection errors in settings. If an error occurred
trying to login, in setting it will show "Server error" and clicking
gives more error info and option to remove the server.
### Related issues
Fixes#907
### Testing
Emulator with simulated errors
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds some metadata about playing media for consumption in remote control
apps such as Google Home or others.
### Related issues
Partially address #889, doesn't add explicit controls yet, but Google
Home works
### Testing
Nvidia shield + Google Home app
## Screenshots
iOS Google Home app:

## AI or LLM usage
None
## Description
Hopefully this is the last round of bug fixes for customizing the home
page!
Fixes a possible crash when scrolling after adding a row after resetting
to default or loading remote settings that have more rows than the
original settings
Fixes some incorrect titles
### Related issues
Related to #399 & #803
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a basic way to add a collection or playlist as a row on the home
page.
There's a simple search dialog to find the collection or playlist.
### Related issues
Related to #399 & #803
### Testing
Emulator testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes/improves thumb image handling by falling back to backdrop images
if available. Episodes without a parent thumb/backdrop will fall back to
their primary image.
Also fixes the nav drawer selected item not updating (bug introduced by
#886).
### Related issues
Fixes#642Fixes#914
### Testing
Emulator in various situations with missing thumbs
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR updates cards that have a title and subtitle, so that they are
visually distinct instead of sharing the same font and style.
Titles use bodyMedium typography with semiBold weight.
Subtitles use bodySmall typography with normal weight.
This affects:
- PersonCard
- EpisodeCard
- DiscoverItemCard
- GridCard
- SeasonCard
### Related issues
Related to [735](https://github.com/damontecres/Wholphin/issues/735)
### Screenshots
<img width="960" height="540" alt="Screenshot_20260125_190057"
src="https://github.com/user-attachments/assets/4bbf29a3-3df8-482b-944e-454a9b091e44"
/>
### AI/LLM usage
No LLM usage
## Description
- Smoothly bumps up the title and subtitle text above the seek progress
label when the seek bar is focused to prevent overlap.
- Adds a semi-transparent black backer to the text label that appears
above the seek progress indicator. Helps with visibility when the image
in the playback matches the color of the text. Note: the backer is black
with Alpha of 0.6 for now because I felt that looked good for contrast
across all themes. However, if you prefer it respect a particular theme
color, I'd be willing to make that change.
### Related issues
In the playback screen, when focusing the seek bar on Android TVs (at
least the ones I was able to test), the seek text above the seek bar
will overlap with the media title. This is a purely cosmetic issue in
the UI, but it seemed worth a fix. See before and after images in in the
Screenshots section below
This problem is briefly mentioned in point number 2 of [#528
](https://github.com/damontecres/Wholphin/issues/528)
### Testing
Tested manually on TCL model 50S434 running Android TV. Tested manually
using AVD emulation on Television (1080p) and Television (4K) preset
devices. Reproduced the problem on all 3 devices and confirmed that my
fix works across all.
## Screenshots
Before:
<img width="2046" height="1184" alt="before"
src="https://github.com/user-attachments/assets/64708e13-3fa4-4248-91e3-e203f8ac1f7d"
/>
After:
<img width="2046" height="1183" alt="after"
src="https://github.com/user-attachments/assets/db9d1767-cf93-4153-b13a-9a18fbc89e0c"
/>
## AI or LLM usage
None
---------
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Fixes several issues with home page customization such as
- Displaying wrong episode-specific display options
- Smaller text on cards without images
- Prefer fill scaling for posters, like the old home page
### Related issues
Related to #803
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
An attempt to fix#769
1. Always fetch the `DisplayManager` when switching refresh rates
2. Use the Activity context instead of Application context
### Related issues
Fixes#769
### Testing
I still haven't reproduced #769 myself, but this change works fine on my
shield + LG C2
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes the nav drawer preference reordering & save logic. Also improves
the UI a bit with animations.
It's also now stored in memory and saved when the dialog is dismissed.
### Related issues
Related to #886
### Testing
Emulator mostly
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes an issue where the "Next Up" popup continuously pops up when
dismissed if you have "Show next up during credits/outro" enabled.
### Related issues
Fixes#895
Bug introduced by #884
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Update header when editing a row
- Exclude non-empty rows to prevent scroll lock
- Only save settings locally if they are different from the source
### Related issues
Related to #803
### Testing
Emulator, tested remote & local settings changes
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR has no user facing changes.
Upgrade AGP to 9.x and Gradle to 9.x. Also updates some dependencies as
well.
Note: still using deprecated `android.newDsl=false` because the protobuf
plugin is not yet compatible:
https://github.com/google/protobuf-gradle-plugin/issues/793.
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Improves home page customization for Live TV & Recordings
- Add row for live tv channels
- Add row for "on now" programs
- Clicking on a TV channel or TV program starts playback
- Better name for "Recently recorded" instead of "Recently added in
Recordings"
Also fixes a back stack issue after adding rows
### Related issues
Follow up from #803
### Testing
Emulator
## Screenshots
<img width="663" height="190" alt="image"
src="https://github.com/user-attachments/assets/49d41953-42ab-4621-92f1-b7936aab7e97"
/>
## AI or LLM usage
None
## Description
Customizing the home page (#803) and nav drawer (#886) were developed
independently, so this PR just cleans up somethings so that those two
changes work together.
This PR restores the default home page when there is no customization to
be the same as before: continue watching/next up row(s) followed by the
order of the nav drawer libraries' recently added rows.
Finally gets rid of `NavDrawerIteRepository` and related functions which
all had weird, hard to follow code.
### Related issues
Related to #803 & #886
### Testing
Emulator testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
Allows for reordering the items in the navigation drawer
This does not change the home page row order since #803 decouples the
nav drawer and home page rows.
The initial order will be the "Library Order" settings on the web under
Profile->Home. Making any changes locally in Wholphin will set the
order. If any new libraries are added or if Seerr integration is
enabled, these will added to the end of the "pinned" list.
### Related issues
Closes#822
Related to #399 & #803
### Testing
Tested on emulator
- Re-ordering
- Granting/Revoking user permission to a new library
## Screenshots
N/A, nav drawer is the same, just sorted differently
## AI or LLM usage
None
## Description
This PR adds the ability to customize the home page in-app
### Features
- Add, remove, & reorder rows
- Persist the configuration locally
- Save the configuration to the server, allowing to pull down on other
devices
- Adjust view options for rows such as card height, image type,
preferring series images, or aspect ratio (similar to libraries)
- Pull down the web client's home rows (no plugins are supported yet!)
- Preview of the home page which is usable & updated as changes are made
### Row options
These are row types that can be added in-app via the UI:
- Continue watching
- Next up
- Combined continue waiting & next up
- Recently added in a library
- Recently released in a library
- Genres in a library
- Suggestions for a library (movie & TV show libraries only)
- Favorite movies, tv shows, episodes, etc
Additionally, there are more row types that don't have a UI to add them
(yet):
- Simple query to get items from a parent ID such as a collection or
playlist
- Complex query to get arbitrary items via the `/Items` API endpoint
### Dev notes
Settings are loaded in order:
1. Locally saved
2. Remote saved
3. Fallback to default similar to Wholphin's original home rows
The remote saved settings are stored via the display preferences API.
I know some server admins would prefer to push a default setup to their
clients. This PR does not have that ability, but it does define a
straightforward API for defining the settings. Something like the
potential server plugin work started in #625 could be slimmed down to
expose a URL to be added in the load order.
I'm also investigating integration with popular home page plugins to
allow for further customization, but will take more time.
### Related issues
Closes#399Closes#361Closes#282
Related to #340
## Description
As of #842, if an error occurred on some pages that display a generic
error message, it would be shown under the nav drawer making it
difficult to read.
This PR fixes that by passing the `modifier` into the `ErrorMessage`
composables ensuring it has proper offset & padding. Also, for
consistency, passes it the `LoadingPage`s.
### Related issues
Caused by #842
### Testing
Threw an exception during home page loading to observe the issue and fix
on the emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fix a bug where movie suggestions would get stuck loading when the
on-disk cache file doesn't exist.
### Related issues
Fixes#876
### Testing
Ran unit tests locally.
## Screenshots
N/A
## AI or LLM usage
Used AI to add testing for this scenario to unit test
---------
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Fixes the segment (intro, credits, etc) skip button appearance behavior
For the button shown on the video: if the button for a given segment is
clicked, dismissed by the back button, or times out after 10 seconds, it
will never appear again even if you rewind the video.
However, the current non-ignored segment will always show a button on
the playback controls though so it's possible to still use it.
### Related issues
Fixes#875
### Testing
Tested a bunch of scenarios on the emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
- Adding `Quick Connect` drawer to the Settings under the Seer
integration, and before Additional options.
- When interacted with, A inline-dialog appears (after internally
resetting itself), with gutter options Cancel / OK, like existing
dialogs, and when that is completed, where it completes the other side
of `Quick Connect`, e.g. login to your phone Jellyfin app by using
Wholphin.
- Failure and success both are indicated to the user and if success, it
closes the window.
### Related issues
Related to #819Closes#819
### Testing
Tested on development build from my repo on a Nvidia Shield 2019.
Performed several logout/login, both success and fail to observe dialog
state and function.
## AI or LLM usage
Used Claude Sonnet 4.5 for most of the code, ChatGPT, reviewed it, then
I reviewed it, made a few tweaks.
## Description
Fixes the possible glitch on some devices when scrolling left-right on
rows on the home page
### Related issues
Fixes#367Fixes#872
### Testing
Emulator & shield 2019
## Screenshots
N/A
## AI or LLM usage
None
## Description
If the continue watching row is empty on recommended pages, it would
scroll to the first successfully loaded row even if previous ones are
still pending.
This PR fixes that so the page waits until the right first row is ready.
### Related issues
Fixes#839
### Testing
Emulator & shield 2019
## Screenshots
N/A
## AI or LLM usage
None
## Description
Update dependencies
### Related issues
Just updating some of the dependencies in #858 that don't require AGP 9
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
Fixes UI issues:
- Showing unplayed corner text on multi-part movies
- Fix duplicate or missing rating info on discover movie & series pages
### Related issues
N/A
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
A rewrite of the `SuggestionsCache` to simplify it as a read-through
cache. The downside of this is that updated suggestions won't be
reflected in the UI unless the user reloads the page, but I think that's
an okay trade off.
Also adds parallelism limits to fetching suggestions to reduce the
number of simultaneous API calls. And moved the combining logic to use
the `Default` dispatcher to free up an IO one.
Also adds an initial delay to both the suggestions & tv provider
workers, so a cold start of the app isn't starved.
### Related issues
Related to #849
### Testing
Emulator & unit testing
## Screenshots
N/A
## AI or LLM usage
None
Welcome to [Renovate](https://redirect.github.com/renovatebot/renovate)!
This is an onboarding PR to help you understand and configure settings
before regular Pull Requests begin.
🚦 To activate Renovate, merge this Pull Request. To disable Renovate,
simply close this Pull Request unmerged.
---
### Detected Package Files
* `.github/actions/native-build/action.yml` (github-actions)
* `.github/actions/setup/action.yml` (github-actions)
* `.github/workflows/main.yml` (github-actions)
* `.github/workflows/pr.yml` (github-actions)
* `.github/workflows/release.yml` (github-actions)
* `gradle.properties` (gradle)
* `settings.gradle.kts` (gradle)
* `build.gradle.kts` (gradle)
* `app/build.gradle.kts` (gradle)
* `gradle/libs.versions.toml` (gradle)
* `gradle/wrapper/gradle-wrapper.properties` (gradle-wrapper)
### Configuration Summary
Based on the default config's presets, Renovate will:
- Start dependency updates only once this onboarding PR is merged
- Hopefully safe environment variables to allow users to configure.
- Show all Merge Confidence badges for pull requests.
- Enable Renovate Dependency Dashboard creation.
- Use semantic commit type `fix` for dependencies and `chore` for all
others if semantic commits are in use.
- Ignore `node_modules`, `bower_components`, `vendor` and various
test/tests (except for nuget) directories.
- Group known monorepo packages together.
- Use curated list of recommended non-monorepo package groupings.
- Show only the Age and Confidence Merge Confidence badges for pull
requests.
- Apply crowd-sourced package replacement rules.
- Apply crowd-sourced workarounds for known problems with packages.
- Ensure that every dependency pinned by digest and sourced from
GitHub.com contains a link to the commit-to-commit diff
- Correctly link to the source code for golang.org/x packages
- Link to pkg.go.dev/... for golang.org/x packages' title
🔡 Do you want to change how Renovate upgrades your dependencies? Add
your custom config to `renovate.json` in this branch. Renovate will
update the Pull Request description the next time it runs.
---
### What to Expect
With your current configuration, Renovate will create 11 Pull Requests:
<details>
<summary>Update Androidx Media3 to v1.9.2</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/androidx-media3`
- Merge into: `main`
- Upgrade
[androidx.media3:media3-effect](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-ui-compose](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-ui](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-exoplayer-dash](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-exoplayer-hls](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-session](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-exoplayer](https://redirect.github.com/androidx/media)
to `1.9.2`
- Upgrade
[androidx.media3:media3-datasource-okhttp](https://redirect.github.com/androidx/media)
to `1.9.2`
</details>
<details>
<summary>Update dependency com.google.devtools.ksp to v2.3.5</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/ksp-monorepo`
- Merge into: `main`
- Upgrade
[com.google.devtools.ksp](https://redirect.github.com/google/ksp) to
`2.3.5`
</details>
<details>
<summary>Update Dependencies</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/dependencies`
- Merge into: `main`
- Upgrade
[com.google.dagger:hilt-android-compiler](https://redirect.github.com/google/dagger)
to `2.59.1`
- Upgrade
[com.google.dagger:hilt-android](https://redirect.github.com/google/dagger)
to `2.59.1`
- Upgrade
[com.google.protobuf:protobuf-kotlin-lite](https://redirect.github.com/protocolbuffers/protobuf)
to `4.33.5`
- Upgrade
[com.mikepenz:multiplatform-markdown-renderer-m3](https://redirect.github.com/mikepenz/multiplatform-markdown-renderer)
to `0.39.2`
- Upgrade
[com.mikepenz:multiplatform-markdown-renderer](https://redirect.github.com/mikepenz/multiplatform-markdown-renderer)
to `0.39.2`
- Upgrade
[io.mockk:mockk-android](https://redirect.github.com/mockk/mockk) to
`1.14.9`
- Upgrade
[io.mockk:mockk-agent](https://redirect.github.com/mockk/mockk) to
`1.14.9`
</details>
<details>
<summary>Update dependency com.google.dagger.hilt.android to
v2.59.1</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/hilt`
- Merge into: `main`
- Upgrade
[com.google.dagger.hilt.android](https://redirect.github.com/google/dagger)
to `2.59.1`
</details>
<details>
<summary>Update dependency python to 3.14</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/github-actions`
- Merge into: `main`
- Upgrade [python](https://redirect.github.com/actions/python-versions)
to `3.14`
</details>
<details>
<summary>Update Gradle to v8.14.4</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/gradle-8.x`
- Merge into: `main`
- Upgrade [gradle](https://redirect.github.com/gradle/gradle) to
`8.14.4`
</details>
<details>
<summary>Update Jellyfin SDK to v1.8.6</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/jellyfin-sdk`
- Merge into: `main`
- Upgrade
[org.jellyfin.sdk:jellyfin-api-okhttp-jvm](https://redirect.github.com/jellyfin/jellyfin-sdk-kotlin)
to `1.8.6`
- Upgrade
[org.jellyfin.sdk:jellyfin-api](https://redirect.github.com/jellyfin/jellyfin-sdk-kotlin)
to `1.8.6`
- Upgrade
[org.jellyfin.sdk:jellyfin-core](https://redirect.github.com/jellyfin/jellyfin-sdk-kotlin)
to `1.8.6`
</details>
<details>
<summary>Update Kotlin</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/kotlin`
- Merge into: `main`
- Upgrade
[org.jetbrains.kotlinx:kotlinx-coroutines-test](https://redirect.github.com/Kotlin/kotlinx.coroutines)
to `1.10.2`
- Upgrade
[org.jetbrains.kotlin.plugin.serialization](https://redirect.github.com/JetBrains/kotlin)
to `2.3.10`
- Upgrade
[org.jetbrains.kotlin.jvm](https://redirect.github.com/JetBrains/kotlin)
to `2.3.10`
- Upgrade
[org.jetbrains.kotlin.plugin.compose](https://redirect.github.com/JetBrains/kotlin)
to `2.3.10`
- Upgrade
[org.jetbrains.kotlin.android](https://redirect.github.com/JetBrains/kotlin)
to `2.3.10`
</details>
<details>
<summary>Update dependency com.android.application to v9</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/major-agp`
- Merge into: `main`
- Upgrade
[com.android.application](https://android.googlesource.com/platform/tools/base)
to `9.0.0`
</details>
<details>
<summary>Update Github Actions (major)</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/major-github-actions`
- Merge into: `main`
- Upgrade [actions/cache](https://redirect.github.com/actions/cache) to
`v5`
- Upgrade
[actions/checkout](https://redirect.github.com/actions/checkout) to `v6`
- Upgrade
[actions/setup-python](https://redirect.github.com/actions/setup-python)
to `v6`
- Upgrade
[actions/upload-artifact](https://redirect.github.com/actions/upload-artifact)
to `v6`
</details>
<details>
<summary>Update Gradle to v9</summary>
- Schedule: ["at any time"]
- Branch name: `renovate/gradle-9.x`
- Merge into: `main`
- Upgrade [gradle](https://redirect.github.com/gradle/gradle) to `9.3.1`
</details>
🚸 Branch creation will be limited to maximum 2 per hour, so it doesn't
swamp any CI resources or overwhelm the project. See docs for
`prhourlylimit` for details.
---
❓ Got questions? Check out Renovate's
[Docs](https://docs.renovatebot.com/), particularly the Getting Started
section.
If you need any further assistance then you can also [request help
here](https://redirect.github.com/renovatebot/renovate/discussions).
---
This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/damontecres/Wholphin).
<!--renovate-config-hash:7cb4aaf8303f9b7801754f83b08207c5265d3d176bdb616169c40f6df7c688c2-->
---------
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Add a settings to set the maximum amount of time items can appear in
Next Up.
Options range from 7 days to a year, or no limit (current & default
behavior)
### Related issues
Closes#662
### Testing
Tested on the emulator and verified URLs, added unit tests
## Screenshots
N/A, just a new slider bar setting
## AI or LLM usage
None
## Description
The nav drawer would always glitch a little when it gained focus due it
to trying to keep the selected page in view. #842 made this even worst.
This PR fixes that issue by hoisting the list scroll state up higher
which ensures it is remembered between recompositions. This change also
eliminates the need for workarounds to try and scroll it
programmatically which is also a tiny performance boost.
### Related issues
Follow up to #842
### Testing
Emulator & nvidia shield
## Screenshots
N/A
## AI or LLM usage
None
## Description
Adds a item in the More menu to send information about the media item to
the server (and app logs).
This includes the "media sources" which is info about the files and
stream/tracks in the files. It also includes device info, the playback
preferences, and the device's transcoding profile. All of this info is
useful for debugging playback issues.
### Related issues
N/A
### Testing
Emulator testing
## Screenshots
N/A
## AI or LLM usage
None
## Description
This updates the navigation drawer with better behavior and UI. It is
also a bit more optimized and is slightly faster.
- When open, content is pushed to the right instead of being covered
- When open, it uses the same background colors from the dynamic
background
- The icons no longer shift as the drawer opens & closes
- The open drawer is not as wide
- Focus highlight on items is less circular, now matching the other
menus in the app
- The animation is a bit faster too
Dev notes:
- This PR copies & modifies `tv-material` code, so there's a customized
`ModalNavigationDrawer` implementation now
- There are fewer `AnimatedVisibility` composables which improves
performance
- Icons are exactly sized
- Content pages generally shouldn't add start/end padding anymore, the
padding is now added by the nav drawer
(b4f1b111edcd7e7ec05edc1100402b2851721fcd) and is consistent across
every page
### Related issues
Closes#699Closes#384
### Testing
Tested on the emulator, nvidia shield, & google sabrina
## Screenshots


## AI or LLM usage
None
## Description
Addresses suggestion logic oversight in recently merged suggestions fix,
and improve performance (especially app homepage startup time)
- Re-implements the logic that sorts/categorizes tailored suggestions
based on the seed items that are queried. This was implemented in the
original PR at first, but I overlooked moving that logic
- Removes `OneTimeWorkRequestBuilder` and replaces with trigger in
`PeriodicWorkRequest` to fire with a 30 seconds delay if cache is empty.
### Related issues
Fixes#833
### Testing
Will be testing in:
Android TV emulator in Android Studio
NVIDIA Shield TV Pro 2019
## AI or LLM usage
I will use AI to help with writing the tests/testing services.
## Description
Small change so that the "Ends" time on the playback overlay takes the
current playback speed into account.
### Related issues
Closes#836
### Testing
Emulator
## Screenshots
N/A
## AI or LLM usage
None
## Description
This PR aims to fix#638 where the "Suggestions" row was mixing up
content from different libraries that shared the same library type. For
example you were in the Anime library, you'd see regular TV shows
showing up, as the TV Shows and Anime libraries would both be "Show"
libraries.
As @damontecres mentioned in the issue discussion, the
`/Items/Suggestions` endpoint Wholphin is using just returns random
items from the same library type, with no special logic or
recommendation algorithm.
I also looked at how the official WebUI handles this. It basically just
mixes "Resume," "Next Up," and "Latest Items" together. That felt a bit
redundant since we usually have dedicated rows for those things anyway;
I wanted to try something that helps discover new content.
I replaced the broken endpoint with a few custom `GetItemsRequest`
calls. These work correctly with the `ParentId` filter, so we only get
results from the library we are actually looking at. Additionally, I
added in some logic that takes into account what the user has been
watching to tailor the suggestions a little bit.
The new logic uses a 40/30/30 mix (this can of course be tweaked as
needed)
- Tailored (40%): Grabs your recently watched items from this library,
checks their genres, and returns items from the same genre.
- Random (30%): Random unwatched stuff from this library.
- New (30%): Recently added items.
This is implemented using only `GetItemsRequest` API calls using
filters, rather than any specialized endpoints like the
`/Items/Suggestions` endpoint. This means the feature should be
predictable and stable. If Jellyfin ever adds an actual recommendation
algorithm and endpoint to call it, I imagine we would want to switch to
that in the future though.
I also added some failsafe logic to make sure there will be diversity in
the "recently watched" items that get pulled, in case a user might have
recently binged a TV show. We wouldn't want every single recommendation
to be based off of that one show.
To fix this, I changed the query to fetch the last 20 history items
instead of just the top 3. Then, we filter that list client-side to find
unique Series IDs. If the user watched 10 episodes of One Piece in a
row, the logic collapses them into a single "One Piece" entry and moves
on to the next distinct show they watched. This guarantees we get
variety in the source material for recommendations.
This is a work in progress right now, mainly due to performance. For my
server it takes about 10-15 seconds to load the Suggestions row when I
tested. This is unacceptable in my opinion, especially if we ever intend
to add a Suggestions row like this to the home page like Plex does. I
have some ideas on how to improve performance, but I'm open to
suggestions.
### Related issues
Resolves#638
### Screenshots
Before:
<img width="1953" height="1162" alt="suggestions1"
src="https://github.com/user-attachments/assets/6e301c67-1a46-46b5-8184-3adb9e52b330"
/>
After:
<img width="1937" height="1108" alt="suggestions3"
src="https://github.com/user-attachments/assets/b9563865-4055-40b6-b452-f94c26c8b6e9"
/>
### AI/LLM usage
I used Claude Code to help write the Kotlin Coroutines so the API calls
run at the same time. I also used it to write the tests in
`TestSuggestionsLogic.kt`.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Add support for viewing photos in photo albums
### Features
- Browse and view albums & photos
- View an album as a slideshow
- Show overlay with photo details and controls
- Rotate, zoom, & pan photos
- Apply basic filters to photos or albums such as contrast, saturation,
red/green/blue, etc
- Setting to include video clips during slideshows
### Controls
- D-Pad left or right: scroll between photos in album
- Hold D-Pad up or down: zoom in or out of the current photo
- D-Pad left/right/up/down, while zoomed: pan around photo
- Back, while zoomed: revert the zoom
- Otherwise: show photo overlay
### Related issues
Closes#458Closes#634
### Screenshots
#### Overlay

#### Applying filters

## Description
Can now configure a different subtitle style for HDR playback.
There is also a new setting to change the opacity/dim image based
subtitles. This is primarily for HDR to dim very bright PGS subtitles,
but the setting is available for SDR too.
Please remember that the styles only apply to unstyled text formats like
SRT.
I haven't been able to consistently get the preview to switch the
display into HDR mode, so the preview is not 100% accurate.
### Related issues
Closes#583
### Testing
Shield pro 2019 w/ LG C2
## AI or LLM usage
None
## Description
Simplifies the refresh rate/display mode change code by removing the
live data go between.
Also, instead of a `CountDownLatch` that blocks the thread, use a
`Deferred` to suspend the coroutine instead.
### Related issues
Might help with #769
### Testing
Tested on nvidia shield
## Description
Add ability to sort and filter items in a playlist. There's a new button
for each added to the playlist details page. Starting playback will run
in the order selected.
The sort and filter are persisted locally per playlist, so returning to
the playlist will remember the previous settings.
Note: playlist playback is still limited to 100 items (#42)
### Related issues
Closes#285
### Testing
Emulator & nvidia shield
## Screenshots

## AI or LLM usage
None
## Description
When using LOCAL or JELLYFIN authentication methods to connect to Seerr,
the login flow was not properly establishing session cookies and user
permissions. The login would 'succeed' and you were led to thinking it
worked, but then...
This caused:
- Request functionality to be greyed out even though the user should
have access
- Users to appear to have no permissions after login (permissions=0)
## Solution
This PR fixes the authentication flow by:
**Fetching full user config after login**: For LOCAL and JELLYFIN
auth methods, the login endpoints (`authLocalPost()` and
`authJellyfinPost()`) establish the session cookie but return incomplete
user data. This PR now calls `authMeGet()` immediately after login to
fetch the fully-populated user configuration including proper
permissions.
## Tests
- Have working apk on my dev fork.
- Ran logins as Local + API. Cannot test Jellyfin due to environment,
but codewise, it looks correct.
## Related issues
Related to https://github.com/damontecres/Wholphin/issues/747
---------
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Makes sure the invalid credentials error is propagated up instead of
displaying "No URL" as the error message.
Makes sure `OkHttp` is initialized since the startup initialization
provider is disabled. This fixes a possible crash if `OkHttp` needs to
access its assets.
Also, shows the URL when removing the Jellyseerr server.
## Description
Improves loading speed for the series overview page (one with list of
episodes). This is mostly accomplished by querying for the cast/crew in
the episodes on demand instead of during the initial query.
Also stops using the date as a replacement for the episode index. Using
this caused an extra unnecessary (long) query.
Going from home to the 109th of a 230 episode season is ~2x faster with
this change on my test server.
### Related issues
Closes#599
## Description
Updates the logic in the Prefer MPV setting to prevent performance
issues when software decoding is selected. When the MPV backend is
configured to use software decoding, the app will now automatically
fallback to ExoPlayer for 4K content. Most Android TV devices cannot
smoothly render 4K video via software decoding, so falling back to
Exoplayer for this content makes sense in most cases when using the
software renderer. This mirrors the existing fallback mechanism used for
HDR content.
### Related issues
N/A
### Screenshots
N/A
### AI/LLM usage
None
---------
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Due a few more MPV bugs showing up, this PR reverts the part of #736
that made "Prefer MPV" the default player option.
User can still manually switch to "Prefer MPV" and its full functional
remains. It's just not set to be the default anymore.
## Description
Bumps dependency versions.
AGP (and hilt by extension) are not yet updated cause moving to AGP 9
has many breaking changes and I think one or two may affect the build
scripts.
Also, one day I will add Renovate to automate these PRs.
## Description
Fixes several bugs and makes it easier to log in to a Jellyseer instance
- Fix initial focus & next focus on the add server dialog
- Don't use an API key for non-API key auth (credit to @voc0der in #734
for suggesting this)
- Show trailers for TV shows
- Fix incorrect/missing similar and recommended rows for both movies &
TV shows
- Allow empty password for Jellyfin users without a password
Log in will now try variations of the input URL such as add the protocol
and/or default port. This is similar to the regular Jellyfin login
process.
### Related issues
Fixes#778Fixes#750Fixes#740
Might help with #747
Fixes
https://github.com/damontecres/Wholphin/issues/764#issuecomment-3801324648
## Description
Fixes a possible crash if a row on the home page is removed.
This could potentially happen if you only have one thing on the resume
row and watch it by accessing it from the bottom row.
## Description
Ensure that MPV is always destroyed before being initialized again.
Also, now creating a new player is only done when switching backends
which means faster, more efficient playing the next up.
### Related issues
Fixes#766
## Description
Disables gpu-next by default in MPV settings. This renderer is
experimental and causes broken playback (purple screen) on some older
devices like the NVIDIA Shield 2019 Pro.
Also adds a migration to ensure gpu-next is turned off for existing
users who upgrade, avoiding playback regressions for users being newly
moved over to the Prefer MPV settings so that they can experience a
smooth transition.
### Related issues
Fixes#754?
### Screenshots
N/A
### AI/LLM usage
PR drafting
## Description
Handle errors on the Discover/Jellyseerr requests tab preventing crashes
Note: this PR fixes the crash, but the error was a 400, which is likely
due to the older Jellyseerr version.
### Related issues
Fixes#764
## Description
This PR has several micro optimizations to the home page & nav drawer.
Basically it computes text off of the main thread and removes redundant
calculations.
## Description
The MPV backend is no longer considered experimental. This PR adds a
setting for player backend "Prefer MPV" which uses MPV for all playback
unless the video is HDR which then uses ExoPlayer.
Switching occurs per video. So if there's a playlist with both SDR &
HDR, the player will switch back and forth as needed.
### Related issues
Closes#253
Related to #235
## Description
- Better focus handling on discover page
- Follow up to #723 to dim the user image when the nav drawer is closed
- Add a missed closeable to stop theme music when leaving a series page
## Description
Adds (back) the ability to set a per-user PIN. This is required to
switch to the user. If the user has a PIN, this overrides and ignores
the "Sign in automatically" setting for that user.
The PIN consists of any combination of D-Pad directional buttons. If you
forget the PIN, you can click the button to login via the server instead
(ie QuickConnect or username/password) which will also remove the PIN so
you can set a new one if desired.
### Related issues
Closes#268
Replaces #398
Related to #321 & #356
### Screenshots

## Description
Replaces the generic user icon with the user's profile image on the nav
drawer
If there's no image it will fall back to the generated color & first
letter just like on the user list page.
### Related issues
Closes#631
### Screenshots
<img width="54" height="160" alt="image"
src="https://github.com/user-attachments/assets/bee21e56-104a-420f-bb7d-aae717f8aeb0"
/>
## Description
Adds the [`dav1d` decoder
module](https://github.com/androidx/media/tree/release/libraries/decoder_av1)
for ExoPlayer as an option. It can be enabled in advanced settings.
Performance will vary between devices.
### Related issues
Closes#103
### Dev notes
As with the ffmpeg module, this one is not required to build the entire
app so it's easier for developers to get started.
This also introduces a customized `DefaultRenderersFactory` for
ExoPlayer, but its largely copied from the super class's implementation.
## Description
Fixes focus jumping back a few cards on rows when returning to a page
I think some behavior changes occurred in a recent compose release.
`requestFocus` doesn't throw an exception anymore for example. Focus in
compose has always been finicky.
This PR basically adds some delegating parent focus down its to
children. Also, most rows now maintain an internal position state to
track where the focus requester is applied.
### Related issues
Closes#632
## Description
- Fixes a copy-paste error where checking if Seerr support 4K for TV
accidently used the movie 4K value
- Always show the loading indicator while waiting for playback to begin
- This has the side effect of fixing MPV playback not starting until the
UI changes (recomposes). That's a hacky fix, but should work while I try
to figure out the real cause
### Related issues
Fixes#715
## Description
If an error occurs while fetching data for the grid, leave the controls
accessible. This is useful in case a particular sort or filter causes an
error because the app remembers those values between page loads.
Essentially the error message is shown in place of the grid content
instead of the entire tab.
### Related issues
Fixes#712
## Description
- Fixes when to show/hide Request & Cancel buttons
- Removes the More button
- Uses a larger 16x9 backdrop so that the fade is better
- Fix reloading the backdrop on series pages
### Related issues
Fixes#706
## Description
This PR optimizes the rendering of "quick details" in headers. That's
the dot separated row of Season/Episode/Date/Runtime, etc
The UI is slightly different because I moved the "Ends at" part to the
end of the row. The dots & spacing is also slightly different.
In testing this on my CCwGTV HD, a pretty low powered device, the PR
improved the time to render by over 3x (59.42ms->17.72ms). Note: these
times are during profiling and are not real world performance, but the
relative change should be applicable in the real world.
### Dev details
- Changes the dot separated row of multiple Text & Box composables into
a row of two Texts
- The text is an `AnnotatedString` which is created on the IO thread
when `BaseItem` is created
- A separate composable creates the "Ends at" to prevent recompositions
of the other strings
## Description
Fixes "Pause with one click" so the desired behavior isn't reversed and
now shows the controls when pausing.
This bug happened because MPV commands are async, so there is a tiny
delay before the pausing occurs and the playback state is updated.
However, the logic to decided whether to show the controls or not checks
the playback state immediately.
### Related issues
Fixes#697
## Description
This PR is a rewrite of a previously submitted PR for this same feature.
It implements native Voice Search for Android TV using Android's
SpeechRecognizer API.
Key Changes
New Files:
- VoiceInputManager.kt - Handles SpeechRecognizer lifecycle, permission
management, and audio level normalization
- VoiceSearchButton.kt - Composable button with audio-reactive pulse and
full-screen listening dialog
UI/UX:
- Audio-reactive microphone button that pulses based on input volume
- Full-screen listening overlay with animated states:
- Listening - Pulsing bubble with ripple rings, shows partial
transcription
- Processing - Animated dots while waiting for final result
- Error - Red bubble with localized error message, auto-dismisses after
3s
### Related issues
Resolves https://github.com/damontecres/Wholphin/issues/515
### AI/LLM usage
The initial draft of this change from the prior PR was created with the
assistance of Claude Code, which was heavily leaned on for the overlay.
I have taken the initial draft, gone through it, and re-implemented by
hand to clean it up, narrow the scope (AI loves to touch and change
things unrelated to what you're working on) and make it more readable.
AI was used in the last stage to assist with finding any logic
errors/bugs I may have missed (by looking through code and also by
looking over ADB logs from testing on Nvidia Shield).. AI was also used
to create the tests found in TestVoiceInputManager.kt. Lastly, I fed the
diffs to an AI to help summarize the changes for this PR in an easy to
read manner.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Use only TV series genres for a TV library's genre tab. This ensures
only series are shown, not episodes, which removes some of the seemingly
empty genres.
Also fixes the image filtering to ensure an item with a backdrop is
always used if available.
### Related issues
Fixes#689Fixes#690
**Description**
Fixes a race condition in the Seerr integration.
When you added a new Seerr server and logged in successfully, the app
would immediately show the "Disable Seerr?" dialog instead of a success
message. The state was getting mixed up during recomposition and
thinking you wanted to remove the server you just added.
Fixed by being more explicit about what mode the dialog is in (adding vs
removing) so it can't get confused by background state changes. Also
added a success toast when login works.
**AI/LLM usage**
Used Claude Code to analyze the issue.
## Description
Clips person images to a circle. The rectangle is still used in grid
pages like favorite people.
This PR also updates the fallback image to use a person icon if there is
no image for that person
Also adds role/job to discover pages' people rows
### Related issues
Closes#682
### Screenshots

## Description
On any grid that wasn't showing movies, tv shows, or videos, the jump to
letter functionality did not work correctly.
This PR fixes jumping for other media types, eg collections or episodes.
Additionally, the jump now also takes filters into account, so if
filtering Movies, it will jump to the right place now within the
available movies.
### Related issues
Fixes#683
## Description
This PR add the ability to integrate with a [Jellyseerr/Seerr
server](https://github.com/seerr-team/seerr).
### Features
- Login to the Seerr server using API Key, Jellyfin credentials, or
local Seerr credentials
- Separate Seerr logins per user profile
- Search Seerr from the search page
- Discover media from Seerr on the nav drawer `Discover` page
- Also from movie, series, or person pages
- Seamless integration with existing media, i.e. selecting media found
from Seerr that already exists on the Jellyfin server, will go directly
to the regular media page
- Mostly consistent UI between Jellyfin media & Seerr media
- Submit requests to Seerr
- Cancel submitted requests
### Screenshots

### Related issues
Closes#54
## Acknowledgements
The `app/src/main/seerr/seerr-api.yml` file is copied & modified from
https://github.com/seerr-team/seerr/blob/develop/seerr-api.yml. I hope
to try to upstream some of the changes in the future.
**Description**
Fixes a regression where season numbers in the tab strip were invisible
when scrolling through long series (15+ seasons).
**Related issues**
Fixes#677
**AI/LLM usage**
Used Claude Code to analyze diffs from recent commits in order to find
which commit caused the regression.
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Previously, if you clicked on a tab and the resulting grid had no
results, the nav drawer would open and focus which is not good.
This PR fixes that and instead returns focus back to the tab row.
### Related issues
Fixes#543
## Description
A few devices, like the Nvidia Shield Pro 2019, are able to correctly
handle Dolby Vision Profile 7. However, when a Full Enhancement Layer
(FEL) is present, these devices effectively ignore the enhancement layer
and only apply the RPU metadata.
Due to this behavior, device capabilities are reported as not supporting
`DOVIWithEL`, which causes the Jellyfin server to strip the DV layer
entirely with a remux.
This commit adds an option to force direct play of Dolby Vision Profile
7 content, preventing the server from stripping the Dolby Vision layer
and allowing compatible devices to handle playback as intended.
### Related issues
Fixes https://github.com/damontecres/Wholphin/issues/449
### Screenshots
<img width="1280" height="720" alt="dovi7"
src="https://github.com/user-attachments/assets/849e9897-93b6-419f-a1fb-7f361f9c88dd"
/>
### AI/LLM usage
None
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Check if playback is for a remote source and, if so, play that remote
URL directly. Works for both ExoPlayer & MPV.
Also adds support for direct playback for DASH in ExoPlayer.
### Related issues
Closes#471Closes#666
## Description
- Always update home page backdrop
- Use smaller font size for episode names & force single line
- Use smaller cards for episode cards on person page
## Description
Adds a dark gradient scrim behind the season tabs on the TV Show Details
screen to improve text readability against bright backdrop images. This
follows the same pattern used in PlaybackOverlay.kt for the playback
controls.
The scrim uses two combined gradients:
- Vertical: Dark at top (0.80α → 0.5α) fading to transparent at bottom
- Horizontal: Fades in from transparent (left) to full opacity (right),
starting at 15% screen width to avoid overlapping with the nav rail
The gradients are composited using BlendMode.DstIn to create a smooth 2D
fade effect.
### Related issues
Fixes#547
### Screenshots
<img width="1920" height="1084" alt="season_darkened_background"
src="https://github.com/user-attachments/assets/a85237e4-2812-433c-902b-36f9860448c5"
/>
### AI/LLM usage
AI used to draft PR description and documentation
## Description
Just a UI change to disable the Search & Download subtitles button if
the user doesn't have permission to manage subtitles.
This avoids the 403 error if the user attempts to use it.
### Related issues
Closes#483
## Description
Adds a client-side "Only Forced Subtitles" option when selecting
subtitles, per the discussion in #571.
### Changes
- **TrackIndex.ONLY_FORCED** (`-3`): New constant for the special case
- **StreamChoiceService**: Intercepts `ONLY_FORCED` at the top of
`chooseSubtitleStream()` before server-side `SubtitlePlaybackMode` logic
(which remains untouched)
- **UI**: Adds "Only Forced Subtitles" option to both:
- In-player subtitle menu (`PlaybackDialog.kt`)
- Pre-playback stream selection dialog (`Dialogs.kt`)
### Forced track detection
Uses metadata `isForced` flag as primary check, with title-based
fallback ("forced", "signs", "songs" patterns) when language matches
audio.
### Related issues
Closes#571
### Screenshots
N/A - subtitle selection menu only
### AI/LLM usage
Used Claude Code for assistance and to draft PR description.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
Fixes duplicate play/pause commands from a remote button press and
`MediaSession`.
This ended up being a bit complicated because Wholphin overrides the
default play/pause behavior for the skip back setting and to show the
controller on pause. This means using a forwarding player which meant
implementing more functionality into `MpvPlayer`.
### Related issues
Fixes#653
## Description
Fixes track selection issues with MPV. This was especially focused on
libraries that disable embedded subtitles, but also fixes issues where
the audio streams are listed before video streams.
Also includes test media source JSON and unit tests to verify many
scenarios.
### Related issues
Fixes#494
## Description
Processing segments happens in a separate coroutine which wasn't
cancelled until _after_ playback of the next item begins. So in some
cases the outro from a previous item might be triggered during the very
beginning of the newly starting item.
So this PR instead cancels segment processing immediately when starting
the process for playback of an item. Additionally, the processing also
now checks that the current segment is for the current playback item as
an additional safeguard.
### Related issues
I believe this should fix both of these issues:
Fixes#369Fixes#647
## Description
Creates a `MediaSession` during playback which allows for external
control such as via smart speakers, Google Home app, etc
Note: one limitation is that seeking previous/next items in the playlist
isn't supported. This is because Wholphin manages that independent of
the `Player`, so additional dev work is needed for this.
### Related issues
Closes#622
Might help with #415
## Description
When switching resolution (and possibly refresh rate less frequently),
the initial surface is not always valid. This means, the MPV playback
treated it as detaching the surface and would never update.
This PR instead subscribes to the `SurfaceHolder`'s callbacks which will
provide a new, valid surface once the switch is ready. Then the surface
is attached and queued commands execute to start playback.
Also temporarily disables content scale options for MPV since the
compose implementation conflicts with MPV rendering.
### Related issues
Fixes#626
## Description
This is an embarrassing bug... In #619, a change was made to avoid a
race condition and wait for the video output surface to be ready by
re-enqueuing commands. Except I missed cancelling executing the current
command.
This meant the command would both be executed and queued again. So, if
attaching the video surface takes a while, such as needing to wait for a
refresh rate change, _a lot_ of commands could have been added to the
queue and trigger a constant start-stop playback loop.
I think technically it would eventually resolve itself, but can take a
while.
### Related issues
Should fix#626
## Description
Removes an incorrect optimization for showing the home page header
Separates logic for focusing on rows on the home page
### Related issues
Fixes#624Fixes#620
## Description
Fixes a race condition where `libmpv` is beginning playback before it
has been attached to the output video surface.
The solution is for the handler to re-enqueue commands if MPV is either
not initialized or not attached yet. Once `libmpv` is ready, the
commands will be processed.
### Related issues
Should fix#576
## Description
On both movie & series detail pages, show a button to play trailers
instead of the row of cards below.
The button works like this:
- If no trailers, the button shows "No trailers" and is
disabled/non-clickable
- If only a single trailer, the button shows "Play trailer" and clicking
it plays the trailer
- Otherwise, shows "Trailers" and clicking displays a dialog to choose
one
Local trailers are shown first in the list. Then there is an attempt to
sort the trailers for the dialog so that names with "Official Trailer"
or "Official Theatrical Trailer" are listed first and "Teaser" is listed
last. This isn't perfect and doesn't work for non-English, but there's
no other metadata to use.
### Related issues
Closes#408
## Description
Makes sure the `ServerEventListener` and `DatePlayedInvalidationService`
are injected so that they run. This was broken in #538.
Also fixes the a delayed stopping of theme music
Also fixes for detecting a non-seamless refresh rate switch
### Related issues
Fixes#615Fixes#568
## Description
Yet more focus issue fixes:
- Restore focus when going back to home page from a later row
- Don't jump to episode row when going back to series overview (such as
from a person page)
This PR also changes how backgrounding the app during playback works.
Now the only goes back to the previous page when the app comes back to
the foreground. This helps with focusing back on the page properly.
There's also a bit more clean up performed.
## Description
Adds a settings for automatic resolution switching. When enabled, the
app tries to output content at the video stream's resolution.
For example, if playing 1080p content on a 4K TV, the actual output by
the Android TV device will be 1080p.
This works in hand-in-hand with automatic refresh rate switching. So
enabling both allows for the output to match both refresh rate and
content resolution. Outputting the best refresh rate is preferred over
best resolution.
Also, shows the current display mode in the playback debug info.
### Related issues
Closes#428Fixes#531
## Description
Instead of triggering the tv guide scroll animation during composition,
do it during key presses. This fixes an edge case that crashes the main
thread when returning the guide but the underlying UI data structures
haven't been updated.
### Related issues
Fixes#592
## Description
Fixes the logic for determining which items need to be added to the
watch next channel row
Also adds some code to clean up existing duplicates. This code can be
removed around the next release.
Adds a channel to the home screen for recently added media. This is
needed to ensure full integration with the OS to allow toggling off any
of the Wholphin channels including watch next.
### Related issues
#372 introduced the bug
Fixes#587
## Description
- Show unwatched counts on the cards in the latest rows instead of child
count, which is more in line with the web UI & official app
- Show exact runtime on the overview dialog for each version
- Don't show controls after clicking up next
### Related issues
Closes#431
## Description
When the app is put into the background with auto sign-in disabled, it
will remove the current page and show a loading indicator. This way when
the app is resumed, the previous content won't flash on screen for a
split second.
The above should technically fix#584 on its own, but as additional safe
guide, this PR also makes sure the `ApiClient` is configured before
trying to create image URLs.
### Related issues
Fixes#584
## Description
* Splits video details into two columns
* Show index number if multiple streams of the same type
* Different font color for media section titles
* Adjustments to spacing
* Add hearing impaired flag to & remove AVC from subtitle info
### Related issues
Closes#549
## Description
Fixes issues when the app is resumed, such as after the screensaver is
dismissed.
For example, if you pause during playback and the screensaver starts,
Wholphin will stop playback and go back to the previous page. But with
this PR, instead of focus being lost and defaulting to teh nav drawer,
focus will restore on the play button or episode card.
### Related issues
Related to #552 &
https://github.com/damontecres/Wholphin/issues/552#issuecomment-3690614491
## Description
Uses backdrop instead of thumb images for the genre cards since the
backdrops tend to have little text to distract from the card title
### Related issues
Closes#574
## Description
Optimizes how both series pages are loaded. If the series has a lot of
seasons and/or lots of episodes per season, the pages load much faster.
This is a combination of reducing queried information, optimizing
finding the right data to display first, and lazy loading seasons.
Note: Series that don't have episode numbers, like daily TV shows, are
not as optimized yet
### Rough speed-ups
These numbers are from my (beefy) server, so it will vary based on your
server
Series details page (one with seasons row) w/ ~50 seasons: 5s->300ms,
~13x faster
Series overview page (one with episode row) w/ ~50 seasons & 25
episodes: 6s->1.1s, ~5x faster
### Related issues
Related to #562
## Description
Improves how the app restores state when auto sign in is enabled. If
auto sign in is enabled, the app will restore back to the last page.
#538 altered the app start up logic. This caused the app to reset state
more often when it goes into the background.
### Related issues
Related to #552
## Description
This PR updates the debug build config to allow side by side
installation with the mainline/stable release by changing the app name
and app ID.
This is a convenience change to allow contributors to test debug builds
on their personal devices without overwriting the existing stable
version of the app
## Description
Prevents the quick details (the dot separated details) from using more
vertical space if there is not enough horizontal space. Instead, the row
will be clipped at the total width.
This PR also gives more horizontal space to the details header on grid
pages. This matches the home page now.
### Related issues
Fixes#542
## Description
The filtering & sorting for the genre results grid is now independent
from the actual library.
This prevents having a filter saved on the actual library that conflicts
with the Genre grid.
Also shows the genre name in the title of the page
### Related issues
Fixes#536
## Description
- Ensures settings changes take effect immediately instead of requiring
an app restart
- Add conditional & catch when checking for app updates
### Related issues
Fixes issues introduced in #538
## Description
The major user facing change in this PR is adding back the setting to
toggle automatic sign in on or off. This controls whether to restore the
app to where you left off or go the user selection page.
If enabled: re-opening the app will restore the previous user's session
If disabled: re-opening the app will always start with the user
selection page for the most recent server, or if there isn't a recent
server, the server selection page
This also separate this setting from the PIN protection settings in
previous PRs.
### Dev changes
This PR refactors the "setup" UI (server & user selection) to be
separate from the "app" UI (the actually app content). There is now a
`NavDisplay` for the setup flow which ends with displaying the
`ApplicationContent`'s `NavDisplay`. Most of the logic is moved out of
Compose code and into a `ViewModel`.
This means to switch users/server, the `SetupNavigationManager` must be
used instead of `NavigationManager`.
### Related issues
Closes#396Fixes#376
## Description
No user facing changes in this PR
This is a continuation of #481 to move initializing and tearing down
`libmpv` onto the looper thread to prevent main thread blocking.
~The only function left that runs on the main thread is
`setVideoSurfaceView`. Since this has to directly interact with the UI,
I need to read the docs and ExoPlayer code a bit more to see how this
can safely done multi-threaded.~ It is safe/correct to attach on another
thread.
## Description
A few minor bug fixes across the app
- Fix the language not showing after search on the subtitle search &
download dialog
- Clear the backdrop whenever the home page is reloading from scratch
- Enable pressing a remote's play (or play/pause) button on the series
page to play the focused episode
### Related issues
Play button follow up from #424
## Description
When playing media with multiple parts, the additional parts will be
added to the playback queue. This means a multi-part movie or episode
will be play all of the parts in order before the next item in the
queue.
This only affects the playback queue. There's no UI changes and no
indication that a movie or episode has multiple parts. I'll work on that
in a separate PR.
### Related issues
Related to #455
## Description
Respect the combine Continue Watching & Next Up setting on TV Show
library recommended tabs.
This uses the same implementation on the home screen, but only includes
items for that particular tv show library.
### Related issues
Fixes#532
## Description
When using a server provided aspect, force a minimum value (.666) in
case the server returns unusable data like zero.
Note: I have not been able to reproduce the errors in #451 to verify
that this PR will fix it. And it's not clear to me why #453 did not fix
it either. However, this PR shouldn't impact anything if the server is
sending valid aspect ratios and should at least prevent the crash on the
UI side.
### Related issues
Follow up to #453
Hopefully fixes#451
## Description
Changes how `init()` works for `ApiRequestPager` so that it 1) always
forces updating the total count if needed and 2) propagates errors
during the initial fetch up to the original caller
This means if an error occurs during the initialization of the pager
data, it won't be quietly logged and ignored. Instead, ViewModel code
will catch it and display an error in the UI.
### Related issues
Should fix#492
## Description
- Cleans up program titles & subtitles by removing newline characters
- Align the start of the guide to half hour, rounded down, ie 9:52->9:30
or 11:05->11:00
- Add some visual indication that a program has started before the guide
start time
- This is done by adding a `<` prefix to the title and using sharp 90
degree corners at the start instead of rounded
### Related issues
Closes#434Closes#435Closes#436
## Description
Overhauls the genre cards for Movies & TV Show libraries.
Now the cards use a semi-transparent, consistent, generated color using
the same logic as in #478. And a random item from the library with that
genre is show in the background. This is heavily inspired from Plex.
If there are not applicable items for the genre, then just the colored
card is shown.
### Related issues
Fixes#490
## Description
This PR adds a background dimming effect behind the playback controls to
fix readability issues against bright video content. It implements a
full-screen vertical gradient that ensures white text and buttons remain
legible.
I added a vertical gradient fading from transparent at the top to ~95%
black at the bottom which adds contrast for UI elements without
obscuring the center of the video.
### Related issues
Solves [#485](https://github.com/damontecres/Wholphin/issues/485)
### Screenshots
<img width="1948" height="1112" alt="playback"
src="https://github.com/user-attachments/assets/f671072c-e37a-491d-b82f-fb6f888a42c7"
/>
<img width="1918" height="1081" alt="no_gradient"
src="https://github.com/user-attachments/assets/30bc2fee-ef5a-4d63-87a6-953e401cd84b"
/>
### AI/LLM usage
This change was developed with the assistance of AI, but was not wholly
generated by AI.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Damontecres <damontecres@gmail.com>
Co-authored-by: Ray <154766448+damontecres@users.noreply.github.com>
## Description
Fixes the backdrop not being cleared when switching tabs on the
favorites page by ensuring each has a unique view model.
Also clears the backdrop if the view option to show details is turned
off.
### Related issues
#476#499
## Description
Always show the "Dolby Vision" label instead of just HDR if the video
has a Dolby Vision title. This matches the behavior of the official app.
### Related issues
Fixes#491
There's no user facing changes in this PR
This PR updates `MpvPlayer` to use a threading model more similar to
[ExoPlayer's
model](https://developer.android.com/reference/androidx/media3/exoplayer/ExoPlayer#threading-model).
There's now a dedicated thread looper message queue between the UI
interactions on the main thread and calls to `libmpv`. This is necessary
because previously, the main thread would make the calls into `libmpv`.
`libmpv` uses internal mutex locking for some operations which meant
that the main thread would be blocked momentarily.
Now, the UI issues a command (eg seek to X position) and the command is
added to the thread looper queue and processed on that separate thread.
This uses the standard Android Handler/Looper mechanisms.
Related to #235
## Description
Creates a `CompositionLocal` which tracks the current time in a single
place and is usable anywhere in the app.
Can be used such as:
```kotlin
val now = LocalClock.current.now
val details = remember(item, now) {
// Create detail strings based the item and current time
// This will be recomposed/updated whenever the time changes
}
```
### Related issues
Related to #475
- Switch to tv.material3.Button with ButtonDefaults for better styling
- Add opacity changes based on focus (0.85f focused, 0.2f unfocused)
- Keep selected letter fully visible when picker is unfocused
- Reduce button size from 24.dp to 14.dp to fit all letters
- Reduce spacing between letters (1.1.dp vertical, 2.dp horizontal)
- Use transparent backgrounds for unfocused letters
- Use border color for selected letter when focused, tertiary when
unfocused
- Add Box with CircleShape clip to prevent focus indicator overflow
- Reduce font size by 15%
- Add 16.dp end padding to push picker away from screen edge
When using the 'Show details' view option, the alphabet picker will
still overflow the bottom edge of the screen. I find this an acceptable
option though, as the backdrop pushes the card grid further down the
page.
Closes https://github.com/damontecres/Wholphin/issues/386
>[!NOTE]
> AI was used in the making of this PR
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
Redesign Select Server and Select User screens with horizontal card
layout, similar to traditional streaming services
- Replace vertical lists with horizontal list of icons
- Center align text in Quick Connect and Username/Password modals
- Display Jellyfin profile images
- Fallback to first letter of username if no image is available
- Filter out duplicate server entries in the auto-discovery list
---------
Co-authored-by: Damontecres <damontecres@gmail.com>
## Description
This PR adjust the logic for audio & subtitle track selection for TV
series. Instead of only saving for future episodes if the newly selected
track is a different language than the user's preferred, always save it.
Basically this means if you change the audio or subtitle language for an
episode, the same audio or subtitle language will be used for future
episodes (if available).
Also, fixes the "default" subtitle mode to take the user's preferred
language into consideration.
## Issues
Follow up to https://github.com/damontecres/Wholphin/issues/378 &
https://github.com/damontecres/Wholphin/issues/427Fixes#469
**This is for MPV playback backend only!** Since this only works in MPV
for now, the option is not shown if using ExoPlayer.
Adds a setting (the gear icon during playback) to adjust the subtitle
delay.
A negative delay shows subtitles sooner. A positive delay shows them
later. For example, if a subtitle is supposed to show between
1000ms-2000ms, a negative 250ms delay will show it at 750ms-1750ms
instead.
The delay is persisted between plays of the same media & subtitle track.
Ref: #12

Moves the clipping of trickplay images from the image loading layer to
the composition/graphics layer.
Effectively, this PR loads and renders the entire trickplay image which
is then moved around to the right position and finally is clipped so
only a single tile is shown. Since moving the image around on screen
plsu clipping is 1) very fast and 2) done during recompositions, this
means it is _much_ faster at rendering each trickplay tile image and
Coil only has to load the image once.
Fixes#432
Use the SDK to get the device name since it handles older Android
versions and has a fallback.
This should not change the device ID or name if the device was working
before this change.
Maybe related to #454
Revert some of the changes to input text fields from #327. This PR
switches back to using the `value/onValueChange` variants for single
line input.
Fixes#413
Use current thread's looper or fallback to the main looper for the
display change callback.
This is the behavior in newer versions of Android whereas older versions
assumed a non-null looper.
Fixes#446
Adds a view options button for the TV guide to allow some basic
customization:
* Show details header
* Adjust how channels are sorted
* Toggle color coding for programs
Related to #250Closes#288Fixes#439
On the home page or any grid, if focused on a "playable" item (movie,
episode, etc), pressing the Play or Play-Pause button on the remote will
immediately start playback.
Dev note: Also guards against an edge case where an item passed into
playback doesn't have media sources due to more limited item fields in
the query.
This only applies if the user has set a preferred audio and/or subtitle
language on the web UI!
If the user chooses an audio/subtitle track that is a different language
than their preferred, it will be remembered for that series. Playback of
future episodes of the series will pick a matching language.
The audio & subtitle selections are independent. You can make the choice
before playback or during playback.
Closes#378
- Semi-bold the movies or series title on the details pages to match
home page
- Shows total subtitle track count if subtitles disabled
- Remove the springy animations from the play/watch/favorite/more
buttons
- Go back a page when playback finishes and there's nothing left in the
queue
Closes#393
Unfortunately, I think the PIN and automatic sign-in features require
more thought before it should be rolled out as a stable feature.
This PR disables the two settings and removes them from the UI.
See discussions in #321 and will reopen#268
Adds an advanced playback setting to enable automatic refresh rate
switching during playback
When enabled, the app looks at the available display modes, chooses the
best candidate, and attempts to switch to it. After playback ends, the
display is switched back to the original mode.
Closes#62Closes#379
Better handling for combining continue/next-up by considering the
premiere date of the episode. So episodes will use the previously
watched one's last played date or the episode's premiere date, whichever
is newer.
This means when a new season comes out or the show returns from a break,
the new episodes will show earlier in the list like the next up rows
shows when not combined.
Fixes#377
Adds a new button implementation which allows for icon buttons to be
circular. Overall the buttons are reduced in size.
Also, the playback controls are are reduced in size, buttons are smaller
and vertical padding is reduced.

Adds a settings allowing for adjusting the thickness of unstyled
subtitle outlines, if outline is enabled.
This is applied for both ExoPlayer & MPV
Closes#179

Dev note: the ExoPlayer implementation is bad code, don't write code
like this. It works though
- Fixes a crash if the same person has multiple roles in an item
- Better tab focus behavior, especially on series pages
- Use consistent series production years on all pages
- Slightly decrease size and increase space on home page
Allows for setting a per-user profile PIN that must be entered to switch
to the user. The PIN is a sequence of directional buttons, ie D-Pad
left/right/up/down.
This PIN is per device per user per server. The PIN is stored locally
and not sent nor shared with the server. If you forget your PIN, can
still access the app using server credentials which also removes the
PIN.
Additionally, there is another setting to disable automatically sign in.
When enabled (default) the last user is automatically restored, even if
there is a PIN. If disabled, a server & user must always be selected
when opening the app.
Closes#268Closes#321
## Details
Adds options to customize how grid pages are displayed. Every grid page
can be individually customized.
The options are saved per user on the device and restored when
navigating back to the page. The pages that support it have a new button
next to the sort/filter buttons and changes are updated live.
The options are:
* Primary or Thumb image
* Aspect ratio
* Show details header w/ backdrop (similar to the one on the home page)
* Number of columns
* Spacing between cards
* Card image content scale (eg Fit, Fill, Crop, etc)
Note: the Genre tabs' grids cannot be customized yet
### Other changes
- Nav drawer now opens over top of content instead of pushing it
- Images for most cards are fetched with actual fill size
### Possible future work
* Customize Genre grids
* Add more image types, banners?
* Add similar customization options to pages with rows, eg Home page and
Recommended tabs
## Example
For example, this is a movie library using thumb images with 8 columns
and reduced spacing. It also shows the details header
## Issues
Closes#186
Related to #72
Fixes thread used by changes from #359
Also create the device direct play profile lazily-ish off of the main
thread because this can take some time to create.
Should help address some of the "too much work" reported in #363, and
also fixes the network-on-main exception in the logs on #363
## Details
This PR contains a number of optimizations
### Images
Updates to request images with maximum sizes
The max sizes are estimates on the maximum number of pixels that will be
actually displayed. This means the server is downscaling large images
which means 1) better quality downscaling and 2) smaller images sent
over the network and cached
This should mean slightly better quality images and more images that can
be cached locally
### Paging
Reduces network calls when fetching paginated data, ie grid pages and
some rows. This PR combines the total record count & first page queries
which slightly reduces total time to load paginated data.
Additionally, on movies/tv show recommended pages, the rows now only
fetch a limited number of items (same as number on home page). While
they previously fetched using pagination, there is an overhead required
to paginate requests, so this is slightly faster.
### Async recommended fetching
On movies/tv show recommended pages, the rows are now fetched completely
separately from each other. Previously, the rows were requested
separtely, but each was only added to the UI in order as each completed.
Now they will add as the data arrived.
Note: Top unwatched TV shows is still very slow on `10.11.x`, see #204
### Image cache
The local, on-disk image cache size is increased from 100mb to 200mb.
Combined with the smaller images, this covers about 1500-2000 poster
images, but will vary by server.
The cache size is now also configurable in advanced settings from 25mb
to 1000mb. Additionally, you can see how much of both the disk and
memory caches are in use.
## Issues
Closes#330
Related to #120
I need to test this more
Fixes the imeAction for search
Initiating the search will move focus down if results are available
Pressing back while focused on the search text box will move focus to
either the results or the nav drawer. I think will will help devices
that do not dismiss on the soft keyboard on back.
Might fix#311
Adds support for Android TV 6.0 & 7.0 by reduce the min SDK to 23. Going
below 23 isn't practical since new releases of Jetpack libs require 23+.
Also includes some clean up for better gradle syncs & actually use the
datetime APIs available from desugaring on older versions.
Needs more testing before merge
Closes#341
Adds a toggle to increase the verbosity of the app logs for
release/production builds
Most users won't need to use this, but it can be helpful when [gathering
app
logs](https://github.com/damontecres/Wholphin/wiki/Gathering-app-logs)
to report an issue.
Fixes an edge case on the home page where having at least two rows that
don't have any latest items (such as collections or playlists) in
between two regular libraries (as configured on your home profile
settings) would prevent scrolling up from the second regular library
- Keep the currently selected nav drawer item in view and focus on when
entering
- Adjust star rating height so that the row is consistent preventing
resizing when moving between episodes with & without a rating
- Simplify checks when switching to a new server and use more specific
keys to prevent needing to switch servers twice
Adds a subtitle style option to set the margin below unstyled subtitles.
This is roughly the percent of the screen below the subtitles. It
applies to both ExoPlayer & MPV.
Also fixes font & background opacity not being applied in some cases.
Fixes#225Closes#178
Adds a "default" sort option for collections/boxsets. This makes the app
request no sort at all and leaves it up to the server to order the
items.
Fixes#83
Filters out TV programs that end before the guide start time. In theory,
the server shouldn't return these, but it seems to for some users.
Related to #276
Adds a simple fallback page to show an episode in cases when it doesn't
have an associated season due to missing metadata,see #280.
The missing metadata is probably a server bug, but it's good to have a
fallback anyway.
- Move loading indicator to center of grid when changing filters so it's
more obvious
- Move 'remove' filter to top for the first filter dropdown for
consistency
- Use theme color for download progress indicator
- Show playback controls at the end of playing a video if there's
nothing up next
Follow up to #279Closes#162
Move the stream labels to the top under the dot-separated details. Also
move the rating up to the dot-separated details line and add it to home
page.
## Details
This PR updates the detail pages for Movies & TV Shows.
- Increase backdrop image size & brightness (now matches home page)
- Increase max width for titles
- Decrease max width for detail text
- Switch to using a customized display labels for video, audio, &
subtitle streams
Additional change for movie pages:
- Don't dim movie details text
- Move video stream details below play buttons
- Move first row up higher
### Labels
Instead of using the server provided `DisplayTitle`, create display
labels in-app. In general, this shortens the text without losing
significant detail. It is similar to the official app.
Some of the changes:
- Don't show "SDR", only "HDR"
- Simplify audio channel info, eg "Surround 5.1" => "5.1"
- Show chosen subtitle language
- Show chosen subtitle codec
- Show "(+#)" if there are additional audio or subtitle tracks
- Use gray background slightly tinted by the theme color
## Issues
Closes#239
Adds a button to filter contents next to the sort button on grid pages
The filters are saved and restored you return to the page. It works with
#278, playing the filtered items in the sorted order.
The drop down menus for both sorting & filtering are updated to closer
match the overall look.
Filters for:
- Played or unplayed
- Favorite or not
- Genres
- Community (star) rating
- Parental rating
- Video type (HD vs SD, etc)
- Release year and/or decade
Adds ability to play all (or shuffle play) most libraries, collections,
folders, TV series, TV seasons, etc
This adds buttons to play all or shuffle play at the top of grid pages
that support. Additionally, a shuffle button is added to TV Series
details. And play all or shuffle play is added when long clicking TV
seasons.
Note: Until #42 is completed, only the first 100 items will play
back-to-back
Closes#197
Changes the skip segment button text to use a single phrase instead of
concatenating two strings. This allows correct context when translating.
Follow up from #292
Use the server scoring API when adding a new server. This allows just
entering a hostname or IP address without the scheme (`http`/`https`) or
port in most cases.
Note: non-standard ports would still need to be provided
Closes#5Closes#296
## Details
This PR updates the device profile used by ExoPlayer. It should more
accurately report support for HDR formats.
## Acknowledgements
This PR contains a commit (cd6351912af72d274b0baf50af4d777b986f0f46)
that has code copied and slightly modified from
https://github.com/jellyfin/jellyfin-androidtv `v0.19.4`.
Major acknowledgement & credit to the Jellyfin Android TV developers for
this code!
## Issues
Related to #15
Adds "Bold Blue" and Black/OLED themes
Also tweaks Orange & Green a bit to make the borders stronger and
backgrounds darker/less colored
Related to #91
Fixes the nav drawer not being focused on when pressing back to open it
on the home page
Dev notes: I think the bug was introduced in #228 due to the new
`Column` not being focusable, but we don't need it to be since we want
to focus on the selected page.
## Details
This PR ended up being larger than I meant. It contains some UI changes
and code organization.
## TV & DVR
- Uses the "Recordings" library position to order "Recently added in
Recordings" instead of Live TV position
- The times at the top of the TV guide are more distinguishable from
each other
- Removes the duplicate title on the Live TV->Recordings tab
## Other libraries
- Bare-bone support for "Music Video" libraries. Full support for "Music
Video" is work-in-progress in #267, but the libraries should be
browseable now.
- Bare-bone support for "Other" (aka Mixed Movies & TV Shows) libraries.
Note: this type of library is considered [broken and
deprecated](https://jellyfin.org/docs/general/server/media/mixed-movies-and-shows),
so this won't be expanded any further
## Grids
Updates various grid types to use poster vs wide cards. For example,
Favorite TV Episodes will use episode images in a wide card now.
## Issues
Closes#249Closes#271
## Details
Adds support for a few more media remote buttons
Adds support for common controller buttons
### Dev notes
Lifts the playback dialogs to the top level allowing them to be called
from anywhere during playback
## Issues
Closes#260
If playing with transcoding, changing tracks during playback will
continue to transcode instead of possibly switching to direct play.
Also support embedded subtitles that are being delivered externally.
Fixes#256Fixes#257
Reduces the size of the rows on the TV guide allowing for 7-8 rows to
show. Also hide the tab row if scrolled down on the guide.
Move the "watch live" button to be initially focused
Channels are sorted with favorites appearing first
Back button behavior is tweaked to move to start of row first, then top
of the guide, then normal back behavior
Closes#259
Related to #250
`PlaybackViewModel` was becoming very unwieldy at over 1,300 LOC. This
PR breaks it up and simplifies some fields so it's easier to work with.
There are no user facing changes
Adds grid layouts specifically for playlists and collections/boxsets
instead of falling back to the generic grid.
In #238, the generic grid was updated to show 4 columns of 16x9 cards.
Now, collections will use 2x3 (poster) cards (like Movies) and playlists
will use square cards.
This also means collections & playlists have more relevant sort options.
Fixes#243Closes#226
A real implementation for combining the `Continue Watching` and `Next
Up` rows on the home page.
It requires a query for every next up item, so it could become slow
especially for many items and larger servers. The app makes a best
effort to cache the information.
The order is determined by querying each episode in next up to associate
it to its immediately previous episode's last played date. Then both
continue watching & next up items are reversed sorted by the last played
data. So this only makes sense if you watch series in order.
Closes#19
## Details
Adds the logo (if available) to the upper left corner of the playback
controls overlay.
I am still experimenting with how logos can be used on the details
pages. It's problematic because of the variety of colors, eg dark logos
can be hard to see on the dark background.
## Issues
Related to #92
## Screenshot
<img width="1280" height="771" alt="logo_playback Large"
src="https://github.com/user-attachments/assets/c6cb43fe-f3bc-42a4-9ef4-0397267e3073"
/>
- General video library grid now shows 16:9 cards
- Ensures the posters on Movie/TV show grid pages fill the bounds (TVDB
posters are often very slightly wider than 2x3)
- Always crossfade home page backdrop image
Related to #66
Fixes an issues where if the playback overlay is showing and there is a
queue of items, then holding the D-Pad down button would scroll down but
visually lose focus on the queue items.
On the series overview page, when moving off of the episode row, such as
to do the play/more buttons, the other episodes will now become dimmer
to indicate which episode you're interacting with.
Also, when moving back to the page from the nav drawer, focus is
restored to where it left from. This is useful when scrolling fast left
on the season tabs and you overshoot onto the nav drawer, moving right
once will always move back to the season tabs.
In this example screenshot, Episode 3 is selected, so the others are
darker.
<img width="1280" height="771" alt="dim Large"
src="https://github.com/user-attachments/assets/fa21ade4-3345-4e97-873f-d654123a1ae5"
/>
This is a follow up from #158
Updates to increase amount of memory available during CI builds. I think
this will help with the intermittent `A failure occurred while executing
"com.android.build.gradle.tasks.PackageAndroidArtifact$IncrementalSplitterRunnable`
CI errors.
No user facing changes
Experimental MPV player backend
Related to #14, #22, & #85
You can switch to MPV in advanced settings and toggle using hardware
decoding or not.
This uses code and buildscripts from
https://github.com/mpv-android/mpv-android, plus other third party
libraries to build MPV
## Description
Shows local extras (clips, interviews, etc) for movies and tv shows
Extras are grouped by type and clicking on the type will go to a grid
page listing all of the items. However, if there is just one of that
type, it is shown in-line.
Trailers still have on their own row. And theme songs & theme videos are
not shown.
## Issues
Closes#123
Fix image aspect ratios for episode cards that differ from the video's
aspect ratio.
Also use the video's aspect ratio for chapter cards instead of assuming
16x9.
Dev: Create constants for the main ratios used
Fixes#215
Fix an issue where if there are no items for the "Continue Watching"
row, the tab row would disappear
Fix an issue where some rows (eg Recently released, recently added,
suggestions, etc) on the recommended pages might never show up
Fixes#207
Previously if the activity tracker got an error from the server during
one of the reports, the timer thread would stop and no further reports
would be sent.
This PR fixes that so it will continue to attempt to send reports since
if playback is continuing without error, the report error is likely
transient. For example, I observed the server returning an error during
its DB optimization scheduled task.
I think this will fix#208
Now when initiating a user or server switch, you can't back out of it
and must select a user/server. This will be useful when PIN code are
implemented down the road.
The app will now disconnect the web socket when backgrounded.
Also lots of internal refactoring
Just adds some logging while navigating around the app. If an error or
crash occurs and users want to share their logs, this information is
useful to know what page.
Logs are never sent without explicit user content and they are only
shared with the Jellyfin server the app is currently connected to. [See
here for more
details](https://github.com/damontecres/Wholphin/wiki/Gathering-app-logs#privacy)
Also stops theme music when the app is paused similar to #191
Refactors lifecycle event observers
When the app is "paused", aka not actively focused, but still visible
(such as when opening Android TV OS menu), playback will pause.
When the app is "stopped", aka not visible (such as pressing Home
button), playback will stop, report playback progress, and return to the
previous screen.
I think this will fix#181, but I don't have a controller to test with
Separates the loading logic into two groups:
1. Continue Watching & Next Up
2. Latest media
The home page displays once the first group is ready and shows
placeholders while the latest media is fetched. This also speeds up
refreshes when returning home after watching an episode.
Also ensure grouped Movie/TV libraries are displayed as Movie/TV
libraries instead of falling back to a generic grid display.
Fixes#189
Previously all of the rows on the recommended tab for Movies & TV Shows
were loaded sequentially before any results were shown. This is not a
great experience because some of the queries (eg top rated unwatched)
can take a while sometimes.
This PR starts displaying content once the first 1 or 2 rows is loaded
and then shows placeholders while the rest are querying.
Finally the row queries are now mostly independent so if one errors out,
the rest of the rows will still be displayed.
Closes#180
Fixes#184
Data was being fetched, but the UI wasn't being properly notified to
display the data.
This PR also adjusts so that "focused episode header" still takes up the
space even if the focused episode hasn't fully loaded yet. This prevents
the UI from jumping up and down.
Also #42 is _not_ fixed by this PR because it is a different issue.
## Changes:
- Hopefully finally fixes next up card image issues
- Remove the slider wraparound behavior in settings
- Removes the shadows/blurry effect on movie & person titles
- Adds the official rating to the home page
- Consistently order the official rating on home, movie, & series pages
- Add 1.75x speed for playback
- Don't time out the audio track selection/playback speed/video scale
dialogs
## Issues
Closes#166Closes#168Closes#169
Related to #162
Follow up to #167
Closes#152
Adds a setting to enable one-click-pausing via the D-Pad center button.
It is disabled by default.
If enabled, when the video is playing, it will be pause and show the
controls which is just like pressing the Pause or Play/Pause button.
If enable, when the video is paused and controls is not showing, it will
resume playback including using the skip back on resume setting.
Various fixes:
* Fixes misnamed "Continue watching" home row title
* Fix person page layout to account for the new clock
* Switch the 5 star rating to a decimal & icon
Additionally, the tab row has been rewritten to allow for:
* Always left justify tab rows
* Always scroll tab rows to ensure the currently selected index is
showing
* Always start focus on currently selected index when entering tab rows
Closes#154
Adds a settings to show the current time in the upper right corner.
If enabled, it will also show the end time for the playback on the
overlay.
Fixes#110
Switches loading the EPG/TV Guide data to lazy loading. The app will
load all of the channels, but only keep ~100 channels worth of data in
memory. As the user scrolls up and down, new channel program data will
be fetched as needed.
Eventually this technique can be applied to supporting more than 48
hours of data.
The logic for determine which program to focus on is also simplified and
more of it is delegated to composition.
Credit to @joshjryan for #131 hinting to use a different API endpoint to
avoid 414 errors.
Closes#97
Adds an option to force an item to play by transcoding. This can be
useful in the few cases where the device reports that it supports direct
play for a file, but it really doesn't.
Also rearranges the "More" dialog list to move picking audio & subtitle
track near the top.
Closes#148Closes#83
Remember the chosen sort order & direction per user profile on device.
Also adds a few more sort options.
This is only saved on the device and not saved/loaded from the server.
Closes#133Fixes#142
Theme songs will play for both TV shows and movies, if available. Also,
if there are multiple theme songs, one will be picked randomly instead
of always playing the first.
Additionally, the music now waits to start until the UI has loaded.
Fixes#144
The error was occurring when querying for server information. The
endpoint used returns information about previous installations and seems
some may be missing a field marked as required in the API. This causes
the deserialization error.
Since the query is just to get the server's name for the UI, we can use
the public server info endpoint instead and also wrap it in a try-catch
since it's not absolutely required.
Additionally, this PR adds exceptions for cases when necessary
information is not available (user id, access token, etc).
Adds an option to the show the next up during the end credits (outro)
instead of at the very end of the video.
The next up pushes up from the bottom and the video scales down to fit
above.
Still a WIP though
### Sample screenshot
<img width="1280" height="771" alt="next_up_credits Large"
src="https://github.com/user-attachments/assets/9a03d0ec-325d-4efe-a28e-51270ddaaa89"
/>
If the user has picked a specific audio track, then downloads a new
external subtitle, the audio track index must be adjusted (add 1) to
account for the shift in track indices.
This is because external subtitles are always indexed first.
Closes#95
Adds a setting (defaults to enabled) to automatically go to a page when
the navigation drawer item is focused after a brief delay.
Basically, if you open the left nav drawer, and scroll down to "Movies"
after a brief delay (600ms), the app will switch to the Movies page
without having to click.
It can be disabled in advanced settings.
When clicking the overview for an item, the dialog now shows details
about each file/media source including video, audio, & subtitle
information.
It only shows the full file path if the user is an administrator.
An error would occur when switching audio or subtitle tracks during
playback, but would be ignored. This meant the tracks can't be changed.
#111 was too aggressive on switching dispatchers. The player can only be
accessed on `Main`, so this PR fixes that.
Fixes#113
- Ensures all audio and subtitle options are shown during playback
- Fix home rows not being at top when going back
- Show error message when background home page refresh fails
Also shows more details for login failures and adds a link to the debug
page (with log info) after multiple login failure attempts. Hopefully
this can help diagnose #112 and #18
Related to #103
Adds ffmpeg decoder module to support a few extra audio formats such as
AC3, EAC3, DTS, & TrueHD.
This increases the apk size by about 5.4mb (release 17.4mb->22.8mb), but
definitely worth it for the added support.
Fetch the theme song info on the IO thread
This would throw an exception on some devices, but it would be caught
without affecting the app, other than just not playing theme music.
Simplifies the process for finding the right season id & episode id
after clicking on an episode. This can also improve loading time for
seasons that have a large number of episodes (hundreds).
Also fixes handling for "unknown" seasons which don't have an
index/season number.
Fixes#99
While not an actual error, it does look like a transitive dependency of
the Jellyfin Kotlin SDK may log using slf4j, so it would be good to
include an slf4j implementation just in case.
Related to #64
If an error occurred while querying for a season's episodes, the app
would crash instead of display the error message.
This PR fixes the app crash, but not the underlying query issue. It also
changes the error page to show a button for sending logs to the server
immediately.
2025-10-28 17:04:15 -04:00
3355 changed files with 195932 additions and 13477 deletions
<!-- By submitting this pull request, you acknowledge that you have read the [contributing guide](https://github.com/damontecres/Wholphin/blob/main/CONTRIBUTING.md, including the AI/LLM policy, and [developer's guide](https://github.com/damontecres/Wholphin/blob/main/DEVELOPMENT.md) -->
## Description
<!-- Describe the changes in detail -->
### Related issues
<!-- If this is a new feature or a change, there must be a discussion in an issue first, reference the issue here -->
<!-- If fixing a bug, reference the bug issue here, or describe the bug, including steps to reproduce -->
### Testing
<!-- Describe how this change was tested and on what device(s) -->
## Screenshots
<!-- Please include screenshots if the PR alters any UI elements -->
## AI or LLM usage
<!-- If you used any AI or LLM assistance, please list where in the code and how you tested it -->
echo '### `${{ env.BRANCH_NAME }}` development build
This pre-release tracks the development debug build of Wholphin from the `${{ env.BRANCH_NAME }}` branch.
This pre-release tracks the development build of Wholphin from the `${{ env.BRANCH_NAME }}` unstable branch.
See https://github.com/damontecres/Wholphin/releases/latest for the latest stable release.
See https://github.com/damontecres/Wholphin/releases/latest for the latest stable release.
If you want to update to this version in-app, change Settings->Advanced->Update URL to https://api.github.com/repos/damontecres/Wholphin/releases/tags/${{ env.TAG_NAME }} (replace `latest` with `tags/${{ env.TAG_NAME }}`).
If you want to update to this version in-app, change Settings->Advanced->Update URL to https://api.github.com/repos/damontecres/Wholphin/releases/tags/${{ env.TAG_NAME }} (replace `latest` with `tags/${{ env.TAG_NAME }}`).
[See changes since `${{ env.LATEST_TAG }}`](https://github.com/damontecres/Wholphin/compare/${{ env.LATEST_TAG }}...${{ env.TAG_NAME }})
@ -12,6 +12,7 @@ You can contribute in several ways:
* File bug reports
* Request new features
* Propose code changes via pull request
* Submit translations to https://translate.codeberg.org/engage/wholphin/
In all cases, be sure to fully explain the issue.
@ -22,3 +23,10 @@ You acknowledge that, if applicable, submitting and subsequent acceptance of any
Please be thoughtful when contributing code and consider the long term maintenance/support aspect for any new features.
See the [developer's guide](./DEVELOPMENT.md) for more information.
#### Policy for AI or LLMs
Wholphin has adopted the [Jellyfin LLM/"AI" Development Policy](https://jellyfin.org/docs/general/contributing/llm-policies/). AI/LLM assisted code contributions are permitted under the policy, but the author must undertand and be able to explain the code changes in their own words.
At a minimum, you must disclose the usage any of AI/LLM assistance and you must write the pull request description _without_ AI assistance.
* [Room](https://developer.android.com/training/data-storage/room) & [DataStore](https://developer.android.com/topic/libraries/architecture/datastore) for local data storage
* [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) for dependency injection
* [Media3/ExoPlayer](https://developer.android.com/media/media3/exoplayer) for media playback
* [MPV/libmpv](https://github.com/mpv-player/mpv) for media playback
* [Coil](https://coil-kt.github.io/coil/) for image loading
* [OkHttp](https://square.github.io/okhttp/) for HTTP requests
@ -23,4 +24,82 @@ We follow GitHub's fork & pull request model for contributions.
After forking and cloning your fork, you can import the project into Android Studio.
You need a compatible Android Studio version for the configured AGP. This is generally `Narwhal 3 Feature Drop | 2025.1.3` or newer. See https://developer.android.com/build/releases/gradle-plugin and [`libs.versions.toml](./gradle/libs.versions.toml).
### Development environment
It is recommended to use a recent version of [Android Studio](https://developer.android.com/studio). Make sure the [version is compatible](https://developer.android.com/build/releases/gradle-plugin#android_gradle_plugin_and_android_studio_compatibility) with Wholphin's AGP version.
Code formatting should follow [ktlint's](https://github.com/pinterest/ktlint) rules. Find the `ktlint` version in [`.pre-commit-config.yaml`](./.pre-commit-config.yaml). Optionally, install the [ktlint plugin](https://plugins.jetbrains.com/plugin/15057-ktlint) in Android Studio to run automatically. Configure the version in `Settings->Tools->KtLint->Ruleset Version`.
Also, it's recommend to add an extra ruleset jar for Compose-specific KtLint: https://mrmans0n.github.io/compose-rules/ktlint/#using-with-ktlint-cli-or-the-ktlint-intellij-plugin
Also setup [pre-commit](https://github.com/pre-commit/pre-commit) which will run `ktlint` as well on each commit, plus check for other common issues.
#### Extensions
Wholphin uses several native components for extra playback compatibility. This includes Media3 ffmpeg/av1 decoders and `libmpv`. These extensions are not required to build the app, but without them some functionality will not work.
If you want to include these in a local build, see the [instructions here](https://github.com/damontecres/wholphin-extensions?tab=readme-ov-file#usage) for configuring the repository.
You can also build the extensions locally from https://github.com/damontecres/wholphin-extensions and include them in `app/libs`. The gradle build dependency resolution prefers these local files over fetching from the remote maven registry.
Finally, if no `wholphin-mpv` implementation is found, `wholphin-mpv-stub` will be used. This allows the app to compile, but any runtime usage of MPV will throw an exception.
## Code organization
Code is split into several packages:
- `data` - app-specific data models and services
- `preferences` - Non-UI related code for user settings and preferences
- `services` - hilt injectable services often used by ViewModels for API calls
- `ui` - User interface code and ViewModels
- `util` - Utility classes and functions
### Settings
There are a few different ways user settings are stored:
1. `AppPreferences` via DataStore
2. Room database
3. Key-Value DataStore
#### App preferences
These are generally settings that apply across the whole app regardless of the currently active user.
The `AppPreferences` object can be retrieved from the `UserPreferencesService` or directly via injecting `DataStore<AppPreferences>`.
The `AppPreference` (note the `s` differences) objects are used to create the UI for configuring settings using the composable functions in `com.github.damontecres.wholphin.ui.preferences`.
##### How to add a new app setting
1. Add entry in `WholphinDataStore.proto`& build to generate classes
2. Add new `AppPreference` object in `AppPreference.kt`
3. Add new object to a `PreferenceGroup` (listed in `AppPreference.kt`)
4. Update `AppPreferencesSerializer` to set the default value for new installs
5. If needed, update `AppUpgradeHandler` to set the default value for app upgrades
- Since preferences use proto3, the [default values](https://protobuf.dev/programming-guides/proto3/#default) are zero, false, or the first enum, so only need this step if the default value is different
#### Room settings
These are settings were generally are applied per user. They are stored in several different tables. The entities are defined in `com.github.damontecres.wholphin.data.model`.
Additionally, all server and user info for both Jellyfin and Seerr are stored in Room tables.
Some examples are:
- `JellyfinServer`
- `JellyfinUser`
- `SeerrServer`
- `LibraryDisplayInfo`
- `ItemPlayback`
Some of the models, such as `GetItemsFilter` or `ViewOptions`, are stored as JSON in a column instead of defined entities. This should be used sparingly.
#### Key-value settings
These settings are similar to Room ones and are usually stored per user. These can be accessed via `KeyValueService`. The values are `@Serializable` objects saved as JSON.
This should be used sparingly.
#### Experimental
Some settings are considered "experimental". These may cause instabilities or may need further testing. They may be changed or removed any time.
These are stored in the `ExperimentalPreferences` object in `AppPreferences`. Accessing them should always be gated with `ExperimentalPreferences.enabled`. There are a few helper functions for this purpose.
> This feature is experimental and the exact behaviors and/or parameters may change at any time
Wholphin accepts [intents](https://developer.android.com/guide/components/intents-filters) to load or play specified media.
Parameters can be specified as "extras" (eg `--es query value`) or as query parameters to the data URI (eg `-d wholphin://search?query=value`).
UUID parameters can be specified with or without hyphens.
If using data URI query parameters on the command line, you need to escape ampersands: `itemId=...\&userId=...`
## Server/user switching
You can choose a server and user to switch to with the `serverId` and `userId` UUID parameters. If the profile is not protected, the app will switch to it.
If not specified, the app will use the current user if auto sign in is enabled.
## Search
Action: `android.intent.action.SEARCH`
Shorthand action: `search`
Parameters:
- `query` - Optional, prepopulate the search field
Wolfskin is a personal fork of [Wholphin](https://github.com/damontecres/Wholphin) by damontecres.
Licensed under GPL-2.0, the same licence as upstream (see [LICENSE](LICENSE)).
Wolfskin is an Android TV client for one household. It is released through Forgejo releases on this repo, and it tracks upstream by merging each upstream release tag.
The app uses Wolfskin launcher artwork and a lilac-on-`#07070a` theme by default on a fresh install. Existing theme choices and all upstream colour themes remain available under Settings → App theme. Settings → About credits and links to Wholphin by damontecres.
---
# Wholphin - an OSS Android TV client for Jellyfin
> "Never half-phin two jellies. Always wholphin one jelly."
Wholphin is an open-source Android TV client for Jellyfin. It aims to provide a different app UI that's inspired by Plex for users interested in migrating to Jellyfin.
This is not a fork of the [official client](https://github.com/jellyfin/jellyfin-androidtv). Wholphin's user interface and controls have been written completely from scratch. Wholphin uses the same media player (media3/ExoPlayer) as the official client.
This is not a fork of the [official client](https://github.com/jellyfin/jellyfin-androidtv). Wholphin's user interface and controls have been written completely from scratch. Wholphin supports playing media using either ExoPlayer or MPV.
- A navigation drawer for quick access to libraries, search, and settings from almost anywhere in the app
- Show Movie/TV Show titles when browsing libraries
- Play TV Show theme music, if available
- Plex inspired playback controls, such as:
### User interface
- Customize the home page to see the content you are interested in
- Use poster or thumb images, show/hide titles, add/remove/re-order different types of rows!
- Option to combine the Continue Watching & Next Up into a single row
- Pin collections, playlists, favorites, genres, and studios
- Remove series from next up
- A navigation drawer for quick access to libraries, favorites, search, and settings from almost anywhere in the app
- Integration with [Seerr](https://github.com/seerr-team/seerr) to discover new movies and TV shows
- Note: only available when installed from [GitHub](https://github.com/damontecres/Wholphin/releases/latest) or the [Play store](https://play.google.com/store/apps/details?id=com.github.damontecres.wholphin)
- Customize library display
- Option to show Movie/TV Show titles
- Choose image types & size
- Choose from grid or list layouts
- Play theme music, if available
- Customize subtitle style for plain text subtitles with separate HDR settings
- Search & download subtitles (requires compatible server plugin such as [OpenSubtitles](https://github.com/jellyfin/jellyfin-plugin-opensubtitles))
- Multiple app color themes
- Protect user profile switches with PIN code or require server login
- **MPV** for direct playing anything with great SSA/ASS subtitle support
- Plex inspired playback controls:
- Using D-Pad left/right for seeking during playback
- Quickly access video chapters during playback
- Quickly access video chapters & queue during playback
- Optionally skip back a few seconds when resuming playback
- Other (subjective) enhancements:
- Subtly show playback position along the bottom of the screen while seeking w/ D-Pad
- Force Continue Watching & Next Up TV episodes to use their Series posters
- Live TV & DVR support
- Music & music video support
- Cinema mode/pre-roll intro support (requires compatible server plugin such as [Intros](https://github.com/jellyfin/jellyfin-plugin-intros))
- Auto play next episodes with pass out protection
- Option for automatic refresh rate & resolution switching on supported displays
- Trickplay (video preview) support
- Subtly show playback position along the bottom of the screen while seeking w/ D-Pad
### Roadmap
@ -27,6 +85,8 @@ See [here for the roadmap](https://github.com/damontecres/Wholphin/wiki#roadmap)
## Installation
Using [Google Play](https://play.google.com/store/apps/details?id=com.github.damontecres.wholphin) or [Amazon appstore](https://www.amazon.com/gp/product/B0G8RQQR9T/ref=mas_pm_wholphin) are the fastest way to install. But you can follow these instructions to install without needing an app store
1. Install the APK on your Android TV device with one of these options:
2. Install the APK on your Android TV device with one of these options:
- Install a browser program such as [Downloader](https://www.aftvnews.com/downloader/), use it to get the latest apk with short code `8668671` or URL: http://aftv.news/8668671
- Download the latest APK release from the [releases page](https://github.com/damontecres/Wholphin/releases/latest) or http://aftv.news/8668671
- Put the APK on an SD Card/USB stick/network share and use a file manager app from the Google Play Store / Amazon AppStore (e.g. `FX File Explorer`). Android's preinstalled file manager probably will not work!
@ -47,18 +107,24 @@ After the initial install above, the app will automatically check for updates. T
The first time you attempt an update, the OS should guide you through enabling the required additional permissions for the app to install updates.
Note: if installed via an app store, the app store will handle updates.
## Compatibility
Requires Android 7.1+ (or Fire TV OS 6+) and Jellyfin server `10.10.x` (tested on primarily `10.10.7`).
Requires Android 6+ (or Fire TV OS 6+) and Jellyfin server `10.10.x` or `10.11.x` (tested on primarily `10.11`).
The app is tested on a variety of Android TV/Fire TV OS devices, but if you encounter issues, please file an issue!
Seerr integration is generally tested with the latest stable version. Older versions may not work as expected. The Seerr server should configured with integration to your Jellyfin server for full compatibility.
## Contributions
Issues and pull requests are always welcome! Please check before submitting that your issue or pull request is not a duplicate.
If you plan to contribute, please read the [contributing guide](CONTRIBUTING.md)!
You can [help translate Wholphin](https://translate.codeberg.org/engage/wholphin/)!
## Acknowledgements
- Thanks to the Jellyfin team for creating and maintaining such a great open-source media server
@ -68,11 +134,21 @@ If you plan to contribute, please read the [contributing guide](CONTRIBUTING.md)
"description":"an implementation of the line breaking and word/grapheme breaking algorithms as described in Unicode Standard Annex 14 (UAX #14) and Unicode Standard Annex 29 (UAX #29)",
THESOFTWAREISPROVIDED\"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'addc8fcbddd84a88e20fbda34849cf26')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '26bbe361923e3aac064c08c52e65264e')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e9bcae45448f995d1f4ebf291e45c510')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dea51adcc724179afa0174d775f97480')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f9b86b73e972aa002238d43ab2c8dcc2')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c6829d764ec85321ab3be9905d6c0e3a')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
},
{
"fieldPath":"requireLogin",
"columnName":"requireLogin",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"false"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '934c38e7f4eb68c9a223ddb5c7e3e35d')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
},
{
"fieldPath":"requireLogin",
"columnName":"requireLogin",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"false"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
},
{
"fieldPath":"uiLanguage",
"columnName":"uiLanguage",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a65eec85c027fac13948a0dd6f99d3cb')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
},
{
"fieldPath":"pin",
"columnName":"pin",
"affinity":"TEXT"
},
{
"fieldPath":"requireLogin",
"columnName":"requireLogin",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"false"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
},
{
"fieldPath":"uiLanguage",
"columnName":"uiLanguage",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"RememberedTab",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"index",
"columnName":"index",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_RememberedTab_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '278e485064fc9f184a2181599f0f8efb')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, `preferredAudioLanguage` TEXT NOT NULL DEFAULT '', `preferredSubtitleLanguage` TEXT NOT NULL DEFAULT '', `subtitleMode` TEXT NOT NULL DEFAULT 'USE_USER_PROFILE', FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"RememberedTab",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"index",
"columnName":"index",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_RememberedTab_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ff9ade58d4ac39e36335ac0df1405a5f')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, `preferredAudioLanguage` TEXT NOT NULL DEFAULT '', `preferredSubtitleLanguage` TEXT NOT NULL DEFAULT '', `subtitleMode` TEXT NOT NULL DEFAULT 'USE_USER_PROFILE', FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"RememberedTab",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"index",
"columnName":"index",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_RememberedTab_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"SeriesTrackChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `parentId` TEXT NOT NULL, `type` TEXT NOT NULL, `parentType` TEXT NOT NULL, `itemId` TEXT, `language` TEXT, `activation` TEXT NOT NULL, `trackFlags` INTEGER NOT NULL, `codec` TEXT, `relativeTrackIndex` INTEGER, `title` TEXT, `audioChannels` INTEGER, PRIMARY KEY(`userId`, `parentId`, `type`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"parentId",
"columnName":"parentId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"parentType",
"columnName":"parentType",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"language",
"columnName":"language",
"affinity":"TEXT"
},
{
"fieldPath":"activation",
"columnName":"activation",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackFlags",
"columnName":"trackFlags",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"codec",
"columnName":"codec",
"affinity":"TEXT"
},
{
"fieldPath":"relativeTrackIndex",
"columnName":"relativeTrackIndex",
"affinity":"INTEGER"
},
{
"fieldPath":"title",
"columnName":"title",
"affinity":"TEXT"
},
{
"fieldPath":"audioChannels",
"columnName":"audioChannels",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"parentId",
"type"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b297c087485321c3ee8282ad1ff675a0')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, `lastUsed` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
},
{
"fieldPath":"lastUsed",
"columnName":"lastUsed",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, `pin` TEXT, `requireLogin` INTEGER NOT NULL DEFAULT false, `lastUsed` TEXT, `uiLanguage` TEXT, `preferredAudioLanguage` TEXT NOT NULL DEFAULT '', `preferredSubtitleLanguage` TEXT NOT NULL DEFAULT '', `subtitleMode` TEXT NOT NULL DEFAULT 'USE_USER_PROFILE', `c_homeSettingsSource` TEXT NOT NULL DEFAULT 'UNSET', FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `order` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"order",
"columnName":"order",
"affinity":"INTEGER",
"notNull":true,
"defaultValue":"-1"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"playback_effects",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, `rotation` INTEGER NOT NULL, `brightness` INTEGER NOT NULL, `contrast` INTEGER NOT NULL, `saturation` INTEGER NOT NULL, `hue` INTEGER NOT NULL, `red` INTEGER NOT NULL, `green` INTEGER NOT NULL, `blue` INTEGER NOT NULL, `blur` INTEGER NOT NULL, PRIMARY KEY(`jellyfinUserRowId`, `itemId`, `type`))",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"videoFilter.rotation",
"columnName":"rotation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.brightness",
"columnName":"brightness",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.contrast",
"columnName":"contrast",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.saturation",
"columnName":"saturation",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.hue",
"columnName":"hue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.red",
"columnName":"red",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.green",
"columnName":"green",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blue",
"columnName":"blue",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"videoFilter.blur",
"columnName":"blur",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"itemId",
"type"
]
}
},
{
"tableName":"PlaybackLanguageChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `seriesId` TEXT NOT NULL, `itemId` TEXT, `audioLanguage` TEXT, `subtitleLanguage` TEXT, `subtitlesDisabled` INTEGER, PRIMARY KEY(`userId`, `seriesId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"seriesId",
"columnName":"seriesId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"audioLanguage",
"columnName":"audioLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitleLanguage",
"columnName":"subtitleLanguage",
"affinity":"TEXT"
},
{
"fieldPath":"subtitlesDisabled",
"columnName":"subtitlesDisabled",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"seriesId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"ItemTrackModification",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `trackIndex` INTEGER NOT NULL, `delayMs` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`, `trackIndex`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackIndex",
"columnName":"trackIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"delayMs",
"columnName":"delayMs",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId",
"trackIndex"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"seerr_servers",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `name` TEXT, `version` TEXT)",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"id"
]
},
"indices":[
{
"name":"index_seerr_servers_url",
"unique":true,
"columnNames":[
"url"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_seerr_servers_url` ON `${TABLE_NAME}` (`url`)"
}
]
},
{
"tableName":"seerr_users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`jellyfinUserRowId` INTEGER NOT NULL, `serverId` INTEGER NOT NULL, `authMethod` TEXT NOT NULL, `username` TEXT, `password` TEXT, `credential` TEXT, PRIMARY KEY(`jellyfinUserRowId`, `serverId`), FOREIGN KEY(`serverId`) REFERENCES `seerr_servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`jellyfinUserRowId`) REFERENCES `users`(`rowId`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"jellyfinUserRowId",
"columnName":"jellyfinUserRowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"authMethod",
"columnName":"authMethod",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"username",
"columnName":"username",
"affinity":"TEXT"
},
{
"fieldPath":"password",
"columnName":"password",
"affinity":"TEXT"
},
{
"fieldPath":"credential",
"columnName":"credential",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"jellyfinUserRowId",
"serverId"
]
},
"foreignKeys":[
{
"table":"seerr_servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
},
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"jellyfinUserRowId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"RememberedTab",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `index` INTEGER NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"index",
"columnName":"index",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_RememberedTab_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_RememberedTab_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"SeriesTrackChoice",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `parentId` TEXT NOT NULL, `type` TEXT NOT NULL, `parentType` TEXT NOT NULL, `itemId` TEXT, `language` TEXT, `activation` TEXT NOT NULL, `trackFlags` INTEGER NOT NULL, `codec` TEXT, `relativeTrackIndex` INTEGER, `title` TEXT, `audioChannels` INTEGER, PRIMARY KEY(`userId`, `parentId`, `type`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"parentId",
"columnName":"parentId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"parentType",
"columnName":"parentType",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT"
},
{
"fieldPath":"language",
"columnName":"language",
"affinity":"TEXT"
},
{
"fieldPath":"activation",
"columnName":"activation",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"trackFlags",
"columnName":"trackFlags",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"codec",
"columnName":"codec",
"affinity":"TEXT"
},
{
"fieldPath":"relativeTrackIndex",
"columnName":"relativeTrackIndex",
"affinity":"INTEGER"
},
{
"fieldPath":"title",
"columnName":"title",
"affinity":"TEXT"
},
{
"fieldPath":"audioChannels",
"columnName":"audioChannels",
"affinity":"INTEGER"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"parentId",
"type"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1f2fe7912f9e3ab83b77029e07488e84')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '24390d64259f19e0e67ecfb827b974fb')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a4fd9e29c8cbcd13c24e8644f62e8551')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd4874262e325932fe00de49471288d82')"
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT, `url` TEXT NOT NULL, `version` TEXT, PRIMARY KEY(`id`))",
"fields":[
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"url",
"columnName":"url",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"version",
"columnName":"version",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"id"
]
}
},
{
"tableName":"users",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `id` TEXT NOT NULL, `name` TEXT, `serverId` TEXT NOT NULL, `accessToken` TEXT, FOREIGN KEY(`serverId`) REFERENCES `servers`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"id",
"columnName":"id",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"name",
"columnName":"name",
"affinity":"TEXT"
},
{
"fieldPath":"serverId",
"columnName":"serverId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"accessToken",
"columnName":"accessToken",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_users_id_serverId",
"unique":true,
"columnNames":[
"id",
"serverId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_users_id_serverId` ON `${TABLE_NAME}` (`id`, `serverId`)"
},
{
"name":"index_users_id",
"unique":false,
"columnNames":[
"id"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_id` ON `${TABLE_NAME}` (`id`)"
},
{
"name":"index_users_serverId",
"unique":false,
"columnNames":[
"serverId"
],
"orders":[],
"createSql":"CREATE INDEX IF NOT EXISTS `index_users_serverId` ON `${TABLE_NAME}` (`serverId`)"
}
],
"foreignKeys":[
{
"table":"servers",
"onDelete":"CASCADE",
"onUpdate":"NO ACTION",
"columns":[
"serverId"
],
"referencedColumns":[
"id"
]
}
]
},
{
"tableName":"ItemPlayback",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`rowId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sourceId` TEXT, `audioIndex` INTEGER NOT NULL, `subtitleIndex` INTEGER NOT NULL, FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"rowId",
"columnName":"rowId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sourceId",
"columnName":"sourceId",
"affinity":"TEXT"
},
{
"fieldPath":"audioIndex",
"columnName":"audioIndex",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"subtitleIndex",
"columnName":"subtitleIndex",
"affinity":"INTEGER",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":true,
"columnNames":[
"rowId"
]
},
"indices":[
{
"name":"index_ItemPlayback_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_ItemPlayback_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"NavDrawerPinnedItem",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"type",
"columnName":"type",
"affinity":"TEXT",
"notNull":true
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
},
{
"tableName":"LibraryDisplayInfo",
"createSql":"CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `itemId` TEXT NOT NULL, `sort` TEXT NOT NULL, `direction` TEXT NOT NULL, `filter` TEXT NOT NULL DEFAULT '{}', `viewOptions` TEXT, PRIMARY KEY(`userId`, `itemId`), FOREIGN KEY(`userId`) REFERENCES `users`(`rowId`) ON UPDATE CASCADE ON DELETE CASCADE )",
"fields":[
{
"fieldPath":"userId",
"columnName":"userId",
"affinity":"INTEGER",
"notNull":true
},
{
"fieldPath":"itemId",
"columnName":"itemId",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"sort",
"columnName":"sort",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"direction",
"columnName":"direction",
"affinity":"TEXT",
"notNull":true
},
{
"fieldPath":"filter",
"columnName":"filter",
"affinity":"TEXT",
"notNull":true,
"defaultValue":"'{}'"
},
{
"fieldPath":"viewOptions",
"columnName":"viewOptions",
"affinity":"TEXT"
}
],
"primaryKey":{
"autoGenerate":false,
"columnNames":[
"userId",
"itemId"
]
},
"indices":[
{
"name":"index_LibraryDisplayInfo_userId_itemId",
"unique":true,
"columnNames":[
"userId",
"itemId"
],
"orders":[],
"createSql":"CREATE UNIQUE INDEX IF NOT EXISTS `index_LibraryDisplayInfo_userId_itemId` ON `${TABLE_NAME}` (`userId`, `itemId`)"
}
],
"foreignKeys":[
{
"table":"users",
"onDelete":"CASCADE",
"onUpdate":"CASCADE",
"columns":[
"userId"
],
"referencedColumns":[
"rowId"
]
}
]
}
],
"setupQueries":[
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4255556f160da7bb269b114400da58da')"