main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
369c6279d6 |
fix(i18n): translate the player, downloads and server-setup text left in English
A Portuguese user reported "Skip Intro" rendering in English on Android TV.
The locale files were not the problem - all 22 were structurally complete.
skip_marker_button.dart simply never imported strings.g.dart and assigned
'Skip Intro' / 'Skip Credits' / 'Next Episode' as plain literals. An audit of
lib/ found ~120 more sites in the same state, in four shapes that need
different fixes:
A literal in a file that never imported the i18n layer is the easy one -
skip_marker_button, performance_stats, track_label_builder and codec_utils all
render text with no `t` in the file at all. TrackLabelBuilder._compose now takes
a fallbackLabel builder instead of an English fallbackPrefix, so the caller
supplies t.audioTracks.track / t.videoControls.subtitleTrack and every unnamed
audio and subtitle row in the track menus is localized.
English reaching the user through an exception message is the widest one, and
it needs care: MediaServerException.message feeds both toString() - logs and
Sentry grouping - and verbatim UI display. Localizing it in place would make
bug-report logs follow the user's locale and split one Sentry issue into 22.
The MediaServer and Seerr families instead gain a nullable `display` alongside
the English `message`, and the six screens that print these errors read
`display ?? message`. PlaybackException keeps the opposite rule, because it
already carries a PlaybackFailureReason for logic and classifyPlaybackFailure
already builds it from t.messages: its stragglers are localized at the throw
site. That also removes the literal "Exception: " prefix Live TV users saw on
a tune failure, since PlaybackException.toString() returns the bare message.
Localized parts hand-concatenated with bare English are the shape no search for
Text('...') can find: '${t.common.pause} auto-scroll' on the home carousel,
'${day} at ${time}' on the Live TV schedule row, and an actor-screen count that
hand-rolled its plural as `n == 1 ? 'title' : 'titles'` - wrong for ru and pl
regardless of translation, now a real Slang plural.
Finally a literal assigned to provider state that a widget renders later:
DownloadProgress.errorMessage, and the four background_downloader notification
bodies, which sit inside a plugin config call where no widget-shaped search
reaches them.
Two things surfaced while converting. track_chapter_controls compared a track
label against 'Audio Track N' to swap in a localized version; once the builder
localized its own fallback that branch became unreachable, so it and the
orphaned _joinTrackLabel are gone. And discovery_view's PeerError fallback arm
looks like a leak but is not - its producers already localize, and a test says
so - so it stays as it is.
All 21 non-base locales are translated, including the 21 keys left empty by
earlier commits that were falling back to English. No locale has an empty value.
scripts/check_hardcoded_strings.py guards the three shapes a structural check
can see, and runs in ci_checks.sh after translation hygiene. Its first draft
passed its own tests while missing this very bug, because 'Skip Intro' is bound
to a local rather than handed to Text(); the name-bound rule that closes that
gap is restricted to phrase-shaped literals, or it cannot tell copy from the
identifiers this codebase binds constantly ('cast_row', 'auto', 'liveTv'). It
cannot see English inside a throw or assigned to a provider field - neither is
distinguishable from a log message without dataflow analysis - and the docstring
says so. label: and actionLabel: are deliberately unscanned: here they name a
diagnostic operation, and a check that is chronically red is a check that gets
switched off.
One commit rather than one per area: the keys, the 22 locale files and the
generated output are a single unit, and any partial split fails the repo's own
unused-key scan on the way through.
close #1856
|
||
|
|
f1d4be70e2 |
feat(trackers): show a QR code in every tracker sign-in dialog and fit them on TV screens
Every tracker auth dialog (Trakt/Simkl/MDBList device-code and MAL/AniList OAuth proxy) now shares the same PendingAuthDialog affordances: a QR code for the sign-in URL, a large copyable URL with the scheme stripped, the browser launch button (hidden on Apple TV, which has no browser), and the polling spinner. On wide viewports (TV logical 960x540, desktop, phone landscape) the QR pane sits beside the instructions so the dialog no longer clips on tvOS, and the content is scrollable as an overflow safety net. Device activation codes scale down instead of wrapping. |
||
|
|
e0a364e26a | chore(tvos): remove the Atmos output diagnostics | ||
|
|
24a041977b |
fix(sheets): size sheets to their content instead of 75% of the window
Sheets rendered at the host's maximum height regardless of content, so a one-item player queue or a two-track picker filled ~75% of a desktop window with empty space. BottomSheetPageScaffold now always lays out Column(mainAxisSize: .min) plus Flexible(child:), and each sheet body shrink-wraps its own scrollable. The scaffold's shrinkWrap flag is gone: its old true branch put the child on an unbounded axis, where an over-tall list overflowed instead of clamping and scrolling. Measured on a 1600x1000 window, the chapter sheet goes from 750px to 118px for one chapter and the two-column track sheet from 750px to 154px for one audio and one subtitle track, both still clamping at the cap. Add SheetSplitColumns for the three side-by-side sheet layouts. A bare VerticalDivider has no intrinsic height, so it inflated those rows to the cap on its own; the rule now paints from a Positioned.fill that cannot size the Stack. IntrinsicHeight is not an option because a Viewport has no intrinsics. Because sheets are bottom-anchored, a content-driven height moves the sheet's top edge and everything above the change point. Three surfaces opt out for that reason and say so at the call site: SubtitleSearchSheet and its language picker keep filling, since both refilter under an autofocused field; FiltersBottomSheet holds the outgoing page's height through its loading transient; and RatingBottomSheet no longer hides MAL/AniList rows asynchronously, which used to slide live rating controls down two rows several hundred ms after open. Wrap the shared StateMessageWidget at the filters sheet boundary rather than editing a widget with 33 filling call sites. The host gains an AnimatedSize keyed per sheet session so nested pushes ease while a replacing show adopts its own height, a 720px absolute height ceiling on desktop windows only, and a min(max(25%, 96px), 60%) drag-dismiss threshold so short sheets neither close on a nudge nor become undismissable. Add videoControls.noAudioDevicesAvailable so the audio output page shows a placeholder instead of a bare header while devices load. |
||
|
|
feb34caeb7 |
fix(i18n): shorten nav labels and complete translations in all locales
Nav bar labels that overflow their tab slot on phones are shortened to idiomatic short forms: fr (Bibliothèque, Téléchargement, Recherche), ru/bg/it (Live TV), pl (Home). All 21 locales get the ~280 keys that were empty (falling back to English at runtime): explore detail/badges/stats, fileInfo, startup repair flow, mediaMenu delete dialogs, addServer, rating sources, downloads sync-rule removal, and more. settings.displayScale was missing entirely in 16 locales; the new exoplayer playbackBuffer keys (upstream feat) are translated too. Fixes mis-translations found in review: es/zh/zh-Hant sidecar-format wording, sv adaptation, nb transcoding. Regenerated with dart run slang; translation hygiene and i18n tests pass. Close #1823 |
||
|
|
f93952ba6f |
fix(android): stop tunneling 24p video on the Fire TV Stick 4K
Tunneled playback on an AFTMM judders continuously through 23.976p direct play. The #1802 reporter isolated it: turning off Tunneled Playback with every other setting unchanged makes it smooth, and their log shows tunneling active for the whole session with E-AC3 bitstreamed and the decoded-PCM guard never firing. Audio Passthrough looked like the trigger only because it is the one user-facing switch that decides it. Passthrough off, or Downmix to Stereo on, both force the Dolby track to decode to PCM, which trips the #1458 guard and takes tunneling down with it. Passthrough on with downmix off is the only combination that keeps a bitstreamed track, so it is the only one that stays tunneled. Withdraw tunneling on that model for content at or below 30fps. The cut-off keeps 4K50/60 tunneled, which is the workload Amazon documents the feature for. The mechanism stays unconfirmed: tunneling fires no VideoFrameMetadataListener and stops media3 counting frames in the codec, so nothing app-side can measure the cadence. Only the trigger is established, and the quirk is scoped to it. That needs a frame rate the app did not have. Neither MatroskaExtractor nor Mp4Extractor populates Format.frameRate, and a tunneled session renders no frames back for the native detector, so the server's rate now rides on the open call. It is sent only for direct play, matching _primeDisplayCriteria: a transcode's metadata describes the source, not what the server is about to send. Also move Audio Passthrough out of the in-player settings sheet. It configures the audio output route rather than the current playback, and applying it mid-stream bounces the audio renderer and re-decides tunneling. Settings > Video Playback already owns it, next to Tunneled Playback, which is applied the same way. That description now mentions stutter, not only black HDR video, so the workaround is findable on hardware this quirk does not cover. The mpv backend failing to start the same 4K file is a separate defect and is not addressed here; its uploaded log is no longer retrievable. |
||
|
|
1b6a811c07 |
fix(delete): name the delete target and verify what its files back
"Delete from server" read identically for an episode, a season and a
whole show: same menu label, same dialog title, same red button, and a
body that named nothing. The menu header did not disambiguate either,
because MediaItem.displayTitle collapses an episode to its show name.
A reporter deleted a whole series from the detail hero's ⋮ believing it
acted on the episode he had highlighted, and the confirmation gave him
nothing to catch it with. Every one of those strings now names the kind,
and the body names the exact item — show, season and episode number, and
episode title.
Deleting a single item also destroyed files the confirmation never
mentioned: a Plex multi-episode file (S01E01-E03.mkv) takes its other
episodes with it, and a split item takes every part. The dialog now
reports that up front and, on success, emits deletion events for the
siblings the server destroyed so their rows do not linger.
The scope behind that warning is only asserted when it is established.
MediaItem.allPartFiles drops parts with no path, so a non-empty set
proves nothing about the ones it filtered out; a version is trusted only
when every part reports a file. A browse row that omits paths is missing
evidence rather than proof of a distinct file, so both the target and
each candidate sibling fall back to the detail endpoint before any
conclusion — otherwise a thin row, including the file-less part
PlexMappers fabricates for an empty payload, would look like a server
that withholds paths. When the answer cannot be established the dialog
says so in an error-tinted block and its button reads "Delete anyway",
separating a transient probe failure from a server that never sends
paths. It deliberately does not refuse: Plex withholds paths from
restricted users the server itself authorizes to delete, so failing
closed would take the feature away from them permanently.
Probing a season stays bounded in both directions. Siblings resolve one
at a time, so a season of thin rows cannot fan out a detail request per
episode, and expiry cancels the walk rather than merely abandoning it —
`Future.timeout` completes the future the caller awaits but leaves the
work behind it running, which would resume on the next sibling once the
outstanding request answered. A cooperative flag is checked before each
lookup, so at most the one already in flight outlives the deadline; the
neutral client exposes no abort handle for item lookups, so that one
cannot be recalled.
The spinner covering the probe was only barrierDismissible, which does
not stop system back. Back dismissed it and the cleanup pop then closed
the screen underneath, dropping the user out of the detail page
mid-flow. It now traps back, matching the non-dismissible contract its
own doc claims, which also repairs the log uploader and the file-info
sheet.
Coverage splits by what each layer owns. The dialog, its copy and the
DELETE wiring are backend-neutral and stay in the menu widget tests.
Plex — the backend multi-episode files actually come from — gets the
resolver over a real PlexClient and a mocked transport: a row with no
media at all, scope recovered from /library/metadata/{id}, siblings and
paths from /children, a Part that names no file, a sibling whose path
never resolves, the request count a sixty-episode thin season may cost,
and the rating key the DELETE carries. Those are plain async tests
because the Plex metadata cache is a real database whose I/O the widget
tester's fake clock never drives. Deadline behaviour needs the opposite,
so it is pinned separately under fakeAsync against a gated fake client,
with no wall-clock waiting anywhere.
close #1781
|
||
|
|
26dbce0277 |
fix(profiles): name the Plex user and account in one translated chip
A Plex account connection labels itself with the account owner's name. Under a profile tile that reads as being signed in as the owner: the Plex Home tile showed the owner beneath the Home user's own name, and a local profile that borrowed a Home user out of someone else's account showed only the lender. Both halves of the relation now go through a single translated string, so a locale orders them itself instead of inheriting the English "user via account" — az, hu, ja, kk, ko, tr, uz, zh and zh-Hant put the account first. When the Home cache cannot resolve the connection's uuid the chip names the account alone rather than falling back to a bare name. ProfilesView carries the Home user cache that resolution needs, and chip labels ellipsize now that an account label can be an email. |
||
|
|
05fd622968 |
feat(emby): add Emby as a MediaBrowser backend alongside Jellyfin
Emby is Jellyfin's upstream ancestor and speaks a near-identical MediaBrowser
API, so the existing Jellyfin stack is parameterised by a `MediaBrowserDialect`
rather than forked. `JellyfinClient`, its auth service, endpoint discovery, LAN
discovery, and the add/edit connection screens all take the dialect and keep one
implementation; `MediaBackend.emby` and `ConnectionKind.emby` carry it through
the neutral models, the Drift `kind` discriminator, downloads, and caches.
Every divergence below was measured against a live Emby 4.9.5 server, not
inferred from documentation, and each is documented at its capability getter.
Jellyfin's request strings stay byte-identical so nothing about its behaviour
changes.
Routes and auth
- Emby only accepts the pre-10.9 user-scoped item routes (`/Users/{id}/Items/…`,
`/Users/{id}/PlayedItems/…`, `/Users/{id}/FavoriteItems/…`); the unprefixed
forms Jellyfin 10.11 added return 404.
- The API is also served under a legacy `/emby` prefix, and both dialects accept
the token as `X-Emby-Token` or `api_key=`.
- Emby answers only its own LAN discovery datagram ("who is EmbyServer?") and
ignores Jellyfin's; its default HTTPS port is 8920.
- No `/QuickConnect` route exists, so Quick Connect stays Jellyfin-only.
Row fields Emby withholds
- `ProductionYear`, `OfficialRating`, `PremiereDate` and `DateCreated` are absent
from list rows unless named in `Fields`, which would otherwise strip the year
and age-rating badge from every card in the app.
- `UserData.LastPlayedDate` never appears on a list row under `Fields=UserData`,
`EnableUserData=true` or the user-scoped `Ids=` form — only on the single-item
detail route, or when the Emby-specific `UserDataLastPlayedDate` token is
requested. Without it every recency-ordered surface silently degrades to
library-add time, and `JellyfinApiCache.applyWatchState` stamps
`DateTime.now()` on watched rows, so an offline watch-state pull would rewrite
the cached play time of everything it walked.
Continue Watching and Next Up
- Emby computes Next Up per series only: the library-wide `/Shows/NextUp` query
returns nothing under every parameter combination tried. The shelf is
therefore reconstructed from a played-episode recency scan plus one
`/Shows/NextUp?SeriesId=` per distinct series, bounded by a shared wall clock
that covers the scan as well — per-request timeouts cannot bound the pass
because `MediaServerHttpClient` times the connect and receive phases
independently. Rows are stamped with their series' newest play from the same
response that ordered them, so no per-series enrichment request is needed.
- `/Shows/NextUp` ignores `NextUpDateCutoff`, and no server-side played-date
filter exists to delegate to (`MinDatePlayed` and `MinDateLastPlayed` are
ignored; `MinDateLastSaved`, `MinDateCreated` and `MinPremiereDate` filter
unrelated dates), so the 365-day window is applied to the scanned dates.
- The resume route returns items with no saved position, including plain next
episodes, so the Emby resume leg reads from `/Items?Filters=IsResumable`.
- Emby is ahead of Jellyfin in one place: `/Users/{id}/Items/{id}/HideFromResume`
makes Continue Watching removal a real capability.
Everything else
- `/Sessions/Playing` and `/Sessions/Playing/Progress` reject a body with no
`PlaySessionId` (HTTP 400), so playback reporting always sends one.
- Passing any `MediaTypes` value to the playlist query returns an empty list.
- There is no aggregate `/Items/Filters` route; the four filter facets are
reassembled from `/Genres`, `/OfficialRatings`, `/Studios` and `/Tags`.
- Metadata writes take name-pair lists (`Genres: [{'Name': 'Action'}]`); the
plain string array is accepted and then silently discarded.
- Custom artwork uploads must be base64 text, not raw bytes — which was broken
for Jellyfin too and is fixed for both.
- Trickplay, media segments and lyrics 404 on Emby, so scrub previews are absent
and intro/credit markers fall back to chapter names.
Verified against a local Emby 4.9.5 and a Jellyfin 10.11.11 control server:
onboarding, browse, detail, playable stream URLs serving real bytes, subtitle
sidecars, watch-state write and restore, hubs, cross-server aggregation and
search across both backends simultaneously.
|
||
|
|
27b994422c |
feat(player): optionally follow the server's per-episode track selections
With the new playback setting enabled, episode advance carries no audio or subtitle preference at all, so both resolve from the streams selected on the server for each individual episode. This serves setups that curate selections server-side (e.g. Plex Auto Languages) and is independent of "Remember track selections", which keeps gating only the write-back of manual changes. close #1717 |
||
|
|
0ef71e6489 |
feat(file-info): detail every version, file and stream the server reports
The sheet collapsed an item to `Media.first` / `MediaSources.first` and
rendered a fixed set of rows, so split files, extra versions, per-track
properties, HDR classification and Dolby Vision were all invisible.
Model the payload the way both servers shape it — versions own parts,
parts own streams — and project every property either backend populates
onto `MediaStreamDetails`. The field set comes from sweeping both test
servers in full through the clients' own request shapes (Plex 6842
Media / 6842 Part / 39864 Stream entries, Jellyfin 6349 sources / 37763
streams / 61224 attachments), so file presence, Dolby Vision layers,
dynamic range, sample rate, spatial audio, sidecar provenance, embedded
attachments, rotation and the lyric stream type all survive. A coverage
test fails when a server key is neither carried, folded into a sibling,
nor excluded with a reason.
File Info also stopped trusting the shared `/library/metadata/{id}`
cache row: `getPlaybackExtras` writes it without `includeStreams` /
`checkFiles`, so the sheet could render with no stream table at all.
Detect that shape and refetch once under the request context captured
before the cache read, so the outgoing token and the cache namespace
stay on one profile.
Rework the layout to match: a summary chip row, then flat tonal cards
with a two-column field grid that collapses to one column on narrow
viewports, per-stream cards with flag chips, and a copyable monospace
path row. The card fill is a tonal step off the text colour rather than
the `bg` token, which is one shade from the sheet surface on OLED.
|
||
|
|
ef310459e5 |
fix(i18n): drop unused video-control strings and restore spinner lookups
`clean_translations.py --strict` reported seven unused keys.
Three are genuinely dead: TrackSelectionHelper.getEmptyMessage was
removed as unreachable in
|
||
|
|
7677d1594c |
feat(i18n): add Turkish locale
Complete the contributed translation against the current English source and register Turkish in the language picker. Fills the 51 keys the contribution predated, including the whole downloads.backgroundWarning block that every locale must translate, and corrects a few contributed strings: "Sesi Kıs" (volume down) for mute, "Disket" (floppy) for disc, "bitiş hızı" (finishing speed) for bitrate, and a "Kısayol Ayaıla" typo. Consolidates #1683 and #1688, which contributed byte-identical files. Co-authored-by: Omc725 <98108290+Omc725@users.noreply.github.com> |