Compare commits

...
Author SHA1 Message Date
dependabot[bot]andGitHub 98bd085bff build(deps): bump actions/setup-java from 5.6.0 to 6.0.0
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 5.6.0 to 6.0.0.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/03ad4de0992f5dab5e18fcb136590ce7c4a0ac95...dd06d9cba3e5552c54d9f8ea23572deb30010f7c)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-01 12:23:37 +00:00
edde746 23f9afa5b1 fix(windows): strip HDR10+ metadata from the Adreno tone-map path
Disabling peak detection (efb0e885) did not stop the per-frame shader LUT regeneration on Snapdragon devices: libplacebo keys the tone-map LUT on the frame's raw HDR metadata, and mpv forwards HDR10+ per-scene values (scene_max, scene_avg, ootf) on every decoded frame, so the LUT - at tens of milliseconds per rebuild on Qualcomm's D3D11 driver - still regenerated continuously and playback kept stuttering and flickering (#2191).

Zero the dynamic metadata before the renderer with vf=format:hdr10plus=no on Qualcomm GPUs; tone mapping uses the static HDR10 mastering metadata and the LUT is generated once. Dolby Vision L1 metadata is deliberately not stripped: dovi=no would break profile-5 rendering. The applied HDR pipeline options are now reported into the app log on first file load so uploaded logs are self-diagnosing.
2026-09-01 09:02:59 +02:00
edde746 efb0e885ae fix(windows): disable dynamic HDR peak detection on Qualcomm GPUs
Playing HDR content tone-mapped to an SDR display on Windows-on-ARM stutters heavily and flickers in brightness: dynamic peak detection moves the tone-mapping parameters every frame, libplacebo regenerates its tone-map and gamut-map shader LUTs each time, and Qualcomm's Adreno D3D11 driver takes tens of milliseconds per regeneration - roughly 100 ms of work against a 42 ms frame budget at 24 fps.

Scan the DXGI adapter list for a Qualcomm vendor id and set hdr-compute-peak=no when one is present, keeping auto elsewhere; static metadata-driven tone mapping generates its LUTs once.

close #2191
2026-09-01 07:37:10 +02:00
edde746 5fb4c78385 fix(jellyfin): restore Live TV on ts-only tuners and honor the quality preset
Since 2.18.0 no Jellyfin Live TV channel plays: opening one fails with
"Could not start the live channel", and switching channel inside the player
does nothing at all. Negotiating video transcodes as fMP4 (#2131) left the
device profile with no MPEG-TS entry, and Jellyfin discards every non-ts
transcoding profile for a live source whose tuner sets
UseMostCompatibleTranscodingProfile - hardcoded for every HDHomeRun host, the
default for M3U ones - so the negotiation came back with no HLS URL at all.

The device profile now sends a ts profile alongside the fMP4 one, listed
second, the way jellyfin-web does. Both of its codec lists are strict subsets
of the fMP4 entry's and the server ranks profiles with a stable sort, so ts
can only win where fMP4 has been filtered out; VOD keeps negotiating fMP4.
flac and truehd are absent because TS cannot carry them, av1 because that is
the gap fMP4 exists to fill.

Live TV also stops being unconditionally transcode-only. It now reads the same
saved quality preset the library path does: Original asks for direct play and
sends no bitrate ceiling, so a channel the server can hand over untouched no
longer costs a GPU encode session per viewer, and a capped preset transcodes at
that ceiling. A direct-play session whose stream dies re-negotiates a forced
transcode instead of re-opening a URL the server will not serve.

Two smaller holes on the same path: a negotiation that bailed out left the live
stream AutoOpenLiveStream had already opened running, because no playback
session existed to ever stop-report it - a few failed tunes could saturate a
provider's connection budget - and a channel switch that failed to start showed
no message at all.

close #2198
2026-08-31 19:23:22 +02:00
edde746 5a666fd10f feat(discover): refresh home and library content from server library push events
New media added server-side was invisible while the app was open - the
client had no channel for library-change notifications, so the home
screen and library tabs sat stale until an app restart.

Each online server now runs a reconnecting websocket push channel
(Plex `/:/websockets/notifications`; Jellyfin/Emby `/socket` with the
dialect deltas, including Emby's capabilities registration), owned per
server by a LibraryEventService supervisor that follows server
online/offline/replaced-client transitions and app lifecycle. Channels
emit one coalesced LibraryChangeEvent per burst on a leading-edge
throttle modeled on Plex Web's repopulate pacing: the first settled
change surfaces immediately and a library scan's flood merges behind
it. Connection failure degrades silently to the stale-refresh paths -
bounded reconnect backoff, re-armed on every status sync.

Consumers pace their reaction through one shared RefreshPacer: burst
debounce, blocked retry while video playback or an active scroll owns
the surface, a cooldown bounding pass frequency during bulk imports,
and credit for committed pull passes so a push landing right after a
fresh load defers to the cooldown's trailing edge.

- Discover runs a debounced full pass and swaps the result in place;
  the hero carousel resets only when a pass lands first content, so a
  push never yanks a screen the user is reading. Pushed removals drop
  from every visible list immediately via the deletion bus, scoped to
  the emitting server.
- The visible library tab swaps its data in place: transaction tabs
  reload without clearing, and the browse and paginated card grids
  refetch their loaded span (Plex Web's repopulateRange) with the
  scroll offset anchored on the first visible item and the span
  clamped after alpha jumps. Hidden tabs - and tabs behind another
  main tab - mark per-library staleness epochs instead and reload when
  next shown; epochs are snapshotted at load start so a push racing an
  in-flight fetch stays stale.
- The stale-resume and tab-shown paths refetch home hubs (previously
  Continue Watching only), covering setups where the socket cannot
  connect.

close #1646
2026-08-31 18:52:03 +02:00
edde746 56312a9eb2 fix(ui): keep clear logos with significant color untouched on heroes
Remapping a mark whose color is part of its identity - a red-outline
wordmark with white fill, a colored badge - fixes legibility but changes
the logo's character, which reads worse on hero surfaces than the
partial invisibility it cures.

The light tone class now splits on measured colored-pixel fraction:
light-dominant marks with incidental color (<=15% saturated pixels,
lightAccented) always remap, while lightMixed marks remap only when the
new ToneMappedLogoImage.remapMixed policy allows it. Heroes pass false
on the network, offline, and TV spotlight logo paths; the Live TV
guide's channel cells keep remapping mixed marks. The policy joins the
image-cache key. The threshold comes from a real clear-logo set:
remap-friendly marks measured at <=0.11 colored, identity-colored marks
at >=0.28.
2026-08-31 18:51:46 +02:00
edde746 976bdbd021 fix(ui): keep hero titles and clear logos legible in light theme
On the light theme, the detail hero's fallback title was hard-coded
white over a scrim that washes artwork toward the near-white background,
and white-on-transparent clear logos vanished into the same wash on the
detail, home, and TV spotlight heroes.

The title fallback now defaults to the theme foreground with a
background-side halo, matching what the TV hero already did. Clear logos
run through the channel-logo tone remap: light-toned marks recolor
toward the theme foreground on light backdrops (network and downloaded
artwork alike), colored marks keep their pixels, and dark themes render
the original artwork. The backdrop-luminance gate is shared with the
Live TV guide as logoToneTargetFor.
2026-08-31 18:51:46 +02:00
edde746 274b31ebac fix(livetv): recolor light channel logos for legibility in light theme
White-on-transparent broadcast logos (CBS, FOX, the NBC wordmark) vanish
on the light theme's white guide cards, making channels hard to identify.

Channel logo decodes now classify the frame's tone once per image-cache
entry and bake light-neutral pixels toward the theme foreground:
monochrome white marks recolor fully, mixed marks (colored peacock plus
white wordmark) keep their colored pixels, and self-backed or dark
artwork decodes unchanged. Applied in the guide channel column, guide
search, and favorite reordering whenever the backdrop is light,
including the dark theme's inverted focus card; dark backdrops keep the
original artwork.

close #2197
2026-08-31 18:51:46 +02:00
edde746 e1ef52aba1 fix(android): fall back to software decoding for non-Main-profile AV1
4:4:4 (High profile) AV1 played as green or black video on Android TV
devices with AV1 hardware decoders: MediaCodec defines no profile
constants beyond Main, but decoders like the Fire Stick 4K Max (MTK)
and Amlogic C2 accept the stream at configure time and emit garbage or
nothing, so no error ever reached the player and no fallback ran.

libmpv v1.2.2 carries an FFmpeg patch that reads seq_profile from the
bitstream at av1_mediacodec init and refuses High/Professional streams,
letting mpv's hwdec probing fall through to dav1d software decoding.
Main-profile AV1 keeps hardware decoding.

close #2194
2026-08-31 13:29:13 +02:00
edde746 9db6f54b7a feat(plex): make the covered-source direct play optional
A quality preset the source already fits under direct plays the file
since #2152, but a transcode above the source bitrate is sometimes the
point: a device that cannot decode the source (10-bit H.264, AV1, 4:4:4)
wants the server's highest-quality encode, and the shortcut silently
handed it the very file it cannot play smoothly.

A new Quality switch, "Play Smaller Videos at Original Quality" (on by
default), gates the shortcut in PlexClient._presetNeedsTranscode; off
restores the pre-#2152 behavior where any non-original preset always
transcodes. Plex-only by design: MediaBrowser servers make the
equivalent direct-play-vs-transcode call server-side.

close #2193
2026-08-31 11:32:07 +02:00
edde746 39ee089212 fix(audio): reload the mpv audio output when the HDMI route changes
TrueHD/Atmos passthrough with Frame Rate Matching enabled never engaged
the receiver on the mpv backend (Ugoos SK1 -> Denon AVR): the display
mode switch settles 2 s before the audio output opens, but some HDMI
chains are still renegotiating audio then, so the IEC 61937 track binds
a half-negotiated route and the sink never locks onto the MAT
bitstream - for the whole session. The same file with Frame Rate
Matching off, or on ExoPlayer, engages Atmos immediately: media3
rebuilds its sink when the audio device set changes, while mpv's
ao_audiotrack never revisited a track once created.

libmpv v1.2.1 polls AudioTrack.getRoutedDevice from the AO thread and
reloads the AO when the route changes - or vanishes and rebinds - under
a live IEC or raw passthrough track, reopening it against the settled
route. PCM tracks are exempt, and audiotrack-route-reload=no is the
escape hatch.

close #2190
2026-08-31 10:05:08 +02:00
edde746 89d54d8f1d feat(watch-together): let the host hand the room to another participant
close #2183

Reusing a room means whoever enters first becomes the host, and moving the
session to someone else's library meant everyone leaving and re-entering in
the right order. The host can now promote any connected guest from the
participant list (Watch Together screen and the in-player session sheet),
with playback, control mode, and reconnect identities carried across.

The relay owns the swap: a new `transferHost` message validates the sender
is the live host and the target a connected modern-protocol guest, then
swaps `HostPeerID` and the reconnect verifiers (each peer keeps its own
token), persists the room, and broadcasts `hostChanged` to every peer.

Clients flip roles in place: the controller swaps its role engine while
keeping the session, message queue, and player attachment. A promoted guest
seeds the coordinator with the room's last known intent (a paused room
stays paused) and the known-peer roster so the fresh epoch re-gates
instead of solo-starting; a demoted host falls back to a reconciler with a
fresh clock sync and asks the new authority for state. Guests re-pin the
host identity, reset their sequence, and re-converge their clocks.

A rejected transfer (`not_host` / `peer_not_found`) surfaces as a toast
instead of tearing the session down. Pre-transfer app builds in the room
ignore `hostChanged` and stop following the room at the next transfer;
targets on an old sync protocol are not offered the action.
2026-08-31 08:55:45 +02:00
edde746 44773fd742 feat(player): remember the swipe brightness level between playbacks
The brightness set with the left-edge swipe was lost the moment playback
ended, so every session started back at the system level. A new
"Remember Brightness Level" toggle under Settings > Playback > Gestures
persists the level a swipe settles on and reapplies it when the next
playback starts (and when the app resumes mid-session). Leaving the
player still restores the pre-playback brightness, so the rest of the
app is unaffected. Default off, preserving current behavior.

close #2178
2026-08-31 06:43:47 +02:00
edde746 4f4a7338e0 fix(search): tighten spacing around the kind filter chip strip
The chip strip sat 24px below the search field and 24px above the first
result. Drop the strip's own top padding (the field's bottom padding
already provides 16px) and halve the results sliver's top padding when
the strip is shown, so both gaps read as an even 16px.
2026-08-31 06:01:48 +02:00
edde746 10f9139ac6 fix(audio): bitstream Dolby/DTS as raw tracks on the mpv path
Audio Passthrough on 2.18.0 played EAC3 silently on a Shield in front of
a Dolby-Digital-only receiver: mpv's IEC 61937 AudioTrack bypasses the
platform's Dolby transcoder, so the pre-packed DD+ bitstream reached a
sink that cannot decode it, drained at full rate, and no failure was
observable app-side. Disabling passthrough lost surround instead, because
multichannel PCM collapses to stereo in the platform mixer before the
HDMI re-encode.

libmpv v1.2.0 unwraps the IEC bursts and feeds AC3, E-AC3 and the DTS
core to a raw ENCODING_AC3/E_AC3/DTS track - the transport ExoPlayer and
Kodi use - keeping the platform decoder/transcoder in the path, with the
IEC track as fallback and audiotrack-raw-passthrough=no as an escape
hatch. TrueHD and DTS-HD MA stay on the 8-channel IEC carrier.

The audio-spdif route probe now accepts a codec when the route takes its
raw track (the same direct-playback tiering media3 uses; encoding-only
below API 29) or one of the IEC shapes, matching the AO's transport
ladder. A dev-only PLEZY_LOCAL_MPV_AAR override allows testing locally
built fork AARs before a release is pinned.

close #2177
close #2179
2026-08-30 19:22:14 +02:00
edde746 3c1ab3cc6c fix(windows): bundle the Vulkan loader so libmpv's vulkan-1.dll import resolves
Since the libmpv bump to 20260809 (2.15.0), libmpv-2.dll dynamically links
the Khronos Vulkan loader instead of statically linking it. plezy.exe links
libmpv at load time, so on machines whose GPU driver does not install
vulkan-1.dll - Windows-on-ARM Adreno drivers, driverless x64 VMs - the
process failed before startup with "vulkan-1.dll was not found".

Download the LunarG-built loader (pinned, hash-verified) at configure time
and install it next to the executable with its license. Without a Vulkan
ICD the gpu-api=auto probe fails cleanly and falls back to D3D11, matching
the old static-loader behavior.

close #2110
2026-08-30 12:16:26 +02:00
github-actions[bot] c597390e1c chore: update cask to 2.18.0 2026-08-30 08:39:52 +00:00
edde746 2806115015 chore: bump version to 2.18.0 2026-08-30 09:28:50 +02:00
edde746 d9493c6157 feat(player): move every Android install to the mpv backend
Making mpv the Android default only moved installs that had never opened
the Player Backend setting. Anyone who had ever picked ExoPlayer kept it,
including the low-end TVs and Hi10P devices the new backend was built for.

The backend choice now lives under a new `android_use_exoplayer` key and
the old `use_exoplayer` one is dropped at startup, so every install starts
on mpv. ExoPlayer stays selectable in Settings > Playback > Player Backend,
and a pick made there writes the new key and sticks.
2026-08-30 09:22:31 +02:00
edde746 7cfb890046 fix(i18n): translate the Live TV guide search and TV options labels
The guide-search sheet and the TV rail's options button fell back to
English in every non-English locale: their six keys existed only as
empty strings in the sibling translation files.

Fill all 126 empty targets across the 21 locales and regenerate the
slang bindings. searchNoResults keeps its ${query} placeholder and each
locale's quote convention, and channelsSection uses the broadcast sense
of "channel" rather than the audio-channel word already used by
fileInfo.channels.
2026-08-30 08:48:32 +02:00
edde746 c6458295a1 fix(player): let the HDR exit settle before restoring the display mode
Stopping HDR playback with frame rate matching enabled left some TVs
black for up to 30 seconds. The teardown reset preferredDisplayModeId
while the display was still signaling HDR, folding the HDR exit and the
refresh-rate restore into one HDMI renegotiation that slow sink chains
take many seconds to complete (8.5 s on a Shield Pro with an HDR10
display; ~30 s on the reporter's DV AVR+TV chain). Sequenced, the HDR
infoframe clear is free and the SDR mode switch takes about a second.

FrameRateManager.clearVideoFrameRate now takes an hdrActive hint from
the core (mpv: the content-color-transfer decision; ExoPlayer: the
selected track's colorInfo transfer) and defers the restore by 400 ms so
the surface teardown commits the HDR exit first. The deferred restore
lives on its own main-looper handler because core dispose clears the
shared player handler wholesale, and a new setVideoFrameRate cancels it
so a fresh session's switch is never clobbered. SDR teardown behavior is
unchanged.

Verified on a Shield Pro (mpv backend, 1917 DV P8/TrueHD, Jellyfin):
link recovery after stop went from ~8.5 s to ~0.8 s, with the SMPTE 2086
clear now committing before the mode set.

close #2172
2026-08-30 08:25:18 +02:00
edde746 6e2dda092d feat(music): make the background fill the mini player's only progress display
The mini player drew a thin scrubbable progress bar along its top edge on
top of the ambient background fill. Drop the scrub lane: the fill is the
card's only progress display, and precise seeking stays on the
now-playing screen's seek bar. Horizontal drags across the card's top
edge swipe-dismiss the card again.
2026-08-29 19:12:03 +02:00
edde746 45e8da1610 feat(music): restore the last music session paused on launch
Quitting the app lost the music session: on the next launch the queue,
current song, and playhead were gone. The session now persists per
profile (new MusicSessions Drift table, schema v22) through throttled
write-through during playback, and the next launch rehydrates it as a
parked-paused queue — no audio core and no source resolution until the
first play, which opens the track at the saved offset. Ending the
session (stop, swipe-dismiss, video claim) clears the snapshot, and a
new "Remember music session" playback setting (default on) gates the
restore.

close #2148
2026-08-29 19:11:58 +02:00
edde746 ad026cecc1 fix(player): keep hardware decoding after a transient VideoToolbox failure
Some HEVC files carry a frame Apple's hardware decoder refuses to decode
(kVTVideoDecoderBadDataErr on an otherwise conformant P-frame). mpv fell
back to software decoding for the rest of the file after the first error,
and 4K HEVC software decode drops frames every few minutes on Apple TV.

MPVKit b8b922ec carries an mpv vd_lavc patch that keeps the configured
fast fallback while probing but lets a hardware decoder that has already
delivered frames ride out a mid-stream error burst: the burst one
undecodable frame causes ends at the next keyframe by construction, so
fallback now requires errors to survive two keyframe intervals. Verified
against the reporter's sample: playback logs "Hardware decoding
recovered after 90 errored frames" and stays on VideoToolbox instead of
switching to software.

close #2167
2026-08-29 17:39:35 +02:00
edde746 ffc95d6b2c feat(tv): mark whole seasons watched from the show detail rail
On TV the show detail screen exposed season actions nowhere: marking a
season watched meant toggling every episode one by one, and the action
row's menu only covered the whole show.

Each season row on the TV detail rail now carries a full-size Options
card that opens the same season context menu the mobile season tabs
offer (mark watched/unwatched, download, delete, add to). The card
rests off-screen left of the first episode — the row is a
center-anchored scroll view and the card lives in the negative-offset
region before the anchor — so the resting layout and default focus are
unchanged and normal playback gains no extra steps; pressing LEFT from
the first episode slides it in. The trailing View All / retry / loading
slots become matching full-size cards instead of compact pills.

close #2156
2026-08-29 13:56:24 +02:00
edde746 a3cbf323b3 feat(livetv): search the guide for channels and programs
Live TV had no search: finding a channel or a program meant scrolling
the guide row by row, which is close to unusable on TV with a few
hundred IPTV channels, and the global search tab covers neither
channels nor EPG data.

A search action on the Live TV tab bar (shown on every tab, since the
D-pad route through the chips selects each tab it crosses) opens a
sheet that filters channels by name, call sign, and number, and the
next 24 hours of programs by title and series name, from one
fetchSchedule fan-out per open. Selecting a result jumps the guide to
it: the favorites filter is dropped when it would hide the target row,
the time window is re-anchored when the airing is outside the visible
six hours, and D-pad focus lands on the channel cell or program block.
While the sheet is open, companion-remote search queries land in it
instead of the global Search tab (same save/restore idiom as the video
player overrides). Jumps requested while a guide load is in flight are
stashed and replayed when the load commits.

Based on #1526 by @l3gitpanda, rebased over the guide virtualization
and source-group work, with the schedule fan-out moved onto
forEachLiveTvServer and the jump focus routed through the guide's
focus-snapshot publisher so the landed cell actually renders focused.

close #2168
2026-08-29 01:54:21 +02:00
edde746 2f2016fdbe feat(player): show the next episode's thumbnail in the play-next prompt
The end-of-episode autoplay prompt only named the next episode; Plex shows
a visual preview, which is what non-readers navigate by. The prompt card now
paints the episode's 16:9 still edge-to-edge behind the text under a bottom
gradient scrim, falling back to the text-only card when no thumbnail can be
served (no thumb, or no client and no downloaded artwork). Unwatched stills
respect the hide-spoilers blur, and the lookup only runs while the prompt is
visible to keep the playback-path build cheap.

close #2166
2026-08-28 18:43:26 +02:00
edde746 ff1064d35f fix(player): render HDR letterbox bars off the SDR window plane
HDR/Dolby Vision playback on Fire TV (and some Sony/Philips models)
shows gray letterbox bars instead of black on OLED panels: the bars
were the player container's window-plane background, and those device
compositors raise SDR graphics-plane black while the display is in
HDR/DV output mode. Black inside the video plane is unaffected (same
mechanism as ExoPlayer #8803 and Kodi #25300).

Add a fullscreen, buffer-less SurfaceView beneath the video surface in
the shared player container. Like any below-window SurfaceView it
registers its rect as a window transparent region, so the letterbox
area scans out as the SurfaceFlinger backdrop instead of window-plane
pixels. No buffer is ever posted: SurfaceFlinger skips buffer-less
layers, and drawing into one would put the bars back on an SDR layer.
Covers both the ExoPlayer and mpv backends.

close #2163
2026-08-28 11:24:37 +02:00
edde746 861111a191 feat(player): offer a 140% zoom preset
close #2158
2026-08-28 08:12:38 +02:00
edde746 463ad6b933 fix(i18n): translate settings and account preferences into all locales
94 strings added since the settings rework were empty placeholders in every
non-English locale, so those screens fell back to blanks. Fill all 1974
missing values across 21 locales, reuse each file's existing terminology,
and translate the few verbatim-English leftovers (es rankPopular, fr tags,
it dynamicRange, pl stop, nb tag). Regenerated Slang output.
2026-08-28 06:51:10 +02:00
edde746 1ee648f442 fix(plex): direct play a source the quality preset already covers
Any quality preset other than Original started a transcode even when the
file was already well under it: a 6.2 Mbps 1080p h264 source under the
"1080p 10 Mbps" preset came back from PMS as a 10.4 Mbps encode, so capping
quality on mobile data spent more data than leaving it uncapped, plus server
CPU nobody asked for (#2152).

A preset is a ceiling, so a version that fits under both its bitrate and its
resolution is now served by the file itself, on the same direct-play path
Original takes, and no transcode decision is sent at all. Height counts as
well as bitrate because the preset's label promises a resolution too, and a
device that asked for 1080p may not decode the 4K source a bitrate-only
comparison would hand it. An unreported bitrate or height leaves the
transcode standing.

The comparison has to happen in the client: PMS answers directPlay=1 with
"Direct play OK" whatever bitrate cap the request carries, measured on 1.43
with a 65 Mbps 4K source under a 10 Mbps cap, so its MDE cannot be asked to
enforce one. Plex Web reaches its own directPlay/directStream flags the same
way, folding the preset's bitrate into its client-side direct-play profile
first. Jellyfin already gets this right server-side.

close #2152
2026-08-27 20:37:56 +02:00
edde746 ed71b200b9 feat(music): make the mini player progress bar scrubbable
While music plays, the persistent mini player showed progress only as a
subtle background tint with no way to seek; the full seek bar sat one
navigation hop away on the now-playing screen (#2141).

The card's top edge now hosts a thin visible progress bar inside a taller
touch lane: tap seeks proportionally, dragging scrubs and commits one seek
on release, and hovering or dragging grows the bar and shows a thumb. Card
taps outside the lane still open the now-playing screen. Pointer-only by
design: the overlay never renders on TV and the keyboard focus chain leads
to the now-playing screen's focusable seek bar.

close #2141
2026-08-27 16:59:08 +02:00
edde746 a1d8ab5210 fix(plex): surface instant mix failures instead of silently doing nothing
Tapping Instant Mix on Plex often did nothing: the /playQueues POST used no
endpoint failover, createPlayQueue swallowed every error into null,
fetchInstantMix mapped that to an empty list, playInstantMix returned
silently on empty tracks, and the errors stream's only listener is the
now-playing screen, which is not mounted at tap time (#2141).

The play-queue POST now rides the shared endpoint failover (replaying it is
safe: an orphaned duplicate queue on the server is inert), createPlayQueue
and fetchInstantMix propagate typed errors, playInstantMix reports
started/empty/superseded and throws fetch failures, and the tap site shows
a translated snackbar for a failed or empty mix. A failed collection or
playlist queue launch now also reaches the real failure snackbar instead of
the misleading "no items" path.
2026-08-27 16:59:08 +02:00
edde746 a13febf67b fix(downloads): store music tracks as Music/{Artist}/{Album}/{NN - Title}
Downloaded tracks landed in the generic downloads/{serverId}/{ratingKey}/
layout as video.mp3 because the path builder only special-cases movies and
episodes, so a user-visible download folder showed opaque folders and
meaningless names (#2141).

Tracks now get Music/{Artist}/{Album}/{NN - Title}.{ext} in both file and
SAF modes, grouped by sanitized album-artist with Unknown Artist/Album
fallbacks and a disc prefix on multi-disc albums. Existing downloads keep
working: playback and deletion resolve the stored path per record, and the
empty-parent cleanup keeps shared album folders while a sibling track
remains.
2026-08-27 16:58:52 +02:00
edde746 a97be5823f feat(player): add play-next countdown, gesture toggles, and deinterlacing
Users could not adjust the five-second Play Next timer, switch off the
edge-swipe/pinch gestures they trigger by accident, deinterlace DVD-era
content, or keep always-on-top across episodes.

Play Next Countdown (0-30 s) joins Auto-Play & Skip; zero skips the
prompt and starts the next episode immediately, while the transient-
retry prompt keeps its fixed five-second spacing. Brightness swipe,
volume swipe, and pinch-to-zoom each get a switch in a mobile-only
Gestures group; a disabled edge also stops stealing the content-strip
drag. A Deinterlacing toggle (mpv-only — ExoPlayer has no filter chain)
sets mpv's deinterlace=auto at player init. The player's always-on-top
toggle now persists via a pref that is re-applied when the next player
opens, including autoplay episode transitions; the window flag is still
dropped when the player closes.

Also resolves the reorg TODO markers per triage: hold-to-speed lock
(#1046) will be gesture behavior rather than a setting, metadata
language (#1668) and delete re-authentication (#1924) are rejected, and
always-on-top is player-persisted state, not a Window setting (#1386).

close #1827
close #1810
close #2149
close #931
2026-08-27 16:42:38 +02:00
edde746 c9de78697b feat(settings): reorganize settings into general, appearance, and playback homes
Settings were grouped by screen history rather than by what they govern:
the profile-selection prompt, startup section, force-TV mode, and app
language sat under Appearance, Discord Rich Presence and the companion
remote host hid inside Playback > Behavior, and maximum volume lived
under Seek & Timing.

Introduce a General screen (language, startup, desktop window), regroup
Appearance into display/library/home/navigation/Live TV, split the
Playback monolith into engine, video & display, audio, quality,
subtitles, seek, auto-play & skip, and behavior groups, surface
auto-play next episode in settings, rename the Keyboard Shortcuts
section to Controls (now also hosting the companion remote host), move
Discord Rich Presence to Services > Integrations, and move the
performance-overlay switch to Advanced. Pref keys are unchanged, so
stored values, export/import, and reset behavior are unaffected.

TODO markers record the agreed homes for upcoming settings (#1998,
#2064, #1641, #1769, #2149, #1827, #2138, #1810, #1046, #1924, #1413,
#1668, #1386, #1795).
2026-08-27 16:02:10 +02:00
edde746 9ac8002b53 feat(player): apply separate default quality on cellular connections
On mobile data the single default streaming quality either burns
through the user's data plan or permanently caps quality at home.
Playback start now applies a dedicated cellular default when the
device is on a cellular-only connection, read from the app's existing
single connectivity subscription. The new setting defaults to
following the general default, ships in Settings > Playback for
phones/tablets only, and explicit per-play quality picks still win.

'Metered connection' had been spelled out independently in the offline
provider, the download WiFi-only gate and the sync-rule cooldown.
Rather than add a fourth copy, all four now share
ConnectivityLinkType. Network *presence* is deliberately left alone:
its callers disagree on whether an empty snapshot counts as
connected, so folding that in would change behavior.

showSelectionDialog now returns the picked DialogOption instead of its
raw value so the tile's null-valued 'Same as Default Quality' option
stays distinguishable from dismissing the dialog.

close #2147
2026-08-27 14:14:01 +02:00
edde746 942065e39d fix(plex): honor disabled credits detection when synthesizing chapter markers
When a Plex admin disables credits detection for a show or movie, PMS
strips detected credits markers from its responses, but Plezy's
chapter-title fallback resurrected the skip action from a credits-named
chapter — surfacing a "Next Episode" button (and auto-skip) during
credits the admin asked to leave alone.

Drop credits markers when the owning show/movie carries an explicit
enableCreditsMarkerGeneration=0. Movies read the attribute off the item
already fetched; episodes resolve the grandparent show through the
shared cache-first metadata row, so the lookup normally costs no extra
request and only runs when a credits marker is actually present. Absent
attribute (servers without the feature) and -1 (library default) keep
the fallback intact; lookup failures fail open. The offline cached
extras path applies the same rule from cached rows.

close #2137
2026-08-27 10:16:05 +02:00
edde746 94565b4ee0 feat(library): make grid spacing between posters configurable
The library grid packs posters edge to edge. The grid delegate's cross-
and main-axis spacing were hardcoded to zero, so the only visible gap
came from each card's own 3px padding, and Library Density changed
poster size rather than the space between posters. The result reads
denser than Plex, Jellyfin, or any commercial client, both horizontally
between posters and vertically between a poster and its title.

Add a Grid Spacing setting - Tight, Normal, Spacious (0/6/12px) - below
Library Density. Tight is the default and reproduces the current layout
exactly, so no existing grid moves on update.

The gutter lands in MediaGridDelegate.spacingFor(), already the single
funnel for non-full-bleed grid spacing, so every grid surface picks it
up: library browse, collections, playlists, downloads, and the detail
screens. Square music grids keep their 8px floor. The pref is watched
once in MediaCardSliverLayout - the only MediaGridGeometry.resolve()
call site - so grids re-layout live instead of on restart.

Grid cells grow the poster-to-title gap to 2/4/6px as well; the
Expanded poster absorbs the delta, so the cell keeps its aspect ratio.
Fixed-height hub-row cards keep the historical 2px because their text
band cannot absorb more, and hub-row cell packing opts out of the
setting entirely so shelves stay byte-identical across it.

Full-bleed TV grids are untouched - they already carry an 8-18px
scale-derived gutter - and are opt-in behind tvFullCardLayout, so the
default TV library grid follows the setting like every other surface.

Verified on macOS across all three steps: gutters and title gap grow,
grids re-pack live, and the segmented control reflects and writes the
value.

close #1597
close #2083
2026-08-27 09:21:48 +02:00
edde746 0c5308cfa1 refactor(networking): drop Cronet and NSURLSession for the tuned dart:io client
Plezy carried three native HTTP clients on the assumption that they beat
Dart's own. Benchmarked against real Plex and Jellyfin servers on macOS,
Windows, Linux and three Android devices, two of them do not.

Cronet loses on every request shape the app issues: 8.1 vs 83.8 MiB/s on a LAN
body read, 84 vs 150 req/s on an artwork fan-out, 14.3 vs 10.7 ms on
sequential API calls. It also fails a 60-way fan-out outright with
net::ERR_CACHE_WRITE_FAILURE under the 2 MiB memory cache we configured, and
cost 70-705 ms of CronetEngine.build on first use. Paying that build off the
critical path is the only reason AndroidPlatformHttpClient,
warmUpPlatformHttpClient and the per-request delegate swap existed; all three
go away with it.

CupertinoClient had no measured advantage either, losing the TLS fan-out 44 vs
60 req/s, and no reported issue ever justified it. tvOS already shipped the
dart:io client, so Apple platforms now agree with it.

WinHttpClient stays. WINHTTP_OPTION_IPV6_FAST_FALLBACK (#1128) has no dart:io
equivalent, and it brings the system proxy and the Schannel trust store.

The pool tuning becomes unconditional. It was opt-in behind usePlexApiClient
so generic tracker and auth clients stayed disposable, but every dart:io client
has carried connectionTimeout and forceCloseOnDrainTimeout since #1972, so
tuned and untuned already share shutdown semantics and the flag only cost
throughput: 12 connections per host with a 90s idle measured ~4x the dart:io
default on a 60-way fan-out on Linux and ~2x on Android.

media3-datasource-cronet and cronet-embedded stay. They back ExoPlayer's
CronetDataSource independently of package:cronet_http.

Refs #2140.
2026-08-27 09:21:48 +02:00
edde746 a18ecb5c4a test(watch-together): stop the relay setup tests racing a real handshake
Two relay setup tests failed on CI while the same commit had passed a
few minutes earlier. The harness rewrote every 10-second timer in the
zone to 10 ms, which also compressed the connect deadline of the
recovery that runs once setup retries are exhausted. On a loaded runner
that deadline expired after the relay had upgraded the socket but
before the client sent its first frame, so the recorded connection
carried no messages and shifted the expected sequence; when all three
recovery attempts were starved this way, the recovery never announced
at all.

A reply that never comes is safe to compress, a real loopback handshake
is not, and one zone hook matching on duration cannot tell them apart.
The service now exposes the setup acknowledgement, the release
handshake, and the release acknowledgement as separate budgets, each
still 10 seconds in production, and every test sets only the one it
means to expire. The blunt helper is gone; what remains collapses the
250 ms and 500 ms retry delays, which are waits rather than races.

Verified by reproducing both failure modes under CPU saturation, about
one run in ten, then 30 consecutive clean runs of the file under the
same load and a full suite at 6332 passing.
2026-08-27 09:21:48 +02:00
edde746 f7060478d1 fix(plex): offer User Rating and Plays sorts in home-video libraries
Browsing a Plex home-video / "Other Videos" library under "Show all"
offered no way to sort by your own rating. The sorts Plex advertises for
those sections only cover critic and audience ratings, which unmatched
files never carry, so nothing rating-based was usable.

Plex honors `sort=userRating:desc` and `sort=viewCount:desc` on those
sections without advertising them, and the client already appends both
for movie and TV libraries. Home-video sections are `type="movie"` on the
wire and only differ by `subtype="clip"`, which the library mapper folds
into MediaKind.clip for folder-first grouping and wide cards, so they
were failing the movie/show allow-list. Add clip to it.
2026-08-26 20:28:19 +02:00
edde746 3f03262833 fix(player): recover playback after a lost native-channel handoff
A player whose predecessor had not released the shared native channel
within three seconds skipped its own native dispose and chained its
release onto that predecessor. If the predecessor's teardown never
completed - one hung 4K session was enough - every later player waited
on the chain, failed to initialize, and playback stayed broken with
"Playback could not be started" until the app was killed.

Initialize and dispose now carry the creating instance's token. The
Android plugins remember which token created the current core and
acknowledge a dispose from any other token without touching the core,
so a dispose that lost the ownership race is provably stale and safe to
send. With that guard, a timed-out ownership wait force-disposes
instead of skipping, settles its release unchained, and frees the
channel for the next session; a dispose watchdog answers Dart even if
a native teardown hangs, leaking that one core instead of wedging all
future playback. Commands wait eight seconds (was three) so a slow but
healthy teardown delays the next session instead of failing it.

Verified on a Shield Pro: 38 back-to-back session races at 0.8-2.2s
gaps with zero failures and balanced teardowns, and a deep-link-over-
playback collision whose stale dispose is ignored, after which Back
returns to the still-playing session and Retry starts the new one.
Apple and desktop handlers ignore the token and keep the historical
skip semantics until they gain the guard.
2026-08-26 18:59:28 +02:00
edde746 3c6d398598 feat(player): default Android playback to the mpv backend
With video on the MediaCodec plane, real bitstream passthrough, media3
demuxing, and the GL fallback ladder in place, mpv measures at parity
or better than ExoPlayer on every device class tested (Tegra, Mali,
Amlogic armv7, Adreno; API 28-36; phone, TV, and foldable), including
the two historical Android mpv failure modes: low-end TV playback and
Hi10P.

Installs that never chose a backend move to mpv; a stored ExoPlayer
choice is preserved, and the Player Backend toggle stays user-visible
as the escape hatch. The automatic ExoPlayer-to-mpv runtime fallback
is unchanged.
2026-08-26 18:59:28 +02:00
edde746 8f893e7a67 fix(player): demux Android direct play with media3 again
The in-app FFmpeg container demuxer classified every DTS variant as
plain DTS (profile-blind MIME mapping at the demux boundary), so
DTS-HD MA lost its lossless identity downstream, and it ignored
container display-aspect-ratio overrides. media3's extractors get both
right.

Delete the FFmpeg demuxer (JNI, extractor, and its setting); ExoPlayer
direct play demuxes with media3's DefaultExtractorsFactory again, and
mpv - the Android default - reads container display dimensions itself.

close #2124
close #2115
2026-08-26 18:59:28 +02:00
edde746 465da3e82a feat(audio): bitstream passthrough by default on Android TV
mpv's ao_audiotrack hardcoded stereo IEC61937 frames, which cannot
carry TrueHD MAT or DTS-HD MA 8-channel bursts, and the app fed
audio-spdif an empty list in self-defense because mpv
force-passthroughs every codec named there with no decode fallback.
The mpv path therefore decoded everything, and passthrough had to be
enabled by hand on every install.

The Kotlin audio route probe (AudioOutputPolicy) now feeds mpv a
per-route audio-spdif list, the fork's multichannel IEC61937 patch
(pinned libmpv v1.1.3) provides the burst geometry, and E-AC3, TrueHD
and DTS-HD MA bitstream on the mpv path. Android TV installs with no
stored preference default to passthrough on, on both backends; an
explicit user choice is preserved.

Verified by route probing and mpv logs on Shield and Box R; no AVR was
in the loop, so receiver-side decode is unconfirmed.
2026-08-26 18:59:28 +02:00
edde746 04615ce2fc feat(player): HDR output for software-decoded video on Android
Software-decoded HDR always collapsed to SDR on Android: the EGL
window surface carried no colorspace, so PQ/HLG content that fell back
to software decode rendered through an SDR surface, and Android was
excluded from the hdr-enabled path outright.

The GL vo now requests a BT2020-PQ window surface (fork patch, pinned
libmpv v1.1.3) and Android joins the hdr-enabled path, so software
decode keeps HDR scanout and tone mapping happens only when the
display genuinely cannot show HDR.
2026-08-26 18:59:28 +02:00
edde746 a1c98b9d27 feat(player): render Android mpv video on the MediaCodec plane
Android mpv drew every frame through vo=gpu: an ImageReader/GLES copy
pinned to 8-bit RGB0 with a 100 ms timed wait per frame, which
truncated 10-bit and HDR output, raced acquireLatestImage, and
performed badly on low-end TVs.

Video now scans out on a SurfaceFlinger video plane through the fork's
vo=mediacodec: decoder buffers are queued to the surface at their
target PTS (av_mediacodec_release_buffer_at_time), so 10-bit and HDR
dataspaces reach the display untouched and frame pacing no longer
depends on GL vsync. Subtitles and OSD render on a sibling transparent
surface presented for the same PTS, frame-locked by construction.

The plane takes decoder buffers only, so software-decoded video stays
on a GL vo, with a chain-failure watchdog re-initializing the output
when the plane refuses a stream mid-session. The GL fallback is vo=gpu
- gpu-next breaks the Tegra GLES linker (#2010) - and GpuVoPolicy
selects gpu-next only when Dolby Vision RPU reshaping needs
libplacebo. AV1 film grain is applied in the decoder
(vd-lavc-film-grain=cpu) because the GL raster grain fallback is not
available on this path.

Pins libmpv-android v1.1.3, which carries the fork patches this path
rides on: the vo itself, BT2020-PQ EGL window surfaces, GLES direct
rendering treated as slow (H.264 Hi10P software decode on Tegra went
from 0.3x realtime with wrong colors to 1.0x with correct 10-bit
output), and multichannel IEC61937 for ao_audiotrack.

Verified on Shield Pro (Tegra/GLES), Pixel 7 (Mali), Box R 4K Plus
(Amlogic armv7), and Galaxy Z Fold3 (Adreno): 520 play/teardown cycles
and 3 hours of continuous 4K HDR playback; HDR engagement confirmed
via SurfaceFlinger dataspace BT2020_ITU_PQ.
2026-08-26 18:59:19 +02:00
edde746 85bf948004 fix(macos): keep text and icons legible by pinning the Skia renderer
On macOS 2.17.1 the UI started flashing garbage: text and Material icon
glyphs rendered as striped noise, and a keypress could flash a whole
glyph-atlas texture across the window. 2.17.0 on the same machine was fine.

2.17.1 moved to Flutter 3.47.1, which made Impeller the default macOS
renderer. Plezy is unusually exposed to that switch: MainFlutterWindow
clears the Flutter view to transparent so the mpv CAMetalLayer behind it
stays visible, so whatever Impeller leaves in the presented drawable is
blended onto the screen instead of being hidden under an opaque frame.

Opt out through the Info.plist switch the macOS embedder reads from the
main bundle at launch, so it applies to release builds too. That restores
the renderer every release up to 2.17.0 shipped; keep it until the engine
fixes for the 3.47 macOS Impeller regressions reach stable.

close #2132
2026-08-26 17:16:21 +02:00
edde746 c564635ad9 fix(jellyfin): advertise video codecs from a native hardware-decode probe
A device with no hardware HEVC decoder could still be handed an HEVC
transcode: the device profile advertised a fixed codec list that assumed
every device decodes everything. Prepending AV1 to reach the AV1 encoders
issue #2131 asks for would have made that worse - an Apple TV 4K and every
iPhone before the A17 Pro have no AV1 decoder at all.

Probe the platform instead. Android enumerates MediaCodecList for a
hardware decoder and iOS/tvOS ask VideoToolbox, both feeding one latched
VideoDecodeCapabilities that the device profile reads when it builds its
codec lists. Desktop deliberately answers nothing: a pre-Kaby-Lake Mac has
no hardware HEVC decoder and an M1 no AV1 one, yet both software-decode in
real time, so narrowing there would force transcodes for nothing. An
unanswered or failed probe advertises everything, so the list never
narrows on missing data.

The transcode target becomes av1,hevc,h264 filtered by the probe. Leading
with AV1 is safe because the server rotates codecs its admin has not
enabled ("Allow encoding in HEVC/AV1 format", both off by default) to the
back before picking one, so it costs nothing on a server that will not
emit AV1.

Audio now accepts everything the path can carry. The direct-play profile
drops its AudioCodec list entirely - an omitted list means "any codec" to
Jellyfin - so an audio stream can no longer be what blocks direct play.
The transcode target lists every codec Jellyfin can put in an fMP4
segment, so a video-only transcode copies DTS or TrueHD instead of
re-encoding it. Two limits bound that string: the server validates it
against ^[a-zA-Z0-9\-\._,|]{0,40}$ when it echoes the list into the
transcode URL, so alac does not fit and * is not a wildcard; and omitting
the key is not "accept everything" here the way it is for direct play,
because the server substitutes the source codec and then ships no audio at
all for a source fMP4 cannot carry.

close #2131
2026-08-26 16:53:14 +02:00
edde746 fce4193f7e fix(jellyfin): negotiate video transcodes as fMP4 HLS instead of MPEG-TS
MPEG-TS segments cannot carry AV1, so a Jellyfin server with an AV1
hardware encoder (Intel Arc and similar) could never pick AV1 as the
transcode output no matter what the codec list advertised. Switch the
video TranscodingProfile container from ts to mp4, matching
jellyfin-web's fMP4 HLS behavior; mpv consumes fMP4 HLS on every
platform and the Plex VOD target has shipped it since issue #1859.

close #2131
2026-08-26 14:41:37 +02:00
edde746 628fd21ec3 fix(windows): keep the video visible behind the OSD by pinning the Skia renderer
After 2.17.1 the video went fully black on Windows for as long as the player
UI was on screen and came back the instant it faded out. Audio was never
affected, mpv.conf made no difference, and 2.17.0 on the same machine and
driver was fine.

2.17.1 moved to Flutter 3.47.1, which made Impeller the default Windows
renderer. Plezy is unusually exposed to that switch: the patched engine
presents the UI on a topmost DirectComposition visual that DWM blends over the
mpv video child, so the video is only as visible as the presented frame's
per-pixel alpha says it is. When Impeller's GLES backend gets that alpha wrong,
the OSD's full-screen scrim presents opaque and hides the video outright;
unmounting the controls presents a transparent frame again, which is why the
picture tracked the UI exactly.

Opt out through the embedder Impeller switch that 3.47 added. Unlike
FLUTTER_ENGINE_SWITCHES it applies to release builds, and it restores the
renderer every release up to 2.17.0 shipped without touching the engine patches.

close #2127
2026-08-26 03:15:59 +02:00
edde746 d2c4e8345f fix(player): retry transient stream errors instead of falling back to MPV
During Android direct play, a single dropped connection or network blip
mid-stream kicked an otherwise healthy ExoPlayer session over to the MPV
fallback - on a Shield this showed as random backend switches minutes
into an episode (log pu4ad: "ffmpeg demuxer read failed: -5" with
contentIsMalformed=true, while MPV reopened the same URL fine).

Three defects lined up behind it, all in the 2.17.0 ffmpeg demux path:

- The AVIO read callback turned the input proxy's stored IOException
  (its -1 return) into a bare AVERROR(EIO) without marking javaError, so
  the extractor classified the failure as a malformed container. media3
  never retries a ParserException, so the designed ERR_JAVA ->
  IOException -> load-error-retry path was unreachable. The callback now
  latches javaError for a negative read and fails fast on every later
  read in the same native call, so matroska resync cannot clobber the
  stored message or skip past the failed range.
- A failed refill latches AVIOContext error/eof_reached and avio never
  drives the callback again, so even a correctly classified retry would
  re-fail on the stale error. nativeReadPacket and nativeSeek now clear
  the latch on entry; a genuine end of file (error == 0) is left alone.
- FfmpegRandomAccessSource kept a handle whose read had thrown, and its
  position matched the retried request, so the retry was handed the same
  dead handle. A failed read now drops the handle and the retry reopens.

A transient failure now surfaces as a retryable IOException, media3
reopens the source, and sample delivery resumes gaplessly; persistent
failures still exhaust the retry policy and reach the MPV fallback as
before.

close #2113
2026-08-25 11:15:43 +02:00
edde746andGitHub ae555ed5d7 feat(settings): account preferences, stored on the server instead of the device (#2112)
Preferences that belong to the media-server account had nowhere to live in Plezy. Jellyfin keeps a user's audio/subtitle language, subtitle mode and several library options in `UserConfiguration`; plex.tv keeps the same language choices plus watched indicators and review visibility on the account. Plezy read four of those fields for auto-track selection and could never show or change any of them.

Adds an Account preferences section under Connections. One account edits in place; several show a picker first, scoped to the active profile's own connections so a managed Plex Home user never edits the owner's record. Every row is gated on what the backend can actually store.

It also moves "rewatching in Next Up" onto the account. There is no `UserConfiguration` field for it, but the per-user `DisplayPreferences` store is keyed `(userId, displayPreferencesId, client)` with no device component, so the switch goes there and follows the account.

`AccountRef` keys by account, not `clientScopeId`: MediaBrowser is `{machineId}/{userId}`, Plex is (account, Home user). Writes are patch-shaped because both backends replace whole objects — Jellyfin's `POST /Users/{id}/Configuration` and its `DisplayPreferences` row both reset omitted fields, so each write re-reads, merges only the patched keys, and posts back. Plex takes its changes as query parameters with an empty body, and its `experience` blob and the PMS `/accounts/1` mirror are deliberately untouched.

`AccountPreferencesController` owns a single repository above the profile session, so changing a language in settings reaches the next playback without a restart. Emby is gated out of rewatching through `MediaBrowserDialect.supportsNextUpRewatching`.

close #1910
2026-08-25 08:42:48 +02:00
edde746 779c532fc7 chore(i18n): regenerate german strings after the premiereDate update
de.i18n.json changed in #2106 without a codegen run, so scripts/codegen.sh
--check failed on strings_de.g.dart. No other locale is affected.
2026-08-25 03:11:11 +02:00
edde746 595cd9fe24 feat(seerr): sign in with quick connect and accept schemeless urls
Linking a Seerr instance had two rough edges, both on the Jellyfin path.
The URL step prepended https:// to anything without a scheme, so a
plain-HTTP instance on the LAN — 192.168.1.5:5055, or a bare host on
Seerr's default port — failed with "could not reach" unless the user knew
to type the scheme. And the only way to sign in with a Jellyfin account
was typing a username and password, which on a TV remote is misery.

Schemeless input now expands into candidates that are probed together,
but TLS wins by construction: a plaintext success is held while any https
candidate is still in flight and is accepted only once they have all
failed. The sign-in that follows posts a password to whichever URL wins
here, so a slow-but-working https endpoint has to beat a fast plaintext
one, and the wait is already bounded by the probe timeout. When nothing
answers, the failure that reached a server outranks a transport error, so
"this instance has not completed first-run setup" is not masked by "could
not reach https://..." from a candidate the user never typed.

Quick Connect goes through Seerr's own proxy routes (3.4+): initiate,
then check polled at Seerr's own 2s cadence through the shared
pollWithBackoff — a 404 mid-poll means the secret is gone and is terminal
— then authenticate, which mints the session cookie through the existing
sign-in path. The secret rides in a query string, so it is registered for
log redaction the way the Jellyfin flow registers its own. An instance
that predates the routes 404s the initiate and is told apart from a
generic rejection.

SeerrAuthMethod.quickConnect deliberately stores no secret: silent
re-auth is impossible by construction, so an expired cookie lands in the
existing "no stored credentials" arm, unlinks the session, and the
connect flow asks for a fresh code. The affordance is limited to the
Jellyfin credential form, since Seerr rejects Quick Connect for Emby, and
is not auto-started on TV the way the MediaBrowser form is:
/settings/public exposes no "Quick Connect enabled" flag to gate that on.

The waiting panel and its attempt/cancel bookkeeping moved out of
AddJellyfinScreen into QuickConnectCodePanel and QuickConnectFlowMixin so
both screens share one implementation, and the post-frame focus helper
both forms use moved to AsyncFormStateMixin.
2026-08-25 03:07:14 +02:00
edde746 75279ca196 refactor(discovery): expand server url candidates through one shared helper
Turning what a user typed into URLs worth probing lived inside
JellyfinEndpointDiscovery, next to a private copy of the scheme-detection
regex that url_utils already had. A second backend needs the same rules
with a different default port, and the existing copy carried two latent
defects: input with no host came back unchanged, so a candidate that can
never resolve went to the prober, and a query string survived into the
base URL that request paths are appended to.

expandBaseUrlCandidates takes the ordered guesses a backend wants — a
scheme plus the port to try when the user typed none — and owns the rest.
A typed port beats a guessed one, so host:8096 collapses to one candidate
per scheme and the same guess list covers both bare hosts and hosts with
ports. Candidates are built field by field rather than through
Uri.replace, whose null query means "keep mine", so query and fragment
are dropped for real. Blank input and input with no host expand to
nothing.

JellyfinEndpointDiscovery passes its existing order, so its probe and
persistence behaviour is unchanged, and hasUrlScheme replaces the regex
copy.
2026-08-25 03:07:14 +02:00
BockiandGitHub bd2d19a1c1 Update de.i18n.json (#2106) 2026-08-25 01:21:35 +02:00
github-actions[bot] 66c6b137a5 chore: update cask to 2.17.1 2026-08-24 23:18:29 +00:00
edde746 805632efc1 chore: bump version to 2.17.1 2026-08-25 00:43:37 +02:00
edde746 fe9d9d5e7d test(player): cover saf reads and the demuxer's jni byte source under r8
The random-access demuxer added two surfaces with no gate behind them. A
download stored through SAF arrives as content://, where an index read has to
open a second descriptor while the loader holds one, and the AVIO callbacks
are reached only by name from C++, so R8 may rename them while every debug
check passes -- the shape #1703 shipped with.

FfmpegExtractorSeekTest now runs the cued fixture over a content:// URI
served by a test-only provider, and FfmpegDemuxerReachabilityTest drives
nativeOpen plus a seek through a media3-free byte source so it can run
minified. Wired into the existing R8 reachability target; verified it fails
when the FfmpegDemuxerJni$Input keep rule is removed.
2026-08-24 23:02:23 +02:00
edde746 1995fc6b75 refactor(player): demux through random-access IO instead of unwinding libavformat
Resuming or seeking a large MKV on the Android FFmpeg demux path buffered
for minutes with no error and no fallback. The cause was the seam, not the
container: libavformat's demuxers own their seeking — end-of-file index,
back to the header, binary search — while media3's Extractor is forward
only, so every backward jump had to abort the in-flight libavformat call
with a synthetic IO error and replay the whole open. matroskadec attempts
its deferred Cues parse exactly once per context, so that abort burned the
index for the session and every later seek fell back to libavformat's
linear generic scan, reading the file up to the target.

That protocol had produced a bug of this shape repeatedly (a NULL deref
resuming find_stream_info across an abort, a use-after-free on fallback
teardown, matroskadec resync skipping the keyframe cluster) and it was held
together by four empirical retry budgets whose exhaustion degraded silently
— which is why #2096 looked like an unbounded spinner instead of an error.

Give libavformat what it requires instead. FfmpegRandomAccessSource serves
any absolute position from a second DataSource built by the same factory
media3 uses, cloning the DataSpec media3 opened for the item so Cronet,
download caches, SAF and per-item request headers still apply. Reads that
the loader's ExtractorInput can serve still go through it, so sample
delivery keeps feeding media3's byte accounting, back-pressure and
load-error policy; the extractor nudges the loader to follow the demuxer
with one RESULT_SEEK, which is an optimization that can never stall
playback because the read succeeds either way.

Deleted with the protocol: the synthetic AVERROR_NEED_SEEK unwind, the
24 MB replay cache, the Cues priming seek and its read guard, the sticky
AVIO error clearing, and all four budgets (seekAttempts, stickyRecovers,
MAX_RECONCILES, MAX_OPEN_LOADER_ROUND_TRIPS). Opens run straight through,
seeks are one avformat_seek_file on the loader thread, and an IO failure is
an IOException that reaches media3's retry policy instead of a silent
degradation. The demuxer is now the only component that resolves seeks, so
the extractor's sample-derived seek index is gone too.

Verified on a SEI Robotics Android TV box against Jellyfin: the DV P8.1
28.4 GB MKV resume that started at a 4m28s spinner opens in 1.1 s and
resolves its seek in one call, a mid-playback scrub resolves in 1 ms from
the in-memory index, a 2 GB MKV resume opens in 0.5 s, fresh playback needs
no index read at all, and MP4/AVI still open and deliver. New coverage:
FfmpegRandomAccessSourceTest pins the read/reopen contract, and
FfmpegExtractorSeekTest asserts on-device that a cued seek lands on target
without walking the file and that a cueless file still reaches it.

close #2096
2026-08-24 23:02:23 +02:00
edde746 defb0f4983 fix(player): publish real dolby vision profiles from the ffmpeg demuxer
The demuxer JNI read AV_PKT_DATA_DOVI_CONF as the bit-packed dvcC/dvvC box
layout, but libavformat surfaces the unpacked
AVDOVIDecoderConfigurationRecord — one byte per field. Profile 8 level 6
streams therefore surfaced as "dvh1.04.00" plain HEVC (visible in #2096's
logs): DoviConvertingTrackOutput's profile 7 conversion could never engage,
and profile 8 was routed to the plain HEVC decoder instead of the Dolby
Vision decoder the media3 demux path selects.

Read the struct fields verbatim and mirror media3's DolbyVisionConfig in
Kotlin: recognized profiles publish video/dolby-vision with the
dvhe/dvav/dav1 RFC 6381 codecs string, unrecognized profiles keep the base
codec's format. A DV P8.1 MKV on the FFmpeg path now reports "dvhe.08.10",
mime video/dolby-vision, and takes the DV P8 passthrough path exactly like
the media3 demuxer.
2026-08-24 23:02:23 +02:00
edde746 371eab8637 fix(downloads): stream from the server when a downloaded SAF copy is unreachable
Playing a downloaded movie or episode failed outright once the storage
holding it was gone — an SD card ejected, a USB volume unplugged, the
files deleted from outside the app (issue #2101). The item still played
fine from the server, but Plezy never tried: it committed to the local
copy and surfaced mpv's open failure.

Resolution already refused a missing local copy and let the caller
stream instead, but only for filesystem paths. SAF `content://` URIs
skipped the check on the premise that they are always playable as
written. They are not: an unmounted volume answers
`FileNotFoundException: No root for <volume>`, and a revoked grant is
just as dead, while the row still reads `completed`.

Probe the stored document through SAF before preferring it, so an
unreachable copy resolves to nothing and every consumer degrades the
way a missing file already did — the player streams, and both
external-player entry points launch against the server instead of a
dead URI.

close #2101
2026-08-24 19:27:20 +02:00
edde746 f357be4077 fix(tvos): match Siri Remote navigation to measured native focus-engine physics
Siri Remote navigation felt sluggish and then over-sensitive next to
native tvOS apps (issue #2006): swipes were priced at a fixed travel
per step, a single flick could glide into a second focus step, hard
lifts coasted several extrapolated steps, and rail scrolls snapped in
65-250ms where native glides.

Retuned the whole path against two hardware instrumentation passes on
an Apple TV 4K: committed-move telemetry through an experimental
UIFocusItem bridge (branch feat/tvos-native-focus-bridge), then a
dedicated native probe app logging every touch sample, pan velocity,
engine hint, focus step, and scroll tick across 101 swipe sessions on
160/230/300pt tiles. What the data showed, now encoded:

- step pricing follows geometry: one step costs the focused item's
  extent along the swipe axis plus ~155pt (measured 314/391/410pt on
  160/230/300pt tiles), not a fixed distance. Thresholds derive per
  axis from the primary focus rect, normalized so a wide-flat control
  steps vertically once the finger covers its height. Locked-focus
  rows (hub rows, the TV browse rail) vend their selected card's rect
  through the new LockedFocusRowNode so the row-wide focus node's
  screen-sized rect never prices the step. Scopes, the player's
  catch-all surfaces, and unbuilt cards fall back to a fixed 400pt.
- a lift never coasts more than one step: sessions with lift
  velocities up to ~11400pt/s never produced a second coast step. The
  glide is gated on a sustained drag (two consecutive same-direction
  steps), cancelled by reversal pivots and new touches, so a discrete
  flick moves exactly one item.
- the native 'inertia' feel is the scroll animation, not focus
  physics: the engine's scrollable containers settle over ~450-900ms
  of ease-out. TV rail and hub-row navigation scrolls now retarget a
  500ms easeOutCubic animation per step, so drags and hold-repeats
  chain into one continuous glide that catches up on release.
2026-08-24 13:41:44 +02:00
edde746 0fdc22b4fa fix(linux): make the render-executor completion handoff visible to TSan
GLib's GMutex is a raw futex, so ThreadSanitizer cannot see the
happens-before edge g_main_context_invoke_full provides and fails the
thread lane on plane_render_executor_test. Publish the completion
payload through an explicit release/acquire pair, and suppress the
remaining reports that originate wholly inside libglib's own source
bookkeeping. Races on repository memory still fail the run; verified
0/100 failing TSan runs in an ubuntu:24.04 container (20/20 before).
2026-08-24 12:42:23 +02:00
edde746 fb85bb279a style(android): apply ktlint to the ffmpeg audio csd code
The Native Formatting lane rejects the unbraced multiline if/else in
FfmpegExtractor and the wrapped expression body in FfmpegAudioCsdTest.
2026-08-24 12:42:23 +02:00
edde746andGitHub cc4eff98c0 build: migrate to Flutter 3.47.1 (#2092) 2026-08-24 12:32:05 +02:00
edde746 0fbf2e205c fix(profiles): skip the resume profile prompt during a live companion-remote session
With "Ask for profile on app open" enabled, every screen-off or app
switch on a phone re-pushed the profile picker and PIN on resume, burying
a companion-remote session that survives backgrounding since the
reconnect cycle was made lifecycle-aware. Suppress the resume prompt
while a companion session is live, mirroring the active-playback
exemption — the session already belongs to the profile that started it.

close #2087
2026-08-24 10:06:00 +02:00
edde746 a9f2c1ff99 fix(player): shape ffmpeg demuxer audio extradata into media3 csd layouts
VP9/WebM-origin files stalled at 0ms in ExoPlayer on 2.17.0 and fell
back to MPV after ~40s. The ffmpeg demuxer published raw extradata as
the only initializationData entry; Android's Opus decoders consume
their first three input buffers as OpusHead/codec-delay/seek-pre-roll,
so they ate the first two real packets as configuration and silently
discarded every decoded sample, pinning the audio-driven clock at 0.
Vorbis (unsplit Xiph lacing) and FLAC (unmarked STREAMINFO) had the
same shape divergence.

Shape audio extradata into the layouts media3's own extractors emit
before publishing the track format.

close #2088
2026-08-24 09:36:10 +02:00
github-actions[bot] fe4e53f912 chore: update cask to 2.17.0 2026-08-24 05:28:32 +00:00
376 changed files with 31847 additions and 5484 deletions
+3 -3
View File
@@ -5,7 +5,7 @@ description: >-
subosito/flutter-action cannot resolve the release for arm64 (no arm64 entry
in the stable manifest) and `channel: master` would clone master HEAD, whose
engine is not the patched revision install-patched-engine.ps1 asserts
(4c525dac). This file is the only place that pin lives: the tag is fetched so
(5d531788). This file is the only place that pin lives: the tag is fetched so
the SDK reports its own version, then verified against the immutable commit
and against the version Flutter itself reports, so a moved tag fails the job
instead of quietly changing SDKs.
@@ -16,8 +16,8 @@ runs:
- name: Clone Flutter from its immutable commit
shell: pwsh
run: |
$version = "3.44.0"
$expectedCommit = "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
$version = "3.47.1"
$expectedCommit = "6655482ec06e547f90abf8ae7590466f4415978d"
$root = "$env:RUNNER_TEMP\flutter"
git init $root
git -C $root remote add origin https://github.com/flutter/flutter.git
+2 -2
View File
@@ -31,7 +31,7 @@ on:
env:
# Only place this workflow names the SDK; .github/actions/setup-flutter-git pins the same release.
FLUTTER_VERSION: "3.44.0"
FLUTTER_VERSION: "3.47.1"
# Shared by every release build command; SENTRY_DIST stays per-platform.
RELEASE_DART_DEFINES: --dart-define=ENABLE_UPDATE_CHECK=true ${{ github.repository == 'edde746/plezy' && '--dart-define=ENABLE_SENTRY=true' || '' }} --dart-define=GIT_COMMIT=${{ github.sha }} --dart-define=SENTRY_ENVIRONMENT=github --dart-define=ENABLE_DONATIONS=true
TRUSTED_BUILD_CACHE_VERSION: trusted-build-v1
@@ -72,7 +72,7 @@ jobs:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v5
with:
distribution: "temurin"
java-version: "21"
+3 -3
View File
@@ -20,7 +20,7 @@ on:
env:
# Only place this workflow names the SDK; .github/actions/setup-flutter-git pins the same release.
FLUTTER_VERSION: "3.44.0"
FLUTTER_VERSION: "3.47.1"
jobs:
analyze:
@@ -175,7 +175,7 @@ jobs:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v5
with:
distribution: "temurin"
java-version: "21"
@@ -244,7 +244,7 @@ jobs:
persist-credentials: false
- name: Setup Java
uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95 # v5
uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v5
with:
distribution: "temurin"
java-version: "17"
+2 -2
View File
@@ -1,6 +1,6 @@
cask "plezy" do
version "2.16.0"
sha256 "87568115810fd627babfd05967fbc7d1bf9f40bd38eed319cf7b404b09e47162"
version "2.18.0"
sha256 "affa0922fb33b6ca79a0d6ce7e5042539097a0ea097f011c9a4cfdbe0e822f94"
url "https://github.com/edde746/plezy/releases/download/#{version}/plezy-macos.dmg"
name "Plezy"
+1 -1
View File
@@ -148,7 +148,7 @@ Package managers:
## Building from Source
### Prerequisites
- Flutter SDK 3.44.0+
- Flutter SDK 3.47.0+
- A Plex account, or a Jellyfin or Emby server with user credentials
### Setup
+7
View File
@@ -4,6 +4,13 @@ analyzer:
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- build/**
- android/**
- ios/**
- web/**
- windows/**
- macos/**
- linux/**
linter:
rules:
+25 -14
View File
@@ -58,12 +58,18 @@ plugins {
id("dev.flutter.flutter-gradle-plugin")
}
val mpvVersion = "v1.0.7"
val mpvSha256 = "d55d440e587b2a9ffb91874d93069460a987be05fe72af8394849983f0df2d7a"
val mpvVersion = "v1.2.2"
val mpvSha256 = "0207bb46660c239268c4c17bddc7fd570722513344b136c7c5336aca135dc8a0"
val mpvDir = layout.buildDirectory.dir("libmpv").get().asFile
val mpvAar = "libmpv-release.aar"
val mpvUrl = "https://github.com/edde746/libmpv-android/releases/download/$mpvVersion/$mpvAar"
// Dev-only escape hatch: point the build at a locally built libmpv AAR
// (libmpv-android's build-local.sh output) to test fork patches before a
// release is pinned. The URL + sha256 above stay authoritative otherwise.
val localMpvAar: String? = (project.findProperty("plezy.localMpvAar") as String?)
?: System.getenv("PLEZY_LOCAL_MPV_AAR")
val media3Version = "1.11.0"
val mpvFfmpegVersion = "8.0.1"
val mpvFfmpegSourceSha256 = "05ee0b03119b45c0bdb4df654b96802e909e0a752f72e4fe3794f487229e5a41"
@@ -76,6 +82,8 @@ val downloadLibmpv = tasks.register("downloadLibmpv") {
inputs.property("version", mpvVersion)
inputs.property("sourceUrl", mpvUrl)
inputs.property("sha256", mpvSha256)
inputs.property("localOverride", localMpvAar ?: "")
localMpvAar?.let { inputs.file(it) }
outputs.files(aar, manifest)
doLast {
mpvDir.parentFile.mkdirs()
@@ -83,15 +91,20 @@ val downloadLibmpv = tasks.register("downloadLibmpv") {
try {
staging.mkdirs()
val stagedAar = File(staging, mpvAar)
try {
providers.exec {
commandLine("curl", "-sfL", mpvUrl, "-o", stagedAar.absolutePath)
}.result.get().assertNormalExitValue()
} catch (error: Exception) {
throw GradleException("Failed to download $mpvAar $mpvVersion", error)
if (localMpvAar != null) {
File(localMpvAar).copyTo(stagedAar, overwrite = true)
File(staging, ".manifest").writeText("version=local\nsource=$localMpvAar\n")
} else {
try {
providers.exec {
commandLine("curl", "-sfL", mpvUrl, "-o", stagedAar.absolutePath)
}.result.get().assertNormalExitValue()
} catch (error: Exception) {
throw GradleException("Failed to download $mpvAar $mpvVersion", error)
}
verifySha256(stagedAar, mpvSha256, "$mpvAar $mpvVersion")
File(staging, ".manifest").writeText("version=$mpvVersion\nsha256=$mpvSha256\n")
}
verifySha256(stagedAar, mpvSha256, "$mpvAar $mpvVersion")
File(staging, ".manifest").writeText("version=$mpvVersion\nsha256=$mpvSha256\n")
promoteDirectory(staging, mpvDir)
} finally {
staging.deleteRecursively()
@@ -132,7 +145,7 @@ val prepareMpvFfmpegDevelopment = tasks.register("prepareMpvFfmpegDevelopment")
val aar = File(mpvDir, mpvAar)
val manifest = File(mpvFfmpegDevelopmentDir, ".manifest")
val abis = listOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64")
val libraries = listOf("avcodec", "avformat", "avutil", "swresample")
val libraries = listOf("avcodec", "avutil", "swresample")
inputs.file(aar)
inputs.property("ffmpegVersion", mpvFfmpegVersion)
inputs.property("sourceUrl", mpvFfmpegSourceUrl)
@@ -145,7 +158,6 @@ val prepareMpvFfmpegDevelopment = tasks.register("prepareMpvFfmpegDevelopment")
outputs.files(
File(mpvFfmpegDevelopmentDir, "include/libavcodec/avcodec.h"),
File(mpvFfmpegDevelopmentDir, "include/libavutil/avconfig.h"),
File(mpvFfmpegDevelopmentDir, "include/libavformat/avformat.h"),
File(mpvFfmpegDevelopmentDir, "include/libswresample/swresample.h"),
manifest
)
@@ -183,7 +195,7 @@ val prepareMpvFfmpegDevelopment = tasks.register("prepareMpvFfmpegDevelopment")
} catch (error: Exception) {
throw GradleException("Failed to extract FFmpeg $mpvFfmpegVersion headers", error)
}
listOf("libavcodec", "libavformat", "libavutil", "libswresample").forEach { library ->
listOf("libavcodec", "libavutil", "libswresample").forEach { library ->
project.copy {
from(File(extractedSource, library)) {
include("*.h")
@@ -207,7 +219,6 @@ val prepareMpvFfmpegDevelopment = tasks.register("prepareMpvFfmpegDevelopment")
from(zipTree(aar)) {
include(
"jni/*/libavcodec.so",
"jni/*/libavformat.so",
"jni/*/libavutil.so",
"jni/*/libswresample.so"
)
+15 -8
View File
@@ -19,12 +19,19 @@
# growOutputBuffer's JNI descriptor names this type, so it may not be renamed either.
-keep class androidx.media3.decoder.SimpleDecoderOutputBuffer { *; }
# ffmpeg_demuxer_jni.cc resolves the AVIO input proxy's callbacks by name
# (FindClass on the interface, GetMethodID for position/read/length). Keeping
# the interface members pins the names on every implementation, including the
# anonymous proxy inside FfmpegExtractor.
-keep interface com.edde746.plezy.exoplayer.FfmpegDemuxerJni$Input {
long position();
int read(byte[], int);
long length();
# MatroskaExtractor.init is final and its ExtractorOutput / subtitle scratch buffer live
# in private fields, so the extractor wrappers reach both by name: AssMatroskaExtractor
# (android/libass/.../media/extractor/AssMatroskaExtractor.kt) resolves extractorOutput
# and subtitleSample with getDeclaredField to redirect ASS subtitle samples, and
# MatroskaLatmSupport (android/app/.../exoplayer/MatroskaLatmSupport.kt) resolves
# extractorOutput the same way to wrap LATM tracks.
#
# R8 renaming either field makes getDeclaredField throw; AssMatroskaExtractor resolves
# them in its companion object, so the throw surfaces as ExceptionInInitializerError
# while constructing the extractor — every MKV direct-play, release builds only. Only
# the *names* need pinning: MatroskaExtractor uses both fields itself, so they survive
# shrinking through the compile-time subclass references.
-keepclassmembernames class androidx.media3.extractor.mkv.MatroskaExtractor {
private androidx.media3.extractor.ExtractorOutput extractorOutput;
private androidx.media3.common.util.ParsableByteArray subtitleSample;
}
@@ -0,0 +1,171 @@
package com.edde746.plezy.exoplayer
import android.net.Uri
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.ProgressiveMediaSource
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorsFactory
import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.extractor.text.DefaultSubtitleParserFactory
import androidx.test.platform.app.InstrumentationRegistry
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.parser.AssSubtitleParserFactory
import java.io.File
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* On-device coverage for MKV files whose SeekHead references Tracks after the Clusters (#1969).
*
* media3 1.11.0 builds the Matroska seek map at the end of the Cues element, which for these files
* is before the Tracks element parsed, so the map permanently reports unseekable and ExoPlayer
* coerces every seek to t=0 (ProgressiveMediaPeriod; tracking issue androidx/media #3377). The
* first test is a canary asserting the upstream defect against a stock MatroskaExtractor — when it
* fails after a media3 upgrade, the TrackAwareSeekMap repair in [CuelessSeekExtractorWrapper] can
* be retired. The second test drives the production wrapper stack and requires the seek to
* actually land.
*/
class MatroskaLateTracksSeekDeviceTest {
private companion object {
const val TAG = "MkvLateTracksSeek"
const val SEEK_TARGET_MS = 500L
const val SETTLE_MS = 2_000L
}
private class Session(
val handler: Handler,
val thread: HandlerThread,
val player: AtomicReference<ExoPlayer?>,
val fixture: File
)
@Test
fun stockMatroskaExtractorSnapsSeeksToStart() {
val result = runSeekScenario("stock") { MatroskaExtractor(DefaultSubtitleParserFactory()) }
assertFalse(
"upstream media3 now reports tracks-after-clusters MKVs seekable — " +
"the TrackAwareSeekMap repair in CuelessSeekExtractorWrapper can be retired",
result.seekable
)
assertTrue(
"unseekable media must snap the seek to the start, position=${result.positionAfterSeekMs}ms",
result.positionAfterSeekMs < SEEK_TARGET_MS / 2
)
}
@Test
fun wrappedExtractorKeepsSeekPosition() {
val result = runSeekScenario("wrapped") {
val assHandler = AssHandler()
CuelessSeekExtractorWrapper(ZlibMatroskaExtractor(AssSubtitleParserFactory(assHandler), assHandler))
}
assertTrue("wrapped extractor must report the item seekable", result.seekable)
assertTrue(
"seek to ${SEEK_TARGET_MS}ms must hold, position=${result.positionAfterSeekMs}ms",
result.positionAfterSeekMs >= SEEK_TARGET_MS / 2
)
}
private class ScenarioResult(val seekable: Boolean, val positionAfterSeekMs: Long)
private fun runSeekScenario(label: String, extractorFactory: () -> Extractor): ScenarioResult {
val session = openPaused(label, extractorFactory)
try {
val seekable = onPlayerThread(session) { it.isCurrentMediaItemSeekable }
session.handler.post { session.player.get()?.seekTo(SEEK_TARGET_MS) }
// The masked position is the seek target; the snap surfaces once the period resolves the
// seek, so give the load a moment before sampling the settled position.
Thread.sleep(SETTLE_MS)
val positionAfterSeekMs = onPlayerThread(session) { it.currentPosition }
Log.i(TAG, "==== $label: seekable=$seekable position=${positionAfterSeekMs}ms ====")
return ScenarioResult(seekable, positionAfterSeekMs)
} finally {
teardown(session)
}
}
private fun openPaused(label: String, extractorFactory: () -> Extractor): Session {
val instrumentation = InstrumentationRegistry.getInstrumentation()
val context = instrumentation.targetContext
val fixture = copyFixture(context)
val thread = HandlerThread("mkv-late-tracks-$label").apply { start() }
val handler = Handler(thread.looper)
val player = AtomicReference<ExoPlayer?>(null)
val ready = CountDownLatch(1)
val failed = AtomicBoolean(false)
handler.post {
val exo = ExoPlayer.Builder(context).build()
player.set(exo)
exo.addListener(
object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
if (playbackState == Player.STATE_READY) ready.countDown()
}
override fun onPlayerError(error: androidx.media3.common.PlaybackException) {
Log.e(TAG, "$label player error", error)
failed.set(true)
ready.countDown()
}
}
)
val source = ProgressiveMediaSource.Factory(
DefaultDataSource.Factory(context),
ExtractorsFactory { arrayOf(extractorFactory()) }
).createMediaSource(MediaItem.fromUri(Uri.fromFile(fixture)))
exo.setMediaSource(source)
exo.playWhenReady = false
exo.prepare()
}
val session = Session(handler, thread, player, fixture)
if (!ready.await(30, TimeUnit.SECONDS) || failed.get()) {
teardown(session)
error("$label playback never became ready")
}
return session
}
private fun <T> onPlayerThread(session: Session, read: (ExoPlayer) -> T): T {
val value = AtomicReference<T>()
val done = CountDownLatch(1)
session.handler.post {
session.player.get()?.let { value.set(read(it)) }
done.countDown()
}
assertTrue("player thread stalled", done.await(5, TimeUnit.SECONDS))
return checkNotNull(value.get())
}
private fun teardown(session: Session) {
val done = CountDownLatch(1)
session.handler.post {
session.player.get()?.release()
done.countDown()
}
done.await(10, TimeUnit.SECONDS)
session.thread.quitSafely()
session.thread.join(5_000)
session.fixture.delete()
}
private fun copyFixture(context: android.content.Context): File {
val output = File.createTempFile("mkv-late-tracks-", ".mkv", context.cacheDir)
InstrumentationRegistry.getInstrumentation().context.assets
.open("ffmpeg/matroska_tracks_at_end.mkv")
.use { input -> output.outputStream().use { input.copyTo(it) } }
return output
}
}
+2 -4
View File
@@ -19,22 +19,20 @@ else()
target_link_libraries(dovi_bridge log)
endif()
foreach(ffmpeg_library avcodec avformat avutil swresample)
foreach(ffmpeg_library avcodec avutil swresample)
add_library(${ffmpeg_library} SHARED IMPORTED)
set_target_properties(${ffmpeg_library} PROPERTIES
IMPORTED_LOCATION "${MPV_FFMPEG_ROOT}/native/${ANDROID_ABI}/lib${ffmpeg_library}.so")
endforeach()
add_library(ffmpegJNI SHARED
media3_ffmpeg_decoder/ffmpeg_jni.cc
media3_ffmpeg_demuxer/ffmpeg_demuxer_jni.cc)
media3_ffmpeg_decoder/ffmpeg_jni.cc)
target_compile_features(ffmpegJNI PRIVATE cxx_std_17)
target_include_directories(ffmpegJNI PRIVATE
"${MPV_FFMPEG_ROOT}/include"
"${CMAKE_CURRENT_SOURCE_DIR}/media3_ffmpeg_decoder")
target_link_libraries(ffmpegJNI
avcodec
avformat
avutil
swresample
android
File diff suppressed because it is too large Load Diff
@@ -32,6 +32,7 @@ import com.edde746.plezy.exoplayer.ExoPlayerPlugin
import com.edde746.plezy.mpv.MpvAudioPlayerPlugin
import com.edde746.plezy.mpv.MpvPlayerPlugin
import com.edde746.plezy.shared.DeviceQuirks
import com.edde746.plezy.shared.MediaCodecQuery
import com.edde746.plezy.shared.ThemeHelper
import com.edde746.plezy.watchnext.WatchNextPlugin
import io.flutter.embedding.android.FlutterActivity
@@ -735,6 +736,7 @@ class MainActivity : FlutterActivity() {
"getTvDetection" -> result.success(getAndroidTvDetection())
"getDeviceName" -> result.success(getDeviceName())
"getPerformanceSignals" -> result.success(getPerformanceSignals())
"getVideoDecodeCapabilities" -> result.success(MediaCodecQuery.hardwareVideoDecodeSupport())
"getBackgroundWorkSignals" -> result.success(
BackgroundWorkClassifier.toMap(BackgroundWorkDiagnostics.read(this))
)
@@ -76,58 +76,93 @@ internal fun isPcmEncoding(encoding: Int): Boolean = when (encoding) {
else -> false
}
/**
* mpv `audio-spdif` codec names and the exact platform encoding a route must
* advertise to carry that bitstream.
*
* Only the codecs mpv's audiotrack AO can physically carry are listed. That AO opens every
* spdif format as a stereo `ENCODING_IEC61937` track and clamps the track rate to
* `getNativeOutputSampleRate` (the 48kHz mixer rate), so E-AC3 (a 192kHz burst), TrueHD
* (192kHz/8ch MAT) and DTS-HD MA (192kHz/8ch) can never leave that AO intact. Naming one
* anyway force-passes the codec into a track that cannot carry it (#1991); those streams
* decode to PCM instead, like every other unnamed codec.
*/
private val MPV_SPDIF_CODECS: List<Pair<String, Int>> = listOf(
"ac3" to C.ENCODING_AC3,
"dts" to C.ENCODING_DTS
/** The IEC 61937 track shape a codec's spdif burst rides. */
internal enum class MpvIecShape { STEREO_48K, STEREO_192K, SURROUND_192K }
private class MpvSpdifCodec(
val name: String,
val encoding: Int,
val shape: MpvIecShape,
/** Whether the fork's audiotrack AO opens this codec as a raw bitstream track first. */
val raw: Boolean = false
)
/**
* Builds an `audio-spdif` value naming only the codecs [supportsEncoding] advertises.
* mpv `audio-spdif` codec names, the platform encoding a route must advertise to carry that
* bitstream, and the track shape the fork's `ao_audiotrack` opens for it.
*
* Since the raw-passthrough patch (0102), the AO feeds AC3, E-AC3 and the DTS core to a raw
* `ENCODING_AC3`/`E_AC3`/`DTS` track at 48kHz/stereo — the transport ExoPlayer and Kodi use —
* and only falls back to the IEC 61937 carrier when the route rejects the raw encoding
* outright. Raw tracks keep the platform's Dolby/DTS transcoder in the path; a pre-packed IEC
* track bypasses it and drains into silence on routes whose sink cannot decode the codec
* itself (#2177's Shield in front of a Dolby-Digital-only Sonos). TrueHD (MAT) and DTS-HD MA
* stay on the 8-channel 192kHz IEC carrier the multichannel patch (0101) added: their raw
* forms are not recoverable from the burst stream.
*
* `dts-hd` supersedes plain `dts`: that literal is what selects the lossless `spdif_dts_hd`
* decoder, and it enables spdif for the whole `dts` codec while doing so, with the core burst
* still chosen per file for tracks that are not HD (`ad_spdif.c:240-249`, `:400-418`). HRA rides
* a 2ch/192kHz burst under the same name, so gating it on the 8-channel carrier is the
* conservative choice.
*/
private val MPV_SPDIF_CODECS: List<MpvSpdifCodec> = listOf(
MpvSpdifCodec("ac3", C.ENCODING_AC3, MpvIecShape.STEREO_48K, raw = true),
MpvSpdifCodec("eac3", C.ENCODING_E_AC3, MpvIecShape.STEREO_192K, raw = true),
MpvSpdifCodec("truehd", C.ENCODING_DOLBY_TRUEHD, MpvIecShape.SURROUND_192K),
MpvSpdifCodec("dts", C.ENCODING_DTS, MpvIecShape.STEREO_48K, raw = true),
MpvSpdifCodec("dts-hd", C.ENCODING_DTS_HD, MpvIecShape.SURROUND_192K)
)
/**
* Builds an `audio-spdif` value naming only the codecs the route can carry: [supportsEncoding]
* advertises the codec's encoding *and* a transport the AO's ladder can open exists —
* [supportsRawTrack] for the raw bitstream track it tries first, or [supportsShape] for the
* IEC 61937 burst it falls back to. Plain `dts` is dropped whenever `dts-hd` qualifies, which
* already covers the core burst.
*
* mpv force-passes through every codec named here and has no decode fallback, so an
* unsupported name leaves the file rendering video against a dead audio output (#1703).
*
* The gate is the exact encoding rather than media3's passthrough probe on purpose. That
* probe answers by downgrading (DTS-HD to the DTS core, E-AC3 JOC to E-AC3) and rejects
* channel counts above the route's PCM maximum, neither of which describes what an IEC
* 61937 track carries.
* channel counts above the route's PCM maximum, neither of which describes what a
* passthrough track carries.
*/
internal fun mpvSpdifCodecs(supportsEncoding: (Int) -> Boolean): String = MPV_SPDIF_CODECS
.filter { (_, encoding) -> supportsEncoding(encoding) }
.joinToString(",") { (codec, _) -> codec }
internal fun mpvSpdifCodecs(
supportsEncoding: (Int) -> Boolean,
supportsShape: (MpvIecShape) -> Boolean,
supportsRawTrack: (Int) -> Boolean = { false }
): String {
val carried = MPV_SPDIF_CODECS.filter {
supportsEncoding(it.encoding) &&
((it.raw && supportsRawTrack(it.encoding)) || supportsShape(it.shape))
}
val dtsHd = carried.any { it.name == "dts-hd" }
return (if (dtsHd) carried.filterNot { it.name == "dts" } else carried).joinToString(",") { it.name }
}
/**
* [mpvSpdifCodecs] resolved against the audio route [context] is currently routed to.
*
* Two conditions, both required:
* - The route must accept the exact track shape mpv opens for spdif output — stereo
* IEC 61937 at the 48kHz mixer rate ([supportsMpvIecShape]). Advertising the raw AC3/DTS
* encodings only says the receiver decodes them, not that the HAL takes an IEC 61937
* AudioTrack: #1991's Shield bitstreams AC3 through ExoPlayer's raw path while every mpv
* spdif attempt strands playback on a dead audio output, leaving nothing but AAC playable.
* Two conditions per codec, both required:
* - The route must accept a track shape the AO's ladder actually opens. For AC3, E-AC3 and the
* DTS core that is the raw bitstream track probed by [supportsMpvRawTrack], with the IEC
* stereo shapes ([supportsMpvIecShape], [supportsMpvHighRateIecShape]) as the AO's fallback
* transport; TrueHD and DTS-HD MA need the 192kHz/7.1 carrier ([supportsIecCarrier]).
* Advertising the raw encoding only says the receiver decodes it, not that the HAL takes the
* track: #1991's Shield strands playback on every mpv IEC attempt while bitstreaming AC3 raw.
* The probes are independent, so none of them may veto the whole list: a route that takes the
* 192kHz carrier but no raw track still bitstreams TrueHD and DTS-HD MA.
* - The receiver must decode the codec itself — the raw encoding on the current
* [AudioCapabilities] — because IEC 61937 is transport, not transcoding.
* [AudioCapabilities] — because passthrough is transport, not transcoding. (On a raw track
* the platform may additionally transcode, which is exactly why the AO prefers it.)
*/
// Deprecated only in favour of an overload that also takes spatializer channel masks, which
// do not affect bitstream routing. Same probe ExoPlayerCore's TrueHD decision uses.
@Suppress("DEPRECATION")
@OptIn(UnstableApi::class)
internal fun supportedMpvSpdifCodecs(context: Context): String {
if (!supportsMpvIecShape(context)) {
Log.i(TAG, "Route takes no stereo IEC 61937 track; mpv will decode instead of bitstreaming")
return ""
}
val audioAttributes = AudioAttributes.Builder()
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.setUsage(C.USAGE_MEDIA)
@@ -138,12 +173,32 @@ internal fun supportedMpvSpdifCodecs(context: Context): String {
Log.w(TAG, "Audio route capabilities unavailable; mpv will decode instead of bitstreaming", error)
return ""
}
return mpvSpdifCodecs(capabilities::supportsEncoding)
// Every probe costs real route calls and may be shared by more than one codec, so probe once.
val shapeProbed = HashMap<MpvIecShape, Boolean>(3)
val rawProbed = HashMap<Int, Boolean>(3)
val codecs = mpvSpdifCodecs(
capabilities::supportsEncoding,
{ shape -> shapeProbed.getOrPut(shape) { routeTakesIecShape(context, shape) } },
{ encoding -> rawProbed.getOrPut(encoding) { supportsMpvRawTrack(context, encoding) } }
)
if (codecs.isEmpty()) {
Log.i(TAG, "Route takes no passthrough track mpv can fill; mpv will decode instead of bitstreaming")
} else {
Log.i(TAG, "mpv will bitstream: $codecs")
}
return codecs
}
/** The track shape mpv's audiotrack AO opens for spdif output: stereo IEC 61937 at the mixer rate. */
private fun routeTakesIecShape(context: Context, shape: MpvIecShape): Boolean = when (shape) {
MpvIecShape.STEREO_48K -> supportsMpvIecShape(context)
MpvIecShape.STEREO_192K -> supportsMpvHighRateIecShape(context)
MpvIecShape.SURROUND_192K -> supportsIecCarrier(context)
}
/** The track shape mpv's audiotrack AO opens for an AC3 or DTS-core burst: stereo at the mixer rate. */
private const val MPV_IEC_SAMPLE_RATE = 48_000
private const val MPV_IEC_CHANNEL_COUNT = 2
private const val MPV_IEC_HIGH_SAMPLE_RATE = 192_000
internal fun supportsMpvIecShape(context: Context): Boolean = iecRouteSupported(
sdkInt = Build.VERSION.SDK_INT,
@@ -161,6 +216,47 @@ internal fun supportsMpvIecShape(context: Context): Boolean = iecRouteSupported(
hdmiRouteAdvertised = { hdmiAdvertisesIecRoute(context, MPV_IEC_SAMPLE_RATE, MPV_IEC_CHANNEL_COUNT) }
)
/**
* Whether the route takes a raw bitstream `AudioTrack` for [encoding] at the shape the fork's
* `ao_audiotrack` opens first: the codec frame rate with a stereo mask (Kodi's raw shape; the
* HAL reads the real channel layout from the bitstream). Same probe tiering as the IEC shapes,
* except below API 29, where no runtime oracle exists for raw tracks and media3 gates its raw
* path on the advertised encoding alone — which [supportedMpvSpdifCodecs] already requires via
* [AudioCapabilities]. #1991's API 28 Shield bitstreams AC3 exactly this way.
*/
internal fun supportsMpvRawTrack(context: Context, encoding: Int): Boolean = iecRouteSupported(
sdkInt = Build.VERSION.SDK_INT,
canSizeBuffer = { canSizeDirectBuffer(MPV_IEC_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO, encoding) },
// The SDK_INT guards repeat iecRouteSupported's tiering only because lint's NewApi
// check cannot see through the injected lambdas.
bitstreamSupported = {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
iecBitstreamSupported(directProbeFormat(encoding, MPV_IEC_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO))
},
directPlaybackSupported = {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
iecDirectPlaybackSupported(directProbeFormat(encoding, MPV_IEC_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO))
},
hdmiRouteAdvertised = { true }
)
/** E-AC3's geometry: the stereo shape at the 192kHz burst rate, same route tiering as the others. */
internal fun supportsMpvHighRateIecShape(context: Context): Boolean = iecRouteSupported(
sdkInt = Build.VERSION.SDK_INT,
canSizeBuffer = { canSizeIecBuffer(MPV_IEC_HIGH_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO) },
// The SDK_INT guards repeat iecRouteSupported's tiering only because lint's NewApi
// check cannot see through the injected lambdas.
bitstreamSupported = {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
iecBitstreamSupported(iecProbeFormat(MPV_IEC_HIGH_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO))
},
directPlaybackSupported = {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
iecDirectPlaybackSupported(iecProbeFormat(MPV_IEC_HIGH_SAMPLE_RATE, AudioFormat.CHANNEL_OUT_STEREO))
},
hdmiRouteAdvertised = { hdmiAdvertisesIecRoute(context, MPV_IEC_HIGH_SAMPLE_RATE, MPV_IEC_CHANNEL_COUNT) }
)
/**
* Whether this route can carry a packed bitstream inside IEC 61937 at 192kHz/7.1 — TrueHD as MAT
* (#1804) and DTS-HD MA as DTS type IV (#1988) both ride this exact tuple.
@@ -226,8 +322,11 @@ internal fun iecRouteSupported(
else -> hdmiRouteAdvertised()
}
private fun canSizeIecBuffer(sampleRate: Int, channelMask: Int): Boolean = try {
AudioTrack.getMinBufferSize(sampleRate, channelMask, AudioFormat.ENCODING_IEC61937) > 0
private fun canSizeIecBuffer(sampleRate: Int, channelMask: Int): Boolean =
canSizeDirectBuffer(sampleRate, channelMask, AudioFormat.ENCODING_IEC61937)
private fun canSizeDirectBuffer(sampleRate: Int, channelMask: Int, encoding: Int): Boolean = try {
AudioTrack.getMinBufferSize(sampleRate, channelMask, encoding) > 0
} catch (error: Exception) {
false
}
@@ -272,8 +371,11 @@ private fun hdmiAdvertisesIecRoute(context: Context, sampleRate: Int, channelCou
}
/** The exact tuple an IEC output's `AudioTrack` is built with; see [PlezyRenderersFactory]. */
private fun iecProbeFormat(sampleRate: Int, channelMask: Int): AudioFormat = AudioFormat.Builder()
.setEncoding(AudioFormat.ENCODING_IEC61937)
private fun iecProbeFormat(sampleRate: Int, channelMask: Int): AudioFormat =
directProbeFormat(AudioFormat.ENCODING_IEC61937, sampleRate, channelMask)
private fun directProbeFormat(encoding: Int, sampleRate: Int, channelMask: Int): AudioFormat = AudioFormat.Builder()
.setEncoding(encoding)
.setChannelMask(channelMask)
.setSampleRate(sampleRate)
.build()
@@ -0,0 +1,226 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.media3.common.C
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.SeekPoint
import androidx.media3.extractor.TrackAwareSeekMap
import androidx.media3.extractor.TrackOutput
import java.util.concurrent.CopyOnWriteArrayList
/**
* Extractor wrapper that repairs seeking for MKV files media3 reports as unseekable.
*
* Two repairs, in priority order:
* - [SeekMap.Unseekable] with a known duration (no Cues element at all): replaced with a
* proportional byte-position estimate, resynced to the nearest Cluster boundary after seeking.
* - A [TrackAwareSeekMap] whose [SeekMap.isSeekable] is false (media3 1.11.0 builds the Matroska
* seek map while parsing Cues, which for files whose Tracks element follows the Clusters is
* before any track is known — the primary seek track stays unset even though the Cues parsed):
* wrapped so seeks resolve through the per-track cue lookups using the track IDs observed on
* this output.
*/
@androidx.media3.common.util.UnstableApi
class CuelessSeekExtractorWrapper(
private val delegate: Extractor
) : Extractor {
companion object {
private const val TAG = "CuelessSeek"
// MKV Cluster element ID: 0x1F43B675 (4-byte EBML Class-D ID)
private val CLUSTER_ID = byteArrayOf(0x1F, 0x43, 0xB6.toByte(), 0x75)
private const val SCAN_BUFFER_SIZE = 8192
// Max bytes to scan for a Cluster boundary before giving up
private const val MAX_SCAN_BYTES = 1024 * 1024 // 1 MB
}
private var inputLength: Long = C.LENGTH_UNSET.toLong()
private var needsClusterResync = false
private var isApproximateSeeking = false
private var pendingSeekTimeUs: Long = C.TIME_UNSET
/** Track IDs and types observed on the output, consulted by [TrackCueSeekMap]. */
private val registeredTracks = CopyOnWriteArrayList<Pair<Int, Int>>()
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
override fun init(output: ExtractorOutput) {
delegate.init(SeekInterceptingOutput(output))
}
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int {
if (inputLength == C.LENGTH_UNSET.toLong()) {
inputLength = input.length
}
if (needsClusterResync) {
needsClusterResync = false
return scanForCluster(input, seekPosition)
}
return delegate.read(input, seekPosition)
}
override fun seek(position: Long, timeUs: Long) {
if (isApproximateSeeking && position > 0) {
needsClusterResync = true
pendingSeekTimeUs = timeUs
}
delegate.seek(position, timeUs)
}
override fun release() = delegate.release()
/**
* Scan forward from the current input position to find the next MKV Cluster
* element ID (0x1F43B675). Returns [Extractor.RESULT_SEEK] with the Cluster's
* byte position so ExoPlayer repositions the DataSource there.
*/
private fun scanForCluster(input: ExtractorInput, seekPosition: PositionHolder): Int {
val buffer = ByteArray(SCAN_BUFFER_SIZE)
var totalScanned = 0L
// Carry over last 3 bytes across buffer boundaries to detect split IDs
var carry = ByteArray(0)
while (totalScanned < MAX_SCAN_BYTES) {
val toRead = minOf(SCAN_BUFFER_SIZE, (MAX_SCAN_BYTES - totalScanned).toInt())
val bytesRead: Int
try {
bytesRead = input.read(buffer, 0, toRead)
} catch (_: Exception) {
break
}
if (bytesRead == C.RESULT_END_OF_INPUT) break
// Combine carry + new data for scanning
val scanData = if (carry.isNotEmpty()) carry + buffer.copyOf(bytesRead) else buffer.copyOf(bytesRead)
for (i in 0..scanData.size - 4) {
if (scanData[i] == CLUSTER_ID[0] &&
scanData[i + 1] == CLUSTER_ID[1] &&
scanData[i + 2] == CLUSTER_ID[2] &&
scanData[i + 3] == CLUSTER_ID[3]
) {
// Compute the absolute byte position of this Cluster
val clusterPosition = input.position - bytesRead - carry.size + i
Log.d(TAG, "Found Cluster at byte $clusterPosition (scanned ${totalScanned + i} bytes)")
seekPosition.position = clusterPosition
delegate.seek(clusterPosition, pendingSeekTimeUs)
pendingSeekTimeUs = C.TIME_UNSET
return Extractor.RESULT_SEEK
}
}
// Keep last 3 bytes as carry for next iteration
carry = if (scanData.size >= 3) scanData.copyOfRange(scanData.size - 3, scanData.size) else scanData.copyOf()
totalScanned += bytesRead
}
// Failed to find a Cluster — fall back to position 0
Log.w(TAG, "No Cluster found after scanning $totalScanned bytes, resetting to start")
seekPosition.position = 0
delegate.seek(0, 0)
return Extractor.RESULT_SEEK
}
/**
* ExtractorOutput wrapper that intercepts [seekMap] calls to replace
* [SeekMap.Unseekable] with an approximate proportional SeekMap.
*/
private inner class SeekInterceptingOutput(
private val delegate: ExtractorOutput
) : ExtractorOutput {
override fun track(id: Int, type: Int): TrackOutput {
if (registeredTracks.none { it.first == id }) {
registeredTracks.add(id to type)
}
return delegate.track(id, type)
}
override fun endTracks() = delegate.endTracks()
override fun seekMap(seekMap: SeekMap) {
if (seekMap is SeekMap.Unseekable) {
val durationUs = seekMap.durationUs
if (durationUs != C.TIME_UNSET && durationUs > 0) {
Log.i(TAG, "Replacing Unseekable with approximate SeekMap (duration=${durationUs / 1_000_000}s)")
isApproximateSeeking = true
delegate.seekMap(ApproximateSeekMap(durationUs))
return
}
} else if (!seekMap.isSeekable && seekMap is TrackAwareSeekMap) {
Log.i(TAG, "Wrapping unseekable TrackAwareSeekMap with per-track cue seeking")
isApproximateSeeking = false
delegate.seekMap(TrackCueSeekMap(seekMap))
return
}
// File has real Cues or unknown duration — pass through
isApproximateSeeking = false
delegate.seekMap(seekMap)
}
}
/**
* Approximate SeekMap that estimates byte positions proportionally.
* Used when the MKV has no Cues but has a known duration.
*/
private inner class ApproximateSeekMap(
private val durationUs: Long
) : SeekMap {
override fun isSeekable(): Boolean = true
override fun getDurationUs(): Long = durationUs
override fun getSeekPoints(timeUs: Long): SeekMap.SeekPoints {
val length = inputLength
if (length == C.LENGTH_UNSET.toLong() || durationUs <= 0) {
return SeekMap.SeekPoints(SeekPoint(0, 0))
}
val clampedTimeUs = timeUs.coerceIn(0, durationUs)
val position = (clampedTimeUs.toDouble() / durationUs * length).toLong().coerceIn(0, length)
return SeekMap.SeekPoints(SeekPoint(clampedTimeUs, position))
}
}
/**
* Routes seeks through [TrackAwareSeekMap]'s per-track cue lookups when the delegate reports
* unseekable overall. The per-track queries read the live cue data (populated once the Cues
* element parsed), so they resolve correctly even when the map was constructed before the
* Tracks element — the media3 1.11.0 tracks-after-clusters case (androidx/media #3377).
*/
private inner class TrackCueSeekMap(
private val delegate: TrackAwareSeekMap
) : SeekMap {
override fun isSeekable(): Boolean = delegate.isSeekable || seekableTrackId() != null
override fun getDurationUs(): Long = delegate.durationUs
override fun getSeekPoints(timeUs: Long): SeekMap.SeekPoints {
if (delegate.isSeekable) return delegate.getSeekPoints(timeUs)
val trackId = seekableTrackId() ?: return delegate.getSeekPoints(timeUs)
return delegate.getSeekPoints(timeUs, trackId)
}
/** Mirrors media3's primary-track priority: video first, then audio, then anything with cues. */
private fun seekableTrackId(): Int? {
var audio: Int? = null
var fallback: Int? = null
for ((id, type) in registeredTracks) {
if (!delegate.isSeekable(id)) continue
when (type) {
C.TRACK_TYPE_VIDEO -> return id
C.TRACK_TYPE_AUDIO -> if (audio == null) audio = id
else -> if (fallback == null) fallback = id
}
}
return audio ?: fallback
}
}
}
@@ -124,6 +124,17 @@ object DoviBridge {
.also { Log.i(TAG, "Device advertises DV Profile 8 (DvheSt): $it") }
}
/**
* Whether this device can natively render single-layer Dolby Vision
* Profile 5 (IPT-PQ-c2): it needs both a decoder advertising DvheStn and a
* Dolby Vision display pipeline. P5 has no compatible base layer, so a
* device that fails either check decodes it as plain HEVC with garbage
* colors; callers route those sessions to software decode + gpu-next,
* where libplacebo applies the RPU reshaping instead.
*/
fun canPlayDolbyVisionP5(context: Context): Boolean = deviceAdvertisesDvProfile(MediaCodecInfo.CodecProfileLevel.DolbyVisionProfileDvheStn) &&
displaySupportsDolbyVision(context)
fun displaySupportsDolbyVision(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
Log.i(TAG, "Display Dolby Vision support: false (HDR capabilities require API 24, device API=${Build.VERSION.SDK_INT})")
@@ -145,6 +156,17 @@ object DoviBridge {
return supported
}
/** Whether the active display advertises any HDR output type. */
fun displaySupportsHdr(context: Context): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false
val display = getCurrentDisplay(context) ?: return false
val hdrTypes = runCatching { getDisplayHdrTypes(display) }.getOrElse { error ->
Log.w(TAG, "Display HDR support: failed to query HDR types", error)
return false
}
return hdrTypes.isNotEmpty()
}
fun describeDisplayHdrCapabilities(context: Context): String {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return "HDR capabilities unavailable (API=${Build.VERSION.SDK_INT})"
val display = getCurrentDisplay(context) ?: return "HDR capabilities unavailable (no active display)"
@@ -64,7 +64,6 @@ import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.extractor.mp4.FragmentedMp4Extractor
import androidx.media3.extractor.mp4.Mp4Extractor
import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput
import androidx.media3.extractor.ts.TsExtractor
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.CaptionStyleCompat
@@ -72,7 +71,6 @@ import androidx.media3.ui.SubtitleView
import com.edde746.plezy.AndroidRuntimeDiagnostics
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.parser.AssSubtitleParserFactory
import com.edde746.plezy.libass.media.text.AssSubtitleExtractorOutput
import com.edde746.plezy.libass.media.widget.AssSubtitleSurfaceView
import com.edde746.plezy.shared.AudioFocusManager
import com.edde746.plezy.shared.DeviceQuirks
@@ -563,18 +561,6 @@ class ExoPlayerCore(private val activity: Activity) :
@Volatile private var activeDoviMp4Wrapper: DoviExtractorWrapper? = null
// The FFmpeg extractor from the current session; getStats reads its
// measured audio bitrate for the playing item.
@Volatile private var activeFfmpegExtractor: FfmpegExtractor? = null
// Container-demuxer placement; read per media item when extractors are built.
@Volatile private var demuxerPreference: FfmpegDemuxerPolicy.Preference = FfmpegDemuxerPolicy.Preference.FFMPEG
fun setDemuxerMode(mode: String) {
demuxerPreference = FfmpegDemuxerPolicy.fromWire(mode)
emitLog("info", "init", "demuxer mode: ${demuxerPreference.wireName}")
}
private fun getConfiguredDvMode(): DvConversionMode {
val override = debugDvModeOverride
if (override != null) return override
@@ -585,17 +571,12 @@ class ExoPlayerCore(private val activity: Activity) :
}
fun initialize(
bufferSizeBytes: Int? = null,
bufferSizeAuto: Boolean = false,
tunnelingEnabled: Boolean = true,
audioPassthroughEnabled: Boolean = false,
// Read-ahead depth, as the wire name Dart sends. Kept a String because `LoadControlPolicy`
// is internal and this function is not; unrecognised names resolve to Auto, which is also
// the default (#1816).
bufferTier: String = "auto",
// Whether FFmpeg demuxes ahead of media3's extractors. Wire values from
// [FfmpegDemuxerPolicy.Preference]; unrecognised resolves to FFmpeg.
demuxerMode: String = "ffmpeg"
bufferTier: String = "auto"
): Boolean {
if (isInitialized) {
Log.d(TAG, "Already initialized")
@@ -604,7 +585,6 @@ class ExoPlayerCore(private val activity: Activity) :
tunnelingUserEnabled = tunnelingEnabled
this.audioPassthroughEnabled = audioPassthroughEnabled
demuxerPreference = FfmpegDemuxerPolicy.fromWire(demuxerMode)
this.dvMode = getConfiguredDvMode()
DoviBridge.logSupportSummary(activity)
Log.i(
@@ -750,11 +730,6 @@ class ExoPlayerCore(private val activity: Activity) :
// High-bitrate Plex DVR MPEG-TS recordings can have sparse PCR packets; the default
// 600-packet window may leave duration unknown and seeking disabled.
.setTsExtractorTimestampSearchBytes(TS_TIMESTAMP_SEARCH_PACKETS * TsExtractor.TS_PACKET_SIZE)
// Raw subtitle samples from MatroskaExtractor: the external text
// pipeline below feeds libass dialogue first and then re-adds cue
// transcoding — with the internal wrapper enabled the dialogue bytes
// would already be consumed before our wrapper could see them.
.setMatroskaExtractorFlags(MatroskaExtractor.FLAG_EMIT_RAW_SUBTITLE_DATA)
// Inline buildWithAssSupport to retain AssHandler reference for font scale control.
val handler = AssHandler()
@@ -764,61 +739,39 @@ class ExoPlayerCore(private val activity: Activity) :
// composition object and blanks palette-only fade updates (#1953).
val subtitleParserFactory = PgsSubtitleParserFactory(AssSubtitleParserFactory(handler))
// Wrap extractors: FFmpeg demuxes the containers media3 is weak at
// (primary, before the list) and anything media3 cannot sniff (catch-all,
// behind it); give MatroskaExtractor the same libass/transcoding text
// pipeline so the media3 fallback (ffmpeg unavailable, media3-only
// preference) keeps ASS rendering; wrap MKV/MP4 extractors with the DV
// converter when enabled.
// Reads this.dvMode and this.demuxerPreference each time (not captured) so
// DV7→8.1 retry and demuxer-mode changes can reload without
// reinitializing the player.
// Wrap extractors: replace MatroskaExtractor with the ASS+zlib+LATM
// variant, wrap MP4 extractors with the DV converter when enabled.
// Reads this.dvMode each time (not captured) so DV7→8.1 retry can
// change mode and reload without reinitializing the player.
val wrappedExtractorsFactory = androidx.media3.extractor.ExtractorsFactory {
val currentDvMode = this.dvMode
val doviEnabled = currentDvMode != DvConversionMode.DISABLED
val currentDemuxerPreference = this.demuxerPreference
Log.i(TAG, "[init] extractors: demuxer=${currentDemuxerPreference.wireName}")
// media3 invokes the factory once per player session and reuses the
// extractor instances for every item, so the sniff reads the live
// preference instead of a captured one.
val liveDemuxerPreference = { this@ExoPlayerCore.demuxerPreference }
val ffmpegFirst =
listOfNotNull(
FfmpegExtractor.create(
liveDemuxerPreference,
currentDvMode,
subtitleParserFactory,
handler
)?.also { activeFfmpegExtractor = it }
)
(
ffmpegFirst + extractorsFactory.createExtractors().map { extractor ->
when {
extractor is MatroskaExtractor -> {
val withTextPipeline = OutputWrappingExtractor(extractor) { out ->
AssSubtitleExtractorOutput(SubtitleTranscodingExtractorOutput(out, subtitleParserFactory), handler)
}
if (doviEnabled) {
DoviExtractorWrapper(withTextPipeline, currentDvMode) { level, prefix, message ->
emitLog(level, prefix, message)
}.also {
activeDoviMkvWrapper = it
}
} else {
withTextPipeline
}
}
doviEnabled && (extractor is Mp4Extractor || extractor is FragmentedMp4Extractor) -> {
DoviExtractorWrapper(extractor, currentDvMode) { level, prefix, message ->
extractorsFactory.createExtractors().map { extractor ->
when {
extractor is MatroskaExtractor -> {
val assExtractor = ZlibMatroskaExtractor(subtitleParserFactory, handler)
val inner = if (doviEnabled) {
DoviExtractorWrapper(assExtractor, currentDvMode) { level, prefix, message ->
emitLog(level, prefix, message)
}.also {
activeDoviMp4Wrapper = it
activeDoviMkvWrapper = it
}
} else {
assExtractor
}
else -> extractor
// Wrap with approximate seeking for MKV files without Cues
CuelessSeekExtractorWrapper(inner)
}
doviEnabled && (extractor is Mp4Extractor || extractor is FragmentedMp4Extractor) -> {
DoviExtractorWrapper(extractor, currentDvMode) { level, prefix, message ->
emitLog(level, prefix, message)
}.also {
activeDoviMp4Wrapper = it
}
}
else -> extractor
}
).toTypedArray()
}.toTypedArray()
}
val mediaSourceFactory = DefaultMediaSourceFactory(dataSourceFactory!!, wrappedExtractorsFactory)
@@ -832,21 +785,15 @@ class ExoPlayerCore(private val activity: Activity) :
.toTypedArray()
}
// Buffer budget. `bufferSizeBytes` carries the user's explicit Buffer Size choice; on
// Auto it still arrives (Dart derives it for mpv's demuxer, which shares the property)
// but `bufferSizeAuto` says to ignore it here, because mpv's demuxer and ExoPlayer's
// sample allocator have different shapes and different failure modes.
// Buffer budget. Derived natively from device memory (LoadControlPolicy);
// mpv's demuxer sizes itself the same way in MpvPlayerCore.
val activityManager = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memoryInfo)
val availableMB = (memoryInfo.availMem / (1024 * 1024)).toInt()
val largeHeapMB = activityManager.largeMemoryClass
val targetBufferBytes = if (!bufferSizeAuto && bufferSizeBytes != null && bufferSizeBytes > 0) {
bufferSizeBytes
} else {
LoadControlPolicy.autoTargetBufferBytes(largeHeapMB, availableMB)
}
val targetBufferBytes = LoadControlPolicy.autoTargetBufferBytes(largeHeapMB, availableMB)
val resolvedTier = LoadControlPolicy.BufferTier.fromWire(bufferTier)
val bufferDurations = LoadControlPolicy.bufferDurations(resolvedTier, availableMB)
@@ -869,8 +816,8 @@ class ExoPlayerCore(private val activity: Activity) :
emitLog(
"info",
"init",
"Buffer: ${targetBufferBytes / 1024 / 1024}MB limit (${if (bufferSizeAuto) "auto" else "manual"}, " +
"heap=${largeHeapMB}MB, available=${availableMB}MB), " +
"Buffer: ${targetBufferBytes / 1024 / 1024}MB limit " +
"(heap=${largeHeapMB}MB, available=${availableMB}MB), " +
"buffer=${bufferDurations.minBufferMs / 1000}-${bufferDurations.maxBufferMs / 1000}s " +
"(${resolvedTier.name.lowercase()}), " +
"tunneling=$tunnelingUserEnabled, dataSource=$dataSourceLabel"
@@ -3776,7 +3723,6 @@ class ExoPlayerCore(private val activity: Activity) :
dv7RetryAttempted = override != null
activeDoviMkvWrapper = null
activeDoviMp4Wrapper = null
activeFfmpegExtractor = null
val debugMode = override?.name ?: "AUTO"
emitLog("info", "dv-debug", "P7 DV conversion mode set to $debugMode (active=$dvMode)")
reloadCurrentMediaForDvMode()
@@ -3813,7 +3759,6 @@ class ExoPlayerCore(private val activity: Activity) :
lastDvPlaybackInfo = null
activeDoviMkvWrapper = null
activeDoviMp4Wrapper = null
activeFfmpegExtractor = null
stopFrameWatchdog()
cancelDecoderHangCheck()
cancelResumeStallWatchdog()
@@ -4189,7 +4134,13 @@ class ExoPlayerCore(private val activity: Activity) :
}
override fun clearVideoFrameRate() {
frameRateManager?.clearVideoFrameRate()
// HDR content on an HDR display means the decoder's dataspace put the
// display into HDR signaling; defer the rate restore past the HDR exit
// (see FrameRateManager.clearVideoFrameRate).
val transfer = currentVideoFormat?.colorInfo?.colorTransfer
val hdrActive = (transfer == C.COLOR_TRANSFER_ST2084 || transfer == C.COLOR_TRANSFER_HLG) &&
DoviBridge.displaySupportsHdr(activity)
frameRateManager?.clearVideoFrameRate(hdrActive = hdrActive)
}
private fun computeFrameRate(timestamps: LongArray): Float {
@@ -4280,12 +4231,7 @@ class ExoPlayerCore(private val activity: Activity) :
"audioMimeType" to audioFormat?.sampleMimeType,
"audioSampleRate" to audioFormat?.sampleRate,
"audioChannels" to audioFormat?.channelCount,
// Container-declared bitrate when present; otherwise the FFmpeg
// demuxer's measured value (Matroska rarely carries BPS tags — #2063).
"audioBitrate" to (
audioFormat?.bitrate?.takeIf { it > 0 }
?: audioFormat?.id?.let { activeFfmpegExtractor?.measuredAudioBitrateBps(it) }
),
"audioBitrate" to audioFormat?.bitrate?.takeIf { it > 0 },
"audioDecoderName" to audioDecoderInitName,
"audioOutputEncoding" to audioTrackConfig?.encoding,
"audioOutputChannels" to audioTrackConfig?.channelConfig?.let { Integer.bitCount(it) },
@@ -1,8 +1,6 @@
package com.edde746.plezy.exoplayer
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.util.Log
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.mpv.MpvPlayerCore
@@ -40,6 +38,11 @@ class ExoPlayerPlugin :
private val mainHandler get() = channels.mainHandler
private fun runOnMain(block: () -> Unit) = channels.runOnMain(block)
private var playerCore: ExoPlayerCore? = null
// The Dart instanceId that created the current core. A `dispose` carrying a
// different token lost the ownership race to a successor and must not tear
// down that successor's session; it is acknowledged without touching it.
private var coreInstanceId: Long? = null
private var mpvCore: MpvPlayerCore? = null // MPV fallback player
private var usingMpvFallback: Boolean = false
private var fallbackInProgress: Boolean = false
@@ -108,8 +111,6 @@ class ExoPlayerPlugin :
private val observedProperties = LinkedHashMap<String, ObservedProperty>()
private var configuredBufferSizeBytes: Int? = null
private var sessionGeneration = 0
private var mediaGeneration = 0
private var fallbackMediaGeneration: Int? = null
@@ -119,7 +120,8 @@ class ExoPlayerPlugin :
private var mpvForwardGeneration: Int? = null
private var mpvSignalGate: MpvSignalGate? = null
private var mpvCoreNeedsReplacement = false
internal var createMpvCore: (Activity) -> MpvPlayerCore = { MpvPlayerCore(it) }
internal var createMpvCore: (Activity) -> MpvPlayerCore =
{ MpvPlayerCore(it, hardwareDecoding = fallbackHardwareDecoding()) }
internal var initializeMpvCore: (MpvPlayerCore, (Boolean) -> Unit) -> Unit = { core, onInitialized ->
core.initialize(onInitialized)
}
@@ -139,6 +141,12 @@ class ExoPlayerPlugin :
// every codec in audio-spdif with no decode fallback, so the fallback core's value is
// derived from the audio route at the moment mpv actually starts (#1703).
private var audioPassthroughRequested = false
// `dv-conversion-mode` is not an mpv property and never reaches
// pendingMpvProperties: Dart routes it through setDvConversionMode (see
// PlayerAndroid.setProperty), so the fallback core has to be seeded from the
// last configured value or it loses the fork's P7/P5 handling entirely.
private var dvConversionMode = "auto"
private var currentExternalSubtitles: List<Map<String, Any?>>? = null
// FlutterPlugin
@@ -158,6 +166,7 @@ class ExoPlayerPlugin :
val exoCore = playerCore
val fallbackCore = mpvCore
playerCore = null
coreInstanceId = null
mpvCore = null
usingMpvFallback = false
fallbackInProgress = false
@@ -174,6 +183,7 @@ class ExoPlayerPlugin :
currentExternalSubtitles = null
pendingMpvProperties.clear()
audioPassthroughRequested = false
dvConversionMode = "auto"
if (clearActivity) {
activity = null
activityBinding = null
@@ -224,7 +234,7 @@ class ExoPlayerPlugin :
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"initialize" -> handleInitialize(call, result)
"dispose" -> handleDispose(result)
"dispose" -> handleDispose(call, result)
"open" -> handleOpen(call, result)
"play" -> handlePlay(result)
"pause" -> handlePause(result)
@@ -250,15 +260,10 @@ class ExoPlayerPlugin :
)
"getStats" -> handleGetStats(result)
"getPlayerType" -> result.success(if (usingMpvFallback) "mpv" else "exoplayer")
"getHeapSize" -> {
val am = activity?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
result.success(am?.largeMemoryClass ?: 0)
}
"setSubtitleStyle" -> handleSetSubtitleStyle(call, result)
"setBoxFitMode" -> handleSetBoxFitMode(call, result)
"setVideoZoom" -> handleSetVideoZoom(call, result)
"setDvConversionMode" -> handleSetDvConversionMode(call, result)
"setDemuxerMode" -> handleSetDemuxerMode(call, result)
"setAudioNormalization" -> handleSetAudioNormalization(call, result)
"setAudioPassthrough" -> handleSetAudioPassthrough(call, result)
"setAudioDownmix" -> handleSetAudioDownmix(call, result)
@@ -292,21 +297,14 @@ class ExoPlayerPlugin :
return
}
val bufferSizeBytes = call.argument<Int>("bufferSizeBytes")
// Auto sizing is decided natively (LoadControlPolicy). `bufferSizeBytes` still arrives
// on Auto because Dart derives one for mpv's demuxer, which shares the property, and
// the fallback replay below needs it.
val bufferSizeAuto = call.argument<Boolean>("bufferSizeAuto") ?: false
val tunnelingEnabled = call.argument<Boolean>("tunnelingEnabled") ?: false
val dvConversionMode = call.argument<String>("dvConversionMode") ?: "auto"
dvConversionMode = call.argument<String>("dvConversionMode") ?: "auto"
val audioPassthroughEnabled = call.argument<Boolean>("audioPassthroughEnabled") ?: false
val assVideoLatencyFrames = call.argument<Int>("assVideoLatencyFrames") ?: 0
val subtitleRenderScale = call.argument<Double>("subtitleRenderScale")?.toFloat() ?: 1.0f
// ExoPlayer-only: mpv's read-ahead is owned by the mpv.conf editor, so there is no
// fallback replay for this one. Resolved in the core; unrecognised means Auto (#1816).
val bufferTier = call.argument<String>("bufferTier") ?: "auto"
val demuxerMode = call.argument<String>("demuxerMode") ?: "ffmpeg"
configuredBufferSizeBytes = bufferSizeBytes
// Seed the request here rather than waiting for Dart's separate setAudioPassthrough
// call, so a fallback raised before that arrives still derives audio-spdif correctly.
audioPassthroughRequested = audioPassthroughEnabled
@@ -345,13 +343,11 @@ class ExoPlayerPlugin :
this.debugLoggingEnabled = this@ExoPlayerPlugin.debugLoggingEnabled
}
playerCore = core
coreInstanceId = call.argument<Number>("instanceId")?.toLong()
val success = core.initialize(
bufferSizeBytes = bufferSizeBytes,
bufferSizeAuto = bufferSizeAuto,
tunnelingEnabled = tunnelingEnabled,
audioPassthroughEnabled = audioPassthroughEnabled,
bufferTier = bufferTier,
demuxerMode = demuxerMode
bufferTier = bufferTier
)
if (!success) {
if (playerCore === core) playerCore = null
@@ -376,8 +372,15 @@ class ExoPlayerPlugin :
}
}
private fun handleDispose(result: MethodChannel.Result) {
private fun handleDispose(call: MethodCall, result: MethodChannel.Result) {
val token = call.argument<Number>("instanceId")?.toLong()
runOnMain {
val owner = coreInstanceId
if ((playerCore != null || mpvCore != null) && token != null && owner != null && token != owner) {
Log.d(TAG, "Ignoring stale dispose (token=$token, core owner=$owner)")
result.success(null)
return@runOnMain
}
teardownSession(clearActivity = false)
Log.d(TAG, "Disposed")
result.success(null)
@@ -1131,12 +1134,24 @@ class ExoPlayerPlugin :
return
}
if (usingMpvFallback) {
result.success(false)
// The fallback core owns the property now; dropping the write would
// strand it on the mode ExoPlayer started with.
val core = mpvCore
if (core == null) {
result.success(false)
return
}
dvConversionMode = mode
core.setProperty("dv-conversion-mode", mode) { outcome ->
completeMpvPropertyResult(result, outcome, successValue = true)
}
return
}
activity?.runOnUiThread {
val handled = playerCore?.setDebugDvConversionMode(mode) == true
if (handled) {
// Keep the fallback seed on the value ExoPlayer is actually running.
dvConversionMode = mode
result.success(true)
} else {
result.error("INVALID_ARGS", "Invalid DV conversion mode: $mode", null)
@@ -1144,18 +1159,6 @@ class ExoPlayerPlugin :
} ?: result.error("NO_ACTIVITY", "Activity not available", null)
}
private fun handleSetDemuxerMode(call: MethodCall, result: MethodChannel.Result) {
val mode = call.argument<String>("mode")
if (mode == null) {
result.error("INVALID_ARGS", "Missing 'mode'", null)
return
}
activity?.runOnUiThread {
playerCore?.setDemuxerMode(mode)
result.success(null)
} ?: result.error("NO_ACTIVITY", "Activity not available", null)
}
private fun handleSetAudioNormalization(call: MethodCall, result: MethodChannel.Result) {
val enabled = call.argument<Boolean>("enabled")
if (enabled == null) {
@@ -1321,6 +1324,16 @@ class ExoPlayerPlugin :
}
}
/**
* Decode intent for a fallback mpv core, read from the `hwdec` value Dart
* already wrote for this session. It picks the core's initial vo chain
* ([MpvPlayerCore.initialVideoOutput]), so a software-decoding session must
* not inherit the hardware default: only gpu-next applies Dolby Vision RPU
* reshaping, and a session that asked for software decode is usually the
* one that needs it.
*/
private fun fallbackHardwareDecoding(): Boolean = pendingMpvProperties["hwdec"]?.let { it != "no" } ?: true
/**
* Configure a freshly initialized MPV fallback core: replay the properties
* and observers Dart registered against the ExoPlayer session, then resume
@@ -1330,17 +1343,13 @@ class ExoPlayerPlugin :
private fun prepareMpvFallback(core: MpvPlayerCore) {
val pendingProps = pendingMpvProperties.filterKeys { it != "audio-spdif" }.toList()
val observedProps = observedProperties.toList()
val bufferSize = configuredBufferSizeBytes
// vo is owned by MpvPlayerCore's init: the fallback core hardware-decodes
// (hwdec below), so it gets the legacy gpu VO — gpu-next under mediacodec
// fails every frame on Tegra (#2010) and reshapes no DV anyway.
core.setProperty("hwdec", "mediacodec,mediacodec-copy")
// hwdec is not seeded here — Dart's write in pendingMpvProperties is the
// single source of the fallback core's chain.
core.setProperty("ao", "audiotrack")
if (bufferSize != null && bufferSize > 0) {
core.setProperty("demuxer-max-bytes", bufferSize.toString())
}
// Dart routes dv-conversion-mode through setDvConversionMode, so unlike
// hwdec it never reaches pendingMpvProperties.
core.setProperty("dv-conversion-mode", dvConversionMode)
for ((propName, propValue) in pendingProps) {
core.setProperty(propName, propValue) { outcome ->
@@ -1,138 +0,0 @@
package com.edde746.plezy.exoplayer
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
/**
* JNI surface for the libavformat demuxer shim (`cpp/media3_ffmpeg_demuxer`).
*
* One demuxer exists per process, owned by whichever [FfmpegExtractor] is
* currently open — Plezy plays at most one progressive source at a time, and
* every other playback mode (HLS/DASH, music) stays on media3's own sources.
*
* The long[] layouts mirror the constants documented in
* `ffmpeg_demuxer_jni.cc`; keep both sides in sync.
*/
@OptIn(UnstableApi::class)
internal object FfmpegDemuxerJni {
/** Re-entry point for the AVIO callbacks while a native call is in flight. */
interface Input {
/** Current byte position of the underlying [androidx.media3.extractor.ExtractorInput]. */
fun position(): Long
/**
* Reads up to [length] bytes into [buf]. Returns the number of bytes read,
* `0` at end of input, or `-1` after storing the failure in [lastError].
*/
fun read(buf: ByteArray, length: Int): Int
/** Total input length in bytes, or `-1` when unknown. */
fun length(): Long
var lastError: String?
}
// readPacket out[] indices. Deferral targets travel through
// [nativeConsumePendingSeek], not this array.
const val OUT_CODE = 0
const val OUT_STREAM_INDEX = 1
const val OUT_PTS_US = 2
const val OUT_FLAGS = 3
const val OUT_SIZE = 4
const val OUT_POSITION = 5
const val OUT_DURATION_US = 6
const val OUT_LENGTH = 7
// Result codes written to out[OUT_CODE]; negative values are raw AVERRORs.
const val CODE_PACKET = 0
const val CODE_EOF = 1
const val CODE_NEED_SEEK = 2
const val CODE_GROW = 3
const val ERR_JAVA = -102
// streamInfo long[] layout.
const val INFO_TRACK_TYPE = 0
const val INFO_WIDTH = 1
const val INFO_HEIGHT = 2
const val INFO_PAR_NUM = 3
const val INFO_PAR_DEN = 4
const val INFO_FPS_NUM = 5
const val INFO_FPS_DEN = 6
const val INFO_SAMPLE_RATE = 7
const val INFO_CHANNELS = 8
const val INFO_PCM_ENCODING = 9
const val INFO_ROTATION = 10
const val INFO_SELECTION_FLAGS = 11
const val INFO_ROLE_FLAGS = 12
const val INFO_BITRATE = 13
const val INFO_DOVI_PROFILE = 14
const val INFO_DOVI_LEVEL = 15
const val INFO_LATM = 16
const val INFO_LENGTH = 17
val available: Boolean = try {
System.loadLibrary("ffmpegJNI")
true
} catch (_: UnsatisfiedLinkError) {
false
}
/** Probes container bytes; returns the ffmpeg short name ("avi", "asf", …) or null. */
external fun nativeProbeFormat(header: ByteArray): String?
/**
* Opens (and resumes opening) the demuxer against [input]. Returns 0 on
* success, [CODE_NEED_SEEK] when the loader must move the input first, or a
* negative AVERROR. The stream count is read separately via
* [nativeStreamCount]; it must not ride the same namespace as result codes.
*/
external fun nativeOpen(input: Input): Int
external fun nativeClose()
external fun nativeStreamCount(): Int
/** Total duration in microseconds, or -1 when unknown. */
external fun nativeDurationUs(): Long
/** Fills [numbers]/[strings] for [index]; false when the stream is unusable. */
external fun nativeStreamInfo(index: Int, numbers: LongArray, strings: Array<String?>): Boolean
/** Codec initialization data (extradata), or null. */
external fun nativeStreamExtradata(index: Int): ByteArray?
/** Number of attachment streams (embedded fonts and friends) with payloads. */
external fun nativeAttachmentCount(): Int
/**
* Fills [strings] with [filename, mimetype] for attachment [ordinal] and
* returns the payload size in bytes, or -1 when out of range. Lets Kotlin
* enforce font budgets before copying any bytes across the boundary.
*/
external fun nativeAttachmentInfo(ordinal: Int, strings: Array<String?>): Long
/** Attachment payload bytes, or null. */
external fun nativeAttachmentData(ordinal: Int): ByteArray?
/**
* Reads one packet into [buffer], filling [out]. See the shim header
* comment for the result contract.
*/
external fun nativeReadPacket(buffer: ByteArray, out: LongArray): Int
/** Returns the recorded seek target, or [Long.MIN_VALUE] when none. */
external fun nativeConsumePendingSeek(): Long
/** Adopts [position] as the AVIO position; the input must already be there. */
external fun nativeResumeAfterSeek(position: Long): Int
/**
* Records a presentation-time seek (media3 timeline microseconds). The next
* [nativeReadPacket] executes it via avformat_seek_file, deferring through
* the loader as needed; the demuxer picks the byte position itself.
*/
external fun nativeSeekTo(timeUs: Long)
/** Drops cached header blocks; called when the extractor binds a new source. */
external fun nativeResetCache()
}
@@ -1,27 +0,0 @@
package com.edde746.plezy.exoplayer
/**
* Decides whether the FFmpeg demuxer sits ahead of media3's own extractors.
*
* FFmpeg demuxes every progressive container by default. media3's bundled
* extractors were weak exactly where this app kept accumulating container
* patches — AVI (XviD packed-bitstream timestamps, missing VOL csd — issue
* #2052), ASF/WMV (no extractor at all), MPEG-PS/VOB (no extractor), and
* Matroska (zlib-compressed subtitles, LOAS/LATM audio as A_MS/ACM, cueless
* seeking, font attachments) — and libavformat handles all of it natively.
* media3's extractors stay behind FFmpeg in the list, so a container FFmpeg
* fails to sniff still reaches media3's parsers. [Preference.MEDIA3_ONLY] is
* the user-facing escape hatch in case a file misbehaves on the FFmpeg path.
*/
internal object FfmpegDemuxerPolicy {
enum class Preference(val wireName: String) {
FFMPEG("ffmpeg"),
MEDIA3_ONLY("media3")
}
/** Unknown wire values (including the retired "auto") resolve to FFmpeg. */
fun fromWire(value: String?): Preference = Preference.entries.firstOrNull { it.wireName == value } ?: Preference.FFMPEG
/** Whether the FFmpeg extractor placed before media3's list demuxes at all. */
fun enabled(preference: Preference): Boolean = preference == Preference.FFMPEG
}
@@ -1,614 +0,0 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.annotation.OptIn
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.ParserException
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.common.util.UnstableApi
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.SeekPoint
import androidx.media3.extractor.TrackOutput
import androidx.media3.extractor.text.SubtitleParser
import androidx.media3.extractor.text.SubtitleTranscodingExtractorOutput
import com.edde746.plezy.libass.media.AssFonts
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.text.AssSubtitleExtractorOutput
import java.io.IOException
/**
* Demuxes progressive containers with libavformat and feeds the packets to
* media3's renderers, so FFmpeg's container coverage replaces media3's on the
* paths where it is weak (AVI/XviD timestamps and VOL csd — #2052 — ASF/WMV,
* MPEG-PS) and where the app accumulated Matroska patches (zlib-compressed
* subtitles, LOAS/LATM audio, cueless seeking) without giving up any of the
* renderer-side work (passthrough carriers, DV sanitizing, libass rendering
* with embedded fonts, subtitle latency calibration).
*
* Text tracks are emitted in the exact sample shape media3's
* MatroskaExtractor writes (see [FfmpegSubtitleSamples]); the output chain
* composed in [init] then feeds libass dialogue and transcodes cues the same
* way the media3 Matroska path does.
*
* Seeking protocol: see `ffmpeg_demuxer_jni.cc`. The extractor never moves the
* input itself; position divergences abort the native call, and Kotlin either
* returns RESULT_SEEK or resumes in place when the loader already positioned
* the input at the requested byte.
*
* The instance sits before media3's extractors and sniffs everything FFmpeg
* can probe while [FfmpegDemuxerPolicy] enables it; media3's list behind it
* is the fallback for anything FFmpeg cannot open.
*/
@OptIn(UnstableApi::class)
internal class FfmpegExtractor private constructor(
private val preferenceSupplier: () -> FfmpegDemuxerPolicy.Preference,
private val dvMode: DvConversionMode,
private val subtitleParserFactory: SubtitleParser.Factory,
private val assHandler: AssHandler
) : Extractor {
companion object {
private const val TAG = "FfmpegExtractor"
private const val SNIFF_BYTES = 64 * 1024
private const val MAX_RECONCILES = 24
// Loader round trips (RESULT_SEEK) allowed for one item's whole open
// phase; matroska needs ~3, avi with an end-of-file index ~4.
private const val MAX_OPEN_LOADER_ROUND_TRIPS = 96
private const val INITIAL_PACKET_BUFFER_BYTES = 2 * 1024 * 1024
private const val AUDIO_INDEX_INTERVAL_US = 500_000L
/** Null when the native library is unavailable. */
fun create(
preferenceSupplier: () -> FfmpegDemuxerPolicy.Preference,
dvMode: DvConversionMode,
subtitleParserFactory: SubtitleParser.Factory,
assHandler: AssHandler
): FfmpegExtractor? = if (FfmpegDemuxerJni.available) {
FfmpegExtractor(preferenceSupplier, dvMode, subtitleParserFactory, assHandler)
} else {
null
}
}
private lateinit var output: ExtractorOutput
private var currentInput: ExtractorInput? = null
private val inputProxy = object : FfmpegDemuxerJni.Input {
override fun position(): Long = currentInput?.position ?: 0L
override fun read(buf: ByteArray, length: Int): Int {
val input = currentInput ?: return 0
lastError = null
return try {
val read = input.read(buf, 0, length)
if (read == C.RESULT_END_OF_INPUT) 0 else read
} catch (e: IOException) {
lastError = e.message ?: "demuxer input failed"
-1
}
}
override fun length(): Long {
val input = currentInput ?: return -1L
val length = input.length
return if (length == C.LENGTH_UNSET.toLong()) -1L else length
}
override var lastError: String? = null
}
private var opened = false
private var doviOutputWrapper: DoviExtractorOutputWrapper? = null
private var durationUs = C.TIME_UNSET
private var trackOutputs: Array<TrackOutput?> = emptyArray()
private var primaryStreamIndex = -1
private var primaryStreamIsAudio = false
private var packetBytes = ByteArray(INITIAL_PACKET_BUFFER_BYTES)
private val packetParsable = ParsableByteArray()
private val packetOut = LongArray(FfmpegDemuxerJni.OUT_LENGTH)
private val prefixParsable = ParsableByteArray()
// Per-stream text handling picked in prepareTracks; parallel to trackOutputs.
private var subtitleKinds: Array<SubtitleKind> = emptyArray()
private val latmOutputs = ArrayList<LatmTrackOutput>()
// Per-audio-stream measured bitrate, parallel to trackOutputs. Written by
// the loader thread, read by ExoPlayerCore.getStats on another thread.
@Volatile private var bitrateMeters: Array<StreamBitrateMeter?> = emptyArray()
// Seek index for the primary stream: (presentation time, input position) of
// video keyframes or periodic audio packets. The SeekMap reads it on the
// playback thread while the loader thread appends, hence the lock.
private val seekIndexTimes = ArrayList<Long>()
private val seekIndexPositions = ArrayList<Long>()
private val seekIndexLock = Any()
private val sniffScratch = ByteArray(SNIFF_BYTES)
override fun sniff(input: ExtractorInput): Boolean {
// media3 materializes the extractor array once per player session, so the
// policy must be read live at sniff time — a preference captured at
// construction would freeze for the session.
if (!FfmpegDemuxerPolicy.enabled(preferenceSupplier())) return false
val probed = probe(input) ?: return false
Log.i(TAG, "sniff accepted $probed")
return true
}
private fun probe(input: ExtractorInput): String? {
// create() only builds instances when the native library loaded.
input.resetPeekPosition()
return try {
val filled =
input.peekFully(
sniffScratch,
/* offset= */
0,
sniffScratch.size,
/* allowEndOfInput= */
true
)
if (!filled) return null
FfmpegDemuxerJni.nativeProbeFormat(sniffScratch)?.lowercase()
} catch (_: Throwable) {
// A sniffer must never take playback down: media3 escalates anything
// that is not an IOException into a fatal loader error.
null
} finally {
input.resetPeekPosition()
}
}
override fun init(output: ExtractorOutput) {
// media3 reuses extractor instances across sources; drop the previous
// item's header cache so this open cannot replay stale bytes.
FfmpegDemuxerJni.nativeResetCache()
openLoaderRoundTrips = 0
// Same composition as the MP4/MKV paths: DoviConvertingTrackOutput
// inspects each video track's codecs string and passes non-DV through.
val doviWrapped = if (dvMode != DvConversionMode.DISABLED) {
// The wrapper forwards emitLog into each converting track output; the
// playback-info hook stays wired to the MKV/MP4 wrappers, so nothing to
// capture here.
DoviExtractorOutputWrapper(
output,
dvMode,
emitLog = { level, prefix, message ->
Log.println(if (level == "error") Log.ERROR else Log.INFO, TAG, "$prefix $message")
},
onVideoTrackWrapped = { _ -> }
).also { doviOutputWrapper = it }
} else {
doviOutputWrapper = null
output
}
// Text pipeline, extractor-outward: raw media3-shape samples first hit
// AssTrackOutput (libass dialogue feed), then the transcoding wrapper
// parses them into cue samples. media3 does NOT wrap third-party
// extractors' outputs; without this wrapper no text track would ever
// reach the renderer as cues.
this.output = AssSubtitleExtractorOutput(
SubtitleTranscodingExtractorOutput(doviWrapped, subtitleParserFactory),
assHandler
)
}
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int {
currentInput = input
try {
if (!opened && !openStreams(input, seekPosition)) return Extractor.RESULT_SEEK
return readPacket(input, seekPosition)
} catch (e: Throwable) {
Log.w(TAG, "read threw at input=${input.position}", e)
throw e
} finally {
currentInput = null
}
}
/**
* Hands the presentation-time target to the demuxer: the next read runs
* avformat_seek_file, which picks the byte position and resets libavformat's
* parse state (jumping the AVIO position under the demuxer would leave
* demuxer-private state pointing into the old stream). The byte position
* media3 supplies is only its SeekMap guess; the loader converges on the
* demuxer's choice through the normal deferral round trip. The DV
* converter's buffered NAL state resets here as well.
*/
override fun seek(position: Long, timeUs: Long) {
Log.i(TAG, "extractor seek position=$position timeUs=$timeUs opened=$opened")
doviOutputWrapper?.resetTracks()
// Buffered LOAS bytes belong to the pre-seek stream; the StreamMuxConfig
// survives so audio resumes without waiting for the next config.
for (latm in latmOutputs) latm.reset()
if (opened) FfmpegDemuxerJni.nativeSeekTo(timeUs)
}
override fun release() {
if (opened) {
opened = false
FfmpegDemuxerJni.nativeClose()
}
// media3's extractors may demux a later item while this instance keeps
// the previous one's stream layout; a stale measurement must not leak
// into stats.
bitrateMeters = emptyArray()
}
/**
* Measured average bitrate (bps) of the audio stream whose index matches
* [formatId] (this extractor sets `Format.id` to the stream index), or null
* when this instance is not demuxing that stream. Fallback for containers
* that declare no per-track bitrate (#2063).
*/
fun measuredAudioBitrateBps(formatId: String?): Int? {
val index = formatId?.toIntOrNull() ?: return null
return bitrateMeters.getOrNull(index)?.bitrateBps()
}
// The in-call `reconciles` counter resets on every read() invocation, so it
// cannot bound RESULT_SEEK ping-pong across loader restarts. This budget
// spans the whole open of one item (reset in init) and turns a
// non-converging header phase into a clean fallback instead of a hang.
private var openLoaderRoundTrips = 0
/** Returns false when a loader seek was requested via [seekPosition]. */
private fun openStreams(input: ExtractorInput, seekPosition: PositionHolder): Boolean {
var reconciles = 0
while (true) {
when (val code = FfmpegDemuxerJni.nativeOpen(inputProxy)) {
0 -> {
prepareTracks()
opened = true
Log.i(TAG, "ffmpeg demuxer ready: ${trackOutputs.count { it != null }} tracks")
return true
}
FfmpegDemuxerJni.CODE_NEED_SEEK -> {
val target = FfmpegDemuxerJni.nativeConsumePendingSeek()
Log.i(TAG, "open: NEED_SEEK target=$target inputPos=${input.position}")
if (target == Long.MIN_VALUE || target < 0) {
// Sticky AVIO error replayed without a fresh deferral: re-sync at
// the loader's current position and continue (bounded below).
if (++reconciles > MAX_RECONCILES) {
throw malformed("demuxer produced an invalid seek target: $target")
}
if (FfmpegDemuxerJni.nativeResumeAfterSeek(input.position) != 0) {
throw malformed("demuxer resync failed at ${input.position}")
}
continue
}
if (++reconciles > MAX_RECONCILES) throw malformed("too many demuxer repositions")
if (!reconcile(input, seekPosition, target)) {
if (++openLoaderRoundTrips > MAX_OPEN_LOADER_ROUND_TRIPS) {
throw malformed("demuxer open did not converge after $openLoaderRoundTrips loader seeks")
}
return false
}
}
FfmpegDemuxerJni.ERR_JAVA ->
throw IOException(inputProxy.lastError ?: "demuxer input failed")
else -> throw malformed("ffmpeg demuxer open failed: $code")
}
}
}
private enum class SubtitleKind { NONE, SSA, SUBRIP, VTT }
private fun prepareTracks() {
val count = FfmpegDemuxerJni.nativeStreamCount()
durationUs = FfmpegDemuxerJni.nativeDurationUs().takeIf { it >= 0 } ?: C.TIME_UNSET
trackOutputs = arrayOfNulls(count)
subtitleKinds = Array(count) { SubtitleKind.NONE }
bitrateMeters = arrayOfNulls(count)
latmOutputs.clear()
// Fonts go in before any text track exists so AssHandler's store flushes
// them into libass when the first ASS track is created.
deliverFontAttachments()
val numbers = LongArray(FfmpegDemuxerJni.INFO_LENGTH)
val strings = arrayOfNulls<String>(3)
var primaryVideo = -1
var primaryAudio = -1
for (index in 0 until count) {
if (!FfmpegDemuxerJni.nativeStreamInfo(index, numbers, strings)) continue
val mime = strings[0] ?: continue
val trackType = when (numbers[FfmpegDemuxerJni.INFO_TRACK_TYPE]) {
0L -> C.TRACK_TYPE_VIDEO
1L -> C.TRACK_TYPE_AUDIO
2L -> C.TRACK_TYPE_TEXT
else -> continue
}
val builder = Format.Builder()
.setId(index.toString())
.setSampleMimeType(mime)
.setLanguage(strings[1])
.setLabel(strings[2])
.setSelectionFlags(numbers[FfmpegDemuxerJni.INFO_SELECTION_FLAGS].toInt())
.setRoleFlags(numbers[FfmpegDemuxerJni.INFO_ROLE_FLAGS].toInt())
.setAverageBitrate(numbers[FfmpegDemuxerJni.INFO_BITRATE].toInt())
val extradata = FfmpegDemuxerJni.nativeStreamExtradata(index)
if (extradata != null) {
builder.setInitializationData(listOf(extradata))
}
when (trackType) {
C.TRACK_TYPE_VIDEO -> {
// A Dolby Vision config record rides the stream as side data; the
// codecs string is what DoviConvertingTrackOutput keys on, exactly
// as it does for Matroska's CodecPrivate.
val doviProfile = numbers[FfmpegDemuxerJni.INFO_DOVI_PROFILE].toInt()
Log.i(TAG, "video track $index: doviProfile=$doviProfile mime=$mime")
if (doviProfile >= 0) {
val doviLevel = numbers[FfmpegDemuxerJni.INFO_DOVI_LEVEL].toInt().coerceAtLeast(0)
builder.setCodecs("dvh1.%02d.%02d".format(doviProfile, doviLevel))
}
val width = numbers[FfmpegDemuxerJni.INFO_WIDTH].toInt()
val height = numbers[FfmpegDemuxerJni.INFO_HEIGHT].toInt()
builder
.setWidth(width)
.setHeight(height)
.setRotationDegrees(numbers[FfmpegDemuxerJni.INFO_ROTATION].toInt())
val parNum = numbers[FfmpegDemuxerJni.INFO_PAR_NUM]
val parDen = numbers[FfmpegDemuxerJni.INFO_PAR_DEN]
if (parNum > 0 && parDen > 0 && parNum != parDen) {
builder.setPixelWidthHeightRatio(parNum.toFloat() / parDen.toFloat())
}
val fpsNum = numbers[FfmpegDemuxerJni.INFO_FPS_NUM]
val fpsDen = numbers[FfmpegDemuxerJni.INFO_FPS_DEN]
if (fpsNum > 0 && fpsDen > 0) {
builder.setFrameRate(fpsNum.toFloat() / fpsDen.toFloat())
}
if (primaryVideo < 0) {
primaryVideo = index
// libass needs the storage size before the first render; the
// Matroska path published it from the Tracks element the same way.
if (width > 0 && height > 0) assHandler.setVideoSize(width, height)
}
}
C.TRACK_TYPE_AUDIO -> {
builder
.setSampleRate(numbers[FfmpegDemuxerJni.INFO_SAMPLE_RATE].toInt())
.setChannelCount(numbers[FfmpegDemuxerJni.INFO_CHANNELS].toInt())
val pcmEncoding = numbers[FfmpegDemuxerJni.INFO_PCM_ENCODING].toInt()
if (pcmEncoding >= 0) builder.setPcmEncoding(pcmEncoding)
if (primaryAudio < 0) primaryAudio = index
bitrateMeters[index] = StreamBitrateMeter()
}
C.TRACK_TYPE_TEXT -> {
val kind = when (mime) {
MimeTypes.TEXT_SSA -> SubtitleKind.SSA
MimeTypes.APPLICATION_SUBRIP -> SubtitleKind.SUBRIP
MimeTypes.TEXT_VTT -> SubtitleKind.VTT
else -> SubtitleKind.NONE
}
subtitleKinds[index] = kind
if (kind == SubtitleKind.SSA) {
// Embedded ASS reaches libass as per-sample dialogue, never as a
// whole file: AssSubtitleParserFactory keys embedded handling off
// the Matroska container mime, and AssHeaderParser reads the
// header from initializationData[1] — the exact layout media3's
// MatroskaExtractor publishes.
builder.setContainerMimeType(MimeTypes.VIDEO_MATROSKA)
if (extradata != null) {
builder.setInitializationData(
listOf(FfmpegSubtitleSamples.SSA_DIALOGUE_FORMAT, extradata)
)
}
}
}
}
val rawOutput = output.track(index, trackType)
val trackOutput = if (trackType == C.TRACK_TYPE_AUDIO && numbers[FfmpegDemuxerJni.INFO_LATM] == 1L) {
// LOAS/LATM-framed AAC: unwrap to raw access units; the wrapper
// swallows this placeholder Format and emits the real one parsed from
// the StreamMuxConfig.
LatmTrackOutput(rawOutput, index).also { latmOutputs.add(it) }
} else {
rawOutput
}
trackOutput.format(builder.build())
trackOutputs[index] = trackOutput
}
primaryStreamIndex = if (primaryVideo >= 0) primaryVideo else primaryAudio
primaryStreamIsAudio = primaryVideo < 0 && primaryAudio >= 0
synchronized(seekIndexLock) {
seekIndexTimes.clear()
seekIndexPositions.clear()
}
output.endTracks()
output.seekMap(SeekMapImpl())
}
/** Hands embedded font attachments to libass, under the shared budgets. */
private fun deliverFontAttachments() {
val count = FfmpegDemuxerJni.nativeAttachmentCount()
if (count <= 0) return
var acceptedBytes = 0L
val meta = arrayOfNulls<String>(2)
for (ordinal in 0 until count) {
val size = FfmpegDemuxerJni.nativeAttachmentInfo(ordinal, meta)
if (size <= 0) continue
val name = meta[0] ?: continue
val mime = meta[1] ?: continue
if (mime !in AssFonts.fontMimeTypes) continue
val rejectionReason = AssFonts.rejectionReason(size, acceptedBytes)
if (rejectionReason != null) {
Log.w(TAG, "Skipping embedded font: $rejectionReason (bytes=$size, accepted=$acceptedBytes)")
continue
}
val data = FfmpegDemuxerJni.nativeAttachmentData(ordinal) ?: continue
acceptedBytes += size
assHandler.addFont(name, data)
}
}
private fun readPacket(input: ExtractorInput, seekPosition: PositionHolder): Int {
var reconciles = 0
while (true) {
val code = FfmpegDemuxerJni.nativeReadPacket(packetBytes, packetOut)
when (code) {
FfmpegDemuxerJni.CODE_PACKET -> {
val streamIndex = packetOut[FfmpegDemuxerJni.OUT_STREAM_INDEX].toInt()
val size = packetOut[FfmpegDemuxerJni.OUT_SIZE].toInt()
val trackOutput = trackOutputs.getOrNull(streamIndex) ?: continue
val ptsUs = packetOut[FfmpegDemuxerJni.OUT_PTS_US]
val isKeyframe = packetOut[FfmpegDemuxerJni.OUT_FLAGS] and 1L != 0L
val subtitleKind = subtitleKinds.getOrNull(streamIndex) ?: SubtitleKind.NONE
if (subtitleKind != SubtitleKind.NONE) {
// Text samples carry their duration inside the sample text, in
// media3's MatroskaExtractor shape; a sample without a duration
// cannot be displayed (same skip media3 performs).
val sampleDurationUs = packetOut[FfmpegDemuxerJni.OUT_DURATION_US]
if (sampleDurationUs <= 0) {
Log.w(TAG, "Skipping subtitle sample with no duration (stream=$streamIndex)")
continue
}
val prefix = when (subtitleKind) {
SubtitleKind.SSA -> FfmpegSubtitleSamples.ssaPrefix(sampleDurationUs)
SubtitleKind.SUBRIP -> FfmpegSubtitleSamples.subripPrefix(sampleDurationUs)
else -> FfmpegSubtitleSamples.vttPrefix(sampleDurationUs)
}
prefixParsable.reset(prefix, prefix.size)
trackOutput.sampleData(prefixParsable, prefix.size)
packetParsable.reset(packetBytes, size)
trackOutput.sampleData(packetParsable, size)
trackOutput.sampleMetadata(
ptsUs,
C.BUFFER_FLAG_KEY_FRAME,
prefix.size + size,
/* offset= */
0,
/* cryptoData= */
null
)
return Extractor.RESULT_CONTINUE
}
packetParsable.reset(packetBytes, size)
trackOutput.sampleData(packetParsable, size)
// Native drops packets without any timestamp, so ptsUs is always real.
trackOutput.sampleMetadata(
ptsUs,
if (isKeyframe) C.BUFFER_FLAG_KEY_FRAME else 0,
size,
/* offset= */
0,
/* cryptoData= */
null
)
recordSeekPoint(streamIndex, ptsUs, packetOut[FfmpegDemuxerJni.OUT_POSITION], isKeyframe)
bitrateMeters.getOrNull(streamIndex)?.onPacket(ptsUs, size)
return Extractor.RESULT_CONTINUE
}
FfmpegDemuxerJni.CODE_EOF -> return Extractor.RESULT_END_OF_INPUT
FfmpegDemuxerJni.CODE_NEED_SEEK -> {
val target = FfmpegDemuxerJni.nativeConsumePendingSeek()
if (target == Long.MIN_VALUE || target < 0) {
// Sticky AVIO error replayed without a fresh deferral: resync in
// place (bounded by reconciles) instead of killing the stream.
if (++reconciles > MAX_RECONCILES || !reSync(input)) {
throw malformed("demuxer produced an invalid seek target: $target")
}
continue
}
if (++reconciles > MAX_RECONCILES) throw malformed("too many demuxer repositions")
if (!reconcile(input, seekPosition, target)) return Extractor.RESULT_SEEK
}
FfmpegDemuxerJni.CODE_GROW -> {
val required = packetOut[FfmpegDemuxerJni.OUT_SIZE].toInt()
packetBytes = ByteArray(required + 64 * 1024)
}
FfmpegDemuxerJni.ERR_JAVA ->
throw IOException(inputProxy.lastError ?: "demuxer input failed")
else -> throw malformed("ffmpeg demuxer read failed: $code")
}
}
}
/** Re-establishes the AVIO position at the loader's current input position. */
private fun reSync(input: ExtractorInput): Boolean {
val position = input.position
return position >= 0 && FfmpegDemuxerJni.nativeResumeAfterSeek(position) == 0
}
/** Returns true when the input is already at [target] and native resumed. */
private fun reconcile(input: ExtractorInput, seekPosition: PositionHolder, target: Long): Boolean {
if (target == input.position) {
val code = FfmpegDemuxerJni.nativeResumeAfterSeek(target)
if (code != 0) throw malformed("demuxer resume failed: $code")
return true
}
seekPosition.position = target
return false
}
private fun recordSeekPoint(streamIndex: Int, timeUs: Long, position: Long, keyframe: Boolean) {
if (streamIndex != primaryStreamIndex || position <= 0) return
if (!primaryStreamIsAudio && !keyframe) return
synchronized(seekIndexLock) {
val lastIndex = seekIndexTimes.size - 1
if (lastIndex >= 0) {
if (timeUs <= seekIndexTimes[lastIndex]) return
if (primaryStreamIsAudio && timeUs - seekIndexTimes[lastIndex] < AUDIO_INDEX_INTERVAL_US) {
return
}
}
seekIndexTimes.add(timeUs)
seekIndexPositions.add(position)
}
}
private fun malformed(message: String): ParserException = ParserException.createForMalformedContainer(message, null)
private inner class SeekMapImpl : SeekMap {
// Seeks are executed by the demuxer (avformat_seek_file), so seekability
// does not depend on the sample-derived index below — that index only
// improves the loader's initial byte guess. The timeline is published
// from this value once, at prepare time, when the index is still empty.
override fun isSeekable(): Boolean = this@FfmpegExtractor.durationUs != C.TIME_UNSET
override fun getSeekPoints(timeUs: Long): SeekMap.SeekPoints {
synchronized(seekIndexLock) {
if (seekIndexTimes.isEmpty()) return SeekMap.SeekPoints(SeekPoint(0, 0))
val index = seekIndexTimes.binarySearchFloor(timeUs)
if (index < 0) return SeekMap.SeekPoints(SeekPoint(seekIndexTimes[0], seekIndexPositions[0]))
val preceding = SeekPoint(seekIndexTimes[index], seekIndexPositions[index])
if (index + 1 >= seekIndexTimes.size || seekIndexTimes[index] == timeUs) {
return SeekMap.SeekPoints(preceding)
}
val following = SeekPoint(seekIndexTimes[index + 1], seekIndexPositions[index + 1])
return SeekMap.SeekPoints(preceding, following)
}
}
// Qualified on purpose: bare `durationUs` binds to the inherited Java
// getter as a synthetic property (this.getDurationUs()) and recurses.
override fun getDurationUs(): Long = this@FfmpegExtractor.durationUs
}
}
/** Floor binary search over an ascending list; returns the greatest index with value <= target. */
private fun List<Long>.binarySearchFloor(value: Long): Int {
var low = 0
var high = size - 1
var result = -1
while (low <= high) {
val mid = (low + high) ushr 1
if (this[mid] <= value) {
result = mid
low = mid + 1
} else {
high = mid - 1
}
}
return result
}
@@ -1,46 +0,0 @@
package com.edde746.plezy.exoplayer
import java.util.Locale
/**
* Builds text subtitle samples in the exact shape media3's MatroskaExtractor
* writes, so the same downstream consumers work behind the FFmpeg demuxer:
* the bundled SsaParser/SubripParser/WebvttParser (via
* SubtitleTranscodingExtractorOutput) and AssTrackOutput's dialogue feed.
*
* MatroskaExtractor prefixes each raw payload with a synthetic header whose
* *second* timecode carries the sample duration (BlockDuration), while the
* first stays zero; parsers reconstruct absolute times as sampleTimeUs +
* parsedStart. FFmpeg hands us the same raw payloads (an ASS packet is the
* MKV block: "ReadOrder,Layer,Style,...,Text"; SubRip/WebVTT packets are the
* bare cue text), so only the prefix needs synthesizing.
*/
internal object FfmpegSubtitleSamples {
val SSA_DIALOGUE_FORMAT: ByteArray =
"Format: Start, End, ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
.toByteArray(Charsets.UTF_8)
/** `Dialogue: 0:00:00:00,H:MM:SS:CC,` — centiseconds, ':' separator (media3's SSA shape). */
fun ssaPrefix(durationUs: Long): ByteArray = "Dialogue: 0:00:00:00,${timecode(durationUs, "%01d:%02d:%02d:%02d", 10_000)},"
.toByteArray(Charsets.UTF_8)
/** `1\n00:00:00,000 --> HH:MM:SS,mmm\n` — a complete SRT cue header. */
fun subripPrefix(durationUs: Long): ByteArray = "1\n00:00:00,000 --> ${timecode(durationUs, "%02d:%02d:%02d,%03d", 1_000)}\n"
.toByteArray(Charsets.UTF_8)
/** `WEBVTT\n\n00:00:00.000 --> HH:MM:SS.mmm\n` — a one-cue WebVTT document header. */
fun vttPrefix(durationUs: Long): ByteArray = "WEBVTT\n\n00:00:00.000 --> ${timecode(durationUs, "%02d:%02d:%02d.%03d", 1_000)}\n"
.toByteArray(Charsets.UTF_8)
private fun timecode(timeUs: Long, format: String, lastValueDivisor: Long): String {
val hours = timeUs / 3_600_000_000L
var remainder = timeUs - hours * 3_600_000_000L
val minutes = remainder / 60_000_000L
remainder -= minutes * 60_000_000L
val seconds = remainder / 1_000_000L
remainder -= seconds * 1_000_000L
val lastValue = remainder / lastValueDivisor
return String.format(Locale.US, format, hours, minutes, seconds, lastValue)
}
}
@@ -0,0 +1,65 @@
package com.edde746.plezy.exoplayer
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
import androidx.media3.extractor.mkv.MatroskaExtractor
/** MKV CodecID for Microsoft ACM compatibility mode. */
private const val CODEC_ID_ACM = "A_MS/ACM"
/**
* MatroskaExtractor.init is final and extractorOutput private; subclasses swap
* in wrapping outputs via reflection once the Segment element starts (shared
* with ZlibMatroskaExtractor).
*/
internal val matroskaExtractorOutputField by lazy {
MatroskaExtractor::class.java.getDeclaredField("extractorOutput").apply {
isAccessible = true
}
}
/** WAVEFORMATEX format tag for LOAS/LATM-wrapped AAC (WAVE_FORMAT_MPEG_LOAS). */
private const val WAVE_FORMAT_MPEG_LOAS = 0x1602
/**
* Returns whether a track is LOAS/LATM AAC muxed as A_MS/ACM — ffmpeg's
* fallback mapping for aac_latm, which Matroska has no native codec ID for.
* The WAVEFORMATEX wFormatTag is the first 2 bytes (LE) of CodecPrivate.
*/
fun isLoasAcmTrack(codecId: String?, codecPrivate: ByteArray?): Boolean = codecId == CODEC_ID_ACM &&
codecPrivate != null &&
codecPrivate.size >= 2 &&
((codecPrivate[0].toInt() and 0xFF) or ((codecPrivate[1].toInt() and 0xFF) shl 8)) == WAVE_FORMAT_MPEG_LOAS
/**
* ExtractorOutput wrapper that wraps marked tracks with [LatmTrackOutput].
* Call [markNextTrackLatm] before the parent extractor creates the track
* (i.e. before super.endMasterElement(ID_TRACK_ENTRY)).
*/
class LatmExtractorOutputWrapper(
private val delegate: ExtractorOutput
) : ExtractorOutput {
private var nextTrackIsLatm = false
private val latmOutputs = mutableListOf<LatmTrackOutput>()
fun markNextTrackLatm() {
nextTrackIsLatm = true
}
/** Resets LATM parser state after an extractor seek. */
fun resetTracks() {
latmOutputs.forEach { it.reset() }
}
override fun track(id: Int, type: Int): TrackOutput {
val original = delegate.track(id, type)
if (!nextTrackIsLatm) return original
nextTrackIsLatm = false
return LatmTrackOutput(original, id).also { latmOutputs.add(it) }
}
override fun endTracks() = delegate.endTracks()
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
}
@@ -1,31 +0,0 @@
package com.edde746.plezy.exoplayer
import androidx.media3.common.util.UnstableApi
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
/**
* Delegates everything to [delegate] but hands it a wrapped [ExtractorOutput].
* Used to compose the text pipeline (libass dialogue feed + subtitle cue
* transcoding) around media3's raw-subtitle MatroskaExtractor without
* subclassing or reflection.
*/
@UnstableApi
internal class OutputWrappingExtractor(
private val delegate: Extractor,
private val wrap: (ExtractorOutput) -> ExtractorOutput
) : Extractor {
override fun sniff(input: ExtractorInput): Boolean = delegate.sniff(input)
override fun init(output: ExtractorOutput) = delegate.init(wrap(output))
override fun read(input: ExtractorInput, seekPosition: PositionHolder): Int = delegate.read(input, seekPosition)
override fun seek(position: Long, timeUs: Long) = delegate.seek(position, timeUs)
override fun release() = delegate.release()
override fun getUnderlyingImplementation(): Extractor = delegate.underlyingImplementation
}
@@ -1,65 +0,0 @@
package com.edde746.plezy.exoplayer
/**
* Rolling average bitrate of one demuxed stream, measured from packet sizes
* over their presentation-time span — the same source of truth mpv uses for
* its `audio-bitrate` property. It exists because containers frequently
* declare no per-track bitrate (Matroska without BPS statistics tags), which
* left the performance overlay with nothing to show (#2063).
*
* Fed from the loader thread ([onPacket]) and read from the stats path
* ([bitrateBps]); both are synchronized. The measurement follows the demux
* position, which runs ahead of the playhead by the buffered duration —
* acceptable for a diagnostics overlay.
*/
internal class StreamBitrateMeter(private val windowUs: Long = DEFAULT_WINDOW_US) {
private val ptsUsQueue = ArrayDeque<Long>()
private val sizeQueue = ArrayDeque<Int>()
private var windowBytes = 0L
@Synchronized
fun onPacket(ptsUs: Long, sizeBytes: Int) {
if (sizeBytes <= 0) return
// A backwards pts jump is a seek; packets on either side of it do not
// form a contiguous span, so the window restarts.
val last = ptsUsQueue.lastOrNull()
if (last != null && ptsUs < last) reset()
ptsUsQueue.addLast(ptsUs)
sizeQueue.addLast(sizeBytes)
windowBytes += sizeBytes
while (ptsUsQueue.size > 1 && ptsUsQueue.last() - ptsUsQueue.first() > windowUs) {
ptsUsQueue.removeFirst()
windowBytes -= sizeQueue.removeFirst()
}
}
/**
* Average bitrate in bits per second over the current window, or null until
* enough packets span a measurable interval.
*/
@Synchronized
fun bitrateBps(): Int? {
if (ptsUsQueue.size < MIN_PACKETS) return null
val spanUs = ptsUsQueue.last() - ptsUsQueue.first()
if (spanUs <= 0) return null
// The pts span covers the durations of every packet except the last one,
// whose bytes are therefore excluded — a constant-rate stream measures
// exactly its rate.
val bytes = windowBytes - sizeQueue.last()
if (bytes <= 0) return null
return (bytes * 8_000_000L / spanUs).toInt()
}
@Synchronized
fun reset() {
ptsUsQueue.clear()
sizeQueue.clear()
windowBytes = 0L
}
companion object {
private const val DEFAULT_WINDOW_US = 10_000_000L
private const val MIN_PACKETS = 16
}
}
@@ -0,0 +1,91 @@
package com.edde746.plezy.exoplayer
import androidx.media3.common.ParserException
import androidx.media3.extractor.TrackOutput
import java.util.zip.DataFormatException
import java.util.zip.Inflater
/**
* TrackOutput wrapper that inflates zlib-compressed sample data (MKV ContentCompAlgo 0).
* Each MKV block is independently zlib-compressed; this wrapper decompresses per-sample
* between sampleData() and sampleMetadata() calls.
*
* All buffers are reused across samples to minimize GC pressure on the hot path.
*/
class ZlibInflatingTrackOutput(
delegate: TrackOutput
) : BufferedTransformingTrackOutput(
delegate,
INITIAL_BUFFER_SIZE,
INFLATE_CHUNK,
MAX_COMPRESSED_SAMPLE_SIZE
) {
companion object {
private const val INITIAL_BUFFER_SIZE = 256 * 1024
private const val INFLATE_CHUNK = 64 * 1024
private const val MAX_COMPRESSED_SAMPLE_SIZE = 16 * 1024 * 1024
private const val MAX_INFLATED_SAMPLE_SIZE = 16 * 1024 * 1024
private const val MAX_COMPRESSION_RATIO = 1024L
private const val MIN_RATIO_ALLOWANCE = 1024L * 1024
}
var active = false
private val inflater = Inflater()
private var inflateBuf = ByteArray(INITIAL_BUFFER_SIZE)
private val overflowProbe = ByteArray(1)
override val transformEnabled: Boolean
get() = active
override val transformedBuffer: ByteArray
get() = inflateBuf
override fun transformSample(inputLength: Int, flags: Int): Int {
inflater.reset()
inflater.setInput(inputBuffer, 0, inputLength)
var written = 0
val ratioBound = maxOf(MIN_RATIO_ALLOWANCE, inputLength.toLong() * MAX_COMPRESSION_RATIO)
try {
while (true) {
if (written == inflateBuf.size) {
if (inflateBuf.size < MAX_INFLATED_SAMPLE_SIZE) {
val nextSize = minOf(MAX_INFLATED_SAMPLE_SIZE, inflateBuf.size * 2)
inflateBuf = inflateBuf.copyOf(nextSize)
} else {
val overflow = inflater.inflate(overflowProbe, 0, 1)
if (overflow > 0) {
throw malformed("Inflated sample exceeds the maximum size")
}
if (inflater.finished()) return written
throw stalledInflate()
}
}
val count = inflater.inflate(inflateBuf, written, inflateBuf.size - written)
written += count
if (written.toLong() > ratioBound) {
throw malformed("Inflated sample exceeds the maximum compression ratio")
}
if (inflater.finished()) return written
if (count == 0) throw stalledInflate()
}
} catch (_: DataFormatException) {
// Preserve playback for corrupt subtitle blocks; the raw sample remains bounded
// by MAX_COMPRESSED_SAMPLE_SIZE and matches the pre-hardening fallback.
if (inflateBuf.size < inputLength) inflateBuf = inflateBuf.copyOf(inputLength)
inputBuffer.copyInto(inflateBuf, endIndex = inputLength)
return inputLength
}
}
private fun stalledInflate(): ParserException = when {
inflater.needsDictionary() -> malformed("Zlib-compressed sample requires a dictionary")
inflater.needsInput() -> malformed("Truncated zlib-compressed sample")
else -> malformed("Zlib decompressor made no progress")
}
private fun malformed(message: String, cause: Throwable? = null): ParserException = ParserException.createForMalformedContainer(message, cause)
}
@@ -0,0 +1,315 @@
package com.edde746.plezy.exoplayer
import android.util.Log
import androidx.media3.common.C
import androidx.media3.common.DataReader
import androidx.media3.extractor.DefaultExtractorInput
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
import androidx.media3.extractor.text.SubtitleParser
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.extractor.AssMatroskaExtractor
import java.io.EOFException
import java.util.zip.DataFormatException
import java.util.zip.Inflater
/**
* Extends AssMatroskaExtractor to add support for MKV quirks media3 rejects:
*
* ContentCompAlgo 0 (zlib) — media3 only supports ContentCompAlgo 3 (header
* stripping). This subclass intercepts the compression algorithm during track
* header parsing:
* - Tells the parent it's header stripping (algo 3) to avoid the ParserException
* - Skips ContentCompSettings for zlib tracks (not applicable)
* - For text subtitle tracks, inflates the block payload *before* the parent
* parses it (see below). For every other zlib track, wraps TrackOutputs with
* ZlibInflatingTrackOutput to decompress per-sample data.
*
* Text subtitle tracks (SRT/ASS/SSA/VTT) cannot be inflated at the TrackOutput
* level: MatroskaExtractor rewrites their samples in-place before any TrackOutput
* runs — it prepends a plaintext timecode prefix ("Dialogue: 0:00:00:00,…,") to
* the still-compressed payload and truncates the sample at the first NUL byte,
* which deflate streams routinely contain (#2023). AssTrackOutput additionally
* feeds that same internal buffer straight to libass. So for zlib text tracks the
* frame payload is inflated at the block level, before the parent's sample
* assembly, and the TrackOutput wrapper is left inactive.
*
* LOAS/LATM AAC as A_MS/ACM — media3 sets audio/x-unknown for non-PCM ACM
* tracks (silent playback). Detected tracks are wrapped with LatmTrackOutput,
* which unwraps LOAS frames to raw AAC for direct-playing Matroska files.
*/
class ZlibMatroskaExtractor(
subtitleParserFactory: SubtitleParser.Factory,
assHandler: AssHandler
) : AssMatroskaExtractor(subtitleParserFactory, assHandler) {
companion object {
private const val TAG = "ZlibMkvExtractor"
// Matroska EBML element IDs
private const val ID_SEGMENT = 0x18538067
private const val ID_TRACK_ENTRY = 0xAE
private const val ID_CONTENT_COMPRESSION_ALGORITHM = 0x4254
private const val ID_CONTENT_COMPRESSION_SETTINGS = 0x4255
private const val ID_CONTENT_COMPRESSION = 0x5034
private const val ID_SIMPLE_BLOCK = 0xA3
private const val ID_BLOCK = 0xA1
/**
* Codec IDs whose samples MatroskaExtractor rewrites in-place (timecode prefix
* plus NUL truncation) before any TrackOutput runs — the authority is the
* prefix list in MatroskaExtractor.writeSampleData.
*/
private val TEXT_SUBTITLE_CODEC_IDS = setOf("S_TEXT/UTF8", "S_TEXT/ASS", "S_TEXT/SSA", "S_TEXT/WEBVTT")
/** Subtitle blocks are tiny; anything above this passes through untouched. */
private const val MAX_TEXT_BLOCK_BYTES = 16 * 1024 * 1024
}
private var zlibOutput: ZlibExtractorOutputWrapper? = null
private var latmOutput: LatmExtractorOutputWrapper? = null
private var currentTrackUsesZlib = false
/** Track numbers whose blocks are inflated before the parent parses them. */
private val zlibTextTrackNumbers = mutableSetOf<Int>()
private val blockInflater = Inflater()
private var blockBuf = ByteArray(0)
private val peekBuf = ByteArray(8)
override fun startMasterElement(id: Int, contentPosition: Long, contentSize: Long) {
super.startMasterElement(id, contentPosition, contentSize)
// ContentCompAlgo DEFAULTS to 0 (zlib), so mkvmerge omits the element for
// zlib tracks entirely — the mere presence of ContentCompression means zlib
// until an explicit ContentCompAlgo says otherwise. media3 ignores an empty
// ContentCompression and would silently emit compressed samples.
if (id == ID_CONTENT_COMPRESSION) {
currentTrackUsesZlib = true
Log.i(TAG, "Track has ContentCompression, assuming ContentCompAlgo 0 (zlib) until told otherwise")
}
// After super installs AssSubtitleExtractorOutput, wrap it with our zlib +
// LATM layers (zlib outermost so inflation runs before LATM parsing).
if (id == ID_SEGMENT && zlibOutput == null) {
val currentOutput = matroskaExtractorOutputField.get(this) as ExtractorOutput
val latmWrapper = LatmExtractorOutputWrapper(currentOutput)
latmOutput = latmWrapper
val wrapper = ZlibExtractorOutputWrapper(latmWrapper)
zlibOutput = wrapper
matroskaExtractorOutputField.set(this, wrapper)
Log.d(TAG, "Installed zlib+LATM ExtractorOutput wrapper")
}
}
override fun integerElement(id: Int, value: Long) {
if (id == ID_CONTENT_COMPRESSION_ALGORITHM) {
if (value == 0L) {
currentTrackUsesZlib = true
Log.i(TAG, "Track uses explicit ContentCompAlgo 0 (zlib), will inflate samples")
// Tell parent it's header stripping (algo 3) to avoid ParserException
super.integerElement(id, 3)
return
}
// Explicit non-zlib algorithm: header stripping (3) is handled by the
// parent; anything else makes the parent throw, matching stock behavior.
currentTrackUsesZlib = false
}
super.integerElement(id, value)
}
override fun binaryElement(id: Int, contentSize: Int, input: ExtractorInput) {
if (id == ID_CONTENT_COMPRESSION_SETTINGS && currentTrackUsesZlib) {
// Skip ContentCompSettings for zlib tracks — parent would store these as
// sampleStrippedBytes and prepend them to every sample, corrupting output.
input.skipFully(contentSize)
return
}
if ((id == ID_SIMPLE_BLOCK || id == ID_BLOCK) &&
zlibTextTrackNumbers.isNotEmpty() &&
contentSize in MIN_BLOCK_BYTES..MAX_TEXT_BLOCK_BYTES &&
peekBlockTrackNumber(input) in zlibTextTrackNumbers
) {
inflateTextBlock(id, contentSize, input)
return
}
super.binaryElement(id, contentSize, input)
}
override fun endMasterElement(id: Int) {
var zlibTextTrackNumber: Int? = null
if (id == ID_TRACK_ENTRY) {
// Must inspect before super — the track output is created inside super's
// endMasterElement, and the current track is cleared afterwards.
val track = getCurrentTrack(id)
if (isLoasAcmTrack(track.codecId, track.codecPrivate)) {
Log.i(TAG, "Track ${track.number} is LOAS/LATM AAC, unwrapping to raw AAC")
latmOutput?.markNextTrackLatm()
}
if (currentTrackUsesZlib && track.codecId in TEXT_SUBTITLE_CODEC_IDS) {
zlibTextTrackNumber = track.number
}
}
val wasZlib = currentTrackUsesZlib
super.endMasterElement(id)
if (id == ID_TRACK_ENTRY && wasZlib) {
currentTrackUsesZlib = false
if (zlibTextTrackNumber != null) {
// Text subtitle samples are rewritten inside the parent before any
// TrackOutput runs, so the TrackOutput wrapper stays inactive and the
// block payload is inflated in binaryElement instead.
zlibTextTrackNumbers.add(zlibTextTrackNumber)
Log.i(TAG, "Track $zlibTextTrackNumber is a zlib text subtitle track, inflating at block level")
} else {
zlibOutput?.activateLast()
Log.i(TAG, "Activated zlib inflation for track")
}
}
}
override fun seek(position: Long, timeUs: Long) {
latmOutput?.resetTracks()
zlibOutput?.resetTracks()
super.seek(position, timeUs)
}
/**
* Peeks the EBML varint at the block start — the block's track number — without
* consuming input. Returns null for malformed varints or truncated input; the
* parent then produces the canonical failure for the untouched stream.
*/
private fun peekBlockTrackNumber(input: ExtractorInput): Int? {
try {
input.peekFully(peekBuf, 0, 1)
val first = peekBuf[0].toInt() and 0xFF
if (first == 0) return null
val length = Integer.numberOfLeadingZeros(first) - 23
var value = (first and (0xFF ushr length)).toLong()
if (length > 1) {
input.peekFully(peekBuf, 1, length - 1)
for (i in 1 until length) {
value = (value shl 8) or (peekBuf[i].toLong() and 0xFF)
}
}
return if (value <= Int.MAX_VALUE) value.toInt() else null
} catch (_: EOFException) {
return null
} finally {
input.resetPeekPosition()
}
}
/**
* Buffers one text-subtitle block, inflates its frame payload, and hands the
* parent a block whose payload is plaintext. A block that cannot be rewritten
* (laced, corrupt, or over-bound) passes through byte-identical.
*/
private fun inflateTextBlock(id: Int, contentSize: Int, input: ExtractorInput) {
val basePosition = input.position
if (blockBuf.size < contentSize) blockBuf = ByteArray(maxOf(contentSize, blockBuf.size * 2))
input.readFully(blockBuf, 0, contentSize)
val rewritten = rewriteZlibTextBlock(blockBuf, contentSize, blockInflater)
if (rewritten == null) {
Log.w(TAG, "Passing zlib text block through uninflated (laced, corrupt, or over-bound)")
super.binaryElement(id, contentSize, bufferedInput(blockBuf, contentSize, basePosition))
} else {
super.binaryElement(id, rewritten.size, bufferedInput(rewritten, rewritten.size, basePosition))
}
}
private fun bufferedInput(data: ByteArray, limit: Int, position: Long): ExtractorInput = DefaultExtractorInput(ByteRangeDataReader(data, limit), position, position + limit)
private class ByteRangeDataReader(private val data: ByteArray, private val limit: Int) : DataReader {
private var position = 0
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (position == limit) return C.RESULT_END_OF_INPUT
val count = minOf(length, limit - position)
System.arraycopy(data, position, buffer, offset, count)
position += count
return count
}
}
/**
* ExtractorOutput wrapper that wraps all TrackOutputs with ZlibInflatingTrackOutput.
* Tracks are created inactive; activateLast() enables inflation for the most recently
* created track (called when we know a track uses zlib compression).
*/
private class ZlibExtractorOutputWrapper(
private val delegate: ExtractorOutput
) : ExtractorOutput {
private var lastCreatedWrapper: ZlibInflatingTrackOutput? = null
private val trackOutputs = mutableListOf<ZlibInflatingTrackOutput>()
override fun track(id: Int, type: Int): TrackOutput {
val original = delegate.track(id, type)
return ZlibInflatingTrackOutput(original).also {
trackOutputs.add(it)
lastCreatedWrapper = it
}
}
fun activateLast() {
lastCreatedWrapper?.active = true
}
fun resetTracks() {
trackOutputs.forEach { it.resetBufferedData() }
}
override fun endTracks() = delegate.endTracks()
override fun seekMap(seekMap: SeekMap) = delegate.seekMap(seekMap)
}
}
/** Smallest well-formed unlaced block: 1-byte varint + 2-byte timecode + flags + 1 payload byte. */
internal const val MIN_BLOCK_BYTES = 5
/**
* Rewrites one unlaced Matroska block whose frame payload is zlib-compressed:
* returns the unchanged block header followed by the inflated payload, or null
* when the block must pass through unchanged — lacing (never produced for text
* subtitle tracks), a truncated or corrupt deflate stream, or an inflated size
* beyond the bounds shared with [ZlibInflatingTrackOutput]'s hardening.
*/
internal fun rewriteZlibTextBlock(data: ByteArray, limit: Int, inflater: Inflater): ByteArray? {
if (limit < MIN_BLOCK_BYTES) return null
val first = data[0].toInt() and 0xFF
if (first == 0) return null // track-number varint longer than 8 bytes
val varintLength = Integer.numberOfLeadingZeros(first) - 23
val headerLength = varintLength + 3 // varint + 2-byte timecode + flags
if (limit <= headerLength) return null
if (data[headerLength - 1].toInt() and 0x06 != 0) return null // laced
val payloadLength = limit - headerLength
val ratioBound = maxOf(1024L * 1024, payloadLength.toLong() * 1024)
val maxInflatedBytes = 16 * 1024 * 1024
inflater.reset()
inflater.setInput(data, headerLength, payloadLength)
var buf = ByteArray(maxOf(4096, payloadLength * 4))
var written = 0
try {
while (true) {
if (written == buf.size) {
if (buf.size >= maxInflatedBytes) return null
buf = buf.copyOf(minOf(maxInflatedBytes, buf.size * 2))
}
val count = inflater.inflate(buf, written, buf.size - written)
written += count
if (written > ratioBound) return null
if (inflater.finished()) break
if (count == 0) return null // truncated stream or preset-dictionary request
}
} catch (_: DataFormatException) {
return null
}
return ByteArray(headerLength + written).also {
System.arraycopy(data, 0, it, 0, headerLength)
System.arraycopy(buf, 0, it, headerLength, written)
}
}
@@ -0,0 +1,28 @@
package com.edde746.plezy.mpv
/**
* Demuxer cache budget derived from the device heap class.
*
* mpv has no device-memory awareness: `demuxer-max-bytes` defaults to a fixed
* 150 MiB forward (+50 MiB back) on every device and the demuxer fills
* whatever it is allowed, which crowds a 1 GB TV box until Android's
* low-memory killer takes the whole app. Tiered off
* [android.app.ActivityManager.getLargeMemoryClass], the same signal
* `stream_buffer_sizing.dart` and ExoPlayer's `LoadControlPolicy` use.
*
* Applied as pre-init *options* in [MpvPlayerCore], so a `demuxer-max-bytes`
* line in the user's mpv.conf still wins.
*/
data class DemuxerBudget(val aheadBytes: Long, val backBytes: Long) {
companion object {
private const val MIB = 1024L * 1024L
/** Null for an unknown class (<= 0): callers keep mpv's own defaults. */
fun forHeapClassMB(largeMemoryClassMB: Int): DemuxerBudget? = when {
largeMemoryClassMB <= 0 -> null
largeMemoryClassMB <= 256 -> DemuxerBudget(aheadBytes = 32 * MIB, backBytes = 16 * MIB)
largeMemoryClassMB <= 512 -> DemuxerBudget(aheadBytes = 64 * MIB, backBytes = 32 * MIB)
else -> DemuxerBudget(aheadBytes = 100 * MIB, backBytes = 48 * MIB)
}
}
}
@@ -0,0 +1,74 @@
package com.edde746.plezy.mpv
import android.opengl.EGL14
/**
* Which mpv `egl-output-format` this device can pair with a BT.2020 PQ window
* surface, or null when HDR GL output is unavailable. 10-bit fixed point is
* preferred; fp16 is the fallback because some drivers (Tegra among them)
* expose the PQ colorspace but no 1010102 window config.
*
* Both halves are required by the mpv side: the fork's android GL context asks
* for `EGL_EXT_gl_colorspace_bt2020_pq` on the window surface, and the app
* pairs it with an `egl-output-format` that makes mpv's EGL config selection
* fail outright when no matching config exists - so this probe must be
* consulted before those options are ever set.
*/
internal object EglHdrCaps {
private const val EGL_COLOR_COMPONENT_TYPE_EXT = 0x3339
private const val EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT = 0x333B
private object Unprobed
@Volatile private var cached: Any? = Unprobed
fun pqOutputFormat(): String? {
val value = cached
if (value !== Unprobed) return value as String?
return probe().also { cached = it }
}
private fun probe(): String? {
val display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
if (display == EGL14.EGL_NO_DISPLAY) return null
val version = IntArray(2)
// Deliberately no eglTerminate: the default display is process-global and
// Flutter's renderer shares it; terminating would invalidate its state.
if (!EGL14.eglInitialize(display, version, 0, version, 1)) return null
val extensions = EGL14.eglQueryString(display, EGL14.EGL_EXTENSIONS) ?: return null
if (!extensions.contains("EGL_EXT_gl_colorspace_bt2020_pq")) return null
if (hasWindowConfig(display, intArrayOf(EGL14.EGL_RED_SIZE, 10, EGL14.EGL_GREEN_SIZE, 10, EGL14.EGL_BLUE_SIZE, 10, EGL14.EGL_ALPHA_SIZE, 2))) {
return "rgb10_a2"
}
if (extensions.contains("EGL_EXT_pixel_format_float") &&
hasWindowConfig(
display,
intArrayOf(
EGL14.EGL_RED_SIZE,
16,
EGL14.EGL_GREEN_SIZE,
16,
EGL14.EGL_BLUE_SIZE,
16,
EGL_COLOR_COMPONENT_TYPE_EXT,
EGL_COLOR_COMPONENT_TYPE_FLOAT_EXT
)
)
) {
return "rgba16f"
}
return null
}
private fun hasWindowConfig(display: android.opengl.EGLDisplay, extra: IntArray): Boolean {
val attribs = intArrayOf(
EGL14.EGL_SURFACE_TYPE,
EGL14.EGL_WINDOW_BIT,
EGL14.EGL_RENDERABLE_TYPE,
EGL14.EGL_OPENGL_ES2_BIT
) + extra + intArrayOf(EGL14.EGL_NONE)
val numConfigs = IntArray(1)
if (!EGL14.eglChooseConfig(display, attribs, 0, null, 0, 0, numConfigs, 0)) return false
return numConfigs[0] > 0
}
}
@@ -0,0 +1,60 @@
package com.edde746.plezy.mpv
/**
* Pure policy for when an mpv session must leave the video plane
* (vo=mediacodec) for a GL video output, and which one. Kept free of player
* and platform state so the routing matrix is unit-testable.
*/
internal object GpuVoPolicy {
/**
* Single-layer Dolby Vision Profile 5 (IPT-PQ-c2) has no compatible base
* layer: on a device without native DV support it decodes as plain HEVC
* with garbage colors, and the video plane applies no reshaping. gpu-next
* (libplacebo) under software decode is the only Android path that
* composites the RPU metadata (#1902). [dvProfile] comes from mpv's
* track-list — the bitstream's DOVI configuration record — never from
* server metadata, which mis-tags DV routinely. Only `auto` routes; the
* other modes are explicit user choices.
*/
fun needsDvReshaping(dvProfile: Long?, conversionMode: String, canPlayP5Natively: Boolean): Boolean = dvProfile == 5L && conversionMode == "auto" && !canPlayP5Natively
/**
* Whether an HDR signal has nowhere to tone-map: the video plane hands
* PQ/HLG straight to a display pipeline that advertises no HDR output, so
* it renders washed out (#2121). The GL vo tone-maps in the render chain.
*/
fun needsHdrToneMapping(gamma: String?, displaySupportsHdr: Boolean): Boolean = (gamma == "pq" || gamma == "hlg") && !displaySupportsHdr
/**
* Whether the decoder is handing mpv software frames, from `hwdec-current`.
*
* The plane refuses every format but MediaCodec buffers, so a per-file
* decode fallback (AV1 on Tegra, Hi10 without a profile match) has to move
* to a GL vo. Routing on this gets there before mpv fails the chain and
* [REASON_CHAIN_FAILURE] has to catch it.
*/
fun needsSoftwareRender(hwdecCurrent: String?): Boolean = !hwdecCurrent.isNullOrBlank() && hwdecCurrent != "mediacodec"
/**
* The vo a session with these active requirements should run, or null for
* the video plane.
*
* dv-reshape is the only reason that needs gpu-next, since libplacebo is
* what composites the RPU. Everything else takes gpu, the battle-tested
* GLES renderer on the Android device zoo. (The magenta field gpu-next
* used to render for 10-bit software frames on Tegra was its AV1 film
* grain shader overrunning the driver's uniform register budget; grain is
* decoder-applied now, but gpu-next buys this path nothing over gpu.)
*/
fun targetFor(reasons: Set<String>): String? = when {
reasons.isEmpty() -> null
REASON_DV_RESHAPE in reasons -> "gpu-next"
else -> "gpu"
}
const val REASON_DV_RESHAPE = "dv-reshape"
const val REASON_SHADERS = "shaders"
const val REASON_CHAIN_FAILURE = "chain-failure"
const val REASON_HDR_SDR = "hdr-sdr"
const val REASON_SW_DECODE = "sw-decode"
}
@@ -1,6 +1,7 @@
package com.edde746.plezy.mpv
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.graphics.PixelFormat
import android.media.AudioAttributes
@@ -15,6 +16,7 @@ import android.view.SurfaceView
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import com.edde746.plezy.exoplayer.DoviBridge
import com.edde746.plezy.shared.AudioFocusManager
import com.edde746.plezy.shared.FrameRateManager
import com.edde746.plezy.shared.PlayerDelegate
@@ -63,24 +65,41 @@ class MpvPlayerCore private constructor(
* The initial `vo` chain, decided by whether this session will hardware-
* decode.
*
* gpu-next (libplacebo) is the only Android path that applies Dolby Vision
* RPU reshaping (#1902), but reshaping only ever happens under software
* decode: FFmpeg's mediacodec wrapper exports no DOVI side data, so a
* hardware-decoded stream renders the untouched base layer on any VO.
* Hardware decode is also where gpu-next breaks: it samples the decoder
* output as a samplerExternalOES that libplacebo declares in both shader
* stages, and the Tegra GLES linker rejects that pair ("struct type
* mismatch between shaders for uniform"), failing every frame — a solid
* blue screen with audio on the Shield (#2010). The in-chain gpu fallback
* cannot catch it because gpu-next initializes fine and only fails
* per-frame renders.
* Hardware sessions use the fork vo=mediacodec: decoded buffers go from
* MediaCodec straight to the compositor with per-frame presentation
* timestamps - no GLES pass, 10-bit and the decoder's dataspace
* (HDR10/HLG) intact - and subtitles/OSD render on the sibling OSD
* surface. The plane takes decoder buffers only and refuses the rest, so
* a per-file decode fallback moves to a GL vo
* ([GpuVoPolicy.needsSoftwareRender], with the chain-failure watchdog as
* the backstop). gpu stays in the chain for preinit failure.
*
* So gpu-next is offered exactly where it can help — sessions that will
* software-decode — and hardware sessions keep the legacy gpu VO. A
* mid-session hwdec fallback to software stays on vo=gpu, which renders
* software frames correctly (the pre-2.15.0 behavior for every session).
* Software sessions run gpu,gpu-next: gpu is the battle-tested GLES
* renderer on the Android device zoo, and with film grain applied by the
* decoder nothing else on this path needs libplacebo. Dolby Vision RPU
* reshaping (#1902) is the one exception - it needs gpu-next, and the
* [GpuVoPolicy.REASON_DV_RESHAPE] observer moves the session there when a
* DV profile that needs reshaping appears. gpu-next under *hardware*
* decode is broken on Tegra (samplerExternalOES double declaration
* rejected by the GLES linker, blue screen on the Shield, #2010);
* vo=mediacodec sidesteps that entire class by never touching GLES.
*/
internal fun initialVideoOutput(hardwareDecoding: Boolean): String = if (hardwareDecoding) "gpu" else "gpu-next,gpu"
internal fun initialVideoOutput(hardwareDecoding: Boolean): String = if (hardwareDecoding) "mediacodec,gpu" else "gpu,gpu-next"
/**
* The `-append` list-option suffixes are not exposed through the property
* interface, so the app's decoder options replace the whole list. FFmpeg
* keeps the last duplicate key, so any user mpv.conf entries go first.
*/
internal fun mergeDecoderOptions(current: String?, ours: String): String = if (current.isNullOrBlank()) ours else "$current,$ours"
/**
* Whether content with this transfer is worth an HDR (BT.2020 PQ) GL
* surface. PQ and HLG both render into a PQ target; everything else -
* including unknown - stays on the default sRGB surface, which renders
* every content correctly (HDR arrives tone-mapped, as before).
*/
internal fun wantsHdrSurface(transfer: String?): Boolean = transfer == "smpte2084" || transfer == "arib-std-b67"
}
/** Video-only paths. The plugin always constructs video cores with the
@@ -89,7 +108,57 @@ class MpvPlayerCore private constructor(
get() = context as Activity
private var surfaceView: SurfaceView? = null
private var osdSurfaceView: SurfaceView? = null
private var surfaceContainer: android.widget.FrameLayout? = null
@Volatile private var pendingOsdSurface: Surface? = null
@Volatile private var attachedOsdSurface: Surface? = null
/** Active reasons the session must render off the plane. */
private val gpuVoReasons = LinkedHashSet<String>()
/** The GL vo this session is running, or null for the video plane. Non-null
* gates off the plane-only machinery: OSD attach, aspect-fitted layout,
* chain-failure watchdog. Written under [gpuVoReasons]. */
@Volatile private var activeGpuVoTarget: String? = null
/** Whether the per-file DV policy is holding hwdec at `no`; the session's
* own hwdec value is parked in [hwdecBeforeDvReshape] meanwhile. */
@Volatile private var dvReshapeActive: Boolean = false
private val hwdecBeforeDvReshape = java.util.concurrent.atomic.AtomicReference<String?>()
/** Last `dv-conversion-mode` Dart applied; input to the per-file DV
* routing policy. */
@Volatile private var currentDvConversionMode: String = "auto"
/** Whether this core already decided its GL surface colorspace; set by the
* first `content-color-transfer` announcement ([applyContentColorTransfer]). */
@Volatile private var hdrSurfaceDecided: Boolean = false
/** Whether this session outputs HDR to an HDR-capable display — via the PQ
* GL surface or the MediaCodec plane's decoder dataspace. Gates the
* deferred display-mode restore on teardown (see
* [FrameRateManager.clearVideoFrameRate]). */
@Volatile private var hdrDisplayActive: Boolean = false
@Volatile private var videoDisplayWidth: Int = 0
@Volatile private var videoDisplayHeight: Int = 0
/** Latest `panscan` (0..1) and `video-zoom` (log2) the app applied. The
* plane owns scaling, so these are view geometry here; see
* [applyVideoRectLayout]. */
@Volatile private var videoPanscan: Float = 0f
@Volatile private var videoZoomLog2: Float = 0f
/** Hardware sessions render through the fork vo=mediacodec (see
* [initialVideoOutput]); the OSD surface and video-rect layout exist only
* there. */
private val usesMediaCodecVo: Boolean
get() = !audioOnly && hardwareDecoding
private var overlayLayoutListener: ViewTreeObserver.OnGlobalLayoutListener? = null
@Volatile private var disposing: Boolean = false
@@ -260,6 +329,22 @@ class MpvPlayerCore private constructor(
lastAppliedSurfaceSize = null
lastKnownSurfaceWidth = 0
lastKnownSurfaceHeight = 0
// Video-output state, so a re-initialized core does not start stranded
// off the plane with a stale reason set.
synchronized(gpuVoReasons) {
gpuVoReasons.clear()
activeGpuVoTarget = null
}
hwdecBeforeDvReshape.set(null)
dvReshapeActive = false
attachedOsdSurface = null
videoDisplayWidth = 0
videoDisplayHeight = 0
videoPanscan = 0f
videoZoomLog2 = 0f
currentDvConversionMode = "auto"
hdrSurfaceDecided = false
hdrDisplayActive = false
if (!audioOnly) ensurePlaceholderSurface()
// Initialize audio focus handling. mpv has none built in, so both modes
@@ -287,6 +372,10 @@ class MpvPlayerCore private constructor(
surfaceContainer = PlayerSurfaceHost.createContainer(activity)
surfaceView = PlayerSurfaceHost.createVideoSurface(activity, this@MpvPlayerCore)
surfaceContainer!!.addView(surfaceView)
if (usesMediaCodecVo) {
osdSurfaceView = PlayerSurfaceHost.createOsdSurface(activity, osdSurfaceCallback)
surfaceContainer!!.addView(osdSurfaceView)
}
val contentView = PlayerSurfaceHost.attachToContent(activity, surfaceContainer!!)
flutterOverlayApplied = PlayerSurfaceHost.ensureFlutterOverlayOnTop(contentView, surfaceContainer)
@@ -295,6 +384,7 @@ class MpvPlayerCore private constructor(
ensureFlutterOverlayOnTop()
val sv = surfaceView
if (sv != null) applySurfaceSize(sv.width, sv.height)
applyVideoRectLayout()
}
contentView.viewTreeObserver.addOnGlobalLayoutListener(overlayLayoutListener)
@@ -308,6 +398,11 @@ class MpvPlayerCore private constructor(
return@launch
}
val displayFpsOverride = currentDisplayFpsOverride()
// Both core kinds cap their demuxer cache off the device heap class;
// rationale on DemuxerBudget. Null (unknown class) keeps mpv defaults.
val demuxerBudget = DemuxerBudget.forHeapClassMB(
(context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager)?.largeMemoryClass ?: 0
)
val p = MpvPlayer.create(context.applicationContext) {
if (audioOnly) {
// Pure audio core (all set before mpv_initialize, mirroring the
@@ -322,15 +417,25 @@ class MpvPlayerCore private constructor(
setOption("gapless-audio", "weak")
} else {
// vo choice is decode-path-dependent; rationale on
// initialVideoOutput. Film grain is left on its `auto` default:
// applied by the VO under gpu-next, by the decoder under gpu.
// initialVideoOutput.
setOption("vo", initialVideoOutput(hardwareDecoding))
setOption("gpu-context", "android")
setOption("opengl-es", "yes")
// Keep AV1 film grain inside the decoder (dav1d). `auto` hands it
// to any vo claiming VO_CAP_FILM_GRAIN, and gpu-next claims it on
// GLES where libplacebo's raster grain fallback fetches luma by
// fragcoord (bottom-up) but chroma by uv: the luma renders
// upside-down (measured on a Shield Pro; desktop GL is unaffected
// because grain runs as a compute pass there).
setOption("vd-lavc-film-grain", "cpu")
if (displayFpsOverride != null) {
setOption("display-fps-override", displayFpsOverride)
}
}
if (demuxerBudget != null) {
setOption("demuxer-max-bytes", demuxerBudget.aheadBytes.toString())
setOption("demuxer-max-back-bytes", demuxerBudget.backBytes.toString())
}
setOption("ao", "audiotrack,opensles")
// Pause on the last frame at EOF instead of unloading the file, so a
// seek after the video ends still works (matches Linux/Windows).
@@ -342,6 +447,13 @@ class MpvPlayerCore private constructor(
// builtin script during mpv_initialize, hence an option here.
setOption("ytdl", "no")
}
if (demuxerBudget != null) {
Log.d(
TAG,
"Demuxer budget: ${demuxerBudget.aheadBytes / (1024 * 1024)}MB ahead, " +
"${demuxerBudget.backBytes / (1024 * 1024)}MB back"
)
}
if (displayFpsOverride != null) {
Log.d(TAG, "Initial display-fps-override=$displayFpsOverride")
}
@@ -361,6 +473,12 @@ class MpvPlayerCore private constructor(
collectEvents(p)
collectPropertyChanges(p)
collectLogMessages(p)
if (usesMediaCodecVo) {
collectVideoDimensions(p)
collectShaderState(p)
collectHdrToneMapState(p)
collectDecoderState(p)
}
Log.d(TAG, "Initialized successfully")
onResult(true)
@@ -397,9 +515,27 @@ class MpvPlayerCore private constructor(
}
is MpvEvent.StartFile -> {
endFileDiagnostics.onStartFile()
// The trigger is per-file (an exotic pixel format, a gralloc
// refusal for that stream), so give the plane back to the next
// file. A genuine failure re-arms it, costing one switch per bad
// file instead of the whole session's HDR/10-bit scanout.
setGpuVoRequirement(GpuVoPolicy.REASON_CHAIN_FAILURE, false)
delegate?.onEvent("start-file", null)
}
is MpvEvent.FileLoaded -> delegate?.onEvent("file-loaded", null)
is MpvEvent.FileLoaded -> {
if (usesMediaCodecVo) {
scope.launch(mpvWriteDispatcher, start = CoroutineStart.ATOMIC) {
try {
applyDvReshapePolicy(p)
} catch (e: CancellationException) {
Log.d(TAG, "Canceled DV routing policy")
} catch (e: Exception) {
Log.w(TAG, "DV routing policy failed", e)
}
}
}
delegate?.onEvent("file-loaded", null)
}
is MpvEvent.PlaybackRestart -> delegate?.onEvent("playback-restart", null)
else -> {}
}
@@ -433,6 +569,18 @@ class MpvPlayerCore private constructor(
scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.logFlow.collect { msg ->
endFileDiagnostics.onLogMessage(msg)
// A chain-init failure is the one runtime signal that frames cannot
// reach the video plane at all (exotic pixel formats, gralloc
// refusal). mpv is pinned in the fork, so the log line is a stable
// contract.
if (usesMediaCodecVo &&
activeGpuVoTarget == null &&
msg.prefix.startsWith("cplayer") &&
msg.text.contains("Could not initialize video chain")
) {
Log.w(TAG, "Video chain init failed under vo=mediacodec; leaving the video plane")
setGpuVoRequirement(GpuVoPolicy.REASON_CHAIN_FAILURE, true)
}
emitLog(msg.level.name.lowercase(), msg.prefix, msg.text)
}
}
@@ -484,6 +632,300 @@ class MpvPlayerCore private constructor(
detachSurfaceInternal(reason = "surfaceDestroyed")
}
// OSD surface (the vo=mediacodec subtitle/OSD plane)
private val osdSurfaceCallback = object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
if (disposing) return
pendingOsdSurface = holder.surface.takeIf { it.isValid }
Log.d(TAG, "OSD surface created")
if (player != null && hasAttachedRealSurface()) {
refreshVideoOutput("osdSurfaceCreated")
}
}
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d(TAG, "OSD surface destroyed")
pendingOsdSurface = null
val wasAttached = attachedOsdSurface != null
attachedOsdSurface = null
val p = player ?: return
if (disposing || !wasAttached) return
// Ordered against every other mpv write: a detach that overtook the
// re-attach of a recreated surface would clear the option the attach
// just set, and nothing re-arms it — subtitles would stay dead for the
// rest of the session.
scope.launch(mpvWriteDispatcher) {
try {
p.detachOsdSurface()
} catch (e: Exception) {
Log.w(TAG, "Failed to detach OSD surface", e)
}
}
}
}
/**
* Hands the OSD Surface to the vo. The vo reads the option when it is
* created, so this has to run before the `vo` or `wid` write that creates
* it, never after.
*/
private fun attachOsdSurfaceIfNeeded(p: MpvPlayer) {
if (!usesMediaCodecVo || activeGpuVoTarget != null) return
val osd = pendingOsdSurface?.takeIf { it.isValid } ?: return
if (osd === attachedOsdSurface) return
p.attachOsdSurface(osd)
attachedOsdSurface = osd
Log.d(TAG, "Attached OSD surface for vo=mediacodec")
}
private fun collectVideoDimensions(p: MpvPlayer) {
scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.observeInt("dwidth").collect { value ->
val w = value.toInt()
if (w > 0 && w != videoDisplayWidth) {
videoDisplayWidth = w
applyVideoRectLayout()
}
}
}
scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.observeInt("dheight").collect { value ->
val h = value.toInt()
if (h > 0 && h != videoDisplayHeight) {
videoDisplayHeight = h
applyVideoRectLayout()
}
}
}
}
/**
* Arbiter for this session's video output: the active [GpuVoPolicy] reasons
* decide whether the session runs on the video plane or on a GL vo.
*/
private fun setGpuVoRequirement(reason: String, active: Boolean) {
if (!usesMediaCodecVo || disposing) return
val transition = synchronized(gpuVoReasons) {
val changed = if (active) gpuVoReasons.add(reason) else gpuVoReasons.remove(reason)
if (!changed) return
val desired = GpuVoPolicy.targetFor(gpuVoReasons)
if (desired == activeGpuVoTarget) return
val line = "${activeGpuVoTarget ?: "mediacodec"} -> ${desired ?: "mediacodec"} " +
"(reasons=[${gpuVoReasons.joinToString(",")}])"
activeGpuVoTarget = desired
line
}
Log.i(TAG, "Video output: $transition")
applyGpuVoTarget()
}
/**
* Moves the session to whatever the arbiter last decided.
*
* The decision is atomic under [gpuVoReasons], but the write cannot be:
* it has to leave the lock to reach mpv. Reasons are raised from different
* threads — per-file DV routing runs on [mpvWriteDispatcher], the gamma,
* shader and chain-failure observers on the main thread — so the order
* writes are *enqueued* is not the order decisions were *made*. Rather
* than trust the target its caller saw, every transition re-reads the
* current one here, which makes the last write the right one under any
* interleaving. Serialized on [mpvWriteDispatcher], so the paired main
* thread work stays in the same order too.
*/
private fun applyGpuVoTarget() {
scope.launch(mpvWriteDispatcher, start = CoroutineStart.ATOMIC) {
val target = synchronized(gpuVoReasons) { activeGpuVoTarget }
runOnMain {
if (disposing) return@runOnMain
if (target == null) {
osdSurfaceView?.visibility = View.VISIBLE
} else {
// The gpu VOs draw their own OSD; a stale subtitle frame must not
// linger on the overlay plane.
osdSurfaceView?.visibility = View.GONE
resetVideoSurfaceToFullContainer()
}
}
try {
// Before the vo write, which recreates the VO: it reads the OSD
// surface option at creation.
val p = player
if (target == null && p != null) attachOsdSurfaceIfNeeded(p)
writeProperty("vo", target ?: "mediacodec")
if (p == null) return@launch
if (target != null) {
// A failed conversion chain makes mpv deselect the video track
// ("Could not initialize video chain" -> vid=no) before the VO
// switch lands. Re-select the explicit track id: mid-file "auto"
// resolves to no selection rather than re-running selection.
if (p.getString("vid").let { it == null || it == "no" }) {
val videoTrackId = videoTracks(p).firstOrNull()?.optLong("id")
if (videoTrackId != null) {
Log.i(TAG, "Re-selecting video track $videoTrackId after chain failure")
writeProperty("vid", videoTrackId.toString())
}
}
}
applySurfaceSizeInternal(p, force = true)
if (target == null) {
// Refit the surfaces to the aspect rectangle now that the plane
// owns scaling again (the gpu VOs letterboxed within full
// containers).
applyVideoRectLayout()
}
} catch (e: CancellationException) {
Log.d(TAG, "Canceled vo transition write")
} catch (e: Exception) {
Log.w(TAG, "Failed to move the video output to ${target ?: "mediacodec"}", e)
}
}
}
/**
* Per-file Dolby Vision routing, decided from the bitstream: mpv exports
* the DOVI configuration record's profile on the track list (never trust
* server metadata for this — it mis-tags DV routinely). Re-evaluated on
* every file-loaded, so a following non-P5 file restores hardware decode
* and returns to the video plane.
*/
private suspend fun applyDvReshapePolicy(p: MpvPlayer) {
val profile = selectedVideoDvProfile(p)
val needs = GpuVoPolicy.needsDvReshaping(
dvProfile = profile,
conversionMode = currentDvConversionMode,
canPlayP5Natively = DoviBridge.canPlayDolbyVisionP5(context)
)
if (needs == dvReshapeActive) return
dvReshapeActive = needs
if (needs) {
Log.i(TAG, "DV P5 (bitstream) without native support: software decode + gpu-next reshaping")
val current = p.getString("hwdec")
hwdecBeforeDvReshape.set(current ?: "no")
writeProperty("hwdec", "no")
} else {
val restore = hwdecBeforeDvReshape.getAndSet(null)
if (restore != null && restore != "no") writeProperty("hwdec", restore)
}
setGpuVoRequirement(GpuVoPolicy.REASON_DV_RESHAPE, needs)
}
/**
* Selected video track's Dolby Vision profile, or null for non-DV content
* (mpv omits the field when the bitstream carries no DOVI configuration
* record).
*/
private suspend fun selectedVideoDvProfile(p: MpvPlayer): Long? = videoTracks(p).firstOrNull()
?.takeIf { it.has("dolby-vision-profile") }
?.getLong("dolby-vision-profile")
/**
* Observed rather than derived from the hardware-decoding setting because
* the fallback is decided per file, inside mpv. Why it matters:
* [GpuVoPolicy.needsSoftwareRender].
*/
private fun collectDecoderState(p: MpvPlayer) {
scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.observeString("hwdec-current").collect { value ->
setGpuVoRequirement(GpuVoPolicy.REASON_SW_DECODE, GpuVoPolicy.needsSoftwareRender(value))
}
}
}
/**
* User shaders need a GL vo; the video plane renders none. Observed
* natively so Dart's `glsl-shaders` change-list writes (ShaderService,
* ambient lighting) switch the session live, without a channel contract.
*/
private fun collectShaderState(p: MpvPlayer) {
scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.observeString("glsl-shaders").collect { value ->
setGpuVoRequirement(GpuVoPolicy.REASON_SHADERS, value.isNotBlank())
}
}
}
/**
* Observed from video-params so the reason follows per-file transfer
* changes, and the display is re-queried per change so an HDMI mode switch
* mid-session is honoured on the next file. Why it matters:
* [GpuVoPolicy.needsHdrToneMapping].
*/
private fun collectHdrToneMapState(p: MpvPlayer) {
scope.launch(start = CoroutineStart.UNDISPATCHED) {
p.observeString("video-params/gamma").collect { value ->
val needsToneMap = GpuVoPolicy.needsHdrToneMapping(
gamma = value,
displaySupportsHdr = DoviBridge.displaySupportsHdr(context)
)
setGpuVoRequirement(GpuVoPolicy.REASON_HDR_SDR, needsToneMap)
}
}
}
/** Video tracks from mpv's track list, selected first; empty on any parse failure. */
private suspend fun videoTracks(p: MpvPlayer): List<org.json.JSONObject> {
val json = p.getString("track-list") ?: return emptyList()
return try {
val tracks = org.json.JSONArray(json)
(0 until tracks.length())
.map { tracks.getJSONObject(it) }
.filter { it.optString("type") == "video" }
.sortedByDescending { it.optBoolean("selected") }
} catch (e: Exception) {
Log.w(TAG, "Failed to parse track-list", e)
emptyList()
}
}
/**
* Sizes the video surface to the rectangle the image should occupy, per
* [VideoRectPolicy], and lets the container clip the overflow.
*
* The OSD surface is left full-container: the vo builds its `mp_osd_res`
* from the OSD window's own size, so libass keeps the whole window as its
* canvas — subtitles sit in the letterbox bars as they did under vo=gpu,
* and stay on screen when a zoomed image runs past the container.
*/
private fun applyVideoRectLayout() {
if (!usesMediaCodecVo || activeGpuVoTarget != null) return
runOnMain {
if (disposing) return@runOnMain
val container = surfaceContainer ?: return@runOnMain
val size = VideoRectPolicy.sizeFor(
containerWidth = container.width,
containerHeight = container.height,
videoWidth = videoDisplayWidth,
videoHeight = videoDisplayHeight,
panscan = videoPanscan,
videoZoomLog2 = videoZoomLog2
) ?: return@runOnMain
// The guard matters: this runs from an OnGlobalLayoutListener, so an
// unconditional write would re-trigger layout forever.
surfaceView?.let { view ->
val lp = view.layoutParams as android.widget.FrameLayout.LayoutParams
if (lp.width != size.width || lp.height != size.height || lp.gravity != android.view.Gravity.CENTER) {
lp.width = size.width
lp.height = size.height
lp.gravity = android.view.Gravity.CENTER
view.layoutParams = lp
}
}
}
}
private fun resetVideoSurfaceToFullContainer() {
val view = surfaceView ?: return
val lp = view.layoutParams as android.widget.FrameLayout.LayoutParams
if (lp.width == android.widget.FrameLayout.LayoutParams.MATCH_PARENT) return
lp.width = android.widget.FrameLayout.LayoutParams.MATCH_PARENT
lp.height = android.widget.FrameLayout.LayoutParams.MATCH_PARENT
lp.gravity = android.view.Gravity.NO_GRAVITY
view.layoutParams = lp
}
private fun rememberSurfaceSize(width: Int, height: Int) {
if (width <= 0 || height <= 0) return
lastKnownSurfaceWidth = width
@@ -559,10 +1001,15 @@ class MpvPlayerCore private constructor(
return@withLock
}
val needsAttach = !hasAttachedSurface || attachedSurface !== surface
// An unattached OSD Surface forces a wid re-attach: the VO reads the
// OSD surface option at creation, and setting wid recreates the VO.
// Suppressed off the plane, where the GL vo draws its own OSD.
val osdNeedsAttach = activeGpuVoTarget == null && pendingOsdSurface !== attachedOsdSurface
val needsAttach = !hasAttachedSurface || attachedSurface !== surface || osdNeedsAttach
val wasAttachedToPlaceholder = attachedToPlaceholder
val wasPausedForSurfaceLoss = pausedForSurfaceLoss
if (needsAttach) {
attachOsdSurfaceIfNeeded(p)
p.attachSurface(surface)
attachedSurface = surface
hasAttachedSurface = true
@@ -880,12 +1327,136 @@ class MpvPlayerCore private constructor(
Log.d(TAG, "Load pause intent updated: paused=$paused")
}
/**
* `dv-conversion-mode` is an app-level property shared with the ExoPlayer
* and Apple cores, not an mpv one. It maps onto the fork FFmpeg
* hevc_mediacodec decoder options, mirroring the ExoPlayer DoviBridge
* decision tree. Single-layer profiles (5/8) use the Dolby Vision decoder
* whenever the path is enabled and the decoder advertises the profile.
*/
private fun applyDvConversionMode(value: String, onComplete: ((Result<Unit>) -> Unit)?) {
val displayDv = DoviBridge.displaySupportsDolbyVision(context)
val (dolbyVision, p7Mode) = when (value.trim().lowercase()) {
"auto" -> if (displayDv) "1" to "auto" else "0" to "strip"
"disabled", "native" -> "1" to "native"
"dv81" -> "1" to "convert"
"hevc", "hevc_strip" -> "1" to "strip"
else -> {
onComplete?.invoke(Result.failure(IllegalArgumentException("Invalid DV conversion mode: $value")))
return
}
}
currentDvConversionMode = value.trim().lowercase()
Log.i(TAG, "DV conversion mode '$value' (displayDV=$displayDv) -> dolby_vision=$dolbyVision dv_p7_mode=$p7Mode")
scope.launch(mpvWriteDispatcher, start = CoroutineStart.ATOMIC) {
val completion = try {
val ours = "dolby_vision=$dolbyVision,dv_p7_mode=$p7Mode"
val merged = mergeDecoderOptions(player?.getString("vd-lavc-o"), ours)
writeProperty("vd-lavc-o", merged)
Result.success(Unit)
} catch (error: CancellationException) {
Result.failure(error)
} catch (error: Exception) {
Log.w(TAG, "DV conversion mode write failed")
Result.failure(error)
}
withContext(NonCancellable + Dispatchers.Main) {
onComplete?.invoke(completion)
}
}
}
/**
* `content-color-transfer` is an app-level property: Dart announces the
* selected stream's transfer (server metadata) before playback so an HDR
* session can get a BT.2020 PQ 10-bit GL surface instead of tone-mapped
* SDR. Consumed by whichever android GL context the session ever creates -
* up front for a software session, or at the fallback transition when a
* plane session leaves vo=mediacodec (the plane itself carries HDR via the
* decoder's dataspace and ignores all of this).
*
* The first announcement decides for the whole core: the surface colorspace
* is fixed at EGL-surface creation, and both latched states stay correct
* for later files (a PQ target renders SDR content correctly, an sRGB
* surface tone-maps HDR as before) - re-deciding mid-session could pair a
* live sRGB surface with a PQ render target, which is wrong everywhere.
*/
private fun applyContentColorTransfer(value: String, onComplete: ((Result<Unit>) -> Unit)?) {
val transfer = value.trim().lowercase()
if (hdrSurfaceDecided) {
onComplete?.invoke(Result.success(Unit))
return
}
hdrSurfaceDecided = true
val wants = wantsHdrSurface(transfer)
val displayHdr = wants && DoviBridge.displaySupportsHdr(context)
// Independent of the GL surface outcome: on the MediaCodec plane the
// decoder's dataspace carries HDR to the display without a PQ GL surface.
hdrDisplayActive = displayHdr
val outputFormat = if (wants) EglHdrCaps.pqOutputFormat() else null
if (!wants || !displayHdr || outputFormat == null) {
if (wants) {
Log.i(TAG, "HDR GL surface unavailable (transfer=$transfer displayHdr=$displayHdr eglFormat=$outputFormat)")
}
onComplete?.invoke(Result.success(Unit))
return
}
Log.i(TAG, "HDR GL surface engaged: BT.2020 PQ / $outputFormat for transfer=$transfer")
scope.launch(mpvWriteDispatcher, start = CoroutineStart.ATOMIC) {
val completion = try {
writeProperty("android-surface-colorspace", "bt2020-pq")
writeProperty("egl-output-format", outputFormat)
writeProperty("target-trc", "pq")
writeProperty("target-prim", "bt.2020")
Result.success(Unit)
} catch (error: CancellationException) {
Result.failure(error)
} catch (error: Exception) {
Log.w(TAG, "HDR surface property write failed")
Result.failure(error)
}
withContext(NonCancellable + Dispatchers.Main) {
onComplete?.invoke(completion)
}
}
}
fun setProperty(name: String, value: String, onComplete: ((Result<Unit>) -> Unit)? = null) {
if (!isInitialized || disposing || !scope.isActive) {
onComplete?.invoke(Result.failure(CancellationException("MPV core unavailable")))
return
}
if (name == "dv-conversion-mode") {
applyDvConversionMode(value, onComplete)
return
}
if (name == "content-color-transfer") {
applyContentColorTransfer(value, onComplete)
return
}
// View geometry on the plane (see VideoRectPolicy), but both still fall
// through to mpv, which is what makes them work unchanged on the GL vos.
if (name == "panscan" || name == "video-zoom") {
val parsed = value.toFloatOrNull()
if (parsed != null) {
if (name == "panscan") videoPanscan = parsed else videoZoomLog2 = parsed
applyVideoRectLayout()
}
}
// While the per-file DV policy holds hwdec at `no`, park writes instead
// of applying them: a hardware value under gpu-next would lose the RPU
// side data (and blue-screen the Tegra class, #2010). The parked value
// is restored when a non-P5 file drops the requirement.
if (name == "hwdec" && dvReshapeActive) {
hwdecBeforeDvReshape.set(value)
onComplete?.invoke(Result.success(Unit))
return
}
val paused = if (name == "pause") normalizePauseValue(value) else null
val pauseIntent = paused?.let {
synchronized(publicPauseIntentLock) {
@@ -1223,7 +1794,7 @@ class MpvPlayerCore private constructor(
}
override fun clearVideoFrameRate() {
frameRateManager?.clearVideoFrameRate()
frameRateManager?.clearVideoFrameRate(hdrActive = hdrDisplayActive)
}
// Cleanup
@@ -1274,11 +1845,15 @@ class MpvPlayerCore private constructor(
// Capture locals for deferred cleanup (audio-only has no views)
val sv = surfaceView
val osdSv = osdSurfaceView
val container = surfaceContainer
val contentView = if (audioOnly) null else activity.findViewById<ViewGroup>(android.R.id.content)
surfaceContainer = null
surfaceView = null
osdSurfaceView = null
pendingOsdSurface = null
attachedOsdSurface = null
// Remove layout listener synchronously
overlayLayoutListener?.let { listener ->
@@ -1330,6 +1905,7 @@ class MpvPlayerCore private constructor(
Log.d(TAG, "Disposed (native)")
Handler(Looper.getMainLooper()).post {
sv?.holder?.removeCallback(this)
osdSv?.holder?.removeCallback(osdSurfaceCallback)
if (container?.parent != null) {
contentView?.removeView(container)
}
@@ -1340,6 +1916,7 @@ class MpvPlayerCore private constructor(
// No player — safe to remove views immediately
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
sv?.holder?.removeCallback(this)
osdSv?.holder?.removeCallback(osdSurfaceCallback)
if (container?.parent != null) {
contentView?.removeView(container)
}
@@ -1,6 +1,7 @@
package com.edde746.plezy.mpv
import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.net.Uri
import android.os.ParcelFileDescriptor
@@ -14,6 +15,7 @@ import io.flutter.plugin.common.EventChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import java.util.concurrent.CancellationException
import java.util.concurrent.atomic.AtomicBoolean
internal fun completeMpvPropertyResult(
result: MethodChannel.Result,
@@ -62,6 +64,17 @@ open class MpvPlayerPlugin(
private val nameToId = mutableMapOf<String, Int>()
private var sessionGeneration = 0
// The Dart instanceId that created the current core. A `dispose` carrying a
// different token lost the ownership race to a successor and must not tear
// down that successor's core; it is acknowledged without touching anything.
private var coreInstanceId: Long? = null
// How long a Dart `dispose` waits for the native teardown before being
// answered anyway. Generous against slow-but-healthy teardowns (a 4K HDR
// session's surface/audio release); small against the alternative, which
// is wedging every subsequent playback session behind a hung teardown.
private val disposeWatchdogMs = 6_000L
/** Same semantics as Activity.runOnUiThread, without needing an Activity. */
private fun runOnMain(block: () -> Unit) = channels.runOnMain(block)
@@ -95,6 +108,7 @@ open class MpvPlayerPlugin(
++sessionGeneration
val core = playerCore
playerCore = null
coreInstanceId = null
cancelPendingInits()
return core
}
@@ -149,7 +163,7 @@ open class MpvPlayerPlugin(
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"initialize" -> handleInitialize(call, result)
"dispose" -> handleDispose(result)
"dispose" -> handleDispose(call, result)
"setProperty" -> handleSetProperty(call, result)
"getProperty" -> handleGetProperty(call, result)
"getStats" -> handleGetStats(result)
@@ -164,6 +178,13 @@ open class MpvPlayerPlugin(
"abandonAudioFocus" -> handleAbandonAudioFocus(result)
"openContentFd" -> handleOpenContentFd(call, result)
"closeContentFd" -> handleCloseContentFd(call, result)
"getHeapSize" -> {
// Device heap class for Dart-side memory tiering (stream ring cache).
// Lives on the always-registered mpv channel so it survives backends.
val context: Context? = activity ?: applicationContext
val am = context?.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager
result.success(am?.largeMemoryClass ?: 0)
}
"isInitialized" -> result.success(playerCore?.isInitialized ?: false)
"setLogLevel" -> handleSetLogLevel(call, result)
else -> result.notImplemented()
@@ -247,6 +268,7 @@ open class MpvPlayerPlugin(
delegate = this@MpvPlayerPlugin
}
playerCore = core
coreInstanceId = call.argument<Number>("instanceId")?.toLong()
} catch (e: Exception) {
Log.e(tag, "Failed to initialize: ${e.message}", e)
completePendingInits(attempt, success = false, errorMessage = e.message)
@@ -258,7 +280,10 @@ open class MpvPlayerPlugin(
playerCore !== core ||
!isCurrentInitAttempt(attempt)
if (stale || !success) {
if (playerCore === core) playerCore = null
if (playerCore === core) {
playerCore = null
coreInstanceId = null
}
core.dispose()
if (stale) {
Log.d(tag, "Stale init callback (gen=$gen, current=$sessionGeneration)")
@@ -315,13 +340,37 @@ open class MpvPlayerPlugin(
}
}
private fun handleDispose(result: MethodChannel.Result) {
private fun handleDispose(call: MethodCall, result: MethodChannel.Result) {
val token = call.argument<Number>("instanceId")?.toLong()
runOnMain {
val core = takeCoreForTeardown()
core?.dispose {
Log.d(tag, "Disposed")
val owner = coreInstanceId
if (playerCore != null && token != null && owner != null && token != owner) {
// This dispose lost the ownership race: a successor already created
// the current core. Acknowledge without touching it.
Log.d(tag, "Ignoring stale dispose (token=$token, core owner=$owner)")
result.success(null)
} ?: result.success(null)
return@runOnMain
}
val core = takeCoreForTeardown()
if (core == null) {
result.success(null)
return@runOnMain
}
// A hung native teardown must not wedge the Dart-side release chain:
// answer after the watchdog even if the teardown thread is stuck, so
// the next session can start on a fresh core. The stuck core leaks its
// resources until the process ends — recoverable, unlike the wedge.
val completed = AtomicBoolean(false)
fun completeOnce(reason: String) {
if (completed.compareAndSet(false, true)) {
Log.d(tag, reason)
result.success(null)
}
}
channels.mainHandler.postDelayed({
completeOnce("Dispose watchdog fired after ${disposeWatchdogMs}ms; teardown continues in background")
}, disposeWatchdogMs)
core.dispose { completeOnce("Disposed") }
}
}
@@ -0,0 +1,53 @@
package com.edde746.plezy.mpv
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow
/**
* Pure geometry for the video plane's surface rectangle.
*
* The fork `vo=mediacodec` scales decoded buffers to the whole Surface and
* implements none of mpv's src/dst rect math, so aspect, cover and zoom are
* view geometry: the video surface is sized to the rectangle the image should
* occupy and the container clips whatever falls outside it. That mirrors the
* ExoPlayer path, which scales its own surface and keeps the subtitle overlay
* on the full-size container.
*/
internal object VideoRectPolicy {
/**
* `video-zoom` is reachable from the user's mpv.conf, where an absurd value
* is a compositor allocation rather than just arithmetic. Bounds it well
* outside the range the player's own zoom control offers.
*/
private const val MAX_ZOOM_LOG2 = 2f
data class Size(val width: Int, val height: Int)
/**
* Size for the video surface, or null when a dimension is not known yet.
*
* [panscan] is mpv's 0..1 property: 0 fits the image inside the container
* (letterbox), 1 fills it (crop), values between interpolate the scale.
* [videoZoomLog2] is mpv's `video-zoom`, a log2 factor, applied on top.
*/
fun sizeFor(
containerWidth: Int,
containerHeight: Int,
videoWidth: Int,
videoHeight: Int,
panscan: Float = 0f,
videoZoomLog2: Float = 0f
): Size? {
if (containerWidth <= 0 || containerHeight <= 0 || videoWidth <= 0 || videoHeight <= 0) return null
val fit = min(containerWidth.toFloat() / videoWidth, containerHeight.toFloat() / videoHeight)
val cover = max(containerWidth.toFloat() / videoWidth, containerHeight.toFloat() / videoHeight)
val pan = panscan.coerceIn(0f, 1f)
val zoom = 2.0f.pow(videoZoomLog2.coerceIn(-MAX_ZOOM_LOG2, MAX_ZOOM_LOG2))
val scale = (fit + (cover - fit) * pan) * zoom
return Size(
width = (videoWidth * scale).toInt().coerceAtLeast(1),
height = (videoHeight * scale).toInt().coerceAtLeast(1)
)
}
}
@@ -19,6 +19,15 @@ class FrameRateManager(
private const val TAG = "FrameRateManager"
private const val DISPLAY_SETTLE_MS = 2000L
private const val WATCHDOG_MARGIN_MS = 3000L
// How long to let the HDR-exit commit land before restoring the refresh
// rate. Restoring while the display is still signaling HDR folds the
// HDR-exit and the mode change into one HDMI renegotiation, which some
// sink chains take 8-30 s to complete (#2172); sequenced, the HDR
// infoframe clear is free and the SDR mode switch takes ~1 s. The
// player's surface teardown commits the HDR exit within ~50 ms of
// dispose, so 400 ms covers it with margin even on a busy main thread.
private const val HDR_EXIT_SETTLE_MS = 400L
}
private var currentVideoFps: Float = 0f
@@ -30,6 +39,12 @@ class FrameRateManager(
private var watchdogRunnable: Runnable? = null
private var pendingCompletion: ((switched: Boolean) -> Unit)? = null
// Owns the deferred HDR-exit restore. Deliberately NOT the shared player
// [handler]: core dispose clears that one wholesale, and the restore must
// survive player disposal or the display stays at the content rate.
private val restoreHandler = Handler(android.os.Looper.getMainLooper())
private var pendingRestoreRunnable: Runnable? = null
private fun getDisplayManager(): DisplayManager = activity.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
// Request a display mode switch for the video's frame rate and/or, with
@@ -54,6 +69,9 @@ class FrameRateManager(
matchResolution: Boolean = false,
onComplete: (switched: Boolean) -> Unit
) {
// A new session's switch must not be clobbered by a still-pending
// deferred restore from the previous session's teardown.
cancelPendingRestore()
currentVideoFps = fps
currentVideoWidth = videoWidth
currentVideoHeight = videoHeight
@@ -78,21 +96,47 @@ class FrameRateManager(
}
}
fun clearVideoFrameRate() {
Log.d(TAG, "clearVideoFrameRate")
// [hdrActive]: the session was outputting HDR. The restore is then deferred
// by [HDR_EXIT_SETTLE_MS] so the caller's surface teardown can commit the
// HDR exit first — see [HDR_EXIT_SETTLE_MS] for why stacking them is slow.
fun clearVideoFrameRate(hdrActive: Boolean = false) {
Log.d(TAG, "clearVideoFrameRate(hdrActive=$hdrActive)")
currentVideoFps = 0f
// Resolve any pending setVideoFrameRate future as "not switched" so
// the Dart caller's await doesn't hang on player dispose.
firePendingCompletion("clear", switched = false)
// Restore default display mode on API M (preferredDisplayModeId persists)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window?.attributes?.let { attrs ->
attrs.preferredDisplayModeId = 0
activity.window?.attributes = attrs
cancelPendingRestore()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return
// Nothing to restore when no preferred mode was ever applied.
if ((activity.window?.attributes?.preferredDisplayModeId ?: 0) == 0) return
if (hdrActive) {
val restore = Runnable {
pendingRestoreRunnable = null
// Log.d, not [log]: this fires after core dispose, when the
// Flutter-channel logger is already gone.
Log.d(TAG, "restoring default display mode after HDR exit")
restorePreferredDisplayMode()
}
pendingRestoreRunnable = restore
restoreHandler.postDelayed(restore, HDR_EXIT_SETTLE_MS)
} else {
restorePreferredDisplayMode()
}
}
private fun restorePreferredDisplayMode() {
// preferredDisplayModeId persists on the window; restore the default.
activity.window?.attributes?.let { attrs ->
attrs.preferredDisplayModeId = 0
activity.window?.attributes = attrs
}
}
private fun cancelPendingRestore() {
pendingRestoreRunnable?.let { restoreHandler.removeCallbacks(it) }
pendingRestoreRunnable = null
}
// Release pending callbacks/listener without restoring the display mode.
// Used by player-core dispose paths so a backend handoff (e.g. ExoPlayer→MPV
// audio fallback) doesn't clobber the just-applied refresh-rate switch —
@@ -7,6 +7,34 @@ import java.util.Locale
/** Canonical decoder lookup and hardware classification for native playback. */
internal object MediaCodecQuery {
/**
* Codecs whose advertised support is decided by hardware: both have a
* software decoder behind them, but a software HEVC or AV1 decode on
* phone/TV-class hardware drops frames.
*/
private val HARDWARE_GATED_VIDEO_MIME_TYPES = mapOf(
"hevc" to "video/hevc",
"av1" to "video/av01"
)
/** Codec name -> whether this device has a hardware decoder for it. */
fun hardwareVideoDecodeSupport(
hardwareMimeTypes: Set<String> = hardwareDecoderMimeTypes()
): Map<String, Boolean> = HARDWARE_GATED_VIDEO_MIME_TYPES.mapValues { (_, mimeType) -> mimeType in hardwareMimeTypes }
/**
* Every MIME type served by a hardware decoder, lowercased. One walk answers
* for all codecs, unlike [findHardwareDecoder], which rescans per lookup.
*/
private fun hardwareDecoderMimeTypes(): Set<String> {
val mimeTypes = HashSet<String>()
for (info in MediaCodecList(MediaCodecList.REGULAR_CODECS).codecInfos) {
if (info.isEncoder || !isHardwareAccelerated(info)) continue
for (type in info.supportedTypes) mimeTypes.add(type.lowercase(Locale.ROOT))
}
return mimeTypes
}
fun findHardwareDecoder(
mimeType: String,
codecKind: Int = MediaCodecList.REGULAR_CODECS,
@@ -2,6 +2,7 @@ package com.edde746.plezy.shared
import android.app.Activity
import android.graphics.Color
import android.graphics.PixelFormat
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.ViewGroup
@@ -14,8 +15,37 @@ internal object PlayerSurfaceHost {
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
// Fallback fill for the frames before the punch surface below is placed.
setBackgroundColor(Color.BLACK)
this.clipChildren = clipChildren
addView(createLetterboxPunchSurface(activity))
}
/**
* Fullscreen, buffer-less SurfaceView kept beneath the video surface.
*
* Its only job is taking the letterbox area off the app-window (graphics)
* plane: like any below-window SurfaceView it punches the parent canvas and
* registers its rect as a window transparent region, so the pixels around
* the video rect scan out as the SurfaceFlinger backdrop instead of
* window-plane black. Several TV compositors (Fire TV Stick 4K Max, some
* Sony/Philips models) raise SDR graphics-plane black while the display is
* in HDR/Dolby Vision mode, which shows as gray letterbox bars on OLED
* panels (issue #2163; same mechanism as ExoPlayer #8803 and Kodi #25300).
*
* No buffer is ever posted to it: SurfaceFlinger skips buffer-less layers,
* and drawing black into it would put the bars back onto an SDR layer —
* exactly the plane being avoided. Views drawn on the parent canvas after
* the punch (Media3 subtitle cues) still re-claim their own bounds.
*/
private fun createLetterboxPunchSurface(activity: Activity): SurfaceView = SurfaceView(activity).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
setZOrderOnTop(false)
setZOrderMediaOverlay(false)
FlutterOverlayHelper.applyCompositionOrder(this, -2)
}
fun createVideoSurface(activity: Activity, callback: SurfaceHolder.Callback): SurfaceView = SurfaceView(activity).apply {
@@ -29,6 +59,23 @@ internal object PlayerSurfaceHost {
FlutterOverlayHelper.applyCompositionOrder(this, -2)
}
/**
* Transparent plane directly above the video surface for the mpv
* `vo=mediacodec` subtitle/OSD output. Media-overlay z-order keeps it above
* the video SurfaceView but still beneath the Flutter window content.
*/
fun createOsdSurface(activity: Activity, callback: SurfaceHolder.Callback): SurfaceView = SurfaceView(activity).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
holder.addCallback(callback)
holder.setFormat(PixelFormat.TRANSLUCENT)
setZOrderOnTop(false)
setZOrderMediaOverlay(true)
FlutterOverlayHelper.applyCompositionOrder(this, -1)
}
fun attachToContent(activity: Activity, container: FrameLayout): ViewGroup {
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
contentView.addView(container, 0)
@@ -94,24 +94,111 @@ class AudioOutputPolicyTest {
}
@Test
fun spdifListNamesOnlyCodecsMpvsStereoIecTrackCanCarry() {
// eac3 (a 192kHz burst), truehd and dts-hd (192kHz/8ch) never survive mpv's audiotrack
// AO, which opens every spdif format as a stereo IEC 61937 track at the mixer rate;
// naming them strands playback on a dead audio output (#1991).
assertEquals("ac3,dts", mpvSpdifCodecs { true })
fun spdifListNamesEveryCodecARouteWithAllShapesCanCarry() {
// libmpv v1.1.0's audiotrack AO opens each burst at its own rate and channel mask, so a route
// that takes all three shapes and advertises everything bitstreams the lossless codecs too.
// `dts-hd` supersedes plain `dts`: ad_spdif picks the core burst per file for non-HD tracks.
assertEquals("ac3,eac3,truehd,dts-hd", spdifCodecs(allEncodings, allShapes))
}
@Test
fun spdifListOnAStereo48kOnlyRouteNamesTheCoreCodecsOnly() {
// E-AC3 (192kHz), TrueHD MAT and DTS-HD MA (192kHz/8ch) have no track to ride here.
assertEquals("ac3,dts", spdifCodecs(allEncodings, setOf(MpvIecShape.STEREO_48K)))
}
@Test
fun dtsHdFallsBackToThePlainCoreWithoutTheCarrierShape() {
// Advertising ENCODING_DTS_HD says the receiver decodes it, not that the route takes the
// 192kHz/8ch track its burst needs (#1988); naming `dts-hd` anyway strands playback.
assertEquals(
"dts",
spdifCodecs(setOf(C.ENCODING_DTS, C.ENCODING_DTS_HD), setOf(MpvIecShape.STEREO_48K))
)
}
@Test
fun eac3IsNotNamedWithoutThe192kStereoShape() {
assertEquals("", spdifCodecs(setOf(C.ENCODING_E_AC3), setOf(MpvIecShape.STEREO_48K)))
}
@Test
fun trueHdIsNotNamedWithoutTheCarrierShape() {
assertEquals(
"",
spdifCodecs(
setOf(C.ENCODING_DOLBY_TRUEHD),
setOf(MpvIecShape.STEREO_48K, MpvIecShape.STEREO_192K)
)
)
}
@Test
fun plainDtsSurvivesWhenTheRouteDoesNotAdvertiseDtsHd() {
// The core burst is stereo/48k, so it rides a full-shape route unchanged when only the
// lossless encoding is missing.
assertEquals("dts", spdifCodecs(setOf(C.ENCODING_DTS), allShapes))
}
@Test
fun spdifListDropsCodecsTheRouteCannotBitstream() {
// Google TV Streamer over HDMI to a Dolby-only sink: AC3 bitstreams, DTS does not (#1703).
// Google TV Streamer over HDMI to a Dolby-only sink: AC3/E-AC3 bitstream, DTS does not (#1703).
val dolbyOnlyRoute = setOf(C.ENCODING_AC3, C.ENCODING_E_AC3)
assertEquals("ac3", mpvSpdifCodecs { encoding -> encoding in dolbyOnlyRoute })
assertEquals("ac3,eac3", spdifCodecs(dolbyOnlyRoute, allShapes))
}
@Test
fun spdifListIsEmptyForPcmOnlyRoutes() {
assertEquals("", mpvSpdifCodecs { false })
assertEquals("", mpvSpdifCodecs({ false }, { false }))
}
@Test
fun spdifListIsEmptyWhenTheRouteTakesNoIecTrackAtAll() {
// Every encoding advertised, but no raw track and no IEC 61937 shape opens: mpv has no
// decode fallback for a named codec, so nothing may be named (#1991).
assertEquals("", spdifCodecs(allEncodings, emptySet()))
}
@Test
fun rawCapableRouteBitstreamsTheCoreCodecsWithoutAnyIecShape() {
// #2177's Shield: every mpv IEC track opens and drains into silence, while raw
// ENCODING_AC3/E_AC3/DTS tracks (the ExoPlayer transport) play. The AO opens raw first,
// so raw support alone must qualify the core codecs.
assertEquals(
"ac3,eac3,dts",
spdifCodecs(allEncodings, emptySet(), raw = setOf(C.ENCODING_AC3, C.ENCODING_E_AC3, C.ENCODING_DTS))
)
}
@Test
fun rawSupportNeverQualifiesTheLosslessCodecs() {
// TrueHD and DTS-HD MA have no raw transport in the AO; they ride the 192kHz/7.1 IEC
// carrier or decode. A route that takes every raw track but no carrier must not name them.
assertEquals("ac3,eac3,dts", spdifCodecs(allEncodings, emptySet(), raw = allEncodings))
}
@Test
fun rawProbeIsOnlyConsultedForRawCandidates() {
// The raw probe costs real route calls; the carrier-only codecs must never trigger it.
val codecs = mpvSpdifCodecs(
{ true },
{ true },
{ encoding ->
if (encoding == C.ENCODING_DOLBY_TRUEHD || encoding == C.ENCODING_DTS_HD) {
throw AssertionError("raw probe consulted for a carrier-only codec")
}
true
}
)
assertEquals("ac3,eac3,truehd,dts-hd", codecs)
}
@Test
fun dtsHdStillSupersedesPlainDtsWhenDtsBitstreamsRaw() {
// dts-hd selects the lossless spdif decoder for the whole dts codec; the raw core track
// must not resurrect the plain name beside it.
assertEquals("ac3,eac3,truehd,dts-hd", spdifCodecs(allEncodings, allShapes, raw = allEncodings))
}
@Test
@@ -204,4 +291,18 @@ class AudioOutputPolicyTest {
)
}
}
/** Every encoding the spdif table can ask for, i.e. a receiver that decodes all of them. */
private val allEncodings = setOf(
C.ENCODING_AC3,
C.ENCODING_E_AC3,
C.ENCODING_DOLBY_TRUEHD,
C.ENCODING_DTS,
C.ENCODING_DTS_HD
)
private val allShapes = MpvIecShape.values().toSet()
private fun spdifCodecs(encodings: Set<Int>, shapes: Set<MpvIecShape>, raw: Set<Int> = emptySet()): String =
mpvSpdifCodecs({ it in encodings }, { it in shapes }, { it in raw })
}
@@ -1,29 +0,0 @@
package com.edde746.plezy.exoplayer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class FfmpegDemuxerPolicyTest {
@Test
fun wireValuesRoundTrip() {
assertEquals(FfmpegDemuxerPolicy.Preference.FFMPEG, FfmpegDemuxerPolicy.fromWire("ffmpeg"))
assertEquals(FfmpegDemuxerPolicy.Preference.MEDIA3_ONLY, FfmpegDemuxerPolicy.fromWire("media3"))
}
@Test
fun unknownWireValuesResolveToFfmpeg() {
assertEquals(FfmpegDemuxerPolicy.Preference.FFMPEG, FfmpegDemuxerPolicy.fromWire(null))
assertEquals(FfmpegDemuxerPolicy.Preference.FFMPEG, FfmpegDemuxerPolicy.fromWire(""))
assertEquals(FfmpegDemuxerPolicy.Preference.FFMPEG, FfmpegDemuxerPolicy.fromWire("bogus"))
// Retired wire value from the container-list era maps to the default.
assertEquals(FfmpegDemuxerPolicy.Preference.FFMPEG, FfmpegDemuxerPolicy.fromWire("auto"))
}
@Test
fun ffmpegDemuxesByDefaultAndMedia3OnlyDisables() {
assertTrue(FfmpegDemuxerPolicy.enabled(FfmpegDemuxerPolicy.Preference.FFMPEG))
assertFalse(FfmpegDemuxerPolicy.enabled(FfmpegDemuxerPolicy.Preference.MEDIA3_ONLY))
}
}
@@ -1,53 +0,0 @@
package com.edde746.plezy.exoplayer
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Byte-exact spec for the media3-shaped subtitle sample prefixes. The shapes
* mirror media3 1.11.0 MatroskaExtractor's SSA_PREFIX/SUBRIP_PREFIX/VTT_PREFIX
* with the duration patched into the second timecode; SsaParser,
* SubripParser, WebvttParser, and AssTrackOutput all parse these exact bytes.
*/
class FfmpegSubtitleSamplesTest {
private fun string(bytes: ByteArray): String = bytes.toString(Charsets.UTF_8)
@Test
fun ssaPrefixMatchesMatroskaExtractorShape() {
// 1h 2m 3s 450ms -> centiseconds, ':' separator, single-digit hour.
assertEquals(
"Dialogue: 0:00:00:00,1:02:03:45,",
string(FfmpegSubtitleSamples.ssaPrefix(3_723_450_000L))
)
assertEquals("Dialogue: 0:00:00:00,0:00:05:00,", string(FfmpegSubtitleSamples.ssaPrefix(5_000_000L)))
// Sub-centisecond remainders truncate, matching integer division.
assertEquals("Dialogue: 0:00:00:00,0:00:00:99,", string(FfmpegSubtitleSamples.ssaPrefix(999_999L)))
}
@Test
fun subripPrefixIsACompleteCueHeader() {
assertEquals(
"1\n00:00:00,000 --> 01:02:03,450\n",
string(FfmpegSubtitleSamples.subripPrefix(3_723_450_000L))
)
assertEquals("1\n00:00:00,000 --> 00:00:05,000\n", string(FfmpegSubtitleSamples.subripPrefix(5_000_000L)))
}
@Test
fun vttPrefixIsAOneCueDocumentHeader() {
assertEquals(
"WEBVTT\n\n00:00:00.000 --> 01:02:03.450\n",
string(FfmpegSubtitleSamples.vttPrefix(3_723_450_000L))
)
assertEquals("WEBVTT\n\n00:00:00.000 --> 00:00:05.000\n", string(FfmpegSubtitleSamples.vttPrefix(5_000_000L)))
}
@Test
fun ssaDialogueFormatMatchesMedia3Constant() {
assertEquals(
"Format: Start, End, ReadOrder, Layer, Style, Name, MarginL, MarginR, MarginV, Effect, Text",
string(FfmpegSubtitleSamples.SSA_DIALOGUE_FORMAT)
)
}
}
@@ -0,0 +1,154 @@
package com.edde746.plezy.exoplayer
import androidx.media3.common.C
import androidx.media3.common.DataReader
import androidx.media3.common.Format
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.DefaultExtractorInput
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.parser.AssSubtitleParserFactory
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/** Regression coverage for Matroska files whose SeekHead references Tracks after the Clusters. */
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class MatroskaLateTracksTest {
private class CapturedTrack(val type: Int) : TrackOutput {
val timesUs = mutableListOf<Long>()
var format: Format? = null
val sampleCount: Int
get() = timesUs.size
override fun format(format: Format) {
this.format = format
}
override fun sampleData(input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int): Int = input.read(ByteArray(length), 0, length)
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
data.skipBytes(length)
}
override fun sampleMetadata(timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?) {
timesUs.add(timeUs)
}
}
private class CapturingExtractorOutput : ExtractorOutput {
val tracks = mutableMapOf<Int, CapturedTrack>()
val seekMaps = mutableListOf<SeekMap>()
override fun track(id: Int, type: Int): TrackOutput = tracks.getOrPut(id) { CapturedTrack(type) }
override fun endTracks() = Unit
override fun seekMap(seekMap: SeekMap) {
seekMaps.add(seekMap)
}
}
private class ByteArrayDataReader(private val data: ByteArray) : DataReader {
var position = 0L
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (position >= data.size) return C.RESULT_END_OF_INPUT
val bytesRead = minOf(length, data.size - position.toInt())
data.copyInto(buffer, offset, position.toInt(), position.toInt() + bytesRead)
position += bytesRead
return bytesRead
}
}
private fun fixtureData(): ByteArray = checkNotNull(javaClass.getResourceAsStream("/matroska_tracks_at_end.mkv")) {
"fixture matroska_tracks_at_end.mkv missing from test resources"
}.use { it.readBytes() }
private fun readToEnd(extractor: Extractor, reader: ByteArrayDataReader, length: Long) {
var input: ExtractorInput = DefaultExtractorInput(reader, reader.position, length)
val seekPosition = PositionHolder()
repeat(100_000) {
when (extractor.read(input, seekPosition)) {
Extractor.RESULT_END_OF_INPUT -> return
Extractor.RESULT_SEEK -> {
reader.position = seekPosition.position
input = DefaultExtractorInput(reader, seekPosition.position, length)
}
}
}
error("extractor did not reach end of input")
}
@Test
fun extractsSamplesWhenSeekHeadReferencesTracksAfterClusters() {
val assHandler = AssHandler()
val extractor = ZlibMatroskaExtractor(AssSubtitleParserFactory(assHandler), assHandler)
val output = CapturingExtractorOutput()
extractor.init(output)
val data = fixtureData()
readToEnd(extractor, ByteArrayDataReader(data), data.size.toLong())
assertEquals(2, output.tracks.size)
assertEquals(setOf(C.TRACK_TYPE_VIDEO, C.TRACK_TYPE_AUDIO), output.tracks.values.map { it.type }.toSet())
output.tracks.values.forEach { track ->
assertNotNull(track.format)
assertTrue("expected extracted samples for track type ${track.type}", track.sampleCount > 0)
}
}
/**
* media3 1.11.0 builds the Matroska seek map at the end of the Cues element, which for
* tracks-after-clusters files is before the Tracks element parsed — the map then permanently
* reports unseekable and resolves every seek to byte 0, snapping playback to the start
* (#1969; upstream androidx/media #3377).
* The production stack must repair it through the per-track cue lookups.
*/
@Test
fun seeksViaCuesWhenSeekMapIsBuiltBeforeTracks() {
val assHandler = AssHandler()
val extractor = CuelessSeekExtractorWrapper(ZlibMatroskaExtractor(AssSubtitleParserFactory(assHandler), assHandler))
val output = CapturingExtractorOutput()
extractor.init(output)
val data = fixtureData()
val reader = ByteArrayDataReader(data)
readToEnd(extractor, reader, data.size.toLong())
assertEquals(1, output.seekMaps.size)
val seekMap = output.seekMaps.single()
assertTrue("tracks-after-clusters file with Cues must be seekable", seekMap.isSeekable)
val points = seekMap.getSeekPoints(500_000L)
// Cue-based resolution snaps to the fixture's only cue point (t=0); a byte-proportional
// estimate would return the requested time instead.
assertEquals(0L, points.first.timeUs)
val position = points.first.position
val clusterId = byteArrayOf(0x1F, 0x43, 0xB6.toByte(), 0x75)
assertTrue(
"seek position $position must point at a Cluster element",
data.copyOfRange(position.toInt(), position.toInt() + 4).contentEquals(clusterId)
)
output.tracks.values.forEach { it.timesUs.clear() }
extractor.seek(position, points.first.timeUs)
reader.position = position
readToEnd(extractor, reader, data.size.toLong())
output.tracks.values.forEach { track ->
assertTrue("expected extracted samples after seek for track type ${track.type}", track.sampleCount > 0)
assertEquals(points.first.timeUs, track.timesUs.first())
}
}
}
@@ -5,7 +5,14 @@ import androidx.media3.common.DataReader
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.DefaultExtractorInput
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.parser.AssSubtitleParserFactory
import java.io.EOFException
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -17,17 +24,16 @@ import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/**
* Drives LatmTrackOutput with LOAS frames from the committed fixture (1s 440Hz
* sine, AAC-LC 48kHz stereo, LATM/LOAS) and verifies it unwraps them to raw
* AAC with a synthesized AudioSpecificConfig. Both demuxer paths deliver LOAS
* this way: the FFmpeg extractor wraps aac_latm tracks directly.
* Extracts the committed fixture (1s 440Hz sine, AAC-LC 48kHz stereo, LATM/LOAS
* muxed into MKV as A_MS/ACM tag 0x1602) and verifies direct Matroska playback
* unwraps LOAS frames to raw AAC with a synthesized AudioSpecificConfig.
*
* Robolectric provides real android.util.* implementations for media3's
* internals.
* Robolectric provides real android.util.* implementations — MatroskaExtractor
* stores tracks in a SparseArray, which is a no-op stub on plain JVM.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class LatmTrackOutputTest {
class MatroskaLatmSupportTest {
private class CapturedSample(val timeUs: Long, val flags: Int, val data: ByteArray)
@@ -65,6 +71,14 @@ class LatmTrackOutputTest {
}
}
private class FakeExtractorOutput : ExtractorOutput {
val tracks = mutableMapOf<Int, FakeTrackOutput>()
override fun track(id: Int, type: Int): TrackOutput = tracks.getOrPut(id) { FakeTrackOutput() }
override fun endTracks() {}
override fun seekMap(seekMap: SeekMap) {}
}
private class ByteArrayDataReader(private val data: ByteArray) : DataReader {
var position = 0L
@@ -104,46 +118,76 @@ class LatmTrackOutputTest {
return frames
}
private fun placeholderFormat(): Format = Format.Builder()
.setId("1")
.setSampleMimeType(MimeTypes.AUDIO_AAC)
.setLabel("LATM track")
.setLanguage("eng")
.build()
private fun extractFixture(): FakeExtractorOutput {
val data = fixtureData()
val assHandler = AssHandler()
val extractor = ZlibMatroskaExtractor(AssSubtitleParserFactory(assHandler), assHandler)
val output = FakeExtractorOutput()
extractor.init(output)
val reader = ByteArrayDataReader(data)
var input = DefaultExtractorInput(reader, 0, data.size.toLong())
val seekPosition = PositionHolder()
while (true) {
when (extractor.read(input, seekPosition)) {
Extractor.RESULT_END_OF_INPUT -> return output
Extractor.RESULT_SEEK -> {
reader.position = seekPosition.position
input = DefaultExtractorInput(reader, seekPosition.position, data.size.toLong())
}
else -> {}
}
}
}
@Test
fun unwrapsLoasFramesToRawAac() {
val delegate = FakeTrackOutput()
val output = LatmTrackOutput(delegate, 1)
output.format(placeholderFormat())
fun unwrapsLoasAcmTrackToRawAac() {
val output = extractFixture()
val frames = loasFrames(40)
var timeUs = 0L
for (frame in frames) {
output.sampleData(ParsableByteArray(frame), frame.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, frame.size, 0, null)
timeUs += 21_333
}
assertEquals(1, output.tracks.size)
val track = output.tracks.values.first()
// The placeholder format must be swallowed; the LATM-derived AAC format
// carries the AudioSpecificConfig and the original track metadata.
val format = delegate.formats.last()
// No audio/x-unknown format may reach the queue; the LATM-derived AAC
// format must carry the AudioSpecificConfig for MediaCodec.
assertFalse(track.formats.any { it.sampleMimeType == MimeTypes.AUDIO_UNKNOWN })
val format = track.formats.last()
assertEquals(MimeTypes.AUDIO_AAC, format.sampleMimeType)
assertEquals(48000, format.sampleRate)
assertEquals(2, format.channelCount)
assertEquals("1", format.id)
assertEquals("LATM track", format.label)
assertTrue(format.initializationData.isNotEmpty())
assertTrue(format.initializationData[0].isNotEmpty())
assertTrue("expected ~40 samples, got ${delegate.samples.size}", delegate.samples.size in 35..40)
for (sample in delegate.samples) {
// 1s at 48kHz / 1024 samples per AAC frame ≈ 47 frames
assertTrue("expected ~47 samples, got ${track.samples.size}", track.samples.size in 40..55)
// Raw AAC payloads: smaller than the LOAS wrapping, no LOAS syncword,
// keyframe-flagged, monotonic timestamps spanning ~1s.
var prevTimeUs = Long.MIN_VALUE
for (sample in track.samples) {
assertTrue(sample.data.isNotEmpty())
val isLoasSync = sample.data.size >= 2 &&
(sample.data[0].toInt() and 0xFF) == 0x56 &&
(sample.data[1].toInt() and 0xE0) == 0xE0
assertFalse("sample still LOAS-framed", isLoasSync)
assertEquals(C.BUFFER_FLAG_KEY_FRAME, sample.flags and C.BUFFER_FLAG_KEY_FRAME)
assertTrue(sample.timeUs >= prevTimeUs)
prevTimeUs = sample.timeUs
}
val spanUs = track.samples.last().timeUs - track.samples.first().timeUs
assertTrue("expected ~1s span, got ${spanUs}us", spanUs in 800_000..1_200_000)
}
@Test
fun detectsLoasAcmCodecPrivate() {
val loas = byteArrayOf(0x02, 0x16, 0, 0, 0, 0)
assertTrue(isLoasAcmTrack("A_MS/ACM", loas))
// Wrong tag (PCM), wrong codec, or missing private data must not match
assertFalse(isLoasAcmTrack("A_MS/ACM", byteArrayOf(0x01, 0x00, 0, 0)))
assertFalse(isLoasAcmTrack("A_AAC", loas))
assertFalse(isLoasAcmTrack("A_MS/ACM", null))
assertFalse(isLoasAcmTrack("A_MS/ACM", byteArrayOf(0x02)))
assertFalse(isLoasAcmTrack(null, loas))
}
@Test
@@ -161,14 +205,19 @@ class LatmTrackOutputTest {
}
@Test
fun preservesStreamMuxConfigAcrossReset() {
fun preservesStreamMuxConfigAcrossExtractorSeek() {
val frames = loasFrames(2)
assertEquals(0, frames[0][3].toInt() and 0x80)
assertEquals(0x80, frames[1][3].toInt() and 0x80)
val delegate = FakeTrackOutput()
val output = LatmTrackOutput(delegate, 1)
output.format(placeholderFormat())
output.format(
Format.Builder()
.setId("1")
.setSampleMimeType(MimeTypes.AUDIO_UNKNOWN)
.build()
)
output.sampleData(ParsableByteArray(frames[0]), frames[0].size, TrackOutput.SAMPLE_DATA_PART_MAIN)
output.sampleMetadata(0, C.BUFFER_FLAG_KEY_FRAME, frames[0].size, 0, null)
@@ -1,65 +0,0 @@
package com.edde746.plezy.exoplayer
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
class StreamBitrateMeterTest {
/** Feeds [count] packets of [sizeBytes] every [intervalUs], starting at [startUs]. */
private fun StreamBitrateMeter.feed(count: Int, sizeBytes: Int, intervalUs: Long, startUs: Long = 0L) {
for (i in 0 until count) onPacket(startUs + i * intervalUs, sizeBytes)
}
@Test
fun `constant packet stream measures its bitrate`() {
val meter = StreamBitrateMeter()
// 1200-byte packets every 32 ms: 1200 * 8 / 0.032 = 300 kbps.
meter.feed(count = 32, sizeBytes = 1200, intervalUs = 32_000)
assertEquals(300_000, meter.bitrateBps())
}
@Test
fun `too few packets report nothing`() {
val meter = StreamBitrateMeter()
meter.feed(count = 15, sizeBytes = 1200, intervalUs = 32_000)
assertNull(meter.bitrateBps())
}
@Test
fun `zero pts span reports nothing`() {
val meter = StreamBitrateMeter()
repeat(32) { meter.onPacket(0L, 1200) }
assertNull(meter.bitrateBps())
}
@Test
fun `window drops old packets so a bitrate change is followed`() {
val meter = StreamBitrateMeter(windowUs = 1_000_000)
meter.feed(count = 100, sizeBytes = 1200, intervalUs = 32_000)
// Continue at four times the packet size; after well over a window of
// new packets, only the new rate remains.
meter.feed(count = 100, sizeBytes = 4800, intervalUs = 32_000, startUs = 100 * 32_000L)
assertEquals(1_200_000, meter.bitrateBps())
}
@Test
fun `backwards pts jump resets the window`() {
val meter = StreamBitrateMeter()
meter.feed(count = 32, sizeBytes = 1200, intervalUs = 32_000)
// Seek back: the single post-seek packet must not combine with the
// pre-seek span.
meter.onPacket(0L, 1200)
assertNull(meter.bitrateBps())
meter.feed(count = 31, sizeBytes = 1200, intervalUs = 32_000, startUs = 32_000)
assertEquals(300_000, meter.bitrateBps())
}
@Test
fun `empty packets are ignored`() {
val meter = StreamBitrateMeter()
meter.feed(count = 32, sizeBytes = 1200, intervalUs = 32_000)
meter.onPacket(32 * 32_000L, 0)
assertEquals(300_000, meter.bitrateBps())
}
}
@@ -0,0 +1,248 @@
package com.edde746.plezy.exoplayer
import androidx.media3.common.C
import androidx.media3.common.DataReader
import androidx.media3.common.Format
import androidx.media3.common.ParserException
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.TrackOutput
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import org.junit.After
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
class ZlibInflatingTrackOutputTest {
@Before
fun enableMedia3TestBoundsChecks() {
ParsableByteArray.setShouldEnforceLimitOnLegacyMethods(true)
}
@After
fun resetMedia3TestBoundsChecks() {
ParsableByteArray.setShouldEnforceLimitOnLegacyMethods(null)
}
@Test
fun activeTransformInflatesChunkedSampleAndPreservesMetadata() {
val original = ByteArray(512 * 1024) { (it % 251).toByte() }
val compressed = deflate(original)
val delegate = RecordingTrackOutput(retainBytes = true)
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
val split = compressed.size / 2
output.sampleData(
ParsableByteArray(compressed.copyOfRange(0, split)),
split,
TrackOutput.SAMPLE_DATA_PART_MAIN
)
output.sampleData(
ParsableByteArray(compressed.copyOfRange(split, compressed.size)),
compressed.size - split,
TrackOutput.SAMPLE_DATA_PART_MAIN
)
output.sampleMetadata(42L, C.BUFFER_FLAG_KEY_FRAME, compressed.size, 0, null)
assertArrayEquals(original, delegate.retained.toByteArray())
assertEquals(42L, delegate.timeUs)
assertEquals(C.BUFFER_FLAG_KEY_FRAME, delegate.flags)
assertEquals(original.size, delegate.sampleSize)
assertEquals(0, delegate.offset)
assertEquals(1, delegate.metadataCount)
}
@Test
fun activeTransformUsesMetadataBoundariesForBufferedSamples() {
val first = ByteArray(4096) { (it % 19).toByte() }
val second = ByteArray(2048) { (it % 23).toByte() }
val firstCompressed = deflate(first)
val secondCompressed = deflate(second)
val combined = firstCompressed + secondCompressed
val delegate = RecordingTrackOutput(retainBytes = true)
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
output.sampleData(ParsableByteArray(combined), combined.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
output.sampleMetadata(1, 0, firstCompressed.size, secondCompressed.size, null)
output.sampleMetadata(2, 0, secondCompressed.size, 0, null)
assertArrayEquals(first + second, delegate.retained.toByteArray())
assertEquals(2, delegate.metadataCount)
}
@Test
fun exactlySixteenMiBInflatedSampleIsAccepted() {
val size = 16 * 1024 * 1024
val compressed = deflateRepeated(size)
val delegate = RecordingTrackOutput()
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
feed(output, compressed)
assertEquals(size, delegate.byteCount)
assertEquals(size, delegate.sampleSize)
assertEquals(1, delegate.metadataCount)
assertTrue(compressed.size < 64 * 1024)
}
@Test
fun oneByteOverInflatedLimitFailsBeforeDelegation() {
val compressed = deflateRepeated(16 * 1024 * 1024 + 1)
val delegate = RecordingTrackOutput()
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
assertThrows(ParserException::class.java) { feed(output, compressed) }
assertEquals(0, delegate.byteCount)
assertEquals(0, delegate.metadataCount)
assertTrue(compressed.size < 64 * 1024)
}
@Test
fun excessiveCompressionRatioFailsBeforeInflatedSizeLimit() {
val compressed = deflate(ByteArray(8 * 1024 * 1024))
val delegate = RecordingTrackOutput()
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
assertThrows(ParserException::class.java) { feed(output, compressed) }
assertEquals(0, delegate.byteCount)
assertEquals(0, delegate.metadataCount)
}
@Test
fun corruptStreamPassesTheBoundedSampleThroughUnchanged() {
val corrupt = byteArrayOf(0, 1, 2, 3)
val delegate = RecordingTrackOutput(retainBytes = true)
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
feed(output, corrupt)
assertArrayEquals(corrupt, delegate.retained.toByteArray())
assertEquals(corrupt.size, delegate.sampleSize)
assertEquals(1, delegate.metadataCount)
}
@Test
fun truncatedAndDictionaryStreamsFailClosed() {
val valid = deflate(ByteArray(4096) { 7 })
val dictionary = "shared-zlib-dictionary".toByteArray()
val cases = listOf(
valid.copyOf(valid.size - 2),
deflate(ByteArray(4096) { 3 }, dictionary)
)
for (compressed in cases) {
val delegate = RecordingTrackOutput()
val output = ZlibInflatingTrackOutput(delegate).apply { active = true }
assertThrows(ParserException::class.java) { feed(output, compressed) }
assertEquals(0, delegate.byteCount)
assertEquals(0, delegate.metadataCount)
}
}
@Test
fun inactiveWrapperDelegatesBytesAndMetadataUnchanged() {
val bytes = byteArrayOf(9, 8, 7, 6)
val trailing = byteArrayOf(5, 4)
val allBytes = bytes + trailing
val delegate = RecordingTrackOutput(retainBytes = true)
val output = ZlibInflatingTrackOutput(delegate)
output.sampleData(ParsableByteArray(allBytes), allBytes.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
output.sampleMetadata(99L, 3, bytes.size, trailing.size, null)
assertArrayEquals(allBytes, delegate.retained.toByteArray())
assertEquals(bytes.size, delegate.sampleSize)
assertEquals(trailing.size, delegate.offset)
assertEquals(99L, delegate.timeUs)
}
private fun feed(output: ZlibInflatingTrackOutput, compressed: ByteArray) {
output.sampleData(
ParsableByteArray(compressed),
compressed.size,
TrackOutput.SAMPLE_DATA_PART_MAIN
)
output.sampleMetadata(1L, C.BUFFER_FLAG_KEY_FRAME, compressed.size, 0, null)
}
private fun deflate(bytes: ByteArray, dictionary: ByteArray? = null): ByteArray {
val target = ByteArrayOutputStream()
val deflater = Deflater().apply {
if (dictionary != null) setDictionary(dictionary)
}
DeflaterOutputStream(target, deflater).use { it.write(bytes) }
return target.toByteArray()
}
private fun deflateRepeated(size: Int): ByteArray {
val target = ByteArrayOutputStream()
val chunk = ByteArray(8192) { (it % 7).toByte() }
DeflaterOutputStream(target).use { stream ->
var remaining = size
while (remaining > 0) {
val count = minOf(remaining, chunk.size)
stream.write(chunk, 0, count)
remaining -= count
}
}
return target.toByteArray()
}
private class RecordingTrackOutput(
private val retainBytes: Boolean = false
) : TrackOutput {
val retained = ByteArrayOutputStream()
var byteCount = 0
var metadataCount = 0
var timeUs = C.TIME_UNSET
var flags = 0
var sampleSize = -1
var offset = -1
override fun format(format: Format) = Unit
override fun sampleData(
input: DataReader,
length: Int,
allowEndOfInput: Boolean,
sampleDataPart: Int
): Int {
val buffer = ByteArray(length)
val read = input.read(buffer, 0, length)
if (read > 0) record(buffer, read)
return read
}
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
val buffer = ByteArray(length)
data.readBytes(buffer, 0, length)
record(buffer, length)
}
private fun record(buffer: ByteArray, length: Int) {
byteCount += length
if (retainBytes) retained.write(buffer, 0, length)
}
override fun sampleMetadata(
timeUs: Long,
flags: Int,
size: Int,
offset: Int,
cryptoData: TrackOutput.CryptoData?
) {
metadataCount++
this.timeUs = timeUs
this.flags = flags
sampleSize = size
this.offset = offset
}
}
}
@@ -0,0 +1,80 @@
package com.edde746.plezy.exoplayer
import java.io.ByteArrayOutputStream
import java.util.zip.Deflater
import java.util.zip.DeflaterOutputStream
import java.util.zip.Inflater
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertNull
import org.junit.Test
/**
* Unit spec for [rewriteZlibTextBlock]: the block header (track-number varint,
* timecode, flags) must survive byte-identically, the payload must inflate, and
* every block the rewriter cannot prove safe must pass through unchanged (null).
*/
class ZlibTextBlockTest {
private val inflater = Inflater()
private fun block(header: ByteArray, payload: ByteArray): ByteArray = header + payload
private fun deflate(bytes: ByteArray): ByteArray {
val target = ByteArrayOutputStream()
DeflaterOutputStream(target, Deflater()).use { it.write(bytes) }
return target.toByteArray()
}
// Track 3 (varint 0x83), relative timecode 0x0102, no lacing.
private val unlacedHeader = byteArrayOf(0x83.toByte(), 0x01, 0x02, 0x00)
@Test
fun inflatesUnlacedBlockPayloadAndPreservesHeader() {
val dialogue = "42,,Default,,0,0,0,,SUBTITLE LINE 1 (0:00:00.00)".toByteArray()
val data = block(unlacedHeader, deflate(dialogue))
val rewritten = rewriteZlibTextBlock(data, data.size, inflater)!!
assertArrayEquals(unlacedHeader + dialogue, rewritten)
}
@Test
fun twoByteTrackNumberVarintIsPreserved() {
val dialogue = "1,,Default,,0,0,0,,hello".toByteArray()
val header = byteArrayOf(0x41, 0x2A, 0x01, 0x02, 0x00) // varint 0x412A = track 298
val data = block(header, deflate(dialogue))
val rewritten = rewriteZlibTextBlock(data, data.size, inflater)!!
assertArrayEquals(header + dialogue, rewritten)
}
@Test
fun lacedBlockPassesThrough() {
val laced = byteArrayOf(0x83.toByte(), 0x01, 0x02, 0x06) // EBML lacing bits set
val data = block(laced, deflate("payload".toByteArray()))
assertNull(rewriteZlibTextBlock(data, data.size, inflater))
}
@Test
fun corruptStreamPassesThrough() {
val data = block(unlacedHeader, byteArrayOf(0x44, 0x69, 0x61, 0x6C)) // "Dial", not zlib
assertNull(rewriteZlibTextBlock(data, data.size, inflater))
}
@Test
fun truncatedStreamPassesThrough() {
val compressed = deflate("a longer payload that spans several deflate symbols".toByteArray())
val truncated = compressed.copyOf(compressed.size - 4)
val data = block(unlacedHeader, truncated)
assertNull(rewriteZlibTextBlock(data, data.size, inflater))
}
@Test
fun headerOnlyBlockPassesThrough() {
assertNull(rewriteZlibTextBlock(unlacedHeader, unlacedHeader.size, inflater))
}
}
@@ -0,0 +1,170 @@
package com.edde746.plezy.exoplayer
import androidx.media3.common.C
import androidx.media3.common.DataReader
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.DefaultExtractorInput
import androidx.media3.extractor.Extractor
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.PositionHolder
import androidx.media3.extractor.SeekMap
import androidx.media3.extractor.TrackOutput
import androidx.media3.extractor.text.CueDecoder
import androidx.media3.extractor.text.DefaultSubtitleParserFactory
import com.edde746.plezy.libass.media.AssHandler
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/**
* Extracts the committed fixtures — one ASS subtitle track muxed by mkvmerge with
* `--compression 0:zlib` (ContentCompAlgo 0, the anime-release convention that
* broke #2023) and an identical uncompressed mux — and verifies the zlib block
* payloads are inflated *before* MatroskaExtractor's subtitle sample assembly.
*
* Regenerate with:
* mkvmerge -o zlib_ssa.mkv --compression 0:zlib repro.ass
* mkvmerge -o plain_ssa.mkv --compression 0:none repro.ass
* where repro.ass holds 45 two-second "SUBTITLE LINE N (H:MM:SS.00)" events.
*
* Robolectric provides real android.util/android.os implementations —
* MatroskaExtractor uses SparseArray and CueDecoder uses Parcel, both of which
* are no-op stubs on plain JVM.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class ZlibTextSubtitleExtractionTest {
private class CapturedSample(val timeUs: Long, val data: ByteArray)
private class FakeTrackOutput : TrackOutput {
val formats = mutableListOf<androidx.media3.common.Format>()
val samples = mutableListOf<CapturedSample>()
private var buf = ByteArray(64 * 1024)
private var bufLen = 0
override fun format(format: androidx.media3.common.Format) {
formats.add(format)
}
override fun sampleData(input: DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int): Int {
ensureCapacity(bufLen + length)
val read = input.read(buf, bufLen, length)
if (read > 0) bufLen += read
return read
}
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
ensureCapacity(bufLen + length)
data.readBytes(buf, bufLen, length)
bufLen += length
}
override fun sampleMetadata(timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?) {
val start = bufLen - offset - size
samples.add(CapturedSample(timeUs, buf.copyOfRange(start, start + size)))
if (offset == 0) bufLen = 0
}
private fun ensureCapacity(needed: Int) {
if (buf.size < needed) buf = buf.copyOf(maxOf(needed, buf.size * 2))
}
}
private class FakeExtractorOutput : ExtractorOutput {
val tracks = mutableMapOf<Int, FakeTrackOutput>()
override fun track(id: Int, type: Int): TrackOutput = tracks.getOrPut(id) { FakeTrackOutput() }
override fun endTracks() {}
override fun seekMap(seekMap: SeekMap) {}
}
private class ByteArrayDataReader(private val data: ByteArray) : DataReader {
var position = 0L
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (position >= data.size) return C.RESULT_END_OF_INPUT
val toRead = minOf(length, data.size - position.toInt())
System.arraycopy(data, position.toInt(), buffer, offset, toRead)
position += toRead
return toRead
}
}
private fun extract(resource: String): FakeExtractorOutput {
val data = checkNotNull(javaClass.getResourceAsStream(resource)) {
"fixture $resource missing from test resources"
}.use { it.readBytes() }
// DefaultSubtitleParserFactory (not AssSubtitleParserFactory) so the SSA track
// transcodes to decodable media3 cues instead of loading native libass.
val extractor = ZlibMatroskaExtractor(DefaultSubtitleParserFactory(), AssHandler())
val output = FakeExtractorOutput()
extractor.init(output)
val reader = ByteArrayDataReader(data)
var input = DefaultExtractorInput(reader, 0, data.size.toLong())
val seekPosition = PositionHolder()
while (true) {
when (extractor.read(input, seekPosition)) {
Extractor.RESULT_END_OF_INPUT -> return output
Extractor.RESULT_SEEK -> {
reader.position = seekPosition.position
input = DefaultExtractorInput(reader, seekPosition.position, data.size.toLong())
}
else -> {}
}
}
}
private class DecodedCue(val startTimeUs: Long, val durationUs: Long, val text: String)
private fun decodeCues(output: FakeExtractorOutput): List<DecodedCue> {
val track = output.tracks.values.single()
val format = track.formats.last()
assertEquals(MimeTypes.APPLICATION_MEDIA3_CUES, format.sampleMimeType)
assertEquals(MimeTypes.TEXT_SSA, format.codecs)
val decoder = CueDecoder()
return track.samples.map { sample ->
val cues = decoder.decode(sample.timeUs, sample.data, 0, sample.data.size)
DecodedCue(
cues.startTimeUs,
cues.durationUs,
cues.cues.joinToString("\n") { it.text.toString() }
)
}
}
@Test
fun zlibCompressedAssDialogueIsInflatedToParseableCues() {
val cues = decodeCues(extract("/zlib_ssa.mkv"))
assertEquals(45, cues.size)
assertEquals("SUBTITLE LINE 1 (0:00:00.00)", cues.first().text)
assertEquals(0L, cues.first().startTimeUs)
assertEquals(2_000_000L, cues.first().durationUs)
assertEquals("SUBTITLE LINE 45 (0:01:28.00)", cues.last().text)
assertEquals(88_000_000L, cues.last().startTimeUs)
cues.forEachIndexed { index, cue ->
assertEquals(index * 2_000_000L, cue.startTimeUs)
assertTrue("cue $index text: ${cue.text}", cue.text.startsWith("SUBTITLE LINE ${index + 1} "))
}
}
@Test
fun zlibAndPlainMuxesProduceIdenticalCues() {
val zlib = decodeCues(extract("/zlib_ssa.mkv"))
val plain = decodeCues(extract("/plain_ssa.mkv"))
assertEquals(plain.size, zlib.size)
plain.zip(zlib).forEachIndexed { index, (expected, actual) ->
assertEquals("startTimeUs of cue $index", expected.startTimeUs, actual.startTimeUs)
assertEquals("durationUs of cue $index", expected.durationUs, actual.durationUs)
assertEquals("text of cue $index", expected.text, actual.text)
}
}
}
@@ -0,0 +1,38 @@
package com.edde746.plezy.mpv
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
/** Boundaries are the contract; rationale on [DemuxerBudget]. */
class DemuxerBudgetTest {
private val mib = 1024L * 1024L
@Test
fun `unknown heap class keeps mpv defaults`() {
assertNull(DemuxerBudget.forHeapClassMB(0))
assertNull(DemuxerBudget.forHeapClassMB(-1))
}
@Test
fun `small heap class gets the tight tier`() {
val budget = DemuxerBudget.forHeapClassMB(256)!!
assertEquals(32 * mib, budget.aheadBytes)
assertEquals(16 * mib, budget.backBytes)
}
@Test
fun `mid heap class gets the middle tier`() {
assertEquals(64 * mib, DemuxerBudget.forHeapClassMB(257)!!.aheadBytes)
val budget = DemuxerBudget.forHeapClassMB(512)!!
assertEquals(64 * mib, budget.aheadBytes)
assertEquals(32 * mib, budget.backBytes)
}
@Test
fun `large heap class gets the full tier`() {
val budget = DemuxerBudget.forHeapClassMB(513)!!
assertEquals(100 * mib, budget.aheadBytes)
assertEquals(48 * mib, budget.backBytes)
}
}
@@ -0,0 +1,114 @@
package com.edde746.plezy.mpv
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* P5 has no compatible base layer: on a device without native DV it must
* leave the video plane for gpu-next software reshaping, and nothing else
* may be routed (wrong-colors class verified on a Pixel 7). The vo target
* matrix keeps gpu-next away from hardware sessions (#2010) while giving
* the reshaping path the only vo that composites RPU metadata (#1902).
*/
class GpuVoPolicyTest {
@Test
fun `P5 without native support is routed`() {
assertTrue(GpuVoPolicy.needsDvReshaping(5L, "auto", canPlayP5Natively = false))
}
@Test
fun `P5 with native support stays on the plane`() {
assertFalse(GpuVoPolicy.needsDvReshaping(5L, "auto", canPlayP5Natively = true))
}
@Test
fun `base-layer-compatible profiles are never routed`() {
// P7/P8 strip to an HDR10/HLG base layer; non-DV content has no profile.
assertFalse(GpuVoPolicy.needsDvReshaping(7L, "auto", canPlayP5Natively = false))
assertFalse(GpuVoPolicy.needsDvReshaping(8L, "auto", canPlayP5Natively = false))
assertFalse(GpuVoPolicy.needsDvReshaping(null, "auto", canPlayP5Natively = false))
}
@Test
fun `explicit conversion modes are user overrides and stay native`() {
for (mode in listOf("disabled", "native", "dv81", "hevc", "hevc_strip")) {
assertFalse(mode, GpuVoPolicy.needsDvReshaping(5L, mode, canPlayP5Natively = false))
}
}
@Test
fun `hdr tone-mapping is needed only for a PQ or HLG signal on a non-HDR display`() {
assertTrue(GpuVoPolicy.needsHdrToneMapping("pq", displaySupportsHdr = false))
assertTrue(GpuVoPolicy.needsHdrToneMapping("hlg", displaySupportsHdr = false))
// An HDR display scans the signal out itself.
assertFalse(GpuVoPolicy.needsHdrToneMapping("pq", displaySupportsHdr = true))
assertFalse(GpuVoPolicy.needsHdrToneMapping("hlg", displaySupportsHdr = true))
// SDR transfers need no mapping, and mpv reports none before the first
// frame of a file.
assertFalse(GpuVoPolicy.needsHdrToneMapping("bt.1886", displaySupportsHdr = false))
assertFalse(GpuVoPolicy.needsHdrToneMapping("srgb", displaySupportsHdr = false))
assertFalse(GpuVoPolicy.needsHdrToneMapping(null, displaySupportsHdr = false))
assertFalse(GpuVoPolicy.needsHdrToneMapping("", displaySupportsHdr = false))
}
@Test
fun `only direct mediacodec output can stay on the plane`() {
// -copy also reads frames back into system memory, so it leaves too.
assertTrue(GpuVoPolicy.needsSoftwareRender("no"))
assertTrue(GpuVoPolicy.needsSoftwareRender("mediacodec-copy"))
assertFalse(GpuVoPolicy.needsSoftwareRender("mediacodec"))
// Unreported until the decoder initializes: stay on the plane.
assertFalse(GpuVoPolicy.needsSoftwareRender(null))
assertFalse(GpuVoPolicy.needsSoftwareRender(""))
}
@Test
fun `a software-decoding session targets gpu, not gpu-next`() {
assertEquals("gpu", GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_SW_DECODE)))
assertEquals("gpu", GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_SHADERS)))
assertEquals(
"gpu",
GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_SHADERS, GpuVoPolicy.REASON_SW_DECODE))
)
}
@Test
fun `no reasons keeps the video plane`() {
assertNull(GpuVoPolicy.targetFor(emptySet()))
}
@Test
fun `dv reshaping targets gpu-next even alongside other reasons`() {
assertEquals("gpu-next", GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_DV_RESHAPE)))
assertEquals(
"gpu-next",
GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_SHADERS, GpuVoPolicy.REASON_DV_RESHAPE))
)
}
@Test
fun `shaders and chain failure target the hardware-safe gpu vo`() {
assertEquals("gpu", GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_SHADERS)))
assertEquals("gpu", GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_CHAIN_FAILURE)))
assertEquals(
"gpu",
GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_SHADERS, GpuVoPolicy.REASON_CHAIN_FAILURE))
)
}
@Test
fun `hdr tone-mapping targets gpu but yields to dv reshaping`() {
assertEquals("gpu", GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_HDR_SDR)))
assertEquals(
"gpu",
GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_HDR_SDR, GpuVoPolicy.REASON_SHADERS))
)
assertEquals(
"gpu-next",
GpuVoPolicy.targetFor(setOf(GpuVoPolicy.REASON_HDR_SDR, GpuVoPolicy.REASON_DV_RESHAPE))
)
}
}
@@ -24,6 +24,7 @@ import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.suspendCancellableCoroutine
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -62,14 +63,24 @@ class MpvPlayerPluginTest {
}
@Test
fun hardwareDecodeSessionsKeepTheLegacyGpuVo() {
// gpu-next under hwdec=mediacodec fails every frame on Tegra (the
// cross-stage samplerExternalOES linker bug): solid blue screen with
// audio on the Shield (#2010). DV reshaping — the reason gpu-next exists
// on Android (#1902) — only happens under software decode anyway, so
// gpu-next is offered exactly there and nowhere else.
assertEquals("gpu", MpvPlayerCore.initialVideoOutput(hardwareDecoding = true))
assertEquals("gpu-next,gpu", MpvPlayerCore.initialVideoOutput(hardwareDecoding = false))
fun hardwareDecodeSessionsUseTheMediaCodecVoWithGpuFallback() {
// Rationale on MpvPlayerCore.initialVideoOutput.
assertEquals("mediacodec,gpu", MpvPlayerCore.initialVideoOutput(hardwareDecoding = true))
assertEquals("gpu,gpu-next", MpvPlayerCore.initialVideoOutput(hardwareDecoding = false))
}
@Test
fun hdrSurfaceIsWantedOnlyForPqAndHlgTransfers() {
// Rationale on MpvPlayerCore.wantsHdrSurface: both render into a PQ
// target; anything else stays on the sRGB surface, which renders every
// content correctly.
assertTrue(MpvPlayerCore.wantsHdrSurface("smpte2084"))
assertTrue(MpvPlayerCore.wantsHdrSurface("arib-std-b67"))
assertFalse(MpvPlayerCore.wantsHdrSurface("bt709"))
assertFalse(MpvPlayerCore.wantsHdrSurface("bt1886"))
assertFalse(MpvPlayerCore.wantsHdrSurface("unknown"))
assertFalse(MpvPlayerCore.wantsHdrSurface(""))
assertFalse(MpvPlayerCore.wantsHdrSurface(null))
}
@Test
@@ -622,6 +633,53 @@ class MpvPlayerPluginTest {
assertEquals(0, pending.size)
}
@Test
fun staleDisposeIsAcknowledgedWithoutTearingDownTheCore() {
// A dispose whose instanceId is not the core creator's lost the ownership
// race to a successor; tearing the core down anyway would kill that
// successor's session. It must be acknowledged as a no-op instead.
val plugin = MpvPlayerPlugin()
installCore(plugin, testCore { _, _ -> })
setPluginField(plugin, "coreInstanceId", 2L)
val result = RecordingResult()
plugin.onMethodCall(MethodCall("dispose", mapOf("instanceId" to 1)), result)
awaitCompletion(result)
assertNull(result.errorCode)
assertNotNull(getPluginField(plugin, "playerCore"))
assertEquals(2L, getPluginField(plugin, "coreInstanceId"))
}
@Test
fun matchingDisposeTearsDownTheCoreAndClearsTheToken() {
val plugin = MpvPlayerPlugin()
installCore(plugin, testCore { _, _ -> })
setPluginField(plugin, "coreInstanceId", 7L)
val result = RecordingResult()
plugin.onMethodCall(MethodCall("dispose", mapOf("instanceId" to 7)), result)
awaitCompletion(result)
assertNull(result.errorCode)
assertNull(getPluginField(plugin, "playerCore"))
assertNull(getPluginField(plugin, "coreInstanceId"))
}
@Test
fun tokenlessDisposeKeepsLegacySemanticsAndTearsDownTheCore() {
val plugin = MpvPlayerPlugin()
installCore(plugin, testCore { _, _ -> })
setPluginField(plugin, "coreInstanceId", 7L)
val result = RecordingResult()
plugin.onMethodCall(MethodCall("dispose", null), result)
awaitCompletion(result)
assertNull(result.errorCode)
assertNull(getPluginField(plugin, "playerCore"))
}
@Test
fun configDetachThenEngineDetachTearsDownVideoCoreAndPendingInitOnce() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
@@ -891,6 +949,97 @@ class MpvPlayerPluginTest {
getBoolean(core)
}
private fun invokeSetGpuVoRequirement(core: MpvPlayerCore, reason: String, active: Boolean) {
MpvPlayerCore::class.java
.getDeclaredMethod("setGpuVoRequirement", String::class.java, Boolean::class.javaPrimitiveType)
.apply {
isAccessible = true
invoke(core, reason, active)
}
}
@Test
fun voTargetFollowsTheActiveReasonSet() {
// Reason precedence through the real property-write path: DV reshaping
// outranks HDR-on-SDR, dropping the winner falls back to the reason still
// active, and dropping the last one returns the session to the plane.
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val writes = ConcurrentLinkedQueue<Pair<String, String>>()
val core = MpvPlayerCore(activity, audioOnly = false, propertyWriter = { name, value ->
writes.add(name to value)
})
setBoolean(core, "isInitialized", true)
fun lastVo(): String? = writes.toList().lastOrNull { it.first == "vo" }?.second
// HDR-on-SDR raises first, then the DV router wins the arbitration.
invokeSetGpuVoRequirement(core, GpuVoPolicy.REASON_HDR_SDR, true)
invokeSetGpuVoRequirement(core, GpuVoPolicy.REASON_DV_RESHAPE, true)
awaitCondition { lastVo() == "gpu-next" }
assertEquals("gpu-next", lastVo())
// Dropping the winning reason must fall back to the one still active,
// not to the plane.
invokeSetGpuVoRequirement(core, GpuVoPolicy.REASON_DV_RESHAPE, false)
awaitCondition { lastVo() == "gpu" }
assertEquals("gpu", lastVo())
// Last reason dropping returns the session to the video plane.
invokeSetGpuVoRequirement(core, GpuVoPolicy.REASON_HDR_SDR, false)
awaitCondition { lastVo() == "mediacodec" }
assertEquals("mediacodec", lastVo())
}
@Test
fun dvConversionModeMapsOntoForkDecoderOptions() {
// The app-level `dv-conversion-mode` property must translate to the fork
// FFmpeg hevc_mediacodec options, mirroring the ExoPlayer DoviBridge
// modes. Robolectric reports no Dolby Vision display, so `auto` takes the
// no-DV branch deterministically.
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val writes = ConcurrentLinkedQueue<Pair<String, String>>()
val core = MpvPlayerCore(activity, audioOnly = false, propertyWriter = { name, value ->
writes.add(name to value)
})
fun apply(mode: String): Result<Unit> {
writes.clear()
var outcome: Result<Unit>? = null
core.setProperty("dv-conversion-mode", mode) { outcome = it }
awaitCondition { outcome != null }
return outcome!!
}
assertTrue(apply("auto").isSuccess)
assertEquals(listOf("vd-lavc-o" to "dolby_vision=0,dv_p7_mode=strip"), writes.toList())
assertTrue(apply("disabled").isSuccess)
assertEquals(listOf("vd-lavc-o" to "dolby_vision=1,dv_p7_mode=native"), writes.toList())
assertTrue(apply("dv81").isSuccess)
assertEquals(listOf("vd-lavc-o" to "dolby_vision=1,dv_p7_mode=convert"), writes.toList())
assertTrue(apply("hevc_strip").isSuccess)
assertEquals(listOf("vd-lavc-o" to "dolby_vision=1,dv_p7_mode=strip"), writes.toList())
val invalid = apply("bogus")
assertTrue(invalid.isFailure)
assertTrue(writes.isEmpty())
}
@Test
fun decoderOptionsMergeKeepsUserEntriesFirst() {
// The property interface cannot append to a list option, so the write
// replaces it wholesale: a user's own mpv.conf `vd-lavc-o` entries must
// survive, with the app's keys last (FFmpeg keeps the last duplicate).
assertEquals(
"threads=4,dolby_vision=1",
MpvPlayerCore.mergeDecoderOptions("threads=4", "dolby_vision=1")
)
assertEquals("dolby_vision=1", MpvPlayerCore.mergeDecoderOptions(null, "dolby_vision=1"))
assertEquals("dolby_vision=1", MpvPlayerCore.mergeDecoderOptions(" ", "dolby_vision=1"))
}
private fun awaitQueueEntry(
queue: ConcurrentLinkedQueue<Pair<String, String>>,
expected: Pair<String, String>
@@ -0,0 +1,72 @@
package com.edde746.plezy.mpv
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
class VideoRectPolicyTest {
@Test
fun `contain letterboxes wide content inside the container`() {
// 2.39:1 content on a 16:9 container: full width, bars top and bottom.
val size = VideoRectPolicy.sizeFor(1920, 1080, 2390, 1000)!!
assertEquals(1920, size.width)
assertEquals(803, size.height)
assertTrue(size.height < 1080)
}
@Test
fun `contain pillarboxes tall content inside the container`() {
val size = VideoRectPolicy.sizeFor(1920, 1080, 1000, 2390)!!
assertEquals(1080, size.height)
assertTrue(size.width < 1920)
}
@Test
fun `panscan 1 fills the container so the image is cropped, not letterboxed`() {
// The regression this exists for: cover used to be a no-op on the plane.
val size = VideoRectPolicy.sizeFor(1920, 1080, 2390, 1000, panscan = 1f)!!
assertTrue("cover must reach the container height", size.height >= 1080)
assertTrue("width must overflow and be clipped", size.width > 1920)
}
@Test
fun `panscan interpolates between contain and cover`() {
val contain = VideoRectPolicy.sizeFor(1920, 1080, 2390, 1000)!!
val half = VideoRectPolicy.sizeFor(1920, 1080, 2390, 1000, panscan = 0.5f)!!
val cover = VideoRectPolicy.sizeFor(1920, 1080, 2390, 1000, panscan = 1f)!!
assertTrue(half.height > contain.height)
assertTrue(half.height < cover.height)
}
@Test
fun `video-zoom is a log2 factor on top of the fit`() {
val base = VideoRectPolicy.sizeFor(1920, 1080, 1920, 1080)!!
val doubled = VideoRectPolicy.sizeFor(1920, 1080, 1920, 1080, videoZoomLog2 = 1f)!!
val halved = VideoRectPolicy.sizeFor(1920, 1080, 1920, 1080, videoZoomLog2 = -1f)!!
assertEquals(base.width * 2, doubled.width)
assertEquals(base.width / 2, halved.width)
}
@Test
fun `cover fills a container whose aspect is wildly mismatched`() {
// Scope content in a portrait container: cover/fit exceeds 4, which a
// fit-relative clamp would truncate back into letterboxing.
val size = VideoRectPolicy.sizeFor(1080, 2400, 2390, 1000, panscan = 1f)!!
assertTrue("height must reach the container", size.height >= 2400)
}
@Test
fun `an absurd zoom is bounded rather than allocating an unbounded surface`() {
val fit = VideoRectPolicy.sizeFor(1920, 1080, 1920, 1080)!!
val absurd = VideoRectPolicy.sizeFor(1920, 1080, 1920, 1080, videoZoomLog2 = 12f)!!
assertEquals(fit.width * 4, absurd.width)
}
@Test
fun `unknown dimensions yield no size instead of a degenerate one`() {
assertNull(VideoRectPolicy.sizeFor(0, 1080, 1920, 1080))
assertNull(VideoRectPolicy.sizeFor(1920, 1080, 0, 0))
assertNull(VideoRectPolicy.sizeFor(1920, 0, 1920, 1080))
}
}
@@ -0,0 +1,95 @@
package com.edde746.plezy.shared
import android.app.Activity
import android.os.Handler
import android.os.Looper
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotEquals
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import java.time.Duration
/**
* Teardown ordering for frame-rate matching (#2172): restoring the display
* mode while the display is still signaling HDR folds the HDR exit and the
* mode change into one slow HDMI renegotiation. An HDR session therefore
* defers the restore; an SDR session restores immediately.
*/
@RunWith(RobolectricTestRunner::class)
class FrameRateManagerRestoreTest {
private fun buildManager(activity: Activity): FrameRateManager =
FrameRateManager(activity, Handler(Looper.getMainLooper()))
private fun preferredModeId(activity: Activity): Int = activity.window.attributes.preferredDisplayModeId
private fun applyPreferredMode(activity: Activity, modeId: Int) {
val attrs = activity.window.attributes
attrs.preferredDisplayModeId = modeId
activity.window.attributes = attrs
}
@Test
fun sdrClearRestoresTheDefaultModeImmediately() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val manager = buildManager(activity)
applyPreferredMode(activity, 4)
manager.clearVideoFrameRate(hdrActive = false)
assertEquals(0, preferredModeId(activity))
}
@Test
fun hdrClearDefersTheRestoreUntilTheHdrExitSettles() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val manager = buildManager(activity)
applyPreferredMode(activity, 4)
manager.clearVideoFrameRate(hdrActive = true)
// Still at the content mode: the surface teardown must commit the HDR
// exit before the mode change renegotiates the link.
assertEquals(4, preferredModeId(activity))
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500))
assertEquals(0, preferredModeId(activity))
}
@Test
fun newSwitchRequestCancelsAPendingDeferredRestore() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val manager = buildManager(activity)
applyPreferredMode(activity, 4)
manager.clearVideoFrameRate(hdrActive = true)
// A new session's request arrives before the deferred restore fires.
manager.setVideoFrameRate(fps = 23.976f, videoDurationMs = 0L, extraDelayMs = 0L) { }
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500))
// The stale restore must not clobber whatever the new request applied.
// (Robolectric's display has no matching mode, so the request itself
// leaves the window untouched; only the cancelled restore could zero it.)
assertNotEquals(0, preferredModeId(activity))
}
@Test
fun hdrClearWithNoAppliedModePostsNoRestore() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val manager = buildManager(activity)
applyPreferredMode(activity, 7)
// Simulate "nothing applied by us": manager sees modeId 0.
applyPreferredMode(activity, 0)
manager.clearVideoFrameRate(hdrActive = true)
// A later, externally applied mode must not be clobbered by a stale
// deferred restore from a session that never switched.
applyPreferredMode(activity, 7)
shadowOf(Looper.getMainLooper()).idleFor(Duration.ofMillis(500))
assertEquals(7, preferredModeId(activity))
}
}
@@ -1,5 +1,6 @@
package com.edde746.plezy.shared
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -60,4 +61,18 @@ class MediaCodecQueryTest {
assertFalse(MediaCodecQuery.isHardwareAccelerated(28, true, "OMX.google.h264.decoder"))
assertTrue(MediaCodecQuery.isHardwareAccelerated(28, false, "OMX.qcom.video.decoder.avc"))
}
@Test
fun mapsGatedCodecsToTheMimeTypesDecodersAdvertise() {
assertEquals(
mapOf("hevc" to true, "av1" to true),
MediaCodecQuery.hardwareVideoDecodeSupport(
setOf("video/avc", "video/hevc", "video/av01", "audio/mp4a-latm")
)
)
assertEquals(
mapOf("hevc" to true, "av1" to false),
MediaCodecQuery.hardwareVideoDecodeSupport(setOf("video/avc", "video/hevc"))
)
}
}
@@ -4,6 +4,7 @@ import android.app.Activity
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.ViewGroup
import android.widget.FrameLayout
import org.junit.Assert.assertEquals
@@ -41,4 +42,27 @@ class PlayerSurfaceHostTest {
assertEquals(FrameLayout.LayoutParams.MATCH_PARENT, surface.layoutParams.width)
assertEquals(FrameLayout.LayoutParams.MATCH_PARENT, surface.layoutParams.height)
}
@Test
fun letterboxAreaIsCoveredByABufferlessPunchSurfaceBeneathTheVideo() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
activity.setContentView(FrameLayout(activity))
val callback = object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) = Unit
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) = Unit
override fun surfaceDestroyed(holder: SurfaceHolder) = Unit
}
val container = PlayerSurfaceHost.createContainer(activity)
val punch = container.getChildAt(0)
assertTrue(punch is SurfaceView)
assertEquals(FrameLayout.LayoutParams.MATCH_PARENT, punch.layoutParams.width)
assertEquals(FrameLayout.LayoutParams.MATCH_PARENT, punch.layoutParams.height)
// Cores append their surfaces after createContainer, so video (and the mpv
// OSD plane) always land above the punch layer in drawing order.
val video = PlayerSurfaceHost.createVideoSurface(activity, callback)
container.addView(video)
assertTrue(container.indexOfChild(video) > container.indexOfChild(punch))
}
}
-2
View File
@@ -58,6 +58,4 @@ dependencies {
implementation("androidx.media3:media3-ui:1.11.0")
testImplementation("junit:junit:4.13.2")
// media3 Format/MimeTypes touch android.util in static initializers.
testImplementation("org.robolectric:robolectric:4.16.1")
}
@@ -1,31 +0,0 @@
package com.edde746.plezy.libass.media
/**
* Shared policy for embedded font attachments delivered to libass: which
* attachment mime types are fonts, and the per-font/aggregate byte budgets
* that keep a hostile container from ballooning the font store.
*/
object AssFonts {
const val MAX_FONT_BYTES = 16L * 1024 * 1024
const val MAX_TOTAL_FONT_BYTES = 32L * 1024 * 1024
val fontMimeTypes = listOf(
"font/ttf",
"font/otf",
"font/sfnt",
"font/woff",
"font/woff2",
"application/font-sfnt",
"application/font-woff",
"application/x-truetype-font",
"application/vnd.ms-opentype",
"application/x-font-ttf"
)
/** Null to accept the font, or a human-readable rejection reason. */
fun rejectionReason(size: Long, acceptedBytes: Long): String? = when {
size > MAX_FONT_BYTES -> "per-font limit"
size > MAX_TOTAL_FONT_BYTES - acceptedBytes -> "aggregate limit"
else -> null
}
}
@@ -0,0 +1,169 @@
package com.edde746.plezy.libass.media.extractor
import android.util.Log
import androidx.annotation.OptIn
import androidx.media3.common.ParserException
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.common.util.UnstableApi
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.mkv.EbmlProcessor
import androidx.media3.extractor.mkv.MatroskaExtractor
import androidx.media3.extractor.text.SubtitleParser
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.text.AssSubtitleExtractorOutput
@OptIn(UnstableApi::class)
open class AssMatroskaExtractor(
subtitleParserFactory: SubtitleParser.Factory,
private val assHandler: AssHandler,
flags: Int = 0
) : MatroskaExtractor(subtitleParserFactory, flags) {
private var currentAttachmentName: String? = null
private var currentAttachmentMime: String? = null
internal var acceptedFontBytes = 0L
private set
internal val subtitleSample = subtitleSampleField.get(this) as ParsableByteArray
override fun getElementType(id: Int): Int = when (id) {
ID_ATTACHMENTS -> EbmlProcessor.ELEMENT_TYPE_MASTER
ID_ATTACHED_FILE -> EbmlProcessor.ELEMENT_TYPE_MASTER
ID_FILE_NAME -> EbmlProcessor.ELEMENT_TYPE_STRING
ID_FILE_MIME_TYPE -> EbmlProcessor.ELEMENT_TYPE_STRING
ID_FILE_DATA -> EbmlProcessor.ELEMENT_TYPE_BINARY
else -> super.getElementType(id)
}
override fun isLevel1Element(id: Int): Boolean = super.isLevel1Element(id) || id == ID_ATTACHMENTS
override fun startMasterElement(id: Int, contentPosition: Long, contentSize: Long) {
when (id) {
ID_EBML -> {
val currentExtractor = extractorOutput.get(this) as ExtractorOutput
if (currentExtractor !is AssSubtitleExtractorOutput) {
extractorOutput.set(
this,
AssSubtitleExtractorOutput(currentExtractor, assHandler, this)
)
}
super.startMasterElement(id, contentPosition, contentSize)
}
ID_ATTACHED_FILE -> clearAttachment()
else -> super.startMasterElement(id, contentPosition, contentSize)
}
}
override fun endMasterElement(id: Int) {
when (id) {
ID_VIDEO -> {
// We need to get the video dimensions very early
val track = getCurrentTrack(id)
assHandler.setVideoSize(track.width, track.height)
super.endMasterElement(id)
}
ID_ATTACHED_FILE -> clearAttachment()
else -> super.endMasterElement(id)
}
}
override fun stringElement(id: Int, value: String) {
when (id) {
ID_FILE_NAME -> currentAttachmentName = value
ID_FILE_MIME_TYPE -> currentAttachmentMime = value
else -> super.stringElement(id, value)
}
}
override fun binaryElement(id: Int, contentSize: Int, input: ExtractorInput) {
when (id) {
ID_FILE_DATA -> {
if (contentSize < 0) {
throw ParserException.createForMalformedContainer(
"Negative Matroska attachment size",
null
)
}
val attachmentName = requireNotNull(currentAttachmentName)
val attachmentMime = requireNotNull(currentAttachmentMime)
if (attachmentMime !in fontMimeTypes) {
input.skipFully(contentSize)
return
}
if (contentSize == 0) {
input.skipFully(0)
return
}
val size = contentSize.toLong()
val rejectionReason = when {
size > MAX_FONT_BYTES -> "per-font limit"
size > MAX_TOTAL_FONT_BYTES - acceptedFontBytes -> "aggregate limit"
else -> null
}
if (rejectionReason != null) {
onFontRejected(contentSize, acceptedFontBytes, rejectionReason)
input.skipFully(contentSize)
return
}
val data = ByteArray(contentSize)
input.readFully(data, 0, contentSize)
acceptedFontBytes += size
assHandler.addFont(attachmentName, data)
}
else -> super.binaryElement(id, contentSize, input)
}
}
protected open fun onFontRejected(
contentSize: Int,
acceptedBytes: Long,
reason: String
) {
Log.w(
TAG,
"Skipping embedded font: $reason (bytes=$contentSize, accepted=$acceptedBytes)"
)
}
private fun clearAttachment() {
currentAttachmentName = null
currentAttachmentMime = null
}
companion object {
private const val TAG = "AssMatroskaExtractor"
internal const val MAX_FONT_BYTES = 16L * 1024 * 1024
internal const val MAX_TOTAL_FONT_BYTES = 32L * 1024 * 1024
const val ID_EBML = 0x1A45DFA3
const val ID_VIDEO = 0xE0
const val ID_ATTACHMENTS = 0x1941A469
const val ID_ATTACHED_FILE = 0x61A7
const val ID_FILE_NAME = 0x466E
const val ID_FILE_MIME_TYPE = 0x4660
const val ID_FILE_DATA = 0x465C
val fontMimeTypes = listOf(
"font/ttf",
"font/otf",
"font/sfnt",
"font/woff",
"font/woff2",
"application/font-sfnt",
"application/font-woff",
"application/x-truetype-font",
"application/vnd.ms-opentype",
"application/x-font-ttf"
)
val extractorOutput = MatroskaExtractor::class.java.getDeclaredField("extractorOutput").apply {
isAccessible = true
}
val subtitleSampleField = MatroskaExtractor::class.java.getDeclaredField("subtitleSample").apply {
isAccessible = true
}
}
}
@@ -5,22 +5,21 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.extractor.ExtractorOutput
import androidx.media3.extractor.TrackOutput
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.extractor.AssMatroskaExtractor
/**
* Wraps every text track with [AssTrackOutput] so embedded ASS dialogue
* reaches libass. Install it on the raw-subtitle side of the output chain —
* between the extractor and the SubtitleTranscodingExtractorOutput — so the
* dialogue bytes are still unparsed when they pass through.
* This class is only used by the overlay renderer. It's needed to get the start time of the subtitles.
*/
@UnstableApi
class AssSubtitleExtractorOutput(
private val delegate: ExtractorOutput,
private val assHandler: AssHandler
private val assHandler: AssHandler,
private val extractor: AssMatroskaExtractor
) : ExtractorOutput by delegate {
override fun track(id: Int, type: Int): TrackOutput = if (type == C.TRACK_TYPE_TEXT) {
// We can't know at this time if the subtitle track is ASS or another
// format, so we wrap every subtitle track; non-ASS tracks pass through.
AssTrackOutput(delegate.track(id, type), assHandler::readTrackDialogue)
// We can't know at this time if the subtitle track is ASS or other format, so we wrap
// every subtitle track
AssTrackOutput(delegate.track(id, type), assHandler, extractor)
} else {
delegate.track(id, type)
}
@@ -1,49 +1,28 @@
package com.edde746.plezy.libass.media.text
import androidx.media3.common.C
import androidx.media3.common.DataReader
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.common.util.UnstableApi
import androidx.media3.common.util.Util
import androidx.media3.extractor.TrackOutput
import java.io.EOFException
import com.edde746.plezy.libass.media.AssHandler
import com.edde746.plezy.libass.media.extractor.AssMatroskaExtractor
import java.util.regex.Pattern
/**
* Feeds embedded ASS dialogue to libass while forwarding every sample
* unchanged. Only used by the overlay renderer, which needs the start time and
* duration of each dialogue line.
*
* Samples must arrive in the media3 MatroskaExtractor subtitle shape
* `Dialogue: 0:00:00:00,<duration timecode>,<ReadOrder,Layer,Style,...>` — the
* second field carries the sample *duration* (MatroskaExtractor patches
* BlockDuration into it; the FFmpeg extractor writes the packet duration).
* The extractor beneath must therefore emit raw subtitle samples: media3's
* MatroskaExtractor with FLAG_EMIT_RAW_SUBTITLE_DATA, or the FFmpeg demuxer
* path. Cue transcoding happens downstream of this wrapper.
* This class is only used by the overlay renderer. It's needed to get the start time of the subtitles.
*/
@UnstableApi
class AssTrackOutput(
private val delegate: TrackOutput,
private val dialogueSink: DialogueSink
) : TrackOutput {
/** Receives one dialogue line; mirrors AssHandler.readTrackDialogue. */
fun interface DialogueSink {
fun onDialogue(trackId: String?, startMs: Long, durationMs: Long, data: ByteArray, offset: Int, length: Int)
}
private val assHandler: AssHandler,
private val extractor: AssMatroskaExtractor
) : TrackOutput by delegate {
private var isAss = false
private var trackId: String? = null
// Sample bytes buffered between sampleData and sampleMetadata; only
// populated for ASS tracks.
private var buf = ByteArray(INITIAL_BUFFER_SIZE)
private var bufLen = 0
private var readBuf = ByteArray(INITIAL_BUFFER_SIZE)
private val forwardParsable = ParsableByteArray()
private var trackId: String? = null
override fun format(format: Format) {
if (format.sampleMimeType == MimeTypes.TEXT_SSA || format.codecs == MimeTypes.TEXT_SSA) {
@@ -53,34 +32,6 @@ class AssTrackOutput(
delegate.format(format)
}
override fun sampleData(
input: DataReader,
length: Int,
allowEndOfInput: Boolean,
sampleDataPart: Int
): Int {
if (!isAss) return delegate.sampleData(input, length, allowEndOfInput, sampleDataPart)
if (readBuf.size < length) readBuf = ByteArray(length)
val bytesRead = input.read(readBuf, 0, length)
if (bytesRead == C.RESULT_END_OF_INPUT) {
if (!allowEndOfInput) throw EOFException()
return C.RESULT_END_OF_INPUT
}
append(readBuf, bytesRead)
forwardParsable.reset(readBuf, bytesRead)
delegate.sampleData(forwardParsable, bytesRead, sampleDataPart)
return bytesRead
}
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
if (isAss) {
ensureCapacity(bufLen + length)
System.arraycopy(data.data, data.position, buf, bufLen, length)
bufLen += length
}
delegate.sampleData(data, length, sampleDataPart)
}
override fun sampleMetadata(
timeUs: Long,
flags: Int,
@@ -88,37 +39,26 @@ class AssTrackOutput(
offset: Int,
cryptoData: TrackOutput.CryptoData?
) {
if (isAss) {
// offset counts bytes buffered after this sample; the slice math matches
// media3's TrackOutput contract even though subtitle samples are
// normally delivered one at a time with offset 0.
val start = bufLen - offset - size
if (timeUs != C.TIME_UNSET && start >= 0) {
feedDialogue(timeUs, start, size)
}
if (offset == 0) bufLen = 0
if (isAss && timeUs.isValidTs) {
val sample = extractor.subtitleSample
val endIndex = findTokenIndex(sample.data, 1)
val lineIndex = findTokenIndex(sample.data, 2)
val rawDuration = sample.data.decodeToString(endIndex, lineIndex - 1)
val durationUs = parseTimecodeUs(rawDuration)
assHandler.readTrackDialogue(
trackId = trackId,
start = timeUs / 1000,
duration = durationUs / 1000,
data = sample.data,
offset = lineIndex,
length = sample.limit() - lineIndex
)
}
delegate.sampleMetadata(timeUs, flags, size, offset, cryptoData)
}
private fun feedDialogue(timeUs: Long, start: Int, size: Int) {
val end = start + size
val durationIndex = findTokenIndex(start, end, 1)
val lineIndex = findTokenIndex(start, end, 2)
if (durationIndex <= start || lineIndex <= durationIndex) return
val rawDuration = buf.decodeToString(durationIndex, lineIndex - 1)
val durationUs = parseTimecodeUs(rawDuration)
if (durationUs == C.TIME_UNSET) return
dialogueSink.onDialogue(
trackId,
timeUs / 1000,
durationUs / 1000,
buf,
lineIndex,
end - lineIndex
)
}
private fun parseTimecodeUs(timeString: String): Long {
val matcher = SSA_TIMECODE_PATTERN.matcher(timeString.trim { it <= ' ' })
if (!matcher.matches()) {
@@ -132,32 +72,24 @@ class AssTrackOutput(
return timestampUs
}
/** Index just past the [tokenNumber]th comma inside [from, to), or [from]. */
private fun findTokenIndex(from: Int, to: Int, tokenNumber: Int): Int {
private fun findTokenIndex(array: ByteArray, tokenNumber: Int): Int {
if (tokenNumber == 0) return 0
var tokensFound = 0
for (index in from until to) {
if (buf[index] == COMMA && ++tokensFound == tokenNumber) {
array.forEachIndexed { index, byte ->
if (byte == COMMA && ++tokensFound == tokenNumber) {
return index + 1
}
}
return from
return 0
}
private fun append(src: ByteArray, length: Int) {
ensureCapacity(bufLen + length)
System.arraycopy(src, 0, buf, bufLen, length)
bufLen += length
}
private fun ensureCapacity(needed: Int) {
if (buf.size < needed) buf = buf.copyOf(maxOf(needed, buf.size * 2))
}
private val Long.isValidTs
get() = this != C.TIME_UNSET
private companion object {
val SSA_TIMECODE_PATTERN: Pattern =
Pattern.compile("""(?:(\d+):)?(\d+):(\d+)[:.](\d+)""")
const val COMMA = ','.code.toByte()
const val INITIAL_BUFFER_SIZE = 1024
}
}
@@ -0,0 +1,156 @@
package com.edde746.plezy.libass.media.extractor
import androidx.media3.common.C
import androidx.media3.common.DataReader
import androidx.media3.common.ParserException
import androidx.media3.extractor.DefaultExtractorInput
import androidx.media3.extractor.ExtractorInput
import androidx.media3.extractor.text.DefaultSubtitleParserFactory
import com.edde746.plezy.libass.media.AssHandler
import java.io.EOFException
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertThrows
import org.junit.Test
class AssMatroskaExtractorTest {
@Test
fun smallFontIsDeliveredAndNonFontDoesNotConsumeBudget() {
val handler = AssHandler()
val extractor = extractor(handler)
attachment(extractor, "font/ttf")
extractor.fileData(4, input(4, seed = 11))
attachment(extractor, "application/octet-stream")
extractor.fileData(9, input(9, seed = 22))
val pending = handler.fontStore.pendingSnapshot()
assertEquals(1, pending.size)
assertEquals("fixture-font", pending.single().first)
assertArrayEquals(byteArrayOf(11, 12, 13, 14), pending.single().second)
assertEquals(4L, extractor.acceptedFontBytes)
}
@Test
fun perFontLimitAcceptsExactBoundaryAndSkipsOneByteOver() {
val handler = AssHandler()
val extractor = extractor(handler)
val limit = AssMatroskaExtractor.MAX_FONT_BYTES.toInt()
attachment(extractor, "font/otf")
extractor.fileData(limit, input(limit))
attachment(extractor, "font/otf")
extractor.fileData(limit + 1, input(limit + 1))
assertEquals(limit.toLong(), extractor.acceptedFontBytes)
assertEquals(1, handler.fontStore.pendingSnapshot().size)
}
@Test
fun aggregateLimitIsDeterministicAcrossAttachmentEntries() {
val handler = AssHandler()
val extractor = extractor(handler)
val perFont = AssMatroskaExtractor.MAX_FONT_BYTES.toInt()
repeat(2) {
attachment(extractor, "font/ttf")
extractor.fileData(perFont, input(perFont, seed = it))
extractor.endAttachment()
}
attachment(extractor, "font/ttf")
extractor.fileData(1, input(1))
assertEquals(AssMatroskaExtractor.MAX_TOTAL_FONT_BYTES, extractor.acceptedFontBytes)
assertEquals(2, handler.fontStore.pendingSnapshot().size)
}
@Test
fun negativeAndZeroSizesAllocateAndDeliverNothing() {
val handler = AssHandler()
val extractor = extractor(handler)
attachment(extractor, "font/woff2")
assertThrows(ParserException::class.java) {
extractor.fileData(-1, input(0))
}
attachment(extractor, "font/woff2")
extractor.fileData(0, input(0))
assertEquals(0L, extractor.acceptedFontBytes)
assertEquals(0, handler.fontStore.pendingSnapshot().size)
}
@Test
fun failedReadDoesNotChargeAggregateBudgetOrDeliverPartialFont() {
val handler = AssHandler()
val extractor = extractor(handler)
attachment(extractor, "font/ttf")
assertThrows(EOFException::class.java) {
extractor.fileData(1024, input(1024, available = 4))
}
assertEquals(0L, extractor.acceptedFontBytes)
assertEquals(0, handler.fontStore.pendingSnapshot().size)
attachment(extractor, "font/ttf")
extractor.fileData(1024, input(1024))
assertEquals(1024L, extractor.acceptedFontBytes)
assertEquals(1, handler.fontStore.pendingSnapshot().size)
}
private fun extractor(handler: AssHandler) = TestExtractor(handler)
private fun attachment(extractor: TestExtractor, mime: String) {
extractor.setAttachment(mime)
}
private class TestExtractor(handler: AssHandler) :
AssMatroskaExtractor(
DefaultSubtitleParserFactory(),
handler
) {
fun setAttachment(mime: String) {
startMasterElement(ID_ATTACHED_FILE, 0, 0)
stringElement(ID_FILE_NAME, "fixture-font")
stringElement(ID_FILE_MIME_TYPE, mime)
}
fun fileData(contentSize: Int, input: ExtractorInput) {
binaryElement(ID_FILE_DATA, contentSize, input)
}
fun endAttachment() {
endMasterElement(ID_ATTACHED_FILE)
}
override fun onFontRejected(contentSize: Int, acceptedBytes: Long, reason: String) = Unit
}
private fun input(
declared: Int,
available: Int = declared,
seed: Int = 0
): ExtractorInput = DefaultExtractorInput(
PatternDataReader(available, seed),
0,
declared.toLong()
)
private class PatternDataReader(
private val size: Int,
private val seed: Int
) : DataReader {
private var position = 0
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (position >= size) return C.RESULT_END_OF_INPUT
val count = minOf(length, size - position)
for (index in 0 until count) {
buffer[offset + index] = ((seed + position + index) and 0xFF).toByte()
}
position += count
return count
}
}
}
@@ -1,157 +0,0 @@
package com.edde746.plezy.libass.media.text
import androidx.media3.common.C
import androidx.media3.common.Format
import androidx.media3.common.MimeTypes
import androidx.media3.common.util.ParsableByteArray
import androidx.media3.extractor.TrackOutput
import org.junit.Assert.assertArrayEquals
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
/**
* Spec for the dialogue feed both demuxer paths rely on: samples arrive in
* media3's MatroskaExtractor shape ("Dialogue: 0:00:00:00,<duration>,<rest>")
* and the sink must receive the sample start, the parsed duration, and the
* payload after the second comma — while every byte is still forwarded
* unchanged to the delegate.
*/
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [34])
class AssTrackOutputTest {
private class CapturedDialogue(val trackId: String?, val startMs: Long, val durationMs: Long, val line: ByteArray)
private class RecordingSink : AssTrackOutput.DialogueSink {
val dialogues = mutableListOf<CapturedDialogue>()
override fun onDialogue(trackId: String?, startMs: Long, durationMs: Long, data: ByteArray, offset: Int, length: Int) {
dialogues.add(CapturedDialogue(trackId, startMs, durationMs, data.copyOfRange(offset, offset + length)))
}
}
private class RecordingTrackOutput : TrackOutput {
val formats = mutableListOf<Format>()
val samples = mutableListOf<ByteArray>()
val sampleTimes = mutableListOf<Long>()
private var buf = ByteArray(1024)
private var bufLen = 0
override fun format(format: Format) {
formats.add(format)
}
override fun sampleData(input: androidx.media3.common.DataReader, length: Int, allowEndOfInput: Boolean, sampleDataPart: Int): Int {
if (buf.size < bufLen + length) buf = buf.copyOf(bufLen + length)
val read = input.read(buf, bufLen, length)
if (read > 0) bufLen += read
return read
}
override fun sampleData(data: ParsableByteArray, length: Int, sampleDataPart: Int) {
if (buf.size < bufLen + length) buf = buf.copyOf(bufLen + length)
data.readBytes(buf, bufLen, length)
bufLen += length
}
override fun sampleMetadata(timeUs: Long, flags: Int, size: Int, offset: Int, cryptoData: TrackOutput.CryptoData?) {
val start = bufLen - offset - size
samples.add(buf.copyOfRange(start, start + size))
sampleTimes.add(timeUs)
if (offset == 0) bufLen = 0
}
}
private fun ssaFormat(id: String = "3"): Format = Format.Builder()
.setId(id)
.setSampleMimeType(MimeTypes.TEXT_SSA)
.build()
private fun deliver(output: AssTrackOutput, sample: String, timeUs: Long) {
val bytes = sample.toByteArray(Charsets.UTF_8)
output.sampleData(ParsableByteArray(bytes), bytes.size, TrackOutput.SAMPLE_DATA_PART_MAIN)
output.sampleMetadata(timeUs, C.BUFFER_FLAG_KEY_FRAME, bytes.size, 0, null)
}
@Test
fun feedsDialogueAndForwardsSampleUnchanged() {
val sink = RecordingSink()
val delegate = RecordingTrackOutput()
val output = AssTrackOutput(delegate, sink)
output.format(ssaFormat())
val sample = "Dialogue: 0:00:00:00,0:00:05:00,7,0,Default,,0,0,0,,Hello, world"
deliver(output, sample, 90_000_000L)
assertEquals(1, sink.dialogues.size)
val dialogue = sink.dialogues.first()
assertEquals("3", dialogue.trackId)
assertEquals(90_000L, dialogue.startMs)
assertEquals(5_000L, dialogue.durationMs)
// Payload after the second comma: the raw MKV/FFmpeg block, commas in the
// text field intact.
assertEquals("7,0,Default,,0,0,0,,Hello, world", dialogue.line.toString(Charsets.UTF_8))
// The delegate still receives the full untouched sample.
assertEquals(1, delegate.samples.size)
assertArrayEquals(sample.toByteArray(Charsets.UTF_8), delegate.samples.first())
assertEquals(90_000_000L, delegate.sampleTimes.first())
}
@Test
fun ignoresNonAssTracksAndMalformedSamples() {
val sink = RecordingSink()
val delegate = RecordingTrackOutput()
val output = AssTrackOutput(delegate, sink)
// SubRip track: forwarded, never fed to libass.
output.format(Format.Builder().setId("1").setSampleMimeType(MimeTypes.APPLICATION_SUBRIP).build())
deliver(output, "1\n00:00:00,000 --> 00:00:05,000\nplain text", 1_000_000L)
assertTrue(sink.dialogues.isEmpty())
assertEquals(1, delegate.samples.size)
// ASS track with an unparseable duration: forwarded, not fed.
val assOutput = AssTrackOutput(RecordingTrackOutput(), sink)
assOutput.format(ssaFormat())
deliver(assOutput, "Dialogue: bogus", 1_000_000L)
assertTrue(sink.dialogues.isEmpty())
}
@Test
fun handlesChunkedSampleDataAndDataReaderPath() {
val sink = RecordingSink()
val delegate = RecordingTrackOutput()
val output = AssTrackOutput(delegate, sink)
output.format(ssaFormat("0"))
val sample = "Dialogue: 0:00:00:00,0:00:01:50,1,0,Style,,0,0,0,,Line"
val bytes = sample.toByteArray(Charsets.UTF_8)
val split = bytes.size / 2
output.sampleData(ParsableByteArray(bytes.copyOfRange(0, split)), split, TrackOutput.SAMPLE_DATA_PART_MAIN)
val rest = bytes.copyOfRange(split, bytes.size)
var consumed = 0
val reader = androidx.media3.common.DataReader { buffer, offset, length ->
if (consumed >= rest.size) {
C.RESULT_END_OF_INPUT
} else {
val toRead = minOf(length, rest.size - consumed)
System.arraycopy(rest, consumed, buffer, offset, toRead)
consumed += toRead
toRead
}
}
while (consumed < rest.size) {
output.sampleData(reader, rest.size - consumed, false, TrackOutput.SAMPLE_DATA_PART_MAIN)
}
output.sampleMetadata(0L, C.BUFFER_FLAG_KEY_FRAME, bytes.size, 0, null)
assertEquals(1, sink.dialogues.size)
assertEquals(1_500L, sink.dialogues.first().durationMs)
assertEquals("1,0,Style,,0,0,0,,Line", sink.dialogues.first().line.toString(Charsets.UTF_8))
assertArrayEquals(bytes, delegate.samples.first())
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/universal_gamepad/ios"
SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
Flutter: 71a624a5bc0c04062bf19101d501e466baf2fb47
os_media_controls: 048eb9a75974191b2496d85575b6edcefc572d4e
universal_gamepad: 7c0cc0c2e3909dfb803d325387e42963b3ed842c
+5 -1
View File
@@ -25,6 +25,7 @@
B1D51A6A2F0011000000000C /* MpvAudioPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F0011000000000D /* MpvAudioPlayerCore.swift */; };
B1D51A6A2F0011000000000E /* MpvAudioPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F0011000000000F /* MpvAudioPlayerPlugin.swift */; };
B1D51A6A2F00110000000008 /* ExternalDisplayManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000007 /* ExternalDisplayManager.swift */; };
B1D51A6A2F00110000000010 /* VideoDecodeCapabilitiesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = B1D51A6A2F00110000000011 /* VideoDecodeCapabilitiesPlugin.swift */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
/* End PBXBuildFile section */
@@ -80,6 +81,7 @@
B1D51A6A2F0011000000000D /* MpvAudioPlayerCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvAudioPlayerCore.swift; path = ../shared/apple/MpvPlayer/MpvAudioPlayerCore.swift; sourceTree = SOURCE_ROOT; };
B1D51A6A2F0011000000000F /* MpvAudioPlayerPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MpvAudioPlayerPlugin.swift; path = ../shared/apple/MpvPlayer/MpvAudioPlayerPlugin.swift; sourceTree = SOURCE_ROOT; };
B1D51A6A2F00110000000007 /* ExternalDisplayManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalDisplayManager.swift; sourceTree = "<group>"; };
B1D51A6A2F00110000000011 /* VideoDecodeCapabilitiesPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDecodeCapabilitiesPlugin.swift; sourceTree = "<group>"; };
BB346A1D0705AB171F80B11B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DE48FF2E074644167608B23D /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
F3553CDE70191F0FC81A782E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
@@ -197,6 +199,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
B1D51A6A2F00110000000011 /* VideoDecodeCapabilitiesPlugin.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
@@ -430,6 +433,7 @@
6A8A46262EDB370C0057B88C /* MpvPlayerCore.swift in Sources */,
92F969587D0E464D999910F5 /* MpvPipController.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
B1D51A6A2F00110000000010 /* VideoDecodeCapabilitiesPlugin.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -798,7 +802,7 @@
repositoryURL = "https://github.com/edde746/MPVKit";
requirement = {
kind = revision;
revision = 0097fea015fe9b59bcfed866e61acb5896454bd4;
revision = b8b922ec74b84ac3a496e29e226a3a3e91491045;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -32,7 +32,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "0097fea015fe9b59bcfed866e61acb5896454bd4"
"revision" : "b8b922ec74b84ac3a496e29e226a3a3e91491045"
}
},
{
@@ -32,7 +32,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"revision" : "0097fea015fe9b59bcfed866e61acb5896454bd4"
"revision" : "b8b922ec74b84ac3a496e29e226a3a3e91491045"
}
},
{
+4
View File
@@ -38,6 +38,10 @@ import MediaPlayer
MpvAudioPlayerPlugin.register(with: registrar)
}
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "VideoDecodeCapabilitiesPlugin") {
VideoDecodeCapabilitiesPlugin.register(with: registrar)
}
if let registrar = engineBridge.pluginRegistry.registrar(forPlugin: "DeviceAdjustmentChannel") {
registerDeviceAdjustmentChannel(messenger: registrar.messenger())
}
@@ -0,0 +1,32 @@
import CoreMedia
import Flutter
import VideoToolbox
/// Answers `com.plezy/device`'s `getVideoDecodeCapabilities` on iOS and tvOS.
///
/// Compiled into the iOS and tvOS Runners only — macOS keeps its own sources
/// and is deliberately left unprobed, because desktop CPUs software-decode
/// both codecs in real time and narrowing the profile there would force
/// transcodes for nothing.
class VideoDecodeCapabilitiesPlugin: NSObject, FlutterPlugin {
static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.plezy/device",
binaryMessenger: registrar.messenger()
)
registrar.addMethodCallDelegate(VideoDecodeCapabilitiesPlugin(), channel: channel)
}
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getVideoDecodeCapabilities":
result([
"hevc": VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC),
"av1": VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1),
])
default:
// The rest of `com.plezy/device` is Android-only.
result(FlutterMethodNotImplemented)
}
}
}
+38 -1
View File
@@ -58,6 +58,7 @@ final class AppDatabaseBootstrap {
Connections,
Profiles,
ProfileConnections,
MusicSessions,
],
)
class AppDatabase extends _$AppDatabase {
@@ -365,7 +366,7 @@ class AppDatabase extends _$AppDatabase {
static const FormatException _invalidRecoveryImage = FormatException('Invalid tvOS database recovery image');
@override
int get schemaVersion => 21;
int get schemaVersion => 22;
@override
MigrationStrategy get migration {
@@ -735,6 +736,10 @@ class AppDatabase extends _$AppDatabase {
appLogger.i('Dropping unused Connections.isDefault column (v21 migration)');
await m.alterTable(TableMigration(connections));
}
if (from < 22) {
appLogger.i('Adding MusicSessions table (v22 migration)');
await _ignoreAlreadyExists('MusicSessions table', () => m.createTable(musicSessions));
}
},
);
}
@@ -1094,6 +1099,38 @@ class AppDatabase extends _$AppDatabase {
});
}
// ===========================================================================
// Music session persistence (#2148)
// ===========================================================================
/// Full snapshot write: replaces the profile's persisted music session.
Future<void> upsertMusicSession(MusicSessionRow row) {
return into(musicSessions).insertOnConflictUpdate(row);
}
/// Cheap write-through for playhead/cursor changes — leaves the (possibly
/// large) queue JSON untouched. No-op when no snapshot row exists.
Future<void> updateMusicSessionProgress({
required String profileId,
required int cursor,
required int positionMs,
required int updatedAt,
}) async {
await (update(musicSessions)..where((t) => t.profileId.equals(profileId))).write(
MusicSessionsCompanion(cursor: Value(cursor), positionMs: Value(positionMs), updatedAt: Value(updatedAt)),
);
}
Future<MusicSessionRow?> getMusicSession(String profileId) {
return (select(musicSessions)..where((t) => t.profileId.equals(profileId))).getSingleOrNull();
}
/// Drop a profile's persisted music session (user session end or profile
/// teardown).
Future<void> deleteMusicSessionForProfile(String profileId) async {
await (delete(musicSessions)..where((t) => t.profileId.equals(profileId))).go();
}
Future<List<SyncRuleItem>> getSyncRules({String? profileId}) {
final query = select(syncRules);
if (profileId != null) {
+948
View File
@@ -5769,6 +5769,647 @@ class ProfileConnectionsCompanion
}
}
class $MusicSessionsTable extends MusicSessions
with TableInfo<$MusicSessionsTable, MusicSessionRow> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$MusicSessionsTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _profileIdMeta = const VerificationMeta(
'profileId',
);
@override
late final GeneratedColumn<String> profileId = GeneratedColumn<String>(
'profile_id',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _queueJsonMeta = const VerificationMeta(
'queueJson',
);
@override
late final GeneratedColumn<String> queueJson = GeneratedColumn<String>(
'queue_json',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _orderJsonMeta = const VerificationMeta(
'orderJson',
);
@override
late final GeneratedColumn<String> orderJson = GeneratedColumn<String>(
'order_json',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: true,
);
static const VerificationMeta _cursorMeta = const VerificationMeta('cursor');
@override
late final GeneratedColumn<int> cursor = GeneratedColumn<int>(
'cursor',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
);
static const VerificationMeta _shuffledMeta = const VerificationMeta(
'shuffled',
);
@override
late final GeneratedColumn<bool> shuffled = GeneratedColumn<bool>(
'shuffled',
aliasedName,
false,
type: DriftSqlType.bool,
requiredDuringInsert: false,
defaultConstraints: GeneratedColumn.constraintIsAlways(
'CHECK ("shuffled" IN (0, 1))',
),
defaultValue: const Constant(false),
);
static const VerificationMeta _repeatModeMeta = const VerificationMeta(
'repeatMode',
);
@override
late final GeneratedColumn<String> repeatMode = GeneratedColumn<String>(
'repeat_mode',
aliasedName,
false,
type: DriftSqlType.string,
requiredDuringInsert: false,
defaultValue: const Constant('off'),
);
static const VerificationMeta _contextTitleMeta = const VerificationMeta(
'contextTitle',
);
@override
late final GeneratedColumn<String> contextTitle = GeneratedColumn<String>(
'context_title',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _contextKindMeta = const VerificationMeta(
'contextKind',
);
@override
late final GeneratedColumn<String> contextKind = GeneratedColumn<String>(
'context_kind',
aliasedName,
true,
type: DriftSqlType.string,
requiredDuringInsert: false,
);
static const VerificationMeta _positionMsMeta = const VerificationMeta(
'positionMs',
);
@override
late final GeneratedColumn<int> positionMs = GeneratedColumn<int>(
'position_ms',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultValue: const Constant(0),
);
static const VerificationMeta _updatedAtMeta = const VerificationMeta(
'updatedAt',
);
@override
late final GeneratedColumn<int> updatedAt = GeneratedColumn<int>(
'updated_at',
aliasedName,
false,
type: DriftSqlType.int,
requiredDuringInsert: true,
);
@override
List<GeneratedColumn> get $columns => [
profileId,
queueJson,
orderJson,
cursor,
shuffled,
repeatMode,
contextTitle,
contextKind,
positionMs,
updatedAt,
];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'music_sessions';
@override
VerificationContext validateIntegrity(
Insertable<MusicSessionRow> instance, {
bool isInserting = false,
}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('profile_id')) {
context.handle(
_profileIdMeta,
profileId.isAcceptableOrUnknown(data['profile_id']!, _profileIdMeta),
);
} else if (isInserting) {
context.missing(_profileIdMeta);
}
if (data.containsKey('queue_json')) {
context.handle(
_queueJsonMeta,
queueJson.isAcceptableOrUnknown(data['queue_json']!, _queueJsonMeta),
);
} else if (isInserting) {
context.missing(_queueJsonMeta);
}
if (data.containsKey('order_json')) {
context.handle(
_orderJsonMeta,
orderJson.isAcceptableOrUnknown(data['order_json']!, _orderJsonMeta),
);
} else if (isInserting) {
context.missing(_orderJsonMeta);
}
if (data.containsKey('cursor')) {
context.handle(
_cursorMeta,
cursor.isAcceptableOrUnknown(data['cursor']!, _cursorMeta),
);
} else if (isInserting) {
context.missing(_cursorMeta);
}
if (data.containsKey('shuffled')) {
context.handle(
_shuffledMeta,
shuffled.isAcceptableOrUnknown(data['shuffled']!, _shuffledMeta),
);
}
if (data.containsKey('repeat_mode')) {
context.handle(
_repeatModeMeta,
repeatMode.isAcceptableOrUnknown(data['repeat_mode']!, _repeatModeMeta),
);
}
if (data.containsKey('context_title')) {
context.handle(
_contextTitleMeta,
contextTitle.isAcceptableOrUnknown(
data['context_title']!,
_contextTitleMeta,
),
);
}
if (data.containsKey('context_kind')) {
context.handle(
_contextKindMeta,
contextKind.isAcceptableOrUnknown(
data['context_kind']!,
_contextKindMeta,
),
);
}
if (data.containsKey('position_ms')) {
context.handle(
_positionMsMeta,
positionMs.isAcceptableOrUnknown(data['position_ms']!, _positionMsMeta),
);
}
if (data.containsKey('updated_at')) {
context.handle(
_updatedAtMeta,
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
);
} else if (isInserting) {
context.missing(_updatedAtMeta);
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {profileId};
@override
MusicSessionRow map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return MusicSessionRow(
profileId: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}profile_id'],
)!,
queueJson: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}queue_json'],
)!,
orderJson: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}order_json'],
)!,
cursor: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}cursor'],
)!,
shuffled: attachedDatabase.typeMapping.read(
DriftSqlType.bool,
data['${effectivePrefix}shuffled'],
)!,
repeatMode: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}repeat_mode'],
)!,
contextTitle: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}context_title'],
),
contextKind: attachedDatabase.typeMapping.read(
DriftSqlType.string,
data['${effectivePrefix}context_kind'],
),
positionMs: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}position_ms'],
)!,
updatedAt: attachedDatabase.typeMapping.read(
DriftSqlType.int,
data['${effectivePrefix}updated_at'],
)!,
);
}
@override
$MusicSessionsTable createAlias(String alias) {
return $MusicSessionsTable(attachedDatabase, alias);
}
}
class MusicSessionRow extends DataClass implements Insertable<MusicSessionRow> {
/// Active Plezy profile that owns this session snapshot.
final String profileId;
/// Canonical queue tracks (insertion order) as a JSON array of MediaItem
/// JSON — self-contained so restore never depends on volatile cache rows.
final String queueJson;
/// Playback-order permutation into [queueJson] as a JSON int array
/// (identity while unshuffled).
final String orderJson;
/// Position of the current track within the playback order.
final int cursor;
final bool shuffled;
/// Stable repeat-mode id ('off' | 'all' | 'one') — not the enum `.name`.
final String repeatMode;
/// Play-context provenance ("Playing from …"); kind is a stable id.
final String? contextTitle;
final String? contextKind;
/// Playhead within the current track in milliseconds.
final int positionMs;
/// Timestamp of the last write (milliseconds since epoch).
final int updatedAt;
const MusicSessionRow({
required this.profileId,
required this.queueJson,
required this.orderJson,
required this.cursor,
required this.shuffled,
required this.repeatMode,
this.contextTitle,
this.contextKind,
required this.positionMs,
required this.updatedAt,
});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['profile_id'] = Variable<String>(profileId);
map['queue_json'] = Variable<String>(queueJson);
map['order_json'] = Variable<String>(orderJson);
map['cursor'] = Variable<int>(cursor);
map['shuffled'] = Variable<bool>(shuffled);
map['repeat_mode'] = Variable<String>(repeatMode);
if (!nullToAbsent || contextTitle != null) {
map['context_title'] = Variable<String>(contextTitle);
}
if (!nullToAbsent || contextKind != null) {
map['context_kind'] = Variable<String>(contextKind);
}
map['position_ms'] = Variable<int>(positionMs);
map['updated_at'] = Variable<int>(updatedAt);
return map;
}
MusicSessionsCompanion toCompanion(bool nullToAbsent) {
return MusicSessionsCompanion(
profileId: Value(profileId),
queueJson: Value(queueJson),
orderJson: Value(orderJson),
cursor: Value(cursor),
shuffled: Value(shuffled),
repeatMode: Value(repeatMode),
contextTitle: contextTitle == null && nullToAbsent
? const Value.absent()
: Value(contextTitle),
contextKind: contextKind == null && nullToAbsent
? const Value.absent()
: Value(contextKind),
positionMs: Value(positionMs),
updatedAt: Value(updatedAt),
);
}
factory MusicSessionRow.fromJson(
Map<String, dynamic> json, {
ValueSerializer? serializer,
}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return MusicSessionRow(
profileId: serializer.fromJson<String>(json['profileId']),
queueJson: serializer.fromJson<String>(json['queueJson']),
orderJson: serializer.fromJson<String>(json['orderJson']),
cursor: serializer.fromJson<int>(json['cursor']),
shuffled: serializer.fromJson<bool>(json['shuffled']),
repeatMode: serializer.fromJson<String>(json['repeatMode']),
contextTitle: serializer.fromJson<String?>(json['contextTitle']),
contextKind: serializer.fromJson<String?>(json['contextKind']),
positionMs: serializer.fromJson<int>(json['positionMs']),
updatedAt: serializer.fromJson<int>(json['updatedAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'profileId': serializer.toJson<String>(profileId),
'queueJson': serializer.toJson<String>(queueJson),
'orderJson': serializer.toJson<String>(orderJson),
'cursor': serializer.toJson<int>(cursor),
'shuffled': serializer.toJson<bool>(shuffled),
'repeatMode': serializer.toJson<String>(repeatMode),
'contextTitle': serializer.toJson<String?>(contextTitle),
'contextKind': serializer.toJson<String?>(contextKind),
'positionMs': serializer.toJson<int>(positionMs),
'updatedAt': serializer.toJson<int>(updatedAt),
};
}
MusicSessionRow copyWith({
String? profileId,
String? queueJson,
String? orderJson,
int? cursor,
bool? shuffled,
String? repeatMode,
Value<String?> contextTitle = const Value.absent(),
Value<String?> contextKind = const Value.absent(),
int? positionMs,
int? updatedAt,
}) => MusicSessionRow(
profileId: profileId ?? this.profileId,
queueJson: queueJson ?? this.queueJson,
orderJson: orderJson ?? this.orderJson,
cursor: cursor ?? this.cursor,
shuffled: shuffled ?? this.shuffled,
repeatMode: repeatMode ?? this.repeatMode,
contextTitle: contextTitle.present ? contextTitle.value : this.contextTitle,
contextKind: contextKind.present ? contextKind.value : this.contextKind,
positionMs: positionMs ?? this.positionMs,
updatedAt: updatedAt ?? this.updatedAt,
);
MusicSessionRow copyWithCompanion(MusicSessionsCompanion data) {
return MusicSessionRow(
profileId: data.profileId.present ? data.profileId.value : this.profileId,
queueJson: data.queueJson.present ? data.queueJson.value : this.queueJson,
orderJson: data.orderJson.present ? data.orderJson.value : this.orderJson,
cursor: data.cursor.present ? data.cursor.value : this.cursor,
shuffled: data.shuffled.present ? data.shuffled.value : this.shuffled,
repeatMode: data.repeatMode.present
? data.repeatMode.value
: this.repeatMode,
contextTitle: data.contextTitle.present
? data.contextTitle.value
: this.contextTitle,
contextKind: data.contextKind.present
? data.contextKind.value
: this.contextKind,
positionMs: data.positionMs.present
? data.positionMs.value
: this.positionMs,
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
);
}
@override
String toString() {
return (StringBuffer('MusicSessionRow(')
..write('profileId: $profileId, ')
..write('queueJson: $queueJson, ')
..write('orderJson: $orderJson, ')
..write('cursor: $cursor, ')
..write('shuffled: $shuffled, ')
..write('repeatMode: $repeatMode, ')
..write('contextTitle: $contextTitle, ')
..write('contextKind: $contextKind, ')
..write('positionMs: $positionMs, ')
..write('updatedAt: $updatedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(
profileId,
queueJson,
orderJson,
cursor,
shuffled,
repeatMode,
contextTitle,
contextKind,
positionMs,
updatedAt,
);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is MusicSessionRow &&
other.profileId == this.profileId &&
other.queueJson == this.queueJson &&
other.orderJson == this.orderJson &&
other.cursor == this.cursor &&
other.shuffled == this.shuffled &&
other.repeatMode == this.repeatMode &&
other.contextTitle == this.contextTitle &&
other.contextKind == this.contextKind &&
other.positionMs == this.positionMs &&
other.updatedAt == this.updatedAt);
}
class MusicSessionsCompanion extends UpdateCompanion<MusicSessionRow> {
final Value<String> profileId;
final Value<String> queueJson;
final Value<String> orderJson;
final Value<int> cursor;
final Value<bool> shuffled;
final Value<String> repeatMode;
final Value<String?> contextTitle;
final Value<String?> contextKind;
final Value<int> positionMs;
final Value<int> updatedAt;
final Value<int> rowid;
const MusicSessionsCompanion({
this.profileId = const Value.absent(),
this.queueJson = const Value.absent(),
this.orderJson = const Value.absent(),
this.cursor = const Value.absent(),
this.shuffled = const Value.absent(),
this.repeatMode = const Value.absent(),
this.contextTitle = const Value.absent(),
this.contextKind = const Value.absent(),
this.positionMs = const Value.absent(),
this.updatedAt = const Value.absent(),
this.rowid = const Value.absent(),
});
MusicSessionsCompanion.insert({
required String profileId,
required String queueJson,
required String orderJson,
required int cursor,
this.shuffled = const Value.absent(),
this.repeatMode = const Value.absent(),
this.contextTitle = const Value.absent(),
this.contextKind = const Value.absent(),
this.positionMs = const Value.absent(),
required int updatedAt,
this.rowid = const Value.absent(),
}) : profileId = Value(profileId),
queueJson = Value(queueJson),
orderJson = Value(orderJson),
cursor = Value(cursor),
updatedAt = Value(updatedAt);
static Insertable<MusicSessionRow> custom({
Expression<String>? profileId,
Expression<String>? queueJson,
Expression<String>? orderJson,
Expression<int>? cursor,
Expression<bool>? shuffled,
Expression<String>? repeatMode,
Expression<String>? contextTitle,
Expression<String>? contextKind,
Expression<int>? positionMs,
Expression<int>? updatedAt,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (profileId != null) 'profile_id': profileId,
if (queueJson != null) 'queue_json': queueJson,
if (orderJson != null) 'order_json': orderJson,
if (cursor != null) 'cursor': cursor,
if (shuffled != null) 'shuffled': shuffled,
if (repeatMode != null) 'repeat_mode': repeatMode,
if (contextTitle != null) 'context_title': contextTitle,
if (contextKind != null) 'context_kind': contextKind,
if (positionMs != null) 'position_ms': positionMs,
if (updatedAt != null) 'updated_at': updatedAt,
if (rowid != null) 'rowid': rowid,
});
}
MusicSessionsCompanion copyWith({
Value<String>? profileId,
Value<String>? queueJson,
Value<String>? orderJson,
Value<int>? cursor,
Value<bool>? shuffled,
Value<String>? repeatMode,
Value<String?>? contextTitle,
Value<String?>? contextKind,
Value<int>? positionMs,
Value<int>? updatedAt,
Value<int>? rowid,
}) {
return MusicSessionsCompanion(
profileId: profileId ?? this.profileId,
queueJson: queueJson ?? this.queueJson,
orderJson: orderJson ?? this.orderJson,
cursor: cursor ?? this.cursor,
shuffled: shuffled ?? this.shuffled,
repeatMode: repeatMode ?? this.repeatMode,
contextTitle: contextTitle ?? this.contextTitle,
contextKind: contextKind ?? this.contextKind,
positionMs: positionMs ?? this.positionMs,
updatedAt: updatedAt ?? this.updatedAt,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (profileId.present) {
map['profile_id'] = Variable<String>(profileId.value);
}
if (queueJson.present) {
map['queue_json'] = Variable<String>(queueJson.value);
}
if (orderJson.present) {
map['order_json'] = Variable<String>(orderJson.value);
}
if (cursor.present) {
map['cursor'] = Variable<int>(cursor.value);
}
if (shuffled.present) {
map['shuffled'] = Variable<bool>(shuffled.value);
}
if (repeatMode.present) {
map['repeat_mode'] = Variable<String>(repeatMode.value);
}
if (contextTitle.present) {
map['context_title'] = Variable<String>(contextTitle.value);
}
if (contextKind.present) {
map['context_kind'] = Variable<String>(contextKind.value);
}
if (positionMs.present) {
map['position_ms'] = Variable<int>(positionMs.value);
}
if (updatedAt.present) {
map['updated_at'] = Variable<int>(updatedAt.value);
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('MusicSessionsCompanion(')
..write('profileId: $profileId, ')
..write('queueJson: $queueJson, ')
..write('orderJson: $orderJson, ')
..write('cursor: $cursor, ')
..write('shuffled: $shuffled, ')
..write('repeatMode: $repeatMode, ')
..write('contextTitle: $contextTitle, ')
..write('contextKind: $contextKind, ')
..write('positionMs: $positionMs, ')
..write('updatedAt: $updatedAt, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
@@ -5787,6 +6428,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
late final $ProfilesTable profiles = $ProfilesTable(this);
late final $ProfileConnectionsTable profileConnections =
$ProfileConnectionsTable(this);
late final $MusicSessionsTable musicSessions = $MusicSessionsTable(this);
late final Index idxDownloadedMediaStatus = Index(
'idx_downloaded_media_status',
'CREATE INDEX idx_downloaded_media_status ON downloaded_media (status)',
@@ -5858,6 +6500,7 @@ abstract class _$AppDatabase extends GeneratedDatabase {
connections,
profiles,
profileConnections,
musicSessions,
idxDownloadedMediaStatus,
idxDownloadedMediaServer,
idxDownloadedMediaParent,
@@ -9190,6 +9833,309 @@ typedef $$ProfileConnectionsTableProcessedTableManager =
ProfileConnectionRow,
PrefetchHooks Function({bool connectionId})
>;
typedef $$MusicSessionsTableCreateCompanionBuilder =
MusicSessionsCompanion Function({
required String profileId,
required String queueJson,
required String orderJson,
required int cursor,
Value<bool> shuffled,
Value<String> repeatMode,
Value<String?> contextTitle,
Value<String?> contextKind,
Value<int> positionMs,
required int updatedAt,
Value<int> rowid,
});
typedef $$MusicSessionsTableUpdateCompanionBuilder =
MusicSessionsCompanion Function({
Value<String> profileId,
Value<String> queueJson,
Value<String> orderJson,
Value<int> cursor,
Value<bool> shuffled,
Value<String> repeatMode,
Value<String?> contextTitle,
Value<String?> contextKind,
Value<int> positionMs,
Value<int> updatedAt,
Value<int> rowid,
});
class $$MusicSessionsTableFilterComposer
extends Composer<_$AppDatabase, $MusicSessionsTable> {
$$MusicSessionsTableFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get profileId => $composableBuilder(
column: $table.profileId,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get queueJson => $composableBuilder(
column: $table.queueJson,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get orderJson => $composableBuilder(
column: $table.orderJson,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get cursor => $composableBuilder(
column: $table.cursor,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<bool> get shuffled => $composableBuilder(
column: $table.shuffled,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get repeatMode => $composableBuilder(
column: $table.repeatMode,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get contextTitle => $composableBuilder(
column: $table.contextTitle,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<String> get contextKind => $composableBuilder(
column: $table.contextKind,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get positionMs => $composableBuilder(
column: $table.positionMs,
builder: (column) => ColumnFilters(column),
);
ColumnFilters<int> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => ColumnFilters(column),
);
}
class $$MusicSessionsTableOrderingComposer
extends Composer<_$AppDatabase, $MusicSessionsTable> {
$$MusicSessionsTableOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get profileId => $composableBuilder(
column: $table.profileId,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get queueJson => $composableBuilder(
column: $table.queueJson,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get orderJson => $composableBuilder(
column: $table.orderJson,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get cursor => $composableBuilder(
column: $table.cursor,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<bool> get shuffled => $composableBuilder(
column: $table.shuffled,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get repeatMode => $composableBuilder(
column: $table.repeatMode,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get contextTitle => $composableBuilder(
column: $table.contextTitle,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<String> get contextKind => $composableBuilder(
column: $table.contextKind,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get positionMs => $composableBuilder(
column: $table.positionMs,
builder: (column) => ColumnOrderings(column),
);
ColumnOrderings<int> get updatedAt => $composableBuilder(
column: $table.updatedAt,
builder: (column) => ColumnOrderings(column),
);
}
class $$MusicSessionsTableAnnotationComposer
extends Composer<_$AppDatabase, $MusicSessionsTable> {
$$MusicSessionsTableAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get profileId =>
$composableBuilder(column: $table.profileId, builder: (column) => column);
GeneratedColumn<String> get queueJson =>
$composableBuilder(column: $table.queueJson, builder: (column) => column);
GeneratedColumn<String> get orderJson =>
$composableBuilder(column: $table.orderJson, builder: (column) => column);
GeneratedColumn<int> get cursor =>
$composableBuilder(column: $table.cursor, builder: (column) => column);
GeneratedColumn<bool> get shuffled =>
$composableBuilder(column: $table.shuffled, builder: (column) => column);
GeneratedColumn<String> get repeatMode => $composableBuilder(
column: $table.repeatMode,
builder: (column) => column,
);
GeneratedColumn<String> get contextTitle => $composableBuilder(
column: $table.contextTitle,
builder: (column) => column,
);
GeneratedColumn<String> get contextKind => $composableBuilder(
column: $table.contextKind,
builder: (column) => column,
);
GeneratedColumn<int> get positionMs => $composableBuilder(
column: $table.positionMs,
builder: (column) => column,
);
GeneratedColumn<int> get updatedAt =>
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
}
class $$MusicSessionsTableTableManager
extends
RootTableManager<
_$AppDatabase,
$MusicSessionsTable,
MusicSessionRow,
$$MusicSessionsTableFilterComposer,
$$MusicSessionsTableOrderingComposer,
$$MusicSessionsTableAnnotationComposer,
$$MusicSessionsTableCreateCompanionBuilder,
$$MusicSessionsTableUpdateCompanionBuilder,
(
MusicSessionRow,
BaseReferences<_$AppDatabase, $MusicSessionsTable, MusicSessionRow>,
),
MusicSessionRow,
PrefetchHooks Function()
> {
$$MusicSessionsTableTableManager(_$AppDatabase db, $MusicSessionsTable table)
: super(
TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$$MusicSessionsTableFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$$MusicSessionsTableOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$$MusicSessionsTableAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
Value<String> profileId = const Value.absent(),
Value<String> queueJson = const Value.absent(),
Value<String> orderJson = const Value.absent(),
Value<int> cursor = const Value.absent(),
Value<bool> shuffled = const Value.absent(),
Value<String> repeatMode = const Value.absent(),
Value<String?> contextTitle = const Value.absent(),
Value<String?> contextKind = const Value.absent(),
Value<int> positionMs = const Value.absent(),
Value<int> updatedAt = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) => MusicSessionsCompanion(
profileId: profileId,
queueJson: queueJson,
orderJson: orderJson,
cursor: cursor,
shuffled: shuffled,
repeatMode: repeatMode,
contextTitle: contextTitle,
contextKind: contextKind,
positionMs: positionMs,
updatedAt: updatedAt,
rowid: rowid,
),
createCompanionCallback:
({
required String profileId,
required String queueJson,
required String orderJson,
required int cursor,
Value<bool> shuffled = const Value.absent(),
Value<String> repeatMode = const Value.absent(),
Value<String?> contextTitle = const Value.absent(),
Value<String?> contextKind = const Value.absent(),
Value<int> positionMs = const Value.absent(),
required int updatedAt,
Value<int> rowid = const Value.absent(),
}) => MusicSessionsCompanion.insert(
profileId: profileId,
queueJson: queueJson,
orderJson: orderJson,
cursor: cursor,
shuffled: shuffled,
repeatMode: repeatMode,
contextTitle: contextTitle,
contextKind: contextKind,
positionMs: positionMs,
updatedAt: updatedAt,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $$MusicSessionsTableProcessedTableManager =
ProcessedTableManager<
_$AppDatabase,
$MusicSessionsTable,
MusicSessionRow,
$$MusicSessionsTableFilterComposer,
$$MusicSessionsTableOrderingComposer,
$$MusicSessionsTableAnnotationComposer,
$$MusicSessionsTableCreateCompanionBuilder,
$$MusicSessionsTableUpdateCompanionBuilder,
(
MusicSessionRow,
BaseReferences<_$AppDatabase, $MusicSessionsTable, MusicSessionRow>,
),
MusicSessionRow,
PrefetchHooks Function()
>;
class $AppDatabaseManager {
final _$AppDatabase _db;
@@ -9214,4 +10160,6 @@ class $AppDatabaseManager {
$$ProfilesTableTableManager(_db, _db.profiles);
$$ProfileConnectionsTableTableManager get profileConnections =>
$$ProfileConnectionsTableTableManager(_db, _db.profileConnections);
$$MusicSessionsTableTableManager get musicSessions =>
$$MusicSessionsTableTableManager(_db, _db.musicSessions);
}
+42
View File
@@ -282,3 +282,45 @@ class OfflineWatchProgress extends Table {
/// Last sync error message
TextColumn get lastError => text().nullable()();
}
/// Last music session per profile, restored paused on the next launch (#2148).
///
/// One row per profile: the serialized queue plus enough arrangement state to
/// rebuild it faithfully (canonical order, shuffle permutation, cursor, modes)
/// and the playhead. Written through during playback (throttled position
/// updates, full rewrites on queue-shape changes) because mobile gives no
/// termination hook; cleared when the user visibly ends the session.
@DataClassName('MusicSessionRow')
class MusicSessions extends Table {
/// Active Plezy profile that owns this session snapshot.
TextColumn get profileId => text()();
/// Canonical queue tracks (insertion order) as a JSON array of MediaItem
/// JSON — self-contained so restore never depends on volatile cache rows.
TextColumn get queueJson => text()();
/// Playback-order permutation into [queueJson] as a JSON int array
/// (identity while unshuffled).
TextColumn get orderJson => text()();
/// Position of the current track within the playback order.
IntColumn get cursor => integer()();
BoolColumn get shuffled => boolean().withDefault(const Constant(false))();
/// Stable repeat-mode id ('off' | 'all' | 'one') — not the enum `.name`.
TextColumn get repeatMode => text().withDefault(const Constant('off'))();
/// Play-context provenance ("Playing from …"); kind is a stable id.
TextColumn get contextTitle => text().nullable()();
TextColumn get contextKind => text().nullable()();
/// Playhead within the current track in milliseconds.
IntColumn get positionMs => integer().withDefault(const Constant(0))();
/// Timestamp of the last write (milliseconds since epoch).
IntColumn get updatedAt => integer()();
@override
Set<Column> get primaryKey => {profileId};
}
+3 -3
View File
@@ -38,7 +38,7 @@ class MediaServerAuthException extends MediaServerException {
/// Auth polling reached a terminal server-side expiry/rejection state before
/// the user completed the external sign-in flow.
class MediaServerPinExpiredException extends MediaServerAuthException {
const MediaServerPinExpiredException({String? display}) : super('PIN expired before sign-in', display: display);
const MediaServerPinExpiredException({super.display}) : super('PIN expired before sign-in');
}
/// HTTP transport / non-2xx errors. Carries the status code (when known),
@@ -57,11 +57,11 @@ class MediaServerHttpException extends MediaServerException {
MediaServerHttpException({
required this.type,
String? message,
String? display,
super.display,
this.statusCode,
this.responseData,
this.requestUri,
}) : super(message ?? '', display: display);
}) : super(message ?? '');
/// Map a caught exception to a [MediaServerHttpException].
factory MediaServerHttpException.from(Object error, {Uri? uri}) {
+20
View File
@@ -33,6 +33,26 @@ class DirectionalShortcutFocusNode extends FocusNode {
node is DirectionalShortcutFocusNode && node.consumesDirectionalKeys(key);
}
/// A [FocusNode] for a locked-focus row: the node spans the whole row while
/// its owner steps an internal selection index between the row's items.
///
/// Swipe-step pricing follows the focused *item's* geometry (see
/// `AppleTvRemoteTouchService`), and for a locked-focus row the node's own
/// rect is the row — screen-wide — which would price a swipe step at the
/// travel cap and make the row feel dead. The owner instead vends the
/// selected item's global rect here; a null return (item not built yet,
/// unknown geometry) falls back to the fixed step distance.
///
/// Like [DirectionalShortcutFocusNode], the fact rides on the node because it
/// depends on what has focus, not on where a widget sits.
class LockedFocusRowNode extends FocusNode {
LockedFocusRowNode({required this.focusedItemRect, super.debugLabel, super.skipTraversal});
/// Global rect of the row's currently selected item, evaluated per swipe
/// frame so selection moves need no syncing.
final Rect? Function() focusedItemRect;
}
/// Whether [event] is evidence that the viewer wants to navigate by focus.
///
/// This is the single answer to two questions that must never disagree:
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Səyahətçini istifadə et",
"or": "və ya",
"connectToMediaBrowser": "${product}-a qoşul",
"quickConnect": "Sürətli Qoşulma",
"useQuickConnect": "Sürətli Qoşulmanı istifadə et",
"quickConnectInstructions": "Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.",
"quickConnectWaiting": "Təsdiq gözlənilir…",
@@ -53,6 +54,7 @@
"mute": "Səsi söndür",
"ok": "Oldu",
"off": "Söndürülüb",
"options": "Seçimlər",
"seasonNumber": "Mövsüm ${number}",
"episodeNumberTitle": "Seriya ${number} - ${title}",
"chapterNumber": "Hissə ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Ekran miqyası",
"compact": "Sıx",
"comfortable": "Rəhat",
"gridSpacing": "Tor aralığı",
"gridSpacingTight": "Sıx",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Geniş",
"tvCornerSpotlightBackdrop": "Künc işıqlandırma fonu",
"tvCornerSpotlightBackdropDescription": "Arxa fonu ekranı örtmək əvəzinə sağ üst küncdə göstər",
"viewMode": "Baxış rejimi",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Avadanlıq kod açılması",
"hardwareDecodingDescription": "Mümkün olduqda avadanlıq sürətləndirməsini istifadə et",
"bufferSize": "Bufer həcmi",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Avtomatik (Tövsiyə olunan)",
"bufferSizeWarning": "${heap}MB yaddaş əlçatandır. ${size}MB bufer oynatmaya təsir edə bilər.",
"playbackBuffer": "Oxutma buferi",
"playbackBufferAuto": "Avtomatik (tövsiyə olunur)",
"playbackBufferLarge": "Böyük",
"playbackBufferExtraLarge": "Çox böyük",
"playbackBufferDescription": "Qeyri-sabit əlaqələrə qarşı daha çox buferləyir. Bufer ölçüsü ilə də məhdudlaşır.",
"defaultQualityTitle": "Defolt keyfiyyət",
"cellularQualityTitle": "Mobil şəbəkədə defolt keyfiyyət",
"cellularQualitySameAsDefault": "Defolt keyfiyyətlə eyni",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Musiqi keyfiyyəti",
"subtitleStyling": "Altyazı tənzimləmələri",
"subtitleStylingDescription": "Altyazı görünüşünü özünüləşdirin",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Hər məzmun üçün səs və altyazı seçimlərini yadda saxla",
"followServerTrackSelections": "Hər epizod üçün serverin trek seçimlərini istifadə et",
"followServerTrackSelectionsDescription": "Epizod dəyişəndə cari seçimi köçürmək əvəzinə serverdə seçilmiş səs və altyazını tətbiq et",
"resumeMusicOnLaunch": "Musiqi sessiyasını yadda saxla",
"resumeMusicOnLaunchDescription": "Tətbiq açılanda son mahnını dayandırıldığı yerdə fasilədə aç",
"showChapterMarkersOnTimeline": "Zaman çubuğunda hissə işarələrini göstər",
"showChapterMarkersOnTimelineDescription": "Zaman çubuğunu hissə sərhədlərinə böl",
"specialsOrdering": "Xüsusi bölmələr epizod sırasına görə",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Qısayol artıq ${action} üçün təyin edilib",
"shortcutUpdated": "${action} üçün qısayol yeniləndi",
"saveFailed": "Dəyişikliklər yadda saxlanıla bilmədi. Təzədən cəhd edin.",
"autoSkip": "Avtomatik ötür",
"autoPlayAndSkip": "Avtomatik oynat və ötür",
"autoPlayNextEpisode": "Növbəti seriyanı avtomatik oynat",
"autoPlayNextEpisodeDescription": "Bir seriya bitdikdə növbətisini avtomatik başlat",
"playNextCountdown": "Növbəti seriya geri sayımı",
"playNextCountdownImmediate": "Dərhal oynat",
"autoSkipIntro": "Girişi avtomatik ötür",
"autoSkipIntroDescription": "Bir neçə saniyədən sonra giriş işarələrini avtomatik ötür",
"autoSkipCredits": "Titrləri avtomatik ötür",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Daxili DV7-ni məcburi et",
"dvConversionDv81Description": "Dolby Vision profile 8.1-ə çevrilməni məcburi et",
"dvConversionHevcStripDescription": "Dolby Vision təbəqələrini sil və sadə HEVC kimi təqdim et",
"demuxer": "Konteyner demukseri",
"demuxerDescription": "Birbaşa oxudulan faylları hansı demukserin təhlil edəcəyi. Fayl düzgün işləmirsə, media3-ə keçin.",
"demuxerFfmpeg": "FFmpeg (Tövsiyə olunan)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterleysinq",
"deinterlaceDescription": "Sətirlərarası videodakı darama artefaktlarını aradan qaldır (yalnız mpv oynadıcısı)",
"requireProfileSelectionOnOpen": "Açılışda profil soruş",
"requireProfileSelectionOnOpenDescription": "Tətbiq hər dəfə açıldıqda profil seçimini göstər",
"forceTvMode": "TV rejimini məcburi et",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Plex Discover və qoşulmuş izləmə xidmətlərindəki məzmunla Kəşf et nişanını göstər",
"liveTvDefaultFavorites": "Canlı TV-də sevimli kanalları defolt et",
"liveTvDefaultFavoritesDescription": "Canlı TV açıldıqda yalnız sevimli kanalları göstər",
"general": "Ümumi",
"generalDescription": "Dil, başlanğıc və pəncərə davranışı",
"languageAndRegion": "Dil və Region",
"startup": "Başlanğıc",
"display": "Ekran",
"libraryAndCards": "Kitabxana və kartlar",
"homeScreen": "Ana ekran",
"navigation": "Naviqasiya",
"window": "Pəncərə",
"content": "Məzmun",
"liveTv": "Canlı TV",
"player": "Oynadıcı",
"subtitlesAndConfig": "Altyazılar və konfiqurasiya",
"videoAndDisplay": "Video və Ekran",
"audio": "Səs",
"quality": "Keyfiyyət",
"subtitles": "Altyazılar",
"seekAndTiming": "Sarğı və vaxt tənzimləməsi",
"behavior": "Davranış",
"gestures": "Jestlər",
"gestureBrightnessSwipe": "Parlaqlıq sürüşdürməsi",
"gestureBrightnessSwipeDescription": "Parlaqlığı tənzimləmək üçün sol kənarda yuxarı və ya aşağı sürüşdürün",
"gestureVolumeSwipe": "Səs sürüşdürməsi",
"gestureVolumeSwipeDescription": "Səsi tənzimləmək üçün sağ kənarda yuxarı və ya aşağı sürüşdürün",
"gesturePinchToZoom": "Çimdiklə yaxınlaşdır",
"gesturePinchToZoomDescription": "Yaxınlaşdırmaq və ya uzaqlaşdırmaq üçün videoda çimdik hərəkəti edin",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "İdarəetmələr",
"rememberPlayerChanges": "Pleyer dəyişikliklərini yadda saxla",
"rememberPlayerChangesDescription": "Oxutma zamanı edilən dəyişikliklərin harada saxlanacağı və yenidən tətbiq ediləcəyi",
"scopePlaybackSpeed": "Oxutma sürəti",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "${product} qoşulmasını düzəliş et",
"editMediaBrowserIntro": "${serverName} üçün URL-lər əlavə edin və ya silin. Plezy əlçatan olan ən aşağı gecikməli URL-i istifadə edəcək."
},
"accountPreferences": {
"sectionTitle": "Hesab tərcihləri",
"hubSubtitleSingle": "${account} hesabında saxlanılan səs, altyazı və kitabxana seçimləri",
"hubSubtitleMultiple": "${count} hesabda saxlanılan səs, altyazı və kitabxana seçimləri",
"pickAccount": "Hər hesab öz tərcihlərini saxlayır. Düzəliş etmək üçün birini seçin.",
"storedOnAccount": "Bu seçimlər hesabın özündə saxlanılır, ona görə də ona daxil olan hər tətbiq onlardan istifadə edir — digər cihazlarınızdakı Plezy də daxil olmaqla.",
"noAccounts": "Konfiqurasiya ediləcək hesab yoxdur",
"noAccountsHint": "Plex-ə daxil olun və ya Jellyfin və ya Emby serverinə qoşulun; o hesabda saxlanılan tərcihlər burada görünəcək.",
"unavailable": "Bu hesaba çatmaq olmur",
"loadFailed": "Bu tərcihlər yüklənə bilmədi",
"noPreference": "Tərcih yoxdur",
"notSet": "Təyin edilməyib",
"groups": {
"audioAndSubtitles": "Səs və altyazılar",
"libraryDisplay": "Kitabxana",
"personalMedia": "Şəxsi media"
},
"preferredAudioLanguage": "Üstünlük verilən səs dili",
"autoSelectAudio": "Səsi dilə görə seç",
"autoSelectAudioDescription": "Söndürüldükdə faylın defolt olaraq işarələdiyi səs treki istifadə olunur.",
"preferredSubtitleLanguage": "Üstünlük verilən altyazı dili",
"subtitleMode": "Altyazıları aç",
"subtitleModes": {
"none": "Əl ilə seçilmiş",
"noneDescription": "Altyazıları heç vaxt özbaşına açma.",
"defaultMode": "Trek bayraqlarına əməl et",
"defaultModeDescription": "Hər altyazı trekində saxlanılan defolt və məcburi bayraqları istifadə et.",
"always": "Həmişə aktivdir",
"alwaysDescription": "Üstünlük verilən dildə altyazı treki mövcuddursa, onu aç.",
"onlyForced": "Yalnız məcburi altyazılar",
"onlyForcedDescription": "Yalnız məcburi işarələnmiş trekləri yüklə.",
"smart": "Xarici səs olduqda göstərilən",
"smartDescription": "Altyazıları yalnız səs başqa dildə olduqda aç."
},
"subtitleAccessibility": "SDH altyazıları",
"subtitleAccessibilityOptions": {
"preferNonSdh": "SDH olmayan altyazılara üstünlük ver",
"preferSdh": "SDH altyazılarına üstünlük ver",
"onlySdh": "Yalnız SDH altyazıları",
"onlyNonSdh": "Yalnız SDH olmayan altyazılar"
},
"forcedSubtitles": "Məcburi altyazılar",
"forcedSubtitleOptions": {
"preferNonForced": "Məcburi olmayan altyazılara üstünlük ver",
"preferForced": "Məcburi altyazılara üstünlük ver",
"onlyForced": "Yalnız məcburi altyazılar",
"onlyNonForced": "Yalnız məcburi olmayan altyazılar"
},
"displayMissingEpisodes": "Çatışmayan seriyaları göstər",
"displayMissingEpisodesDescription": "Serverin bildiyi, lakin faylı olmayan seriyaları siyahıya al.",
"hidePlayedInLatest": "Baxılmış elementləri Son əlavə olunanlarda gizlət",
"hidePlayedInLatestDescription": "Artıq baxdığınız elementləri serverin Son əlavə olunanlar sətirlərində göstərmə.",
"displayCollectionsView": "Kolleksiyalar görünüşünü göstər",
"displayCollectionsViewDescription": "Kitabxanalarınızla yanaşı serverin Kolleksiyalar görünüşünü də təklif et.",
"rewatchingInNextUp": "Təkrar baxılan serialları Sırada saxla",
"rewatchingInNextUpDescription": "Bir serialı bitirdikdən sonra yenidən başlasanız, Sırada serialı atmaq əvəzinə təkrar baxışı izləyir.",
"watchedIndicator": "Baxıldı göstəriciləri",
"watchedIndicatorOptions": {
"none": "Heç vaxt",
"moviesAndShows": "Kinolar və TV şoular",
"movies": "Yalnız kinolar",
"shows": "Yalnız TV şoular"
},
"mediaReviewsVisibility": "Reytinq və rəylər",
"mediaReviewsOptions": {
"usersAndCritics": "İstifadəçilər və tənqidçilər",
"usersOnly": "Yalnız istifadəçilər",
"criticsOnly": "Yalnız tənqidçilər",
"nobody": "Gizlədilib"
}
},
"discover": {
"title": "Kəşf et",
"noContentAvailable": "Məzmun əlçatan deyil",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Bilinməyən kanal",
"live": "CANLI",
"reloadGuide": "Bələdçini yenilə",
"searchGuide": "Bələdçidə axtar",
"searchHint": "Kanal və proqram axtar",
"searchNoResults": "\"${query}\" üçün uyğunluq tapılmadı",
"channelsSection": "Kanallar",
"programsSection": "Proqramlar",
"now": "İndi",
"today": "Bu gün",
"tomorrow": "Sabah",
@@ -1452,6 +1556,8 @@
"repeatAll": "Hamısını təkrarla",
"repeatOne": "Birini təkrarla",
"instantMixNoServer": "Ani miks üçün heç bir server mövcud deyil",
"instantMixFailed": "Anında qarışıq yüklənə bilmədi",
"instantMixEmpty": "Anında qarışıq heç bir mahnı qaytarmadı",
"noAudioUrl": "${track} üçün səs URL-i mövcud deyil",
"discography": {
"singlesAndEps": "Single-lar və EP-lər",
@@ -1483,6 +1589,13 @@
"host": "Təşkilatçı",
"hostBadge": "TƏŞKİLATÇI",
"youAreHost": "Təşkilatçı sizsiniz",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Başqaları ilə izlənilir",
"endSession": "Seansı bitir",
"leaveSession": "Seansdan çıx",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Bloklanmış",
"couldNotReach": "${url} ünvanına çatmaq olmadı: ${error}",
"noInstanceAtUrl": "${url} ünvanında Seerr instansiyası yoxdur (HTTP ${status})",
"invalidUrl": "https://seerr.example.com kimi server ünvanı daxil edin",
"quickConnectUnsupported": "Bu Seerr nüsxəsi Sürətli Qoşulmanı dəstəkləmir. Seerr 3.4 və ya daha yeni versiya tələb olunur.",
"notInitialized": "Bu Seerr instansiyasının ilkin quraşdırılması tamamlanmayıb",
"noPlexTokenForReauth": "Yenidən daxil olmaq üçün Plex tokeni mövcud deyil",
"noStoredCredentials": "Yenidən daxil olmaq üçün yadda saxlanmış giriş məlumatları mövcud deyil",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Xidmətlər",
"hubSubtitle": "İzləmə tərəqqisini eyniləşdirin və yeni başlıqlar sorğulayın.",
"integrations": "İnteqrasiyalar",
"notConnected": "Qoşulmayıb",
"connectedAs": "@${username} olaraq qoşuldu",
"scrobble": "Tərəqqini avtomatik izlə",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Използвай браузър",
"or": "или",
"connectToMediaBrowser": "Свържи се с ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Използвай Quick Connect",
"quickConnectInstructions": "Отворете Quick Connect в Jellyfin и въведете този код.",
"quickConnectWaiting": "Изчакване на одобрение…",
@@ -53,6 +54,7 @@
"mute": "Заглуши",
"ok": "OK",
"off": "Изкл.",
"options": "Опции",
"seasonNumber": "Сезон ${number}",
"episodeNumberTitle": "Епизод ${number} - ${title}",
"chapterNumber": "Глава ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Мащаб на дисплея",
"compact": "Компактна",
"comfortable": "Удобна",
"gridSpacing": "Разстояние на мрежата",
"gridSpacingTight": "Плътно",
"gridSpacingNormal": "Нормално",
"gridSpacingSpacious": "Просторно",
"tvCornerSpotlightBackdrop": "Фон с акцент в ъгъла",
"tvCornerSpotlightBackdropDescription": "Показвай акцентното изображение в горния десен ъгъл, вместо на целия екран",
"viewMode": "Режим на изглед",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Хардуерно декодиране",
"hardwareDecodingDescription": "Използвай хардуерно ускорение, когато е налично",
"bufferSize": "Размер на буфера",
"bufferSizeMB": "${size} MB",
"bufferSizeAuto": "Автоматично (препоръчително)",
"bufferSizeWarning": "Налична памет: ${heap} MB. Буфер от ${size} MB може да повлияе на възпроизвеждането.",
"playbackBuffer": "Буфер за възпроизвеждане",
"playbackBufferAuto": "Автоматично (препоръчително)",
"playbackBufferLarge": "Голям",
"playbackBufferExtraLarge": "Изключително голям",
"playbackBufferDescription": "Буферира повече при нестабилни връзки. Също ограничен от размера на буфера.",
"defaultQualityTitle": "Качество по подразбиране",
"cellularQualityTitle": "Качество по подразбиране при мобилни данни",
"cellularQualitySameAsDefault": "Същото като качеството по подразбиране",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Качество на музиката",
"subtitleStyling": "Стил на субтитрите",
"subtitleStylingDescription": "Настройване на вида на субтитрите",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Запомняй избора на аудиопътечка и субтитри за всяко заглавие",
"followServerTrackSelections": "Използвай избора на пътечки от сървъра за всеки епизод",
"followServerTrackSelectionsDescription": "При смяна на епизода прилагай избраните на сървъра аудио и субтитри, вместо да се пренася текущият избор",
"resumeMusicOnLaunch": "Запомняне на музикалната сесия",
"resumeMusicOnLaunchDescription": "При стартиране на приложението отваряй последната песен на пауза от мястото, докъдето е стигнала",
"showChapterMarkersOnTimeline": "Показвай маркери на глави върху времевата линия",
"showChapterMarkersOnTimelineDescription": "Разделяй времевата линия на сегменти по границите на главите",
"specialsOrdering": "Специални епизоди в реда на епизодите",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Клавишната комбинация вече е назначена за ${action}",
"shortcutUpdated": "Клавишната комбинация е обновена за ${action}",
"saveFailed": "Промените не можаха да бъдат запазени. Опитайте отново.",
"autoSkip": "Автоматично прескачане",
"autoPlayAndSkip": "Автоматично пускане и прескачане",
"autoPlayNextEpisode": "Автоматично пускане на следващия епизод",
"autoPlayNextEpisodeDescription": "Пускай следващия епизод автоматично, когато текущият свърши",
"playNextCountdown": "Отброяване до следващия епизод",
"playNextCountdownImmediate": "Пусни веднага",
"autoSkipIntro": "Автоматично прескачане на интро",
"autoSkipIntroDescription": "Автоматично прескачай интро маркери след няколко секунди",
"autoSkipCredits": "Автоматично прескачане на финални надписи",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Принуждава директно възпроизвеждане на DV7 и изключва повторния опит за преобразуване",
"dvConversionDv81Description": "Принуждава директно преобразуване на RPU към Dolby Vision Profile 8.1",
"dvConversionHevcStripDescription": "Премахва слоевете Dolby Vision RPU/EL и подава обикновен HEVC поток",
"demuxer": "Демултиплексор на контейнера",
"demuxerDescription": "Кой демултиплексор анализира файловете при директно възпроизвеждане. Превключете на media3, ако даден файл създава проблеми.",
"demuxerFfmpeg": "FFmpeg (препоръчително)",
"demuxerMedia3": "media3",
"deinterlace": "Деинтерлейсинг",
"deinterlaceDescription": "Премахва гребеновидните артефакти от интерлейсирано видео (само за mpv плейъра)",
"requireProfileSelectionOnOpen": "Питай за профил при отваряне на приложението",
"requireProfileSelectionOnOpenDescription": "Показвай избор на профил всеки път при отваряне на приложението",
"forceTvMode": "Принуди TV режим",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Показва раздела „Открий“ със съдържание от Plex Discover и свързаните тракери",
"liveTvDefaultFavorites": "По подразбиране към любими канали",
"liveTvDefaultFavoritesDescription": "Показвай само любими канали при отваряне на телевизия на живо",
"general": "Общи",
"generalDescription": "Език, стартиране и поведение на прозореца",
"languageAndRegion": "Език и регион",
"startup": "Стартиране",
"display": "Дисплей",
"libraryAndCards": "Библиотека и карти",
"homeScreen": "Начален екран",
"navigation": "Навигация",
"window": "Прозорец",
"content": "Съдържание",
"liveTv": "Телевизия на живо",
"player": "Плейър",
"subtitlesAndConfig": "Субтитри и конфигурация",
"videoAndDisplay": "Видео и дисплей",
"audio": "Аудио",
"quality": "Качество",
"subtitles": "Субтитри",
"seekAndTiming": "Търсене и време",
"behavior": "Поведение",
"gestures": "Жестове",
"gestureBrightnessSwipe": "Плъзгане за яркост",
"gestureBrightnessSwipeDescription": "Плъзни нагоре или надолу по левия ръб, за да регулираш яркостта",
"gestureVolumeSwipe": "Плъзгане за сила на звука",
"gestureVolumeSwipeDescription": "Плъзни нагоре или надолу по десния ръб, за да регулираш силата на звука",
"gesturePinchToZoom": "Стискане за мащабиране",
"gesturePinchToZoomDescription": "Стисни видеото, за да увеличиш или намалиш мащаба",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Контроли",
"rememberPlayerChanges": "Запомняне на промените в плейъра",
"rememberPlayerChangesDescription": "Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане",
"scopePlaybackSpeed": "Скорост на възпроизвеждане",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Редактирай връзката с ${product}",
"editMediaBrowserIntro": "Добавете или премахнете URL адреси за ${serverName}. Plezy ще използва достъпния URL адрес с най-ниска латентност."
},
"accountPreferences": {
"sectionTitle": "Предпочитания на акаунта",
"hubSubtitleSingle": "Опции за аудио, субтитри и библиотека, запазени на ${account}",
"hubSubtitleMultiple": "Опции за аудио, субтитри и библиотека, запазени на ${count} акаунта",
"pickAccount": "Всеки акаунт съхранява собствени предпочитания. Изберете кой да редактирате.",
"storedOnAccount": "Тези опции се запазват на самия акаунт, така че всяко приложение, вписано в него, ги използва — включително Plezy на другите ви устройства.",
"noAccounts": "Няма акаунти за настройка",
"noAccountsHint": "Влезте в Plex или свържете Jellyfin или Emby сървър и предпочитанията, запазени на този акаунт, ще се появят тук.",
"unavailable": "Акаунтът не може да бъде достигнат",
"loadFailed": "Предпочитанията не можаха да бъдат заредени",
"noPreference": "Без предпочитание",
"notSet": "Не е зададено",
"groups": {
"audioAndSubtitles": "Аудио и субтитри",
"libraryDisplay": "Библиотека",
"personalMedia": "Лична медия"
},
"preferredAudioLanguage": "Предпочитан аудио език",
"autoSelectAudio": "Избирай аудио по език",
"autoSelectAudioDescription": "При изключено се запазва аудиопътечката, която файлът маркира като подразбираща се.",
"preferredSubtitleLanguage": "Предпочитан език за субтитри",
"subtitleMode": "Включване на субтитри",
"subtitleModes": {
"none": "Ръчно избрани",
"noneDescription": "Никога не включвай субтитри автоматично.",
"defaultMode": "Следвай флаговете на пътечката",
"defaultModeDescription": "Използвай флаговете по подразбиране и принудителните, съхранени на всяка пътечка със субтитри.",
"always": "Винаги включени",
"alwaysDescription": "Включвай пътечка със субтитри на предпочитания език, когато има такава.",
"onlyForced": "Само принудителни субтитри",
"onlyForcedDescription": "Зареждай само пътечките, маркирани като принудителни.",
"smart": "Показване при чуждоезично аудио",
"smartDescription": "Включвай субтитри само когато аудиото е на друг език."
},
"subtitleAccessibility": "SDH субтитри",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Предпочитай субтитри без SDH",
"preferSdh": "Предпочитай SDH субтитри",
"onlySdh": "Само SDH субтитри",
"onlyNonSdh": "Само субтитри без SDH"
},
"forcedSubtitles": "Принудителни субтитри",
"forcedSubtitleOptions": {
"preferNonForced": "Предпочитай субтитри без принудителни",
"preferForced": "Предпочитай принудителни субтитри",
"onlyForced": "Само принудителни субтитри",
"onlyNonForced": "Само субтитри без принудителни"
},
"displayMissingEpisodes": "Показвай липсващи епизоди",
"displayMissingEpisodesDescription": "Изброявай епизоди, за които сървърът знае, но няма файл.",
"hidePlayedInLatest": "Скривай изгледаните елементи в „Последни“",
"hidePlayedInLatestDescription": "Не включвай вече изгледаните елементи в редовете „Последни“ на сървъра.",
"displayCollectionsView": "Показвай изгледа „Колекции“",
"displayCollectionsViewDescription": "Предлагай изгледа „Колекции“ на сървъра редом с библиотеките ви.",
"rewatchingInNextUp": "Запазвай повторно гледаните сериали в „Следва“",
"rewatchingInNextUpDescription": "Когато завършите сериал и го пуснете отново, „Следва“ проследява повторното гледане, вместо да премахва сериала.",
"watchedIndicator": "Индикатори за изгледано",
"watchedIndicatorOptions": {
"none": "Никога",
"moviesAndShows": "Филми и ТВ сериали",
"movies": "Само филми",
"shows": "Само ТВ сериали"
},
"mediaReviewsVisibility": "Оценки и ревюта",
"mediaReviewsOptions": {
"usersAndCritics": "Потребители и критици",
"usersOnly": "Само потребители",
"criticsOnly": "Само критици",
"nobody": "Скрити"
}
},
"discover": {
"title": "Открий",
"noContentAvailable": "Няма налично съдържание",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Неизвестен канал",
"live": "НА ЖИВО",
"reloadGuide": "Презареди ТВ програмата",
"searchGuide": "Търсене в програмата",
"searchHint": "Търсене на канали и предавания",
"searchNoResults": "Няма съвпадения за \"${query}\"",
"channelsSection": "Канали",
"programsSection": "Предавания",
"now": "Сега",
"today": "Днес",
"tomorrow": "Утре",
@@ -1452,6 +1556,8 @@
"repeatAll": "Повтаряне на всички",
"repeatOne": "Повтаряне на една",
"instantMixNoServer": "Няма наличен сървър за незабавен микс",
"instantMixFailed": "Мигновеният микс не можа да бъде зареден",
"instantMixEmpty": "Мигновеният микс не върна песни",
"noAudioUrl": "Няма наличен URL за аудиото на ${track}",
"discography": {
"singlesAndEps": "Сингли и EP",
@@ -1483,6 +1589,13 @@
"host": "Организатор",
"hostBadge": "ОРГАНИЗАТОР",
"youAreHost": "Вие сте организаторът",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Гледате с други",
"endSession": "Край на сесията",
"leaveSession": "Напусни сесията",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "В списъка с блокирани",
"couldNotReach": "Неуспешна връзка с ${url}: ${error}",
"noInstanceAtUrl": "На ${url} няма инстанция на Seerr (HTTP ${status})",
"invalidUrl": "Въведете адрес на сървър като https://seerr.example.com",
"quickConnectUnsupported": "Тази Seerr инстанция не поддържа Quick Connect. Изисква се Seerr 3.4 или по-нова версия.",
"notInitialized": "Тази инстанция на Seerr не е завършила първоначалната настройка",
"noPlexTokenForReauth": "Няма наличен Plex токен за повторен вход",
"noStoredCredentials": "Няма запазени данни за повторен вход",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Услуги",
"hubSubtitle": "Синхронизирай прогреса на гледане и заявявай нови заглавия.",
"integrations": "Интеграции",
"notConnected": "Няма връзка",
"connectedAs": "Свързан като @${username}",
"scrobble": "Проследявай прогреса автоматично",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Brug browseren",
"or": "eller",
"connectToMediaBrowser": "Opret forbindelse til ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Brug Quick Connect",
"quickConnectInstructions": "Åbn Quick Connect i Jellyfin, og indtast denne kode.",
"quickConnectWaiting": "Venter på godkendelse…",
@@ -53,6 +54,7 @@
"mute": "Lydløs",
"ok": "OK",
"off": "Fra",
"options": "Valgmuligheder",
"seasonNumber": "Sæson ${number}",
"episodeNumberTitle": "Episode ${number} – ${title}",
"chapterNumber": "Kapitel ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Skalering",
"compact": "Kompakt",
"comfortable": "Komfortabel",
"gridSpacing": "Gitterafstand",
"gridSpacingTight": "Tæt",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Rummelig",
"tvCornerSpotlightBackdrop": "Fremhævet baggrundsbillede i hjørnet",
"tvCornerSpotlightBackdropDescription": "Vis fremhævet grafik i øverste højre hjørne i stedet for at fylde skærmen",
"viewMode": "Visningstilstand",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Hardwaredekodning",
"hardwareDecodingDescription": "Brug hardwareacceleration, når den er tilgængelig",
"bufferSize": "Bufferstørrelse",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automatisk (anbefalet)",
"bufferSizeWarning": "${heap} MB hukommelse tilgængelig. En buffer på ${size} MB kan påvirke afspilningen.",
"playbackBuffer": "Afspilningsbuffer",
"playbackBufferAuto": "Auto (anbefalet)",
"playbackBufferLarge": "Stor",
"playbackBufferExtraLarge": "Ekstra stor",
"playbackBufferDescription": "Bufrer mere mod ustabile forbindelser. Begrænses også af bufferstørrelsen.",
"defaultQualityTitle": "Standardkvalitet",
"cellularQualityTitle": "Standardkvalitet på mobildata",
"cellularQualitySameAsDefault": "Samme som standardkvalitet",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Musikkvalitet",
"subtitleStyling": "Undertekststil",
"subtitleStylingDescription": "Tilpas underteksters udseende",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Husk valget af lyd og undertekster for hver titel",
"followServerTrackSelections": "Brug serverens sporvalg for hvert afsnit",
"followServerTrackSelectionsDescription": "Ved afsnitsskift anvendes lyden og underteksterne valgt på serveren i stedet for at videreføre det aktuelle valg",
"resumeMusicOnLaunch": "Husk musiksession",
"resumeMusicOnLaunchDescription": "Åbn den seneste sang på pause der, hvor den slap, når appen starter",
"showChapterMarkersOnTimeline": "Vis kapitelmarkører på tidslinjen",
"showChapterMarkersOnTimelineDescription": "Opdel tidslinjen ved kapitelgrænser",
"specialsOrdering": "Specialafsnit i episoderekkefølge",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Genvejen er allerede tildelt ${action}",
"shortcutUpdated": "Genvejen for ${action} er opdateret",
"saveFailed": "Ændringerne kunne ikke gemmes. Prøv igen.",
"autoSkip": "Automatisk spring",
"autoPlayAndSkip": "Autoafspilning og spring",
"autoPlayNextEpisode": "Autoafspil næste afsnit",
"autoPlayNextEpisodeDescription": "Start automatisk næste afsnit, når et afsnit slutter",
"playNextCountdown": "Nedtælling til næste afsnit",
"playNextCountdownImmediate": "Afspil med det samme",
"autoSkipIntro": "Spring intro over automatisk",
"autoSkipIntroDescription": "Spring automatisk intromarkører over efter få sekunder",
"autoSkipCredits": "Spring rulletekster over automatisk",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Gennemtving indbygget DV7-understøttelse, og undlad at forsøge DV-konvertering igen",
"dvConversionDv81Description": "Tving inline RPU-konvertering til Dolby Vision profil 8.1",
"dvConversionHevcStripDescription": "Fjern Dolby Vision RPU/EL-lag og brug almindelig HEVC",
"demuxer": "Container-demuxer",
"demuxerDescription": "Hvilken demuxer der læser direkte afspillede filer. Skift til media3, hvis en fil driller.",
"demuxerFfmpeg": "FFmpeg (anbefalet)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacing",
"deinterlaceDescription": "Fjern kamartefakter fra interlaced video (kun mpv-afspiller)",
"requireProfileSelectionOnOpen": "Spørg om profil ved åbning",
"requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åbnes",
"forceTvMode": "Gennemtving TV-tilstand",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Vis fanen Opdag med indhold fra Plex Discover og tilknyttede trackere",
"liveTvDefaultFavorites": "Vis favoritkanaler som standard",
"liveTvDefaultFavoritesDescription": "Vis kun favoritkanaler ved åbning af Live TV",
"general": "Generelt",
"generalDescription": "Sprog, opstart og vinduesadfærd",
"languageAndRegion": "Sprog og region",
"startup": "Opstart",
"display": "Skærm",
"libraryAndCards": "Bibliotek og kort",
"homeScreen": "Startskærm",
"navigation": "Navigation",
"window": "Vindue",
"content": "Indhold",
"liveTv": "Live TV",
"player": "Afspiller",
"subtitlesAndConfig": "Undertekster og konfiguration",
"videoAndDisplay": "Video og skærm",
"audio": "Lyd",
"quality": "Kvalitet",
"subtitles": "Undertekster",
"seekAndTiming": "Søgning og timing",
"behavior": "Adfærd",
"gestures": "Bevægelser",
"gestureBrightnessSwipe": "Lysstyrke-strygning",
"gestureBrightnessSwipeDescription": "Stryg op eller ned i venstre kant for at justere lysstyrken",
"gestureVolumeSwipe": "Lydstyrke-strygning",
"gestureVolumeSwipeDescription": "Stryg op eller ned i højre kant for at justere lydstyrken",
"gesturePinchToZoom": "Klem for at zoome",
"gesturePinchToZoomDescription": "Klem på videoen for at zoome ind eller ud",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Kontroller",
"rememberPlayerChanges": "Husk afspillerændringer",
"rememberPlayerChangesDescription": "Hvor en ændring under afspilning gemmes og anvendes igen",
"scopePlaybackSpeed": "Afspilningshastighed",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Rediger ${product}-forbindelse",
"editMediaBrowserIntro": "Tilføj eller fjern URL'er for ${serverName}. Plezy bruger den tilgængelige URL med laveste latenstid."
},
"accountPreferences": {
"sectionTitle": "Kontopræferencer",
"hubSubtitleSingle": "Lyd-, undertekst- og biblioteksindstillinger gemt på ${account}",
"hubSubtitleMultiple": "Lyd-, undertekst- og biblioteksindstillinger gemt på ${count} konti",
"pickAccount": "Hver konto gemmer sine egne præferencer. Vælg den, du vil redigere.",
"storedOnAccount": "Disse indstillinger gemmes på selve kontoen, så alle apps, der er logget ind på den, bruger dem — inklusive Plezy på dine andre enheder.",
"noAccounts": "Ingen konti at konfigurere",
"noAccountsHint": "Log ind på Plex, eller forbind en Jellyfin- eller Emby-server, så vises de præferencer, der er gemt på kontoen, her.",
"unavailable": "Kan ikke nå denne konto",
"loadFailed": "Kunne ikke indlæse disse præferencer",
"noPreference": "Ingen præference",
"notSet": "Ikke indstillet",
"groups": {
"audioAndSubtitles": "Lyd og undertekster",
"libraryDisplay": "Bibliotek",
"personalMedia": "Personlige medier"
},
"preferredAudioLanguage": "Foretrukket lydsprog",
"autoSelectAudio": "Vælg lyd ud fra sprog",
"autoSelectAudioDescription": "Fra beholder det lydspor, som filen markerer som standard.",
"preferredSubtitleLanguage": "Foretrukket undertekstsprog",
"subtitleMode": "Slå undertekster til",
"subtitleModes": {
"none": "Manuelt valgt",
"noneDescription": "Slår aldrig undertekster til af sig selv.",
"defaultMode": "Følg sporets flag",
"defaultModeDescription": "Brug standard- og tvungne flag, der er gemt på hvert undertekstspor.",
"always": "Altid aktiveret",
"alwaysDescription": "Slå et undertekstspor til på det foretrukne sprog, når der findes ét.",
"onlyForced": "Kun tvungne undertekster",
"onlyForcedDescription": "Indlæs kun spor, der er markeret som tvungne.",
"smart": "Vis ved fremmedsproget lyd",
"smartDescription": "Slå kun undertekster til, når lyden er på et andet sprog."
},
"subtitleAccessibility": "SDH-undertekster",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Foretræk ikke-SDH-undertekster",
"preferSdh": "Foretræk SDH-undertekster",
"onlySdh": "Kun SDH-undertekster",
"onlyNonSdh": "Kun ikke-SDH-undertekster"
},
"forcedSubtitles": "Tvungne undertekster",
"forcedSubtitleOptions": {
"preferNonForced": "Foretræk ikke-tvungne undertekster",
"preferForced": "Foretræk tvungne undertekster",
"onlyForced": "Kun tvungne undertekster",
"onlyNonForced": "Kun ikke-tvungne undertekster"
},
"displayMissingEpisodes": "Vis manglende afsnit",
"displayMissingEpisodesDescription": "Vis afsnit, som serveren kender til, men som ikke har nogen fil.",
"hidePlayedInLatest": "Skjul sete elementer i Seneste",
"hidePlayedInLatestDescription": "Hold elementer, du allerede har set, ude af serverens Seneste-rækker.",
"displayCollectionsView": "Vis samlingsvisningen",
"displayCollectionsViewDescription": "Vis serverens samlingsvisning sammen med dine biblioteker.",
"rewatchingInNextUp": "Behold gensete serier i Næste afsnit",
"rewatchingInNextUpDescription": "Når du er færdig med en serie og ser den igen, følger Næste afsnit med i gensynet i stedet for at fjerne serien.",
"watchedIndicator": "Set-indikatorer",
"watchedIndicatorOptions": {
"none": "Aldrig",
"moviesAndShows": "Film og TV-serier",
"movies": "Kun film",
"shows": "Kun TV-serier"
},
"mediaReviewsVisibility": "Bedømmelser og anmeldelser",
"mediaReviewsOptions": {
"usersAndCritics": "Brugere og anmeldere",
"usersOnly": "Kun brugere",
"criticsOnly": "Kun anmeldere",
"nobody": "Skjult"
}
},
"discover": {
"title": "Opdag",
"noContentAvailable": "Intet indhold tilgængeligt",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Ukendt kanal",
"live": "LIVE",
"reloadGuide": "Genindlæs guide",
"searchGuide": "Søg i guiden",
"searchHint": "Søg efter kanaler og programmer",
"searchNoResults": "Ingen match for \"${query}\"",
"channelsSection": "Kanaler",
"programsSection": "Programmer",
"now": "Nu",
"today": "I dag",
"tomorrow": "I morgen",
@@ -1452,6 +1556,8 @@
"repeatAll": "Gentag alle",
"repeatOne": "Gentag ét nummer",
"instantMixNoServer": "Ingen server er tilgængelig til et øjeblikkeligt mix",
"instantMixFailed": "Kunne ikke indlæse det direkte miks",
"instantMixEmpty": "Det direkte miks indeholdt ingen numre",
"noAudioUrl": "Ingen lyd-URL er tilgængelig for ${track}",
"discography": {
"singlesAndEps": "Singler og EP'er",
@@ -1483,6 +1589,13 @@
"host": "Vært",
"hostBadge": "VÆRT",
"youAreHost": "Du er vært",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Ser med andre",
"endSession": "Afslut session",
"leaveSession": "Forlad session",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "På blokeringslisten",
"couldNotReach": "Kunne ikke nå ${url}: ${error}",
"noInstanceAtUrl": "Ingen Seerr-instans på ${url} (HTTP ${status})",
"invalidUrl": "Indtast en serveradresse som https://seerr.example.com",
"quickConnectUnsupported": "Denne Seerr-instans understøtter ikke Quick Connect. Den kræver Seerr 3.4 eller nyere.",
"notInitialized": "Denne Seerr-instans har ikke fuldført førstegangsopsætningen",
"noPlexTokenForReauth": "Intet Plex-token er tilgængeligt til at logge ind igen",
"noStoredCredentials": "Ingen gemte loginoplysninger er tilgængelige til at logge ind igen",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Tjenester",
"hubSubtitle": "Synkroniser dit visningsfremskridt, og anmod om nye titler.",
"integrations": "Integrationer",
"notConnected": "Ikke forbundet",
"connectedAs": "Forbundet som @${username}",
"scrobble": "Registrer fremgang automatisk",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Browser verwenden",
"or": "oder",
"connectToMediaBrowser": "Mit ${product} verbinden",
"quickConnect": "Quick Connect",
"useQuickConnect": "Quick Connect verwenden",
"quickConnectInstructions": "Öffne Quick Connect in Jellyfin und gib diesen Code ein.",
"quickConnectWaiting": "Warte auf Bestätigung…",
@@ -53,6 +54,7 @@
"mute": "Stumm",
"ok": "OK",
"off": "Aus",
"options": "Optionen",
"seasonNumber": "Staffel ${number}",
"episodeNumberTitle": "Folge ${number} – ${title}",
"chapterNumber": "Kapitel ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Skalierung",
"compact": "Kompakt",
"comfortable": "Großzügig",
"gridSpacing": "Rasterabstand",
"gridSpacingTight": "Eng",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Großzügig",
"tvCornerSpotlightBackdrop": "Backdrop in der Ecke",
"tvCornerSpotlightBackdropDescription": "Zeigt das Spotlight-Artwork oben rechts statt bildschirmfüllend",
"viewMode": "Ansichtsmodus",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Hardwaredekodierung",
"hardwareDecodingDescription": "Hardwarebeschleunigung verwenden, sofern verfügbar",
"bufferSize": "Puffergröße",
"bufferSizeMB": "${size} MB",
"bufferSizeAuto": "Automatisch (empfohlen)",
"bufferSizeWarning": "${heap} MB Speicher verfügbar. Ein Puffer von ${size} MB kann die Wiedergabe beeinträchtigen.",
"playbackBuffer": "Wiedergabepuffer",
"playbackBufferAuto": "Automatisch (empfohlen)",
"playbackBufferLarge": "Groß",
"playbackBufferExtraLarge": "Extra groß",
"playbackBufferDescription": "Puffert mehr gegen instabile Verbindungen. Zusätzlich durch die Puffergröße begrenzt.",
"defaultQualityTitle": "Standardqualität",
"cellularQualityTitle": "Standardqualität im Mobilfunknetz",
"cellularQualitySameAsDefault": "Wie Standardqualität",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Musikqualität",
"subtitleStyling": "Untertitel-Stil",
"subtitleStylingDescription": "Aussehen von Untertiteln anpassen",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Audio- und Untertitelauswahl je Titel merken",
"followServerTrackSelections": "Serverseitige Spurauswahl pro Episode verwenden",
"followServerTrackSelectionsDescription": "Beim Episodenwechsel die auf dem Server ausgewählten Audio- und Untertitelspuren übernehmen, statt die aktuelle Auswahl zu übertragen",
"resumeMusicOnLaunch": "Musiksitzung merken",
"resumeMusicOnLaunchDescription": "Beim App-Start den letzten Titel pausiert an der letzten Stelle wieder öffnen",
"showChapterMarkersOnTimeline": "Kapitelmarkierungen auf der Suchleiste anzeigen",
"showChapterMarkersOnTimelineDescription": "Die Suchleiste an Kapitelgrenzen unterteilen",
"specialsOrdering": "Specials in Episodenreihenfolge",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Tastenkürzel bereits zugewiesen an ${action}",
"shortcutUpdated": "Tastenkürzel aktualisiert für ${action}",
"saveFailed": "Änderungen konnten nicht gespeichert werden. Versuche es erneut.",
"autoSkip": "Automatisches Überspringen",
"autoPlayAndSkip": "Autoplay & Überspringen",
"autoPlayNextEpisode": "Nächste Episode automatisch abspielen",
"autoPlayNextEpisodeDescription": "Die nächste Episode automatisch starten, wenn die aktuelle endet",
"playNextCountdown": "Countdown bis zur nächsten Episode",
"playNextCountdownImmediate": "Sofort abspielen",
"autoSkipIntro": "Intro automatisch überspringen",
"autoSkipIntroDescription": "Intro-Marker nach wenigen Sekunden automatisch überspringen",
"autoSkipCredits": "Abspann automatisch überspringen",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Natives DV7 erzwingen und einen erneuten DV-Konvertierungsversuch unterdrücken",
"dvConversionDv81Description": "Inline-RPU-Konvertierung in Dolby-Vision-Profil 8.1 erzwingen",
"dvConversionHevcStripDescription": "Dolby-Vision-RPU/EL-Schichten entfernen und reines HEVC ausgeben",
"demuxer": "Container-Demuxer",
"demuxerDescription": "Welcher Demuxer direkt abgespielte Dateien einliest. Wechsle zu media3, wenn eine Datei Probleme macht.",
"demuxerFfmpeg": "FFmpeg (empfohlen)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacing",
"deinterlaceDescription": "Kammartefakte aus interlaced Videos entfernen (nur mpv-Player)",
"requireProfileSelectionOnOpen": "Profil beim Öffnen abfragen",
"requireProfileSelectionOnOpenDescription": "Profilauswahl bei jedem Öffnen der App anzeigen",
"forceTvMode": "TV-Modus erzwingen",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Zeigt den Tab „Entdecken“ mit Inhalten aus Plex Discover und verbundenen Trackern an",
"liveTvDefaultFavorites": "Standardmäßig Favoritenkanäle",
"liveTvDefaultFavoritesDescription": "Beim Öffnen von Live TV nur Favoritenkanäle anzeigen",
"general": "Allgemein",
"generalDescription": "Sprache, Start und Fensterverhalten",
"languageAndRegion": "Sprache & Region",
"startup": "Start",
"display": "Anzeige",
"libraryAndCards": "Mediathek & Karten",
"homeScreen": "Startseite",
"navigation": "Navigation",
"window": "Fenster",
"content": "Inhalt",
"liveTv": "Live-TV",
"player": "Wiedergabe",
"subtitlesAndConfig": "Untertitel und Konfiguration",
"videoAndDisplay": "Video & Anzeige",
"audio": "Audio",
"quality": "Qualität",
"subtitles": "Untertitel",
"seekAndTiming": "Spulen & Timing",
"behavior": "Verhalten",
"gestures": "Gesten",
"gestureBrightnessSwipe": "Wischen für Helligkeit",
"gestureBrightnessSwipeDescription": "Wische am linken Rand nach oben oder unten, um die Helligkeit anzupassen",
"gestureVolumeSwipe": "Wischen für Lautstärke",
"gestureVolumeSwipeDescription": "Wische am rechten Rand nach oben oder unten, um die Lautstärke anzupassen",
"gesturePinchToZoom": "Zum Zoomen kneifen",
"gesturePinchToZoomDescription": "Kneife auf dem Video, um hinein- oder herauszuzoomen",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Steuerung",
"rememberPlayerChanges": "Playeränderungen merken",
"rememberPlayerChangesDescription": "Wo eine während der Wiedergabe vorgenommene Änderung gespeichert und erneut angewendet wird",
"scopePlaybackSpeed": "Wiedergabegeschwindigkeit",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Verbindung zu ${product} bearbeiten",
"editMediaBrowserIntro": "URLs für ${serverName} hinzufügen oder entfernen. Plezy verwendet die erreichbare URL mit der geringsten Latenz."
},
"accountPreferences": {
"sectionTitle": "Kontoeinstellungen",
"hubSubtitleSingle": "Audio-, Untertitel- und Mediathek-Optionen werden auf ${account} gespeichert",
"hubSubtitleMultiple": "Audio-, Untertitel- und Mediathek-Optionen werden auf ${count} Konten gespeichert",
"pickAccount": "Jedes Konto speichert seine eigenen Einstellungen. Wähle das Konto, das du bearbeiten möchtest.",
"storedOnAccount": "Diese Optionen werden auf dem Konto selbst gespeichert, sodass jede damit angemeldete App sie verwendet — auch Plezy auf deinen anderen Geräten.",
"noAccounts": "Keine Konten zum Konfigurieren",
"noAccountsHint": "Melde dich bei Plex an oder verbinde einen Jellyfin- oder Emby-Server — die auf diesem Konto gespeicherten Einstellungen erscheinen dann hier.",
"unavailable": "Dieses Konto ist nicht erreichbar",
"loadFailed": "Diese Einstellungen konnten nicht geladen werden",
"noPreference": "Keine Einstellung",
"notSet": "Nicht festgelegt",
"groups": {
"audioAndSubtitles": "Audio & Untertitel",
"libraryDisplay": "Mediathek",
"personalMedia": "Persönliche Medien"
},
"preferredAudioLanguage": "Bevorzugte Audiosprache",
"autoSelectAudio": "Audio nach Sprache auswählen",
"autoSelectAudioDescription": "Bei „Aus“ wird die Audiospur verwendet, die die Datei als Standard markiert.",
"preferredSubtitleLanguage": "Bevorzugte Untertitelsprache",
"subtitleMode": "Untertitel einschalten",
"subtitleModes": {
"none": "Manuell ausgewählt",
"noneDescription": "Untertitel niemals von selbst einschalten.",
"defaultMode": "Spurmarkierungen folgen",
"defaultModeDescription": "Die Standard- und Erzwungen-Markierungen jeder Untertitelspur verwenden.",
"always": "Immer aktiviert",
"alwaysDescription": "Eine Untertitelspur in der bevorzugten Sprache einschalten, sofern eine vorhanden ist.",
"onlyForced": "Nur erzwungene Untertitel",
"onlyForcedDescription": "Nur die als erzwungen markierten Spuren laden.",
"smart": "Bei fremdsprachigem Audio anzeigen",
"smartDescription": "Untertitel nur einschalten, wenn das Audio in einer anderen Sprache ist."
},
"subtitleAccessibility": "SDH-Untertitel",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Nicht-SDH-Untertitel bevorzugen",
"preferSdh": "SDH-Untertitel bevorzugen",
"onlySdh": "Nur SDH-Untertitel",
"onlyNonSdh": "Nur Nicht-SDH-Untertitel"
},
"forcedSubtitles": "Erzwungene Untertitel",
"forcedSubtitleOptions": {
"preferNonForced": "Nicht erzwungene Untertitel bevorzugen",
"preferForced": "Erzwungene Untertitel bevorzugen",
"onlyForced": "Nur erzwungene Untertitel",
"onlyNonForced": "Nur nicht erzwungene Untertitel"
},
"displayMissingEpisodes": "Fehlende Episoden anzeigen",
"displayMissingEpisodesDescription": "Episoden auflisten, die dem Server bekannt sind, für die aber keine Datei vorliegt.",
"hidePlayedInLatest": "Gesehene Elemente in „Neueste“ ausblenden",
"hidePlayedInLatestDescription": "Elemente, die du bereits gesehen hast, erscheinen nicht mehr in den „Neueste“-Zeilen des Servers.",
"displayCollectionsView": "Sammlungsansicht anzeigen",
"displayCollectionsViewDescription": "Die Sammlungsansicht des Servers zusätzlich zu deinen Mediatheken anzeigen.",
"rewatchingInNextUp": "Erneut geschaute Serien in „Als Nächstes“ behalten",
"rewatchingInNextUpDescription": "Beendest du eine Serie und startest sie erneut, folgt „Als Nächstes“ dem erneuten Anschauen, statt die Serie zu verwerfen.",
"watchedIndicator": "Gesehen-Markierung",
"watchedIndicatorOptions": {
"none": "Nie",
"moviesAndShows": "Filme und Serien",
"movies": "Nur Filme",
"shows": "Nur Serien"
},
"mediaReviewsVisibility": "Bewertungen & Rezensionen",
"mediaReviewsOptions": {
"usersAndCritics": "Benutzer und Kritiker",
"usersOnly": "Nur Benutzer",
"criticsOnly": "Nur Kritiker",
"nobody": "Ausgeblendet"
}
},
"discover": {
"title": "Entdecken",
"noContentAvailable": "Kein Inhalt verfügbar",
@@ -1034,7 +1133,7 @@
"productionYear": "Produktionsjahr",
"runtime": "Laufzeit",
"officialRating": "Offizielle Bewertung",
"premiereDate": "Premierendatum",
"premiereDate": "Veröffentlichungsdatum",
"startDate": "Startdatum",
"airTime": "Sendezeit",
"studio": "Studio",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Unbekannter Kanal",
"live": "LIVE",
"reloadGuide": "TV-Programm neu laden",
"searchGuide": "TV-Programm durchsuchen",
"searchHint": "Kanäle & Sendungen suchen",
"searchNoResults": "Keine Treffer für „${query}“",
"channelsSection": "Kanäle",
"programsSection": "Sendungen",
"now": "Jetzt",
"today": "Heute",
"tomorrow": "Morgen",
@@ -1452,6 +1556,8 @@
"repeatAll": "Alle wiederholen",
"repeatOne": "Titel wiederholen",
"instantMixNoServer": "Für einen Instant Mix ist kein Server verfügbar",
"instantMixFailed": "Instant-Mix konnte nicht geladen werden",
"instantMixEmpty": "Der Instant-Mix enthält keine Titel",
"noAudioUrl": "Für ${track} ist keine Audio-URL verfügbar",
"discography": {
"singlesAndEps": "Singles & EPs",
@@ -1483,6 +1589,13 @@
"host": "Host",
"hostBadge": "HOST",
"youAreHost": "Du bist der Host",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Mit anderen schauen",
"endSession": "Sitzung beenden",
"leaveSession": "Sitzung verlassen",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Auf der Sperrliste",
"couldNotReach": "${url} nicht erreichbar: ${error}",
"noInstanceAtUrl": "Keine Seerr-Instanz unter ${url} (HTTP ${status})",
"invalidUrl": "Gib eine Serveradresse ein, z. B. https://seerr.example.com",
"quickConnectUnsupported": "Diese Seerr-Instanz unterstützt Quick Connect nicht. Dafür ist Seerr 3.4 oder neuer erforderlich.",
"notInitialized": "Die Ersteinrichtung dieser Seerr-Instanz wurde noch nicht abgeschlossen",
"noPlexTokenForReauth": "Für die erneute Anmeldung ist kein Plex-Token verfügbar",
"noStoredCredentials": "Für die erneute Anmeldung sind keine gespeicherten Anmeldedaten verfügbar",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Dienste",
"hubSubtitle": "Wiedergabefortschritt synchronisieren und neue Titel anfragen.",
"integrations": "Integrationen",
"notConnected": "Nicht verbunden",
"connectedAs": "Verbunden als @${username}",
"scrobble": "Fortschritt automatisch verfolgen",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Use browser",
"or": "or",
"connectToMediaBrowser": "Connect to ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Use Quick Connect",
"quickConnectInstructions": "Open Quick Connect in Jellyfin and enter this code.",
"quickConnectWaiting": "Waiting for approval…",
@@ -53,6 +54,7 @@
"mute": "Mute",
"ok": "OK",
"off": "Off",
"options": "Options",
"seasonNumber": "Season ${number}",
"episodeNumberTitle": "Episode ${number} - ${title}",
"chapterNumber": "Chapter ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Display Scale",
"compact": "Compact",
"comfortable": "Comfortable",
"gridSpacing": "Grid Spacing",
"gridSpacingTight": "Tight",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Spacious",
"tvCornerSpotlightBackdrop": "Corner Spotlight Backdrop",
"tvCornerSpotlightBackdropDescription": "Show spotlight artwork in the top-right corner instead of filling the screen",
"viewMode": "View Mode",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Hardware Decoding",
"hardwareDecodingDescription": "Use hardware acceleration when available",
"bufferSize": "Buffer Size",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Auto (Recommended)",
"bufferSizeWarning": "${heap}MB memory available. A ${size}MB buffer may affect playback.",
"playbackBuffer": "Playback Buffer",
"playbackBufferAuto": "Auto (Recommended)",
"playbackBufferLarge": "Large",
"playbackBufferExtraLarge": "Extra Large",
"playbackBufferDescription": "Buffer more against unstable connections. Also limited by Buffer Size.",
"defaultQualityTitle": "Default Quality",
"cellularQualityTitle": "Default Quality on Cellular",
"cellularQualitySameAsDefault": "Same as Default Quality",
"directPlayCoveredQuality": "Play Smaller Videos at Original Quality",
"directPlayCoveredQualityDescription": "Direct play videos already within the quality limit instead of transcoding them",
"musicQualityTitle": "Music Quality",
"subtitleStyling": "Subtitle Styling",
"subtitleStylingDescription": "Customize subtitle appearance",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Remember audio and subtitle choices per title",
"followServerTrackSelections": "Use server's per-episode track selections",
"followServerTrackSelectionsDescription": "On episode change, apply the audio and subtitles selected on the server instead of carrying over the current choice",
"resumeMusicOnLaunch": "Remember music session",
"resumeMusicOnLaunchDescription": "Reopen the last song paused where it left off when the app starts",
"showChapterMarkersOnTimeline": "Show chapter markers on seek bar",
"showChapterMarkersOnTimelineDescription": "Segment the seek bar at chapter boundaries",
"specialsOrdering": "Specials in episode order",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Shortcut already assigned to ${action}",
"shortcutUpdated": "Shortcut updated for ${action}",
"saveFailed": "Could not save changes. Try again.",
"autoSkip": "Auto Skip",
"autoPlayAndSkip": "Auto-Play & Skip",
"autoPlayNextEpisode": "Auto-Play Next Episode",
"autoPlayNextEpisodeDescription": "Start the next episode automatically when one ends",
"playNextCountdown": "Play Next Countdown",
"playNextCountdownImmediate": "Play immediately",
"autoSkipIntro": "Auto Skip Intro",
"autoSkipIntroDescription": "Automatically skip intro markers after a few seconds",
"autoSkipCredits": "Auto Skip Credits",
@@ -326,7 +338,7 @@
"audioDownmixNormalize": "Normalize Volume on Downmix",
"audioDownmixNormalizeDescription": "Lower the mix to prevent clipping. Turn off to keep the original volume (may distort loud scenes).",
"dvConversionMode": "Dolby Vision Conversion",
"dvConversionModeDescription": "Choose how ExoPlayer handles Dolby Vision Profile 7 files.",
"dvConversionModeDescription": "Choose how Dolby Vision Profile 7 files are handled.",
"dvConversionAuto": "Auto",
"dvConversionNative": "Native / Disabled",
"dvConversionDv81": "P7 → P8.1",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Force native DV7 and suppress DV conversion retry",
"dvConversionDv81Description": "Force inline RPU conversion to Dolby Vision profile 8.1",
"dvConversionHevcStripDescription": "Strip Dolby Vision RPU/EL layers and present plain HEVC",
"demuxer": "Container Demuxer",
"demuxerDescription": "Which demuxer parses direct-played files. Switch to media3 if a file misbehaves.",
"demuxerFfmpeg": "FFmpeg (Recommended)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacing",
"deinterlaceDescription": "Remove combing artifacts from interlaced video (mpv player only)",
"requireProfileSelectionOnOpen": "Ask for profile on app open",
"requireProfileSelectionOnOpenDescription": "Show profile selection every time the app is opened",
"forceTvMode": "Force TV mode",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Display the Explore tab with content from Plex Discover and connected trackers",
"liveTvDefaultFavorites": "Default to Favorite Channels",
"liveTvDefaultFavoritesDescription": "Show only favorite channels when opening Live TV",
"general": "General",
"generalDescription": "Language, startup, and window behavior",
"languageAndRegion": "Language & Region",
"startup": "Startup",
"display": "Display",
"libraryAndCards": "Library & Cards",
"homeScreen": "Home Screen",
"navigation": "Navigation",
"window": "Window",
"content": "Content",
"liveTv": "Live TV",
"player": "Player",
"subtitlesAndConfig": "Subtitles & Configuration",
"videoAndDisplay": "Video & Display",
"audio": "Audio",
"quality": "Quality",
"subtitles": "Subtitles",
"seekAndTiming": "Seek & Timing",
"behavior": "Behavior",
"gestures": "Gestures",
"gestureBrightnessSwipe": "Brightness Swipe",
"gestureBrightnessSwipeDescription": "Swipe up or down on the left edge to adjust brightness",
"gestureVolumeSwipe": "Volume Swipe",
"gestureVolumeSwipeDescription": "Swipe up or down on the right edge to adjust volume",
"gesturePinchToZoom": "Pinch to Zoom",
"gesturePinchToZoomDescription": "Pinch on the video to zoom in or out",
"rememberBrightnessLevel": "Remember Brightness Level",
"rememberBrightnessLevelDescription": "Start playback at the brightness set by the last swipe",
"controls": "Controls",
"rememberPlayerChanges": "Remember Player Changes",
"rememberPlayerChangesDescription": "Where a change made during playback is saved and reapplied from",
"scopePlaybackSpeed": "Playback Speed",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Edit ${product} connection",
"editMediaBrowserIntro": "Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency."
},
"accountPreferences": {
"sectionTitle": "Account preferences",
"hubSubtitleSingle": "Audio, subtitle and library options saved on ${account}",
"hubSubtitleMultiple": "Audio, subtitle and library options saved on ${count} accounts",
"pickAccount": "Every account stores its own preferences. Pick the one to edit.",
"storedOnAccount": "These options are saved on the account itself, so every app signed in to it uses them — including Plezy on your other devices.",
"noAccounts": "No accounts to configure",
"noAccountsHint": "Sign in to Plex, or connect a Jellyfin or Emby server, and the preferences stored on that account show up here.",
"unavailable": "Can't reach this account",
"loadFailed": "Couldn't load these preferences",
"noPreference": "No preference",
"notSet": "Not set",
"groups": {
"audioAndSubtitles": "Audio & subtitles",
"libraryDisplay": "Library",
"personalMedia": "Personal media"
},
"preferredAudioLanguage": "Preferred audio language",
"autoSelectAudio": "Choose audio by language",
"autoSelectAudioDescription": "Off keeps whatever audio track the file marks as default.",
"preferredSubtitleLanguage": "Preferred subtitle language",
"subtitleMode": "Turn subtitles on",
"subtitleModes": {
"none": "Manually selected",
"noneDescription": "Never turn subtitles on by itself.",
"defaultMode": "Follow the track flags",
"defaultModeDescription": "Use the default and forced flags stored on each subtitle track.",
"always": "Always enabled",
"alwaysDescription": "Turn on a subtitle track in the preferred language whenever one exists.",
"onlyForced": "Only forced subtitles",
"onlyForcedDescription": "Load only the tracks marked as forced.",
"smart": "Shown with foreign audio",
"smartDescription": "Turn subtitles on only when the audio is in another language."
},
"subtitleAccessibility": "SDH subtitles",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Prefer non-SDH subtitles",
"preferSdh": "Prefer SDH subtitles",
"onlySdh": "Only SDH subtitles",
"onlyNonSdh": "Only non-SDH subtitles"
},
"forcedSubtitles": "Forced subtitles",
"forcedSubtitleOptions": {
"preferNonForced": "Prefer non-forced subtitles",
"preferForced": "Prefer forced subtitles",
"onlyForced": "Only forced subtitles",
"onlyNonForced": "Only non-forced subtitles"
},
"displayMissingEpisodes": "Show missing episodes",
"displayMissingEpisodesDescription": "List episodes the server knows about but has no file for.",
"hidePlayedInLatest": "Hide watched items in Latest",
"hidePlayedInLatestDescription": "Leave items you have already watched out of the server's Latest rows.",
"displayCollectionsView": "Show the Collections view",
"displayCollectionsViewDescription": "Offer the server's Collections view alongside your libraries.",
"rewatchingInNextUp": "Keep rewatched shows in Next Up",
"rewatchingInNextUpDescription": "Once you finish a show, start it again and Next Up follows the rewatch instead of dropping the show.",
"watchedIndicator": "Watched indicators",
"watchedIndicatorOptions": {
"none": "Never",
"moviesAndShows": "Movies and TV shows",
"movies": "Movies only",
"shows": "TV shows only"
},
"mediaReviewsVisibility": "Ratings & reviews",
"mediaReviewsOptions": {
"usersAndCritics": "Users and critics",
"usersOnly": "Users only",
"criticsOnly": "Critics only",
"nobody": "Hidden"
}
},
"discover": {
"title": "Discover",
"noContentAvailable": "No content available",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Unknown channel",
"live": "LIVE",
"reloadGuide": "Reload Guide",
"searchGuide": "Search Guide",
"searchHint": "Search channels & programs",
"searchNoResults": "No matches for \"${query}\"",
"channelsSection": "Channels",
"programsSection": "Programs",
"now": "Now",
"today": "Today",
"tomorrow": "Tomorrow",
@@ -1452,6 +1556,8 @@
"repeatAll": "Repeat all",
"repeatOne": "Repeat one",
"instantMixNoServer": "No server is available for an instant mix",
"instantMixFailed": "Failed to load the instant mix",
"instantMixEmpty": "The instant mix came back with no tracks",
"noAudioUrl": "No audio URL is available for ${track}",
"discography": {
"singlesAndEps": "Singles & EPs",
@@ -1483,6 +1589,13 @@
"host": "Host",
"hostBadge": "HOST",
"youAreHost": "You are the host",
"makeHost": "Make host",
"makeHostQuestion": "Transfer host?",
"makeHostConfirm": "${name} will control playback and drive the session for everyone.",
"transfer": "Transfer",
"hostChangedTo": "${name} is now the host",
"youAreNowHost": "You are now the host",
"hostTransferFailed": "Couldn't make ${name} the host",
"watchingWithOthers": "Watching with others",
"endSession": "End Session",
"leaveSession": "Leave Session",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Blocklisted",
"couldNotReach": "Could not reach ${url}: ${error}",
"noInstanceAtUrl": "No Seerr instance at ${url} (HTTP ${status})",
"invalidUrl": "Enter a server address like https://seerr.example.com",
"quickConnectUnsupported": "This Seerr instance does not support Quick Connect. It needs Seerr 3.4 or newer.",
"notInitialized": "This Seerr instance has not completed first-run setup",
"noPlexTokenForReauth": "No Plex token is available to sign in again",
"noStoredCredentials": "No stored credentials are available to sign in again",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Services",
"hubSubtitle": "Sync watch progress and request new titles.",
"integrations": "Integrations",
"notConnected": "Not connected",
"connectedAs": "Connected as @${username}",
"scrobble": "Track progress automatically",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Usar navegador",
"or": "o",
"connectToMediaBrowser": "Conectar a ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Usar Quick Connect",
"quickConnectInstructions": "Abre Quick Connect en Jellyfin e introduce este código.",
"quickConnectWaiting": "Esperando aprobación…",
@@ -53,6 +54,7 @@
"mute": "Silenciar",
"ok": "OK",
"off": "Desactivado",
"options": "Opciones",
"seasonNumber": "Temporada ${number}",
"episodeNumberTitle": "Episodio ${number} - ${title}",
"chapterNumber": "Capítulo ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Escala de visualización",
"compact": "Compacto",
"comfortable": "Cómodo",
"gridSpacing": "Espaciado de la cuadrícula",
"gridSpacingTight": "Ajustado",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Espacioso",
"tvCornerSpotlightBackdrop": "Imagen destacada en la esquina",
"tvCornerSpotlightBackdropDescription": "Mostrar la imagen destacada en la esquina superior derecha en lugar de ocupar toda la pantalla",
"viewMode": "Modo de vista",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Decodificación por hardware",
"hardwareDecodingDescription": "Usar aceleración por hardware cuando esté disponible",
"bufferSize": "Tamaño del búfer",
"bufferSizeMB": "${size} MB",
"bufferSizeAuto": "Automático (recomendado)",
"bufferSizeWarning": "${heap} MB de memoria disponible. Un búfer de ${size} MB puede afectar a la reproducción.",
"playbackBuffer": "Búfer de reproducción",
"playbackBufferAuto": "Automático (recomendado)",
"playbackBufferLarge": "Grande",
"playbackBufferExtraLarge": "Extra grande",
"playbackBufferDescription": "Almacena más en búfer para conexiones inestables. También limitado por el tamaño del búfer.",
"defaultQualityTitle": "Calidad predeterminada",
"cellularQualityTitle": "Calidad predeterminada en datos móviles",
"cellularQualitySameAsDefault": "Igual que la calidad predeterminada",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Calidad de música",
"subtitleStyling": "Estilo de subtítulos",
"subtitleStylingDescription": "Personalizar la apariencia de los subtítulos",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Recordar opciones de audio y subtítulos por título",
"followServerTrackSelections": "Usar la selección de pistas del servidor por episodio",
"followServerTrackSelectionsDescription": "Al cambiar de episodio, aplicar el audio y los subtítulos seleccionados en el servidor en lugar de mantener la elección actual",
"resumeMusicOnLaunch": "Recordar la sesión de música",
"resumeMusicOnLaunchDescription": "Al iniciar la aplicación, reabrir la última canción en pausa donde se quedó",
"showChapterMarkersOnTimeline": "Mostrar marcadores de capítulos en la barra de progreso",
"showChapterMarkersOnTimelineDescription": "Dividir la barra de progreso en los límites de capítulos",
"specialsOrdering": "Especiales en orden de episodios",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "El atajo ya está asignado a ${action}",
"shortcutUpdated": "Atajo actualizado para ${action}",
"saveFailed": "No se pudieron guardar los cambios. Inténtalo de nuevo.",
"autoSkip": "Salto automático",
"autoPlayAndSkip": "Reproducción automática y saltos",
"autoPlayNextEpisode": "Reproducción automática del siguiente episodio",
"autoPlayNextEpisodeDescription": "Iniciar automáticamente el siguiente episodio cuando termine el actual",
"playNextCountdown": "Cuenta atrás para el siguiente episodio",
"playNextCountdownImmediate": "Reproducir de inmediato",
"autoSkipIntro": "Saltar introducción automáticamente",
"autoSkipIntroDescription": "Saltar automáticamente los marcadores de introducción después de unos segundos",
"autoSkipCredits": "Saltar créditos automáticamente",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Forzar DV7 nativo y suprimir el reintento de conversión DV",
"dvConversionDv81Description": "Forzar la conversión de RPU en línea al perfil 8.1 de Dolby Vision",
"dvConversionHevcStripDescription": "Eliminar las capas RPU/EL de Dolby Vision y presentar HEVC convencional",
"demuxer": "Demultiplexor de contenedor",
"demuxerDescription": "Qué demultiplexor analiza los archivos de reproducción directa. Cambia a media3 si un archivo da problemas.",
"demuxerFfmpeg": "FFmpeg (recomendado)",
"demuxerMedia3": "media3",
"deinterlace": "Desentrelazado",
"deinterlaceDescription": "Elimina los artefactos de peine del video entrelazado (solo reproductor mpv)",
"requireProfileSelectionOnOpen": "Pedir perfil al abrir la aplicación",
"requireProfileSelectionOnOpenDescription": "Mostrar selección de perfil cada vez que se abre la aplicación",
"forceTvMode": "Forzar modo TV",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Muestra la pestaña Explorar con contenido de Plex Discover y servicios de seguimiento conectados",
"liveTvDefaultFavorites": "Canales favoritos por defecto",
"liveTvDefaultFavoritesDescription": "Mostrar solo canales favoritos al abrir TV en vivo",
"general": "General",
"generalDescription": "Idioma, inicio y comportamiento de la ventana",
"languageAndRegion": "Idioma y región",
"startup": "Inicio",
"display": "Pantalla",
"libraryAndCards": "Biblioteca y tarjetas",
"homeScreen": "Pantalla de inicio",
"navigation": "Navegación",
"window": "Ventana",
"content": "Contenido",
"liveTv": "TV en vivo",
"player": "Reproductor",
"subtitlesAndConfig": "Subtítulos y configuración",
"videoAndDisplay": "Video y pantalla",
"audio": "Audio",
"quality": "Calidad",
"subtitles": "Subtítulos",
"seekAndTiming": "Desplazamiento y tiempos",
"behavior": "Comportamiento",
"gestures": "Gestos",
"gestureBrightnessSwipe": "Gesto de brillo",
"gestureBrightnessSwipeDescription": "Desliza hacia arriba o abajo en el borde izquierdo para ajustar el brillo",
"gestureVolumeSwipe": "Gesto de volumen",
"gestureVolumeSwipeDescription": "Desliza hacia arriba o abajo en el borde derecho para ajustar el volumen",
"gesturePinchToZoom": "Pellizcar para hacer zoom",
"gesturePinchToZoomDescription": "Pellizca el video para acercar o alejar",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Controles",
"rememberPlayerChanges": "Recordar cambios del reproductor",
"rememberPlayerChangesDescription": "Dónde se guarda y se vuelve a aplicar un cambio realizado durante la reproducción",
"scopePlaybackSpeed": "Velocidad de reproducción",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Editar conexión de ${product}",
"editMediaBrowserIntro": "Añade o elimina URL para ${serverName}. Plezy usará la URL accesible con menor latencia."
},
"accountPreferences": {
"sectionTitle": "Preferencias de cuenta",
"hubSubtitleSingle": "Opciones de audio, subtítulos y biblioteca guardadas en ${account}",
"hubSubtitleMultiple": "Opciones de audio, subtítulos y biblioteca guardadas en ${count} cuentas",
"pickAccount": "Cada cuenta guarda sus propias preferencias. Elige la que quieras editar.",
"storedOnAccount": "Estas opciones se guardan en la propia cuenta, así que cualquier aplicación con la sesión iniciada las usa, incluido Plezy en tus otros dispositivos.",
"noAccounts": "No hay cuentas que configurar",
"noAccountsHint": "Inicia sesión con Plex o conecta un servidor de Jellyfin o Emby y las preferencias guardadas en esa cuenta aparecerán aquí.",
"unavailable": "No se puede acceder a esta cuenta",
"loadFailed": "No se pudieron cargar estas preferencias",
"noPreference": "Sin preferencia",
"notSet": "No establecido",
"groups": {
"audioAndSubtitles": "Audio y subtítulos",
"libraryDisplay": "Biblioteca",
"personalMedia": "Medios personales"
},
"preferredAudioLanguage": "Idioma de audio preferido",
"autoSelectAudio": "Elegir audio por idioma",
"autoSelectAudioDescription": "Desactivado mantiene la pista de audio que el archivo marca como predeterminada.",
"preferredSubtitleLanguage": "Idioma de subtítulos preferido",
"subtitleMode": "Activar subtítulos",
"subtitleModes": {
"none": "Seleccionado manualmente",
"noneDescription": "No activar los subtítulos por sí solo.",
"defaultMode": "Seguir las marcas de la pista",
"defaultModeDescription": "Usar las marcas de predeterminada y forzada guardadas en cada pista de subtítulos.",
"always": "Siempre activado",
"alwaysDescription": "Activar una pista de subtítulos en el idioma preferido siempre que exista.",
"onlyForced": "Solo subtítulos forzados",
"onlyForcedDescription": "Cargar solo las pistas marcadas como forzadas.",
"smart": "Mostrar con audio extranjero",
"smartDescription": "Activar los subtítulos solo cuando el audio está en otro idioma."
},
"subtitleAccessibility": "Subtítulos SDH",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Preferir subtítulos no SDH",
"preferSdh": "Preferir subtítulos SDH",
"onlySdh": "Solo subtítulos SDH",
"onlyNonSdh": "Solo subtítulos no SDH"
},
"forcedSubtitles": "Subtítulos forzados",
"forcedSubtitleOptions": {
"preferNonForced": "Preferir subtítulos no forzados",
"preferForced": "Preferir subtítulos forzados",
"onlyForced": "Solo subtítulos forzados",
"onlyNonForced": "Solo subtítulos no forzados"
},
"displayMissingEpisodes": "Mostrar episodios faltantes",
"displayMissingEpisodesDescription": "Enumera los episodios que el servidor conoce pero para los que no tiene ningún archivo.",
"hidePlayedInLatest": "Ocultar elementos vistos en Recientes",
"hidePlayedInLatestDescription": "Deja fuera de las filas de Recientes del servidor los elementos que ya has visto.",
"displayCollectionsView": "Mostrar la vista de colecciones",
"displayCollectionsViewDescription": "Ofrecer la vista de colecciones del servidor junto a tus bibliotecas.",
"rewatchingInNextUp": "Mantener las series revistas en A continuación",
"rewatchingInNextUpDescription": "Cuando termines una serie y la vuelvas a empezar, A continuación seguirá tu revisión en lugar de descartarla.",
"watchedIndicator": "Indicadores de visto",
"watchedIndicatorOptions": {
"none": "Nunca",
"moviesAndShows": "Películas y series de TV",
"movies": "Solo películas",
"shows": "Solo series de TV"
},
"mediaReviewsVisibility": "Valoraciones y reseñas",
"mediaReviewsOptions": {
"usersAndCritics": "Usuarios y críticos",
"usersOnly": "Solo usuarios",
"criticsOnly": "Solo críticos",
"nobody": "Ocultos"
}
},
"discover": {
"title": "Descubrir",
"noContentAvailable": "No hay contenido disponible",
@@ -1159,7 +1258,7 @@
"searchPrompt": "Busca películas y series en ${source}.",
"searchFailed": "La búsqueda falló. Comprueba tu conexión e inténtalo de nuevo.",
"badge": {
"rankPopular": "#${n} popular",
"rankPopular": "#${n} más popular",
"rankAiring": "#${n} en emisión",
"rankRated": "#${n} puntuado",
"rankTrending": "#${n} en tendencia",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Canal desconocido",
"live": "EN VIVO",
"reloadGuide": "Recargar guía",
"searchGuide": "Buscar en la guía",
"searchHint": "Buscar canales y programas",
"searchNoResults": "Sin coincidencias para \"${query}\"",
"channelsSection": "Canales",
"programsSection": "Programas",
"now": "Ahora",
"today": "Hoy",
"tomorrow": "Mañana",
@@ -1452,6 +1556,8 @@
"repeatAll": "Repetir todo",
"repeatOne": "Repetir una",
"instantMixNoServer": "No hay ningún servidor disponible para una mezcla instantánea",
"instantMixFailed": "Error al cargar la mezcla instantánea",
"instantMixEmpty": "La mezcla instantánea no devolvió ninguna canción",
"noAudioUrl": "No hay ninguna URL de audio disponible para ${track}",
"discography": {
"singlesAndEps": "Sencillos y EP",
@@ -1483,6 +1589,13 @@
"host": "Anfitrión",
"hostBadge": "ANFITRIÓN",
"youAreHost": "Eres el anfitrión",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Viendo contenido con otras personas",
"endSession": "Finalizar sesión",
"leaveSession": "Salir de la sesión",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "En la lista de bloqueo",
"couldNotReach": "No se pudo conectar con ${url}: ${error}",
"noInstanceAtUrl": "No hay ninguna instancia de Seerr en ${url} (HTTP ${status})",
"invalidUrl": "Introduce una dirección de servidor como https://seerr.example.com",
"quickConnectUnsupported": "Esta instancia de Seerr no admite Quick Connect. Necesita Seerr 3.4 o más reciente.",
"notInitialized": "Esta instancia de Seerr no ha completado la configuración inicial",
"noPlexTokenForReauth": "No hay ningún token de Plex disponible para volver a iniciar sesión",
"noStoredCredentials": "No hay credenciales guardadas disponibles para volver a iniciar sesión",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Servicios",
"hubSubtitle": "Sincroniza tu progreso de visualización y solicita nuevos títulos.",
"integrations": "Integraciones",
"notConnected": "No conectado",
"connectedAs": "Conectado como @${username}",
"scrobble": "Registrar progreso automáticamente",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Utiliser le navigateur",
"or": "ou",
"connectToMediaBrowser": "Se connecter à ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Utiliser Quick Connect",
"quickConnectInstructions": "Ouvrez Quick Connect dans Jellyfin et saisissez ce code.",
"quickConnectWaiting": "En attente d'approbation…",
@@ -53,6 +54,7 @@
"mute": "Couper le son",
"ok": "OK",
"off": "Désactivé",
"options": "Options",
"seasonNumber": "Saison ${number}",
"episodeNumberTitle": "Épisode ${number} – ${title}",
"chapterNumber": "Chapitre ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Échelle d'affichage",
"compact": "Compact",
"comfortable": "Confortable",
"gridSpacing": "Espacement de la grille",
"gridSpacingTight": "Resserré",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Aéré",
"tvCornerSpotlightBackdrop": "Illustration en vedette dans le coin",
"tvCornerSpotlightBackdropDescription": "Afficher l’illustration en vedette dans le coin supérieur droit plutôt qu’en plein écran",
"viewMode": "Mode d'affichage",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Décodage matériel",
"hardwareDecodingDescription": "Utiliser l’accélération matérielle lorsqu’elle est disponible",
"bufferSize": "Taille du tampon",
"bufferSizeMB": "${size} Mo",
"bufferSizeAuto": "Automatique (recommandé)",
"bufferSizeWarning": "${heap} Mo de mémoire disponible. Un tampon de ${size} Mo peut affecter la lecture.",
"playbackBuffer": "Tampon de lecture",
"playbackBufferAuto": "Auto (recommandé)",
"playbackBufferLarge": "Grand",
"playbackBufferExtraLarge": "Très grand",
"playbackBufferDescription": "Tampon supplémentaire contre les connexions instables. Également limité par la taille du tampon.",
"defaultQualityTitle": "Qualité par défaut",
"cellularQualityTitle": "Qualité par défaut sur les données mobiles",
"cellularQualitySameAsDefault": "Identique à la qualité par défaut",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Qualité de la musique",
"subtitleStyling": "Style des sous-titres",
"subtitleStylingDescription": "Personnaliser l’apparence des sous-titres",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Mémoriser les choix audio et sous-titres par titre",
"followServerTrackSelections": "Utiliser les pistes sélectionnées sur le serveur pour chaque épisode",
"followServerTrackSelectionsDescription": "Au changement d'épisode, appliquer l'audio et les sous-titres sélectionnés sur le serveur au lieu de conserver le choix en cours",
"resumeMusicOnLaunch": "Mémoriser la session musicale",
"resumeMusicOnLaunchDescription": "Au lancement de l'application, rouvrir le dernier titre en pause là où il s'était arrêté",
"showChapterMarkersOnTimeline": "Afficher les marqueurs de chapitres sur la barre de lecture",
"showChapterMarkersOnTimelineDescription": "Segmenter la barre de lecture aux limites des chapitres",
"specialsOrdering": "Épisodes spéciaux dans l’ordre des épisodes",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Raccourci déjà attribué à ${action}",
"shortcutUpdated": "Raccourci mis à jour pour ${action}",
"saveFailed": "Impossible d’enregistrer les modifications. Réessayez.",
"autoSkip": "Saut automatique",
"autoPlayAndSkip": "Lecture automatique et saut",
"autoPlayNextEpisode": "Lire automatiquement l'épisode suivant",
"autoPlayNextEpisodeDescription": "Lancer automatiquement l'épisode suivant lorsqu'un épisode se termine",
"playNextCountdown": "Compte à rebours avant l'épisode suivant",
"playNextCountdownImmediate": "Lire immédiatement",
"autoSkipIntro": "Passer automatiquement l’introduction",
"autoSkipIntroDescription": "Passer automatiquement les marqueurs d’introduction après quelques secondes",
"autoSkipCredits": "Passer automatiquement le générique",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Forcer le DV7 natif et bloquer la nouvelle tentative de conversion DV",
"dvConversionDv81Description": "Forcer la conversion RPU intégrée vers le profil 8.1 de Dolby Vision",
"dvConversionHevcStripDescription": "Supprimer les couches RPU/EL Dolby Vision et présenter du HEVC simple",
"demuxer": "Démultiplexeur de conteneur",
"demuxerDescription": "Quel démultiplexeur analyse les fichiers lus en direct. Passez à media3 si un fichier pose problème.",
"demuxerFfmpeg": "FFmpeg (recommandé)",
"demuxerMedia3": "media3",
"deinterlace": "Désentrelacement",
"deinterlaceDescription": "Supprimer les artefacts de peignage des vidéos entrelacées (lecteur mpv uniquement)",
"requireProfileSelectionOnOpen": "Demander le profil à l'ouverture",
"requireProfileSelectionOnOpenDescription": "Afficher la sélection de profil à chaque ouverture de l'application",
"forceTvMode": "Forcer le mode TV",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Afficher l’onglet Explorer avec le contenu de Plex Discover et des services de suivi connectés",
"liveTvDefaultFavorites": "Chaînes favorites par défaut",
"liveTvDefaultFavoritesDescription": "Afficher uniquement les chaînes favorites à l'ouverture de la TV en direct",
"general": "Général",
"generalDescription": "Langue, démarrage et comportement de la fenêtre",
"languageAndRegion": "Langue et région",
"startup": "Démarrage",
"display": "Affichage",
"libraryAndCards": "Bibliothèque et cartes",
"homeScreen": "Écran d'accueil",
"navigation": "Navigation",
"window": "Fenêtre",
"content": "Contenu",
"liveTv": "TV en direct",
"player": "Lecteur",
"subtitlesAndConfig": "Sous-titres et configuration",
"videoAndDisplay": "Vidéo et affichage",
"audio": "Audio",
"quality": "Qualité",
"subtitles": "Sous-titres",
"seekAndTiming": "Déplacement et minutage",
"behavior": "Comportement",
"gestures": "Gestes",
"gestureBrightnessSwipe": "Balayage de luminosité",
"gestureBrightnessSwipeDescription": "Balayez vers le haut ou le bas sur le bord gauche pour régler la luminosité",
"gestureVolumeSwipe": "Balayage de volume",
"gestureVolumeSwipeDescription": "Balayez vers le haut ou le bas sur le bord droit pour régler le volume",
"gesturePinchToZoom": "Pincer pour zoomer",
"gesturePinchToZoomDescription": "Pincez la vidéo pour zoomer ou dézoomer",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Commandes",
"rememberPlayerChanges": "Mémoriser les modifications du lecteur",
"rememberPlayerChangesDescription": "Où une modification effectuée pendant la lecture est enregistrée et réappliquée",
"scopePlaybackSpeed": "Vitesse de lecture",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Modifier la connexion ${product}",
"editMediaBrowserIntro": "Ajoutez ou retirez des URL pour ${serverName}. Plezy utilisera l'URL joignable avec la latence la plus faible."
},
"accountPreferences": {
"sectionTitle": "Préférences du compte",
"hubSubtitleSingle": "Options audio, sous-titres et bibliothèque enregistrées sur ${account}",
"hubSubtitleMultiple": "Options audio, sous-titres et bibliothèque enregistrées sur ${count} comptes",
"pickAccount": "Chaque compte enregistre ses propres préférences. Choisissez celui à modifier.",
"storedOnAccount": "Ces options sont enregistrées sur le compte lui-même : toutes les applications connectées les utilisent, y compris Plezy sur vos autres appareils.",
"noAccounts": "Aucun compte à configurer",
"noAccountsHint": "Connectez-vous à Plex, ou connectez un serveur Jellyfin ou Emby : les préférences enregistrées sur ce compte apparaîtront ici.",
"unavailable": "Impossible de joindre ce compte",
"loadFailed": "Impossible de charger ces préférences",
"noPreference": "Aucune préférence",
"notSet": "Non défini",
"groups": {
"audioAndSubtitles": "Audio et sous-titres",
"libraryDisplay": "Bibliothèque",
"personalMedia": "Médias personnels"
},
"preferredAudioLanguage": "Langue audio préférée",
"autoSelectAudio": "Choisir l'audio par langue",
"autoSelectAudioDescription": "Désactivé : la piste audio marquée par défaut dans le fichier est conservée.",
"preferredSubtitleLanguage": "Langue de sous-titres préférée",
"subtitleMode": "Activer les sous-titres",
"subtitleModes": {
"none": "Sélection manuelle",
"noneDescription": "Ne jamais activer les sous-titres automatiquement.",
"defaultMode": "Suivre les indicateurs des pistes",
"defaultModeDescription": "Utiliser les indicateurs par défaut et forcés enregistrés sur chaque piste de sous-titres.",
"always": "Toujours activé",
"alwaysDescription": "Activer une piste de sous-titres dans la langue préférée lorsqu'elle existe.",
"onlyForced": "Sous-titres forcés uniquement",
"onlyForcedDescription": "Charger uniquement les pistes marquées comme forcées.",
"smart": "Avec l'audio en langue étrangère",
"smartDescription": "Activer les sous-titres uniquement lorsque l'audio est dans une autre langue."
},
"subtitleAccessibility": "Sous-titres SDH",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Préférer les sous-titres non SDH",
"preferSdh": "Préférer les sous-titres SDH",
"onlySdh": "Sous-titres SDH uniquement",
"onlyNonSdh": "Sous-titres non SDH uniquement"
},
"forcedSubtitles": "Sous-titres forcés",
"forcedSubtitleOptions": {
"preferNonForced": "Préférer les sous-titres non forcés",
"preferForced": "Préférer les sous-titres forcés",
"onlyForced": "Sous-titres forcés uniquement",
"onlyNonForced": "Sous-titres non forcés uniquement"
},
"displayMissingEpisodes": "Afficher les épisodes manquants",
"displayMissingEpisodesDescription": "Répertorier les épisodes connus du serveur mais sans fichier.",
"hidePlayedInLatest": "Masquer les éléments vus dans « Récemment ajouté »",
"hidePlayedInLatestDescription": "Exclure les éléments déjà vus des lignes « Récemment ajouté » du serveur.",
"displayCollectionsView": "Afficher la vue Collections",
"displayCollectionsViewDescription": "Proposer la vue Collections du serveur en plus de vos bibliothèques.",
"rewatchingInNextUp": "Conserver les séries revues dans « À suivre »",
"rewatchingInNextUpDescription": "Une fois une série terminée, si vous la recommencez, « À suivre » suit le revisionnage au lieu de la retirer.",
"watchedIndicator": "Indicateurs de visionnage",
"watchedIndicatorOptions": {
"none": "Jamais",
"moviesAndShows": "Films et séries TV",
"movies": "Films uniquement",
"shows": "Séries TV uniquement"
},
"mediaReviewsVisibility": "Notes et critiques",
"mediaReviewsOptions": {
"usersAndCritics": "Utilisateurs et critiques",
"usersOnly": "Utilisateurs uniquement",
"criticsOnly": "Critiques uniquement",
"nobody": "Masquées"
}
},
"discover": {
"title": "Découvrir",
"noContentAvailable": "Aucun contenu disponible",
@@ -1263,7 +1362,7 @@
"budget": "Budget",
"revenue": "Box-office",
"contentAdvisory": "Recommandation d'âge",
"tags": "Tags",
"tags": "Étiquettes",
"revealSpoilerTags": "Afficher les tags spoiler",
"links": "Liens",
"watchOn": "Regarder sur",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Chaîne inconnue",
"live": "EN DIRECT",
"reloadGuide": "Recharger le guide",
"searchGuide": "Rechercher dans le guide",
"searchHint": "Rechercher des chaînes et des programmes",
"searchNoResults": "Aucune correspondance pour \"${query}\"",
"channelsSection": "Chaînes",
"programsSection": "Programmes",
"now": "Maintenant",
"today": "Aujourd'hui",
"tomorrow": "Demain",
@@ -1452,6 +1556,8 @@
"repeatAll": "Tout répéter",
"repeatOne": "Répéter le titre",
"instantMixNoServer": "Aucun serveur n’est disponible pour créer un mix instantané",
"instantMixFailed": "Impossible de charger le mix instantané",
"instantMixEmpty": "Le mix instantané n'a renvoyé aucun titre",
"noAudioUrl": "Aucune URL audio n’est disponible pour ${track}",
"discography": {
"singlesAndEps": "Singles et EP",
@@ -1483,6 +1589,13 @@
"host": "Hôte",
"hostBadge": "HÔTE",
"youAreHost": "Vous êtes l'hôte",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Regarder avec d'autres personnes",
"endSession": "Terminer la session",
"leaveSession": "Quitter la session",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Sur la liste de blocage",
"couldNotReach": "Impossible de joindre ${url} : ${error}",
"noInstanceAtUrl": "Aucune instance Seerr à l’adresse ${url} (HTTP ${status})",
"invalidUrl": "Saisissez une adresse de serveur comme https://seerr.example.com",
"quickConnectUnsupported": "Cette instance Seerr ne prend pas en charge Quick Connect. Elle nécessite Seerr 3.4 ou version ultérieure.",
"notInitialized": "La configuration initiale de cette instance Seerr n’est pas terminée",
"noPlexTokenForReauth": "Aucun jeton Plex n’est disponible pour se reconnecter",
"noStoredCredentials": "Aucun identifiant enregistré n’est disponible pour se reconnecter",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Services",
"hubSubtitle": "Synchronisez votre progression et demandez de nouveaux titres.",
"integrations": "Intégrations",
"notConnected": "Non connecté",
"connectedAs": "Connecté en tant que @${username}",
"scrobble": "Suivre la progression automatiquement",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Böngésző használata",
"or": "vagy",
"connectToMediaBrowser": "Csatlakozás a(z) ${product} szolgáltatáshoz",
"quickConnect": "Quick Connect",
"useQuickConnect": "Quick Connect használata",
"quickConnectInstructions": "Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.",
"quickConnectWaiting": "Várakozás a jóváhagyásra…",
@@ -53,6 +54,7 @@
"mute": "Némítás",
"ok": "OK",
"off": "Ki",
"options": "Lehetőségek",
"seasonNumber": "${number}. évad",
"episodeNumberTitle": "${number}. epizód - ${title}",
"chapterNumber": "${number}. fejezet",
@@ -141,6 +143,10 @@
"displayScale": "Kijelző méretezése",
"compact": "Kompakt",
"comfortable": "Kényelmes",
"gridSpacing": "Rácstávolság",
"gridSpacingTight": "Sűrű",
"gridSpacingNormal": "Normál",
"gridSpacingSpacious": "Tágas",
"tvCornerSpotlightBackdrop": "Sarokban megjelenő kiemelt háttérkép",
"tvCornerSpotlightBackdropDescription": "A kiemelt háttérkép megjelenítése a jobb felső sarokban a teljes képernyő helyett",
"viewMode": "Nézetmód",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Hardveres dekódolás",
"hardwareDecodingDescription": "Hardveres gyorsítás használata, ha elérhető",
"bufferSize": "Puffer mérete",
"bufferSizeMB": "${size} MB",
"bufferSizeAuto": "Automatikus (ajánlott)",
"bufferSizeWarning": "${heap} MB memória érhető el. A(z) ${size} MB méretű puffer befolyásolhatja a lejátszást.",
"playbackBuffer": "Lejátszási puffer",
"playbackBufferAuto": "Automatikus (ajánlott)",
"playbackBufferLarge": "Nagy",
"playbackBufferExtraLarge": "Extra nagy",
"playbackBufferDescription": "Több puffert használ instabil kapcsolatok ellen. A pufferméret is korlátozza.",
"defaultQualityTitle": "Alapértelmezett minőség",
"cellularQualityTitle": "Alapértelmezett minőség mobilhálózaton",
"cellularQualitySameAsDefault": "Ugyanaz, mint az alapértelmezett minőség",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Zene minősége",
"subtitleStyling": "Feliratok stílusa",
"subtitleStylingDescription": "Feliratok megjelenésének testreszabása",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Hang- és feliratválasztások megjegyzése címenként",
"followServerTrackSelections": "A szerver epizódonkénti sávválasztásának használata",
"followServerTrackSelectionsDescription": "Epizódváltáskor a szerveren kiválasztott hang és felirat lép életbe az aktuális választás átvitele helyett",
"resumeMusicOnLaunch": "Zenei munkamenet megjegyzése",
"resumeMusicOnLaunchDescription": "Az alkalmazás indításakor az utolsó szám szüneteltetve nyílik meg ott, ahol abbamaradt",
"showChapterMarkersOnTimeline": "Fejezetjelölők megjelenítése az idősávon",
"showChapterMarkersOnTimelineDescription": "Az idősáv felosztása a fejezetek határainál",
"specialsOrdering": "Különkiadások epizódsorrendben",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "A billentyűparancs már hozzá van rendelve a következőhöz: ${action}",
"shortcutUpdated": "Billentyűparancs frissítve a következőhöz: ${action}",
"saveFailed": "Nem sikerült menteni a módosításokat. Próbáld újra.",
"autoSkip": "Automatikus átugrás",
"autoPlayAndSkip": "Automatikus lejátszás és átugrás",
"autoPlayNextEpisode": "Következő epizód automatikus lejátszása",
"autoPlayNextEpisodeDescription": "A következő epizód automatikus elindítása, amikor az aktuális véget ér",
"playNextCountdown": "Visszaszámlálás a következő epizódig",
"playNextCountdownImmediate": "Azonnali lejátszás",
"autoSkipIntro": "Intró automatikus átugrása",
"autoSkipIntroDescription": "Az intrójelölők automatikus átugrása néhány másodperc után",
"autoSkipCredits": "Stáblista automatikus átugrása",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "A natív DV7 kényszerítése és a DV-átalakítási újrapróbálkozás letiltása",
"dvConversionDv81Description": "A közvetlen RPU-átalakítás kényszerítése Dolby Vision Profile 8.1-re",
"dvConversionHevcStripDescription": "A Dolby Vision RPU/EL-rétegek eltávolítása és egyszerű HEVC-ként való megjelenítés",
"demuxer": "Konténer-demuxer",
"demuxerDescription": "Melyik demuxer dolgozza fel a közvetlenül lejátszott fájlokat. Válts media3-ra, ha egy fájl hibásan viselkedik.",
"demuxerFfmpeg": "FFmpeg (ajánlott)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacing",
"deinterlaceDescription": "Fésűfog-műtermékek eltávolítása a váltott soros videóból (csak mpv lejátszóval)",
"requireProfileSelectionOnOpen": "Profil kérése az alkalmazás megnyitásakor",
"requireProfileSelectionOnOpenDescription": "Profilválasztó megjelenítése minden alkalommal, amikor az alkalmazást megnyitod",
"forceTvMode": "TV-mód kényszerítése",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "A Plex Discover és a csatlakoztatott követők tartalmait megjelenítő Felfedezés lap megjelenítése",
"liveTvDefaultFavorites": "Kedvenc csatornák alapértelmezés szerint",
"liveTvDefaultFavoritesDescription": "Csak a kedvenc csatornák megjelenítése az Élő TV megnyitásakor",
"general": "Általános",
"generalDescription": "Nyelv, indítás és ablakviselkedés",
"languageAndRegion": "Nyelv és régió",
"startup": "Indítás",
"display": "Kijelző",
"libraryAndCards": "Könyvtár és kártyák",
"homeScreen": "Kezdőképernyő",
"navigation": "Navigáció",
"window": "Ablak",
"content": "Tartalom",
"liveTv": "Élő TV",
"player": "Lejátszó",
"subtitlesAndConfig": "Feliratok és konfiguráció",
"videoAndDisplay": "Videó és kijelző",
"audio": "Hang",
"quality": "Minőség",
"subtitles": "Feliratok",
"seekAndTiming": "Tekerés és időzítés",
"behavior": "Viselkedés",
"gestures": "Mozdulatok",
"gestureBrightnessSwipe": "Fényerő-húzás",
"gestureBrightnessSwipeDescription": "Húzd felfelé vagy lefelé a bal szélén a fényerő beállításához",
"gestureVolumeSwipe": "Hangerő-húzás",
"gestureVolumeSwipeDescription": "Húzd felfelé vagy lefelé a jobb szélén a hangerő beállításához",
"gesturePinchToZoom": "Csípés a nagyításhoz",
"gesturePinchToZoomDescription": "Csípj a videóra a nagyításhoz vagy kicsinyítéshez",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Vezérlők",
"rememberPlayerChanges": "Lejátszó módosításainak megjegyzése",
"rememberPlayerChangesDescription": "A lejátszás közben végzett módosítások mentési és újbóli alkalmazási helye",
"scopePlaybackSpeed": "Lejátszási sebesség",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "A(z) ${product} kapcsolat szerkesztése",
"editMediaBrowserIntro": "Adjon hozzá vagy távolítson el URL-eket a(z) ${serverName} esetén. A Plezy a legkisebb késleltetésű, elérhető URL-t fogja használni."
},
"accountPreferences": {
"sectionTitle": "Fiókbeállítások",
"hubSubtitleSingle": "Hang-, felirat- és könyvtárbeállítások mentve a(z) ${account} fiókra",
"hubSubtitleMultiple": "Hang-, felirat- és könyvtárbeállítások mentve ${count} fiókra",
"pickAccount": "Minden fiók a saját beállításait tárolja. Válaszd ki a szerkesztendőt.",
"storedOnAccount": "Ezek a beállítások magán a fiókon tárolódnak, így minden bejelentkezett alkalmazás használja őket — beleértve a Plezyt a többi eszközödön.",
"noAccounts": "Nincs beállítható fiók",
"noAccountsHint": "Jelentkezz be a Plexbe, vagy csatlakoztass egy Jellyfin- vagy Emby-szervert, és az azon a fiókon tárolt beállítások itt jelennek meg.",
"unavailable": "Ez a fiók nem érhető el",
"loadFailed": "Nem sikerült betölteni ezeket a beállításokat",
"noPreference": "Nincs preferencia",
"notSet": "Nincs beállítva",
"groups": {
"audioAndSubtitles": "Hang és feliratok",
"libraryDisplay": "Könyvtár",
"personalMedia": "Személyes média"
},
"preferredAudioLanguage": "Elsődleges hangnyelv",
"autoSelectAudio": "Hangsáv kiválasztása nyelv alapján",
"autoSelectAudioDescription": "Ki kapcsolásnál a fájl által alapértelmezettként megjelölt hangsáv marad.",
"preferredSubtitleLanguage": "Elsődleges feliratnyelv",
"subtitleMode": "Feliratok bekapcsolása",
"subtitleModes": {
"none": "Kézzel kiválasztva",
"noneDescription": "Soha ne kapcsolja be magától a feliratokat.",
"defaultMode": "A sáv jelzőinek követése",
"defaultModeDescription": "Az egyes feliratsávokon tárolt alapértelmezett és kényszerített jelzők használata.",
"always": "Mindig engedélyezve",
"alwaysDescription": "Feliratsáv bekapcsolása az elsődleges nyelven, ha van ilyen.",
"onlyForced": "Csak kényszerített feliratok",
"onlyForcedDescription": "Csak a kényszerítettként megjelölt sávok betöltése.",
"smart": "Idegen nyelvű hang esetén megjelenítve",
"smartDescription": "Feliratok bekapcsolása csak akkor, ha a hang más nyelven szól."
},
"subtitleAccessibility": "SDH-feliratok",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Nem SDH-feliratok előnyben részesítése",
"preferSdh": "SDH-feliratok előnyben részesítése",
"onlySdh": "Csak SDH-feliratok",
"onlyNonSdh": "Csak nem SDH-feliratok"
},
"forcedSubtitles": "Kényszerített feliratok",
"forcedSubtitleOptions": {
"preferNonForced": "Nem kényszerített feliratok előnyben részesítése",
"preferForced": "Kényszerített feliratok előnyben részesítése",
"onlyForced": "Csak kényszerített feliratok",
"onlyNonForced": "Csak nem kényszerített feliratok"
},
"displayMissingEpisodes": "Hiányzó epizódok megjelenítése",
"displayMissingEpisodesDescription": "Azoknak az epizódoknak a listázása, amelyeket a szerver ismer, de nincs hozzájuk fájl.",
"hidePlayedInLatest": "Megnézett elemek elrejtése a Legújabbak között",
"hidePlayedInLatestDescription": "A már megnézett elemek kimaradnak a szerver Legújabbak soraiból.",
"displayCollectionsView": "Gyűjtemények nézet megjelenítése",
"displayCollectionsViewDescription": "A szerver Gyűjtemények nézetének megjelenítése a könyvtárak mellett.",
"rewatchingInNextUp": "Újranézett sorozatok megtartása a Következikben",
"rewatchingInNextUpDescription": "Ha befejezel egy sorozatot, és újra elkezded, a Következik az újranézést követi, ahelyett hogy eldobná a sorozatot.",
"watchedIndicator": "Megtekintett jelzések",
"watchedIndicatorOptions": {
"none": "Soha",
"moviesAndShows": "Filmek és TV-sorozatok",
"movies": "Csak filmek",
"shows": "Csak TV-sorozatok"
},
"mediaReviewsVisibility": "Értékelések és kritikák",
"mediaReviewsOptions": {
"usersAndCritics": "Felhasználók és kritikusok",
"usersOnly": "Csak felhasználók",
"criticsOnly": "Csak kritikusok",
"nobody": "Rejtett"
}
},
"discover": {
"title": "Felfedezés",
"noContentAvailable": "Nincs elérhető tartalom",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Ismeretlen csatorna",
"live": "ÉLŐ",
"reloadGuide": "Műsorújság újratöltése",
"searchGuide": "Keresés a műsorújságban",
"searchHint": "Csatornák és műsorok keresése",
"searchNoResults": "Nincs találat a következőre: \"${query}\"",
"channelsSection": "Csatornák",
"programsSection": "Műsorok",
"now": "Most",
"today": "Ma",
"tomorrow": "Holnap",
@@ -1452,6 +1556,8 @@
"repeatAll": "Összes ismétlése",
"repeatOne": "Egy szám ismétlése",
"instantMixNoServer": "Nem érhető el szerver az azonnali mixhez",
"instantMixFailed": "Nem sikerült betölteni az azonnali mixet",
"instantMixEmpty": "Az azonnali mix nem adott vissza zeneszámokat",
"noAudioUrl": "Nem érhető el hang-URL ehhez: ${track}",
"discography": {
"singlesAndEps": "Kislemezek és EP-k",
@@ -1483,6 +1589,13 @@
"host": "Házigazda",
"hostBadge": "HÁZIGAZDA",
"youAreHost": "Te vagy a házigazda",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Nézés másokkal",
"endSession": "Munkamenet befejezése",
"leaveSession": "Munkamenet elhagyása",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Tiltólistán",
"couldNotReach": "Nem sikerült elérni ezt: ${url}: ${error}",
"noInstanceAtUrl": "Nem található Seerr-példány ezen a címen: ${url} (HTTP ${status})",
"invalidUrl": "Adj meg egy szervercímet, például: https://seerr.example.com",
"quickConnectUnsupported": "Ez a Seerr-példány nem támogatja a Quick Connectet. Seerr 3.4 vagy újabb verzió szükséges.",
"notInitialized": "Ennek a Seerr-példánynak a kezdeti beállítása még nem fejeződött be",
"noPlexTokenForReauth": "Nem áll rendelkezésre Plex-token az újbóli bejelentkezéshez",
"noStoredCredentials": "Nem állnak rendelkezésre mentett hitelesítő adatok az újbóli bejelentkezéshez",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Szolgáltatások",
"hubSubtitle": "Megtekintési haladás szinkronizálása és új tartalmak igénylése.",
"integrations": "Integrációk",
"notConnected": "Nincs csatlakoztatva",
"connectedAs": "@${username} néven csatlakoztatva",
"scrobble": "Haladás automatikus követése",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Usa il browser",
"or": "o",
"connectToMediaBrowser": "Connettiti a ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Usa Quick Connect",
"quickConnectInstructions": "Apri Quick Connect in Jellyfin e inserisci questo codice.",
"quickConnectWaiting": "In attesa di approvazione…",
@@ -53,6 +54,7 @@
"mute": "Disattiva audio",
"ok": "OK",
"off": "Disattivato",
"options": "Opzioni",
"seasonNumber": "Stagione ${number}",
"episodeNumberTitle": "Episodio ${number} - ${title}",
"chapterNumber": "Capitolo ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Scala di visualizzazione",
"compact": "Compatta",
"comfortable": "Comoda",
"gridSpacing": "Spaziatura della griglia",
"gridSpacingTight": "Ristretta",
"gridSpacingNormal": "Normale",
"gridSpacingSpacious": "Ampia",
"tvCornerSpotlightBackdrop": "Sfondo in evidenza nell'angolo",
"tvCornerSpotlightBackdropDescription": "Mostra l'immagine in evidenza nell'angolo in alto a destra anziché a schermo intero",
"viewMode": "Modalità di visualizzazione",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Decodifica hardware",
"hardwareDecodingDescription": "Utilizza l'accelerazione hardware quando disponibile",
"bufferSize": "Dimensione buffer",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automatica (consigliata)",
"bufferSizeWarning": "${heap}MB di memoria disponibile. Un buffer di ${size}MB può influire sulla riproduzione.",
"playbackBuffer": "Buffer di riproduzione",
"playbackBufferAuto": "Auto (consigliato)",
"playbackBufferLarge": "Grande",
"playbackBufferExtraLarge": "Extra grande",
"playbackBufferDescription": "Bufferizza di più contro connessioni instabili. Limitato anche dalla dimensione del buffer.",
"defaultQualityTitle": "Qualità predefinita",
"cellularQualityTitle": "Qualità predefinita sulla rete mobile",
"cellularQualitySameAsDefault": "Come la qualità predefinita",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Qualità musicale",
"subtitleStyling": "Stile sottotitoli",
"subtitleStylingDescription": "Personalizza l'aspetto dei sottotitoli",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Ricorda le scelte di audio e sottotitoli per ogni titolo",
"followServerTrackSelections": "Usa le tracce selezionate sul server per ogni episodio",
"followServerTrackSelectionsDescription": "Al cambio di episodio applica l'audio e i sottotitoli selezionati sul server invece di mantenere la scelta corrente",
"resumeMusicOnLaunch": "Ricorda la sessione musicale",
"resumeMusicOnLaunchDescription": "All'avvio dell'app riapri l'ultimo brano in pausa dal punto in cui era rimasto",
"showChapterMarkersOnTimeline": "Mostra i marcatori dei capitoli sulla barra di avanzamento",
"showChapterMarkersOnTimelineDescription": "Segmenta la barra di avanzamento ai confini dei capitoli",
"specialsOrdering": "Speciali nell'ordine degli episodi",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Scorciatoia già assegnata a ${action}",
"shortcutUpdated": "Scorciatoia aggiornata per ${action}",
"saveFailed": "Impossibile salvare le modifiche. Riprova.",
"autoSkip": "Salto automatico",
"autoPlayAndSkip": "Riproduzione automatica e salti",
"autoPlayNextEpisode": "Riproduci automaticamente l'episodio successivo",
"autoPlayNextEpisodeDescription": "Avvia automaticamente l'episodio successivo quando termina quello in riproduzione",
"playNextCountdown": "Conto alla rovescia del successivo",
"playNextCountdownImmediate": "Riproduci subito",
"autoSkipIntro": "Salta automaticamente la sigla iniziale",
"autoSkipIntroDescription": "Salta automaticamente i marcatori della sigla iniziale dopo alcuni secondi",
"autoSkipCredits": "Salta automaticamente i titoli di coda",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Forza il DV7 nativo e impedisce nuovi tentativi di conversione DV",
"dvConversionDv81Description": "Forza la conversione RPU diretta al profilo Dolby Vision 8.1",
"dvConversionHevcStripDescription": "Rimuove i livelli RPU/EL di Dolby Vision e riproduce il video come semplice HEVC",
"demuxer": "Demuxer del contenitore",
"demuxerDescription": "Quale demuxer analizza i file riprodotti in diretta. Passa a media3 se un file dà problemi.",
"demuxerFfmpeg": "FFmpeg (consigliato)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacciamento",
"deinterlaceDescription": "Rimuove gli artefatti a pettine dai video interlacciati (solo lettore mpv)",
"requireProfileSelectionOnOpen": "Chiedi di scegliere il profilo all'apertura",
"requireProfileSelectionOnOpenDescription": "Mostra la selezione del profilo ogni volta che l'app viene aperta",
"forceTvMode": "Forza modalità TV",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Visualizza la scheda Esplora con contenuti di Esplora di Plex e dei tracker collegati",
"liveTvDefaultFavorites": "Apri sui canali preferiti",
"liveTvDefaultFavoritesDescription": "Mostra solo i canali preferiti quando apri la TV in diretta",
"general": "Generali",
"generalDescription": "Lingua, avvio e comportamento della finestra",
"languageAndRegion": "Lingua e regione",
"startup": "Avvio",
"display": "Schermo",
"libraryAndCards": "Libreria e schede",
"homeScreen": "Schermata iniziale",
"navigation": "Navigazione",
"window": "Finestra",
"content": "Contenuti",
"liveTv": "TV in diretta",
"player": "Lettore",
"subtitlesAndConfig": "Sottotitoli e impostazioni",
"videoAndDisplay": "Video e schermo",
"audio": "Audio",
"quality": "Qualità",
"subtitles": "Sottotitoli",
"seekAndTiming": "Avanzamento e tempi",
"behavior": "Comportamento",
"gestures": "Gesti",
"gestureBrightnessSwipe": "Scorrimento per la luminosità",
"gestureBrightnessSwipeDescription": "Scorri verso l'alto o il basso sul bordo sinistro per regolare la luminosità",
"gestureVolumeSwipe": "Scorrimento per il volume",
"gestureVolumeSwipeDescription": "Scorri verso l'alto o il basso sul bordo destro per regolare il volume",
"gesturePinchToZoom": "Pizzica per lo zoom",
"gesturePinchToZoomDescription": "Pizzica il video per ingrandire o ridurre",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Controlli",
"rememberPlayerChanges": "Ricorda le modifiche del lettore",
"rememberPlayerChangesDescription": "Dove viene salvata e riapplicata una modifica effettuata durante la riproduzione",
"scopePlaybackSpeed": "Velocità di riproduzione",
@@ -466,7 +494,7 @@
"interlaced": "Interlacciato",
"anamorphic": "Anamorfico",
"referenceFrames": "Frame di riferimento",
"dynamicRange": "Dynamic Range",
"dynamicRange": "Gamma dinamica",
"dolbyVision": "Dolby Vision",
"dolbyVisionLevel": "Dolby Vision Level",
"dolbyVisionVersion": "Dolby Vision Version",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Modifica connessione ${product}",
"editMediaBrowserIntro": "Aggiungi o rimuovi gli URL per ${serverName}. Plezy userà l'URL raggiungibile con la latenza più bassa."
},
"accountPreferences": {
"sectionTitle": "Preferenze dell'account",
"hubSubtitleSingle": "Opzioni di audio, sottotitoli e libreria salvate su ${account}",
"hubSubtitleMultiple": "Opzioni di audio, sottotitoli e libreria salvate su ${count} account",
"pickAccount": "Ogni account conserva le proprie preferenze. Scegli quale modificare.",
"storedOnAccount": "Queste opzioni vengono salvate sull'account stesso, quindi ogni app che vi accede le usa — inclusa Plezy sugli altri tuoi dispositivi.",
"noAccounts": "Nessun account da configurare",
"noAccountsHint": "Accedi a Plex oppure connetti un server Jellyfin o Emby: le preferenze salvate su quell'account appariranno qui.",
"unavailable": "Impossibile raggiungere questo account",
"loadFailed": "Impossibile caricare queste preferenze",
"noPreference": "Nessuna preferenza",
"notSet": "Non impostato",
"groups": {
"audioAndSubtitles": "Audio e sottotitoli",
"libraryDisplay": "Libreria",
"personalMedia": "Media personali"
},
"preferredAudioLanguage": "Lingua audio preferita",
"autoSelectAudio": "Scegli l'audio in base alla lingua",
"autoSelectAudioDescription": "Se disattivata, viene mantenuta la traccia audio che il file indica come predefinita.",
"preferredSubtitleLanguage": "Lingua dei sottotitoli preferita",
"subtitleMode": "Attiva i sottotitoli",
"subtitleModes": {
"none": "Selezionati manualmente",
"noneDescription": "Non attivare mai i sottotitoli automaticamente.",
"defaultMode": "Segui le flag della traccia",
"defaultModeDescription": "Usa le flag predefinite e forzate salvate su ogni traccia di sottotitoli.",
"always": "Sempre attivi",
"alwaysDescription": "Attiva una traccia di sottotitoli nella lingua preferita quando disponibile.",
"onlyForced": "Solo sottotitoli forzati",
"onlyForcedDescription": "Carica solo le tracce contrassegnate come forzate.",
"smart": "Mostrati con audio straniero",
"smartDescription": "Attiva i sottotitoli solo quando l'audio è in un'altra lingua."
},
"subtitleAccessibility": "Sottotitoli SDH",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Preferisci i sottotitoli non SDH",
"preferSdh": "Preferisci i sottotitoli SDH",
"onlySdh": "Solo sottotitoli SDH",
"onlyNonSdh": "Solo sottotitoli non SDH"
},
"forcedSubtitles": "Sottotitoli forzati",
"forcedSubtitleOptions": {
"preferNonForced": "Preferisci i sottotitoli non forzati",
"preferForced": "Preferisci i sottotitoli forzati",
"onlyForced": "Solo sottotitoli forzati",
"onlyNonForced": "Solo sottotitoli non forzati"
},
"displayMissingEpisodes": "Mostra gli episodi mancanti",
"displayMissingEpisodesDescription": "Elenca gli episodi noti al server ma senza un file associato.",
"hidePlayedInLatest": "Nascondi gli elementi visti da Aggiunti di recente",
"hidePlayedInLatestDescription": "Esclude dagli Aggiunti di recente del server gli elementi che hai già visto.",
"displayCollectionsView": "Mostra la vista Raccolte",
"displayCollectionsViewDescription": "Mostra la vista Raccolte del server insieme alle tue librerie.",
"rewatchingInNextUp": "Mantieni le serie riviste in Prossimi episodi",
"rewatchingInNextUpDescription": "Quando finisci una serie e la ricominci, Prossimi episodi segue la nuova visione invece di rimuovere la serie.",
"watchedIndicator": "Indicatori di visione",
"watchedIndicatorOptions": {
"none": "Mai",
"moviesAndShows": "Film e serie TV",
"movies": "Solo film",
"shows": "Solo serie TV"
},
"mediaReviewsVisibility": "Valutazioni e recensioni",
"mediaReviewsOptions": {
"usersAndCritics": "Utenti e critici",
"usersOnly": "Solo utenti",
"criticsOnly": "Solo critici",
"nobody": "Nascoste"
}
},
"discover": {
"title": "Esplora",
"noContentAvailable": "Nessun contenuto disponibile",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Canale sconosciuto",
"live": "IN DIRETTA",
"reloadGuide": "Ricarica la guida",
"searchGuide": "Cerca nella guida",
"searchHint": "Cerca canali e programmi",
"searchNoResults": "Nessuna corrispondenza per \"${query}\"",
"channelsSection": "Canali",
"programsSection": "Programmi",
"now": "Ora",
"today": "Oggi",
"tomorrow": "Domani",
@@ -1452,6 +1556,8 @@
"repeatAll": "Ripeti tutto",
"repeatOne": "Ripeti il brano",
"instantMixNoServer": "Nessun server disponibile per un mix istantaneo",
"instantMixFailed": "Impossibile caricare il mix istantaneo",
"instantMixEmpty": "Il mix istantaneo non ha prodotto alcun brano",
"noAudioUrl": "Nessun URL audio disponibile per ${track}",
"discography": {
"singlesAndEps": "Singoli ed EP",
@@ -1483,6 +1589,13 @@
"host": "Host",
"hostBadge": "HOST",
"youAreHost": "Sei l'host",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "In visione con altri partecipanti",
"endSession": "Termina la sessione",
"leaveSession": "Abbandona la sessione",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Nella lista di blocco",
"couldNotReach": "Impossibile raggiungere ${url}: ${error}",
"noInstanceAtUrl": "Nessuna istanza Seerr all'indirizzo ${url} (HTTP ${status})",
"invalidUrl": "Inserisci un indirizzo del server come https://seerr.example.com",
"quickConnectUnsupported": "Questa istanza Seerr non supporta Quick Connect. È necessaria la versione 3.4 o successiva di Seerr.",
"notInitialized": "Questa istanza Seerr non ha completato la configurazione iniziale",
"noPlexTokenForReauth": "Nessun token Plex disponibile per effettuare nuovamente l'accesso",
"noStoredCredentials": "Nessuna credenziale salvata disponibile per effettuare nuovamente l'accesso",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Servizi",
"hubSubtitle": "Sincronizza i progressi di visione e richiedi nuovi titoli.",
"integrations": "Integrazioni",
"notConnected": "Non connesso",
"connectedAs": "Connesso come @${username}",
"scrobble": "Registra automaticamente i progressi",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "ブラウザを使用",
"or": "または",
"connectToMediaBrowser": "${product}に接続",
"quickConnect": "Quick Connect",
"useQuickConnect": "Quick Connect を使う",
"quickConnectInstructions": "JellyfinでQuick Connectを開き、このコードを入力してください。",
"quickConnectWaiting": "承認を待っています…",
@@ -53,6 +54,7 @@
"mute": "ミュート",
"ok": "OK",
"off": "オフ",
"options": "オプション",
"seasonNumber": "シーズン${number}",
"episodeNumberTitle": "エピソード${number} - ${title}",
"chapterNumber": "チャプター${number}",
@@ -141,6 +143,10 @@
"displayScale": "表示スケール",
"compact": "コンパクト",
"comfortable": "ゆったり",
"gridSpacing": "グリッド間隔",
"gridSpacingTight": "密",
"gridSpacingNormal": "標準",
"gridSpacingSpacious": "ゆったり",
"tvCornerSpotlightBackdrop": "画面隅の注目コンテンツ背景",
"tvCornerSpotlightBackdropDescription": "画面全体ではなく、右上隅に注目コンテンツのアートワークを表示します",
"viewMode": "表示モード",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "ハードウェアデコード",
"hardwareDecodingDescription": "利用可能な場合にハードウェアアクセラレーションを使用",
"bufferSize": "バッファサイズ",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "自動(推奨)",
"bufferSizeWarning": "${heap}MBのメモリが利用可能です。${size}MBのバッファは再生に影響する可能性があります。",
"playbackBuffer": "再生バッファー",
"playbackBufferAuto": "自動(推奨)",
"playbackBufferLarge": "大",
"playbackBufferExtraLarge": "特大",
"playbackBufferDescription": "不安定な接続に備えてより多くのバッファーを使用します。バッファーサイズによっても制限されます。",
"defaultQualityTitle": "デフォルト画質",
"cellularQualityTitle": "モバイルデータ通信時のデフォルト画質",
"cellularQualitySameAsDefault": "デフォルト画質と同じ",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "音楽の音質",
"subtitleStyling": "字幕スタイル",
"subtitleStylingDescription": "字幕の外観をカスタマイズ",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "タイトルごとに音声と字幕の選択を記憶します",
"followServerTrackSelections": "サーバーのエピソードごとのトラック選択を使用",
"followServerTrackSelectionsDescription": "エピソード切り替え時に、現在の選択を引き継ぐ代わりにサーバーで選択された音声と字幕を適用します",
"resumeMusicOnLaunch": "音楽セッションを記憶",
"resumeMusicOnLaunchDescription": "アプリ起動時に最後の曲を停止位置で一時停止のまま開きます",
"showChapterMarkersOnTimeline": "シークバーにチャプターマーカーを表示",
"showChapterMarkersOnTimelineDescription": "チャプターの境界でシークバーを区切る",
"specialsOrdering": "エピソード順にスペシャルを表示",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "ショートカットは既に${action}に割り当てられています",
"shortcutUpdated": "${action}のショートカットを更新しました",
"saveFailed": "変更を保存できませんでした。もう一度お試しください。",
"autoSkip": "自動スキップ",
"autoPlayAndSkip": "自動再生とスキップ",
"autoPlayNextEpisode": "次のエピソードを自動再生",
"autoPlayNextEpisodeDescription": "エピソードが終了すると、次のエピソードを自動的に再生します",
"playNextCountdown": "次回再生のカウントダウン",
"playNextCountdownImmediate": "すぐに再生",
"autoSkipIntro": "イントロを自動スキップ",
"autoSkipIntroDescription": "数秒後にイントロマーカーを自動的にスキップ",
"autoSkipCredits": "クレジットを自動スキップ",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "ネイティブ DV7 を強制し、DV 変換の再試行を抑制します",
"dvConversionDv81Description": "Dolby Vision プロファイル 8.1 へのインライン RPU 変換を強制します",
"dvConversionHevcStripDescription": "Dolby Vision の RPU/EL レイヤーを削除し、通常の HEVC として扱います",
"demuxer": "コンテナデマルチプレクサ",
"demuxerDescription": "ダイレクト再生のファイルを解析するデマルチプレクサ。ファイルの再生に問題がある場合は media3 に切り替えてください。",
"demuxerFfmpeg": "FFmpeg(推奨)",
"demuxerMedia3": "media3",
"deinterlace": "デインターレース",
"deinterlaceDescription": "インターレース映像のくし型ノイズを除去します(mpvプレーヤーのみ)",
"requireProfileSelectionOnOpen": "アプリ起動時にプロフィールを確認",
"requireProfileSelectionOnOpenDescription": "アプリを開くたびにプロフィール選択を表示",
"forceTvMode": "TVモードを強制",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Plex Discoverと接続済みのトラッカーのコンテンツを「探す」タブに表示します",
"liveTvDefaultFavorites": "お気に入りチャンネルをデフォルトに",
"liveTvDefaultFavoritesDescription": "ライブTVを開いたときにお気に入りチャンネルのみ表示",
"general": "一般",
"generalDescription": "言語、起動、ウィンドウの動作",
"languageAndRegion": "言語と地域",
"startup": "起動",
"display": "ディスプレイ",
"libraryAndCards": "ライブラリとカード",
"homeScreen": "ホーム画面",
"navigation": "ナビゲーション",
"window": "ウィンドウ",
"content": "コンテンツ",
"liveTv": "ライブTV",
"player": "プレーヤー",
"subtitlesAndConfig": "字幕と設定",
"videoAndDisplay": "映像とディスプレイ",
"audio": "音声",
"quality": "画質",
"subtitles": "字幕",
"seekAndTiming": "シークとタイミング",
"behavior": "動作",
"gestures": "ジェスチャー",
"gestureBrightnessSwipe": "明るさスワイプ",
"gestureBrightnessSwipeDescription": "左端を上下にスワイプして明るさを調整します",
"gestureVolumeSwipe": "音量スワイプ",
"gestureVolumeSwipeDescription": "右端を上下にスワイプして音量を調整します",
"gesturePinchToZoom": "ピンチでズーム",
"gesturePinchToZoomDescription": "動画をピンチしてズームイン・アウト",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "コントロール",
"rememberPlayerChanges": "プレーヤーの変更を記憶",
"rememberPlayerChangesDescription": "再生中に行った変更を保存し、再適用する場所",
"scopePlaybackSpeed": "再生速度",
@@ -904,6 +932,77 @@
"editMediaBrowserTitle": "${product}接続を編集",
"editMediaBrowserIntro": "${serverName}のURLを追加または削除します。Plezyは到達可能なURLのうち、レイテンシーが最も低いものを使用します。"
},
"accountPreferences": {
"sectionTitle": "アカウント設定",
"hubSubtitleSingle": "音声・字幕・ライブラリのオプションは${account}に保存されています",
"hubSubtitleMultiple": "音声・字幕・ライブラリのオプションは${count}個のアカウントに保存されています",
"pickAccount": "各アカウントに独自の設定が保存されています。編集するアカウントを選択してください。",
"storedOnAccount": "これらのオプションはアカウント自体に保存されるため、サインインしているすべてのアプリ(他のデバイスのPlezyを含む)で使用されます。",
"noAccounts": "設定できるアカウントがありません",
"noAccountsHint": "Plexにサインインするか、JellyfinまたはEmbyサーバーに接続すると、そのアカウントに保存された設定がここに表示されます。",
"unavailable": "このアカウントにアクセスできません",
"loadFailed": "この設定を読み込めませんでした",
"noPreference": "指定なし",
"notSet": "未設定",
"groups": {
"audioAndSubtitles": "音声と字幕",
"libraryDisplay": "ライブラリ",
"personalMedia": "個人メディア"
},
"preferredAudioLanguage": "優先音声言語",
"autoSelectAudio": "言語で音声を選択",
"autoSelectAudioDescription": "オフにすると、ファイルでデフォルトとマークされた音声トラックがそのまま使われます。",
"preferredSubtitleLanguage": "優先字幕言語",
"subtitleMode": "字幕の自動表示",
"subtitleModes": {
"none": "手動選択",
"noneDescription": "自動では字幕をオンにしません。",
"defaultMode": "トラックのフラグに従う",
"defaultModeDescription": "各字幕トラックに保存されたデフォルトと強制のフラグを使用します。",
"always": "常に有効",
"alwaysDescription": "優先言語の字幕トラックがあれば、常にオンにします。",
"onlyForced": "強制字幕のみ",
"onlyForcedDescription": "強制とマークされたトラックのみ読み込みます。",
"smart": "外国語音声時に表示",
"smartDescription": "音声が別の言語の場合にのみ字幕をオンにします。"
},
"subtitleAccessibility": "SDH字幕",
"subtitleAccessibilityOptions": {
"preferNonSdh": "非SDH字幕を優先",
"preferSdh": "SDH字幕を優先",
"onlySdh": "SDH字幕のみ",
"onlyNonSdh": "非SDH字幕のみ"
},
"forcedSubtitles": "強制字幕",
"forcedSubtitleOptions": {
"preferNonForced": "非強制字幕を優先",
"preferForced": "強制字幕を優先",
"onlyForced": "強制字幕のみ",
"onlyNonForced": "非強制字幕のみ"
},
"displayMissingEpisodes": "欠落エピソードを表示",
"displayMissingEpisodesDescription": "サーバーが把握しているがファイルがないエピソードを一覧表示します。",
"hidePlayedInLatest": "「最新」の視聴済みアイテムを非表示",
"hidePlayedInLatestDescription": "すでに視聴したアイテムは、サーバーの「最新」に表示されません。",
"displayCollectionsView": "コレクションビューを表示",
"displayCollectionsViewDescription": "ライブラリに加えて、サーバーのコレクションビューを表示します。",
"rewatchingInNextUp": "再視聴した番組を「次のエピソード」に残す",
"rewatchingInNextUpDescription": "番組を見終えた後に再び見始めると、「次のエピソード」は番組を外さずに再視聴に追従します。",
"watchedIndicator": "視聴済みマーク",
"watchedIndicatorOptions": {
"none": "しない",
"moviesAndShows": "映画とテレビ番組",
"movies": "映画のみ",
"shows": "テレビ番組のみ"
},
"mediaReviewsVisibility": "評価とレビュー",
"mediaReviewsOptions": {
"usersAndCritics": "ユーザーと批評家",
"usersOnly": "ユーザーのみ",
"criticsOnly": "批評家のみ",
"nobody": "非表示"
}
},
"discover": {
"title": "探す",
"noContentAvailable": "コンテンツがありません",
@@ -1295,6 +1394,11 @@
"unknownChannel": "不明なチャンネル",
"live": "ライブ",
"reloadGuide": "番組表を再読み込み",
"searchGuide": "番組表を検索",
"searchHint": "チャンネル・番組を検索",
"searchNoResults": "「${query}」に一致するものがありません",
"channelsSection": "チャンネル",
"programsSection": "番組",
"now": "現在",
"today": "今日",
"tomorrow": "明日",
@@ -1442,6 +1546,8 @@
"repeatAll": "全曲リピート",
"repeatOne": "1曲リピート",
"instantMixNoServer": "インスタントミックスに利用できるサーバーがありません",
"instantMixFailed": "インスタントミックスを読み込めませんでした",
"instantMixEmpty": "インスタントミックスにトラックがありませんでした",
"noAudioUrl": "${track}で利用可能な音声URLがありません",
"discography": {
"singlesAndEps": "シングル・EP",
@@ -1473,6 +1579,13 @@
"host": "ホスト",
"hostBadge": "ホスト",
"youAreHost": "あなたはホストです",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "他の人と視聴中",
"endSession": "セッションを終了",
"leaveSession": "セッションを退出",
@@ -1962,6 +2075,8 @@
"statusBlocklisted": "ブロックリスト登録済み",
"couldNotReach": "${url}に接続できませんでした: ${error}",
"noInstanceAtUrl": "${url}にSeerrインスタンスがありません(HTTP ${status})",
"invalidUrl": "https://seerr.example.comのようなサーバーアドレスを入力してください",
"quickConnectUnsupported": "このSeerrインスタンスはQuick Connectに対応していません。Seerr 3.4以降が必要です。",
"notInitialized": "このSeerrインスタンスでは初回セットアップが完了していません",
"noPlexTokenForReauth": "再サインインに使用できるPlexトークンがありません",
"noStoredCredentials": "再サインインに使用できる保存済みの認証情報がありません",
@@ -1974,6 +2089,7 @@
"services": {
"title": "サービス",
"hubSubtitle": "視聴の進捗を同期して、新しいタイトルをリクエストします。",
"integrations": "インテグレーション",
"notConnected": "未接続",
"connectedAs": "@${username} として接続済み",
"scrobble": "進捗を自動で記録",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Браузерді пайдалану",
"or": "немесе",
"connectToMediaBrowser": "${product} қосылу",
"quickConnect": "Жылдам қосылу",
"useQuickConnect": "Жылдам қосылуды пайдалану",
"quickConnectInstructions": "Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.",
"quickConnectWaiting": "Растау күтілуде…",
@@ -53,6 +54,7 @@
"mute": "Дыбысты өшіру",
"ok": "Түсінікті",
"off": "Өшірулі",
"options": "Параметрлер",
"seasonNumber": "${number}-маусым",
"episodeNumberTitle": "${number}-бөлім - ${title}",
"chapterNumber": "${number}-бөлім",
@@ -141,6 +143,10 @@
"displayScale": "Дисплей масштабы",
"compact": "Тығыз",
"comfortable": "Ыңғайлы",
"gridSpacing": "Тор аралығы",
"gridSpacingTight": "Тығыз",
"gridSpacingNormal": "Қалыпты",
"gridSpacingSpacious": "Кең",
"tvCornerSpotlightBackdrop": "Бұрыштық жарық фоны",
"tvCornerSpotlightBackdropDescription": "Фонды толық экран орнына жоғарғы оң жақ бұрышта көрсету",
"viewMode": "Көрініс режимі",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Аппараттық декодтау",
"hardwareDecodingDescription": "Мүмкіндігінше аппараттық үдетуді пайдалану",
"bufferSize": "Буфер көлемі",
"bufferSizeMB": "${size} МБ",
"bufferSizeAuto": "Автоматты (Ұсынылатын)",
"bufferSizeWarning": "${heap} МБ ЖҰД қолжетімді. ${size} МБ буфер ойнатуға әсер етуі мүмкін.",
"playbackBuffer": "Ойнату буфері",
"playbackBufferAuto": "Авто (ұсынылады)",
"playbackBufferLarge": "Үлкен",
"playbackBufferExtraLarge": "Өте үлкен",
"playbackBufferDescription": "Тұрақсыз қосылыстарға қарсы көбірек буферлейді. Буфер өлшемімен де шектеледі.",
"defaultQualityTitle": "Әдепкі сапа",
"cellularQualityTitle": "Мобильді желідегі әдепкі сапа",
"cellularQualitySameAsDefault": "Әдепкі сапамен бірдей",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Музыка сапасы",
"subtitleStyling": "Субтитр баптаулары",
"subtitleStylingDescription": "Субтитрлердің сыртқы келбетін теңшеу",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Әр медиа үшін дыбыс пен субтитр таңдауын сақтау",
"followServerTrackSelections": "Әр бөлім үшін сервердегі жолдар таңдауын қолдану",
"followServerTrackSelectionsDescription": "Бөлім ауысқанда ағымдағы таңдауды көшірудің орнына серверде таңдалған дыбыс пен субтитрлерді қолдану",
"resumeMusicOnLaunch": "Музыка сессиясын есте сақтау",
"resumeMusicOnLaunchDescription": "Қолданба ашылғанда соңғы әнді тоқтаған жерінен кідіртілген күйде ашу",
"showChapterMarkersOnTimeline": "Уақыт шкаласында бөлім белгілерін көрсету",
"showChapterMarkersOnTimelineDescription": "Уақыт шкаласын бөлімдерге бөлу",
"specialsOrdering": "Арнайы бөлімдер эпизодтар ретімен",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Бұл тіркес ${action} әрекетіне тағайындалған",
"shortcutUpdated": "${action} үшін пернелер тіркесі жаңартылды",
"saveFailed": "Өзгерістер сақталмады. Қайтадан байқап көріңіз.",
"autoSkip": "Автоматты өткізу",
"autoPlayAndSkip": "Автоойнату және өткізу",
"autoPlayNextEpisode": "Келесі бөлімді автоматты ойнату",
"autoPlayNextEpisodeDescription": "Бір бөлім аяқталғанда келесі бөлімді автоматты түрде бастау",
"playNextCountdown": "Келесіні ойнату санағы",
"playNextCountdownImmediate": "Дереу ойнату",
"autoSkipIntro": "Киріс бөлімді (Intro) автоматты өткізу",
"autoSkipIntroDescription": "Бірнеше секундтан кейін киріс белгілерін автоматты өткізу",
"autoSkipCredits": "Титрлерді автоматты өткізу",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Ішкі DV7 режимін мәжбүрлеу",
"dvConversionDv81Description": "Dolby Vision profile 8.1 форматына түрлендіру",
"dvConversionHevcStripDescription": "Dolby Vision қабаттарын алып тастап, HEVC ретінде көрсету",
"demuxer": "Контейнер демультиплексоры",
"demuxerDescription": "Тікелей ойнатылатын файлдарды қай демультиплексор талдайды. Файл дұрыс ойнатылмаса, media3-ке ауысыңыз.",
"demuxerFfmpeg": "FFmpeg (ұсынылады)",
"demuxerMedia3": "media3",
"deinterlace": "Деинтерлейсинг",
"deinterlaceDescription": "Жоларалық видеодағы тарақ тәрізді артефактілерді жою (тек mpv ойнатқышында)",
"requireProfileSelectionOnOpen": "Ашқанда профильді сұрау",
"requireProfileSelectionOnOpenDescription": "Қолданба ашылған сайын профильді таңдауды көрсету",
"forceTvMode": "TV режимін мәжбүрлеу",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Plex шолуынан және қосылған трекерлерден алынған контенті бар «Шолу» қойындысын көрсету",
"liveTvDefaultFavorites": "Тікелей TV-де таңдаулы арналарды әдепкі ету",
"liveTvDefaultFavoritesDescription": "Тікелей TV ашылғанда тек таңдаулы арналарды көрсету",
"general": "Жалпы",
"generalDescription": "Тіл, іске қосу және терезе мінез-құлқы",
"languageAndRegion": "Тіл және аймақ",
"startup": "Іске қосу",
"display": "Дисплей",
"libraryAndCards": "Кітапхана және карточкалар",
"homeScreen": "Басты экран",
"navigation": "Навигация",
"window": "Терезе",
"content": "Мазмұн",
"liveTv": "Тікелей TV",
"player": "Ойнатқыш",
"subtitlesAndConfig": "Субтитрлер мен конфигурация",
"videoAndDisplay": "Видео және дисплей",
"audio": "Аудио",
"quality": "Сапа",
"subtitles": "Субтитрлер",
"seekAndTiming": "Айналдыру және уақытты реттеу",
"behavior": "Мінез-құлық",
"gestures": "Ым-ишаралар",
"gestureBrightnessSwipe": "Жарықтық сырғытуы",
"gestureBrightnessSwipeDescription": "Сол жақ шетінде жоғары немесе төмен сырғытып, жарықтықты реттеу",
"gestureVolumeSwipe": "Дыбыс сырғытуы",
"gestureVolumeSwipeDescription": "Оң жақ шетінде жоғары немесе төмен сырғытып, дыбыс деңгейін реттеу",
"gesturePinchToZoom": "Шымшу арқылы масштабтау",
"gesturePinchToZoomDescription": "Видеода шымшып, жақындату немесе алыстату",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Басқару элементтері",
"rememberPlayerChanges": "Ойнатқыш өзгерістерін есте сақтау",
"rememberPlayerChangesDescription": "Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын",
"scopePlaybackSpeed": "Ойнату жылдамдығы",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "${product} қосылымын өңдеу",
"editMediaBrowserIntro": "${serverName} үшін URL мекенжайларды қосыңыз немесе өшіріңіз. Plezy ең төмен кідірісі бар қолжетімді URL мекенжайды пайдаланады."
},
"accountPreferences": {
"sectionTitle": "Тіркелгі баптаулары",
"hubSubtitleSingle": "${account} тіркелгісінде сақталған аудио, субтитр және кітапхана баптаулары",
"hubSubtitleMultiple": "${count} тіркелгіде сақталған аудио, субтитр және кітапхана баптаулары",
"pickAccount": "Әр тіркелгі өз баптауларын сақтайды. Өңдеу үшін біреуін таңдаңыз.",
"storedOnAccount": "Бұл баптаулар тіркелгінің өзінде сақталады, сондықтан оған кірген әрбір қолданба оларды пайдаланады — басқа құрылғыларыңыздағы Plezy қоса.",
"noAccounts": "Баптауға тіркелгілер жоқ",
"noAccountsHint": "Plex-ке кіріңіз немесе Jellyfin не Emby серверін қосыңыз, сонда сол тіркелгіде сақталған баптаулар осында көрсетіледі.",
"unavailable": "Бұл тіркелгіге қол жеткізу мүмкін емес",
"loadFailed": "Бұл баптауларды жүктеу мүмкін болмады",
"noPreference": "Таңдау жоқ",
"notSet": "Орнатылмаған",
"groups": {
"audioAndSubtitles": "Аудио және субтитрлер",
"libraryDisplay": "Кітапхана",
"personalMedia": "Жеке медиа"
},
"preferredAudioLanguage": "Қалаулы аудио тілі",
"autoSelectAudio": "Аудионы тілі бойынша таңдау",
"autoSelectAudioDescription": "Өшірулі күйде файл әдепкі деп белгілеген аудио трек қолданылады.",
"preferredSubtitleLanguage": "Қалаулы субтитр тілі",
"subtitleMode": "Субтитрлерді қосу",
"subtitleModes": {
"none": "Қолмен таңдалған",
"noneDescription": "Субтитрлерді өзінен-өзі ешқашан қоспау.",
"defaultMode": "Трек белгілерін қолдану",
"defaultModeDescription": "Әр субтитр трегінде сақталған әдепкі және мәжбүрлі белгілерді пайдалану.",
"always": "Әрдайым қосылған",
"alwaysDescription": "Қалаулы тілдегі субтитр трегі бар болса, оны қосу.",
"onlyForced": "Тек мәжбүрлі субтитрлер",
"onlyForcedDescription": "Тек мәжбүрлі деп белгіленген тректерді жүктеу.",
"smart": "Шет тіліндегі аудио кезінде көрсету",
"smartDescription": "Субтитрлерді тек аудио басқа тілде болғанда қосу."
},
"subtitleAccessibility": "SDH субтитрлері",
"subtitleAccessibilityOptions": {
"preferNonSdh": "SDH емес субтитрлерді артық көру",
"preferSdh": "SDH субтитрлерін артық көру",
"onlySdh": "Тек SDH субтитрлері",
"onlyNonSdh": "Тек SDH емес субтитрлер"
},
"forcedSubtitles": "Мәжбүрлі субтитрлер",
"forcedSubtitleOptions": {
"preferNonForced": "Мәжбүрлі емес субтитрлерді артық көру",
"preferForced": "Мәжбүрлі субтитрлерді артық көру",
"onlyForced": "Тек мәжбүрлі субтитрлер",
"onlyNonForced": "Тек мәжбүрлі емес субтитрлер"
},
"displayMissingEpisodes": "Жоқ бөлімдерді көрсету",
"displayMissingEpisodesDescription": "Сервер білетін, бірақ файлы жоқ бөлімдерді тізімдеу.",
"hidePlayedInLatest": "«Latest» айдарында көрілген элементтерді жасыру",
"hidePlayedInLatestDescription": "Сервердің «Latest» қатарларында сіз көрген элементтерді көрсетпеу.",
"displayCollectionsView": "Топтамалар көрінісін көрсету",
"displayCollectionsViewDescription": "Кітапханаларыңызбен қатар сервердің Топтамалар көрінісін ұсыну.",
"rewatchingInNextUp": "Қайта көрілген сериалдарды «Next Up» айдарында қалдыру",
"rewatchingInNextUpDescription": "Сериалды аяқтаған соң оны қайта бастасаңыз, «Next Up» сериалды тастамай, қайта көруді жалғастырады.",
"watchedIndicator": "Көрілген белгілері",
"watchedIndicatorOptions": {
"none": "Ешқашан",
"moviesAndShows": "Фильмдер және сериалдар",
"movies": "Тек фильмдер",
"shows": "Тек сериалдар"
},
"mediaReviewsVisibility": "Бағалар мен пікірлер",
"mediaReviewsOptions": {
"usersAndCritics": "Пайдаланушылар және сыншылар",
"usersOnly": "Тек пайдаланушылар",
"criticsOnly": "Тек сыншылар",
"nobody": "Жасырылған"
}
},
"discover": {
"title": "Шолу",
"noContentAvailable": "Мазмұн қолжетімсіз",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Белгісіз арна",
"live": "ТІКЕЛЕЙ ЭФИР",
"reloadGuide": "Бағдарламаны жаңарту",
"searchGuide": "Телебағдарламадан іздеу",
"searchHint": "Арналар мен бағдарламаларды іздеу",
"searchNoResults": "\"${query}\" бойынша сәйкестік табылмады",
"channelsSection": "Арналар",
"programsSection": "Бағдарламалар",
"now": "Қазір",
"today": "Бүгін",
"tomorrow": "Ертең",
@@ -1452,6 +1556,8 @@
"repeatAll": "Барлығын қайталау",
"repeatOne": "Біреуін қайталау",
"instantMixNoServer": "Жедел микс үшін қолжетімді сервер жоқ",
"instantMixFailed": "Сәттік микс жүктелмеді",
"instantMixEmpty": "Сәттік микс ешбір тректі қамтымады",
"noAudioUrl": "${track} үшін аудио URL-і қолжетімді емес",
"discography": {
"singlesAndEps": "Синглдер және EP",
@@ -1483,6 +1589,13 @@
"host": "Ұйымдастырушы",
"hostBadge": "ҰЙЫМДАСТЫРУШЫ",
"youAreHost": "Сіз ұйымдастырушысыз",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Басқалармен бірге көрілуде",
"endSession": "Сеансты аяқтау",
"leaveSession": "Сеанстан шығу",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Бұғаттау тізімінде",
"couldNotReach": "${url} мекенжайына қосылу мүмкін болмады: ${error}",
"noInstanceAtUrl": "${url} мекенжайында Seerr данасы жоқ (HTTP ${status})",
"invalidUrl": "Сервер мекенжайын енгізіңіз, мысалы: https://seerr.example.com",
"quickConnectUnsupported": "Бұл Seerr данасы Жылдам қосылуды қолдамайды. Оған Seerr 3.4 немесе одан жаңарақ нұсқа қажет.",
"notInitialized": "Бұл Seerr данасының бастапқы баптауы аяқталмаған",
"noPlexTokenForReauth": "Қайта кіру үшін Plex токені қолжетімді емес",
"noStoredCredentials": "Қайта кіру үшін сақталған тіркелгі деректері жоқ",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Қызметтер",
"hubSubtitle": "Көру барысын синхрондаңыз және жаңа мазмұн сұраңыз.",
"integrations": "Интеграциялар",
"notConnected": "Қосылмаған",
"connectedAs": "@${username} ретінде қосылды",
"scrobble": "Барысты автоматты бақылау",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "브라우저 사용",
"or": "또는",
"connectToMediaBrowser": "${product}에 연결",
"quickConnect": "Quick Connect",
"useQuickConnect": "Quick Connect 사용",
"quickConnectInstructions": "Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.",
"quickConnectWaiting": "승인 대기 중…",
@@ -53,6 +54,7 @@
"mute": "음소거",
"ok": "확인",
"off": "꺼짐",
"options": "옵션",
"seasonNumber": "시즌 ${number}",
"episodeNumberTitle": "${number}화 - ${title}",
"chapterNumber": "챕터 ${number}",
@@ -141,6 +143,10 @@
"displayScale": "표시 배율",
"compact": "조밀하게",
"comfortable": "여유롭게",
"gridSpacing": "그리드 간격",
"gridSpacingTight": "조밀하게",
"gridSpacingNormal": "보통",
"gridSpacingSpacious": "여유롭게",
"tvCornerSpotlightBackdrop": "모서리 스포트라이트 배경",
"tvCornerSpotlightBackdropDescription": "화면 전체를 채우는 대신 오른쪽 상단에 스포트라이트 아트워크를 표시합니다",
"viewMode": "보기 모드",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "하드웨어 디코딩",
"hardwareDecodingDescription": "가능한 경우 하드웨어 가속을 사용합니다",
"bufferSize": "버퍼 크기",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "자동 (권장)",
"bufferSizeWarning": "${heap}MB 메모리를 사용할 수 있습니다. ${size}MB 버퍼는 재생에 영향을 줄 수 있습니다.",
"playbackBuffer": "재생 버퍼",
"playbackBufferAuto": "자동 (권장)",
"playbackBufferLarge": "대형",
"playbackBufferExtraLarge": "초대형",
"playbackBufferDescription": "불안정한 연결에 대비해 더 많은 버퍼를 사용합니다. 버퍼 크기에 의해서도 제한됩니다.",
"defaultQualityTitle": "기본 화질",
"cellularQualityTitle": "셀룰러에서 기본 화질",
"cellularQualitySameAsDefault": "기본 화질과 동일",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "음악 음질",
"subtitleStyling": "자막 스타일",
"subtitleStylingDescription": "자막 모양을 사용자 지정합니다",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "작품별 오디오 및 자막 선택 기억",
"followServerTrackSelections": "서버의 에피소드별 트랙 선택 사용",
"followServerTrackSelectionsDescription": "에피소드 전환 시 현재 선택을 유지하는 대신 서버에서 선택된 오디오와 자막을 적용합니다",
"resumeMusicOnLaunch": "음악 세션 기억",
"resumeMusicOnLaunchDescription": "앱 시작 시 마지막 곡을 멈춘 위치에서 일시정지 상태로 엽니다",
"showChapterMarkersOnTimeline": "탐색 막대에 챕터 마커 표시",
"showChapterMarkersOnTimelineDescription": "챕터 경계에서 탐색 막대 구분",
"specialsOrdering": "에피소드 순서로 스페셜 표시",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "단축키가 이미 ${action}에 할당되었습니다",
"shortcutUpdated": "${action} 단축키가 업데이트되었습니다",
"saveFailed": "변경 사항을 저장하지 못했습니다. 다시 시도하세요.",
"autoSkip": "자동 건너뛰기",
"autoPlayAndSkip": "자동 재생 및 건너뛰기",
"autoPlayNextEpisode": "다음 에피소드 자동 재생",
"autoPlayNextEpisodeDescription": "에피소드가 끝나면 다음 에피소드를 자동으로 재생",
"playNextCountdown": "다음 재생 카운트다운",
"playNextCountdownImmediate": "즉시 재생",
"autoSkipIntro": "자동으로 오프닝 건너뛰기",
"autoSkipIntroDescription": "몇 초 후 오프닝을 자동으로 건너뛰기",
"autoSkipCredits": "자동으로 엔딩 건너뛰기",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "네이티브 DV7을 강제하고 DV 변환 재시도를 억제합니다",
"dvConversionDv81Description": "Dolby Vision 프로필 8.1로 인라인 RPU 변환을 강제합니다",
"dvConversionHevcStripDescription": "Dolby Vision RPU/EL 레이어를 제거하고 일반 HEVC로 제공합니다",
"demuxer": "컨테이너 디먹서",
"demuxerDescription": "다이렉트 재생 파일을 분석할 디먹서입니다. 파일이 제대로 재생되지 않으면 media3으로 전환하세요.",
"demuxerFfmpeg": "FFmpeg (권장)",
"demuxerMedia3": "media3",
"deinterlace": "디인터레이싱",
"deinterlaceDescription": "인터레이스 비디오의 빗살 무늬 아티팩트를 제거합니다 (mpv 플레이어만 해당)",
"requireProfileSelectionOnOpen": "앱 실행 시 프로필 선택",
"requireProfileSelectionOnOpenDescription": "앱을 열 때마다 프로필 선택 화면을 표시합니다",
"forceTvMode": "TV 모드 강제 사용",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Plex Discover와 연결된 트래커의 콘텐츠가 포함된 둘러보기 탭을 표시합니다",
"liveTvDefaultFavorites": "즐겨찾기 채널 기본 설정",
"liveTvDefaultFavoritesDescription": "라이브 TV를 열 때 즐겨찾기 채널만 표시",
"general": "일반",
"generalDescription": "언어, 시작 및 창 동작",
"languageAndRegion": "언어 및 지역",
"startup": "시작",
"display": "디스플레이",
"libraryAndCards": "라이브러리 및 카드",
"homeScreen": "홈 화면",
"navigation": "탐색",
"window": "창",
"content": "콘텐츠",
"liveTv": "라이브 TV",
"player": "플레이어",
"subtitlesAndConfig": "자막 및 구성",
"videoAndDisplay": "비디오 및 디스플레이",
"audio": "오디오",
"quality": "화질",
"subtitles": "자막",
"seekAndTiming": "탐색 및 타이밍",
"behavior": "동작",
"gestures": "제스처",
"gestureBrightnessSwipe": "밝기 스와이프",
"gestureBrightnessSwipeDescription": "왼쪽 가장자리에서 위아래로 스와이프하여 밝기를 조절합니다",
"gestureVolumeSwipe": "볼륨 스와이프",
"gestureVolumeSwipeDescription": "오른쪽 가장자리에서 위아래로 스와이프하여 볼륨을 조절합니다",
"gesturePinchToZoom": "핀치 줌",
"gesturePinchToZoomDescription": "비디오에서 핀치하여 확대하거나 축소합니다",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "컨트롤",
"rememberPlayerChanges": "플레이어 변경 사항 기억",
"rememberPlayerChangesDescription": "재생 중 변경한 사항을 저장하고 다시 적용할 위치",
"scopePlaybackSpeed": "재생 속도",
@@ -904,6 +932,77 @@
"editMediaBrowserTitle": "${product} 연결 편집",
"editMediaBrowserIntro": "${serverName}의 URL을 추가하거나 제거하세요. Plezy는 연결 가능한 URL 중 지연 시간이 가장 낮은 URL을 사용합니다."
},
"accountPreferences": {
"sectionTitle": "계정 기본 설정",
"hubSubtitleSingle": "${account}에 저장된 오디오, 자막 및 라이브러리 옵션",
"hubSubtitleMultiple": "${count}개 계정에 저장된 오디오, 자막 및 라이브러리 옵션",
"pickAccount": "각 계정은 고유한 기본 설정을 저장합니다. 편집할 계정을 선택하세요.",
"storedOnAccount": "이 옵션은 계정 자체에 저장되므로 해당 계정으로 로그인한 모든 앱에서 사용됩니다 — 다른 기기의 Plezy를 포함합니다.",
"noAccounts": "구성할 계정이 없습니다",
"noAccountsHint": "Plex에 로그인하거나 Jellyfin 또는 Emby 서버에 연결하면 해당 계정에 저장된 기본 설정이 여기에 표시됩니다.",
"unavailable": "이 계정에 연결할 수 없습니다",
"loadFailed": "이 기본 설정을 불러올 수 없습니다",
"noPreference": "기본 설정 없음",
"notSet": "설정되지 않음",
"groups": {
"audioAndSubtitles": "오디오 및 자막",
"libraryDisplay": "라이브러리",
"personalMedia": "개인 미디어"
},
"preferredAudioLanguage": "선호 오디오 언어",
"autoSelectAudio": "언어별로 오디오 선택",
"autoSelectAudioDescription": "끄면 파일이 기본으로 표시한 오디오 트랙을 유지합니다.",
"preferredSubtitleLanguage": "선호 자막 언어",
"subtitleMode": "자막 자동 켜기",
"subtitleModes": {
"none": "수동 선택",
"noneDescription": "자동으로 자막을 켜지 않습니다.",
"defaultMode": "트랙 플래그 따르기",
"defaultModeDescription": "각 자막 트랙에 저장된 기본 및 강제 플래그를 사용합니다.",
"always": "항상 활성화",
"alwaysDescription": "선호 언어의 자막 트랙이 있으면 항상 켭니다.",
"onlyForced": "강제 자막만",
"onlyForcedDescription": "강제로 표시된 트랙만 로드합니다.",
"smart": "외국어 오디오 시 표시",
"smartDescription": "오디오가 다른 언어일 때만 자막을 켭니다."
},
"subtitleAccessibility": "SDH 자막",
"subtitleAccessibilityOptions": {
"preferNonSdh": "SDH가 아닌 자막 선호",
"preferSdh": "SDH 자막 선호",
"onlySdh": "SDH 자막만",
"onlyNonSdh": "SDH가 아닌 자막만"
},
"forcedSubtitles": "강제 자막",
"forcedSubtitleOptions": {
"preferNonForced": "강제가 아닌 자막 선호",
"preferForced": "강제 자막 선호",
"onlyForced": "강제 자막만",
"onlyNonForced": "강제가 아닌 자막만"
},
"displayMissingEpisodes": "누락된 에피소드 표시",
"displayMissingEpisodesDescription": "서버가 알고 있지만 파일이 없는 에피소드를 나열합니다.",
"hidePlayedInLatest": "최신 항목에서 시청한 항목 숨기기",
"hidePlayedInLatestDescription": "이미 시청한 항목은 서버의 최신 행에서 제외합니다.",
"displayCollectionsView": "컬렉션 보기 표시",
"displayCollectionsViewDescription": "라이브러리와 함께 서버의 컬렉션 보기를 제공합니다.",
"rewatchingInNextUp": "재시청한 프로그램을 다음 에피소드에 유지",
"rewatchingInNextUpDescription": "프로그램을 끝까지 본 뒤 다시 시작하면, 목록에서 제거하는 대신 다음 에피소드가 재시청을 따라갑니다.",
"watchedIndicator": "시청 표시",
"watchedIndicatorOptions": {
"none": "안 함",
"moviesAndShows": "영화 및 TV 프로그램",
"movies": "영화만",
"shows": "TV 프로그램만"
},
"mediaReviewsVisibility": "평점 및 리뷰",
"mediaReviewsOptions": {
"usersAndCritics": "사용자 및 평론가",
"usersOnly": "사용자만",
"criticsOnly": "평론가만",
"nobody": "숨김"
}
},
"discover": {
"title": "둘러보기",
"noContentAvailable": "사용 가능한 콘텐츠가 없습니다",
@@ -1295,6 +1394,11 @@
"unknownChannel": "알 수 없는 채널",
"live": "실시간",
"reloadGuide": "편성표 새로고침",
"searchGuide": "편성표 검색",
"searchHint": "채널 및 프로그램 검색",
"searchNoResults": "「${query}」와(과) 일치하는 항목이 없습니다",
"channelsSection": "채널",
"programsSection": "프로그램",
"now": "지금",
"today": "오늘",
"tomorrow": "내일",
@@ -1442,6 +1546,8 @@
"repeatAll": "전체 반복",
"repeatOne": "한 곡 반복",
"instantMixNoServer": "즉석 믹스에 사용할 수 있는 서버가 없습니다",
"instantMixFailed": "인스턴트 믹스를 불러오지 못했습니다",
"instantMixEmpty": "인스턴트 믹스에 트랙이 없습니다",
"noAudioUrl": "${track}에 사용할 수 있는 오디오 URL이 없습니다",
"discography": {
"singlesAndEps": "싱글 및 EP",
@@ -1473,6 +1579,13 @@
"host": "호스트",
"hostBadge": "호스트",
"youAreHost": "호스트입니다",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "다른 사람과 함께 시청 중",
"endSession": "세션 종료",
"leaveSession": "세션 나가기",
@@ -1962,6 +2075,8 @@
"statusBlocklisted": "차단 목록에 있음",
"couldNotReach": "${url}에 연결할 수 없습니다: ${error}",
"noInstanceAtUrl": "${url}에 Seerr 인스턴스가 없습니다(HTTP ${status})",
"invalidUrl": "https://seerr.example.com과 같은 서버 주소를 입력하세요",
"quickConnectUnsupported": "이 Seerr 인스턴스는 Quick Connect를 지원하지 않습니다. Seerr 3.4 이상이 필요합니다.",
"notInitialized": "이 Seerr 인스턴스는 최초 실행 설정을 완료하지 않았습니다",
"noPlexTokenForReauth": "다시 로그인하는 데 사용할 수 있는 Plex 토큰이 없습니다",
"noStoredCredentials": "다시 로그인하는 데 사용할 수 있는 저장된 인증 정보가 없습니다",
@@ -1974,6 +2089,7 @@
"services": {
"title": "서비스",
"hubSubtitle": "시청 진행률을 동기화하고 새 작품을 요청하세요.",
"integrations": "연동",
"notConnected": "연결되지 않음",
"connectedAs": "@${username}(으)로 연결됨",
"scrobble": "진행률 자동 추적",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Bruk nettleser",
"or": "eller",
"connectToMediaBrowser": "Koble til ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Bruk Quick Connect",
"quickConnectInstructions": "Åpne Quick Connect i Jellyfin og skriv inn denne koden.",
"quickConnectWaiting": "Venter på godkjenning…",
@@ -53,6 +54,7 @@
"mute": "Demp",
"ok": "OK",
"off": "Av",
"options": "Valg",
"seasonNumber": "Sesong ${number}",
"episodeNumberTitle": "Episode ${number} - ${title}",
"chapterNumber": "Kapittel ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Skalering",
"compact": "Kompakt",
"comfortable": "Komfortabel",
"gridSpacing": "Rutenettavstand",
"gridSpacingTight": "Tett",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Rommelig",
"tvCornerSpotlightBackdrop": "Fremhevet bakgrunn i hjørnet",
"tvCornerSpotlightBackdropDescription": "Vis fremhevet grafikk øverst til høyre i stedet for å fylle skjermen",
"viewMode": "Visningsmodus",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Maskinvaredekoding",
"hardwareDecodingDescription": "Bruk maskinvareakselerasjon når tilgjengelig",
"bufferSize": "Bufferstørrelse",
"bufferSizeMB": "${size} MB",
"bufferSizeAuto": "Automatisk (anbefalt)",
"bufferSizeWarning": "${heap} MB minne tilgjengelig. En buffer på ${size} MB kan påvirke avspillingen.",
"playbackBuffer": "Avspillingsbuffer",
"playbackBufferAuto": "Auto (anbefalt)",
"playbackBufferLarge": "Stor",
"playbackBufferExtraLarge": "Ekstra stor",
"playbackBufferDescription": "Bufrer mer mot ustabile tilkoblinger. Begrenses også av bufferstørrelsen.",
"defaultQualityTitle": "Standardkvalitet",
"cellularQualityTitle": "Standardkvalitet på mobilnett",
"cellularQualitySameAsDefault": "Samme som standardkvalitet",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Musikkvalitet",
"subtitleStyling": "Undertekststil",
"subtitleStylingDescription": "Tilpass utseendet på undertekster",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Husk lyd- og undertekstvalg per tittel",
"followServerTrackSelections": "Bruk serverens sporvalg per episode",
"followServerTrackSelectionsDescription": "Ved episodebytte brukes lyden og undertekstene som er valgt på serveren, i stedet for å videreføre gjeldende valg",
"resumeMusicOnLaunch": "Husk musikkøkt",
"resumeMusicOnLaunchDescription": "Åpne den siste sangen på pause der den slapp når appen starter",
"showChapterMarkersOnTimeline": "Vis kapittelmarkører på tidslinjen",
"showChapterMarkersOnTimelineDescription": "Del tidslinjen ved kapittelgrenser",
"specialsOrdering": "Spesialepisoder i episoderekkefølge",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Snarvei allerede tilordnet til ${action}",
"shortcutUpdated": "Snarvei oppdatert for ${action}",
"saveFailed": "Kunne ikke lagre endringene. Prøv igjen.",
"autoSkip": "Automatisk hopp",
"autoPlayAndSkip": "Autospill og hopp",
"autoPlayNextEpisode": "Spill neste episode automatisk",
"autoPlayNextEpisodeDescription": "Start neste episode automatisk når en episode er ferdig",
"playNextCountdown": "Nedtelling for neste episode",
"playNextCountdownImmediate": "Spill umiddelbart",
"autoSkipIntro": "Hopp over intro automatisk",
"autoSkipIntroDescription": "Hopp automatisk over intromarkører etter noen sekunder",
"autoSkipCredits": "Hopp over rulletekst automatisk",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Tving opprinnelig DV7-avspilling og ikke prøv DV-konvertering på nytt",
"dvConversionDv81Description": "Tving direkte RPU-konvertering til Dolby Vision-profil 8.1",
"dvConversionHevcStripDescription": "Fjern Dolby Vision RPU/EL-lag og lever som vanlig HEVC",
"demuxer": "Container-demukser",
"demuxerDescription": "Hvilken demukser som leser direkteavspilte filer. Bytt til media3 hvis en fil oppfører seg rart.",
"demuxerFfmpeg": "FFmpeg (anbefalt)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacing",
"deinterlaceDescription": "Fjern combing-artefakter fra interlaced video (kun mpv-spiller)",
"requireProfileSelectionOnOpen": "Spør om profil ved appåpning",
"requireProfileSelectionOnOpenDescription": "Vis profilvalg hver gang appen åpnes",
"forceTvMode": "Tving TV-modus",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Vis Oppdag-fanen med innhold fra Plex Discover og tilkoblede sporingstjenester",
"liveTvDefaultFavorites": "Vis favorittkanaler som standard",
"liveTvDefaultFavoritesDescription": "Vis bare favorittkanaler når du åpner Direkte-TV",
"general": "Generelt",
"generalDescription": "Språk, oppstart og vindusatferd",
"languageAndRegion": "Språk og region",
"startup": "Oppstart",
"display": "Skjerm",
"libraryAndCards": "Bibliotek og kort",
"homeScreen": "Hjemmeskjerm",
"navigation": "Navigering",
"window": "Vindu",
"content": "Innhold",
"liveTv": "Direkte-TV",
"player": "Spiller",
"subtitlesAndConfig": "Undertekster og konfigurasjon",
"videoAndDisplay": "Video og skjerm",
"audio": "Lyd",
"quality": "Kvalitet",
"subtitles": "Undertekster",
"seekAndTiming": "Spoling og tidsinnstillinger",
"behavior": "Oppførsel",
"gestures": "Bevegelser",
"gestureBrightnessSwipe": "Lysstyrke-sveip",
"gestureBrightnessSwipeDescription": "Sveip opp eller ned på venstre kant for å justere lysstyrken",
"gestureVolumeSwipe": "Volum-sveip",
"gestureVolumeSwipeDescription": "Sveip opp eller ned på høyre kant for å justere volumet",
"gesturePinchToZoom": "Klyp for å zoome",
"gesturePinchToZoomDescription": "Klyp på videoen for å zoome inn eller ut",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Kontroller",
"rememberPlayerChanges": "Husk endringer i spilleren",
"rememberPlayerChangesDescription": "Hvor en endring under avspilling lagres og brukes på nytt",
"scopePlaybackSpeed": "Avspillingshastighet",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Rediger ${product}-tilkobling",
"editMediaBrowserIntro": "Legg til eller fjern URL-er for ${serverName}. Plezy vil bruke den nåbare URL-en med lavest ventetid."
},
"accountPreferences": {
"sectionTitle": "Kontoinnstillinger",
"hubSubtitleSingle": "Lyd-, undertekst- og bibliotekalternativer lagret på ${account}",
"hubSubtitleMultiple": "Lyd-, undertekst- og bibliotekalternativer lagret på ${count} kontoer",
"pickAccount": "Hver konto lagrer sine egne innstillinger. Velg hvilken du vil redigere.",
"storedOnAccount": "Disse alternativene lagres på selve kontoen, så alle apper som er logget på den, bruker dem — inkludert Plezy på de andre enhetene dine.",
"noAccounts": "Ingen kontoer å konfigurere",
"noAccountsHint": "Logg inn på Plex, eller koble til en Jellyfin- eller Emby-server, så vises innstillingene som er lagret på den kontoen, her.",
"unavailable": "Kan ikke nå denne kontoen",
"loadFailed": "Kunne ikke laste inn disse innstillingene",
"noPreference": "Ingen preferanse",
"notSet": "Ikke angitt",
"groups": {
"audioAndSubtitles": "Lyd og undertekster",
"libraryDisplay": "Bibliotek",
"personalMedia": "Personlige medier"
},
"preferredAudioLanguage": "Foretrukket lydspråk",
"autoSelectAudio": "Velg lyd etter språk",
"autoSelectAudioDescription": "Av beholder det lydsporet filen markerer som standard.",
"preferredSubtitleLanguage": "Foretrukket undertekstspråk",
"subtitleMode": "Slå på undertekster",
"subtitleModes": {
"none": "Manuelt valgt",
"noneDescription": "Slå aldri på undertekster av seg selv.",
"defaultMode": "Følg sporflaggene",
"defaultModeDescription": "Bruk standard- og tvunget-flaggene som er lagret på hvert undertekstspor.",
"always": "Alltid aktivert",
"alwaysDescription": "Slå på et undertekstspor på foretrukket språk når det finnes et.",
"onlyForced": "Kun tvungne undertekster",
"onlyForcedDescription": "Last inn bare sporene som er merket som tvungne.",
"smart": "Vist med fremmedspråklig lyd",
"smartDescription": "Slå på undertekster bare når lyden er på et annet språk."
},
"subtitleAccessibility": "SDH-undertekster",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Foretrekk ikke-SDH-undertekster",
"preferSdh": "Foretrekk SDH-undertekster",
"onlySdh": "Kun SDH-undertekster",
"onlyNonSdh": "Kun ikke-SDH-undertekster"
},
"forcedSubtitles": "Tvungne undertekster",
"forcedSubtitleOptions": {
"preferNonForced": "Foretrekk ikke-tvungne undertekster",
"preferForced": "Foretrekk tvungne undertekster",
"onlyForced": "Kun tvungne undertekster",
"onlyNonForced": "Kun ikke-tvungne undertekster"
},
"displayMissingEpisodes": "Vis manglende episoder",
"displayMissingEpisodesDescription": "Vis episoder serveren kjenner til, men som ikke har noen fil.",
"hidePlayedInLatest": "Skjul sette elementer i Siste",
"hidePlayedInLatestDescription": "Hold elementer du allerede har sett utenfor serverens Siste-rader.",
"displayCollectionsView": "Vis samlingsvisningen",
"displayCollectionsViewDescription": "Vis serverens samlingsvisning sammen med bibliotekene dine.",
"rewatchingInNextUp": "Behold serier du ser på nytt i Neste opp",
"rewatchingInNextUpDescription": "Når du er ferdig med en serie og starter den på nytt, følger Neste opp den nye avspillingen i stedet for å fjerne serien.",
"watchedIndicator": "Sett-indikatorer",
"watchedIndicatorOptions": {
"none": "Aldri",
"moviesAndShows": "Filmer og TV-serier",
"movies": "Kun filmer",
"shows": "Kun TV-serier"
},
"mediaReviewsVisibility": "Vurderinger og anmeldelser",
"mediaReviewsOptions": {
"usersAndCritics": "Brukere og kritikere",
"usersOnly": "Kun brukere",
"criticsOnly": "Kun kritikere",
"nobody": "Skjult"
}
},
"discover": {
"title": "Oppdag",
"noContentAvailable": "Ikke noe innhold tilgjengelig",
@@ -1016,7 +1115,7 @@
"genre": "Sjanger",
"year": "År",
"contentRating": "Aldersgrense",
"tag": "Tag",
"tag": "Tagg",
"unwatched": "Usette",
"unplayed": "Ikke avspilt",
"favorites": "Favoritter"
@@ -1304,6 +1403,11 @@
"unknownChannel": "Ukjent kanal",
"live": "DIREKTE",
"reloadGuide": "Last inn programguide på nytt",
"searchGuide": "Søk i programguiden",
"searchHint": "Søk etter kanaler og programmer",
"searchNoResults": "Ingen treff for \"${query}\"",
"channelsSection": "Kanaler",
"programsSection": "Programmer",
"now": "Nå",
"today": "I dag",
"tomorrow": "I morgen",
@@ -1452,6 +1556,8 @@
"repeatAll": "Gjenta alle",
"repeatOne": "Gjenta ett spor",
"instantMixNoServer": "Ingen server er tilgjengelig for en hurtigmiks",
"instantMixFailed": "Kunne ikke laste inn direktemiksen",
"instantMixEmpty": "Direktemiksen ga ingen spor",
"noAudioUrl": "Ingen lyd-URL er tilgjengelig for ${track}",
"discography": {
"singlesAndEps": "Singler og EP-er",
@@ -1483,6 +1589,13 @@
"host": "Vert",
"hostBadge": "VERT",
"youAreHost": "Du er verten",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Ser med andre",
"endSession": "Avslutt økt",
"leaveSession": "Forlat økt",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "På blokkeringslisten",
"couldNotReach": "Kunne ikke nå ${url}: ${error}",
"noInstanceAtUrl": "Ingen Seerr-instans på ${url} (HTTP ${status})",
"invalidUrl": "Skriv inn en serveradresse som https://seerr.example.com",
"quickConnectUnsupported": "Denne Seerr-instansen støtter ikke Quick Connect. Den krever Seerr 3.4 eller nyere.",
"notInitialized": "Denne Seerr-instansen har ikke fullført førstegangsoppsettet",
"noPlexTokenForReauth": "Ingen Plex-token er tilgjengelig for å logge inn på nytt",
"noStoredCredentials": "Ingen lagret påloggingsinformasjon er tilgjengelig for å logge inn på nytt",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Tjenester",
"hubSubtitle": "Synkroniser fremdrift og forespør nye titler.",
"integrations": "Integrasjoner",
"notConnected": "Ikke tilkoblet",
"connectedAs": "Tilkoblet som @${username}",
"scrobble": "Registrer fremdrift automatisk",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Gebruik browser",
"or": "of",
"connectToMediaBrowser": "Verbinding maken met ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Quick Connect gebruiken",
"quickConnectInstructions": "Open Quick Connect in Jellyfin en voer deze code in.",
"quickConnectWaiting": "Wachten op goedkeuring…",
@@ -53,6 +54,7 @@
"mute": "Dempen",
"ok": "OK",
"off": "Uit",
"options": "Opties",
"seasonNumber": "Seizoen ${number}",
"episodeNumberTitle": "Aflevering ${number} - ${title}",
"chapterNumber": "Hoofdstuk ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Weergaveschaal",
"compact": "Compact",
"comfortable": "Comfortabel",
"gridSpacing": "Rasterafstand",
"gridSpacingTight": "Compact",
"gridSpacingNormal": "Normaal",
"gridSpacingSpacious": "Ruim",
"tvCornerSpotlightBackdrop": "Uitgelichte achtergrond in de hoek",
"tvCornerSpotlightBackdropDescription": "Toon de uitgelichte afbeelding rechtsboven in plaats van schermvullend",
"viewMode": "Weergavemodus",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Hardwaredecodering",
"hardwareDecodingDescription": "Gebruik hardwareversnelling indien beschikbaar",
"bufferSize": "Buffergrootte",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automatisch (aanbevolen)",
"bufferSizeWarning": "${heap}MB geheugen beschikbaar. Een buffer van ${size}MB kan afspelen beïnvloeden.",
"playbackBuffer": "Afspeelbuffer",
"playbackBufferAuto": "Auto (aanbevolen)",
"playbackBufferLarge": "Groot",
"playbackBufferExtraLarge": "Extra groot",
"playbackBufferDescription": "Buffert meer tegen onstabiele verbindingen. Ook beperkt door de buffergrootte.",
"defaultQualityTitle": "Standaardkwaliteit",
"cellularQualityTitle": "Standaardkwaliteit bij mobiele data",
"cellularQualitySameAsDefault": "Zelfde als standaardkwaliteit",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Muziekkwaliteit",
"subtitleStyling": "Ondertitelopmaak",
"subtitleStylingDescription": "Pas de weergave van ondertitels aan",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Onthoud audio- en ondertitelkeuzes per titel",
"followServerTrackSelections": "Trackselecties van de server per aflevering gebruiken",
"followServerTrackSelectionsDescription": "Pas bij het wisselen van aflevering de op de server geselecteerde audio en ondertitels toe in plaats van de huidige keuze over te nemen",
"resumeMusicOnLaunch": "Muzieksessie onthouden",
"resumeMusicOnLaunchDescription": "Open bij het starten van de app het laatste nummer gepauzeerd waar het gebleven was",
"showChapterMarkersOnTimeline": "Hoofdstukmarkeringen op tijdlijn tonen",
"showChapterMarkersOnTimelineDescription": "Verdeel de tijdlijn bij hoofdstukgrenzen",
"specialsOrdering": "Specials in afleveringsvolgorde",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Sneltoets al toegewezen aan ${action}",
"shortcutUpdated": "Sneltoets bijgewerkt voor ${action}",
"saveFailed": "Wijzigingen konden niet worden opgeslagen. Probeer het opnieuw.",
"autoSkip": "Automatisch overslaan",
"autoPlayAndSkip": "Automatisch afspelen en overslaan",
"autoPlayNextEpisode": "Volgende aflevering automatisch afspelen",
"autoPlayNextEpisodeDescription": "Start de volgende aflevering automatisch wanneer een aflevering eindigt",
"playNextCountdown": "Aftellen voor volgende aflevering",
"playNextCountdownImmediate": "Direct afspelen",
"autoSkipIntro": "Intro automatisch overslaan",
"autoSkipIntroDescription": "Intromarkeringen na enkele seconden automatisch overslaan",
"autoSkipCredits": "Aftiteling automatisch overslaan",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Dwing native DV7 af en voorkom een nieuwe poging met DV-conversie",
"dvConversionDv81Description": "Dwing directe RPU-conversie naar Dolby Vision-profiel 8.1 af",
"dvConversionHevcStripDescription": "Verwijder Dolby Vision RPU/EL-lagen en bied gewone HEVC aan",
"demuxer": "Container-demuxer",
"demuxerDescription": "Welke demuxer direct afgespeelde bestanden inleest. Schakel over naar media3 als een bestand zich vreemd gedraagt.",
"demuxerFfmpeg": "FFmpeg (aanbevolen)",
"demuxerMedia3": "media3",
"deinterlace": "Deinterlacing",
"deinterlaceDescription": "Verwijder kamartefacten uit interlaced video (alleen mpv-speler)",
"requireProfileSelectionOnOpen": "Vraag om profiel bij openen",
"requireProfileSelectionOnOpenDescription": "Toon profielselectie telkens wanneer de app wordt geopend",
"forceTvMode": "Tv-modus afdwingen",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Toon het tabblad Ontdekken met content uit Plex Discover en gekoppelde trackers",
"liveTvDefaultFavorites": "Standaard favoriete zenders",
"liveTvDefaultFavoritesDescription": "Toon alleen favoriete zenders bij het openen van Live TV",
"general": "Algemeen",
"generalDescription": "Taal, opstart- en venstergedrag",
"languageAndRegion": "Taal en regio",
"startup": "Opstarten",
"display": "Weergave",
"libraryAndCards": "Bibliotheek en kaarten",
"homeScreen": "Startscherm",
"navigation": "Navigatie",
"window": "Venster",
"content": "Inhoud",
"liveTv": "Live-tv",
"player": "Speler",
"subtitlesAndConfig": "Ondertitels en instellingen",
"videoAndDisplay": "Video en weergave",
"audio": "Audio",
"quality": "Kwaliteit",
"subtitles": "Ondertitels",
"seekAndTiming": "Spoelen en timing",
"behavior": "Gedrag",
"gestures": "Gebaren",
"gestureBrightnessSwipe": "Vegen voor helderheid",
"gestureBrightnessSwipeDescription": "Veeg op de linkerrand omhoog of omlaag om de helderheid aan te passen",
"gestureVolumeSwipe": "Vegen voor volume",
"gestureVolumeSwipeDescription": "Veeg op de rechterrand omhoog of omlaag om het volume aan te passen",
"gesturePinchToZoom": "Knijpen om te zoomen",
"gesturePinchToZoomDescription": "Knijp op de video om in of uit te zoomen",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Bediening",
"rememberPlayerChanges": "Spelerwijzigingen onthouden",
"rememberPlayerChangesDescription": "Waar een wijziging tijdens het afspelen wordt opgeslagen en opnieuw toegepast",
"scopePlaybackSpeed": "Afspeelsnelheid",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "${product}-verbinding bewerken",
"editMediaBrowserIntro": "Voeg URL's toe aan ${serverName} of verwijder ze. Plezy gebruikt de bereikbare URL met de laagste latentie."
},
"accountPreferences": {
"sectionTitle": "Accountvoorkeuren",
"hubSubtitleSingle": "Audio-, ondertitel- en bibliotheekopties opgeslagen op ${account}",
"hubSubtitleMultiple": "Audio-, ondertitel- en bibliotheekopties opgeslagen op ${count} accounts",
"pickAccount": "Elk account slaat zijn eigen voorkeuren op. Kies het account dat je wilt bewerken.",
"storedOnAccount": "Deze opties worden op het account zelf opgeslagen, zodat elke app die ermee is aangemeld ze gebruikt — ook Plezy op je andere apparaten.",
"noAccounts": "Geen accounts om te configureren",
"noAccountsHint": "Log in bij Plex of verbind een Jellyfin- of Emby-server en de voorkeuren die op dat account zijn opgeslagen, verschijnen hier.",
"unavailable": "Dit account is niet bereikbaar",
"loadFailed": "Deze voorkeuren konden niet worden geladen",
"noPreference": "Geen voorkeur",
"notSet": "Niet ingesteld",
"groups": {
"audioAndSubtitles": "Audio en ondertitels",
"libraryDisplay": "Bibliotheek",
"personalMedia": "Persoonlijke media"
},
"preferredAudioLanguage": "Voorkeurstaal voor audio",
"autoSelectAudio": "Kies audio op taal",
"autoSelectAudioDescription": "Bij 'Uit' wordt de audiotrack gebruikt die het bestand als standaard markeert.",
"preferredSubtitleLanguage": "Voorkeurstaal voor ondertitels",
"subtitleMode": "Ondertitels inschakelen",
"subtitleModes": {
"none": "Handmatig geselecteerd",
"noneDescription": "Schakel ondertitels nooit automatisch in.",
"defaultMode": "Trackvlaggen volgen",
"defaultModeDescription": "Gebruik de standaard- en geforceerde vlaggen die op elke ondertiteltrack zijn opgeslagen.",
"always": "Altijd ingeschakeld",
"alwaysDescription": "Schakel een ondertiteltrack in de voorkeurstaal in zodra er een beschikbaar is.",
"onlyForced": "Alleen geforceerde ondertitels",
"onlyForcedDescription": "Laad alleen de tracks die als geforceerd zijn gemarkeerd.",
"smart": "Weergeven bij anderstalig geluid",
"smartDescription": "Schakel ondertitels alleen in wanneer de audio in een andere taal is."
},
"subtitleAccessibility": "SDH-ondertitels",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Voorkeur voor niet-SDH-ondertitels",
"preferSdh": "Voorkeur voor SDH-ondertitels",
"onlySdh": "Alleen SDH-ondertitels",
"onlyNonSdh": "Alleen niet-SDH-ondertitels"
},
"forcedSubtitles": "Geforceerde ondertitels",
"forcedSubtitleOptions": {
"preferNonForced": "Voorkeur voor niet-geforceerde ondertitels",
"preferForced": "Voorkeur voor geforceerde ondertitels",
"onlyForced": "Alleen geforceerde ondertitels",
"onlyNonForced": "Alleen niet-geforceerde ondertitels"
},
"displayMissingEpisodes": "Ontbrekende afleveringen tonen",
"displayMissingEpisodesDescription": "Toon afleveringen die de server kent maar waarvoor geen bestand aanwezig is.",
"hidePlayedInLatest": "Bekeken items verbergen in 'Nieuwste'",
"hidePlayedInLatestDescription": "Laat items die je al hebt bekeken buiten de 'Nieuwste'-rijen van de server.",
"displayCollectionsView": "Collecties-weergave tonen",
"displayCollectionsViewDescription": "Bied de collecties-weergave van de server naast je bibliotheken aan.",
"rewatchingInNextUp": "Opnieuw bekeken series in 'Volgende' houden",
"rewatchingInNextUpDescription": "Als je een serie afrondt en opnieuw start, blijft 'Volgende' de herkijk volgen in plaats van de serie te laten vallen.",
"watchedIndicator": "Bekeken-indicatoren",
"watchedIndicatorOptions": {
"none": "Nooit",
"moviesAndShows": "Films en tv-series",
"movies": "Alleen films",
"shows": "Alleen tv-series"
},
"mediaReviewsVisibility": "Beoordelingen en recensies",
"mediaReviewsOptions": {
"usersAndCritics": "Gebruikers en critici",
"usersOnly": "Alleen gebruikers",
"criticsOnly": "Alleen critici",
"nobody": "Verborgen"
}
},
"discover": {
"title": "Ontdekken",
"noContentAvailable": "Geen inhoud beschikbaar",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Onbekend kanaal",
"live": "LIVE",
"reloadGuide": "Gids herladen",
"searchGuide": "Zoeken in gids",
"searchHint": "Zoek zenders en programma's",
"searchNoResults": "Geen overeenkomsten voor \"${query}\"",
"channelsSection": "Zenders",
"programsSection": "Programma's",
"now": "Nu",
"today": "Vandaag",
"tomorrow": "Morgen",
@@ -1452,6 +1556,8 @@
"repeatAll": "Alles herhalen",
"repeatOne": "Eén herhalen",
"instantMixNoServer": "Er is geen server beschikbaar voor een instantmix",
"instantMixFailed": "De instantmix kon niet worden geladen",
"instantMixEmpty": "De instantmix leverde geen nummers op",
"noAudioUrl": "Er is geen audio-URL beschikbaar voor ${track}",
"discography": {
"singlesAndEps": "Singles en EP's",
@@ -1483,6 +1589,13 @@
"host": "Host",
"hostBadge": "HOST",
"youAreHost": "Jij bent de host",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Kijken met anderen",
"endSession": "Sessie beëindigen",
"leaveSession": "Sessie verlaten",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Op de blokkeerlijst",
"couldNotReach": "Kon ${url} niet bereiken: ${error}",
"noInstanceAtUrl": "Geen Seerr-instantie op ${url} (HTTP ${status})",
"invalidUrl": "Voer een serveradres in, zoals https://seerr.example.com",
"quickConnectUnsupported": "Deze Seerr-instantie ondersteunt Quick Connect niet. Hiervoor is Seerr 3.4 of nieuwer nodig.",
"notInitialized": "De eerste configuratie van deze Seerr-instantie is niet voltooid",
"noPlexTokenForReauth": "Er is geen Plex-token beschikbaar om opnieuw in te loggen",
"noStoredCredentials": "Er zijn geen opgeslagen inloggegevens beschikbaar om opnieuw in te loggen",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Diensten",
"hubSubtitle": "Synchroniseer kijkvoortgang en vraag nieuwe titels aan.",
"integrations": "Integraties",
"notConnected": "Niet verbonden",
"connectedAs": "Verbonden als @${username}",
"scrobble": "Voortgang automatisch volgen",
+128 -12
View File
@@ -12,6 +12,7 @@
"useBrowser": "Użyj przeglądarki",
"or": "lub",
"connectToMediaBrowser": "Połącz z ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Użyj Quick Connect",
"quickConnectInstructions": "Otwórz Quick Connect w Jellyfin i wpisz ten kod.",
"quickConnectWaiting": "Oczekiwanie na zatwierdzenie…",
@@ -53,6 +54,7 @@
"mute": "Wycisz",
"ok": "OK",
"off": "Wył.",
"options": "Opcje",
"seasonNumber": "Sezon ${number}",
"episodeNumberTitle": "Odcinek ${number} - ${title}",
"chapterNumber": "Rozdział ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Skalowanie",
"compact": "Kompaktowy",
"comfortable": "Wygodny",
"gridSpacing": "Odstępy siatki",
"gridSpacingTight": "Ciasny",
"gridSpacingNormal": "Normalny",
"gridSpacingSpacious": "Przestronny",
"tvCornerSpotlightBackdrop": "Tło wyróżnionej pozycji w rogu",
"tvCornerSpotlightBackdropDescription": "Wyświetlaj grafikę wyróżnionej pozycji w prawym górnym rogu zamiast na całym ekranie",
"viewMode": "Tryb widoku",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Dekodowanie sprzętowe",
"hardwareDecodingDescription": "Użyj akceleracji sprzętowej, gdy dostępna",
"bufferSize": "Rozmiar bufora",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automatyczny (zalecany)",
"bufferSizeWarning": "Dostępna pamięć: ${heap}MB. Bufor ${size}MB może wpłynąć na odtwarzanie.",
"playbackBuffer": "Bufor odtwarzania",
"playbackBufferAuto": "Auto (zalecane)",
"playbackBufferLarge": "Duży",
"playbackBufferExtraLarge": "Bardzo duży",
"playbackBufferDescription": "Buforuje więcej w przypadku niestabilnych połączeń. Ograniczony również przez rozmiar bufora.",
"defaultQualityTitle": "Domyślna jakość",
"cellularQualityTitle": "Domyślna jakość w sieci komórkowej",
"cellularQualitySameAsDefault": "Taka sama jak domyślna jakość",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Jakość muzyki",
"subtitleStyling": "Styl napisów",
"subtitleStylingDescription": "Dostosuj wygląd napisów",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Zapamiętuj wybór ścieżki dźwiękowej i napisów dla każdego tytułu",
"followServerTrackSelections": "Używaj wyboru ścieżek z serwera dla każdego odcinka",
"followServerTrackSelectionsDescription": "Przy zmianie odcinka stosuj ścieżkę dźwiękową i napisy wybrane na serwerze zamiast przenosić bieżący wybór",
"resumeMusicOnLaunch": "Zapamiętaj sesję muzyczną",
"resumeMusicOnLaunchDescription": "Przy uruchomieniu aplikacji otwieraj ostatni utwór wstrzymany w miejscu, w którym przerwano",
"showChapterMarkersOnTimeline": "Pokaż znaczniki rozdziałów na pasku przewijania",
"showChapterMarkersOnTimelineDescription": "Podziel pasek przewijania na granicach rozdziałów",
"specialsOrdering": "Specjale w kolejności odcinków",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Skrót jest już przypisany do ${action}",
"shortcutUpdated": "Skrót zaktualizowany dla ${action}",
"saveFailed": "Nie udało się zapisać zmian. Spróbuj ponownie.",
"autoSkip": "Automatyczne pomijanie",
"autoPlayAndSkip": "Auto-odtwarzanie i pomijanie",
"autoPlayNextEpisode": "Automatycznie odtwarzaj następny odcinek",
"autoPlayNextEpisodeDescription": "Automatycznie uruchamiaj następny odcinek, gdy bieżący się skończy",
"playNextCountdown": "Odliczanie do następnego odcinka",
"playNextCountdownImmediate": "Odtwarzaj natychmiast",
"autoSkipIntro": "Automatyczne pomijanie intro",
"autoSkipIntroDescription": "Automatycznie pomijaj znaczniki intro po kilku sekundach",
"autoSkipCredits": "Automatyczne pomijanie napisów końcowych",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Wymuś natywne DV7 i wyłącz ponowną próbę konwersji DV",
"dvConversionDv81Description": "Wymuś wbudowaną konwersję RPU do profilu Dolby Vision 8.1",
"dvConversionHevcStripDescription": "Usuń warstwy Dolby Vision RPU/EL i przedstaw zwykłe HEVC",
"demuxer": "Demukser kontenera",
"demuxerDescription": "Który demukser analizuje pliki odtwarzane bezpośrednio. Przełącz na media3, jeśli plik sprawia problemy.",
"demuxerFfmpeg": "FFmpeg (zalecany)",
"demuxerMedia3": "media3",
"deinterlace": "Usuwanie przeplotu",
"deinterlaceDescription": "Usuwa artefakty grzebienia z wideo z przeplotem (tylko odtwarzacz mpv)",
"requireProfileSelectionOnOpen": "Pytaj o profil przy otwarciu aplikacji",
"requireProfileSelectionOnOpenDescription": "Pokaż wybór profilu za każdym razem, gdy aplikacja jest otwierana",
"forceTvMode": "Wymuś tryb TV",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Wyświetlaj kartę Odkryj z treściami z Plex Discover i połączonych serwisów śledzących",
"liveTvDefaultFavorites": "Domyślnie ulubione kanały",
"liveTvDefaultFavoritesDescription": "Pokaż tylko ulubione kanały po otwarciu telewizji na żywo",
"general": "Ogólne",
"generalDescription": "Język, uruchamianie i zachowanie okna",
"languageAndRegion": "Język i region",
"startup": "Uruchamianie",
"display": "Ekran",
"libraryAndCards": "Biblioteka i karty",
"homeScreen": "Ekran główny",
"navigation": "Nawigacja",
"window": "Okno",
"content": "Zawartość",
"liveTv": "TV na żywo",
"player": "Odtwarzacz",
"subtitlesAndConfig": "Napisy i konfiguracja",
"videoAndDisplay": "Wideo i ekran",
"audio": "Audio",
"quality": "Jakość",
"subtitles": "Napisy",
"seekAndTiming": "Przewijanie i czas",
"behavior": "Zachowanie",
"gestures": "Gesty",
"gestureBrightnessSwipe": "Zmiana jasności przesunięciem",
"gestureBrightnessSwipeDescription": "Przesuwaj palcem w górę lub w dół na lewej krawędzi, aby regulować jasność",
"gestureVolumeSwipe": "Zmiana głośności przesunięciem",
"gestureVolumeSwipeDescription": "Przesuwaj palcem w górę lub w dół na prawej krawędzi, aby regulować głośność",
"gesturePinchToZoom": "Powiększanie ściskaniem",
"gesturePinchToZoomDescription": "Ściśnij wideo palcami, aby powiększyć lub pomniejszyć",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Sterowanie",
"rememberPlayerChanges": "Zapamiętuj zmiany odtwarzacza",
"rememberPlayerChangesDescription": "Miejsce zapisywania i ponownego stosowania zmian dokonanych podczas odtwarzania",
"scopePlaybackSpeed": "Prędkość odtwarzania",
@@ -916,6 +944,77 @@
"editMediaBrowserTitle": "Edytuj połączenie z ${product}",
"editMediaBrowserIntro": "Dodaj lub usuń adresy URL dla ${serverName}. Plezy będzie używać osiągalnego adresu URL o najniższym opóźnieniu."
},
"accountPreferences": {
"sectionTitle": "Preferencje konta",
"hubSubtitleSingle": "Opcje audio, napisów i biblioteki zapisane na koncie ${account}",
"hubSubtitleMultiple": "Opcje audio, napisów i biblioteki zapisane na ${count} kontach",
"pickAccount": "Każde konto przechowuje własne preferencje. Wybierz to, które chcesz edytować.",
"storedOnAccount": "Te opcje są zapisywane na samym koncie, więc używa ich każda aplikacja zalogowana na to konto — w tym Plezy na Twoich innych urządzeniach.",
"noAccounts": "Brak kont do skonfigurowania",
"noAccountsHint": "Zaloguj się do Plex albo połącz serwer Jellyfin lub Emby, a preferencje zapisane na tym koncie pojawią się tutaj.",
"unavailable": "Nie można połączyć się z tym kontem",
"loadFailed": "Nie udało się wczytać tych preferencji",
"noPreference": "Brak preferencji",
"notSet": "Nie ustawiono",
"groups": {
"audioAndSubtitles": "Audio i napisy",
"libraryDisplay": "Biblioteka",
"personalMedia": "Osobiste multimedia"
},
"preferredAudioLanguage": "Preferowany język audio",
"autoSelectAudio": "Wybieraj audio według języka",
"autoSelectAudioDescription": "Ustawienie „Wył.” pozostawia ścieżkę audio, którą plik oznacza jako domyślną.",
"preferredSubtitleLanguage": "Preferowany język napisów",
"subtitleMode": "Włączanie napisów",
"subtitleModes": {
"none": "Wybór ręczny",
"noneDescription": "Nigdy nie włączaj napisów automatycznie.",
"defaultMode": "Kieruj się flagami ścieżki",
"defaultModeDescription": "Używaj flag domyślna i wymuszona zapisanych na każdej ścieżce napisów.",
"always": "Zawsze włączone",
"alwaysDescription": "Włączaj ścieżkę napisów w preferowanym języku, gdy tylko istnieje.",
"onlyForced": "Tylko wymuszone napisy",
"onlyForcedDescription": "Wczytuj tylko ścieżki oznaczone jako wymuszone.",
"smart": "Wyświetlane przy dźwięku w obcym języku",
"smartDescription": "Włączaj napisy tylko wtedy, gdy dźwięk jest w innym języku."
},
"subtitleAccessibility": "Napisy SDH",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Preferuj napisy bez SDH",
"preferSdh": "Preferuj napisy SDH",
"onlySdh": "Tylko napisy SDH",
"onlyNonSdh": "Tylko napisy bez SDH"
},
"forcedSubtitles": "Wymuszone napisy",
"forcedSubtitleOptions": {
"preferNonForced": "Preferuj niewymuszone napisy",
"preferForced": "Preferuj wymuszone napisy",
"onlyForced": "Tylko wymuszone napisy",
"onlyNonForced": "Tylko niewymuszone napisy"
},
"displayMissingEpisodes": "Pokaż brakujące odcinki",
"displayMissingEpisodesDescription": "Wyświetlaj odcinki, które serwer zna, ale dla których nie ma pliku.",
"hidePlayedInLatest": "Ukrywaj obejrzane pozycje w sekcji Ostatnio dodane",
"hidePlayedInLatestDescription": "Pomijaj pozycje, które już obejrzałeś, w wierszach „Ostatnio dodane” serwera.",
"displayCollectionsView": "Pokaż widok „Kolekcje”",
"displayCollectionsViewDescription": "Pokazuj widok kolekcji serwera obok Twoich bibliotek.",
"rewatchingInNextUp": "Zachowuj ponownie oglądane seriale w sekcji „Następny odcinek”",
"rewatchingInNextUpDescription": "Gdy skończysz serial i zaczniesz go oglądać ponownie, sekcja „Następny odcinek” podąży za ponownym oglądaniem zamiast usuwać serial.",
"watchedIndicator": "Wskaźniki obejrzanych",
"watchedIndicatorOptions": {
"none": "Nigdy",
"moviesAndShows": "Filmy i seriale TV",
"movies": "Tylko filmy",
"shows": "Tylko seriale TV"
},
"mediaReviewsVisibility": "Oceny i recenzje",
"mediaReviewsOptions": {
"usersAndCritics": "Użytkownicy i krytycy",
"usersOnly": "Tylko użytkownicy",
"criticsOnly": "Tylko krytycy",
"nobody": "Ukryte"
}
},
"discover": {
"title": "Odkryj",
"noContentAvailable": "Brak dostępnych treści",
@@ -1322,6 +1421,11 @@
"unknownChannel": "Nieznany kanał",
"live": "NA ŻYWO",
"reloadGuide": "Odśwież przewodnik",
"searchGuide": "Szukaj w przewodniku",
"searchHint": "Szukaj kanałów i programów",
"searchNoResults": "Brak dopasowań dla \"${query}\"",
"channelsSection": "Kanały",
"programsSection": "Programy",
"now": "Teraz",
"today": "Dzisiaj",
"tomorrow": "Jutro",
@@ -1472,6 +1576,8 @@
"repeatAll": "Powtarzaj wszystko",
"repeatOne": "Powtarzaj jeden",
"instantMixNoServer": "Brak dostępnego serwera dla szybkiego miksu",
"instantMixFailed": "Nie udało się wczytać miksu błyskawicznego",
"instantMixEmpty": "Miks błyskawiczny nie zwrócił żadnych utworów",
"noAudioUrl": "Brak dostępnego adresu URL audio dla utworu ${track}",
"discography": {
"singlesAndEps": "Single i EP",
@@ -1503,6 +1609,13 @@
"host": "Gospodarz",
"hostBadge": "GOSPODARZ",
"youAreHost": "Jesteś gospodarzem",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Oglądasz z innymi",
"endSession": "Zakończ sesję",
"leaveSession": "Opuść sesję",
@@ -1738,7 +1851,7 @@
"playPause": "Odtwórz/wstrzymaj",
"next": "Następny",
"seekBack": "Przewiń wstecz",
"stop": "Stop",
"stop": "Zatrzymaj",
"seekForward": "Przewiń w przód",
"volume": "Głośność",
"volumeDown": "Ciszej",
@@ -1992,6 +2105,8 @@
"statusBlocklisted": "Na liście blokowanych",
"couldNotReach": "Nie udało się połączyć z ${url}: ${error}",
"noInstanceAtUrl": "Pod adresem ${url} nie ma instancji Seerr (HTTP ${status})",
"invalidUrl": "Wprowadź adres serwera, np. https://seerr.example.com",
"quickConnectUnsupported": "Ta instancja Seerr nie obsługuje Quick Connect. Wymagany jest Seerr 3.4 lub nowszy.",
"notInitialized": "Ta instancja Seerr nie ukończyła konfiguracji początkowej",
"noPlexTokenForReauth": "Brak tokenu Plex umożliwiającego ponowne zalogowanie",
"noStoredCredentials": "Brak zapisanych danych logowania umożliwiających ponowne zalogowanie",
@@ -2004,6 +2119,7 @@
"services": {
"title": "Usługi",
"hubSubtitle": "Synchronizuj postęp oglądania i zamawiaj nowe tytuły.",
"integrations": "Integracje",
"notConnected": "Nie połączono",
"connectedAs": "Połączono jako @${username}",
"scrobble": "Automatycznie śledź postęp",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Usar navegador",
"or": "ou",
"connectToMediaBrowser": "Conectar ao ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Usar Quick Connect",
"quickConnectInstructions": "Abra o Quick Connect no Jellyfin e insira este código.",
"quickConnectWaiting": "Aguardando aprovação…",
@@ -53,6 +54,7 @@
"mute": "Silenciar",
"ok": "OK",
"off": "Desativado",
"options": "Opções",
"seasonNumber": "Temporada ${number}",
"episodeNumberTitle": "Episódio ${number} - ${title}",
"chapterNumber": "Capítulo ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Escala de exibição",
"compact": "Compacto",
"comfortable": "Confortável",
"gridSpacing": "Espaçamento da grade",
"gridSpacingTight": "Compacto",
"gridSpacingNormal": "Normal",
"gridSpacingSpacious": "Espaçoso",
"tvCornerSpotlightBackdrop": "Imagem de destaque no canto",
"tvCornerSpotlightBackdropDescription": "Mostrar a imagem de destaque no canto superior direito em vez de preencher a tela",
"viewMode": "Modo de Visualização",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Decodificação por Hardware",
"hardwareDecodingDescription": "Usar aceleração por hardware quando disponível",
"bufferSize": "Tamanho do Buffer",
"bufferSizeMB": "${size}MB",
"bufferSizeAuto": "Automático (Recomendado)",
"bufferSizeWarning": "${heap}MB de memória disponível. Um buffer de ${size}MB pode afetar a reprodução.",
"playbackBuffer": "Buffer de reprodução",
"playbackBufferAuto": "Automático (recomendado)",
"playbackBufferLarge": "Grande",
"playbackBufferExtraLarge": "Extra grande",
"playbackBufferDescription": "Faz buffer extra contra conexões instáveis. Também limitado pelo tamanho do buffer.",
"defaultQualityTitle": "Qualidade padrão",
"cellularQualityTitle": "Qualidade padrão nos dados móveis",
"cellularQualitySameAsDefault": "Igual à qualidade padrão",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Qualidade da música",
"subtitleStyling": "Estilo de Legendas",
"subtitleStylingDescription": "Personalizar aparência das legendas",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Lembrar escolhas de áudio e legendas por título",
"followServerTrackSelections": "Usar a seleção de faixas do servidor por episódio",
"followServerTrackSelectionsDescription": "Ao mudar de episódio, aplicar o áudio e as legendas selecionados no servidor em vez de manter a escolha atual",
"resumeMusicOnLaunch": "Lembrar a sessão de música",
"resumeMusicOnLaunchDescription": "Ao iniciar a aplicação, reabrir a última música em pausa onde parou",
"showChapterMarkersOnTimeline": "Mostrar marcadores de capítulos na barra de reprodução",
"showChapterMarkersOnTimelineDescription": "Segmentar a barra de reprodução nos limites dos capítulos",
"specialsOrdering": "Especiais na ordem dos episódios",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Atalho já atribuído a ${action}",
"shortcutUpdated": "Atalho atualizado para ${action}",
"saveFailed": "Não foi possível salvar as alterações. Tente novamente.",
"autoSkip": "Pular automaticamente",
"autoPlayAndSkip": "Reprodução automática e pulos",
"autoPlayNextEpisode": "Reproduzir próximo episódio automaticamente",
"autoPlayNextEpisodeDescription": "Iniciar o próximo episódio automaticamente quando um terminar",
"playNextCountdown": "Contagem regressiva do próximo episódio",
"playNextCountdownImmediate": "Reproduzir imediatamente",
"autoSkipIntro": "Pular introdução automaticamente",
"autoSkipIntroDescription": "Pular marcadores de introdução automaticamente após alguns segundos",
"autoSkipCredits": "Pular créditos automaticamente",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Forçar DV7 nativo e impedir uma nova tentativa de conversão de DV",
"dvConversionDv81Description": "Forçar a conversão RPU integrada para Dolby Vision perfil 8.1",
"dvConversionHevcStripDescription": "Remover as camadas RPU/EL do Dolby Vision e apresentar HEVC sem Dolby Vision",
"demuxer": "Demuxer de contêiner",
"demuxerDescription": "Qual demuxer processa os arquivos reproduzidos diretamente. Mude para media3 se algum arquivo apresentar problemas.",
"demuxerFfmpeg": "FFmpeg (recomendado)",
"demuxerMedia3": "media3",
"deinterlace": "Desentrelaçamento",
"deinterlaceDescription": "Remover artefatos de pente de vídeo entrelaçado (apenas no reprodutor mpv)",
"requireProfileSelectionOnOpen": "Pedir perfil ao abrir o app",
"requireProfileSelectionOnOpenDescription": "Mostrar a seleção de perfil sempre que o app for aberto",
"forceTvMode": "Forçar modo TV",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Exibir a aba Explorar com conteúdo do Plex Discover e dos rastreadores conectados",
"liveTvDefaultFavorites": "Canais favoritos por padrão",
"liveTvDefaultFavoritesDescription": "Mostrar apenas os canais favoritos ao abrir a TV ao vivo",
"general": "Geral",
"generalDescription": "Idioma, inicialização e comportamento da janela",
"languageAndRegion": "Idioma e Região",
"startup": "Inicialização",
"display": "Tela",
"libraryAndCards": "Biblioteca e Cards",
"homeScreen": "Tela inicial",
"navigation": "Navegação",
"window": "Janela",
"content": "Conteúdo",
"liveTv": "TV ao Vivo",
"player": "Reprodutor",
"subtitlesAndConfig": "Legendas e configuração",
"videoAndDisplay": "Vídeo e Tela",
"audio": "Áudio",
"quality": "Qualidade",
"subtitles": "Legendas",
"seekAndTiming": "Busca e tempo",
"behavior": "Comportamento",
"gestures": "Gestos",
"gestureBrightnessSwipe": "Deslize para brilho",
"gestureBrightnessSwipeDescription": "Deslize para cima ou para baixo na borda esquerda para ajustar o brilho",
"gestureVolumeSwipe": "Deslize para volume",
"gestureVolumeSwipeDescription": "Deslize para cima ou para baixo na borda direita para ajustar o volume",
"gesturePinchToZoom": "Pinça para zoom",
"gesturePinchToZoomDescription": "Pince o vídeo para ampliar ou reduzir",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Controles",
"rememberPlayerChanges": "Lembrar alterações do reprodutor",
"rememberPlayerChangesDescription": "Onde uma alteração feita durante a reprodução é salva e reaplicada",
"scopePlaybackSpeed": "Velocidade de reprodução",
@@ -908,6 +936,77 @@
"editMediaBrowserTitle": "Editar conexão do ${product}",
"editMediaBrowserIntro": "Adicione ou remova URLs para ${serverName}. O Plezy usará a URL acessível com menor latência."
},
"accountPreferences": {
"sectionTitle": "Preferências da conta",
"hubSubtitleSingle": "Opções de áudio, legendas e biblioteca salvas em ${account}",
"hubSubtitleMultiple": "Opções de áudio, legendas e biblioteca salvas em ${count} contas",
"pickAccount": "Cada conta armazena suas próprias preferências. Escolha a que deseja editar.",
"storedOnAccount": "Estas opções são salvas na própria conta, então todos os apps conectados a ela as utilizam — inclusive o Plezy nos seus outros dispositivos.",
"noAccounts": "Nenhuma conta para configurar",
"noAccountsHint": "Entre no Plex ou conecte um servidor Jellyfin ou Emby, e as preferências armazenadas nessa conta aparecerão aqui.",
"unavailable": "Não foi possível acessar esta conta",
"loadFailed": "Não foi possível carregar estas preferências",
"noPreference": "Sem preferência",
"notSet": "Não definido",
"groups": {
"audioAndSubtitles": "Áudio e legendas",
"libraryDisplay": "Biblioteca",
"personalMedia": "Mídia pessoal"
},
"preferredAudioLanguage": "Idioma de áudio preferido",
"autoSelectAudio": "Escolher áudio pelo idioma",
"autoSelectAudioDescription": "Desativado mantém a faixa de áudio que o arquivo marca como padrão.",
"preferredSubtitleLanguage": "Idioma de legenda preferido",
"subtitleMode": "Ativar legendas",
"subtitleModes": {
"none": "Seleção manual",
"noneDescription": "Nunca ativar legendas por conta própria.",
"defaultMode": "Seguir as marcações da faixa",
"defaultModeDescription": "Usar as marcações padrão e forçada armazenadas em cada faixa de legenda.",
"always": "Sempre ativado",
"alwaysDescription": "Ativar uma faixa de legenda no idioma preferido sempre que houver uma.",
"onlyForced": "Apenas legendas forçadas",
"onlyForcedDescription": "Carregar apenas as faixas marcadas como forçadas.",
"smart": "Exibir com áudio estrangeiro",
"smartDescription": "Ativar legendas apenas quando o áudio estiver em outro idioma."
},
"subtitleAccessibility": "Legendas SDH",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Preferir legendas não SDH",
"preferSdh": "Preferir legendas SDH",
"onlySdh": "Apenas legendas SDH",
"onlyNonSdh": "Apenas legendas não SDH"
},
"forcedSubtitles": "Legendas forçadas",
"forcedSubtitleOptions": {
"preferNonForced": "Preferir legendas não forçadas",
"preferForced": "Preferir legendas forçadas",
"onlyForced": "Apenas legendas forçadas",
"onlyNonForced": "Apenas legendas não forçadas"
},
"displayMissingEpisodes": "Mostrar episódios ausentes",
"displayMissingEpisodesDescription": "Listar episódios que o servidor conhece, mas para os quais não há arquivo.",
"hidePlayedInLatest": "Ocultar itens assistidos em Recentes",
"hidePlayedInLatestDescription": "Manter fora das linhas Recentes do servidor os itens que você já assistiu.",
"displayCollectionsView": "Mostrar a visualização de Coleções",
"displayCollectionsViewDescription": "Oferecer a visualização de Coleções do servidor junto às suas bibliotecas.",
"rewatchingInNextUp": "Manter séries reassistidas em A seguir",
"rewatchingInNextUpDescription": "Ao terminar uma série, comece a assistir de novo e A seguir acompanha a reexibição em vez de remover a série.",
"watchedIndicator": "Indicadores de assistido",
"watchedIndicatorOptions": {
"none": "Nunca",
"moviesAndShows": "Filmes e séries de TV",
"movies": "Apenas filmes",
"shows": "Apenas séries de TV"
},
"mediaReviewsVisibility": "Avaliações e resenhas",
"mediaReviewsOptions": {
"usersAndCritics": "Usuários e críticos",
"usersOnly": "Apenas usuários",
"criticsOnly": "Apenas críticos",
"nobody": "Oculto"
}
},
"discover": {
"title": "Descobrir",
"noContentAvailable": "Nenhum conteúdo disponível",
@@ -1304,6 +1403,11 @@
"unknownChannel": "Canal desconhecido",
"live": "AO VIVO",
"reloadGuide": "Recarregar Guia",
"searchGuide": "Buscar no guia",
"searchHint": "Buscar canais e programas",
"searchNoResults": "Nenhuma correspondência para \"${query}\"",
"channelsSection": "Canais",
"programsSection": "Programas",
"now": "Agora",
"today": "Hoje",
"tomorrow": "Amanhã",
@@ -1452,6 +1556,8 @@
"repeatAll": "Repetir tudo",
"repeatOne": "Repetir uma faixa",
"instantMixNoServer": "Nenhum servidor está disponível para uma mistura instantânea",
"instantMixFailed": "Falha ao carregar o mix instantâneo",
"instantMixEmpty": "O mix instantâneo não retornou nenhuma faixa",
"noAudioUrl": "Nenhuma URL de áudio está disponível para ${track}",
"discography": {
"singlesAndEps": "Singles e EPs",
@@ -1483,6 +1589,13 @@
"host": "Anfitrião",
"hostBadge": "ANFITRIÃO",
"youAreHost": "Você é o anfitrião",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Assistindo com outras pessoas",
"endSession": "Encerrar sessão",
"leaveSession": "Sair da sessão",
@@ -1972,6 +2085,8 @@
"statusBlocklisted": "Na lista de bloqueio",
"couldNotReach": "Não foi possível acessar ${url}: ${error}",
"noInstanceAtUrl": "Nenhuma instância do Seerr em ${url} (HTTP ${status})",
"invalidUrl": "Insira um endereço de servidor como https://seerr.example.com",
"quickConnectUnsupported": "Esta instância do Seerr não oferece suporte a Quick Connect. Ela precisa do Seerr 3.4 ou mais recente.",
"notInitialized": "Esta instância do Seerr não concluiu a configuração inicial",
"noPlexTokenForReauth": "Nenhum token do Plex está disponível para entrar novamente",
"noStoredCredentials": "Nenhuma credencial armazenada está disponível para entrar novamente",
@@ -1984,6 +2099,7 @@
"services": {
"title": "Serviços",
"hubSubtitle": "Sincronize o progresso de exibição e solicite novos títulos.",
"integrations": "Integrações",
"notConnected": "Não conectado",
"connectedAs": "Conectado como @${username}",
"scrobble": "Registrar progresso automaticamente",
+127 -11
View File
@@ -12,6 +12,7 @@
"useBrowser": "Использовать браузер",
"or": "или",
"connectToMediaBrowser": "Подключиться к ${product}",
"quickConnect": "Quick Connect",
"useQuickConnect": "Использовать Quick Connect",
"quickConnectInstructions": "Откройте Quick Connect в Jellyfin и введите этот код.",
"quickConnectWaiting": "Ожидание подтверждения…",
@@ -53,6 +54,7 @@
"mute": "Без звука",
"ok": "OK",
"off": "Выкл.",
"options": "Параметры",
"seasonNumber": "Сезон ${number}",
"episodeNumberTitle": "Эпизод ${number} - ${title}",
"chapterNumber": "Глава ${number}",
@@ -141,6 +143,10 @@
"displayScale": "Масштаб отображения",
"compact": "Компактный",
"comfortable": "Комфортный",
"gridSpacing": "Отступы сетки",
"gridSpacingTight": "Плотные",
"gridSpacingNormal": "Обычные",
"gridSpacingSpacious": "Просторные",
"tvCornerSpotlightBackdrop": "Фоновое изображение в углу",
"tvCornerSpotlightBackdropDescription": "Показывать изображение избранного материала в правом верхнем углу, а не на весь экран",
"viewMode": "Режим просмотра",
@@ -184,16 +190,16 @@
"mpv": "mpv",
"hardwareDecoding": "Аппаратное декодирование",
"hardwareDecodingDescription": "Использовать аппаратное ускорение, когда доступно",
"bufferSize": "Размер буфера",
"bufferSizeMB": "${size}МБ",
"bufferSizeAuto": "Авто (Рекомендуется)",
"bufferSizeWarning": "Доступно памяти: ${heap} МБ. Буфер размером ${size} МБ может повлиять на воспроизведение.",
"playbackBuffer": "Буфер воспроизведения",
"playbackBufferAuto": "Авто (рекомендуется)",
"playbackBufferLarge": "Большой",
"playbackBufferExtraLarge": "Очень большой",
"playbackBufferDescription": "Буферизирует больше при нестабильном соединении. Также ограничено размером буфера.",
"defaultQualityTitle": "Качество по умолчанию",
"cellularQualityTitle": "Качество по умолчанию в мобильной сети",
"cellularQualitySameAsDefault": "Как качество по умолчанию",
"directPlayCoveredQuality": "",
"directPlayCoveredQualityDescription": "",
"musicQualityTitle": "Качество музыки",
"subtitleStyling": "Стиль субтитров",
"subtitleStylingDescription": "Настроить внешний вид субтитров",
@@ -207,6 +213,8 @@
"rememberTrackSelectionsDescription": "Запоминать выбор аудиодорожки и субтитров для каждого материала",
"followServerTrackSelections": "Использовать выбор дорожек сервера для каждой серии",
"followServerTrackSelectionsDescription": "При смене серии применять аудио и субтитры, выбранные на сервере, вместо переноса текущего выбора",
"resumeMusicOnLaunch": "Запоминать музыкальную сессию",
"resumeMusicOnLaunchDescription": "При запуске приложения открывать последний трек на паузе с того места, где он остановился",
"showChapterMarkersOnTimeline": "Показывать маркеры глав на шкале перемотки",
"showChapterMarkersOnTimelineDescription": "Разделять шкалу перемотки по границам глав",
"specialsOrdering": "Спецвыпуски в порядке эпизодов",
@@ -260,7 +268,11 @@
"shortcutAlreadyAssigned": "Сочетание клавиш уже назначено действию «${action}»",
"shortcutUpdated": "Сочетание клавиш для действия «${action}» обновлено",
"saveFailed": "Не удалось сохранить изменения. Попробуйте снова.",
"autoSkip": "Автопропуск",
"autoPlayAndSkip": "Автовоспроизведение и пропуск",
"autoPlayNextEpisode": "Автовоспроизведение следующего эпизода",
"autoPlayNextEpisodeDescription": "Автоматически запускать следующий эпизод, когда заканчивается текущий",
"playNextCountdown": "Обратный отсчёт перед следующим",
"playNextCountdownImmediate": "Воспроизводить сразу",
"autoSkipIntro": "Автопропуск вступления",
"autoSkipIntroDescription": "Автоматически пропускать маркеры вступления через несколько секунд",
"autoSkipCredits": "Автопропуск титров",
@@ -335,10 +347,8 @@
"dvConversionNativeDescription": "Принудительно использовать нативный DV7 и не повторять DV-конвертацию",
"dvConversionDv81Description": "Принудительно выполнять inline-конвертацию RPU в Dolby Vision профиль 8.1",
"dvConversionHevcStripDescription": "Удалять слои Dolby Vision RPU/EL и передавать обычный HEVC",
"demuxer": "Демультиплексор контейнера",
"demuxerDescription": "Какой демультиплексор разбирает файлы при прямом воспроизведении. Переключитесь на media3, если файл воспроизводится неправильно.",
"demuxerFfmpeg": "FFmpeg (рекомендуется)",
"demuxerMedia3": "media3",
"deinterlace": "Деинтерлейсинг",
"deinterlaceDescription": "Устранять гребёнку на чересстрочном видео (только в плеере mpv)",
"requireProfileSelectionOnOpen": "Запрашивать профиль при запуске",
"requireProfileSelectionOnOpenDescription": "Показывать выбор профиля при каждом открытии приложения",
"forceTvMode": "Принудительный режим ТВ",
@@ -356,15 +366,33 @@
"showExploreTabDescription": "Показывать вкладку «Обзор» с контентом из Plex Discover и подключённых трекеров",
"liveTvDefaultFavorites": "Избранные каналы по умолчанию",
"liveTvDefaultFavoritesDescription": "Показывать только избранные каналы при открытии ТВ",
"general": "Основные",
"generalDescription": "Язык, запуск и поведение окна",
"languageAndRegion": "Язык и регион",
"startup": "Запуск",
"display": "Экран",
"libraryAndCards": "Библиотека и карточки",
"homeScreen": "Главный экран",
"navigation": "Навигация",
"window": "Окно",
"content": "Контент",
"liveTv": "Прямой эфир",
"player": "Плеер",
"subtitlesAndConfig": "Субтитры и конфигурация",
"videoAndDisplay": "Видео и экран",
"audio": "Аудио",
"quality": "Качество",
"subtitles": "Субтитры",
"seekAndTiming": "Перемотка и время",
"behavior": "Поведение",
"gestures": "Жесты",
"gestureBrightnessSwipe": "Свайп для яркости",
"gestureBrightnessSwipeDescription": "Проведите вверх или вниз по левому краю, чтобы изменить яркость",
"gestureVolumeSwipe": "Свайп для громкости",
"gestureVolumeSwipeDescription": "Проведите вверх или вниз по правому краю, чтобы изменить громкость",
"gesturePinchToZoom": "Щипок для масштабирования",
"gesturePinchToZoomDescription": "Сведите или разведите пальцы на видео, чтобы изменить масштаб",
"rememberBrightnessLevel": "",
"rememberBrightnessLevelDescription": "",
"controls": "Элементы управления",
"rememberPlayerChanges": "Запоминать изменения плеера",
"rememberPlayerChangesDescription": "Где сохраняется и откуда повторно применяется изменение, сделанное во время воспроизведения",
"scopePlaybackSpeed": "Скорость воспроизведения",
@@ -916,6 +944,77 @@
"editMediaBrowserTitle": "Изменить подключение ${product}",
"editMediaBrowserIntro": "Добавьте или удалите URL-адреса для ${serverName}. Plezy будет использовать доступный URL с наименьшей задержкой."
},
"accountPreferences": {
"sectionTitle": "Настройки аккаунта",
"hubSubtitleSingle": "Параметры аудио, субтитров и библиотеки сохранены на аккаунте ${account}",
"hubSubtitleMultiple": "Параметры аудио, субтитров и библиотеки сохранены на ${count} аккаунтах",
"pickAccount": "Каждый аккаунт хранит собственные настройки. Выберите, какой редактировать.",
"storedOnAccount": "Эти параметры сохраняются на самом аккаунте, поэтому их используют все приложения, вошедшие в него, — включая Plezy на ваших других устройствах.",
"noAccounts": "Нет аккаунтов для настройки",
"noAccountsHint": "Войдите в Plex или подключите сервер Jellyfin или Emby — и настройки, сохранённые на этом аккаунте, появятся здесь.",
"unavailable": "Не удаётся получить доступ к этому аккаунту",
"loadFailed": "Не удалось загрузить эти настройки",
"noPreference": "Без предпочтений",
"notSet": "Не задано",
"groups": {
"audioAndSubtitles": "Аудио и субтитры",
"libraryDisplay": "Библиотека",
"personalMedia": "Личные медиа"
},
"preferredAudioLanguage": "Предпочитаемый язык аудио",
"autoSelectAudio": "Выбирать аудиодорожку по языку",
"autoSelectAudioDescription": "В выключенном состоянии используется аудиодорожка, помеченная в файле как дорожка по умолчанию.",
"preferredSubtitleLanguage": "Предпочитаемый язык субтитров",
"subtitleMode": "Включать субтитры",
"subtitleModes": {
"none": "Выбор вручную",
"noneDescription": "Никогда не включать субтитры автоматически.",
"defaultMode": "Следовать флагам дорожки",
"defaultModeDescription": "Использовать флаги «по умолчанию» и «принудительная», сохранённые на каждой дорожке субтитров.",
"always": "Всегда включены",
"alwaysDescription": "Включать дорожку субтитров на предпочитаемом языке, когда такая существует.",
"onlyForced": "Только принудительные субтитры",
"onlyForcedDescription": "Загружать только дорожки, помеченные как принудительные.",
"smart": "Показывать при иноязычном аудио",
"smartDescription": "Включать субтитры только когда аудио на другом языке."
},
"subtitleAccessibility": "Субтитры SDH",
"subtitleAccessibilityOptions": {
"preferNonSdh": "Предпочитать субтитры без SDH",
"preferSdh": "Предпочитать субтитры SDH",
"onlySdh": "Только субтитры SDH",
"onlyNonSdh": "Только субтитры без SDH"
},
"forcedSubtitles": "Принудительные субтитры",
"forcedSubtitleOptions": {
"preferNonForced": "Предпочитать непринудительные субтитры",
"preferForced": "Предпочитать принудительные субтитры",
"onlyForced": "Только принудительные субтитры",
"onlyNonForced": "Только непринудительные субтитры"
},
"displayMissingEpisodes": "Показывать отсутствующие эпизоды",
"displayMissingEpisodesDescription": "Перечислять эпизоды, которые знает сервер, но для которых нет файлов.",
"hidePlayedInLatest": "Скрывать просмотренное в «Недавно добавленном»",
"hidePlayedInLatestDescription": "Исключать уже просмотренные материалы из рядов «Недавно добавленное» сервера.",
"displayCollectionsView": "Показывать раздел «Коллекции»",
"displayCollectionsViewDescription": "Показывать раздел коллекций сервера рядом с вашими библиотеками.",
"rewatchingInNextUp": "Сохранять пересматриваемые сериалы в «Далее»",
"rewatchingInNextUpDescription": "Когда вы досматриваете сериал и начинаете его заново, «Далее» продолжает отслеживать повторный просмотр, а не убирает сериал.",
"watchedIndicator": "Индикаторы просмотра",
"watchedIndicatorOptions": {
"none": "Никогда",
"moviesAndShows": "Фильмы и сериалы",
"movies": "Только фильмы",
"shows": "Только сериалы"
},
"mediaReviewsVisibility": "Оценки и отзывы",
"mediaReviewsOptions": {
"usersAndCritics": "Пользователи и критики",
"usersOnly": "Только пользователи",
"criticsOnly": "Только критики",
"nobody": "Скрыто"
}
},
"discover": {
"title": "Обзор",
"noContentAvailable": "Контент недоступен",
@@ -1322,6 +1421,11 @@
"unknownChannel": "Неизвестный канал",
"live": "ЭФИР",
"reloadGuide": "Перезагрузить программу",
"searchGuide": "Поиск в программе",
"searchHint": "Поиск каналов и программ",
"searchNoResults": "Нет совпадений по запросу \"${query}\"",
"channelsSection": "Каналы",
"programsSection": "Программы",
"now": "Сейчас",
"today": "Сегодня",
"tomorrow": "Завтра",
@@ -1472,6 +1576,8 @@
"repeatAll": "Повторять все",
"repeatOne": "Повторять один",
"instantMixNoServer": "Нет доступного сервера для мгновенного микса",
"instantMixFailed": "Не удалось загрузить быстрый микс",
"instantMixEmpty": "Быстрый микс не нашёл ни одного трека",
"noAudioUrl": "Для трека ${track} нет доступного URL аудио",
"discography": {
"singlesAndEps": "Синглы и EP",
@@ -1503,6 +1609,13 @@
"host": "Организатор",
"hostBadge": "ОРГАНИЗАТОР",
"youAreHost": "Вы — организатор",
"makeHost": "",
"makeHostQuestion": "",
"makeHostConfirm": "",
"transfer": "",
"hostChangedTo": "",
"youAreNowHost": "",
"hostTransferFailed": "",
"watchingWithOthers": "Совместный просмотр",
"endSession": "Завершить сессию",
"leaveSession": "Покинуть сессию",
@@ -1992,6 +2105,8 @@
"statusBlocklisted": "В списке блокировки",
"couldNotReach": "Не удалось связаться с ${url}: ${error}",
"noInstanceAtUrl": "По адресу ${url} нет экземпляра Seerr (HTTP ${status})",
"invalidUrl": "Введите адрес сервера, например https://seerr.example.com",
"quickConnectUnsupported": "Этот экземпляр Seerr не поддерживает Quick Connect. Нужна версия Seerr 3.4 или новее.",
"notInitialized": "Первоначальная настройка этого экземпляра Seerr не завершена",
"noPlexTokenForReauth": "Нет токена Plex для повторного входа",
"noStoredCredentials": "Нет сохранённых учётных данных для повторного входа",
@@ -2004,6 +2119,7 @@
"services": {
"title": "Сервисы",
"hubSubtitle": "Синхронизируйте прогресс просмотра и запрашивайте новые фильмы и сериалы.",
"integrations": "Интеграции",
"notConnected": "Не подключено",
"connectedAs": "Подключено как @${username}",
"scrobble": "Автоматически отслеживать прогресс",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 22
/// Strings: 41734 (1897 per locale)
/// Strings: 43747 (1988 per locale)
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsAz extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$az dialog = _Translations$dialog$az._(_root);
@override late final _Translations$profiles$az profiles = _Translations$profiles$az._(_root);
@override late final _Translations$connections$az connections = _Translations$connections$az._(_root);
@override late final _Translations$accountPreferences$az accountPreferences = _Translations$accountPreferences$az._(_root);
@override late final _Translations$discover$az discover = _Translations$discover$az._(_root);
@override late final _Translations$errors$az errors = _Translations$errors$az._(_root);
@override late final _Translations$libraries$az libraries = _Translations$libraries$az._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$az extends Translations$auth$en {
@override String get useBrowser => 'Səyahətçini istifadə et';
@override String get or => 'və ya';
@override String connectToMediaBrowser({required Object product}) => '${product}-a qoşul';
@override String get quickConnect => 'Sürətli Qoşulma';
@override String get useQuickConnect => 'Sürətli Qoşulmanı istifadə et';
@override String get quickConnectInstructions => 'Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.';
@override String get quickConnectWaiting => 'Təsdiq gözlənilir…';
@@ -165,6 +167,7 @@ class _Translations$common$az extends Translations$common$en {
@override String get mute => 'Səsi söndür';
@override String get ok => 'Oldu';
@override String get off => 'Söndürülüb';
@override String get options => 'Seçimlər';
@override String seasonNumber({required Object number}) => 'Mövsüm ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Seriya ${number} - ${title}';
@override String chapterNumber({required Object number}) => 'Hissə ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$az extends Translations$settings$en {
@override String get displayScale => 'Ekran miqyası';
@override String get compact => 'Sıx';
@override String get comfortable => 'Rəhat';
@override String get gridSpacing => 'Tor aralığı';
@override String get gridSpacingTight => 'Sıx';
@override String get gridSpacingNormal => 'Normal';
@override String get gridSpacingSpacious => 'Geniş';
@override String get tvCornerSpotlightBackdrop => 'Künc işıqlandırma fonu';
@override String get tvCornerSpotlightBackdropDescription => 'Arxa fonu ekranı örtmək əvəzinə sağ üst küncdə göstər';
@override String get viewMode => 'Baxış rejimi';
@@ -292,16 +299,14 @@ class _Translations$settings$az extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Avadanlıq kod açılması';
@override String get hardwareDecodingDescription => 'Mümkün olduqda avadanlıq sürətləndirməsini istifadə et';
@override String get bufferSize => 'Bufer həcmi';
@override String bufferSizeMB({required Object size}) => '${size}MB';
@override String get bufferSizeAuto => 'Avtomatik (Tövsiyə olunan)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MB yaddaş əlçatandır. ${size}MB bufer oynatmaya təsir edə bilər.';
@override String get playbackBuffer => 'Oxutma buferi';
@override String get playbackBufferAuto => 'Avtomatik (tövsiyə olunur)';
@override String get playbackBufferLarge => 'Böyük';
@override String get playbackBufferExtraLarge => 'Çox böyük';
@override String get playbackBufferDescription => 'Qeyri-sabit əlaqələrə qarşı daha çox buferləyir. Bufer ölçüsü ilə də məhdudlaşır.';
@override String get defaultQualityTitle => 'Defolt keyfiyyət';
@override String get cellularQualityTitle => 'Mobil şəbəkədə defolt keyfiyyət';
@override String get cellularQualitySameAsDefault => 'Defolt keyfiyyətlə eyni';
@override String get musicQualityTitle => 'Musiqi keyfiyyəti';
@override String get subtitleStyling => 'Altyazı tənzimləmələri';
@override String get subtitleStylingDescription => 'Altyazı görünüşünü özünüləşdirin';
@@ -315,6 +320,8 @@ class _Translations$settings$az extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Hər məzmun üçün səs və altyazı seçimlərini yadda saxla';
@override String get followServerTrackSelections => 'Hər epizod üçün serverin trek seçimlərini istifadə et';
@override String get followServerTrackSelectionsDescription => 'Epizod dəyişəndə cari seçimi köçürmək əvəzinə serverdə seçilmiş səs və altyazını tətbiq et';
@override String get resumeMusicOnLaunch => 'Musiqi sessiyasını yadda saxla';
@override String get resumeMusicOnLaunchDescription => 'Tətbiq açılanda son mahnını dayandırıldığı yerdə fasilədə aç';
@override String get showChapterMarkersOnTimeline => 'Zaman çubuğunda hissə işarələrini göstər';
@override String get showChapterMarkersOnTimelineDescription => 'Zaman çubuğunu hissə sərhədlərinə böl';
@override String get specialsOrdering => 'Xüsusi bölmələr epizod sırasına görə';
@@ -368,7 +375,11 @@ class _Translations$settings$az extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Qısayol artıq ${action} üçün təyin edilib';
@override String shortcutUpdated({required Object action}) => '${action} üçün qısayol yeniləndi';
@override String get saveFailed => 'Dəyişikliklər yadda saxlanıla bilmədi. Təzədən cəhd edin.';
@override String get autoSkip => 'Avtomatik ötür';
@override String get autoPlayAndSkip => 'Avtomatik oynat və ötür';
@override String get autoPlayNextEpisode => 'Növbəti seriyanı avtomatik oynat';
@override String get autoPlayNextEpisodeDescription => 'Bir seriya bitdikdə növbətisini avtomatik başlat';
@override String get playNextCountdown => 'Növbəti seriya geri sayımı';
@override String get playNextCountdownImmediate => 'Dərhal oynat';
@override String get autoSkipIntro => 'Girişi avtomatik ötür';
@override String get autoSkipIntroDescription => 'Bir neçə saniyədən sonra giriş işarələrini avtomatik ötür';
@override String get autoSkipCredits => 'Titrləri avtomatik ötür';
@@ -443,10 +454,8 @@ class _Translations$settings$az extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Daxili DV7-ni məcburi et';
@override String get dvConversionDv81Description => 'Dolby Vision profile 8.1-ə çevrilməni məcburi et';
@override String get dvConversionHevcStripDescription => 'Dolby Vision təbəqələrini sil və sadə HEVC kimi təqdim et';
@override String get demuxer => 'Konteyner demukseri';
@override String get demuxerDescription => 'Birbaşa oxudulan faylları hansı demukserin təhlil edəcəyi. Fayl düzgün işləmirsə, media3-ə keçin.';
@override String get demuxerFfmpeg => 'FFmpeg (Tövsiyə olunan)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Deinterleysinq';
@override String get deinterlaceDescription => 'Sətirlərarası videodakı darama artefaktlarını aradan qaldır (yalnız mpv oynadıcısı)';
@override String get requireProfileSelectionOnOpen => 'Açılışda profil soruş';
@override String get requireProfileSelectionOnOpenDescription => 'Tətbiq hər dəfə açıldıqda profil seçimini göstər';
@override String get forceTvMode => 'TV rejimini məcburi et';
@@ -464,15 +473,31 @@ class _Translations$settings$az extends Translations$settings$en {
@override String get showExploreTabDescription => 'Plex Discover və qoşulmuş izləmə xidmətlərindəki məzmunla Kəşf et nişanını göstər';
@override String get liveTvDefaultFavorites => 'Canlı TV-də sevimli kanalları defolt et';
@override String get liveTvDefaultFavoritesDescription => 'Canlı TV açıldıqda yalnız sevimli kanalları göstər';
@override String get general => 'Ümumi';
@override String get generalDescription => 'Dil, başlanğıc və pəncərə davranışı';
@override String get languageAndRegion => 'Dil və Region';
@override String get startup => 'Başlanğıc';
@override String get display => 'Ekran';
@override String get libraryAndCards => 'Kitabxana və kartlar';
@override String get homeScreen => 'Ana ekran';
@override String get navigation => 'Naviqasiya';
@override String get window => 'Pəncərə';
@override String get content => 'Məzmun';
@override String get liveTv => 'Canlı TV';
@override String get player => 'Oynadıcı';
@override String get subtitlesAndConfig => 'Altyazılar və konfiqurasiya';
@override String get videoAndDisplay => 'Video və Ekran';
@override String get audio => 'Səs';
@override String get quality => 'Keyfiyyət';
@override String get subtitles => 'Altyazılar';
@override String get seekAndTiming => 'Sarğı və vaxt tənzimləməsi';
@override String get behavior => 'Davranış';
@override String get gestures => 'Jestlər';
@override String get gestureBrightnessSwipe => 'Parlaqlıq sürüşdürməsi';
@override String get gestureBrightnessSwipeDescription => 'Parlaqlığı tənzimləmək üçün sol kənarda yuxarı və ya aşağı sürüşdürün';
@override String get gestureVolumeSwipe => 'Səs sürüşdürməsi';
@override String get gestureVolumeSwipeDescription => 'Səsi tənzimləmək üçün sağ kənarda yuxarı və ya aşağı sürüşdürün';
@override String get gesturePinchToZoom => 'Çimdiklə yaxınlaşdır';
@override String get gesturePinchToZoomDescription => 'Yaxınlaşdırmaq və ya uzaqlaşdırmaq üçün videoda çimdik hərəkəti edin';
@override String get controls => 'İdarəetmələr';
@override String get rememberPlayerChanges => 'Pleyer dəyişikliklərini yadda saxla';
@override String get rememberPlayerChangesDescription => 'Oxutma zamanı edilən dəyişikliklərin harada saxlanacağı və yenidən tətbiq ediləcəyi';
@override String get scopePlaybackSpeed => 'Oxutma sürəti';
@@ -1086,6 +1111,49 @@ class _Translations$connections$az extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => '${serverName} üçün URL-lər əlavə edin və ya silin. Plezy əlçatan olan ən aşağı gecikməli URL-i istifadə edəcək.';
}
// Path: accountPreferences
class _Translations$accountPreferences$az extends Translations$accountPreferences$en {
_Translations$accountPreferences$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Hesab tərcihləri';
@override String hubSubtitleSingle({required Object account}) => '${account} hesabında saxlanılan səs, altyazı və kitabxana seçimləri';
@override String hubSubtitleMultiple({required Object count}) => '${count} hesabda saxlanılan səs, altyazı və kitabxana seçimləri';
@override String get pickAccount => 'Hər hesab öz tərcihlərini saxlayır. Düzəliş etmək üçün birini seçin.';
@override String get storedOnAccount => 'Bu seçimlər hesabın özündə saxlanılır, ona görə də ona daxil olan hər tətbiq onlardan istifadə edir — digər cihazlarınızdakı Plezy də daxil olmaqla.';
@override String get noAccounts => 'Konfiqurasiya ediləcək hesab yoxdur';
@override String get noAccountsHint => 'Plex-ə daxil olun və ya Jellyfin və ya Emby serverinə qoşulun; o hesabda saxlanılan tərcihlər burada görünəcək.';
@override String get unavailable => 'Bu hesaba çatmaq olmur';
@override String get loadFailed => 'Bu tərcihlər yüklənə bilmədi';
@override String get noPreference => 'Tərcih yoxdur';
@override String get notSet => 'Təyin edilməyib';
@override late final _Translations$accountPreferences$groups$az groups = _Translations$accountPreferences$groups$az._(_root);
@override String get preferredAudioLanguage => 'Üstünlük verilən səs dili';
@override String get autoSelectAudio => 'Səsi dilə görə seç';
@override String get autoSelectAudioDescription => 'Söndürüldükdə faylın defolt olaraq işarələdiyi səs treki istifadə olunur.';
@override String get preferredSubtitleLanguage => 'Üstünlük verilən altyazı dili';
@override String get subtitleMode => 'Altyazıları aç';
@override late final _Translations$accountPreferences$subtitleModes$az subtitleModes = _Translations$accountPreferences$subtitleModes$az._(_root);
@override String get subtitleAccessibility => 'SDH altyazıları';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$az subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$az._(_root);
@override String get forcedSubtitles => 'Məcburi altyazılar';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$az forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$az._(_root);
@override String get displayMissingEpisodes => 'Çatışmayan seriyaları göstər';
@override String get displayMissingEpisodesDescription => 'Serverin bildiyi, lakin faylı olmayan seriyaları siyahıya al.';
@override String get hidePlayedInLatest => 'Baxılmış elementləri Son əlavə olunanlarda gizlət';
@override String get hidePlayedInLatestDescription => 'Artıq baxdığınız elementləri serverin Son əlavə olunanlar sətirlərində göstərmə.';
@override String get displayCollectionsView => 'Kolleksiyalar görünüşünü göstər';
@override String get displayCollectionsViewDescription => 'Kitabxanalarınızla yanaşı serverin Kolleksiyalar görünüşünü də təklif et.';
@override String get rewatchingInNextUp => 'Təkrar baxılan serialları Sırada saxla';
@override String get rewatchingInNextUpDescription => 'Bir serialı bitirdikdən sonra yenidən başlasanız, Sırada serialı atmaq əvəzinə təkrar baxışı izləyir.';
@override String get watchedIndicator => 'Baxıldı göstəriciləri';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$az watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$az._(_root);
@override String get mediaReviewsVisibility => 'Reytinq və rəylər';
@override late final _Translations$accountPreferences$mediaReviewsOptions$az mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$az._(_root);
}
// Path: discover
class _Translations$discover$az extends Translations$discover$en {
_Translations$discover$az._(TranslationsAz root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$az extends Translations$liveTv$en {
@override String get unknownChannel => 'Bilinməyən kanal';
@override String get live => 'CANLI';
@override String get reloadGuide => 'Bələdçini yenilə';
@override String get searchGuide => 'Bələdçidə axtar';
@override String get searchHint => 'Kanal və proqram axtar';
@override String searchNoResults({required Object query}) => '"${query}" üçün uyğunluq tapılmadı';
@override String get channelsSection => 'Kanallar';
@override String get programsSection => 'Proqramlar';
@override String get now => 'İndi';
@override String get today => 'Bu gün';
@override String get tomorrow => 'Sabah';
@@ -1545,6 +1618,8 @@ class _Translations$music$az extends Translations$music$en {
@override String get repeatAll => 'Hamısını təkrarla';
@override String get repeatOne => 'Birini təkrarla';
@override String get instantMixNoServer => 'Ani miks üçün heç bir server mövcud deyil';
@override String get instantMixFailed => 'Anında qarışıq yüklənə bilmədi';
@override String get instantMixEmpty => 'Anında qarışıq heç bir mahnı qaytarmadı';
@override String noAudioUrl({required Object track}) => '${track} üçün səs URL-i mövcud deyil';
@override late final _Translations$music$discography$az discography = _Translations$music$discography$az._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$az extends Translations$seerr$en {
@override String get statusBlocklisted => 'Bloklanmış';
@override String couldNotReach({required Object url, required Object error}) => '${url} ünvanına çatmaq olmadı: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => '${url} ünvanında Seerr instansiyası yoxdur (HTTP ${status})';
@override String get invalidUrl => 'https://seerr.example.com kimi server ünvanı daxil edin';
@override String get quickConnectUnsupported => 'Bu Seerr nüsxəsi Sürətli Qoşulmanı dəstəkləmir. Seerr 3.4 və ya daha yeni versiya tələb olunur.';
@override String get notInitialized => 'Bu Seerr instansiyasının ilkin quraşdırılması tamamlanmayıb';
@override String get noPlexTokenForReauth => 'Yenidən daxil olmaq üçün Plex tokeni mövcud deyil';
@override String get noStoredCredentials => 'Yenidən daxil olmaq üçün yadda saxlanmış giriş məlumatları mövcud deyil';
@@ -2066,6 +2143,7 @@ class _Translations$services$az extends Translations$services$en {
// Translations
@override String get title => 'Xidmətlər';
@override String get hubSubtitle => 'İzləmə tərəqqisini eyniləşdirin və yeni başlıqlar sorğulayın.';
@override String get integrations => 'İnteqrasiyalar';
@override String get notConnected => 'Qoşulmayıb';
@override String connectedAs({required Object username}) => '@${username} olaraq qoşuldu';
@override String get scrobble => 'Tərəqqini avtomatik izlə';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$az extends Translations$videoControl
@override String unknown({required Object error}) => 'Xəta baş verdi: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$az extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Səs və altyazılar';
@override String get libraryDisplay => 'Kitabxana';
@override String get personalMedia => 'Şəxsi media';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$az extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get none => 'Əl ilə seçilmiş';
@override String get noneDescription => 'Altyazıları heç vaxt özbaşına açma.';
@override String get defaultMode => 'Trek bayraqlarına əməl et';
@override String get defaultModeDescription => 'Hər altyazı trekində saxlanılan defolt və məcburi bayraqları istifadə et.';
@override String get always => 'Həmişə aktivdir';
@override String get alwaysDescription => 'Üstünlük verilən dildə altyazı treki mövcuddursa, onu aç.';
@override String get onlyForced => 'Yalnız məcburi altyazılar';
@override String get onlyForcedDescription => 'Yalnız məcburi işarələnmiş trekləri yüklə.';
@override String get smart => 'Xarici səs olduqda göstərilən';
@override String get smartDescription => 'Altyazıları yalnız səs başqa dildə olduqda aç.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$az extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'SDH olmayan altyazılara üstünlük ver';
@override String get preferSdh => 'SDH altyazılarına üstünlük ver';
@override String get onlySdh => 'Yalnız SDH altyazıları';
@override String get onlyNonSdh => 'Yalnız SDH olmayan altyazılar';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$az extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Məcburi olmayan altyazılara üstünlük ver';
@override String get preferForced => 'Məcburi altyazılara üstünlük ver';
@override String get onlyForced => 'Yalnız məcburi altyazılar';
@override String get onlyNonForced => 'Yalnız məcburi olmayan altyazılar';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$az extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get none => 'Heç vaxt';
@override String get moviesAndShows => 'Kinolar və TV şoular';
@override String get movies => 'Yalnız kinolar';
@override String get shows => 'Yalnız TV şoular';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$az extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$az._(TranslationsAz root) : this._root = root, super.internal(root);
final TranslationsAz _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'İstifadəçilər və tənqidçilər';
@override String get usersOnly => 'Yalnız istifadəçilər';
@override String get criticsOnly => 'Yalnız tənqidçilər';
@override String get nobody => 'Gizlədilib';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$az extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$az._(TranslationsAz root) : this._root = root, super.internal(root);
@@ -2786,6 +2947,7 @@ extension on TranslationsAz {
'auth.useBrowser' => 'Səyahətçini istifadə et',
'auth.or' => 'və ya',
'auth.connectToMediaBrowser' => ({required Object product}) => '${product}-a qoşul',
'auth.quickConnect' => 'Sürətli Qoşulma',
'auth.useQuickConnect' => 'Sürətli Qoşulmanı istifadə et',
'auth.quickConnectInstructions' => 'Jellyfin-də Sürətli Qoşulmanı açın və bu kodu daxil edin.',
'auth.quickConnectWaiting' => 'Təsdiq gözlənilir…',
@@ -2825,6 +2987,7 @@ extension on TranslationsAz {
'common.mute' => 'Səsi söndür',
'common.ok' => 'Oldu',
'common.off' => 'Söndürülüb',
'common.options' => 'Seçimlər',
'common.seasonNumber' => ({required Object number}) => 'Mövsüm ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Seriya ${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'Hissə ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsAz {
'settings.displayScale' => 'Ekran miqyası',
'settings.compact' => 'Sıx',
'settings.comfortable' => 'Rəhat',
'settings.gridSpacing' => 'Tor aralığı',
'settings.gridSpacingTight' => 'Sıx',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Geniş',
'settings.tvCornerSpotlightBackdrop' => 'Künc işıqlandırma fonu',
'settings.tvCornerSpotlightBackdropDescription' => 'Arxa fonu ekranı örtmək əvəzinə sağ üst küncdə göstər',
'settings.viewMode' => 'Baxış rejimi',
@@ -2946,16 +3113,14 @@ extension on TranslationsAz {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Avadanlıq kod açılması',
'settings.hardwareDecodingDescription' => 'Mümkün olduqda avadanlıq sürətləndirməsini istifadə et',
'settings.bufferSize' => 'Bufer həcmi',
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
'settings.bufferSizeAuto' => 'Avtomatik (Tövsiyə olunan)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB yaddaş əlçatandır. ${size}MB bufer oynatmaya təsir edə bilər.',
'settings.playbackBuffer' => 'Oxutma buferi',
'settings.playbackBufferAuto' => 'Avtomatik (tövsiyə olunur)',
'settings.playbackBufferLarge' => 'Böyük',
'settings.playbackBufferExtraLarge' => 'Çox böyük',
'settings.playbackBufferDescription' => 'Qeyri-sabit əlaqələrə qarşı daha çox buferləyir. Bufer ölçüsü ilə də məhdudlaşır.',
'settings.defaultQualityTitle' => 'Defolt keyfiyyət',
'settings.cellularQualityTitle' => 'Mobil şəbəkədə defolt keyfiyyət',
'settings.cellularQualitySameAsDefault' => 'Defolt keyfiyyətlə eyni',
'settings.musicQualityTitle' => 'Musiqi keyfiyyəti',
'settings.subtitleStyling' => 'Altyazı tənzimləmələri',
'settings.subtitleStylingDescription' => 'Altyazı görünüşünü özünüləşdirin',
@@ -2969,6 +3134,8 @@ extension on TranslationsAz {
'settings.rememberTrackSelectionsDescription' => 'Hər məzmun üçün səs və altyazı seçimlərini yadda saxla',
'settings.followServerTrackSelections' => 'Hər epizod üçün serverin trek seçimlərini istifadə et',
'settings.followServerTrackSelectionsDescription' => 'Epizod dəyişəndə cari seçimi köçürmək əvəzinə serverdə seçilmiş səs və altyazını tətbiq et',
'settings.resumeMusicOnLaunch' => 'Musiqi sessiyasını yadda saxla',
'settings.resumeMusicOnLaunchDescription' => 'Tətbiq açılanda son mahnını dayandırıldığı yerdə fasilədə aç',
'settings.showChapterMarkersOnTimeline' => 'Zaman çubuğunda hissə işarələrini göstər',
'settings.showChapterMarkersOnTimelineDescription' => 'Zaman çubuğunu hissə sərhədlərinə böl',
'settings.specialsOrdering' => 'Xüsusi bölmələr epizod sırasına görə',
@@ -3022,7 +3189,11 @@ extension on TranslationsAz {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Qısayol artıq ${action} üçün təyin edilib',
'settings.shortcutUpdated' => ({required Object action}) => '${action} üçün qısayol yeniləndi',
'settings.saveFailed' => 'Dəyişikliklər yadda saxlanıla bilmədi. Təzədən cəhd edin.',
'settings.autoSkip' => 'Avtomatik ötür',
'settings.autoPlayAndSkip' => 'Avtomatik oynat və ötür',
'settings.autoPlayNextEpisode' => 'Növbəti seriyanı avtomatik oynat',
'settings.autoPlayNextEpisodeDescription' => 'Bir seriya bitdikdə növbətisini avtomatik başlat',
'settings.playNextCountdown' => 'Növbəti seriya geri sayımı',
'settings.playNextCountdownImmediate' => 'Dərhal oynat',
'settings.autoSkipIntro' => 'Girişi avtomatik ötür',
'settings.autoSkipIntroDescription' => 'Bir neçə saniyədən sonra giriş işarələrini avtomatik ötür',
'settings.autoSkipCredits' => 'Titrləri avtomatik ötür',
@@ -3097,10 +3268,8 @@ extension on TranslationsAz {
'settings.dvConversionNativeDescription' => 'Daxili DV7-ni məcburi et',
'settings.dvConversionDv81Description' => 'Dolby Vision profile 8.1-ə çevrilməni məcburi et',
'settings.dvConversionHevcStripDescription' => 'Dolby Vision təbəqələrini sil və sadə HEVC kimi təqdim et',
'settings.demuxer' => 'Konteyner demukseri',
'settings.demuxerDescription' => 'Birbaşa oxudulan faylları hansı demukserin təhlil edəcəyi. Fayl düzgün işləmirsə, media3-ə keçin.',
'settings.demuxerFfmpeg' => 'FFmpeg (Tövsiyə olunan)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterleysinq',
'settings.deinterlaceDescription' => 'Sətirlərarası videodakı darama artefaktlarını aradan qaldır (yalnız mpv oynadıcısı)',
'settings.requireProfileSelectionOnOpen' => 'Açılışda profil soruş',
'settings.requireProfileSelectionOnOpenDescription' => 'Tətbiq hər dəfə açıldıqda profil seçimini göstər',
'settings.forceTvMode' => 'TV rejimini məcburi et',
@@ -3118,15 +3287,31 @@ extension on TranslationsAz {
'settings.showExploreTabDescription' => 'Plex Discover və qoşulmuş izləmə xidmətlərindəki məzmunla Kəşf et nişanını göstər',
'settings.liveTvDefaultFavorites' => 'Canlı TV-də sevimli kanalları defolt et',
'settings.liveTvDefaultFavoritesDescription' => 'Canlı TV açıldıqda yalnız sevimli kanalları göstər',
'settings.general' => 'Ümumi',
'settings.generalDescription' => 'Dil, başlanğıc və pəncərə davranışı',
'settings.languageAndRegion' => 'Dil və Region',
'settings.startup' => 'Başlanğıc',
'settings.display' => 'Ekran',
'settings.libraryAndCards' => 'Kitabxana və kartlar',
'settings.homeScreen' => 'Ana ekran',
'settings.navigation' => 'Naviqasiya',
'settings.window' => 'Pəncərə',
'settings.content' => 'Məzmun',
'settings.liveTv' => 'Canlı TV',
'settings.player' => 'Oynadıcı',
'settings.subtitlesAndConfig' => 'Altyazılar və konfiqurasiya',
'settings.videoAndDisplay' => 'Video və Ekran',
'settings.audio' => 'Səs',
'settings.quality' => 'Keyfiyyət',
'settings.subtitles' => 'Altyazılar',
'settings.seekAndTiming' => 'Sarğı və vaxt tənzimləməsi',
'settings.behavior' => 'Davranış',
'settings.gestures' => 'Jestlər',
'settings.gestureBrightnessSwipe' => 'Parlaqlıq sürüşdürməsi',
'settings.gestureBrightnessSwipeDescription' => 'Parlaqlığı tənzimləmək üçün sol kənarda yuxarı və ya aşağı sürüşdürün',
'settings.gestureVolumeSwipe' => 'Səs sürüşdürməsi',
'settings.gestureVolumeSwipeDescription' => 'Səsi tənzimləmək üçün sağ kənarda yuxarı və ya aşağı sürüşdürün',
'settings.gesturePinchToZoom' => 'Çimdiklə yaxınlaşdır',
'settings.gesturePinchToZoomDescription' => 'Yaxınlaşdırmaq və ya uzaqlaşdırmaq üçün videoda çimdik hərəkəti edin',
'settings.controls' => 'İdarəetmələr',
'settings.rememberPlayerChanges' => 'Pleyer dəyişikliklərini yadda saxla',
'settings.rememberPlayerChangesDescription' => 'Oxutma zamanı edilən dəyişikliklərin harada saxlanacağı və yenidən tətbiq ediləcəyi',
'settings.scopePlaybackSpeed' => 'Oxutma sürəti',
@@ -3264,6 +3449,8 @@ extension on TranslationsAz {
'fileInfo.has64bitOffsets' => '64-bit ofsetlər',
'fileInfo.protocol' => 'Protokol',
'fileInfo.mediaType' => 'Media növü',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Mənbə növü',
'fileInfo.optimizedVersion' => 'Optimallaşdırılmış versiya',
'fileInfo.optimizationTarget' => 'Optimallaşdırma hədəfi',
@@ -3288,8 +3475,6 @@ extension on TranslationsAz {
'fileInfo.flagOriginal' => 'Orijinal',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profil ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Baxıldı olaraq işarələ',
'mediaMenu.markAsUnwatched' => 'Baxılmadı olaraq işarələ',
'mediaMenu.removeFromContinueWatching' => 'İzləməyə davam et-dən sil',
@@ -3624,6 +3809,63 @@ extension on TranslationsAz {
'connections.signInAgain' => 'Yenidən daxil ol',
'connections.editMediaBrowserTitle' => ({required Object product}) => '${product} qoşulmasını düzəliş et',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => '${serverName} üçün URL-lər əlavə edin və ya silin. Plezy əlçatan olan ən aşağı gecikməli URL-i istifadə edəcək.',
'accountPreferences.sectionTitle' => 'Hesab tərcihləri',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => '${account} hesabında saxlanılan səs, altyazı və kitabxana seçimləri',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => '${count} hesabda saxlanılan səs, altyazı və kitabxana seçimləri',
'accountPreferences.pickAccount' => 'Hər hesab öz tərcihlərini saxlayır. Düzəliş etmək üçün birini seçin.',
'accountPreferences.storedOnAccount' => 'Bu seçimlər hesabın özündə saxlanılır, ona görə də ona daxil olan hər tətbiq onlardan istifadə edir — digər cihazlarınızdakı Plezy də daxil olmaqla.',
'accountPreferences.noAccounts' => 'Konfiqurasiya ediləcək hesab yoxdur',
'accountPreferences.noAccountsHint' => 'Plex-ə daxil olun və ya Jellyfin və ya Emby serverinə qoşulun; o hesabda saxlanılan tərcihlər burada görünəcək.',
'accountPreferences.unavailable' => 'Bu hesaba çatmaq olmur',
'accountPreferences.loadFailed' => 'Bu tərcihlər yüklənə bilmədi',
'accountPreferences.noPreference' => 'Tərcih yoxdur',
'accountPreferences.notSet' => 'Təyin edilməyib',
'accountPreferences.groups.audioAndSubtitles' => 'Səs və altyazılar',
'accountPreferences.groups.libraryDisplay' => 'Kitabxana',
'accountPreferences.groups.personalMedia' => 'Şəxsi media',
'accountPreferences.preferredAudioLanguage' => 'Üstünlük verilən səs dili',
'accountPreferences.autoSelectAudio' => 'Səsi dilə görə seç',
'accountPreferences.autoSelectAudioDescription' => 'Söndürüldükdə faylın defolt olaraq işarələdiyi səs treki istifadə olunur.',
'accountPreferences.preferredSubtitleLanguage' => 'Üstünlük verilən altyazı dili',
'accountPreferences.subtitleMode' => 'Altyazıları aç',
'accountPreferences.subtitleModes.none' => 'Əl ilə seçilmiş',
'accountPreferences.subtitleModes.noneDescription' => 'Altyazıları heç vaxt özbaşına açma.',
'accountPreferences.subtitleModes.defaultMode' => 'Trek bayraqlarına əməl et',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Hər altyazı trekində saxlanılan defolt və məcburi bayraqları istifadə et.',
'accountPreferences.subtitleModes.always' => 'Həmişə aktivdir',
'accountPreferences.subtitleModes.alwaysDescription' => 'Üstünlük verilən dildə altyazı treki mövcuddursa, onu aç.',
'accountPreferences.subtitleModes.onlyForced' => 'Yalnız məcburi altyazılar',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Yalnız məcburi işarələnmiş trekləri yüklə.',
'accountPreferences.subtitleModes.smart' => 'Xarici səs olduqda göstərilən',
'accountPreferences.subtitleModes.smartDescription' => 'Altyazıları yalnız səs başqa dildə olduqda aç.',
'accountPreferences.subtitleAccessibility' => 'SDH altyazıları',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'SDH olmayan altyazılara üstünlük ver',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'SDH altyazılarına üstünlük ver',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Yalnız SDH altyazıları',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Yalnız SDH olmayan altyazılar',
'accountPreferences.forcedSubtitles' => 'Məcburi altyazılar',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Məcburi olmayan altyazılara üstünlük ver',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Məcburi altyazılara üstünlük ver',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Yalnız məcburi altyazılar',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Yalnız məcburi olmayan altyazılar',
'accountPreferences.displayMissingEpisodes' => 'Çatışmayan seriyaları göstər',
'accountPreferences.displayMissingEpisodesDescription' => 'Serverin bildiyi, lakin faylı olmayan seriyaları siyahıya al.',
'accountPreferences.hidePlayedInLatest' => 'Baxılmış elementləri Son əlavə olunanlarda gizlət',
'accountPreferences.hidePlayedInLatestDescription' => 'Artıq baxdığınız elementləri serverin Son əlavə olunanlar sətirlərində göstərmə.',
'accountPreferences.displayCollectionsView' => 'Kolleksiyalar görünüşünü göstər',
'accountPreferences.displayCollectionsViewDescription' => 'Kitabxanalarınızla yanaşı serverin Kolleksiyalar görünüşünü də təklif et.',
'accountPreferences.rewatchingInNextUp' => 'Təkrar baxılan serialları Sırada saxla',
'accountPreferences.rewatchingInNextUpDescription' => 'Bir serialı bitirdikdən sonra yenidən başlasanız, Sırada serialı atmaq əvəzinə təkrar baxışı izləyir.',
'accountPreferences.watchedIndicator' => 'Baxıldı göstəriciləri',
'accountPreferences.watchedIndicatorOptions.none' => 'Heç vaxt',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Kinolar və TV şoular',
'accountPreferences.watchedIndicatorOptions.movies' => 'Yalnız kinolar',
'accountPreferences.watchedIndicatorOptions.shows' => 'Yalnız TV şoular',
'accountPreferences.mediaReviewsVisibility' => 'Reytinq və rəylər',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'İstifadəçilər və tənqidçilər',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Yalnız istifadəçilər',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Yalnız tənqidçilər',
'accountPreferences.mediaReviewsOptions.nobody' => 'Gizlədilib',
'discover.title' => 'Kəşf et',
'discover.noContentAvailable' => 'Məzmun əlçatan deyil',
'discover.addMediaToLibraries' => 'Kitabxanalarınıza bir az media əlavə edin',
@@ -3721,6 +3963,8 @@ extension on TranslationsAz {
'libraries.filterCategories.contentRating' => 'Məzmun reytinqi',
'libraries.filterCategories.tag' => 'Teq',
'libraries.filterCategories.unwatched' => 'Baxılmayıb',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Oynadılmayıb',
'libraries.filterCategories.favorites' => 'Sevimlilər',
'libraries.sortLabels.title' => 'Ad',
@@ -3802,8 +4046,6 @@ extension on TranslationsAz {
'explore.rows.watchlist' => 'İzləmə siyahısı',
'explore.rows.recommendedMovies' => 'Tövsiyə olunan kinolar',
'explore.rows.recommendedShows' => 'Tövsiyə olunan seriallar',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Trend kinolar',
'explore.rows.trendingShows' => 'Trend seriallar',
'explore.rows.popularMovies' => 'Məşhur kinolar',
@@ -3956,6 +4198,11 @@ extension on TranslationsAz {
'liveTv.unknownChannel' => 'Bilinməyən kanal',
'liveTv.live' => 'CANLI',
'liveTv.reloadGuide' => 'Bələdçini yenilə',
'liveTv.searchGuide' => 'Bələdçidə axtar',
'liveTv.searchHint' => 'Kanal və proqram axtar',
'liveTv.searchNoResults' => ({required Object query}) => '"${query}" üçün uyğunluq tapılmadı',
'liveTv.channelsSection' => 'Kanallar',
'liveTv.programsSection' => 'Proqramlar',
'liveTv.now' => 'İndi',
'liveTv.today' => 'Bu gün',
'liveTv.tomorrow' => 'Sabah',
@@ -4093,6 +4340,8 @@ extension on TranslationsAz {
'music.repeatAll' => 'Hamısını təkrarla',
'music.repeatOne' => 'Birini təkrarla',
'music.instantMixNoServer' => 'Ani miks üçün heç bir server mövcud deyil',
'music.instantMixFailed' => 'Anında qarışıq yüklənə bilmədi',
'music.instantMixEmpty' => 'Anında qarışıq heç bir mahnı qaytarmadı',
'music.noAudioUrl' => ({required Object track}) => '${track} üçün səs URL-i mövcud deyil',
'music.discography.singlesAndEps' => 'Single-lar və EP-lər',
'music.discography.live' => 'Canlı',
@@ -4228,6 +4477,8 @@ extension on TranslationsAz {
'downloads.syncRuleCleanupUnavailable' => 'Əlaqəli yükləmələr təhlükəsiz şəkildə müəyyən edilə bilmədi. Serverə yenidən qoşulub cəhd edin və ya qaydanı yükləmələri silmədən silin.',
'downloads.syncedNewEpisodes' => ({required Object title, required Object count}) => '${title} üçün ${count} yeni seriya eyniləşdirildi',
'downloads.activeSyncRules' => 'Eyniləşdirmə qaydaları',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Eyniləşdirmə qaydası yoxdur',
'downloads.manageSyncRule' => 'Eyniləşdirməni idarə et',
'downloads.editEpisodeCount' => 'Seriya sayı',
@@ -4316,8 +4567,6 @@ extension on TranslationsAz {
'companionRemote.pairing.availableDevices' => 'Əlçatan cihazlar',
'companionRemote.pairing.manualConnection' => 'Əl ilə qoşulma',
'companionRemote.pairing.cryptoInitFailed' => 'Təhlükəsiz qoşulma başladılarkən xəta. Əvvəlcə Plex-ə daxil olun.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Lütfən əsas cihaz ünvanını daxil edin',
'companionRemote.pairing.validationHostFormat' => 'Format IP:port şəklində olmalıdır',
'companionRemote.pairing.connectionTimedOut' => 'Qoşulma vaxtı bitdi.',
@@ -4577,6 +4826,8 @@ extension on TranslationsAz {
'seerr.statusBlocklisted' => 'Bloklanmış',
'seerr.couldNotReach' => ({required Object url, required Object error}) => '${url} ünvanına çatmaq olmadı: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => '${url} ünvanında Seerr instansiyası yoxdur (HTTP ${status})',
'seerr.invalidUrl' => 'https://seerr.example.com kimi server ünvanı daxil edin',
'seerr.quickConnectUnsupported' => 'Bu Seerr nüsxəsi Sürətli Qoşulmanı dəstəkləmir. Seerr 3.4 və ya daha yeni versiya tələb olunur.',
'seerr.notInitialized' => 'Bu Seerr instansiyasının ilkin quraşdırılması tamamlanmayıb',
'seerr.noPlexTokenForReauth' => 'Yenidən daxil olmaq üçün Plex tokeni mövcud deyil',
'seerr.noStoredCredentials' => 'Yenidən daxil olmaq üçün yadda saxlanmış giriş məlumatları mövcud deyil',
@@ -4587,6 +4838,7 @@ extension on TranslationsAz {
'seerr.sessionRejectedAfterReauth' => 'Yenidən daxil olduqdan sonra sessiya rədd edildi',
'services.title' => 'Xidmətlər',
'services.hubSubtitle' => 'İzləmə tərəqqisini eyniləşdirin və yeni başlıqlar sorğulayın.',
'services.integrations' => 'İnteqrasiyalar',
'services.notConnected' => 'Qoşulmayıb',
'services.connectedAs' => ({required Object username}) => '@${username} olaraq qoşuldu',
'services.scrobble' => 'Tərəqqini avtomatik izlə',
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsBg extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$bg dialog = _Translations$dialog$bg._(_root);
@override late final _Translations$profiles$bg profiles = _Translations$profiles$bg._(_root);
@override late final _Translations$connections$bg connections = _Translations$connections$bg._(_root);
@override late final _Translations$accountPreferences$bg accountPreferences = _Translations$accountPreferences$bg._(_root);
@override late final _Translations$discover$bg discover = _Translations$discover$bg._(_root);
@override late final _Translations$errors$bg errors = _Translations$errors$bg._(_root);
@override late final _Translations$libraries$bg libraries = _Translations$libraries$bg._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$bg extends Translations$auth$en {
@override String get useBrowser => 'Използвай браузър';
@override String get or => 'или';
@override String connectToMediaBrowser({required Object product}) => 'Свържи се с ${product}';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Използвай Quick Connect';
@override String get quickConnectInstructions => 'Отворете Quick Connect в Jellyfin и въведете този код.';
@override String get quickConnectWaiting => 'Изчакване на одобрение…';
@@ -165,6 +167,7 @@ class _Translations$common$bg extends Translations$common$en {
@override String get mute => 'Заглуши';
@override String get ok => 'OK';
@override String get off => 'Изкл.';
@override String get options => 'Опции';
@override String seasonNumber({required Object number}) => 'Сезон ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Епизод ${number} - ${title}';
@override String chapterNumber({required Object number}) => 'Глава ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$bg extends Translations$settings$en {
@override String get displayScale => 'Мащаб на дисплея';
@override String get compact => 'Компактна';
@override String get comfortable => 'Удобна';
@override String get gridSpacing => 'Разстояние на мрежата';
@override String get gridSpacingTight => 'Плътно';
@override String get gridSpacingNormal => 'Нормално';
@override String get gridSpacingSpacious => 'Просторно';
@override String get tvCornerSpotlightBackdrop => 'Фон с акцент в ъгъла';
@override String get tvCornerSpotlightBackdropDescription => 'Показвай акцентното изображение в горния десен ъгъл, вместо на целия екран';
@override String get viewMode => 'Режим на изглед';
@@ -292,16 +299,14 @@ class _Translations$settings$bg extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Хардуерно декодиране';
@override String get hardwareDecodingDescription => 'Използвай хардуерно ускорение, когато е налично';
@override String get bufferSize => 'Размер на буфера';
@override String bufferSizeMB({required Object size}) => '${size} MB';
@override String get bufferSizeAuto => 'Автоматично (препоръчително)';
@override String bufferSizeWarning({required Object heap, required Object size}) => 'Налична памет: ${heap} MB. Буфер от ${size} MB може да повлияе на възпроизвеждането.';
@override String get playbackBuffer => 'Буфер за възпроизвеждане';
@override String get playbackBufferAuto => 'Автоматично (препоръчително)';
@override String get playbackBufferLarge => 'Голям';
@override String get playbackBufferExtraLarge => 'Изключително голям';
@override String get playbackBufferDescription => 'Буферира повече при нестабилни връзки. Също ограничен от размера на буфера.';
@override String get defaultQualityTitle => 'Качество по подразбиране';
@override String get cellularQualityTitle => 'Качество по подразбиране при мобилни данни';
@override String get cellularQualitySameAsDefault => 'Същото като качеството по подразбиране';
@override String get musicQualityTitle => 'Качество на музиката';
@override String get subtitleStyling => 'Стил на субтитрите';
@override String get subtitleStylingDescription => 'Настройване на вида на субтитрите';
@@ -315,6 +320,8 @@ class _Translations$settings$bg extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Запомняй избора на аудиопътечка и субтитри за всяко заглавие';
@override String get followServerTrackSelections => 'Използвай избора на пътечки от сървъра за всеки епизод';
@override String get followServerTrackSelectionsDescription => 'При смяна на епизода прилагай избраните на сървъра аудио и субтитри, вместо да се пренася текущият избор';
@override String get resumeMusicOnLaunch => 'Запомняне на музикалната сесия';
@override String get resumeMusicOnLaunchDescription => 'При стартиране на приложението отваряй последната песен на пауза от мястото, докъдето е стигнала';
@override String get showChapterMarkersOnTimeline => 'Показвай маркери на глави върху времевата линия';
@override String get showChapterMarkersOnTimelineDescription => 'Разделяй времевата линия на сегменти по границите на главите';
@override String get specialsOrdering => 'Специални епизоди в реда на епизодите';
@@ -368,7 +375,11 @@ class _Translations$settings$bg extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Клавишната комбинация вече е назначена за ${action}';
@override String shortcutUpdated({required Object action}) => 'Клавишната комбинация е обновена за ${action}';
@override String get saveFailed => 'Промените не можаха да бъдат запазени. Опитайте отново.';
@override String get autoSkip => 'Автоматично прескачане';
@override String get autoPlayAndSkip => 'Автоматично пускане и прескачане';
@override String get autoPlayNextEpisode => 'Автоматично пускане на следващия епизод';
@override String get autoPlayNextEpisodeDescription => 'Пускай следващия епизод автоматично, когато текущият свърши';
@override String get playNextCountdown => 'Отброяване до следващия епизод';
@override String get playNextCountdownImmediate => 'Пусни веднага';
@override String get autoSkipIntro => 'Автоматично прескачане на интро';
@override String get autoSkipIntroDescription => 'Автоматично прескачай интро маркери след няколко секунди';
@override String get autoSkipCredits => 'Автоматично прескачане на финални надписи';
@@ -443,10 +454,8 @@ class _Translations$settings$bg extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Принуждава директно възпроизвеждане на DV7 и изключва повторния опит за преобразуване';
@override String get dvConversionDv81Description => 'Принуждава директно преобразуване на RPU към Dolby Vision Profile 8.1';
@override String get dvConversionHevcStripDescription => 'Премахва слоевете Dolby Vision RPU/EL и подава обикновен HEVC поток';
@override String get demuxer => 'Демултиплексор на контейнера';
@override String get demuxerDescription => 'Кой демултиплексор анализира файловете при директно възпроизвеждане. Превключете на media3, ако даден файл създава проблеми.';
@override String get demuxerFfmpeg => 'FFmpeg (препоръчително)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Деинтерлейсинг';
@override String get deinterlaceDescription => 'Премахва гребеновидните артефакти от интерлейсирано видео (само за mpv плейъра)';
@override String get requireProfileSelectionOnOpen => 'Питай за профил при отваряне на приложението';
@override String get requireProfileSelectionOnOpenDescription => 'Показвай избор на профил всеки път при отваряне на приложението';
@override String get forceTvMode => 'Принуди TV режим';
@@ -464,15 +473,31 @@ class _Translations$settings$bg extends Translations$settings$en {
@override String get showExploreTabDescription => 'Показва раздела „Открий“ със съдържание от Plex Discover и свързаните тракери';
@override String get liveTvDefaultFavorites => 'По подразбиране към любими канали';
@override String get liveTvDefaultFavoritesDescription => 'Показвай само любими канали при отваряне на телевизия на живо';
@override String get general => 'Общи';
@override String get generalDescription => 'Език, стартиране и поведение на прозореца';
@override String get languageAndRegion => 'Език и регион';
@override String get startup => 'Стартиране';
@override String get display => 'Дисплей';
@override String get libraryAndCards => 'Библиотека и карти';
@override String get homeScreen => 'Начален екран';
@override String get navigation => 'Навигация';
@override String get window => 'Прозорец';
@override String get content => 'Съдържание';
@override String get liveTv => 'Телевизия на живо';
@override String get player => 'Плейър';
@override String get subtitlesAndConfig => 'Субтитри и конфигурация';
@override String get videoAndDisplay => 'Видео и дисплей';
@override String get audio => 'Аудио';
@override String get quality => 'Качество';
@override String get subtitles => 'Субтитри';
@override String get seekAndTiming => 'Търсене и време';
@override String get behavior => 'Поведение';
@override String get gestures => 'Жестове';
@override String get gestureBrightnessSwipe => 'Плъзгане за яркост';
@override String get gestureBrightnessSwipeDescription => 'Плъзни нагоре или надолу по левия ръб, за да регулираш яркостта';
@override String get gestureVolumeSwipe => 'Плъзгане за сила на звука';
@override String get gestureVolumeSwipeDescription => 'Плъзни нагоре или надолу по десния ръб, за да регулираш силата на звука';
@override String get gesturePinchToZoom => 'Стискане за мащабиране';
@override String get gesturePinchToZoomDescription => 'Стисни видеото, за да увеличиш или намалиш мащаба';
@override String get controls => 'Контроли';
@override String get rememberPlayerChanges => 'Запомняне на промените в плейъра';
@override String get rememberPlayerChangesDescription => 'Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане';
@override String get scopePlaybackSpeed => 'Скорост на възпроизвеждане';
@@ -1086,6 +1111,49 @@ class _Translations$connections$bg extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Добавете или премахнете URL адреси за ${serverName}. Plezy ще използва достъпния URL адрес с най-ниска латентност.';
}
// Path: accountPreferences
class _Translations$accountPreferences$bg extends Translations$accountPreferences$en {
_Translations$accountPreferences$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Предпочитания на акаунта';
@override String hubSubtitleSingle({required Object account}) => 'Опции за аудио, субтитри и библиотека, запазени на ${account}';
@override String hubSubtitleMultiple({required Object count}) => 'Опции за аудио, субтитри и библиотека, запазени на ${count} акаунта';
@override String get pickAccount => 'Всеки акаунт съхранява собствени предпочитания. Изберете кой да редактирате.';
@override String get storedOnAccount => 'Тези опции се запазват на самия акаунт, така че всяко приложение, вписано в него, ги използва — включително Plezy на другите ви устройства.';
@override String get noAccounts => 'Няма акаунти за настройка';
@override String get noAccountsHint => 'Влезте в Plex или свържете Jellyfin или Emby сървър и предпочитанията, запазени на този акаунт, ще се появят тук.';
@override String get unavailable => 'Акаунтът не може да бъде достигнат';
@override String get loadFailed => 'Предпочитанията не можаха да бъдат заредени';
@override String get noPreference => 'Без предпочитание';
@override String get notSet => 'Не е зададено';
@override late final _Translations$accountPreferences$groups$bg groups = _Translations$accountPreferences$groups$bg._(_root);
@override String get preferredAudioLanguage => 'Предпочитан аудио език';
@override String get autoSelectAudio => 'Избирай аудио по език';
@override String get autoSelectAudioDescription => 'При изключено се запазва аудиопътечката, която файлът маркира като подразбираща се.';
@override String get preferredSubtitleLanguage => 'Предпочитан език за субтитри';
@override String get subtitleMode => 'Включване на субтитри';
@override late final _Translations$accountPreferences$subtitleModes$bg subtitleModes = _Translations$accountPreferences$subtitleModes$bg._(_root);
@override String get subtitleAccessibility => 'SDH субтитри';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$bg subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$bg._(_root);
@override String get forcedSubtitles => 'Принудителни субтитри';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$bg forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$bg._(_root);
@override String get displayMissingEpisodes => 'Показвай липсващи епизоди';
@override String get displayMissingEpisodesDescription => 'Изброявай епизоди, за които сървърът знае, но няма файл.';
@override String get hidePlayedInLatest => 'Скривай изгледаните елементи в „Последни“';
@override String get hidePlayedInLatestDescription => 'Не включвай вече изгледаните елементи в редовете „Последни“ на сървъра.';
@override String get displayCollectionsView => 'Показвай изгледа „Колекции“';
@override String get displayCollectionsViewDescription => 'Предлагай изгледа „Колекции“ на сървъра редом с библиотеките ви.';
@override String get rewatchingInNextUp => 'Запазвай повторно гледаните сериали в „Следва“';
@override String get rewatchingInNextUpDescription => 'Когато завършите сериал и го пуснете отново, „Следва“ проследява повторното гледане, вместо да премахва сериала.';
@override String get watchedIndicator => 'Индикатори за изгледано';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$bg watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$bg._(_root);
@override String get mediaReviewsVisibility => 'Оценки и ревюта';
@override late final _Translations$accountPreferences$mediaReviewsOptions$bg mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$bg._(_root);
}
// Path: discover
class _Translations$discover$bg extends Translations$discover$en {
_Translations$discover$bg._(TranslationsBg root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$bg extends Translations$liveTv$en {
@override String get unknownChannel => 'Неизвестен канал';
@override String get live => 'НА ЖИВО';
@override String get reloadGuide => 'Презареди ТВ програмата';
@override String get searchGuide => 'Търсене в програмата';
@override String get searchHint => 'Търсене на канали и предавания';
@override String searchNoResults({required Object query}) => 'Няма съвпадения за "${query}"';
@override String get channelsSection => 'Канали';
@override String get programsSection => 'Предавания';
@override String get now => 'Сега';
@override String get today => 'Днес';
@override String get tomorrow => 'Утре';
@@ -1545,6 +1618,8 @@ class _Translations$music$bg extends Translations$music$en {
@override String get repeatAll => 'Повтаряне на всички';
@override String get repeatOne => 'Повтаряне на една';
@override String get instantMixNoServer => 'Няма наличен сървър за незабавен микс';
@override String get instantMixFailed => 'Мигновеният микс не можа да бъде зареден';
@override String get instantMixEmpty => 'Мигновеният микс не върна песни';
@override String noAudioUrl({required Object track}) => 'Няма наличен URL за аудиото на ${track}';
@override late final _Translations$music$discography$bg discography = _Translations$music$discography$bg._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$bg extends Translations$seerr$en {
@override String get statusBlocklisted => 'В списъка с блокирани';
@override String couldNotReach({required Object url, required Object error}) => 'Неуспешна връзка с ${url}: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'На ${url} няма инстанция на Seerr (HTTP ${status})';
@override String get invalidUrl => 'Въведете адрес на сървър като https://seerr.example.com';
@override String get quickConnectUnsupported => 'Тази Seerr инстанция не поддържа Quick Connect. Изисква се Seerr 3.4 или по-нова версия.';
@override String get notInitialized => 'Тази инстанция на Seerr не е завършила първоначалната настройка';
@override String get noPlexTokenForReauth => 'Няма наличен Plex токен за повторен вход';
@override String get noStoredCredentials => 'Няма запазени данни за повторен вход';
@@ -2066,6 +2143,7 @@ class _Translations$services$bg extends Translations$services$en {
// Translations
@override String get title => 'Услуги';
@override String get hubSubtitle => 'Синхронизирай прогреса на гледане и заявявай нови заглавия.';
@override String get integrations => 'Интеграции';
@override String get notConnected => 'Няма връзка';
@override String connectedAs({required Object username}) => 'Свързан като @${username}';
@override String get scrobble => 'Проследявай прогреса автоматично';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$bg extends Translations$videoControl
@override String unknown({required Object error}) => 'Възникна грешка: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$bg extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Аудио и субтитри';
@override String get libraryDisplay => 'Библиотека';
@override String get personalMedia => 'Лична медия';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$bg extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get none => 'Ръчно избрани';
@override String get noneDescription => 'Никога не включвай субтитри автоматично.';
@override String get defaultMode => 'Следвай флаговете на пътечката';
@override String get defaultModeDescription => 'Използвай флаговете по подразбиране и принудителните, съхранени на всяка пътечка със субтитри.';
@override String get always => 'Винаги включени';
@override String get alwaysDescription => 'Включвай пътечка със субтитри на предпочитания език, когато има такава.';
@override String get onlyForced => 'Само принудителни субтитри';
@override String get onlyForcedDescription => 'Зареждай само пътечките, маркирани като принудителни.';
@override String get smart => 'Показване при чуждоезично аудио';
@override String get smartDescription => 'Включвай субтитри само когато аудиото е на друг език.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$bg extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Предпочитай субтитри без SDH';
@override String get preferSdh => 'Предпочитай SDH субтитри';
@override String get onlySdh => 'Само SDH субтитри';
@override String get onlyNonSdh => 'Само субтитри без SDH';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$bg extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Предпочитай субтитри без принудителни';
@override String get preferForced => 'Предпочитай принудителни субтитри';
@override String get onlyForced => 'Само принудителни субтитри';
@override String get onlyNonForced => 'Само субтитри без принудителни';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$bg extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get none => 'Никога';
@override String get moviesAndShows => 'Филми и ТВ сериали';
@override String get movies => 'Само филми';
@override String get shows => 'Само ТВ сериали';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$bg extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$bg._(TranslationsBg root) : this._root = root, super.internal(root);
final TranslationsBg _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Потребители и критици';
@override String get usersOnly => 'Само потребители';
@override String get criticsOnly => 'Само критици';
@override String get nobody => 'Скрити';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$bg extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$bg._(TranslationsBg root) : this._root = root, super.internal(root);
@@ -2786,6 +2947,7 @@ extension on TranslationsBg {
'auth.useBrowser' => 'Използвай браузър',
'auth.or' => 'или',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Свържи се с ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Използвай Quick Connect',
'auth.quickConnectInstructions' => 'Отворете Quick Connect в Jellyfin и въведете този код.',
'auth.quickConnectWaiting' => 'Изчакване на одобрение…',
@@ -2825,6 +2987,7 @@ extension on TranslationsBg {
'common.mute' => 'Заглуши',
'common.ok' => 'OK',
'common.off' => 'Изкл.',
'common.options' => 'Опции',
'common.seasonNumber' => ({required Object number}) => 'Сезон ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Епизод ${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'Глава ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsBg {
'settings.displayScale' => 'Мащаб на дисплея',
'settings.compact' => 'Компактна',
'settings.comfortable' => 'Удобна',
'settings.gridSpacing' => 'Разстояние на мрежата',
'settings.gridSpacingTight' => 'Плътно',
'settings.gridSpacingNormal' => 'Нормално',
'settings.gridSpacingSpacious' => 'Просторно',
'settings.tvCornerSpotlightBackdrop' => 'Фон с акцент в ъгъла',
'settings.tvCornerSpotlightBackdropDescription' => 'Показвай акцентното изображение в горния десен ъгъл, вместо на целия екран',
'settings.viewMode' => 'Режим на изглед',
@@ -2946,16 +3113,14 @@ extension on TranslationsBg {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Хардуерно декодиране',
'settings.hardwareDecodingDescription' => 'Използвай хардуерно ускорение, когато е налично',
'settings.bufferSize' => 'Размер на буфера',
'settings.bufferSizeMB' => ({required Object size}) => '${size} MB',
'settings.bufferSizeAuto' => 'Автоматично (препоръчително)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => 'Налична памет: ${heap} MB. Буфер от ${size} MB може да повлияе на възпроизвеждането.',
'settings.playbackBuffer' => 'Буфер за възпроизвеждане',
'settings.playbackBufferAuto' => 'Автоматично (препоръчително)',
'settings.playbackBufferLarge' => 'Голям',
'settings.playbackBufferExtraLarge' => 'Изключително голям',
'settings.playbackBufferDescription' => 'Буферира повече при нестабилни връзки. Също ограничен от размера на буфера.',
'settings.defaultQualityTitle' => 'Качество по подразбиране',
'settings.cellularQualityTitle' => 'Качество по подразбиране при мобилни данни',
'settings.cellularQualitySameAsDefault' => 'Същото като качеството по подразбиране',
'settings.musicQualityTitle' => 'Качество на музиката',
'settings.subtitleStyling' => 'Стил на субтитрите',
'settings.subtitleStylingDescription' => 'Настройване на вида на субтитрите',
@@ -2969,6 +3134,8 @@ extension on TranslationsBg {
'settings.rememberTrackSelectionsDescription' => 'Запомняй избора на аудиопътечка и субтитри за всяко заглавие',
'settings.followServerTrackSelections' => 'Използвай избора на пътечки от сървъра за всеки епизод',
'settings.followServerTrackSelectionsDescription' => 'При смяна на епизода прилагай избраните на сървъра аудио и субтитри, вместо да се пренася текущият избор',
'settings.resumeMusicOnLaunch' => 'Запомняне на музикалната сесия',
'settings.resumeMusicOnLaunchDescription' => 'При стартиране на приложението отваряй последната песен на пауза от мястото, докъдето е стигнала',
'settings.showChapterMarkersOnTimeline' => 'Показвай маркери на глави върху времевата линия',
'settings.showChapterMarkersOnTimelineDescription' => 'Разделяй времевата линия на сегменти по границите на главите',
'settings.specialsOrdering' => 'Специални епизоди в реда на епизодите',
@@ -3022,7 +3189,11 @@ extension on TranslationsBg {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Клавишната комбинация вече е назначена за ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Клавишната комбинация е обновена за ${action}',
'settings.saveFailed' => 'Промените не можаха да бъдат запазени. Опитайте отново.',
'settings.autoSkip' => 'Автоматично прескачане',
'settings.autoPlayAndSkip' => 'Автоматично пускане и прескачане',
'settings.autoPlayNextEpisode' => 'Автоматично пускане на следващия епизод',
'settings.autoPlayNextEpisodeDescription' => 'Пускай следващия епизод автоматично, когато текущият свърши',
'settings.playNextCountdown' => 'Отброяване до следващия епизод',
'settings.playNextCountdownImmediate' => 'Пусни веднага',
'settings.autoSkipIntro' => 'Автоматично прескачане на интро',
'settings.autoSkipIntroDescription' => 'Автоматично прескачай интро маркери след няколко секунди',
'settings.autoSkipCredits' => 'Автоматично прескачане на финални надписи',
@@ -3097,10 +3268,8 @@ extension on TranslationsBg {
'settings.dvConversionNativeDescription' => 'Принуждава директно възпроизвеждане на DV7 и изключва повторния опит за преобразуване',
'settings.dvConversionDv81Description' => 'Принуждава директно преобразуване на RPU към Dolby Vision Profile 8.1',
'settings.dvConversionHevcStripDescription' => 'Премахва слоевете Dolby Vision RPU/EL и подава обикновен HEVC поток',
'settings.demuxer' => 'Демултиплексор на контейнера',
'settings.demuxerDescription' => 'Кой демултиплексор анализира файловете при директно възпроизвеждане. Превключете на media3, ако даден файл създава проблеми.',
'settings.demuxerFfmpeg' => 'FFmpeg (препоръчително)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Деинтерлейсинг',
'settings.deinterlaceDescription' => 'Премахва гребеновидните артефакти от интерлейсирано видео (само за mpv плейъра)',
'settings.requireProfileSelectionOnOpen' => 'Питай за профил при отваряне на приложението',
'settings.requireProfileSelectionOnOpenDescription' => 'Показвай избор на профил всеки път при отваряне на приложението',
'settings.forceTvMode' => 'Принуди TV режим',
@@ -3118,15 +3287,31 @@ extension on TranslationsBg {
'settings.showExploreTabDescription' => 'Показва раздела „Открий“ със съдържание от Plex Discover и свързаните тракери',
'settings.liveTvDefaultFavorites' => 'По подразбиране към любими канали',
'settings.liveTvDefaultFavoritesDescription' => 'Показвай само любими канали при отваряне на телевизия на живо',
'settings.general' => 'Общи',
'settings.generalDescription' => 'Език, стартиране и поведение на прозореца',
'settings.languageAndRegion' => 'Език и регион',
'settings.startup' => 'Стартиране',
'settings.display' => 'Дисплей',
'settings.libraryAndCards' => 'Библиотека и карти',
'settings.homeScreen' => 'Начален екран',
'settings.navigation' => 'Навигация',
'settings.window' => 'Прозорец',
'settings.content' => 'Съдържание',
'settings.liveTv' => 'Телевизия на живо',
'settings.player' => 'Плейър',
'settings.subtitlesAndConfig' => 'Субтитри и конфигурация',
'settings.videoAndDisplay' => 'Видео и дисплей',
'settings.audio' => 'Аудио',
'settings.quality' => 'Качество',
'settings.subtitles' => 'Субтитри',
'settings.seekAndTiming' => 'Търсене и време',
'settings.behavior' => 'Поведение',
'settings.gestures' => 'Жестове',
'settings.gestureBrightnessSwipe' => 'Плъзгане за яркост',
'settings.gestureBrightnessSwipeDescription' => 'Плъзни нагоре или надолу по левия ръб, за да регулираш яркостта',
'settings.gestureVolumeSwipe' => 'Плъзгане за сила на звука',
'settings.gestureVolumeSwipeDescription' => 'Плъзни нагоре или надолу по десния ръб, за да регулираш силата на звука',
'settings.gesturePinchToZoom' => 'Стискане за мащабиране',
'settings.gesturePinchToZoomDescription' => 'Стисни видеото, за да увеличиш или намалиш мащаба',
'settings.controls' => 'Контроли',
'settings.rememberPlayerChanges' => 'Запомняне на промените в плейъра',
'settings.rememberPlayerChangesDescription' => 'Къде се записва и откъде се прилага отново промяна, направена по време на възпроизвеждане',
'settings.scopePlaybackSpeed' => 'Скорост на възпроизвеждане',
@@ -3264,6 +3449,8 @@ extension on TranslationsBg {
'fileInfo.has64bitOffsets' => '64-битови отмествания',
'fileInfo.protocol' => 'Протокол',
'fileInfo.mediaType' => 'Тип медия',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Вид източник',
'fileInfo.optimizedVersion' => 'Оптимизирана версия',
'fileInfo.optimizationTarget' => 'Цел на оптимизацията',
@@ -3288,8 +3475,6 @@ extension on TranslationsBg {
'fileInfo.flagOriginal' => 'Оригинал',
'fileInfo.channelsMono' => 'Моно',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Профил ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Маркирай като гледано',
'mediaMenu.markAsUnwatched' => 'Маркирай като негледано',
'mediaMenu.removeFromContinueWatching' => 'Премахни от продължаване на гледането',
@@ -3624,6 +3809,63 @@ extension on TranslationsBg {
'connections.signInAgain' => 'Влез отново',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Редактирай връзката с ${product}',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Добавете или премахнете URL адреси за ${serverName}. Plezy ще използва достъпния URL адрес с най-ниска латентност.',
'accountPreferences.sectionTitle' => 'Предпочитания на акаунта',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Опции за аудио, субтитри и библиотека, запазени на ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Опции за аудио, субтитри и библиотека, запазени на ${count} акаунта',
'accountPreferences.pickAccount' => 'Всеки акаунт съхранява собствени предпочитания. Изберете кой да редактирате.',
'accountPreferences.storedOnAccount' => 'Тези опции се запазват на самия акаунт, така че всяко приложение, вписано в него, ги използва — включително Plezy на другите ви устройства.',
'accountPreferences.noAccounts' => 'Няма акаунти за настройка',
'accountPreferences.noAccountsHint' => 'Влезте в Plex или свържете Jellyfin или Emby сървър и предпочитанията, запазени на този акаунт, ще се появят тук.',
'accountPreferences.unavailable' => 'Акаунтът не може да бъде достигнат',
'accountPreferences.loadFailed' => 'Предпочитанията не можаха да бъдат заредени',
'accountPreferences.noPreference' => 'Без предпочитание',
'accountPreferences.notSet' => 'Не е зададено',
'accountPreferences.groups.audioAndSubtitles' => 'Аудио и субтитри',
'accountPreferences.groups.libraryDisplay' => 'Библиотека',
'accountPreferences.groups.personalMedia' => 'Лична медия',
'accountPreferences.preferredAudioLanguage' => 'Предпочитан аудио език',
'accountPreferences.autoSelectAudio' => 'Избирай аудио по език',
'accountPreferences.autoSelectAudioDescription' => 'При изключено се запазва аудиопътечката, която файлът маркира като подразбираща се.',
'accountPreferences.preferredSubtitleLanguage' => 'Предпочитан език за субтитри',
'accountPreferences.subtitleMode' => 'Включване на субтитри',
'accountPreferences.subtitleModes.none' => 'Ръчно избрани',
'accountPreferences.subtitleModes.noneDescription' => 'Никога не включвай субтитри автоматично.',
'accountPreferences.subtitleModes.defaultMode' => 'Следвай флаговете на пътечката',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Използвай флаговете по подразбиране и принудителните, съхранени на всяка пътечка със субтитри.',
'accountPreferences.subtitleModes.always' => 'Винаги включени',
'accountPreferences.subtitleModes.alwaysDescription' => 'Включвай пътечка със субтитри на предпочитания език, когато има такава.',
'accountPreferences.subtitleModes.onlyForced' => 'Само принудителни субтитри',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Зареждай само пътечките, маркирани като принудителни.',
'accountPreferences.subtitleModes.smart' => 'Показване при чуждоезично аудио',
'accountPreferences.subtitleModes.smartDescription' => 'Включвай субтитри само когато аудиото е на друг език.',
'accountPreferences.subtitleAccessibility' => 'SDH субтитри',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Предпочитай субтитри без SDH',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Предпочитай SDH субтитри',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Само SDH субтитри',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Само субтитри без SDH',
'accountPreferences.forcedSubtitles' => 'Принудителни субтитри',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Предпочитай субтитри без принудителни',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Предпочитай принудителни субтитри',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Само принудителни субтитри',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Само субтитри без принудителни',
'accountPreferences.displayMissingEpisodes' => 'Показвай липсващи епизоди',
'accountPreferences.displayMissingEpisodesDescription' => 'Изброявай епизоди, за които сървърът знае, но няма файл.',
'accountPreferences.hidePlayedInLatest' => 'Скривай изгледаните елементи в „Последни“',
'accountPreferences.hidePlayedInLatestDescription' => 'Не включвай вече изгледаните елементи в редовете „Последни“ на сървъра.',
'accountPreferences.displayCollectionsView' => 'Показвай изгледа „Колекции“',
'accountPreferences.displayCollectionsViewDescription' => 'Предлагай изгледа „Колекции“ на сървъра редом с библиотеките ви.',
'accountPreferences.rewatchingInNextUp' => 'Запазвай повторно гледаните сериали в „Следва“',
'accountPreferences.rewatchingInNextUpDescription' => 'Когато завършите сериал и го пуснете отново, „Следва“ проследява повторното гледане, вместо да премахва сериала.',
'accountPreferences.watchedIndicator' => 'Индикатори за изгледано',
'accountPreferences.watchedIndicatorOptions.none' => 'Никога',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Филми и ТВ сериали',
'accountPreferences.watchedIndicatorOptions.movies' => 'Само филми',
'accountPreferences.watchedIndicatorOptions.shows' => 'Само ТВ сериали',
'accountPreferences.mediaReviewsVisibility' => 'Оценки и ревюта',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Потребители и критици',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Само потребители',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Само критици',
'accountPreferences.mediaReviewsOptions.nobody' => 'Скрити',
'discover.title' => 'Открий',
'discover.noContentAvailable' => 'Няма налично съдържание',
'discover.addMediaToLibraries' => 'Добавете медия към библиотеките си',
@@ -3721,6 +3963,8 @@ extension on TranslationsBg {
'libraries.filterCategories.contentRating' => 'Възрастов рейтинг',
'libraries.filterCategories.tag' => 'Таг',
'libraries.filterCategories.unwatched' => 'Негледани',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Непускани',
'libraries.filterCategories.favorites' => 'Любими',
'libraries.sortLabels.title' => 'Заглавие',
@@ -3802,8 +4046,6 @@ extension on TranslationsBg {
'explore.rows.watchlist' => 'Списък за гледане',
'explore.rows.recommendedMovies' => 'Препоръчани филми',
'explore.rows.recommendedShows' => 'Препоръчани сериали',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Набиращи популярност филми',
'explore.rows.trendingShows' => 'Набиращи популярност сериали',
'explore.rows.popularMovies' => 'Популярни филми',
@@ -3956,6 +4198,11 @@ extension on TranslationsBg {
'liveTv.unknownChannel' => 'Неизвестен канал',
'liveTv.live' => 'НА ЖИВО',
'liveTv.reloadGuide' => 'Презареди ТВ програмата',
'liveTv.searchGuide' => 'Търсене в програмата',
'liveTv.searchHint' => 'Търсене на канали и предавания',
'liveTv.searchNoResults' => ({required Object query}) => 'Няма съвпадения за "${query}"',
'liveTv.channelsSection' => 'Канали',
'liveTv.programsSection' => 'Предавания',
'liveTv.now' => 'Сега',
'liveTv.today' => 'Днес',
'liveTv.tomorrow' => 'Утре',
@@ -4093,6 +4340,8 @@ extension on TranslationsBg {
'music.repeatAll' => 'Повтаряне на всички',
'music.repeatOne' => 'Повтаряне на една',
'music.instantMixNoServer' => 'Няма наличен сървър за незабавен микс',
'music.instantMixFailed' => 'Мигновеният микс не можа да бъде зареден',
'music.instantMixEmpty' => 'Мигновеният микс не върна песни',
'music.noAudioUrl' => ({required Object track}) => 'Няма наличен URL за аудиото на ${track}',
'music.discography.singlesAndEps' => 'Сингли и EP',
'music.discography.live' => 'Концертни',
@@ -4228,6 +4477,8 @@ extension on TranslationsBg {
'downloads.syncRuleCleanupUnavailable' => 'Свързаните изтегляния не можаха да бъдат идентифицирани безопасно. Свържете се отново със сървъра и опитайте отново, или премахнете правилото, без да изтривате изтеглянията.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => 'Синхронизирани са ${count} нови епизода за ${title}',
'downloads.activeSyncRules' => 'Правила за синхронизация',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Няма правила за синхронизация',
'downloads.manageSyncRule' => 'Управление на синхронизацията',
'downloads.editEpisodeCount' => 'Брой епизоди',
@@ -4316,8 +4567,6 @@ extension on TranslationsBg {
'companionRemote.pairing.availableDevices' => 'Налични устройства',
'companionRemote.pairing.manualConnection' => 'Ръчно свързване',
'companionRemote.pairing.cryptoInitFailed' => 'Не може да се стартира защитена връзка. Първо влезте в Plex.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Моля, въведете адрес на хоста',
'companionRemote.pairing.validationHostFormat' => 'Форматът трябва да е IP:port (напр. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Връзката изтече. Използвайте една и съща мрежа на двете устройства.',
@@ -4577,6 +4826,8 @@ extension on TranslationsBg {
'seerr.statusBlocklisted' => 'В списъка с блокирани',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Неуспешна връзка с ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'На ${url} няма инстанция на Seerr (HTTP ${status})',
'seerr.invalidUrl' => 'Въведете адрес на сървър като https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Тази Seerr инстанция не поддържа Quick Connect. Изисква се Seerr 3.4 или по-нова версия.',
'seerr.notInitialized' => 'Тази инстанция на Seerr не е завършила първоначалната настройка',
'seerr.noPlexTokenForReauth' => 'Няма наличен Plex токен за повторен вход',
'seerr.noStoredCredentials' => 'Няма запазени данни за повторен вход',
@@ -4587,6 +4838,7 @@ extension on TranslationsBg {
'seerr.sessionRejectedAfterReauth' => 'Сесията беше отхвърлена след повторния вход',
'services.title' => 'Услуги',
'services.hubSubtitle' => 'Синхронизирай прогреса на гледане и заявявай нови заглавия.',
'services.integrations' => 'Интеграции',
'services.notConnected' => 'Няма връзка',
'services.connectedAs' => ({required Object username}) => 'Свързан като @${username}',
'services.scrobble' => 'Проследявай прогреса автоматично',
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsDa extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$da dialog = _Translations$dialog$da._(_root);
@override late final _Translations$profiles$da profiles = _Translations$profiles$da._(_root);
@override late final _Translations$connections$da connections = _Translations$connections$da._(_root);
@override late final _Translations$accountPreferences$da accountPreferences = _Translations$accountPreferences$da._(_root);
@override late final _Translations$discover$da discover = _Translations$discover$da._(_root);
@override late final _Translations$errors$da errors = _Translations$errors$da._(_root);
@override late final _Translations$libraries$da libraries = _Translations$libraries$da._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$da extends Translations$auth$en {
@override String get useBrowser => 'Brug browseren';
@override String get or => 'eller';
@override String connectToMediaBrowser({required Object product}) => 'Opret forbindelse til ${product}';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Brug Quick Connect';
@override String get quickConnectInstructions => 'Åbn Quick Connect i Jellyfin, og indtast denne kode.';
@override String get quickConnectWaiting => 'Venter på godkendelse…';
@@ -165,6 +167,7 @@ class _Translations$common$da extends Translations$common$en {
@override String get mute => 'Lydløs';
@override String get ok => 'OK';
@override String get off => 'Fra';
@override String get options => 'Valgmuligheder';
@override String seasonNumber({required Object number}) => 'Sæson ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Episode ${number} – ${title}';
@override String chapterNumber({required Object number}) => 'Kapitel ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$da extends Translations$settings$en {
@override String get displayScale => 'Skalering';
@override String get compact => 'Kompakt';
@override String get comfortable => 'Komfortabel';
@override String get gridSpacing => 'Gitterafstand';
@override String get gridSpacingTight => 'Tæt';
@override String get gridSpacingNormal => 'Normal';
@override String get gridSpacingSpacious => 'Rummelig';
@override String get tvCornerSpotlightBackdrop => 'Fremhævet baggrundsbillede i hjørnet';
@override String get tvCornerSpotlightBackdropDescription => 'Vis fremhævet grafik i øverste højre hjørne i stedet for at fylde skærmen';
@override String get viewMode => 'Visningstilstand';
@@ -292,16 +299,14 @@ class _Translations$settings$da extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Hardwaredekodning';
@override String get hardwareDecodingDescription => 'Brug hardwareacceleration, når den er tilgængelig';
@override String get bufferSize => 'Bufferstørrelse';
@override String bufferSizeMB({required Object size}) => '${size}MB';
@override String get bufferSizeAuto => 'Automatisk (anbefalet)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} MB hukommelse tilgængelig. En buffer på ${size} MB kan påvirke afspilningen.';
@override String get playbackBuffer => 'Afspilningsbuffer';
@override String get playbackBufferAuto => 'Auto (anbefalet)';
@override String get playbackBufferLarge => 'Stor';
@override String get playbackBufferExtraLarge => 'Ekstra stor';
@override String get playbackBufferDescription => 'Bufrer mere mod ustabile forbindelser. Begrænses også af bufferstørrelsen.';
@override String get defaultQualityTitle => 'Standardkvalitet';
@override String get cellularQualityTitle => 'Standardkvalitet på mobildata';
@override String get cellularQualitySameAsDefault => 'Samme som standardkvalitet';
@override String get musicQualityTitle => 'Musikkvalitet';
@override String get subtitleStyling => 'Undertekststil';
@override String get subtitleStylingDescription => 'Tilpas underteksters udseende';
@@ -315,6 +320,8 @@ class _Translations$settings$da extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Husk valget af lyd og undertekster for hver titel';
@override String get followServerTrackSelections => 'Brug serverens sporvalg for hvert afsnit';
@override String get followServerTrackSelectionsDescription => 'Ved afsnitsskift anvendes lyden og underteksterne valgt på serveren i stedet for at videreføre det aktuelle valg';
@override String get resumeMusicOnLaunch => 'Husk musiksession';
@override String get resumeMusicOnLaunchDescription => 'Åbn den seneste sang på pause der, hvor den slap, når appen starter';
@override String get showChapterMarkersOnTimeline => 'Vis kapitelmarkører på tidslinjen';
@override String get showChapterMarkersOnTimelineDescription => 'Opdel tidslinjen ved kapitelgrænser';
@override String get specialsOrdering => 'Specialafsnit i episoderekkefølge';
@@ -368,7 +375,11 @@ class _Translations$settings$da extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Genvejen er allerede tildelt ${action}';
@override String shortcutUpdated({required Object action}) => 'Genvejen for ${action} er opdateret';
@override String get saveFailed => 'Ændringerne kunne ikke gemmes. Prøv igen.';
@override String get autoSkip => 'Automatisk spring';
@override String get autoPlayAndSkip => 'Autoafspilning og spring';
@override String get autoPlayNextEpisode => 'Autoafspil næste afsnit';
@override String get autoPlayNextEpisodeDescription => 'Start automatisk næste afsnit, når et afsnit slutter';
@override String get playNextCountdown => 'Nedtælling til næste afsnit';
@override String get playNextCountdownImmediate => 'Afspil med det samme';
@override String get autoSkipIntro => 'Spring intro over automatisk';
@override String get autoSkipIntroDescription => 'Spring automatisk intromarkører over efter få sekunder';
@override String get autoSkipCredits => 'Spring rulletekster over automatisk';
@@ -443,10 +454,8 @@ class _Translations$settings$da extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Gennemtving indbygget DV7-understøttelse, og undlad at forsøge DV-konvertering igen';
@override String get dvConversionDv81Description => 'Tving inline RPU-konvertering til Dolby Vision profil 8.1';
@override String get dvConversionHevcStripDescription => 'Fjern Dolby Vision RPU/EL-lag og brug almindelig HEVC';
@override String get demuxer => 'Container-demuxer';
@override String get demuxerDescription => 'Hvilken demuxer der læser direkte afspillede filer. Skift til media3, hvis en fil driller.';
@override String get demuxerFfmpeg => 'FFmpeg (anbefalet)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Deinterlacing';
@override String get deinterlaceDescription => 'Fjern kamartefakter fra interlaced video (kun mpv-afspiller)';
@override String get requireProfileSelectionOnOpen => 'Spørg om profil ved åbning';
@override String get requireProfileSelectionOnOpenDescription => 'Vis profilvalg hver gang appen åbnes';
@override String get forceTvMode => 'Gennemtving TV-tilstand';
@@ -464,15 +473,31 @@ class _Translations$settings$da extends Translations$settings$en {
@override String get showExploreTabDescription => 'Vis fanen Opdag med indhold fra Plex Discover og tilknyttede trackere';
@override String get liveTvDefaultFavorites => 'Vis favoritkanaler som standard';
@override String get liveTvDefaultFavoritesDescription => 'Vis kun favoritkanaler ved åbning af Live TV';
@override String get general => 'Generelt';
@override String get generalDescription => 'Sprog, opstart og vinduesadfærd';
@override String get languageAndRegion => 'Sprog og region';
@override String get startup => 'Opstart';
@override String get display => 'Skærm';
@override String get libraryAndCards => 'Bibliotek og kort';
@override String get homeScreen => 'Startskærm';
@override String get navigation => 'Navigation';
@override String get window => 'Vindue';
@override String get content => 'Indhold';
@override String get liveTv => 'Live TV';
@override String get player => 'Afspiller';
@override String get subtitlesAndConfig => 'Undertekster og konfiguration';
@override String get videoAndDisplay => 'Video og skærm';
@override String get audio => 'Lyd';
@override String get quality => 'Kvalitet';
@override String get subtitles => 'Undertekster';
@override String get seekAndTiming => 'Søgning og timing';
@override String get behavior => 'Adfærd';
@override String get gestures => 'Bevægelser';
@override String get gestureBrightnessSwipe => 'Lysstyrke-strygning';
@override String get gestureBrightnessSwipeDescription => 'Stryg op eller ned i venstre kant for at justere lysstyrken';
@override String get gestureVolumeSwipe => 'Lydstyrke-strygning';
@override String get gestureVolumeSwipeDescription => 'Stryg op eller ned i højre kant for at justere lydstyrken';
@override String get gesturePinchToZoom => 'Klem for at zoome';
@override String get gesturePinchToZoomDescription => 'Klem på videoen for at zoome ind eller ud';
@override String get controls => 'Kontroller';
@override String get rememberPlayerChanges => 'Husk afspillerændringer';
@override String get rememberPlayerChangesDescription => 'Hvor en ændring under afspilning gemmes og anvendes igen';
@override String get scopePlaybackSpeed => 'Afspilningshastighed';
@@ -1086,6 +1111,49 @@ class _Translations$connections$da extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Tilføj eller fjern URL\'er for ${serverName}. Plezy bruger den tilgængelige URL med laveste latenstid.';
}
// Path: accountPreferences
class _Translations$accountPreferences$da extends Translations$accountPreferences$en {
_Translations$accountPreferences$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Kontopræferencer';
@override String hubSubtitleSingle({required Object account}) => 'Lyd-, undertekst- og biblioteksindstillinger gemt på ${account}';
@override String hubSubtitleMultiple({required Object count}) => 'Lyd-, undertekst- og biblioteksindstillinger gemt på ${count} konti';
@override String get pickAccount => 'Hver konto gemmer sine egne præferencer. Vælg den, du vil redigere.';
@override String get storedOnAccount => 'Disse indstillinger gemmes på selve kontoen, så alle apps, der er logget ind på den, bruger dem — inklusive Plezy på dine andre enheder.';
@override String get noAccounts => 'Ingen konti at konfigurere';
@override String get noAccountsHint => 'Log ind på Plex, eller forbind en Jellyfin- eller Emby-server, så vises de præferencer, der er gemt på kontoen, her.';
@override String get unavailable => 'Kan ikke nå denne konto';
@override String get loadFailed => 'Kunne ikke indlæse disse præferencer';
@override String get noPreference => 'Ingen præference';
@override String get notSet => 'Ikke indstillet';
@override late final _Translations$accountPreferences$groups$da groups = _Translations$accountPreferences$groups$da._(_root);
@override String get preferredAudioLanguage => 'Foretrukket lydsprog';
@override String get autoSelectAudio => 'Vælg lyd ud fra sprog';
@override String get autoSelectAudioDescription => 'Fra beholder det lydspor, som filen markerer som standard.';
@override String get preferredSubtitleLanguage => 'Foretrukket undertekstsprog';
@override String get subtitleMode => 'Slå undertekster til';
@override late final _Translations$accountPreferences$subtitleModes$da subtitleModes = _Translations$accountPreferences$subtitleModes$da._(_root);
@override String get subtitleAccessibility => 'SDH-undertekster';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$da subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$da._(_root);
@override String get forcedSubtitles => 'Tvungne undertekster';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$da forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$da._(_root);
@override String get displayMissingEpisodes => 'Vis manglende afsnit';
@override String get displayMissingEpisodesDescription => 'Vis afsnit, som serveren kender til, men som ikke har nogen fil.';
@override String get hidePlayedInLatest => 'Skjul sete elementer i Seneste';
@override String get hidePlayedInLatestDescription => 'Hold elementer, du allerede har set, ude af serverens Seneste-rækker.';
@override String get displayCollectionsView => 'Vis samlingsvisningen';
@override String get displayCollectionsViewDescription => 'Vis serverens samlingsvisning sammen med dine biblioteker.';
@override String get rewatchingInNextUp => 'Behold gensete serier i Næste afsnit';
@override String get rewatchingInNextUpDescription => 'Når du er færdig med en serie og ser den igen, følger Næste afsnit med i gensynet i stedet for at fjerne serien.';
@override String get watchedIndicator => 'Set-indikatorer';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$da watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$da._(_root);
@override String get mediaReviewsVisibility => 'Bedømmelser og anmeldelser';
@override late final _Translations$accountPreferences$mediaReviewsOptions$da mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$da._(_root);
}
// Path: discover
class _Translations$discover$da extends Translations$discover$en {
_Translations$discover$da._(TranslationsDa root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$da extends Translations$liveTv$en {
@override String get unknownChannel => 'Ukendt kanal';
@override String get live => 'LIVE';
@override String get reloadGuide => 'Genindlæs guide';
@override String get searchGuide => 'Søg i guiden';
@override String get searchHint => 'Søg efter kanaler og programmer';
@override String searchNoResults({required Object query}) => 'Ingen match for "${query}"';
@override String get channelsSection => 'Kanaler';
@override String get programsSection => 'Programmer';
@override String get now => 'Nu';
@override String get today => 'I dag';
@override String get tomorrow => 'I morgen';
@@ -1545,6 +1618,8 @@ class _Translations$music$da extends Translations$music$en {
@override String get repeatAll => 'Gentag alle';
@override String get repeatOne => 'Gentag ét nummer';
@override String get instantMixNoServer => 'Ingen server er tilgængelig til et øjeblikkeligt mix';
@override String get instantMixFailed => 'Kunne ikke indlæse det direkte miks';
@override String get instantMixEmpty => 'Det direkte miks indeholdt ingen numre';
@override String noAudioUrl({required Object track}) => 'Ingen lyd-URL er tilgængelig for ${track}';
@override late final _Translations$music$discography$da discography = _Translations$music$discography$da._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$da extends Translations$seerr$en {
@override String get statusBlocklisted => 'På blokeringslisten';
@override String couldNotReach({required Object url, required Object error}) => 'Kunne ikke nå ${url}: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'Ingen Seerr-instans på ${url} (HTTP ${status})';
@override String get invalidUrl => 'Indtast en serveradresse som https://seerr.example.com';
@override String get quickConnectUnsupported => 'Denne Seerr-instans understøtter ikke Quick Connect. Den kræver Seerr 3.4 eller nyere.';
@override String get notInitialized => 'Denne Seerr-instans har ikke fuldført førstegangsopsætningen';
@override String get noPlexTokenForReauth => 'Intet Plex-token er tilgængeligt til at logge ind igen';
@override String get noStoredCredentials => 'Ingen gemte loginoplysninger er tilgængelige til at logge ind igen';
@@ -2066,6 +2143,7 @@ class _Translations$services$da extends Translations$services$en {
// Translations
@override String get title => 'Tjenester';
@override String get hubSubtitle => 'Synkroniser dit visningsfremskridt, og anmod om nye titler.';
@override String get integrations => 'Integrationer';
@override String get notConnected => 'Ikke forbundet';
@override String connectedAs({required Object username}) => 'Forbundet som @${username}';
@override String get scrobble => 'Registrer fremgang automatisk';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$da extends Translations$videoControl
@override String unknown({required Object error}) => 'Der opstod en fejl: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$da extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Lyd og undertekster';
@override String get libraryDisplay => 'Bibliotek';
@override String get personalMedia => 'Personlige medier';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$da extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get none => 'Manuelt valgt';
@override String get noneDescription => 'Slår aldrig undertekster til af sig selv.';
@override String get defaultMode => 'Følg sporets flag';
@override String get defaultModeDescription => 'Brug standard- og tvungne flag, der er gemt på hvert undertekstspor.';
@override String get always => 'Altid aktiveret';
@override String get alwaysDescription => 'Slå et undertekstspor til på det foretrukne sprog, når der findes ét.';
@override String get onlyForced => 'Kun tvungne undertekster';
@override String get onlyForcedDescription => 'Indlæs kun spor, der er markeret som tvungne.';
@override String get smart => 'Vis ved fremmedsproget lyd';
@override String get smartDescription => 'Slå kun undertekster til, når lyden er på et andet sprog.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$da extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Foretræk ikke-SDH-undertekster';
@override String get preferSdh => 'Foretræk SDH-undertekster';
@override String get onlySdh => 'Kun SDH-undertekster';
@override String get onlyNonSdh => 'Kun ikke-SDH-undertekster';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$da extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Foretræk ikke-tvungne undertekster';
@override String get preferForced => 'Foretræk tvungne undertekster';
@override String get onlyForced => 'Kun tvungne undertekster';
@override String get onlyNonForced => 'Kun ikke-tvungne undertekster';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$da extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get none => 'Aldrig';
@override String get moviesAndShows => 'Film og TV-serier';
@override String get movies => 'Kun film';
@override String get shows => 'Kun TV-serier';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$da extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$da._(TranslationsDa root) : this._root = root, super.internal(root);
final TranslationsDa _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Brugere og anmeldere';
@override String get usersOnly => 'Kun brugere';
@override String get criticsOnly => 'Kun anmeldere';
@override String get nobody => 'Skjult';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$da extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$da._(TranslationsDa root) : this._root = root, super.internal(root);
@@ -2786,6 +2947,7 @@ extension on TranslationsDa {
'auth.useBrowser' => 'Brug browseren',
'auth.or' => 'eller',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Opret forbindelse til ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Brug Quick Connect',
'auth.quickConnectInstructions' => 'Åbn Quick Connect i Jellyfin, og indtast denne kode.',
'auth.quickConnectWaiting' => 'Venter på godkendelse…',
@@ -2825,6 +2987,7 @@ extension on TranslationsDa {
'common.mute' => 'Lydløs',
'common.ok' => 'OK',
'common.off' => 'Fra',
'common.options' => 'Valgmuligheder',
'common.seasonNumber' => ({required Object number}) => 'Sæson ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Episode ${number} – ${title}',
'common.chapterNumber' => ({required Object number}) => 'Kapitel ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsDa {
'settings.displayScale' => 'Skalering',
'settings.compact' => 'Kompakt',
'settings.comfortable' => 'Komfortabel',
'settings.gridSpacing' => 'Gitterafstand',
'settings.gridSpacingTight' => 'Tæt',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Rummelig',
'settings.tvCornerSpotlightBackdrop' => 'Fremhævet baggrundsbillede i hjørnet',
'settings.tvCornerSpotlightBackdropDescription' => 'Vis fremhævet grafik i øverste højre hjørne i stedet for at fylde skærmen',
'settings.viewMode' => 'Visningstilstand',
@@ -2946,16 +3113,14 @@ extension on TranslationsDa {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Hardwaredekodning',
'settings.hardwareDecodingDescription' => 'Brug hardwareacceleration, når den er tilgængelig',
'settings.bufferSize' => 'Bufferstørrelse',
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
'settings.bufferSizeAuto' => 'Automatisk (anbefalet)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} MB hukommelse tilgængelig. En buffer på ${size} MB kan påvirke afspilningen.',
'settings.playbackBuffer' => 'Afspilningsbuffer',
'settings.playbackBufferAuto' => 'Auto (anbefalet)',
'settings.playbackBufferLarge' => 'Stor',
'settings.playbackBufferExtraLarge' => 'Ekstra stor',
'settings.playbackBufferDescription' => 'Bufrer mere mod ustabile forbindelser. Begrænses også af bufferstørrelsen.',
'settings.defaultQualityTitle' => 'Standardkvalitet',
'settings.cellularQualityTitle' => 'Standardkvalitet på mobildata',
'settings.cellularQualitySameAsDefault' => 'Samme som standardkvalitet',
'settings.musicQualityTitle' => 'Musikkvalitet',
'settings.subtitleStyling' => 'Undertekststil',
'settings.subtitleStylingDescription' => 'Tilpas underteksters udseende',
@@ -2969,6 +3134,8 @@ extension on TranslationsDa {
'settings.rememberTrackSelectionsDescription' => 'Husk valget af lyd og undertekster for hver titel',
'settings.followServerTrackSelections' => 'Brug serverens sporvalg for hvert afsnit',
'settings.followServerTrackSelectionsDescription' => 'Ved afsnitsskift anvendes lyden og underteksterne valgt på serveren i stedet for at videreføre det aktuelle valg',
'settings.resumeMusicOnLaunch' => 'Husk musiksession',
'settings.resumeMusicOnLaunchDescription' => 'Åbn den seneste sang på pause der, hvor den slap, når appen starter',
'settings.showChapterMarkersOnTimeline' => 'Vis kapitelmarkører på tidslinjen',
'settings.showChapterMarkersOnTimelineDescription' => 'Opdel tidslinjen ved kapitelgrænser',
'settings.specialsOrdering' => 'Specialafsnit i episoderekkefølge',
@@ -3022,7 +3189,11 @@ extension on TranslationsDa {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Genvejen er allerede tildelt ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Genvejen for ${action} er opdateret',
'settings.saveFailed' => 'Ændringerne kunne ikke gemmes. Prøv igen.',
'settings.autoSkip' => 'Automatisk spring',
'settings.autoPlayAndSkip' => 'Autoafspilning og spring',
'settings.autoPlayNextEpisode' => 'Autoafspil næste afsnit',
'settings.autoPlayNextEpisodeDescription' => 'Start automatisk næste afsnit, når et afsnit slutter',
'settings.playNextCountdown' => 'Nedtælling til næste afsnit',
'settings.playNextCountdownImmediate' => 'Afspil med det samme',
'settings.autoSkipIntro' => 'Spring intro over automatisk',
'settings.autoSkipIntroDescription' => 'Spring automatisk intromarkører over efter få sekunder',
'settings.autoSkipCredits' => 'Spring rulletekster over automatisk',
@@ -3097,10 +3268,8 @@ extension on TranslationsDa {
'settings.dvConversionNativeDescription' => 'Gennemtving indbygget DV7-understøttelse, og undlad at forsøge DV-konvertering igen',
'settings.dvConversionDv81Description' => 'Tving inline RPU-konvertering til Dolby Vision profil 8.1',
'settings.dvConversionHevcStripDescription' => 'Fjern Dolby Vision RPU/EL-lag og brug almindelig HEVC',
'settings.demuxer' => 'Container-demuxer',
'settings.demuxerDescription' => 'Hvilken demuxer der læser direkte afspillede filer. Skift til media3, hvis en fil driller.',
'settings.demuxerFfmpeg' => 'FFmpeg (anbefalet)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterlacing',
'settings.deinterlaceDescription' => 'Fjern kamartefakter fra interlaced video (kun mpv-afspiller)',
'settings.requireProfileSelectionOnOpen' => 'Spørg om profil ved åbning',
'settings.requireProfileSelectionOnOpenDescription' => 'Vis profilvalg hver gang appen åbnes',
'settings.forceTvMode' => 'Gennemtving TV-tilstand',
@@ -3118,15 +3287,31 @@ extension on TranslationsDa {
'settings.showExploreTabDescription' => 'Vis fanen Opdag med indhold fra Plex Discover og tilknyttede trackere',
'settings.liveTvDefaultFavorites' => 'Vis favoritkanaler som standard',
'settings.liveTvDefaultFavoritesDescription' => 'Vis kun favoritkanaler ved åbning af Live TV',
'settings.general' => 'Generelt',
'settings.generalDescription' => 'Sprog, opstart og vinduesadfærd',
'settings.languageAndRegion' => 'Sprog og region',
'settings.startup' => 'Opstart',
'settings.display' => 'Skærm',
'settings.libraryAndCards' => 'Bibliotek og kort',
'settings.homeScreen' => 'Startskærm',
'settings.navigation' => 'Navigation',
'settings.window' => 'Vindue',
'settings.content' => 'Indhold',
'settings.liveTv' => 'Live TV',
'settings.player' => 'Afspiller',
'settings.subtitlesAndConfig' => 'Undertekster og konfiguration',
'settings.videoAndDisplay' => 'Video og skærm',
'settings.audio' => 'Lyd',
'settings.quality' => 'Kvalitet',
'settings.subtitles' => 'Undertekster',
'settings.seekAndTiming' => 'Søgning og timing',
'settings.behavior' => 'Adfærd',
'settings.gestures' => 'Bevægelser',
'settings.gestureBrightnessSwipe' => 'Lysstyrke-strygning',
'settings.gestureBrightnessSwipeDescription' => 'Stryg op eller ned i venstre kant for at justere lysstyrken',
'settings.gestureVolumeSwipe' => 'Lydstyrke-strygning',
'settings.gestureVolumeSwipeDescription' => 'Stryg op eller ned i højre kant for at justere lydstyrken',
'settings.gesturePinchToZoom' => 'Klem for at zoome',
'settings.gesturePinchToZoomDescription' => 'Klem på videoen for at zoome ind eller ud',
'settings.controls' => 'Kontroller',
'settings.rememberPlayerChanges' => 'Husk afspillerændringer',
'settings.rememberPlayerChangesDescription' => 'Hvor en ændring under afspilning gemmes og anvendes igen',
'settings.scopePlaybackSpeed' => 'Afspilningshastighed',
@@ -3264,6 +3449,8 @@ extension on TranslationsDa {
'fileInfo.has64bitOffsets' => '64-bit-forskydninger',
'fileInfo.protocol' => 'Protokol',
'fileInfo.mediaType' => 'Medietype',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Kildetype',
'fileInfo.optimizedVersion' => 'Optimeret version',
'fileInfo.optimizationTarget' => 'Optimeringsmål',
@@ -3288,8 +3475,6 @@ extension on TranslationsDa {
'fileInfo.flagOriginal' => 'Original',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profil ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Markér som set',
'mediaMenu.markAsUnwatched' => 'Markér som uset',
'mediaMenu.removeFromContinueWatching' => 'Fjern fra Fortsæt med at se',
@@ -3624,6 +3809,63 @@ extension on TranslationsDa {
'connections.signInAgain' => 'Log ind igen',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Rediger ${product}-forbindelse',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Tilføj eller fjern URL\'er for ${serverName}. Plezy bruger den tilgængelige URL med laveste latenstid.',
'accountPreferences.sectionTitle' => 'Kontopræferencer',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Lyd-, undertekst- og biblioteksindstillinger gemt på ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Lyd-, undertekst- og biblioteksindstillinger gemt på ${count} konti',
'accountPreferences.pickAccount' => 'Hver konto gemmer sine egne præferencer. Vælg den, du vil redigere.',
'accountPreferences.storedOnAccount' => 'Disse indstillinger gemmes på selve kontoen, så alle apps, der er logget ind på den, bruger dem — inklusive Plezy på dine andre enheder.',
'accountPreferences.noAccounts' => 'Ingen konti at konfigurere',
'accountPreferences.noAccountsHint' => 'Log ind på Plex, eller forbind en Jellyfin- eller Emby-server, så vises de præferencer, der er gemt på kontoen, her.',
'accountPreferences.unavailable' => 'Kan ikke nå denne konto',
'accountPreferences.loadFailed' => 'Kunne ikke indlæse disse præferencer',
'accountPreferences.noPreference' => 'Ingen præference',
'accountPreferences.notSet' => 'Ikke indstillet',
'accountPreferences.groups.audioAndSubtitles' => 'Lyd og undertekster',
'accountPreferences.groups.libraryDisplay' => 'Bibliotek',
'accountPreferences.groups.personalMedia' => 'Personlige medier',
'accountPreferences.preferredAudioLanguage' => 'Foretrukket lydsprog',
'accountPreferences.autoSelectAudio' => 'Vælg lyd ud fra sprog',
'accountPreferences.autoSelectAudioDescription' => 'Fra beholder det lydspor, som filen markerer som standard.',
'accountPreferences.preferredSubtitleLanguage' => 'Foretrukket undertekstsprog',
'accountPreferences.subtitleMode' => 'Slå undertekster til',
'accountPreferences.subtitleModes.none' => 'Manuelt valgt',
'accountPreferences.subtitleModes.noneDescription' => 'Slår aldrig undertekster til af sig selv.',
'accountPreferences.subtitleModes.defaultMode' => 'Følg sporets flag',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Brug standard- og tvungne flag, der er gemt på hvert undertekstspor.',
'accountPreferences.subtitleModes.always' => 'Altid aktiveret',
'accountPreferences.subtitleModes.alwaysDescription' => 'Slå et undertekstspor til på det foretrukne sprog, når der findes ét.',
'accountPreferences.subtitleModes.onlyForced' => 'Kun tvungne undertekster',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Indlæs kun spor, der er markeret som tvungne.',
'accountPreferences.subtitleModes.smart' => 'Vis ved fremmedsproget lyd',
'accountPreferences.subtitleModes.smartDescription' => 'Slå kun undertekster til, når lyden er på et andet sprog.',
'accountPreferences.subtitleAccessibility' => 'SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Foretræk ikke-SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Foretræk SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Kun SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Kun ikke-SDH-undertekster',
'accountPreferences.forcedSubtitles' => 'Tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Foretræk ikke-tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Foretræk tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Kun tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Kun ikke-tvungne undertekster',
'accountPreferences.displayMissingEpisodes' => 'Vis manglende afsnit',
'accountPreferences.displayMissingEpisodesDescription' => 'Vis afsnit, som serveren kender til, men som ikke har nogen fil.',
'accountPreferences.hidePlayedInLatest' => 'Skjul sete elementer i Seneste',
'accountPreferences.hidePlayedInLatestDescription' => 'Hold elementer, du allerede har set, ude af serverens Seneste-rækker.',
'accountPreferences.displayCollectionsView' => 'Vis samlingsvisningen',
'accountPreferences.displayCollectionsViewDescription' => 'Vis serverens samlingsvisning sammen med dine biblioteker.',
'accountPreferences.rewatchingInNextUp' => 'Behold gensete serier i Næste afsnit',
'accountPreferences.rewatchingInNextUpDescription' => 'Når du er færdig med en serie og ser den igen, følger Næste afsnit med i gensynet i stedet for at fjerne serien.',
'accountPreferences.watchedIndicator' => 'Set-indikatorer',
'accountPreferences.watchedIndicatorOptions.none' => 'Aldrig',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Film og TV-serier',
'accountPreferences.watchedIndicatorOptions.movies' => 'Kun film',
'accountPreferences.watchedIndicatorOptions.shows' => 'Kun TV-serier',
'accountPreferences.mediaReviewsVisibility' => 'Bedømmelser og anmeldelser',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Brugere og anmeldere',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Kun brugere',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Kun anmeldere',
'accountPreferences.mediaReviewsOptions.nobody' => 'Skjult',
'discover.title' => 'Opdag',
'discover.noContentAvailable' => 'Intet indhold tilgængeligt',
'discover.addMediaToLibraries' => 'Tilføj medier til dine biblioteker',
@@ -3721,6 +3963,8 @@ extension on TranslationsDa {
'libraries.filterCategories.contentRating' => 'Aldersvurdering',
'libraries.filterCategories.tag' => 'Tag',
'libraries.filterCategories.unwatched' => 'Usete',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Ikke afspillet',
'libraries.filterCategories.favorites' => 'Favoritter',
'libraries.sortLabels.title' => 'Titel',
@@ -3802,8 +4046,6 @@ extension on TranslationsDa {
'explore.rows.watchlist' => 'Ønskeliste',
'explore.rows.recommendedMovies' => 'Anbefalede film',
'explore.rows.recommendedShows' => 'Anbefalede serier',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Populære film lige nu',
'explore.rows.trendingShows' => 'Populære serier lige nu',
'explore.rows.popularMovies' => 'Populære film',
@@ -3956,6 +4198,11 @@ extension on TranslationsDa {
'liveTv.unknownChannel' => 'Ukendt kanal',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Genindlæs guide',
'liveTv.searchGuide' => 'Søg i guiden',
'liveTv.searchHint' => 'Søg efter kanaler og programmer',
'liveTv.searchNoResults' => ({required Object query}) => 'Ingen match for "${query}"',
'liveTv.channelsSection' => 'Kanaler',
'liveTv.programsSection' => 'Programmer',
'liveTv.now' => 'Nu',
'liveTv.today' => 'I dag',
'liveTv.tomorrow' => 'I morgen',
@@ -4093,6 +4340,8 @@ extension on TranslationsDa {
'music.repeatAll' => 'Gentag alle',
'music.repeatOne' => 'Gentag ét nummer',
'music.instantMixNoServer' => 'Ingen server er tilgængelig til et øjeblikkeligt mix',
'music.instantMixFailed' => 'Kunne ikke indlæse det direkte miks',
'music.instantMixEmpty' => 'Det direkte miks indeholdt ingen numre',
'music.noAudioUrl' => ({required Object track}) => 'Ingen lyd-URL er tilgængelig for ${track}',
'music.discography.singlesAndEps' => 'Singler og EP\'er',
'music.discography.live' => 'Live',
@@ -4228,6 +4477,8 @@ extension on TranslationsDa {
'downloads.syncRuleCleanupUnavailable' => 'Tilknyttede downloads kunne ikke identificeres sikkert. Genopret forbindelse til serveren og prøv igen, eller fjern reglen uden at slette downloads.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => 'Synkroniserede ${count} nye episoder for ${title}',
'downloads.activeSyncRules' => 'Synkroniseringsregler',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Ingen synkroniseringsregler',
'downloads.manageSyncRule' => 'Administrer synkronisering',
'downloads.editEpisodeCount' => 'Antal episoder',
@@ -4316,8 +4567,6 @@ extension on TranslationsDa {
'companionRemote.pairing.availableDevices' => 'Tilgængelige enheder',
'companionRemote.pairing.manualConnection' => 'Manuel forbindelse',
'companionRemote.pairing.cryptoInitFailed' => 'Kunne ikke starte sikker forbindelse. Log ind på Plex først.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Angiv en værtsadresse',
'companionRemote.pairing.validationHostFormat' => 'Format skal være IP:port (f.eks. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Forbindelsen fik timeout. Brug samme netværk på begge enheder.',
@@ -4577,6 +4826,8 @@ extension on TranslationsDa {
'seerr.statusBlocklisted' => 'På blokeringslisten',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Kunne ikke nå ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'Ingen Seerr-instans på ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Indtast en serveradresse som https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Denne Seerr-instans understøtter ikke Quick Connect. Den kræver Seerr 3.4 eller nyere.',
'seerr.notInitialized' => 'Denne Seerr-instans har ikke fuldført førstegangsopsætningen',
'seerr.noPlexTokenForReauth' => 'Intet Plex-token er tilgængeligt til at logge ind igen',
'seerr.noStoredCredentials' => 'Ingen gemte loginoplysninger er tilgængelige til at logge ind igen',
@@ -4587,6 +4838,7 @@ extension on TranslationsDa {
'seerr.sessionRejectedAfterReauth' => 'Sessionen blev afvist efter at være logget ind igen',
'services.title' => 'Tjenester',
'services.hubSubtitle' => 'Synkroniser dit visningsfremskridt, og anmod om nye titler.',
'services.integrations' => 'Integrationer',
'services.notConnected' => 'Ikke forbundet',
'services.connectedAs' => ({required Object username}) => 'Forbundet som @${username}',
'services.scrobble' => 'Registrer fremgang automatisk',
+282 -30
View File
@@ -60,6 +60,7 @@ class TranslationsDe extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$de dialog = _Translations$dialog$de._(_root);
@override late final _Translations$profiles$de profiles = _Translations$profiles$de._(_root);
@override late final _Translations$connections$de connections = _Translations$connections$de._(_root);
@override late final _Translations$accountPreferences$de accountPreferences = _Translations$accountPreferences$de._(_root);
@override late final _Translations$discover$de discover = _Translations$discover$de._(_root);
@override late final _Translations$errors$de errors = _Translations$errors$de._(_root);
@override late final _Translations$libraries$de libraries = _Translations$libraries$de._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$de extends Translations$auth$en {
@override String get useBrowser => 'Browser verwenden';
@override String get or => 'oder';
@override String connectToMediaBrowser({required Object product}) => 'Mit ${product} verbinden';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Quick Connect verwenden';
@override String get quickConnectInstructions => 'Öffne Quick Connect in Jellyfin und gib diesen Code ein.';
@override String get quickConnectWaiting => 'Warte auf Bestätigung…';
@@ -165,6 +167,7 @@ class _Translations$common$de extends Translations$common$en {
@override String get mute => 'Stumm';
@override String get ok => 'OK';
@override String get off => 'Aus';
@override String get options => 'Optionen';
@override String seasonNumber({required Object number}) => 'Staffel ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Folge ${number} – ${title}';
@override String chapterNumber({required Object number}) => 'Kapitel ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$de extends Translations$settings$en {
@override String get displayScale => 'Skalierung';
@override String get compact => 'Kompakt';
@override String get comfortable => 'Großzügig';
@override String get gridSpacing => 'Rasterabstand';
@override String get gridSpacingTight => 'Eng';
@override String get gridSpacingNormal => 'Normal';
@override String get gridSpacingSpacious => 'Großzügig';
@override String get tvCornerSpotlightBackdrop => 'Backdrop in der Ecke';
@override String get tvCornerSpotlightBackdropDescription => 'Zeigt das Spotlight-Artwork oben rechts statt bildschirmfüllend';
@override String get viewMode => 'Ansichtsmodus';
@@ -292,16 +299,14 @@ class _Translations$settings$de extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Hardwaredekodierung';
@override String get hardwareDecodingDescription => 'Hardwarebeschleunigung verwenden, sofern verfügbar';
@override String get bufferSize => 'Puffergröße';
@override String bufferSizeMB({required Object size}) => '${size} MB';
@override String get bufferSizeAuto => 'Automatisch (empfohlen)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} MB Speicher verfügbar. Ein Puffer von ${size} MB kann die Wiedergabe beeinträchtigen.';
@override String get playbackBuffer => 'Wiedergabepuffer';
@override String get playbackBufferAuto => 'Automatisch (empfohlen)';
@override String get playbackBufferLarge => 'Groß';
@override String get playbackBufferExtraLarge => 'Extra groß';
@override String get playbackBufferDescription => 'Puffert mehr gegen instabile Verbindungen. Zusätzlich durch die Puffergröße begrenzt.';
@override String get defaultQualityTitle => 'Standardqualität';
@override String get cellularQualityTitle => 'Standardqualität im Mobilfunknetz';
@override String get cellularQualitySameAsDefault => 'Wie Standardqualität';
@override String get musicQualityTitle => 'Musikqualität';
@override String get subtitleStyling => 'Untertitel-Stil';
@override String get subtitleStylingDescription => 'Aussehen von Untertiteln anpassen';
@@ -315,6 +320,8 @@ class _Translations$settings$de extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Audio- und Untertitelauswahl je Titel merken';
@override String get followServerTrackSelections => 'Serverseitige Spurauswahl pro Episode verwenden';
@override String get followServerTrackSelectionsDescription => 'Beim Episodenwechsel die auf dem Server ausgewählten Audio- und Untertitelspuren übernehmen, statt die aktuelle Auswahl zu übertragen';
@override String get resumeMusicOnLaunch => 'Musiksitzung merken';
@override String get resumeMusicOnLaunchDescription => 'Beim App-Start den letzten Titel pausiert an der letzten Stelle wieder öffnen';
@override String get showChapterMarkersOnTimeline => 'Kapitelmarkierungen auf der Suchleiste anzeigen';
@override String get showChapterMarkersOnTimelineDescription => 'Die Suchleiste an Kapitelgrenzen unterteilen';
@override String get specialsOrdering => 'Specials in Episodenreihenfolge';
@@ -368,7 +375,11 @@ class _Translations$settings$de extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Tastenkürzel bereits zugewiesen an ${action}';
@override String shortcutUpdated({required Object action}) => 'Tastenkürzel aktualisiert für ${action}';
@override String get saveFailed => 'Änderungen konnten nicht gespeichert werden. Versuche es erneut.';
@override String get autoSkip => 'Automatisches Überspringen';
@override String get autoPlayAndSkip => 'Autoplay & Überspringen';
@override String get autoPlayNextEpisode => 'Nächste Episode automatisch abspielen';
@override String get autoPlayNextEpisodeDescription => 'Die nächste Episode automatisch starten, wenn die aktuelle endet';
@override String get playNextCountdown => 'Countdown bis zur nächsten Episode';
@override String get playNextCountdownImmediate => 'Sofort abspielen';
@override String get autoSkipIntro => 'Intro automatisch überspringen';
@override String get autoSkipIntroDescription => 'Intro-Marker nach wenigen Sekunden automatisch überspringen';
@override String get autoSkipCredits => 'Abspann automatisch überspringen';
@@ -443,10 +454,8 @@ class _Translations$settings$de extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Natives DV7 erzwingen und einen erneuten DV-Konvertierungsversuch unterdrücken';
@override String get dvConversionDv81Description => 'Inline-RPU-Konvertierung in Dolby-Vision-Profil 8.1 erzwingen';
@override String get dvConversionHevcStripDescription => 'Dolby-Vision-RPU/EL-Schichten entfernen und reines HEVC ausgeben';
@override String get demuxer => 'Container-Demuxer';
@override String get demuxerDescription => 'Welcher Demuxer direkt abgespielte Dateien einliest. Wechsle zu media3, wenn eine Datei Probleme macht.';
@override String get demuxerFfmpeg => 'FFmpeg (empfohlen)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Deinterlacing';
@override String get deinterlaceDescription => 'Kammartefakte aus interlaced Videos entfernen (nur mpv-Player)';
@override String get requireProfileSelectionOnOpen => 'Profil beim Öffnen abfragen';
@override String get requireProfileSelectionOnOpenDescription => 'Profilauswahl bei jedem Öffnen der App anzeigen';
@override String get forceTvMode => 'TV-Modus erzwingen';
@@ -464,15 +473,31 @@ class _Translations$settings$de extends Translations$settings$en {
@override String get showExploreTabDescription => 'Zeigt den Tab „Entdecken“ mit Inhalten aus Plex Discover und verbundenen Trackern an';
@override String get liveTvDefaultFavorites => 'Standardmäßig Favoritenkanäle';
@override String get liveTvDefaultFavoritesDescription => 'Beim Öffnen von Live TV nur Favoritenkanäle anzeigen';
@override String get general => 'Allgemein';
@override String get generalDescription => 'Sprache, Start und Fensterverhalten';
@override String get languageAndRegion => 'Sprache & Region';
@override String get startup => 'Start';
@override String get display => 'Anzeige';
@override String get libraryAndCards => 'Mediathek & Karten';
@override String get homeScreen => 'Startseite';
@override String get navigation => 'Navigation';
@override String get window => 'Fenster';
@override String get content => 'Inhalt';
@override String get liveTv => 'Live-TV';
@override String get player => 'Wiedergabe';
@override String get subtitlesAndConfig => 'Untertitel und Konfiguration';
@override String get videoAndDisplay => 'Video & Anzeige';
@override String get audio => 'Audio';
@override String get quality => 'Qualität';
@override String get subtitles => 'Untertitel';
@override String get seekAndTiming => 'Spulen & Timing';
@override String get behavior => 'Verhalten';
@override String get gestures => 'Gesten';
@override String get gestureBrightnessSwipe => 'Wischen für Helligkeit';
@override String get gestureBrightnessSwipeDescription => 'Wische am linken Rand nach oben oder unten, um die Helligkeit anzupassen';
@override String get gestureVolumeSwipe => 'Wischen für Lautstärke';
@override String get gestureVolumeSwipeDescription => 'Wische am rechten Rand nach oben oder unten, um die Lautstärke anzupassen';
@override String get gesturePinchToZoom => 'Zum Zoomen kneifen';
@override String get gesturePinchToZoomDescription => 'Kneife auf dem Video, um hinein- oder herauszuzoomen';
@override String get controls => 'Steuerung';
@override String get rememberPlayerChanges => 'Playeränderungen merken';
@override String get rememberPlayerChangesDescription => 'Wo eine während der Wiedergabe vorgenommene Änderung gespeichert und erneut angewendet wird';
@override String get scopePlaybackSpeed => 'Wiedergabegeschwindigkeit';
@@ -1086,6 +1111,49 @@ class _Translations$connections$de extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'URLs für ${serverName} hinzufügen oder entfernen. Plezy verwendet die erreichbare URL mit der geringsten Latenz.';
}
// Path: accountPreferences
class _Translations$accountPreferences$de extends Translations$accountPreferences$en {
_Translations$accountPreferences$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Kontoeinstellungen';
@override String hubSubtitleSingle({required Object account}) => 'Audio-, Untertitel- und Mediathek-Optionen werden auf ${account} gespeichert';
@override String hubSubtitleMultiple({required Object count}) => 'Audio-, Untertitel- und Mediathek-Optionen werden auf ${count} Konten gespeichert';
@override String get pickAccount => 'Jedes Konto speichert seine eigenen Einstellungen. Wähle das Konto, das du bearbeiten möchtest.';
@override String get storedOnAccount => 'Diese Optionen werden auf dem Konto selbst gespeichert, sodass jede damit angemeldete App sie verwendet — auch Plezy auf deinen anderen Geräten.';
@override String get noAccounts => 'Keine Konten zum Konfigurieren';
@override String get noAccountsHint => 'Melde dich bei Plex an oder verbinde einen Jellyfin- oder Emby-Server — die auf diesem Konto gespeicherten Einstellungen erscheinen dann hier.';
@override String get unavailable => 'Dieses Konto ist nicht erreichbar';
@override String get loadFailed => 'Diese Einstellungen konnten nicht geladen werden';
@override String get noPreference => 'Keine Einstellung';
@override String get notSet => 'Nicht festgelegt';
@override late final _Translations$accountPreferences$groups$de groups = _Translations$accountPreferences$groups$de._(_root);
@override String get preferredAudioLanguage => 'Bevorzugte Audiosprache';
@override String get autoSelectAudio => 'Audio nach Sprache auswählen';
@override String get autoSelectAudioDescription => 'Bei „Aus“ wird die Audiospur verwendet, die die Datei als Standard markiert.';
@override String get preferredSubtitleLanguage => 'Bevorzugte Untertitelsprache';
@override String get subtitleMode => 'Untertitel einschalten';
@override late final _Translations$accountPreferences$subtitleModes$de subtitleModes = _Translations$accountPreferences$subtitleModes$de._(_root);
@override String get subtitleAccessibility => 'SDH-Untertitel';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$de subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$de._(_root);
@override String get forcedSubtitles => 'Erzwungene Untertitel';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$de forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$de._(_root);
@override String get displayMissingEpisodes => 'Fehlende Episoden anzeigen';
@override String get displayMissingEpisodesDescription => 'Episoden auflisten, die dem Server bekannt sind, für die aber keine Datei vorliegt.';
@override String get hidePlayedInLatest => 'Gesehene Elemente in „Neueste“ ausblenden';
@override String get hidePlayedInLatestDescription => 'Elemente, die du bereits gesehen hast, erscheinen nicht mehr in den „Neueste“-Zeilen des Servers.';
@override String get displayCollectionsView => 'Sammlungsansicht anzeigen';
@override String get displayCollectionsViewDescription => 'Die Sammlungsansicht des Servers zusätzlich zu deinen Mediatheken anzeigen.';
@override String get rewatchingInNextUp => 'Erneut geschaute Serien in „Als Nächstes“ behalten';
@override String get rewatchingInNextUpDescription => 'Beendest du eine Serie und startest sie erneut, folgt „Als Nächstes“ dem erneuten Anschauen, statt die Serie zu verwerfen.';
@override String get watchedIndicator => 'Gesehen-Markierung';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$de watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$de._(_root);
@override String get mediaReviewsVisibility => 'Bewertungen & Rezensionen';
@override late final _Translations$accountPreferences$mediaReviewsOptions$de mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$de._(_root);
}
// Path: discover
class _Translations$discover$de extends Translations$discover$en {
_Translations$discover$de._(TranslationsDe root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$de extends Translations$liveTv$en {
@override String get unknownChannel => 'Unbekannter Kanal';
@override String get live => 'LIVE';
@override String get reloadGuide => 'TV-Programm neu laden';
@override String get searchGuide => 'TV-Programm durchsuchen';
@override String get searchHint => 'Kanäle & Sendungen suchen';
@override String searchNoResults({required Object query}) => 'Keine Treffer für „${query}“';
@override String get channelsSection => 'Kanäle';
@override String get programsSection => 'Sendungen';
@override String get now => 'Jetzt';
@override String get today => 'Heute';
@override String get tomorrow => 'Morgen';
@@ -1545,6 +1618,8 @@ class _Translations$music$de extends Translations$music$en {
@override String get repeatAll => 'Alle wiederholen';
@override String get repeatOne => 'Titel wiederholen';
@override String get instantMixNoServer => 'Für einen Instant Mix ist kein Server verfügbar';
@override String get instantMixFailed => 'Instant-Mix konnte nicht geladen werden';
@override String get instantMixEmpty => 'Der Instant-Mix enthält keine Titel';
@override String noAudioUrl({required Object track}) => 'Für ${track} ist keine Audio-URL verfügbar';
@override late final _Translations$music$discography$de discography = _Translations$music$discography$de._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$de extends Translations$seerr$en {
@override String get statusBlocklisted => 'Auf der Sperrliste';
@override String couldNotReach({required Object url, required Object error}) => '${url} nicht erreichbar: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'Keine Seerr-Instanz unter ${url} (HTTP ${status})';
@override String get invalidUrl => 'Gib eine Serveradresse ein, z. B. https://seerr.example.com';
@override String get quickConnectUnsupported => 'Diese Seerr-Instanz unterstützt Quick Connect nicht. Dafür ist Seerr 3.4 oder neuer erforderlich.';
@override String get notInitialized => 'Die Ersteinrichtung dieser Seerr-Instanz wurde noch nicht abgeschlossen';
@override String get noPlexTokenForReauth => 'Für die erneute Anmeldung ist kein Plex-Token verfügbar';
@override String get noStoredCredentials => 'Für die erneute Anmeldung sind keine gespeicherten Anmeldedaten verfügbar';
@@ -2066,6 +2143,7 @@ class _Translations$services$de extends Translations$services$en {
// Translations
@override String get title => 'Dienste';
@override String get hubSubtitle => 'Wiedergabefortschritt synchronisieren und neue Titel anfragen.';
@override String get integrations => 'Integrationen';
@override String get notConnected => 'Nicht verbunden';
@override String connectedAs({required Object username}) => 'Verbunden als @${username}';
@override String get scrobble => 'Fortschritt automatisch verfolgen';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$de extends Translations$videoControl
@override String unknown({required Object error}) => 'Ein Fehler ist aufgetreten: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$de extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Audio & Untertitel';
@override String get libraryDisplay => 'Mediathek';
@override String get personalMedia => 'Persönliche Medien';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$de extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get none => 'Manuell ausgewählt';
@override String get noneDescription => 'Untertitel niemals von selbst einschalten.';
@override String get defaultMode => 'Spurmarkierungen folgen';
@override String get defaultModeDescription => 'Die Standard- und Erzwungen-Markierungen jeder Untertitelspur verwenden.';
@override String get always => 'Immer aktiviert';
@override String get alwaysDescription => 'Eine Untertitelspur in der bevorzugten Sprache einschalten, sofern eine vorhanden ist.';
@override String get onlyForced => 'Nur erzwungene Untertitel';
@override String get onlyForcedDescription => 'Nur die als erzwungen markierten Spuren laden.';
@override String get smart => 'Bei fremdsprachigem Audio anzeigen';
@override String get smartDescription => 'Untertitel nur einschalten, wenn das Audio in einer anderen Sprache ist.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$de extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Nicht-SDH-Untertitel bevorzugen';
@override String get preferSdh => 'SDH-Untertitel bevorzugen';
@override String get onlySdh => 'Nur SDH-Untertitel';
@override String get onlyNonSdh => 'Nur Nicht-SDH-Untertitel';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$de extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Nicht erzwungene Untertitel bevorzugen';
@override String get preferForced => 'Erzwungene Untertitel bevorzugen';
@override String get onlyForced => 'Nur erzwungene Untertitel';
@override String get onlyNonForced => 'Nur nicht erzwungene Untertitel';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$de extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get none => 'Nie';
@override String get moviesAndShows => 'Filme und Serien';
@override String get movies => 'Nur Filme';
@override String get shows => 'Nur Serien';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$de extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$de._(TranslationsDe root) : this._root = root, super.internal(root);
final TranslationsDe _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Benutzer und Kritiker';
@override String get usersOnly => 'Nur Benutzer';
@override String get criticsOnly => 'Nur Kritiker';
@override String get nobody => 'Ausgeblendet';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$de extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$de._(TranslationsDe root) : this._root = root, super.internal(root);
@@ -2295,7 +2456,7 @@ class _Translations$libraries$sortLabels$de extends Translations$libraries$sortL
@override String get productionYear => 'Produktionsjahr';
@override String get runtime => 'Laufzeit';
@override String get officialRating => 'Offizielle Bewertung';
@override String get premiereDate => 'Premierendatum';
@override String get premiereDate => 'Veröffentlichungsdatum';
@override String get startDate => 'Startdatum';
@override String get airTime => 'Sendezeit';
@override String get studio => 'Studio';
@@ -2786,6 +2947,7 @@ extension on TranslationsDe {
'auth.useBrowser' => 'Browser verwenden',
'auth.or' => 'oder',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Mit ${product} verbinden',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Quick Connect verwenden',
'auth.quickConnectInstructions' => 'Öffne Quick Connect in Jellyfin und gib diesen Code ein.',
'auth.quickConnectWaiting' => 'Warte auf Bestätigung…',
@@ -2825,6 +2987,7 @@ extension on TranslationsDe {
'common.mute' => 'Stumm',
'common.ok' => 'OK',
'common.off' => 'Aus',
'common.options' => 'Optionen',
'common.seasonNumber' => ({required Object number}) => 'Staffel ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Folge ${number} – ${title}',
'common.chapterNumber' => ({required Object number}) => 'Kapitel ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsDe {
'settings.displayScale' => 'Skalierung',
'settings.compact' => 'Kompakt',
'settings.comfortable' => 'Großzügig',
'settings.gridSpacing' => 'Rasterabstand',
'settings.gridSpacingTight' => 'Eng',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Großzügig',
'settings.tvCornerSpotlightBackdrop' => 'Backdrop in der Ecke',
'settings.tvCornerSpotlightBackdropDescription' => 'Zeigt das Spotlight-Artwork oben rechts statt bildschirmfüllend',
'settings.viewMode' => 'Ansichtsmodus',
@@ -2946,16 +3113,14 @@ extension on TranslationsDe {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Hardwaredekodierung',
'settings.hardwareDecodingDescription' => 'Hardwarebeschleunigung verwenden, sofern verfügbar',
'settings.bufferSize' => 'Puffergröße',
'settings.bufferSizeMB' => ({required Object size}) => '${size} MB',
'settings.bufferSizeAuto' => 'Automatisch (empfohlen)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} MB Speicher verfügbar. Ein Puffer von ${size} MB kann die Wiedergabe beeinträchtigen.',
'settings.playbackBuffer' => 'Wiedergabepuffer',
'settings.playbackBufferAuto' => 'Automatisch (empfohlen)',
'settings.playbackBufferLarge' => 'Groß',
'settings.playbackBufferExtraLarge' => 'Extra groß',
'settings.playbackBufferDescription' => 'Puffert mehr gegen instabile Verbindungen. Zusätzlich durch die Puffergröße begrenzt.',
'settings.defaultQualityTitle' => 'Standardqualität',
'settings.cellularQualityTitle' => 'Standardqualität im Mobilfunknetz',
'settings.cellularQualitySameAsDefault' => 'Wie Standardqualität',
'settings.musicQualityTitle' => 'Musikqualität',
'settings.subtitleStyling' => 'Untertitel-Stil',
'settings.subtitleStylingDescription' => 'Aussehen von Untertiteln anpassen',
@@ -2969,6 +3134,8 @@ extension on TranslationsDe {
'settings.rememberTrackSelectionsDescription' => 'Audio- und Untertitelauswahl je Titel merken',
'settings.followServerTrackSelections' => 'Serverseitige Spurauswahl pro Episode verwenden',
'settings.followServerTrackSelectionsDescription' => 'Beim Episodenwechsel die auf dem Server ausgewählten Audio- und Untertitelspuren übernehmen, statt die aktuelle Auswahl zu übertragen',
'settings.resumeMusicOnLaunch' => 'Musiksitzung merken',
'settings.resumeMusicOnLaunchDescription' => 'Beim App-Start den letzten Titel pausiert an der letzten Stelle wieder öffnen',
'settings.showChapterMarkersOnTimeline' => 'Kapitelmarkierungen auf der Suchleiste anzeigen',
'settings.showChapterMarkersOnTimelineDescription' => 'Die Suchleiste an Kapitelgrenzen unterteilen',
'settings.specialsOrdering' => 'Specials in Episodenreihenfolge',
@@ -3022,7 +3189,11 @@ extension on TranslationsDe {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Tastenkürzel bereits zugewiesen an ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Tastenkürzel aktualisiert für ${action}',
'settings.saveFailed' => 'Änderungen konnten nicht gespeichert werden. Versuche es erneut.',
'settings.autoSkip' => 'Automatisches Überspringen',
'settings.autoPlayAndSkip' => 'Autoplay & Überspringen',
'settings.autoPlayNextEpisode' => 'Nächste Episode automatisch abspielen',
'settings.autoPlayNextEpisodeDescription' => 'Die nächste Episode automatisch starten, wenn die aktuelle endet',
'settings.playNextCountdown' => 'Countdown bis zur nächsten Episode',
'settings.playNextCountdownImmediate' => 'Sofort abspielen',
'settings.autoSkipIntro' => 'Intro automatisch überspringen',
'settings.autoSkipIntroDescription' => 'Intro-Marker nach wenigen Sekunden automatisch überspringen',
'settings.autoSkipCredits' => 'Abspann automatisch überspringen',
@@ -3097,10 +3268,8 @@ extension on TranslationsDe {
'settings.dvConversionNativeDescription' => 'Natives DV7 erzwingen und einen erneuten DV-Konvertierungsversuch unterdrücken',
'settings.dvConversionDv81Description' => 'Inline-RPU-Konvertierung in Dolby-Vision-Profil 8.1 erzwingen',
'settings.dvConversionHevcStripDescription' => 'Dolby-Vision-RPU/EL-Schichten entfernen und reines HEVC ausgeben',
'settings.demuxer' => 'Container-Demuxer',
'settings.demuxerDescription' => 'Welcher Demuxer direkt abgespielte Dateien einliest. Wechsle zu media3, wenn eine Datei Probleme macht.',
'settings.demuxerFfmpeg' => 'FFmpeg (empfohlen)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterlacing',
'settings.deinterlaceDescription' => 'Kammartefakte aus interlaced Videos entfernen (nur mpv-Player)',
'settings.requireProfileSelectionOnOpen' => 'Profil beim Öffnen abfragen',
'settings.requireProfileSelectionOnOpenDescription' => 'Profilauswahl bei jedem Öffnen der App anzeigen',
'settings.forceTvMode' => 'TV-Modus erzwingen',
@@ -3118,15 +3287,31 @@ extension on TranslationsDe {
'settings.showExploreTabDescription' => 'Zeigt den Tab „Entdecken“ mit Inhalten aus Plex Discover und verbundenen Trackern an',
'settings.liveTvDefaultFavorites' => 'Standardmäßig Favoritenkanäle',
'settings.liveTvDefaultFavoritesDescription' => 'Beim Öffnen von Live TV nur Favoritenkanäle anzeigen',
'settings.general' => 'Allgemein',
'settings.generalDescription' => 'Sprache, Start und Fensterverhalten',
'settings.languageAndRegion' => 'Sprache & Region',
'settings.startup' => 'Start',
'settings.display' => 'Anzeige',
'settings.libraryAndCards' => 'Mediathek & Karten',
'settings.homeScreen' => 'Startseite',
'settings.navigation' => 'Navigation',
'settings.window' => 'Fenster',
'settings.content' => 'Inhalt',
'settings.liveTv' => 'Live-TV',
'settings.player' => 'Wiedergabe',
'settings.subtitlesAndConfig' => 'Untertitel und Konfiguration',
'settings.videoAndDisplay' => 'Video & Anzeige',
'settings.audio' => 'Audio',
'settings.quality' => 'Qualität',
'settings.subtitles' => 'Untertitel',
'settings.seekAndTiming' => 'Spulen & Timing',
'settings.behavior' => 'Verhalten',
'settings.gestures' => 'Gesten',
'settings.gestureBrightnessSwipe' => 'Wischen für Helligkeit',
'settings.gestureBrightnessSwipeDescription' => 'Wische am linken Rand nach oben oder unten, um die Helligkeit anzupassen',
'settings.gestureVolumeSwipe' => 'Wischen für Lautstärke',
'settings.gestureVolumeSwipeDescription' => 'Wische am rechten Rand nach oben oder unten, um die Lautstärke anzupassen',
'settings.gesturePinchToZoom' => 'Zum Zoomen kneifen',
'settings.gesturePinchToZoomDescription' => 'Kneife auf dem Video, um hinein- oder herauszuzoomen',
'settings.controls' => 'Steuerung',
'settings.rememberPlayerChanges' => 'Playeränderungen merken',
'settings.rememberPlayerChangesDescription' => 'Wo eine während der Wiedergabe vorgenommene Änderung gespeichert und erneut angewendet wird',
'settings.scopePlaybackSpeed' => 'Wiedergabegeschwindigkeit',
@@ -3264,6 +3449,8 @@ extension on TranslationsDe {
'fileInfo.has64bitOffsets' => '64-Bit-Offsets',
'fileInfo.protocol' => 'Protokoll',
'fileInfo.mediaType' => 'Medientyp',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Quellenart',
'fileInfo.optimizedVersion' => 'Optimierte Version',
'fileInfo.optimizationTarget' => 'Optimierungsziel',
@@ -3288,8 +3475,6 @@ extension on TranslationsDe {
'fileInfo.flagOriginal' => 'Original',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profil ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Als gesehen markieren',
'mediaMenu.markAsUnwatched' => 'Als ungesehen markieren',
'mediaMenu.removeFromContinueWatching' => 'Aus ‚Weiterschauen‘ entfernen',
@@ -3624,6 +3809,63 @@ extension on TranslationsDe {
'connections.signInAgain' => 'Erneut anmelden',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Verbindung zu ${product} bearbeiten',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'URLs für ${serverName} hinzufügen oder entfernen. Plezy verwendet die erreichbare URL mit der geringsten Latenz.',
'accountPreferences.sectionTitle' => 'Kontoeinstellungen',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Audio-, Untertitel- und Mediathek-Optionen werden auf ${account} gespeichert',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Audio-, Untertitel- und Mediathek-Optionen werden auf ${count} Konten gespeichert',
'accountPreferences.pickAccount' => 'Jedes Konto speichert seine eigenen Einstellungen. Wähle das Konto, das du bearbeiten möchtest.',
'accountPreferences.storedOnAccount' => 'Diese Optionen werden auf dem Konto selbst gespeichert, sodass jede damit angemeldete App sie verwendet — auch Plezy auf deinen anderen Geräten.',
'accountPreferences.noAccounts' => 'Keine Konten zum Konfigurieren',
'accountPreferences.noAccountsHint' => 'Melde dich bei Plex an oder verbinde einen Jellyfin- oder Emby-Server — die auf diesem Konto gespeicherten Einstellungen erscheinen dann hier.',
'accountPreferences.unavailable' => 'Dieses Konto ist nicht erreichbar',
'accountPreferences.loadFailed' => 'Diese Einstellungen konnten nicht geladen werden',
'accountPreferences.noPreference' => 'Keine Einstellung',
'accountPreferences.notSet' => 'Nicht festgelegt',
'accountPreferences.groups.audioAndSubtitles' => 'Audio & Untertitel',
'accountPreferences.groups.libraryDisplay' => 'Mediathek',
'accountPreferences.groups.personalMedia' => 'Persönliche Medien',
'accountPreferences.preferredAudioLanguage' => 'Bevorzugte Audiosprache',
'accountPreferences.autoSelectAudio' => 'Audio nach Sprache auswählen',
'accountPreferences.autoSelectAudioDescription' => 'Bei „Aus“ wird die Audiospur verwendet, die die Datei als Standard markiert.',
'accountPreferences.preferredSubtitleLanguage' => 'Bevorzugte Untertitelsprache',
'accountPreferences.subtitleMode' => 'Untertitel einschalten',
'accountPreferences.subtitleModes.none' => 'Manuell ausgewählt',
'accountPreferences.subtitleModes.noneDescription' => 'Untertitel niemals von selbst einschalten.',
'accountPreferences.subtitleModes.defaultMode' => 'Spurmarkierungen folgen',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Die Standard- und Erzwungen-Markierungen jeder Untertitelspur verwenden.',
'accountPreferences.subtitleModes.always' => 'Immer aktiviert',
'accountPreferences.subtitleModes.alwaysDescription' => 'Eine Untertitelspur in der bevorzugten Sprache einschalten, sofern eine vorhanden ist.',
'accountPreferences.subtitleModes.onlyForced' => 'Nur erzwungene Untertitel',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Nur die als erzwungen markierten Spuren laden.',
'accountPreferences.subtitleModes.smart' => 'Bei fremdsprachigem Audio anzeigen',
'accountPreferences.subtitleModes.smartDescription' => 'Untertitel nur einschalten, wenn das Audio in einer anderen Sprache ist.',
'accountPreferences.subtitleAccessibility' => 'SDH-Untertitel',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Nicht-SDH-Untertitel bevorzugen',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'SDH-Untertitel bevorzugen',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Nur SDH-Untertitel',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Nur Nicht-SDH-Untertitel',
'accountPreferences.forcedSubtitles' => 'Erzwungene Untertitel',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Nicht erzwungene Untertitel bevorzugen',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Erzwungene Untertitel bevorzugen',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Nur erzwungene Untertitel',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Nur nicht erzwungene Untertitel',
'accountPreferences.displayMissingEpisodes' => 'Fehlende Episoden anzeigen',
'accountPreferences.displayMissingEpisodesDescription' => 'Episoden auflisten, die dem Server bekannt sind, für die aber keine Datei vorliegt.',
'accountPreferences.hidePlayedInLatest' => 'Gesehene Elemente in „Neueste“ ausblenden',
'accountPreferences.hidePlayedInLatestDescription' => 'Elemente, die du bereits gesehen hast, erscheinen nicht mehr in den „Neueste“-Zeilen des Servers.',
'accountPreferences.displayCollectionsView' => 'Sammlungsansicht anzeigen',
'accountPreferences.displayCollectionsViewDescription' => 'Die Sammlungsansicht des Servers zusätzlich zu deinen Mediatheken anzeigen.',
'accountPreferences.rewatchingInNextUp' => 'Erneut geschaute Serien in „Als Nächstes“ behalten',
'accountPreferences.rewatchingInNextUpDescription' => 'Beendest du eine Serie und startest sie erneut, folgt „Als Nächstes“ dem erneuten Anschauen, statt die Serie zu verwerfen.',
'accountPreferences.watchedIndicator' => 'Gesehen-Markierung',
'accountPreferences.watchedIndicatorOptions.none' => 'Nie',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Filme und Serien',
'accountPreferences.watchedIndicatorOptions.movies' => 'Nur Filme',
'accountPreferences.watchedIndicatorOptions.shows' => 'Nur Serien',
'accountPreferences.mediaReviewsVisibility' => 'Bewertungen & Rezensionen',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Benutzer und Kritiker',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Nur Benutzer',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Nur Kritiker',
'accountPreferences.mediaReviewsOptions.nobody' => 'Ausgeblendet',
'discover.title' => 'Entdecken',
'discover.noContentAvailable' => 'Kein Inhalt verfügbar',
'discover.addMediaToLibraries' => 'Medien zur Mediathek hinzufügen',
@@ -3721,6 +3963,8 @@ extension on TranslationsDe {
'libraries.filterCategories.contentRating' => 'Altersfreigabe',
'libraries.filterCategories.tag' => 'Tag',
'libraries.filterCategories.unwatched' => 'Ungesehene',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Nicht abgespielt',
'libraries.filterCategories.favorites' => 'Favoriten',
'libraries.sortLabels.title' => 'Titel',
@@ -3735,7 +3979,7 @@ extension on TranslationsDe {
'libraries.sortLabels.productionYear' => 'Produktionsjahr',
'libraries.sortLabels.runtime' => 'Laufzeit',
'libraries.sortLabels.officialRating' => 'Offizielle Bewertung',
'libraries.sortLabels.premiereDate' => 'Premierendatum',
'libraries.sortLabels.premiereDate' => 'Veröffentlichungsdatum',
'libraries.sortLabels.startDate' => 'Startdatum',
'libraries.sortLabels.airTime' => 'Sendezeit',
'libraries.sortLabels.studio' => 'Studio',
@@ -3802,8 +4046,6 @@ extension on TranslationsDe {
'explore.rows.watchlist' => 'Merkliste',
'explore.rows.recommendedMovies' => 'Empfohlene Filme',
'explore.rows.recommendedShows' => 'Empfohlene Serien',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Angesagte Filme',
'explore.rows.trendingShows' => 'Angesagte Serien',
'explore.rows.popularMovies' => 'Beliebte Filme',
@@ -3956,6 +4198,11 @@ extension on TranslationsDe {
'liveTv.unknownChannel' => 'Unbekannter Kanal',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'TV-Programm neu laden',
'liveTv.searchGuide' => 'TV-Programm durchsuchen',
'liveTv.searchHint' => 'Kanäle & Sendungen suchen',
'liveTv.searchNoResults' => ({required Object query}) => 'Keine Treffer für „${query}“',
'liveTv.channelsSection' => 'Kanäle',
'liveTv.programsSection' => 'Sendungen',
'liveTv.now' => 'Jetzt',
'liveTv.today' => 'Heute',
'liveTv.tomorrow' => 'Morgen',
@@ -4093,6 +4340,8 @@ extension on TranslationsDe {
'music.repeatAll' => 'Alle wiederholen',
'music.repeatOne' => 'Titel wiederholen',
'music.instantMixNoServer' => 'Für einen Instant Mix ist kein Server verfügbar',
'music.instantMixFailed' => 'Instant-Mix konnte nicht geladen werden',
'music.instantMixEmpty' => 'Der Instant-Mix enthält keine Titel',
'music.noAudioUrl' => ({required Object track}) => 'Für ${track} ist keine Audio-URL verfügbar',
'music.discography.singlesAndEps' => 'Singles & EPs',
'music.discography.live' => 'Live',
@@ -4228,6 +4477,8 @@ extension on TranslationsDe {
'downloads.syncRuleCleanupUnavailable' => 'Zugehörige Downloads konnten nicht sicher ermittelt werden. Verbinde den Server erneut und versuche es noch einmal, oder entferne die Regel, ohne die Downloads zu löschen.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => '${count} neue Episoden für ${title} synchronisiert',
'downloads.activeSyncRules' => 'Synchronisierungsregeln',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Keine Synchronisierungsregeln',
'downloads.manageSyncRule' => 'Synchronisierung verwalten',
'downloads.editEpisodeCount' => 'Episodenanzahl',
@@ -4316,8 +4567,6 @@ extension on TranslationsDe {
'companionRemote.pairing.availableDevices' => 'Verfügbare Geräte',
'companionRemote.pairing.manualConnection' => 'Manuelle Verbindung',
'companionRemote.pairing.cryptoInitFailed' => 'Sichere Verbindung konnte nicht gestartet werden. Melde dich zuerst bei Plex an.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Bitte Host-Adresse eingeben',
'companionRemote.pairing.validationHostFormat' => 'Format muss IP:Port sein (z. B. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Zeitüberschreitung bei der Verbindung. Nutze auf beiden Geräten dasselbe Netzwerk.',
@@ -4577,6 +4826,8 @@ extension on TranslationsDe {
'seerr.statusBlocklisted' => 'Auf der Sperrliste',
'seerr.couldNotReach' => ({required Object url, required Object error}) => '${url} nicht erreichbar: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'Keine Seerr-Instanz unter ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Gib eine Serveradresse ein, z. B. https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Diese Seerr-Instanz unterstützt Quick Connect nicht. Dafür ist Seerr 3.4 oder neuer erforderlich.',
'seerr.notInitialized' => 'Die Ersteinrichtung dieser Seerr-Instanz wurde noch nicht abgeschlossen',
'seerr.noPlexTokenForReauth' => 'Für die erneute Anmeldung ist kein Plex-Token verfügbar',
'seerr.noStoredCredentials' => 'Für die erneute Anmeldung sind keine gespeicherten Anmeldedaten verfügbar',
@@ -4587,6 +4838,7 @@ extension on TranslationsDe {
'seerr.sessionRejectedAfterReauth' => 'Die Sitzung wurde nach der erneuten Anmeldung abgelehnt',
'services.title' => 'Dienste',
'services.hubSubtitle' => 'Wiedergabefortschritt synchronisieren und neue Titel anfragen.',
'services.integrations' => 'Integrationen',
'services.notConnected' => 'Nicht verbunden',
'services.connectedAs' => ({required Object username}) => 'Verbunden als @${username}',
'services.scrobble' => 'Fortschritt automatisch verfolgen',
+532 -48
View File
@@ -61,6 +61,7 @@ class Translations with BaseTranslations<AppLocale, Translations> {
late final Translations$dialog$en dialog = Translations$dialog$en.internal(_root);
late final Translations$profiles$en profiles = Translations$profiles$en.internal(_root);
late final Translations$connections$en connections = Translations$connections$en.internal(_root);
late final Translations$accountPreferences$en accountPreferences = Translations$accountPreferences$en.internal(_root);
late final Translations$discover$en discover = Translations$discover$en.internal(_root);
late final Translations$errors$en errors = Translations$errors$en.internal(_root);
late final Translations$libraries$en libraries = Translations$libraries$en.internal(_root);
@@ -139,6 +140,9 @@ class Translations$auth$en {
/// en: 'Connect to ${product}'
String connectToMediaBrowser({required Object product}) => 'Connect to ${product}';
/// en: 'Quick Connect'
String get quickConnect => 'Quick Connect';
/// en: 'Use Quick Connect'
String get useQuickConnect => 'Use Quick Connect';
@@ -265,6 +269,9 @@ class Translations$common$en {
/// en: 'Off'
String get off => 'Off';
/// en: 'Options'
String get options => 'Options';
/// en: 'Season ${number}'
String seasonNumber({required Object number}) => 'Season ${number}';
@@ -461,6 +468,18 @@ class Translations$settings$en {
/// en: 'Comfortable'
String get comfortable => 'Comfortable';
/// en: 'Grid Spacing'
String get gridSpacing => 'Grid Spacing';
/// en: 'Tight'
String get gridSpacingTight => 'Tight';
/// en: 'Normal'
String get gridSpacingNormal => 'Normal';
/// en: 'Spacious'
String get gridSpacingSpacious => 'Spacious';
/// en: 'Corner Spotlight Backdrop'
String get tvCornerSpotlightBackdrop => 'Corner Spotlight Backdrop';
@@ -590,18 +609,6 @@ class Translations$settings$en {
/// en: 'Use hardware acceleration when available'
String get hardwareDecodingDescription => 'Use hardware acceleration when available';
/// en: 'Buffer Size'
String get bufferSize => 'Buffer Size';
/// en: '${size}MB'
String bufferSizeMB({required Object size}) => '${size}MB';
/// en: 'Auto (Recommended)'
String get bufferSizeAuto => 'Auto (Recommended)';
/// en: '${heap}MB memory available. A ${size}MB buffer may affect playback.'
String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.';
/// en: 'Playback Buffer'
String get playbackBuffer => 'Playback Buffer';
@@ -620,6 +627,18 @@ class Translations$settings$en {
/// en: 'Default Quality'
String get defaultQualityTitle => 'Default Quality';
/// en: 'Default Quality on Cellular'
String get cellularQualityTitle => 'Default Quality on Cellular';
/// en: 'Same as Default Quality'
String get cellularQualitySameAsDefault => 'Same as Default Quality';
/// en: 'Play Smaller Videos at Original Quality'
String get directPlayCoveredQuality => 'Play Smaller Videos at Original Quality';
/// en: 'Direct play videos already within the quality limit instead of transcoding them'
String get directPlayCoveredQualityDescription => 'Direct play videos already within the quality limit instead of transcoding them';
/// en: 'Music Quality'
String get musicQualityTitle => 'Music Quality';
@@ -659,6 +678,12 @@ class Translations$settings$en {
/// en: 'On episode change, apply the audio and subtitles selected on the server instead of carrying over the current choice'
String get followServerTrackSelectionsDescription => 'On episode change, apply the audio and subtitles selected on the server instead of carrying over the current choice';
/// en: 'Remember music session'
String get resumeMusicOnLaunch => 'Remember music session';
/// en: 'Reopen the last song paused where it left off when the app starts'
String get resumeMusicOnLaunchDescription => 'Reopen the last song paused where it left off when the app starts';
/// en: 'Show chapter markers on seek bar'
String get showChapterMarkersOnTimeline => 'Show chapter markers on seek bar';
@@ -818,8 +843,20 @@ class Translations$settings$en {
/// en: 'Could not save changes. Try again.'
String get saveFailed => 'Could not save changes. Try again.';
/// en: 'Auto Skip'
String get autoSkip => 'Auto Skip';
/// en: 'Auto-Play & Skip'
String get autoPlayAndSkip => 'Auto-Play & Skip';
/// en: 'Auto-Play Next Episode'
String get autoPlayNextEpisode => 'Auto-Play Next Episode';
/// en: 'Start the next episode automatically when one ends'
String get autoPlayNextEpisodeDescription => 'Start the next episode automatically when one ends';
/// en: 'Play Next Countdown'
String get playNextCountdown => 'Play Next Countdown';
/// en: 'Play immediately'
String get playNextCountdownImmediate => 'Play immediately';
/// en: 'Auto Skip Intro'
String get autoSkipIntro => 'Auto Skip Intro';
@@ -1016,8 +1053,8 @@ class Translations$settings$en {
/// en: 'Dolby Vision Conversion'
String get dvConversionMode => 'Dolby Vision Conversion';
/// en: 'Choose how ExoPlayer handles Dolby Vision Profile 7 files.'
String get dvConversionModeDescription => 'Choose how ExoPlayer handles Dolby Vision Profile 7 files.';
/// en: 'Choose how Dolby Vision Profile 7 files are handled.'
String get dvConversionModeDescription => 'Choose how Dolby Vision Profile 7 files are handled.';
/// en: 'Auto'
String get dvConversionAuto => 'Auto';
@@ -1043,17 +1080,11 @@ class Translations$settings$en {
/// en: 'Strip Dolby Vision RPU/EL layers and present plain HEVC'
String get dvConversionHevcStripDescription => 'Strip Dolby Vision RPU/EL layers and present plain HEVC';
/// en: 'Container Demuxer'
String get demuxer => 'Container Demuxer';
/// en: 'Deinterlacing'
String get deinterlace => 'Deinterlacing';
/// en: 'Which demuxer parses direct-played files. Switch to media3 if a file misbehaves.'
String get demuxerDescription => 'Which demuxer parses direct-played files. Switch to media3 if a file misbehaves.';
/// en: 'FFmpeg (Recommended)'
String get demuxerFfmpeg => 'FFmpeg (Recommended)';
/// en: 'media3'
String get demuxerMedia3 => 'media3';
/// en: 'Remove combing artifacts from interlaced video (mpv player only)'
String get deinterlaceDescription => 'Remove combing artifacts from interlaced video (mpv player only)';
/// en: 'Ask for profile on app open'
String get requireProfileSelectionOnOpen => 'Ask for profile on app open';
@@ -1106,9 +1137,24 @@ class Translations$settings$en {
/// en: 'Show only favorite channels when opening Live TV'
String get liveTvDefaultFavoritesDescription => 'Show only favorite channels when opening Live TV';
/// en: 'General'
String get general => 'General';
/// en: 'Language, startup, and window behavior'
String get generalDescription => 'Language, startup, and window behavior';
/// en: 'Language & Region'
String get languageAndRegion => 'Language & Region';
/// en: 'Startup'
String get startup => 'Startup';
/// en: 'Display'
String get display => 'Display';
/// en: 'Library & Cards'
String get libraryAndCards => 'Library & Cards';
/// en: 'Home Screen'
String get homeScreen => 'Home Screen';
@@ -1118,14 +1164,23 @@ class Translations$settings$en {
/// en: 'Window'
String get window => 'Window';
/// en: 'Content'
String get content => 'Content';
/// en: 'Live TV'
String get liveTv => 'Live TV';
/// en: 'Player'
String get player => 'Player';
/// en: 'Subtitles & Configuration'
String get subtitlesAndConfig => 'Subtitles & Configuration';
/// en: 'Video & Display'
String get videoAndDisplay => 'Video & Display';
/// en: 'Audio'
String get audio => 'Audio';
/// en: 'Quality'
String get quality => 'Quality';
/// en: 'Subtitles'
String get subtitles => 'Subtitles';
/// en: 'Seek & Timing'
String get seekAndTiming => 'Seek & Timing';
@@ -1133,6 +1188,36 @@ class Translations$settings$en {
/// en: 'Behavior'
String get behavior => 'Behavior';
/// en: 'Gestures'
String get gestures => 'Gestures';
/// en: 'Brightness Swipe'
String get gestureBrightnessSwipe => 'Brightness Swipe';
/// en: 'Swipe up or down on the left edge to adjust brightness'
String get gestureBrightnessSwipeDescription => 'Swipe up or down on the left edge to adjust brightness';
/// en: 'Volume Swipe'
String get gestureVolumeSwipe => 'Volume Swipe';
/// en: 'Swipe up or down on the right edge to adjust volume'
String get gestureVolumeSwipeDescription => 'Swipe up or down on the right edge to adjust volume';
/// en: 'Pinch to Zoom'
String get gesturePinchToZoom => 'Pinch to Zoom';
/// en: 'Pinch on the video to zoom in or out'
String get gesturePinchToZoomDescription => 'Pinch on the video to zoom in or out';
/// en: 'Remember Brightness Level'
String get rememberBrightnessLevel => 'Remember Brightness Level';
/// en: 'Start playback at the brightness set by the last swipe'
String get rememberBrightnessLevelDescription => 'Start playback at the brightness set by the last swipe';
/// en: 'Controls'
String get controls => 'Controls';
/// en: 'Remember Player Changes'
String get rememberPlayerChanges => 'Remember Player Changes';
@@ -2671,6 +2756,111 @@ class Translations$connections$en {
String editMediaBrowserIntro({required Object serverName}) => 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.';
}
// Path: accountPreferences
class Translations$accountPreferences$en {
Translations$accountPreferences$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Account preferences'
String get sectionTitle => 'Account preferences';
/// en: 'Audio, subtitle and library options saved on ${account}'
String hubSubtitleSingle({required Object account}) => 'Audio, subtitle and library options saved on ${account}';
/// en: 'Audio, subtitle and library options saved on ${count} accounts'
String hubSubtitleMultiple({required Object count}) => 'Audio, subtitle and library options saved on ${count} accounts';
/// en: 'Every account stores its own preferences. Pick the one to edit.'
String get pickAccount => 'Every account stores its own preferences. Pick the one to edit.';
/// en: 'These options are saved on the account itself, so every app signed in to it uses them — including Plezy on your other devices.'
String get storedOnAccount => 'These options are saved on the account itself, so every app signed in to it uses them — including Plezy on your other devices.';
/// en: 'No accounts to configure'
String get noAccounts => 'No accounts to configure';
/// en: 'Sign in to Plex, or connect a Jellyfin or Emby server, and the preferences stored on that account show up here.'
String get noAccountsHint => 'Sign in to Plex, or connect a Jellyfin or Emby server, and the preferences stored on that account show up here.';
/// en: 'Can't reach this account'
String get unavailable => 'Can\'t reach this account';
/// en: 'Couldn't load these preferences'
String get loadFailed => 'Couldn\'t load these preferences';
/// en: 'No preference'
String get noPreference => 'No preference';
/// en: 'Not set'
String get notSet => 'Not set';
late final Translations$accountPreferences$groups$en groups = Translations$accountPreferences$groups$en.internal(_root);
/// en: 'Preferred audio language'
String get preferredAudioLanguage => 'Preferred audio language';
/// en: 'Choose audio by language'
String get autoSelectAudio => 'Choose audio by language';
/// en: 'Off keeps whatever audio track the file marks as default.'
String get autoSelectAudioDescription => 'Off keeps whatever audio track the file marks as default.';
/// en: 'Preferred subtitle language'
String get preferredSubtitleLanguage => 'Preferred subtitle language';
/// en: 'Turn subtitles on'
String get subtitleMode => 'Turn subtitles on';
late final Translations$accountPreferences$subtitleModes$en subtitleModes = Translations$accountPreferences$subtitleModes$en.internal(_root);
/// en: 'SDH subtitles'
String get subtitleAccessibility => 'SDH subtitles';
late final Translations$accountPreferences$subtitleAccessibilityOptions$en subtitleAccessibilityOptions = Translations$accountPreferences$subtitleAccessibilityOptions$en.internal(_root);
/// en: 'Forced subtitles'
String get forcedSubtitles => 'Forced subtitles';
late final Translations$accountPreferences$forcedSubtitleOptions$en forcedSubtitleOptions = Translations$accountPreferences$forcedSubtitleOptions$en.internal(_root);
/// en: 'Show missing episodes'
String get displayMissingEpisodes => 'Show missing episodes';
/// en: 'List episodes the server knows about but has no file for.'
String get displayMissingEpisodesDescription => 'List episodes the server knows about but has no file for.';
/// en: 'Hide watched items in Latest'
String get hidePlayedInLatest => 'Hide watched items in Latest';
/// en: 'Leave items you have already watched out of the server's Latest rows.'
String get hidePlayedInLatestDescription => 'Leave items you have already watched out of the server\'s Latest rows.';
/// en: 'Show the Collections view'
String get displayCollectionsView => 'Show the Collections view';
/// en: 'Offer the server's Collections view alongside your libraries.'
String get displayCollectionsViewDescription => 'Offer the server\'s Collections view alongside your libraries.';
/// en: 'Keep rewatched shows in Next Up'
String get rewatchingInNextUp => 'Keep rewatched shows in Next Up';
/// en: 'Once you finish a show, start it again and Next Up follows the rewatch instead of dropping the show.'
String get rewatchingInNextUpDescription => 'Once you finish a show, start it again and Next Up follows the rewatch instead of dropping the show.';
/// en: 'Watched indicators'
String get watchedIndicator => 'Watched indicators';
late final Translations$accountPreferences$watchedIndicatorOptions$en watchedIndicatorOptions = Translations$accountPreferences$watchedIndicatorOptions$en.internal(_root);
/// en: 'Ratings & reviews'
String get mediaReviewsVisibility => 'Ratings & reviews';
late final Translations$accountPreferences$mediaReviewsOptions$en mediaReviewsOptions = Translations$accountPreferences$mediaReviewsOptions$en.internal(_root);
}
// Path: discover
class Translations$discover$en {
Translations$discover$en.internal(this._root);
@@ -3315,6 +3505,21 @@ class Translations$liveTv$en {
/// en: 'Reload Guide'
String get reloadGuide => 'Reload Guide';
/// en: 'Search Guide'
String get searchGuide => 'Search Guide';
/// en: 'Search channels & programs'
String get searchHint => 'Search channels & programs';
/// en: 'No matches for "${query}"'
String searchNoResults({required Object query}) => 'No matches for "${query}"';
/// en: 'Channels'
String get channelsSection => 'Channels';
/// en: 'Programs'
String get programsSection => 'Programs';
/// en: 'Now'
String get now => 'Now';
@@ -3734,6 +3939,12 @@ class Translations$music$en {
/// en: 'No server is available for an instant mix'
String get instantMixNoServer => 'No server is available for an instant mix';
/// en: 'Failed to load the instant mix'
String get instantMixFailed => 'Failed to load the instant mix';
/// en: 'The instant mix came back with no tracks'
String get instantMixEmpty => 'The instant mix came back with no tracks';
/// en: 'No audio URL is available for ${track}'
String noAudioUrl({required Object track}) => 'No audio URL is available for ${track}';
@@ -3817,6 +4028,27 @@ class Translations$watchTogether$en {
/// en: 'You are the host'
String get youAreHost => 'You are the host';
/// en: 'Make host'
String get makeHost => 'Make host';
/// en: 'Transfer host?'
String get makeHostQuestion => 'Transfer host?';
/// en: '${name} will control playback and drive the session for everyone.'
String makeHostConfirm({required Object name}) => '${name} will control playback and drive the session for everyone.';
/// en: 'Transfer'
String get transfer => 'Transfer';
/// en: '${name} is now the host'
String hostChangedTo({required Object name}) => '${name} is now the host';
/// en: 'You are now the host'
String get youAreNowHost => 'You are now the host';
/// en: 'Couldn't make ${name} the host'
String hostTransferFailed({required Object name}) => 'Couldn\'t make ${name} the host';
/// en: 'Watching with others'
String get watchingWithOthers => 'Watching with others';
@@ -5014,6 +5246,12 @@ class Translations$seerr$en {
/// en: 'No Seerr instance at ${url} (HTTP ${status})'
String noInstanceAtUrl({required Object url, required Object status}) => 'No Seerr instance at ${url} (HTTP ${status})';
/// en: 'Enter a server address like https://seerr.example.com'
String get invalidUrl => 'Enter a server address like https://seerr.example.com';
/// en: 'This Seerr instance does not support Quick Connect. It needs Seerr 3.4 or newer.'
String get quickConnectUnsupported => 'This Seerr instance does not support Quick Connect. It needs Seerr 3.4 or newer.';
/// en: 'This Seerr instance has not completed first-run setup'
String get notInitialized => 'This Seerr instance has not completed first-run setup';
@@ -5053,6 +5291,9 @@ class Translations$services$en {
/// en: 'Sync watch progress and request new titles.'
String get hubSubtitle => 'Sync watch progress and request new titles.';
/// en: 'Integrations'
String get integrations => 'Integrations';
/// en: 'Not connected'
String get notConnected => 'Not connected';
@@ -5435,6 +5676,147 @@ class Translations$videoControls$pipErrors$en {
String unknown({required Object error}) => 'An error occurred: ${error}';
}
// Path: accountPreferences.groups
class Translations$accountPreferences$groups$en {
Translations$accountPreferences$groups$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Audio & subtitles'
String get audioAndSubtitles => 'Audio & subtitles';
/// en: 'Library'
String get libraryDisplay => 'Library';
/// en: 'Personal media'
String get personalMedia => 'Personal media';
}
// Path: accountPreferences.subtitleModes
class Translations$accountPreferences$subtitleModes$en {
Translations$accountPreferences$subtitleModes$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Manually selected'
String get none => 'Manually selected';
/// en: 'Never turn subtitles on by itself.'
String get noneDescription => 'Never turn subtitles on by itself.';
/// en: 'Follow the track flags'
String get defaultMode => 'Follow the track flags';
/// en: 'Use the default and forced flags stored on each subtitle track.'
String get defaultModeDescription => 'Use the default and forced flags stored on each subtitle track.';
/// en: 'Always enabled'
String get always => 'Always enabled';
/// en: 'Turn on a subtitle track in the preferred language whenever one exists.'
String get alwaysDescription => 'Turn on a subtitle track in the preferred language whenever one exists.';
/// en: 'Only forced subtitles'
String get onlyForced => 'Only forced subtitles';
/// en: 'Load only the tracks marked as forced.'
String get onlyForcedDescription => 'Load only the tracks marked as forced.';
/// en: 'Shown with foreign audio'
String get smart => 'Shown with foreign audio';
/// en: 'Turn subtitles on only when the audio is in another language.'
String get smartDescription => 'Turn subtitles on only when the audio is in another language.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class Translations$accountPreferences$subtitleAccessibilityOptions$en {
Translations$accountPreferences$subtitleAccessibilityOptions$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Prefer non-SDH subtitles'
String get preferNonSdh => 'Prefer non-SDH subtitles';
/// en: 'Prefer SDH subtitles'
String get preferSdh => 'Prefer SDH subtitles';
/// en: 'Only SDH subtitles'
String get onlySdh => 'Only SDH subtitles';
/// en: 'Only non-SDH subtitles'
String get onlyNonSdh => 'Only non-SDH subtitles';
}
// Path: accountPreferences.forcedSubtitleOptions
class Translations$accountPreferences$forcedSubtitleOptions$en {
Translations$accountPreferences$forcedSubtitleOptions$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Prefer non-forced subtitles'
String get preferNonForced => 'Prefer non-forced subtitles';
/// en: 'Prefer forced subtitles'
String get preferForced => 'Prefer forced subtitles';
/// en: 'Only forced subtitles'
String get onlyForced => 'Only forced subtitles';
/// en: 'Only non-forced subtitles'
String get onlyNonForced => 'Only non-forced subtitles';
}
// Path: accountPreferences.watchedIndicatorOptions
class Translations$accountPreferences$watchedIndicatorOptions$en {
Translations$accountPreferences$watchedIndicatorOptions$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Never'
String get none => 'Never';
/// en: 'Movies and TV shows'
String get moviesAndShows => 'Movies and TV shows';
/// en: 'Movies only'
String get movies => 'Movies only';
/// en: 'TV shows only'
String get shows => 'TV shows only';
}
// Path: accountPreferences.mediaReviewsOptions
class Translations$accountPreferences$mediaReviewsOptions$en {
Translations$accountPreferences$mediaReviewsOptions$en.internal(this._root);
final Translations _root; // ignore: unused_field
// Translations
/// en: 'Users and critics'
String get usersAndCritics => 'Users and critics';
/// en: 'Users only'
String get usersOnly => 'Users only';
/// en: 'Critics only'
String get criticsOnly => 'Critics only';
/// en: 'Hidden'
String get nobody => 'Hidden';
}
// Path: libraries.tabs
class Translations$libraries$tabs$en {
Translations$libraries$tabs$en.internal(this._root);
@@ -6575,6 +6957,7 @@ extension on Translations {
'auth.useBrowser' => 'Use browser',
'auth.or' => 'or',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Connect to ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Use Quick Connect',
'auth.quickConnectInstructions' => 'Open Quick Connect in Jellyfin and enter this code.',
'auth.quickConnectWaiting' => 'Waiting for approval…',
@@ -6614,6 +6997,7 @@ extension on Translations {
'common.mute' => 'Mute',
'common.ok' => 'OK',
'common.off' => 'Off',
'common.options' => 'Options',
'common.seasonNumber' => ({required Object number}) => 'Season ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Episode ${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'Chapter ${number}',
@@ -6692,6 +7076,10 @@ extension on Translations {
'settings.displayScale' => 'Display Scale',
'settings.compact' => 'Compact',
'settings.comfortable' => 'Comfortable',
'settings.gridSpacing' => 'Grid Spacing',
'settings.gridSpacingTight' => 'Tight',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Spacious',
'settings.tvCornerSpotlightBackdrop' => 'Corner Spotlight Backdrop',
'settings.tvCornerSpotlightBackdropDescription' => 'Show spotlight artwork in the top-right corner instead of filling the screen',
'settings.viewMode' => 'View Mode',
@@ -6735,16 +7123,16 @@ extension on Translations {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Hardware Decoding',
'settings.hardwareDecodingDescription' => 'Use hardware acceleration when available',
'settings.bufferSize' => 'Buffer Size',
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
'settings.bufferSizeAuto' => 'Auto (Recommended)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB memory available. A ${size}MB buffer may affect playback.',
'settings.playbackBuffer' => 'Playback Buffer',
'settings.playbackBufferAuto' => 'Auto (Recommended)',
'settings.playbackBufferLarge' => 'Large',
'settings.playbackBufferExtraLarge' => 'Extra Large',
'settings.playbackBufferDescription' => 'Buffer more against unstable connections. Also limited by Buffer Size.',
'settings.defaultQualityTitle' => 'Default Quality',
'settings.cellularQualityTitle' => 'Default Quality on Cellular',
'settings.cellularQualitySameAsDefault' => 'Same as Default Quality',
'settings.directPlayCoveredQuality' => 'Play Smaller Videos at Original Quality',
'settings.directPlayCoveredQualityDescription' => 'Direct play videos already within the quality limit instead of transcoding them',
'settings.musicQualityTitle' => 'Music Quality',
'settings.subtitleStyling' => 'Subtitle Styling',
'settings.subtitleStylingDescription' => 'Customize subtitle appearance',
@@ -6758,6 +7146,8 @@ extension on Translations {
'settings.rememberTrackSelectionsDescription' => 'Remember audio and subtitle choices per title',
'settings.followServerTrackSelections' => 'Use server\'s per-episode track selections',
'settings.followServerTrackSelectionsDescription' => 'On episode change, apply the audio and subtitles selected on the server instead of carrying over the current choice',
'settings.resumeMusicOnLaunch' => 'Remember music session',
'settings.resumeMusicOnLaunchDescription' => 'Reopen the last song paused where it left off when the app starts',
'settings.showChapterMarkersOnTimeline' => 'Show chapter markers on seek bar',
'settings.showChapterMarkersOnTimelineDescription' => 'Segment the seek bar at chapter boundaries',
'settings.specialsOrdering' => 'Specials in episode order',
@@ -6811,7 +7201,11 @@ extension on Translations {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Shortcut already assigned to ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Shortcut updated for ${action}',
'settings.saveFailed' => 'Could not save changes. Try again.',
'settings.autoSkip' => 'Auto Skip',
'settings.autoPlayAndSkip' => 'Auto-Play & Skip',
'settings.autoPlayNextEpisode' => 'Auto-Play Next Episode',
'settings.autoPlayNextEpisodeDescription' => 'Start the next episode automatically when one ends',
'settings.playNextCountdown' => 'Play Next Countdown',
'settings.playNextCountdownImmediate' => 'Play immediately',
'settings.autoSkipIntro' => 'Auto Skip Intro',
'settings.autoSkipIntroDescription' => 'Automatically skip intro markers after a few seconds',
'settings.autoSkipCredits' => 'Auto Skip Credits',
@@ -6877,7 +7271,7 @@ extension on Translations {
'settings.audioDownmixNormalize' => 'Normalize Volume on Downmix',
'settings.audioDownmixNormalizeDescription' => 'Lower the mix to prevent clipping. Turn off to keep the original volume (may distort loud scenes).',
'settings.dvConversionMode' => 'Dolby Vision Conversion',
'settings.dvConversionModeDescription' => 'Choose how ExoPlayer handles Dolby Vision Profile 7 files.',
'settings.dvConversionModeDescription' => 'Choose how Dolby Vision Profile 7 files are handled.',
'settings.dvConversionAuto' => 'Auto',
'settings.dvConversionNative' => 'Native / Disabled',
'settings.dvConversionDv81' => 'P7 → P8.1',
@@ -6886,10 +7280,8 @@ extension on Translations {
'settings.dvConversionNativeDescription' => 'Force native DV7 and suppress DV conversion retry',
'settings.dvConversionDv81Description' => 'Force inline RPU conversion to Dolby Vision profile 8.1',
'settings.dvConversionHevcStripDescription' => 'Strip Dolby Vision RPU/EL layers and present plain HEVC',
'settings.demuxer' => 'Container Demuxer',
'settings.demuxerDescription' => 'Which demuxer parses direct-played files. Switch to media3 if a file misbehaves.',
'settings.demuxerFfmpeg' => 'FFmpeg (Recommended)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterlacing',
'settings.deinterlaceDescription' => 'Remove combing artifacts from interlaced video (mpv player only)',
'settings.requireProfileSelectionOnOpen' => 'Ask for profile on app open',
'settings.requireProfileSelectionOnOpenDescription' => 'Show profile selection every time the app is opened',
'settings.forceTvMode' => 'Force TV mode',
@@ -6907,15 +7299,33 @@ extension on Translations {
'settings.showExploreTabDescription' => 'Display the Explore tab with content from Plex Discover and connected trackers',
'settings.liveTvDefaultFavorites' => 'Default to Favorite Channels',
'settings.liveTvDefaultFavoritesDescription' => 'Show only favorite channels when opening Live TV',
'settings.general' => 'General',
'settings.generalDescription' => 'Language, startup, and window behavior',
'settings.languageAndRegion' => 'Language & Region',
'settings.startup' => 'Startup',
'settings.display' => 'Display',
'settings.libraryAndCards' => 'Library & Cards',
'settings.homeScreen' => 'Home Screen',
'settings.navigation' => 'Navigation',
'settings.window' => 'Window',
'settings.content' => 'Content',
'settings.liveTv' => 'Live TV',
'settings.player' => 'Player',
'settings.subtitlesAndConfig' => 'Subtitles & Configuration',
'settings.videoAndDisplay' => 'Video & Display',
'settings.audio' => 'Audio',
'settings.quality' => 'Quality',
'settings.subtitles' => 'Subtitles',
'settings.seekAndTiming' => 'Seek & Timing',
'settings.behavior' => 'Behavior',
'settings.gestures' => 'Gestures',
'settings.gestureBrightnessSwipe' => 'Brightness Swipe',
'settings.gestureBrightnessSwipeDescription' => 'Swipe up or down on the left edge to adjust brightness',
'settings.gestureVolumeSwipe' => 'Volume Swipe',
'settings.gestureVolumeSwipeDescription' => 'Swipe up or down on the right edge to adjust volume',
'settings.gesturePinchToZoom' => 'Pinch to Zoom',
'settings.gesturePinchToZoomDescription' => 'Pinch on the video to zoom in or out',
'settings.rememberBrightnessLevel' => 'Remember Brightness Level',
'settings.rememberBrightnessLevelDescription' => 'Start playback at the brightness set by the last swipe',
'settings.controls' => 'Controls',
'settings.rememberPlayerChanges' => 'Remember Player Changes',
'settings.rememberPlayerChangesDescription' => 'Where a change made during playback is saved and reapplied from',
'settings.scopePlaybackSpeed' => 'Playback Speed',
@@ -7049,6 +7459,8 @@ extension on Translations {
'fileInfo.filePresent' => 'File Present',
'fileInfo.fileReadable' => 'Readable by Server',
'fileInfo.streamPath' => 'Stream Path',
_ => null,
} ?? switch (path) {
'fileInfo.optimizedForStreaming' => 'Optimized for Streaming',
'fileInfo.has64bitOffsets' => '64-bit Offsets',
'fileInfo.protocol' => 'Protocol',
@@ -7077,8 +7489,6 @@ extension on Translations {
'fileInfo.flagOriginal' => 'Original',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profile ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Mark as Watched',
'mediaMenu.markAsUnwatched' => 'Mark as Unwatched',
'mediaMenu.removeFromContinueWatching' => 'Remove from Continue Watching',
@@ -7413,6 +7823,63 @@ extension on Translations {
'connections.signInAgain' => 'Sign in again',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Edit ${product} connection',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Add or remove URLs for ${serverName}. Plezy will use the reachable URL with the lowest latency.',
'accountPreferences.sectionTitle' => 'Account preferences',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Audio, subtitle and library options saved on ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Audio, subtitle and library options saved on ${count} accounts',
'accountPreferences.pickAccount' => 'Every account stores its own preferences. Pick the one to edit.',
'accountPreferences.storedOnAccount' => 'These options are saved on the account itself, so every app signed in to it uses them — including Plezy on your other devices.',
'accountPreferences.noAccounts' => 'No accounts to configure',
'accountPreferences.noAccountsHint' => 'Sign in to Plex, or connect a Jellyfin or Emby server, and the preferences stored on that account show up here.',
'accountPreferences.unavailable' => 'Can\'t reach this account',
'accountPreferences.loadFailed' => 'Couldn\'t load these preferences',
'accountPreferences.noPreference' => 'No preference',
'accountPreferences.notSet' => 'Not set',
'accountPreferences.groups.audioAndSubtitles' => 'Audio & subtitles',
'accountPreferences.groups.libraryDisplay' => 'Library',
'accountPreferences.groups.personalMedia' => 'Personal media',
'accountPreferences.preferredAudioLanguage' => 'Preferred audio language',
'accountPreferences.autoSelectAudio' => 'Choose audio by language',
'accountPreferences.autoSelectAudioDescription' => 'Off keeps whatever audio track the file marks as default.',
'accountPreferences.preferredSubtitleLanguage' => 'Preferred subtitle language',
'accountPreferences.subtitleMode' => 'Turn subtitles on',
'accountPreferences.subtitleModes.none' => 'Manually selected',
'accountPreferences.subtitleModes.noneDescription' => 'Never turn subtitles on by itself.',
'accountPreferences.subtitleModes.defaultMode' => 'Follow the track flags',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Use the default and forced flags stored on each subtitle track.',
'accountPreferences.subtitleModes.always' => 'Always enabled',
'accountPreferences.subtitleModes.alwaysDescription' => 'Turn on a subtitle track in the preferred language whenever one exists.',
'accountPreferences.subtitleModes.onlyForced' => 'Only forced subtitles',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Load only the tracks marked as forced.',
'accountPreferences.subtitleModes.smart' => 'Shown with foreign audio',
'accountPreferences.subtitleModes.smartDescription' => 'Turn subtitles on only when the audio is in another language.',
'accountPreferences.subtitleAccessibility' => 'SDH subtitles',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Prefer non-SDH subtitles',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Prefer SDH subtitles',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Only SDH subtitles',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Only non-SDH subtitles',
'accountPreferences.forcedSubtitles' => 'Forced subtitles',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Prefer non-forced subtitles',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Prefer forced subtitles',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Only forced subtitles',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Only non-forced subtitles',
'accountPreferences.displayMissingEpisodes' => 'Show missing episodes',
'accountPreferences.displayMissingEpisodesDescription' => 'List episodes the server knows about but has no file for.',
'accountPreferences.hidePlayedInLatest' => 'Hide watched items in Latest',
'accountPreferences.hidePlayedInLatestDescription' => 'Leave items you have already watched out of the server\'s Latest rows.',
'accountPreferences.displayCollectionsView' => 'Show the Collections view',
'accountPreferences.displayCollectionsViewDescription' => 'Offer the server\'s Collections view alongside your libraries.',
'accountPreferences.rewatchingInNextUp' => 'Keep rewatched shows in Next Up',
'accountPreferences.rewatchingInNextUpDescription' => 'Once you finish a show, start it again and Next Up follows the rewatch instead of dropping the show.',
'accountPreferences.watchedIndicator' => 'Watched indicators',
'accountPreferences.watchedIndicatorOptions.none' => 'Never',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Movies and TV shows',
'accountPreferences.watchedIndicatorOptions.movies' => 'Movies only',
'accountPreferences.watchedIndicatorOptions.shows' => 'TV shows only',
'accountPreferences.mediaReviewsVisibility' => 'Ratings & reviews',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Users and critics',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Users only',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Critics only',
'accountPreferences.mediaReviewsOptions.nobody' => 'Hidden',
'discover.title' => 'Discover',
'discover.noContentAvailable' => 'No content available',
'discover.addMediaToLibraries' => 'Add some media to your libraries',
@@ -7506,6 +7973,8 @@ extension on Translations {
'libraries.groupings.tracks' => 'Tracks',
'libraries.groupings.folders' => 'Folders',
'libraries.filterCategories.genre' => 'Genre',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.year' => 'Year',
'libraries.filterCategories.contentRating' => 'Content Rating',
'libraries.filterCategories.tag' => 'Tag',
@@ -7591,8 +8060,6 @@ extension on Translations {
'explore.rows.watchlist' => 'Watchlist',
'explore.rows.recommendedMovies' => 'Recommended Movies',
'explore.rows.recommendedShows' => 'Recommended Shows',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Trending Movies',
'explore.rows.trendingShows' => 'Trending Shows',
'explore.rows.popularMovies' => 'Popular Movies',
@@ -7745,6 +8212,11 @@ extension on Translations {
'liveTv.unknownChannel' => 'Unknown channel',
'liveTv.live' => 'LIVE',
'liveTv.reloadGuide' => 'Reload Guide',
'liveTv.searchGuide' => 'Search Guide',
'liveTv.searchHint' => 'Search channels & programs',
'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"',
'liveTv.channelsSection' => 'Channels',
'liveTv.programsSection' => 'Programs',
'liveTv.now' => 'Now',
'liveTv.today' => 'Today',
'liveTv.tomorrow' => 'Tomorrow',
@@ -7882,6 +8354,8 @@ extension on Translations {
'music.repeatAll' => 'Repeat all',
'music.repeatOne' => 'Repeat one',
'music.instantMixNoServer' => 'No server is available for an instant mix',
'music.instantMixFailed' => 'Failed to load the instant mix',
'music.instantMixEmpty' => 'The instant mix came back with no tracks',
'music.noAudioUrl' => ({required Object track}) => 'No audio URL is available for ${track}',
'music.discography.singlesAndEps' => 'Singles & EPs',
'music.discography.live' => 'Live',
@@ -7909,6 +8383,13 @@ extension on Translations {
'watchTogether.host' => 'Host',
'watchTogether.hostBadge' => 'HOST',
'watchTogether.youAreHost' => 'You are the host',
'watchTogether.makeHost' => 'Make host',
'watchTogether.makeHostQuestion' => 'Transfer host?',
'watchTogether.makeHostConfirm' => ({required Object name}) => '${name} will control playback and drive the session for everyone.',
'watchTogether.transfer' => 'Transfer',
'watchTogether.hostChangedTo' => ({required Object name}) => '${name} is now the host',
'watchTogether.youAreNowHost' => 'You are now the host',
'watchTogether.hostTransferFailed' => ({required Object name}) => 'Couldn\'t make ${name} the host',
'watchTogether.watchingWithOthers' => 'Watching with others',
'watchTogether.endSession' => 'End Session',
'watchTogether.leaveSession' => 'Leave Session',
@@ -8006,6 +8487,8 @@ extension on Translations {
'downloads.editSyncRule' => 'Edit sync rule',
'downloads.removeSyncRule' => 'Remove sync rule',
'downloads.removeSyncRuleConfirm' => ({required Object title}) => 'Stop syncing "${title}"? Downloaded episodes will be kept.',
_ => null,
} ?? switch (path) {
'downloads.removeListSyncRuleConfirm' => ({required Object title}) => 'Stop syncing "${title}"?',
'downloads.deleteSyncRuleDownloads' => 'Also delete associated downloads',
'downloads.deleteSyncRuleDownloadsDescription' => 'Downloads used by another sync rule or profile will be kept.',
@@ -8105,8 +8588,6 @@ extension on Translations {
'companionRemote.pairing.availableDevices' => 'Available Devices',
'companionRemote.pairing.manualConnection' => 'Manual Connection',
'companionRemote.pairing.cryptoInitFailed' => 'Couldn\'t start secure connection. Sign in to Plex first.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Please enter host address',
'companionRemote.pairing.validationHostFormat' => 'Format must be IP:port (e.g., 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Connection timed out. Use the same network on both devices.',
@@ -8366,6 +8847,8 @@ extension on Translations {
'seerr.statusBlocklisted' => 'Blocklisted',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Could not reach ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'No Seerr instance at ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Enter a server address like https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'This Seerr instance does not support Quick Connect. It needs Seerr 3.4 or newer.',
'seerr.notInitialized' => 'This Seerr instance has not completed first-run setup',
'seerr.noPlexTokenForReauth' => 'No Plex token is available to sign in again',
'seerr.noStoredCredentials' => 'No stored credentials are available to sign in again',
@@ -8376,6 +8859,7 @@ extension on Translations {
'seerr.sessionRejectedAfterReauth' => 'The session was rejected after signing in again',
'services.title' => 'Services',
'services.hubSubtitle' => 'Sync watch progress and request new titles.',
'services.integrations' => 'Integrations',
'services.notConnected' => 'Not connected',
'services.connectedAs' => ({required Object username}) => 'Connected as @${username}',
'services.scrobble' => 'Track progress automatically',
+282 -30
View File
@@ -60,6 +60,7 @@ class TranslationsEs extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$es dialog = _Translations$dialog$es._(_root);
@override late final _Translations$profiles$es profiles = _Translations$profiles$es._(_root);
@override late final _Translations$connections$es connections = _Translations$connections$es._(_root);
@override late final _Translations$accountPreferences$es accountPreferences = _Translations$accountPreferences$es._(_root);
@override late final _Translations$discover$es discover = _Translations$discover$es._(_root);
@override late final _Translations$errors$es errors = _Translations$errors$es._(_root);
@override late final _Translations$libraries$es libraries = _Translations$libraries$es._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$es extends Translations$auth$en {
@override String get useBrowser => 'Usar navegador';
@override String get or => 'o';
@override String connectToMediaBrowser({required Object product}) => 'Conectar a ${product}';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Usar Quick Connect';
@override String get quickConnectInstructions => 'Abre Quick Connect en Jellyfin e introduce este código.';
@override String get quickConnectWaiting => 'Esperando aprobación…';
@@ -165,6 +167,7 @@ class _Translations$common$es extends Translations$common$en {
@override String get mute => 'Silenciar';
@override String get ok => 'OK';
@override String get off => 'Desactivado';
@override String get options => 'Opciones';
@override String seasonNumber({required Object number}) => 'Temporada ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Episodio ${number} - ${title}';
@override String chapterNumber({required Object number}) => 'Capítulo ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$es extends Translations$settings$en {
@override String get displayScale => 'Escala de visualización';
@override String get compact => 'Compacto';
@override String get comfortable => 'Cómodo';
@override String get gridSpacing => 'Espaciado de la cuadrícula';
@override String get gridSpacingTight => 'Ajustado';
@override String get gridSpacingNormal => 'Normal';
@override String get gridSpacingSpacious => 'Espacioso';
@override String get tvCornerSpotlightBackdrop => 'Imagen destacada en la esquina';
@override String get tvCornerSpotlightBackdropDescription => 'Mostrar la imagen destacada en la esquina superior derecha en lugar de ocupar toda la pantalla';
@override String get viewMode => 'Modo de vista';
@@ -292,16 +299,14 @@ class _Translations$settings$es extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Decodificación por hardware';
@override String get hardwareDecodingDescription => 'Usar aceleración por hardware cuando esté disponible';
@override String get bufferSize => 'Tamaño del búfer';
@override String bufferSizeMB({required Object size}) => '${size} MB';
@override String get bufferSizeAuto => 'Automático (recomendado)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} MB de memoria disponible. Un búfer de ${size} MB puede afectar a la reproducción.';
@override String get playbackBuffer => 'Búfer de reproducción';
@override String get playbackBufferAuto => 'Automático (recomendado)';
@override String get playbackBufferLarge => 'Grande';
@override String get playbackBufferExtraLarge => 'Extra grande';
@override String get playbackBufferDescription => 'Almacena más en búfer para conexiones inestables. También limitado por el tamaño del búfer.';
@override String get defaultQualityTitle => 'Calidad predeterminada';
@override String get cellularQualityTitle => 'Calidad predeterminada en datos móviles';
@override String get cellularQualitySameAsDefault => 'Igual que la calidad predeterminada';
@override String get musicQualityTitle => 'Calidad de música';
@override String get subtitleStyling => 'Estilo de subtítulos';
@override String get subtitleStylingDescription => 'Personalizar la apariencia de los subtítulos';
@@ -315,6 +320,8 @@ class _Translations$settings$es extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Recordar opciones de audio y subtítulos por título';
@override String get followServerTrackSelections => 'Usar la selección de pistas del servidor por episodio';
@override String get followServerTrackSelectionsDescription => 'Al cambiar de episodio, aplicar el audio y los subtítulos seleccionados en el servidor en lugar de mantener la elección actual';
@override String get resumeMusicOnLaunch => 'Recordar la sesión de música';
@override String get resumeMusicOnLaunchDescription => 'Al iniciar la aplicación, reabrir la última canción en pausa donde se quedó';
@override String get showChapterMarkersOnTimeline => 'Mostrar marcadores de capítulos en la barra de progreso';
@override String get showChapterMarkersOnTimelineDescription => 'Dividir la barra de progreso en los límites de capítulos';
@override String get specialsOrdering => 'Especiales en orden de episodios';
@@ -368,7 +375,11 @@ class _Translations$settings$es extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'El atajo ya está asignado a ${action}';
@override String shortcutUpdated({required Object action}) => 'Atajo actualizado para ${action}';
@override String get saveFailed => 'No se pudieron guardar los cambios. Inténtalo de nuevo.';
@override String get autoSkip => 'Salto automático';
@override String get autoPlayAndSkip => 'Reproducción automática y saltos';
@override String get autoPlayNextEpisode => 'Reproducción automática del siguiente episodio';
@override String get autoPlayNextEpisodeDescription => 'Iniciar automáticamente el siguiente episodio cuando termine el actual';
@override String get playNextCountdown => 'Cuenta atrás para el siguiente episodio';
@override String get playNextCountdownImmediate => 'Reproducir de inmediato';
@override String get autoSkipIntro => 'Saltar introducción automáticamente';
@override String get autoSkipIntroDescription => 'Saltar automáticamente los marcadores de introducción después de unos segundos';
@override String get autoSkipCredits => 'Saltar créditos automáticamente';
@@ -443,10 +454,8 @@ class _Translations$settings$es extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Forzar DV7 nativo y suprimir el reintento de conversión DV';
@override String get dvConversionDv81Description => 'Forzar la conversión de RPU en línea al perfil 8.1 de Dolby Vision';
@override String get dvConversionHevcStripDescription => 'Eliminar las capas RPU/EL de Dolby Vision y presentar HEVC convencional';
@override String get demuxer => 'Demultiplexor de contenedor';
@override String get demuxerDescription => 'Qué demultiplexor analiza los archivos de reproducción directa. Cambia a media3 si un archivo da problemas.';
@override String get demuxerFfmpeg => 'FFmpeg (recomendado)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Desentrelazado';
@override String get deinterlaceDescription => 'Elimina los artefactos de peine del video entrelazado (solo reproductor mpv)';
@override String get requireProfileSelectionOnOpen => 'Pedir perfil al abrir la aplicación';
@override String get requireProfileSelectionOnOpenDescription => 'Mostrar selección de perfil cada vez que se abre la aplicación';
@override String get forceTvMode => 'Forzar modo TV';
@@ -464,15 +473,31 @@ class _Translations$settings$es extends Translations$settings$en {
@override String get showExploreTabDescription => 'Muestra la pestaña Explorar con contenido de Plex Discover y servicios de seguimiento conectados';
@override String get liveTvDefaultFavorites => 'Canales favoritos por defecto';
@override String get liveTvDefaultFavoritesDescription => 'Mostrar solo canales favoritos al abrir TV en vivo';
@override String get general => 'General';
@override String get generalDescription => 'Idioma, inicio y comportamiento de la ventana';
@override String get languageAndRegion => 'Idioma y región';
@override String get startup => 'Inicio';
@override String get display => 'Pantalla';
@override String get libraryAndCards => 'Biblioteca y tarjetas';
@override String get homeScreen => 'Pantalla de inicio';
@override String get navigation => 'Navegación';
@override String get window => 'Ventana';
@override String get content => 'Contenido';
@override String get liveTv => 'TV en vivo';
@override String get player => 'Reproductor';
@override String get subtitlesAndConfig => 'Subtítulos y configuración';
@override String get videoAndDisplay => 'Video y pantalla';
@override String get audio => 'Audio';
@override String get quality => 'Calidad';
@override String get subtitles => 'Subtítulos';
@override String get seekAndTiming => 'Desplazamiento y tiempos';
@override String get behavior => 'Comportamiento';
@override String get gestures => 'Gestos';
@override String get gestureBrightnessSwipe => 'Gesto de brillo';
@override String get gestureBrightnessSwipeDescription => 'Desliza hacia arriba o abajo en el borde izquierdo para ajustar el brillo';
@override String get gestureVolumeSwipe => 'Gesto de volumen';
@override String get gestureVolumeSwipeDescription => 'Desliza hacia arriba o abajo en el borde derecho para ajustar el volumen';
@override String get gesturePinchToZoom => 'Pellizcar para hacer zoom';
@override String get gesturePinchToZoomDescription => 'Pellizca el video para acercar o alejar';
@override String get controls => 'Controles';
@override String get rememberPlayerChanges => 'Recordar cambios del reproductor';
@override String get rememberPlayerChangesDescription => 'Dónde se guarda y se vuelve a aplicar un cambio realizado durante la reproducción';
@override String get scopePlaybackSpeed => 'Velocidad de reproducción';
@@ -1086,6 +1111,49 @@ class _Translations$connections$es extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Añade o elimina URL para ${serverName}. Plezy usará la URL accesible con menor latencia.';
}
// Path: accountPreferences
class _Translations$accountPreferences$es extends Translations$accountPreferences$en {
_Translations$accountPreferences$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Preferencias de cuenta';
@override String hubSubtitleSingle({required Object account}) => 'Opciones de audio, subtítulos y biblioteca guardadas en ${account}';
@override String hubSubtitleMultiple({required Object count}) => 'Opciones de audio, subtítulos y biblioteca guardadas en ${count} cuentas';
@override String get pickAccount => 'Cada cuenta guarda sus propias preferencias. Elige la que quieras editar.';
@override String get storedOnAccount => 'Estas opciones se guardan en la propia cuenta, así que cualquier aplicación con la sesión iniciada las usa, incluido Plezy en tus otros dispositivos.';
@override String get noAccounts => 'No hay cuentas que configurar';
@override String get noAccountsHint => 'Inicia sesión con Plex o conecta un servidor de Jellyfin o Emby y las preferencias guardadas en esa cuenta aparecerán aquí.';
@override String get unavailable => 'No se puede acceder a esta cuenta';
@override String get loadFailed => 'No se pudieron cargar estas preferencias';
@override String get noPreference => 'Sin preferencia';
@override String get notSet => 'No establecido';
@override late final _Translations$accountPreferences$groups$es groups = _Translations$accountPreferences$groups$es._(_root);
@override String get preferredAudioLanguage => 'Idioma de audio preferido';
@override String get autoSelectAudio => 'Elegir audio por idioma';
@override String get autoSelectAudioDescription => 'Desactivado mantiene la pista de audio que el archivo marca como predeterminada.';
@override String get preferredSubtitleLanguage => 'Idioma de subtítulos preferido';
@override String get subtitleMode => 'Activar subtítulos';
@override late final _Translations$accountPreferences$subtitleModes$es subtitleModes = _Translations$accountPreferences$subtitleModes$es._(_root);
@override String get subtitleAccessibility => 'Subtítulos SDH';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$es subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$es._(_root);
@override String get forcedSubtitles => 'Subtítulos forzados';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$es forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$es._(_root);
@override String get displayMissingEpisodes => 'Mostrar episodios faltantes';
@override String get displayMissingEpisodesDescription => 'Enumera los episodios que el servidor conoce pero para los que no tiene ningún archivo.';
@override String get hidePlayedInLatest => 'Ocultar elementos vistos en Recientes';
@override String get hidePlayedInLatestDescription => 'Deja fuera de las filas de Recientes del servidor los elementos que ya has visto.';
@override String get displayCollectionsView => 'Mostrar la vista de colecciones';
@override String get displayCollectionsViewDescription => 'Ofrecer la vista de colecciones del servidor junto a tus bibliotecas.';
@override String get rewatchingInNextUp => 'Mantener las series revistas en A continuación';
@override String get rewatchingInNextUpDescription => 'Cuando termines una serie y la vuelvas a empezar, A continuación seguirá tu revisión en lugar de descartarla.';
@override String get watchedIndicator => 'Indicadores de visto';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$es watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$es._(_root);
@override String get mediaReviewsVisibility => 'Valoraciones y reseñas';
@override late final _Translations$accountPreferences$mediaReviewsOptions$es mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$es._(_root);
}
// Path: discover
class _Translations$discover$es extends Translations$discover$en {
_Translations$discover$es._(TranslationsEs root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$es extends Translations$liveTv$en {
@override String get unknownChannel => 'Canal desconocido';
@override String get live => 'EN VIVO';
@override String get reloadGuide => 'Recargar guía';
@override String get searchGuide => 'Buscar en la guía';
@override String get searchHint => 'Buscar canales y programas';
@override String searchNoResults({required Object query}) => 'Sin coincidencias para "${query}"';
@override String get channelsSection => 'Canales';
@override String get programsSection => 'Programas';
@override String get now => 'Ahora';
@override String get today => 'Hoy';
@override String get tomorrow => 'Mañana';
@@ -1545,6 +1618,8 @@ class _Translations$music$es extends Translations$music$en {
@override String get repeatAll => 'Repetir todo';
@override String get repeatOne => 'Repetir una';
@override String get instantMixNoServer => 'No hay ningún servidor disponible para una mezcla instantánea';
@override String get instantMixFailed => 'Error al cargar la mezcla instantánea';
@override String get instantMixEmpty => 'La mezcla instantánea no devolvió ninguna canción';
@override String noAudioUrl({required Object track}) => 'No hay ninguna URL de audio disponible para ${track}';
@override late final _Translations$music$discography$es discography = _Translations$music$discography$es._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$es extends Translations$seerr$en {
@override String get statusBlocklisted => 'En la lista de bloqueo';
@override String couldNotReach({required Object url, required Object error}) => 'No se pudo conectar con ${url}: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'No hay ninguna instancia de Seerr en ${url} (HTTP ${status})';
@override String get invalidUrl => 'Introduce una dirección de servidor como https://seerr.example.com';
@override String get quickConnectUnsupported => 'Esta instancia de Seerr no admite Quick Connect. Necesita Seerr 3.4 o más reciente.';
@override String get notInitialized => 'Esta instancia de Seerr no ha completado la configuración inicial';
@override String get noPlexTokenForReauth => 'No hay ningún token de Plex disponible para volver a iniciar sesión';
@override String get noStoredCredentials => 'No hay credenciales guardadas disponibles para volver a iniciar sesión';
@@ -2066,6 +2143,7 @@ class _Translations$services$es extends Translations$services$en {
// Translations
@override String get title => 'Servicios';
@override String get hubSubtitle => 'Sincroniza tu progreso de visualización y solicita nuevos títulos.';
@override String get integrations => 'Integraciones';
@override String get notConnected => 'No conectado';
@override String connectedAs({required Object username}) => 'Conectado como @${username}';
@override String get scrobble => 'Registrar progreso automáticamente';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$es extends Translations$videoControl
@override String unknown({required Object error}) => 'Ocurrió un error: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$es extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Audio y subtítulos';
@override String get libraryDisplay => 'Biblioteca';
@override String get personalMedia => 'Medios personales';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$es extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get none => 'Seleccionado manualmente';
@override String get noneDescription => 'No activar los subtítulos por sí solo.';
@override String get defaultMode => 'Seguir las marcas de la pista';
@override String get defaultModeDescription => 'Usar las marcas de predeterminada y forzada guardadas en cada pista de subtítulos.';
@override String get always => 'Siempre activado';
@override String get alwaysDescription => 'Activar una pista de subtítulos en el idioma preferido siempre que exista.';
@override String get onlyForced => 'Solo subtítulos forzados';
@override String get onlyForcedDescription => 'Cargar solo las pistas marcadas como forzadas.';
@override String get smart => 'Mostrar con audio extranjero';
@override String get smartDescription => 'Activar los subtítulos solo cuando el audio está en otro idioma.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$es extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Preferir subtítulos no SDH';
@override String get preferSdh => 'Preferir subtítulos SDH';
@override String get onlySdh => 'Solo subtítulos SDH';
@override String get onlyNonSdh => 'Solo subtítulos no SDH';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$es extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Preferir subtítulos no forzados';
@override String get preferForced => 'Preferir subtítulos forzados';
@override String get onlyForced => 'Solo subtítulos forzados';
@override String get onlyNonForced => 'Solo subtítulos no forzados';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$es extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get none => 'Nunca';
@override String get moviesAndShows => 'Películas y series de TV';
@override String get movies => 'Solo películas';
@override String get shows => 'Solo series de TV';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$es extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$es._(TranslationsEs root) : this._root = root, super.internal(root);
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Usuarios y críticos';
@override String get usersOnly => 'Solo usuarios';
@override String get criticsOnly => 'Solo críticos';
@override String get nobody => 'Ocultos';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$es extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$es._(TranslationsEs root) : this._root = root, super.internal(root);
@@ -2348,7 +2509,7 @@ class _Translations$explore$badge$es extends Translations$explore$badge$en {
final TranslationsEs _root; // ignore: unused_field
// Translations
@override String rankPopular({required Object n}) => '#${n} popular';
@override String rankPopular({required Object n}) => '#${n} más popular';
@override String rankAiring({required Object n}) => '#${n} en emisión';
@override String rankRated({required Object n}) => '#${n} puntuado';
@override String rankTrending({required Object n}) => '#${n} en tendencia';
@@ -2786,6 +2947,7 @@ extension on TranslationsEs {
'auth.useBrowser' => 'Usar navegador',
'auth.or' => 'o',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Conectar a ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Usar Quick Connect',
'auth.quickConnectInstructions' => 'Abre Quick Connect en Jellyfin e introduce este código.',
'auth.quickConnectWaiting' => 'Esperando aprobación…',
@@ -2825,6 +2987,7 @@ extension on TranslationsEs {
'common.mute' => 'Silenciar',
'common.ok' => 'OK',
'common.off' => 'Desactivado',
'common.options' => 'Opciones',
'common.seasonNumber' => ({required Object number}) => 'Temporada ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Episodio ${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'Capítulo ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsEs {
'settings.displayScale' => 'Escala de visualización',
'settings.compact' => 'Compacto',
'settings.comfortable' => 'Cómodo',
'settings.gridSpacing' => 'Espaciado de la cuadrícula',
'settings.gridSpacingTight' => 'Ajustado',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Espacioso',
'settings.tvCornerSpotlightBackdrop' => 'Imagen destacada en la esquina',
'settings.tvCornerSpotlightBackdropDescription' => 'Mostrar la imagen destacada en la esquina superior derecha en lugar de ocupar toda la pantalla',
'settings.viewMode' => 'Modo de vista',
@@ -2946,16 +3113,14 @@ extension on TranslationsEs {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Decodificación por hardware',
'settings.hardwareDecodingDescription' => 'Usar aceleración por hardware cuando esté disponible',
'settings.bufferSize' => 'Tamaño del búfer',
'settings.bufferSizeMB' => ({required Object size}) => '${size} MB',
'settings.bufferSizeAuto' => 'Automático (recomendado)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} MB de memoria disponible. Un búfer de ${size} MB puede afectar a la reproducción.',
'settings.playbackBuffer' => 'Búfer de reproducción',
'settings.playbackBufferAuto' => 'Automático (recomendado)',
'settings.playbackBufferLarge' => 'Grande',
'settings.playbackBufferExtraLarge' => 'Extra grande',
'settings.playbackBufferDescription' => 'Almacena más en búfer para conexiones inestables. También limitado por el tamaño del búfer.',
'settings.defaultQualityTitle' => 'Calidad predeterminada',
'settings.cellularQualityTitle' => 'Calidad predeterminada en datos móviles',
'settings.cellularQualitySameAsDefault' => 'Igual que la calidad predeterminada',
'settings.musicQualityTitle' => 'Calidad de música',
'settings.subtitleStyling' => 'Estilo de subtítulos',
'settings.subtitleStylingDescription' => 'Personalizar la apariencia de los subtítulos',
@@ -2969,6 +3134,8 @@ extension on TranslationsEs {
'settings.rememberTrackSelectionsDescription' => 'Recordar opciones de audio y subtítulos por título',
'settings.followServerTrackSelections' => 'Usar la selección de pistas del servidor por episodio',
'settings.followServerTrackSelectionsDescription' => 'Al cambiar de episodio, aplicar el audio y los subtítulos seleccionados en el servidor en lugar de mantener la elección actual',
'settings.resumeMusicOnLaunch' => 'Recordar la sesión de música',
'settings.resumeMusicOnLaunchDescription' => 'Al iniciar la aplicación, reabrir la última canción en pausa donde se quedó',
'settings.showChapterMarkersOnTimeline' => 'Mostrar marcadores de capítulos en la barra de progreso',
'settings.showChapterMarkersOnTimelineDescription' => 'Dividir la barra de progreso en los límites de capítulos',
'settings.specialsOrdering' => 'Especiales en orden de episodios',
@@ -3022,7 +3189,11 @@ extension on TranslationsEs {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'El atajo ya está asignado a ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Atajo actualizado para ${action}',
'settings.saveFailed' => 'No se pudieron guardar los cambios. Inténtalo de nuevo.',
'settings.autoSkip' => 'Salto automático',
'settings.autoPlayAndSkip' => 'Reproducción automática y saltos',
'settings.autoPlayNextEpisode' => 'Reproducción automática del siguiente episodio',
'settings.autoPlayNextEpisodeDescription' => 'Iniciar automáticamente el siguiente episodio cuando termine el actual',
'settings.playNextCountdown' => 'Cuenta atrás para el siguiente episodio',
'settings.playNextCountdownImmediate' => 'Reproducir de inmediato',
'settings.autoSkipIntro' => 'Saltar introducción automáticamente',
'settings.autoSkipIntroDescription' => 'Saltar automáticamente los marcadores de introducción después de unos segundos',
'settings.autoSkipCredits' => 'Saltar créditos automáticamente',
@@ -3097,10 +3268,8 @@ extension on TranslationsEs {
'settings.dvConversionNativeDescription' => 'Forzar DV7 nativo y suprimir el reintento de conversión DV',
'settings.dvConversionDv81Description' => 'Forzar la conversión de RPU en línea al perfil 8.1 de Dolby Vision',
'settings.dvConversionHevcStripDescription' => 'Eliminar las capas RPU/EL de Dolby Vision y presentar HEVC convencional',
'settings.demuxer' => 'Demultiplexor de contenedor',
'settings.demuxerDescription' => 'Qué demultiplexor analiza los archivos de reproducción directa. Cambia a media3 si un archivo da problemas.',
'settings.demuxerFfmpeg' => 'FFmpeg (recomendado)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Desentrelazado',
'settings.deinterlaceDescription' => 'Elimina los artefactos de peine del video entrelazado (solo reproductor mpv)',
'settings.requireProfileSelectionOnOpen' => 'Pedir perfil al abrir la aplicación',
'settings.requireProfileSelectionOnOpenDescription' => 'Mostrar selección de perfil cada vez que se abre la aplicación',
'settings.forceTvMode' => 'Forzar modo TV',
@@ -3118,15 +3287,31 @@ extension on TranslationsEs {
'settings.showExploreTabDescription' => 'Muestra la pestaña Explorar con contenido de Plex Discover y servicios de seguimiento conectados',
'settings.liveTvDefaultFavorites' => 'Canales favoritos por defecto',
'settings.liveTvDefaultFavoritesDescription' => 'Mostrar solo canales favoritos al abrir TV en vivo',
'settings.general' => 'General',
'settings.generalDescription' => 'Idioma, inicio y comportamiento de la ventana',
'settings.languageAndRegion' => 'Idioma y región',
'settings.startup' => 'Inicio',
'settings.display' => 'Pantalla',
'settings.libraryAndCards' => 'Biblioteca y tarjetas',
'settings.homeScreen' => 'Pantalla de inicio',
'settings.navigation' => 'Navegación',
'settings.window' => 'Ventana',
'settings.content' => 'Contenido',
'settings.liveTv' => 'TV en vivo',
'settings.player' => 'Reproductor',
'settings.subtitlesAndConfig' => 'Subtítulos y configuración',
'settings.videoAndDisplay' => 'Video y pantalla',
'settings.audio' => 'Audio',
'settings.quality' => 'Calidad',
'settings.subtitles' => 'Subtítulos',
'settings.seekAndTiming' => 'Desplazamiento y tiempos',
'settings.behavior' => 'Comportamiento',
'settings.gestures' => 'Gestos',
'settings.gestureBrightnessSwipe' => 'Gesto de brillo',
'settings.gestureBrightnessSwipeDescription' => 'Desliza hacia arriba o abajo en el borde izquierdo para ajustar el brillo',
'settings.gestureVolumeSwipe' => 'Gesto de volumen',
'settings.gestureVolumeSwipeDescription' => 'Desliza hacia arriba o abajo en el borde derecho para ajustar el volumen',
'settings.gesturePinchToZoom' => 'Pellizcar para hacer zoom',
'settings.gesturePinchToZoomDescription' => 'Pellizca el video para acercar o alejar',
'settings.controls' => 'Controles',
'settings.rememberPlayerChanges' => 'Recordar cambios del reproductor',
'settings.rememberPlayerChangesDescription' => 'Dónde se guarda y se vuelve a aplicar un cambio realizado durante la reproducción',
'settings.scopePlaybackSpeed' => 'Velocidad de reproducción',
@@ -3264,6 +3449,8 @@ extension on TranslationsEs {
'fileInfo.has64bitOffsets' => 'Desplazamientos de 64 bits',
'fileInfo.protocol' => 'Protocolo',
'fileInfo.mediaType' => 'Tipo de medio',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Tipo de origen',
'fileInfo.optimizedVersion' => 'Versión optimizada',
'fileInfo.optimizationTarget' => 'Destino de optimización',
@@ -3288,8 +3475,6 @@ extension on TranslationsEs {
'fileInfo.flagOriginal' => 'Original',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Perfil ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Marcar como visto',
'mediaMenu.markAsUnwatched' => 'Marcar como no visto',
'mediaMenu.removeFromContinueWatching' => 'Eliminar de Seguir viendo',
@@ -3624,6 +3809,63 @@ extension on TranslationsEs {
'connections.signInAgain' => 'Iniciar sesión de nuevo',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Editar conexión de ${product}',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Añade o elimina URL para ${serverName}. Plezy usará la URL accesible con menor latencia.',
'accountPreferences.sectionTitle' => 'Preferencias de cuenta',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Opciones de audio, subtítulos y biblioteca guardadas en ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Opciones de audio, subtítulos y biblioteca guardadas en ${count} cuentas',
'accountPreferences.pickAccount' => 'Cada cuenta guarda sus propias preferencias. Elige la que quieras editar.',
'accountPreferences.storedOnAccount' => 'Estas opciones se guardan en la propia cuenta, así que cualquier aplicación con la sesión iniciada las usa, incluido Plezy en tus otros dispositivos.',
'accountPreferences.noAccounts' => 'No hay cuentas que configurar',
'accountPreferences.noAccountsHint' => 'Inicia sesión con Plex o conecta un servidor de Jellyfin o Emby y las preferencias guardadas en esa cuenta aparecerán aquí.',
'accountPreferences.unavailable' => 'No se puede acceder a esta cuenta',
'accountPreferences.loadFailed' => 'No se pudieron cargar estas preferencias',
'accountPreferences.noPreference' => 'Sin preferencia',
'accountPreferences.notSet' => 'No establecido',
'accountPreferences.groups.audioAndSubtitles' => 'Audio y subtítulos',
'accountPreferences.groups.libraryDisplay' => 'Biblioteca',
'accountPreferences.groups.personalMedia' => 'Medios personales',
'accountPreferences.preferredAudioLanguage' => 'Idioma de audio preferido',
'accountPreferences.autoSelectAudio' => 'Elegir audio por idioma',
'accountPreferences.autoSelectAudioDescription' => 'Desactivado mantiene la pista de audio que el archivo marca como predeterminada.',
'accountPreferences.preferredSubtitleLanguage' => 'Idioma de subtítulos preferido',
'accountPreferences.subtitleMode' => 'Activar subtítulos',
'accountPreferences.subtitleModes.none' => 'Seleccionado manualmente',
'accountPreferences.subtitleModes.noneDescription' => 'No activar los subtítulos por sí solo.',
'accountPreferences.subtitleModes.defaultMode' => 'Seguir las marcas de la pista',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Usar las marcas de predeterminada y forzada guardadas en cada pista de subtítulos.',
'accountPreferences.subtitleModes.always' => 'Siempre activado',
'accountPreferences.subtitleModes.alwaysDescription' => 'Activar una pista de subtítulos en el idioma preferido siempre que exista.',
'accountPreferences.subtitleModes.onlyForced' => 'Solo subtítulos forzados',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Cargar solo las pistas marcadas como forzadas.',
'accountPreferences.subtitleModes.smart' => 'Mostrar con audio extranjero',
'accountPreferences.subtitleModes.smartDescription' => 'Activar los subtítulos solo cuando el audio está en otro idioma.',
'accountPreferences.subtitleAccessibility' => 'Subtítulos SDH',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Preferir subtítulos no SDH',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Preferir subtítulos SDH',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Solo subtítulos SDH',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Solo subtítulos no SDH',
'accountPreferences.forcedSubtitles' => 'Subtítulos forzados',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Preferir subtítulos no forzados',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Preferir subtítulos forzados',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Solo subtítulos forzados',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Solo subtítulos no forzados',
'accountPreferences.displayMissingEpisodes' => 'Mostrar episodios faltantes',
'accountPreferences.displayMissingEpisodesDescription' => 'Enumera los episodios que el servidor conoce pero para los que no tiene ningún archivo.',
'accountPreferences.hidePlayedInLatest' => 'Ocultar elementos vistos en Recientes',
'accountPreferences.hidePlayedInLatestDescription' => 'Deja fuera de las filas de Recientes del servidor los elementos que ya has visto.',
'accountPreferences.displayCollectionsView' => 'Mostrar la vista de colecciones',
'accountPreferences.displayCollectionsViewDescription' => 'Ofrecer la vista de colecciones del servidor junto a tus bibliotecas.',
'accountPreferences.rewatchingInNextUp' => 'Mantener las series revistas en A continuación',
'accountPreferences.rewatchingInNextUpDescription' => 'Cuando termines una serie y la vuelvas a empezar, A continuación seguirá tu revisión en lugar de descartarla.',
'accountPreferences.watchedIndicator' => 'Indicadores de visto',
'accountPreferences.watchedIndicatorOptions.none' => 'Nunca',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Películas y series de TV',
'accountPreferences.watchedIndicatorOptions.movies' => 'Solo películas',
'accountPreferences.watchedIndicatorOptions.shows' => 'Solo series de TV',
'accountPreferences.mediaReviewsVisibility' => 'Valoraciones y reseñas',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Usuarios y críticos',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Solo usuarios',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Solo críticos',
'accountPreferences.mediaReviewsOptions.nobody' => 'Ocultos',
'discover.title' => 'Descubrir',
'discover.noContentAvailable' => 'No hay contenido disponible',
'discover.addMediaToLibraries' => 'Añade contenido a tus bibliotecas',
@@ -3721,6 +3963,8 @@ extension on TranslationsEs {
'libraries.filterCategories.contentRating' => 'Clasificación por edad',
'libraries.filterCategories.tag' => 'Etiqueta',
'libraries.filterCategories.unwatched' => 'No vistos',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'No reproducidos',
'libraries.filterCategories.favorites' => 'Favoritos',
'libraries.sortLabels.title' => 'Título',
@@ -3802,8 +4046,6 @@ extension on TranslationsEs {
'explore.rows.watchlist' => 'Lista de seguimiento',
'explore.rows.recommendedMovies' => 'Películas recomendadas',
'explore.rows.recommendedShows' => 'Series recomendadas',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Películas en tendencia',
'explore.rows.trendingShows' => 'Series en tendencia',
'explore.rows.popularMovies' => 'Películas populares',
@@ -3837,7 +4079,7 @@ extension on TranslationsEs {
'explore.searchEmpty' => ({required Object query}) => 'Sin resultados para "${query}"',
'explore.searchPrompt' => ({required Object source}) => 'Busca películas y series en ${source}.',
'explore.searchFailed' => 'La búsqueda falló. Comprueba tu conexión e inténtalo de nuevo.',
'explore.badge.rankPopular' => ({required Object n}) => '#${n} popular',
'explore.badge.rankPopular' => ({required Object n}) => '#${n} más popular',
'explore.badge.rankAiring' => ({required Object n}) => '#${n} en emisión',
'explore.badge.rankRated' => ({required Object n}) => '#${n} puntuado',
'explore.badge.rankTrending' => ({required Object n}) => '#${n} en tendencia',
@@ -3956,6 +4198,11 @@ extension on TranslationsEs {
'liveTv.unknownChannel' => 'Canal desconocido',
'liveTv.live' => 'EN VIVO',
'liveTv.reloadGuide' => 'Recargar guía',
'liveTv.searchGuide' => 'Buscar en la guía',
'liveTv.searchHint' => 'Buscar canales y programas',
'liveTv.searchNoResults' => ({required Object query}) => 'Sin coincidencias para "${query}"',
'liveTv.channelsSection' => 'Canales',
'liveTv.programsSection' => 'Programas',
'liveTv.now' => 'Ahora',
'liveTv.today' => 'Hoy',
'liveTv.tomorrow' => 'Mañana',
@@ -4093,6 +4340,8 @@ extension on TranslationsEs {
'music.repeatAll' => 'Repetir todo',
'music.repeatOne' => 'Repetir una',
'music.instantMixNoServer' => 'No hay ningún servidor disponible para una mezcla instantánea',
'music.instantMixFailed' => 'Error al cargar la mezcla instantánea',
'music.instantMixEmpty' => 'La mezcla instantánea no devolvió ninguna canción',
'music.noAudioUrl' => ({required Object track}) => 'No hay ninguna URL de audio disponible para ${track}',
'music.discography.singlesAndEps' => 'Sencillos y EP',
'music.discography.live' => 'En directo',
@@ -4228,6 +4477,8 @@ extension on TranslationsEs {
'downloads.syncRuleCleanupUnavailable' => 'No se pudieron identificar las descargas asociadas de forma segura. Vuelve a conectar el servidor e inténtalo de nuevo, o elimina la regla sin borrar las descargas.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => '${count} nuevos episodios sincronizados para ${title}',
'downloads.activeSyncRules' => 'Reglas de sincronización',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Sin reglas de sincronización',
'downloads.manageSyncRule' => 'Gestionar sincronización',
'downloads.editEpisodeCount' => 'Número de episodios',
@@ -4316,8 +4567,6 @@ extension on TranslationsEs {
'companionRemote.pairing.availableDevices' => 'Dispositivos disponibles',
'companionRemote.pairing.manualConnection' => 'Conexión manual',
'companionRemote.pairing.cryptoInitFailed' => 'No se pudo iniciar la conexión segura. Inicia sesión en Plex primero.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Introduce la dirección del host',
'companionRemote.pairing.validationHostFormat' => 'El formato debe ser IP:puerto (p. ej., 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Tiempo de conexión agotado. Usa la misma red en ambos dispositivos.',
@@ -4577,6 +4826,8 @@ extension on TranslationsEs {
'seerr.statusBlocklisted' => 'En la lista de bloqueo',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'No se pudo conectar con ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'No hay ninguna instancia de Seerr en ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Introduce una dirección de servidor como https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Esta instancia de Seerr no admite Quick Connect. Necesita Seerr 3.4 o más reciente.',
'seerr.notInitialized' => 'Esta instancia de Seerr no ha completado la configuración inicial',
'seerr.noPlexTokenForReauth' => 'No hay ningún token de Plex disponible para volver a iniciar sesión',
'seerr.noStoredCredentials' => 'No hay credenciales guardadas disponibles para volver a iniciar sesión',
@@ -4587,6 +4838,7 @@ extension on TranslationsEs {
'seerr.sessionRejectedAfterReauth' => 'La sesión fue rechazada después de volver a iniciar sesión',
'services.title' => 'Servicios',
'services.hubSubtitle' => 'Sincroniza tu progreso de visualización y solicita nuevos títulos.',
'services.integrations' => 'Integraciones',
'services.notConnected' => 'No conectado',
'services.connectedAs' => ({required Object username}) => 'Conectado como @${username}',
'services.scrobble' => 'Registrar progreso automáticamente',
+282 -30
View File
@@ -60,6 +60,7 @@ class TranslationsFr extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$fr dialog = _Translations$dialog$fr._(_root);
@override late final _Translations$profiles$fr profiles = _Translations$profiles$fr._(_root);
@override late final _Translations$connections$fr connections = _Translations$connections$fr._(_root);
@override late final _Translations$accountPreferences$fr accountPreferences = _Translations$accountPreferences$fr._(_root);
@override late final _Translations$discover$fr discover = _Translations$discover$fr._(_root);
@override late final _Translations$errors$fr errors = _Translations$errors$fr._(_root);
@override late final _Translations$libraries$fr libraries = _Translations$libraries$fr._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$fr extends Translations$auth$en {
@override String get useBrowser => 'Utiliser le navigateur';
@override String get or => 'ou';
@override String connectToMediaBrowser({required Object product}) => 'Se connecter à ${product}';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Utiliser Quick Connect';
@override String get quickConnectInstructions => 'Ouvrez Quick Connect dans Jellyfin et saisissez ce code.';
@override String get quickConnectWaiting => 'En attente d\'approbation…';
@@ -165,6 +167,7 @@ class _Translations$common$fr extends Translations$common$en {
@override String get mute => 'Couper le son';
@override String get ok => 'OK';
@override String get off => 'Désactivé';
@override String get options => 'Options';
@override String seasonNumber({required Object number}) => 'Saison ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Épisode ${number} – ${title}';
@override String chapterNumber({required Object number}) => 'Chapitre ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$fr extends Translations$settings$en {
@override String get displayScale => 'Échelle d\'affichage';
@override String get compact => 'Compact';
@override String get comfortable => 'Confortable';
@override String get gridSpacing => 'Espacement de la grille';
@override String get gridSpacingTight => 'Resserré';
@override String get gridSpacingNormal => 'Normal';
@override String get gridSpacingSpacious => 'Aéré';
@override String get tvCornerSpotlightBackdrop => 'Illustration en vedette dans le coin';
@override String get tvCornerSpotlightBackdropDescription => 'Afficher l’illustration en vedette dans le coin supérieur droit plutôt qu’en plein écran';
@override String get viewMode => 'Mode d\'affichage';
@@ -292,16 +299,14 @@ class _Translations$settings$fr extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Décodage matériel';
@override String get hardwareDecodingDescription => 'Utiliser l’accélération matérielle lorsqu’elle est disponible';
@override String get bufferSize => 'Taille du tampon';
@override String bufferSizeMB({required Object size}) => '${size} Mo';
@override String get bufferSizeAuto => 'Automatique (recommandé)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} Mo de mémoire disponible. Un tampon de ${size} Mo peut affecter la lecture.';
@override String get playbackBuffer => 'Tampon de lecture';
@override String get playbackBufferAuto => 'Auto (recommandé)';
@override String get playbackBufferLarge => 'Grand';
@override String get playbackBufferExtraLarge => 'Très grand';
@override String get playbackBufferDescription => 'Tampon supplémentaire contre les connexions instables. Également limité par la taille du tampon.';
@override String get defaultQualityTitle => 'Qualité par défaut';
@override String get cellularQualityTitle => 'Qualité par défaut sur les données mobiles';
@override String get cellularQualitySameAsDefault => 'Identique à la qualité par défaut';
@override String get musicQualityTitle => 'Qualité de la musique';
@override String get subtitleStyling => 'Style des sous-titres';
@override String get subtitleStylingDescription => 'Personnaliser l’apparence des sous-titres';
@@ -315,6 +320,8 @@ class _Translations$settings$fr extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Mémoriser les choix audio et sous-titres par titre';
@override String get followServerTrackSelections => 'Utiliser les pistes sélectionnées sur le serveur pour chaque épisode';
@override String get followServerTrackSelectionsDescription => 'Au changement d\'épisode, appliquer l\'audio et les sous-titres sélectionnés sur le serveur au lieu de conserver le choix en cours';
@override String get resumeMusicOnLaunch => 'Mémoriser la session musicale';
@override String get resumeMusicOnLaunchDescription => 'Au lancement de l\'application, rouvrir le dernier titre en pause là où il s\'était arrêté';
@override String get showChapterMarkersOnTimeline => 'Afficher les marqueurs de chapitres sur la barre de lecture';
@override String get showChapterMarkersOnTimelineDescription => 'Segmenter la barre de lecture aux limites des chapitres';
@override String get specialsOrdering => 'Épisodes spéciaux dans l’ordre des épisodes';
@@ -368,7 +375,11 @@ class _Translations$settings$fr extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Raccourci déjà attribué à ${action}';
@override String shortcutUpdated({required Object action}) => 'Raccourci mis à jour pour ${action}';
@override String get saveFailed => 'Impossible d’enregistrer les modifications. Réessayez.';
@override String get autoSkip => 'Saut automatique';
@override String get autoPlayAndSkip => 'Lecture automatique et saut';
@override String get autoPlayNextEpisode => 'Lire automatiquement l\'épisode suivant';
@override String get autoPlayNextEpisodeDescription => 'Lancer automatiquement l\'épisode suivant lorsqu\'un épisode se termine';
@override String get playNextCountdown => 'Compte à rebours avant l\'épisode suivant';
@override String get playNextCountdownImmediate => 'Lire immédiatement';
@override String get autoSkipIntro => 'Passer automatiquement l’introduction';
@override String get autoSkipIntroDescription => 'Passer automatiquement les marqueurs d’introduction après quelques secondes';
@override String get autoSkipCredits => 'Passer automatiquement le générique';
@@ -443,10 +454,8 @@ class _Translations$settings$fr extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Forcer le DV7 natif et bloquer la nouvelle tentative de conversion DV';
@override String get dvConversionDv81Description => 'Forcer la conversion RPU intégrée vers le profil 8.1 de Dolby Vision';
@override String get dvConversionHevcStripDescription => 'Supprimer les couches RPU/EL Dolby Vision et présenter du HEVC simple';
@override String get demuxer => 'Démultiplexeur de conteneur';
@override String get demuxerDescription => 'Quel démultiplexeur analyse les fichiers lus en direct. Passez à media3 si un fichier pose problème.';
@override String get demuxerFfmpeg => 'FFmpeg (recommandé)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Désentrelacement';
@override String get deinterlaceDescription => 'Supprimer les artefacts de peignage des vidéos entrelacées (lecteur mpv uniquement)';
@override String get requireProfileSelectionOnOpen => 'Demander le profil à l\'ouverture';
@override String get requireProfileSelectionOnOpenDescription => 'Afficher la sélection de profil à chaque ouverture de l\'application';
@override String get forceTvMode => 'Forcer le mode TV';
@@ -464,15 +473,31 @@ class _Translations$settings$fr extends Translations$settings$en {
@override String get showExploreTabDescription => 'Afficher l’onglet Explorer avec le contenu de Plex Discover et des services de suivi connectés';
@override String get liveTvDefaultFavorites => 'Chaînes favorites par défaut';
@override String get liveTvDefaultFavoritesDescription => 'Afficher uniquement les chaînes favorites à l\'ouverture de la TV en direct';
@override String get general => 'Général';
@override String get generalDescription => 'Langue, démarrage et comportement de la fenêtre';
@override String get languageAndRegion => 'Langue et région';
@override String get startup => 'Démarrage';
@override String get display => 'Affichage';
@override String get libraryAndCards => 'Bibliothèque et cartes';
@override String get homeScreen => 'Écran d\'accueil';
@override String get navigation => 'Navigation';
@override String get window => 'Fenêtre';
@override String get content => 'Contenu';
@override String get liveTv => 'TV en direct';
@override String get player => 'Lecteur';
@override String get subtitlesAndConfig => 'Sous-titres et configuration';
@override String get videoAndDisplay => 'Vidéo et affichage';
@override String get audio => 'Audio';
@override String get quality => 'Qualité';
@override String get subtitles => 'Sous-titres';
@override String get seekAndTiming => 'Déplacement et minutage';
@override String get behavior => 'Comportement';
@override String get gestures => 'Gestes';
@override String get gestureBrightnessSwipe => 'Balayage de luminosité';
@override String get gestureBrightnessSwipeDescription => 'Balayez vers le haut ou le bas sur le bord gauche pour régler la luminosité';
@override String get gestureVolumeSwipe => 'Balayage de volume';
@override String get gestureVolumeSwipeDescription => 'Balayez vers le haut ou le bas sur le bord droit pour régler le volume';
@override String get gesturePinchToZoom => 'Pincer pour zoomer';
@override String get gesturePinchToZoomDescription => 'Pincez la vidéo pour zoomer ou dézoomer';
@override String get controls => 'Commandes';
@override String get rememberPlayerChanges => 'Mémoriser les modifications du lecteur';
@override String get rememberPlayerChangesDescription => 'Où une modification effectuée pendant la lecture est enregistrée et réappliquée';
@override String get scopePlaybackSpeed => 'Vitesse de lecture';
@@ -1086,6 +1111,49 @@ class _Translations$connections$fr extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Ajoutez ou retirez des URL pour ${serverName}. Plezy utilisera l\'URL joignable avec la latence la plus faible.';
}
// Path: accountPreferences
class _Translations$accountPreferences$fr extends Translations$accountPreferences$en {
_Translations$accountPreferences$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Préférences du compte';
@override String hubSubtitleSingle({required Object account}) => 'Options audio, sous-titres et bibliothèque enregistrées sur ${account}';
@override String hubSubtitleMultiple({required Object count}) => 'Options audio, sous-titres et bibliothèque enregistrées sur ${count} comptes';
@override String get pickAccount => 'Chaque compte enregistre ses propres préférences. Choisissez celui à modifier.';
@override String get storedOnAccount => 'Ces options sont enregistrées sur le compte lui-même : toutes les applications connectées les utilisent, y compris Plezy sur vos autres appareils.';
@override String get noAccounts => 'Aucun compte à configurer';
@override String get noAccountsHint => 'Connectez-vous à Plex, ou connectez un serveur Jellyfin ou Emby : les préférences enregistrées sur ce compte apparaîtront ici.';
@override String get unavailable => 'Impossible de joindre ce compte';
@override String get loadFailed => 'Impossible de charger ces préférences';
@override String get noPreference => 'Aucune préférence';
@override String get notSet => 'Non défini';
@override late final _Translations$accountPreferences$groups$fr groups = _Translations$accountPreferences$groups$fr._(_root);
@override String get preferredAudioLanguage => 'Langue audio préférée';
@override String get autoSelectAudio => 'Choisir l\'audio par langue';
@override String get autoSelectAudioDescription => 'Désactivé : la piste audio marquée par défaut dans le fichier est conservée.';
@override String get preferredSubtitleLanguage => 'Langue de sous-titres préférée';
@override String get subtitleMode => 'Activer les sous-titres';
@override late final _Translations$accountPreferences$subtitleModes$fr subtitleModes = _Translations$accountPreferences$subtitleModes$fr._(_root);
@override String get subtitleAccessibility => 'Sous-titres SDH';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$fr subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$fr._(_root);
@override String get forcedSubtitles => 'Sous-titres forcés';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$fr forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$fr._(_root);
@override String get displayMissingEpisodes => 'Afficher les épisodes manquants';
@override String get displayMissingEpisodesDescription => 'Répertorier les épisodes connus du serveur mais sans fichier.';
@override String get hidePlayedInLatest => 'Masquer les éléments vus dans « Récemment ajouté »';
@override String get hidePlayedInLatestDescription => 'Exclure les éléments déjà vus des lignes « Récemment ajouté » du serveur.';
@override String get displayCollectionsView => 'Afficher la vue Collections';
@override String get displayCollectionsViewDescription => 'Proposer la vue Collections du serveur en plus de vos bibliothèques.';
@override String get rewatchingInNextUp => 'Conserver les séries revues dans « À suivre »';
@override String get rewatchingInNextUpDescription => 'Une fois une série terminée, si vous la recommencez, « À suivre » suit le revisionnage au lieu de la retirer.';
@override String get watchedIndicator => 'Indicateurs de visionnage';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$fr watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$fr._(_root);
@override String get mediaReviewsVisibility => 'Notes et critiques';
@override late final _Translations$accountPreferences$mediaReviewsOptions$fr mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$fr._(_root);
}
// Path: discover
class _Translations$discover$fr extends Translations$discover$en {
_Translations$discover$fr._(TranslationsFr root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$fr extends Translations$liveTv$en {
@override String get unknownChannel => 'Chaîne inconnue';
@override String get live => 'EN DIRECT';
@override String get reloadGuide => 'Recharger le guide';
@override String get searchGuide => 'Rechercher dans le guide';
@override String get searchHint => 'Rechercher des chaînes et des programmes';
@override String searchNoResults({required Object query}) => 'Aucune correspondance pour "${query}"';
@override String get channelsSection => 'Chaînes';
@override String get programsSection => 'Programmes';
@override String get now => 'Maintenant';
@override String get today => 'Aujourd\'hui';
@override String get tomorrow => 'Demain';
@@ -1545,6 +1618,8 @@ class _Translations$music$fr extends Translations$music$en {
@override String get repeatAll => 'Tout répéter';
@override String get repeatOne => 'Répéter le titre';
@override String get instantMixNoServer => 'Aucun serveur n’est disponible pour créer un mix instantané';
@override String get instantMixFailed => 'Impossible de charger le mix instantané';
@override String get instantMixEmpty => 'Le mix instantané n\'a renvoyé aucun titre';
@override String noAudioUrl({required Object track}) => 'Aucune URL audio n’est disponible pour ${track}';
@override late final _Translations$music$discography$fr discography = _Translations$music$discography$fr._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$fr extends Translations$seerr$en {
@override String get statusBlocklisted => 'Sur la liste de blocage';
@override String couldNotReach({required Object url, required Object error}) => 'Impossible de joindre ${url} : ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'Aucune instance Seerr à l’adresse ${url} (HTTP ${status})';
@override String get invalidUrl => 'Saisissez une adresse de serveur comme https://seerr.example.com';
@override String get quickConnectUnsupported => 'Cette instance Seerr ne prend pas en charge Quick Connect. Elle nécessite Seerr 3.4 ou version ultérieure.';
@override String get notInitialized => 'La configuration initiale de cette instance Seerr n’est pas terminée';
@override String get noPlexTokenForReauth => 'Aucun jeton Plex n’est disponible pour se reconnecter';
@override String get noStoredCredentials => 'Aucun identifiant enregistré n’est disponible pour se reconnecter';
@@ -2066,6 +2143,7 @@ class _Translations$services$fr extends Translations$services$en {
// Translations
@override String get title => 'Services';
@override String get hubSubtitle => 'Synchronisez votre progression et demandez de nouveaux titres.';
@override String get integrations => 'Intégrations';
@override String get notConnected => 'Non connecté';
@override String connectedAs({required Object username}) => 'Connecté en tant que @${username}';
@override String get scrobble => 'Suivre la progression automatiquement';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$fr extends Translations$videoControl
@override String unknown({required Object error}) => 'Une erreur s\'est produite : ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$fr extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Audio et sous-titres';
@override String get libraryDisplay => 'Bibliothèque';
@override String get personalMedia => 'Médias personnels';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$fr extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get none => 'Sélection manuelle';
@override String get noneDescription => 'Ne jamais activer les sous-titres automatiquement.';
@override String get defaultMode => 'Suivre les indicateurs des pistes';
@override String get defaultModeDescription => 'Utiliser les indicateurs par défaut et forcés enregistrés sur chaque piste de sous-titres.';
@override String get always => 'Toujours activé';
@override String get alwaysDescription => 'Activer une piste de sous-titres dans la langue préférée lorsqu\'elle existe.';
@override String get onlyForced => 'Sous-titres forcés uniquement';
@override String get onlyForcedDescription => 'Charger uniquement les pistes marquées comme forcées.';
@override String get smart => 'Avec l\'audio en langue étrangère';
@override String get smartDescription => 'Activer les sous-titres uniquement lorsque l\'audio est dans une autre langue.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$fr extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Préférer les sous-titres non SDH';
@override String get preferSdh => 'Préférer les sous-titres SDH';
@override String get onlySdh => 'Sous-titres SDH uniquement';
@override String get onlyNonSdh => 'Sous-titres non SDH uniquement';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$fr extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Préférer les sous-titres non forcés';
@override String get preferForced => 'Préférer les sous-titres forcés';
@override String get onlyForced => 'Sous-titres forcés uniquement';
@override String get onlyNonForced => 'Sous-titres non forcés uniquement';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$fr extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get none => 'Jamais';
@override String get moviesAndShows => 'Films et séries TV';
@override String get movies => 'Films uniquement';
@override String get shows => 'Séries TV uniquement';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$fr extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$fr._(TranslationsFr root) : this._root = root, super.internal(root);
final TranslationsFr _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Utilisateurs et critiques';
@override String get usersOnly => 'Utilisateurs uniquement';
@override String get criticsOnly => 'Critiques uniquement';
@override String get nobody => 'Masquées';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$fr extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$fr._(TranslationsFr root) : this._root = root, super.internal(root);
@@ -2499,7 +2660,7 @@ class _Translations$explore$detail$fr extends Translations$explore$detail$en {
@override String get budget => 'Budget';
@override String get revenue => 'Box-office';
@override String get contentAdvisory => 'Recommandation d\'âge';
@override String get tags => 'Tags';
@override String get tags => 'Étiquettes';
@override String get revealSpoilerTags => 'Afficher les tags spoiler';
@override String get links => 'Liens';
@override String get watchOn => 'Regarder sur';
@@ -2786,6 +2947,7 @@ extension on TranslationsFr {
'auth.useBrowser' => 'Utiliser le navigateur',
'auth.or' => 'ou',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Se connecter à ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Utiliser Quick Connect',
'auth.quickConnectInstructions' => 'Ouvrez Quick Connect dans Jellyfin et saisissez ce code.',
'auth.quickConnectWaiting' => 'En attente d\'approbation…',
@@ -2825,6 +2987,7 @@ extension on TranslationsFr {
'common.mute' => 'Couper le son',
'common.ok' => 'OK',
'common.off' => 'Désactivé',
'common.options' => 'Options',
'common.seasonNumber' => ({required Object number}) => 'Saison ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Épisode ${number} – ${title}',
'common.chapterNumber' => ({required Object number}) => 'Chapitre ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsFr {
'settings.displayScale' => 'Échelle d\'affichage',
'settings.compact' => 'Compact',
'settings.comfortable' => 'Confortable',
'settings.gridSpacing' => 'Espacement de la grille',
'settings.gridSpacingTight' => 'Resserré',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Aéré',
'settings.tvCornerSpotlightBackdrop' => 'Illustration en vedette dans le coin',
'settings.tvCornerSpotlightBackdropDescription' => 'Afficher l’illustration en vedette dans le coin supérieur droit plutôt qu’en plein écran',
'settings.viewMode' => 'Mode d\'affichage',
@@ -2946,16 +3113,14 @@ extension on TranslationsFr {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Décodage matériel',
'settings.hardwareDecodingDescription' => 'Utiliser l’accélération matérielle lorsqu’elle est disponible',
'settings.bufferSize' => 'Taille du tampon',
'settings.bufferSizeMB' => ({required Object size}) => '${size} Mo',
'settings.bufferSizeAuto' => 'Automatique (recommandé)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} Mo de mémoire disponible. Un tampon de ${size} Mo peut affecter la lecture.',
'settings.playbackBuffer' => 'Tampon de lecture',
'settings.playbackBufferAuto' => 'Auto (recommandé)',
'settings.playbackBufferLarge' => 'Grand',
'settings.playbackBufferExtraLarge' => 'Très grand',
'settings.playbackBufferDescription' => 'Tampon supplémentaire contre les connexions instables. Également limité par la taille du tampon.',
'settings.defaultQualityTitle' => 'Qualité par défaut',
'settings.cellularQualityTitle' => 'Qualité par défaut sur les données mobiles',
'settings.cellularQualitySameAsDefault' => 'Identique à la qualité par défaut',
'settings.musicQualityTitle' => 'Qualité de la musique',
'settings.subtitleStyling' => 'Style des sous-titres',
'settings.subtitleStylingDescription' => 'Personnaliser l’apparence des sous-titres',
@@ -2969,6 +3134,8 @@ extension on TranslationsFr {
'settings.rememberTrackSelectionsDescription' => 'Mémoriser les choix audio et sous-titres par titre',
'settings.followServerTrackSelections' => 'Utiliser les pistes sélectionnées sur le serveur pour chaque épisode',
'settings.followServerTrackSelectionsDescription' => 'Au changement d\'épisode, appliquer l\'audio et les sous-titres sélectionnés sur le serveur au lieu de conserver le choix en cours',
'settings.resumeMusicOnLaunch' => 'Mémoriser la session musicale',
'settings.resumeMusicOnLaunchDescription' => 'Au lancement de l\'application, rouvrir le dernier titre en pause là où il s\'était arrêté',
'settings.showChapterMarkersOnTimeline' => 'Afficher les marqueurs de chapitres sur la barre de lecture',
'settings.showChapterMarkersOnTimelineDescription' => 'Segmenter la barre de lecture aux limites des chapitres',
'settings.specialsOrdering' => 'Épisodes spéciaux dans l’ordre des épisodes',
@@ -3022,7 +3189,11 @@ extension on TranslationsFr {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Raccourci déjà attribué à ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Raccourci mis à jour pour ${action}',
'settings.saveFailed' => 'Impossible d’enregistrer les modifications. Réessayez.',
'settings.autoSkip' => 'Saut automatique',
'settings.autoPlayAndSkip' => 'Lecture automatique et saut',
'settings.autoPlayNextEpisode' => 'Lire automatiquement l\'épisode suivant',
'settings.autoPlayNextEpisodeDescription' => 'Lancer automatiquement l\'épisode suivant lorsqu\'un épisode se termine',
'settings.playNextCountdown' => 'Compte à rebours avant l\'épisode suivant',
'settings.playNextCountdownImmediate' => 'Lire immédiatement',
'settings.autoSkipIntro' => 'Passer automatiquement l’introduction',
'settings.autoSkipIntroDescription' => 'Passer automatiquement les marqueurs d’introduction après quelques secondes',
'settings.autoSkipCredits' => 'Passer automatiquement le générique',
@@ -3097,10 +3268,8 @@ extension on TranslationsFr {
'settings.dvConversionNativeDescription' => 'Forcer le DV7 natif et bloquer la nouvelle tentative de conversion DV',
'settings.dvConversionDv81Description' => 'Forcer la conversion RPU intégrée vers le profil 8.1 de Dolby Vision',
'settings.dvConversionHevcStripDescription' => 'Supprimer les couches RPU/EL Dolby Vision et présenter du HEVC simple',
'settings.demuxer' => 'Démultiplexeur de conteneur',
'settings.demuxerDescription' => 'Quel démultiplexeur analyse les fichiers lus en direct. Passez à media3 si un fichier pose problème.',
'settings.demuxerFfmpeg' => 'FFmpeg (recommandé)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Désentrelacement',
'settings.deinterlaceDescription' => 'Supprimer les artefacts de peignage des vidéos entrelacées (lecteur mpv uniquement)',
'settings.requireProfileSelectionOnOpen' => 'Demander le profil à l\'ouverture',
'settings.requireProfileSelectionOnOpenDescription' => 'Afficher la sélection de profil à chaque ouverture de l\'application',
'settings.forceTvMode' => 'Forcer le mode TV',
@@ -3118,15 +3287,31 @@ extension on TranslationsFr {
'settings.showExploreTabDescription' => 'Afficher l’onglet Explorer avec le contenu de Plex Discover et des services de suivi connectés',
'settings.liveTvDefaultFavorites' => 'Chaînes favorites par défaut',
'settings.liveTvDefaultFavoritesDescription' => 'Afficher uniquement les chaînes favorites à l\'ouverture de la TV en direct',
'settings.general' => 'Général',
'settings.generalDescription' => 'Langue, démarrage et comportement de la fenêtre',
'settings.languageAndRegion' => 'Langue et région',
'settings.startup' => 'Démarrage',
'settings.display' => 'Affichage',
'settings.libraryAndCards' => 'Bibliothèque et cartes',
'settings.homeScreen' => 'Écran d\'accueil',
'settings.navigation' => 'Navigation',
'settings.window' => 'Fenêtre',
'settings.content' => 'Contenu',
'settings.liveTv' => 'TV en direct',
'settings.player' => 'Lecteur',
'settings.subtitlesAndConfig' => 'Sous-titres et configuration',
'settings.videoAndDisplay' => 'Vidéo et affichage',
'settings.audio' => 'Audio',
'settings.quality' => 'Qualité',
'settings.subtitles' => 'Sous-titres',
'settings.seekAndTiming' => 'Déplacement et minutage',
'settings.behavior' => 'Comportement',
'settings.gestures' => 'Gestes',
'settings.gestureBrightnessSwipe' => 'Balayage de luminosité',
'settings.gestureBrightnessSwipeDescription' => 'Balayez vers le haut ou le bas sur le bord gauche pour régler la luminosité',
'settings.gestureVolumeSwipe' => 'Balayage de volume',
'settings.gestureVolumeSwipeDescription' => 'Balayez vers le haut ou le bas sur le bord droit pour régler le volume',
'settings.gesturePinchToZoom' => 'Pincer pour zoomer',
'settings.gesturePinchToZoomDescription' => 'Pincez la vidéo pour zoomer ou dézoomer',
'settings.controls' => 'Commandes',
'settings.rememberPlayerChanges' => 'Mémoriser les modifications du lecteur',
'settings.rememberPlayerChangesDescription' => 'Où une modification effectuée pendant la lecture est enregistrée et réappliquée',
'settings.scopePlaybackSpeed' => 'Vitesse de lecture',
@@ -3264,6 +3449,8 @@ extension on TranslationsFr {
'fileInfo.has64bitOffsets' => 'Décalages 64 bits',
'fileInfo.protocol' => 'Protocole',
'fileInfo.mediaType' => 'Type de média',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Type de source',
'fileInfo.optimizedVersion' => 'Version optimisée',
'fileInfo.optimizationTarget' => 'Cible d\'optimisation',
@@ -3288,8 +3475,6 @@ extension on TranslationsFr {
'fileInfo.flagOriginal' => 'Originale',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profil ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Marquer comme vu',
'mediaMenu.markAsUnwatched' => 'Marquer comme non visionné',
'mediaMenu.removeFromContinueWatching' => 'Supprimer de la liste "Continuer à regarder"',
@@ -3624,6 +3809,63 @@ extension on TranslationsFr {
'connections.signInAgain' => 'Se reconnecter',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Modifier la connexion ${product}',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Ajoutez ou retirez des URL pour ${serverName}. Plezy utilisera l\'URL joignable avec la latence la plus faible.',
'accountPreferences.sectionTitle' => 'Préférences du compte',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Options audio, sous-titres et bibliothèque enregistrées sur ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Options audio, sous-titres et bibliothèque enregistrées sur ${count} comptes',
'accountPreferences.pickAccount' => 'Chaque compte enregistre ses propres préférences. Choisissez celui à modifier.',
'accountPreferences.storedOnAccount' => 'Ces options sont enregistrées sur le compte lui-même : toutes les applications connectées les utilisent, y compris Plezy sur vos autres appareils.',
'accountPreferences.noAccounts' => 'Aucun compte à configurer',
'accountPreferences.noAccountsHint' => 'Connectez-vous à Plex, ou connectez un serveur Jellyfin ou Emby : les préférences enregistrées sur ce compte apparaîtront ici.',
'accountPreferences.unavailable' => 'Impossible de joindre ce compte',
'accountPreferences.loadFailed' => 'Impossible de charger ces préférences',
'accountPreferences.noPreference' => 'Aucune préférence',
'accountPreferences.notSet' => 'Non défini',
'accountPreferences.groups.audioAndSubtitles' => 'Audio et sous-titres',
'accountPreferences.groups.libraryDisplay' => 'Bibliothèque',
'accountPreferences.groups.personalMedia' => 'Médias personnels',
'accountPreferences.preferredAudioLanguage' => 'Langue audio préférée',
'accountPreferences.autoSelectAudio' => 'Choisir l\'audio par langue',
'accountPreferences.autoSelectAudioDescription' => 'Désactivé : la piste audio marquée par défaut dans le fichier est conservée.',
'accountPreferences.preferredSubtitleLanguage' => 'Langue de sous-titres préférée',
'accountPreferences.subtitleMode' => 'Activer les sous-titres',
'accountPreferences.subtitleModes.none' => 'Sélection manuelle',
'accountPreferences.subtitleModes.noneDescription' => 'Ne jamais activer les sous-titres automatiquement.',
'accountPreferences.subtitleModes.defaultMode' => 'Suivre les indicateurs des pistes',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Utiliser les indicateurs par défaut et forcés enregistrés sur chaque piste de sous-titres.',
'accountPreferences.subtitleModes.always' => 'Toujours activé',
'accountPreferences.subtitleModes.alwaysDescription' => 'Activer une piste de sous-titres dans la langue préférée lorsqu\'elle existe.',
'accountPreferences.subtitleModes.onlyForced' => 'Sous-titres forcés uniquement',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Charger uniquement les pistes marquées comme forcées.',
'accountPreferences.subtitleModes.smart' => 'Avec l\'audio en langue étrangère',
'accountPreferences.subtitleModes.smartDescription' => 'Activer les sous-titres uniquement lorsque l\'audio est dans une autre langue.',
'accountPreferences.subtitleAccessibility' => 'Sous-titres SDH',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Préférer les sous-titres non SDH',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Préférer les sous-titres SDH',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Sous-titres SDH uniquement',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Sous-titres non SDH uniquement',
'accountPreferences.forcedSubtitles' => 'Sous-titres forcés',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Préférer les sous-titres non forcés',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Préférer les sous-titres forcés',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Sous-titres forcés uniquement',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Sous-titres non forcés uniquement',
'accountPreferences.displayMissingEpisodes' => 'Afficher les épisodes manquants',
'accountPreferences.displayMissingEpisodesDescription' => 'Répertorier les épisodes connus du serveur mais sans fichier.',
'accountPreferences.hidePlayedInLatest' => 'Masquer les éléments vus dans « Récemment ajouté »',
'accountPreferences.hidePlayedInLatestDescription' => 'Exclure les éléments déjà vus des lignes « Récemment ajouté » du serveur.',
'accountPreferences.displayCollectionsView' => 'Afficher la vue Collections',
'accountPreferences.displayCollectionsViewDescription' => 'Proposer la vue Collections du serveur en plus de vos bibliothèques.',
'accountPreferences.rewatchingInNextUp' => 'Conserver les séries revues dans « À suivre »',
'accountPreferences.rewatchingInNextUpDescription' => 'Une fois une série terminée, si vous la recommencez, « À suivre » suit le revisionnage au lieu de la retirer.',
'accountPreferences.watchedIndicator' => 'Indicateurs de visionnage',
'accountPreferences.watchedIndicatorOptions.none' => 'Jamais',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Films et séries TV',
'accountPreferences.watchedIndicatorOptions.movies' => 'Films uniquement',
'accountPreferences.watchedIndicatorOptions.shows' => 'Séries TV uniquement',
'accountPreferences.mediaReviewsVisibility' => 'Notes et critiques',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Utilisateurs et critiques',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Utilisateurs uniquement',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Critiques uniquement',
'accountPreferences.mediaReviewsOptions.nobody' => 'Masquées',
'discover.title' => 'Découvrir',
'discover.noContentAvailable' => 'Aucun contenu disponible',
'discover.addMediaToLibraries' => 'Ajoutez des médias à vos bibliothèques',
@@ -3721,6 +3963,8 @@ extension on TranslationsFr {
'libraries.filterCategories.contentRating' => 'Classification',
'libraries.filterCategories.tag' => 'Étiquette',
'libraries.filterCategories.unwatched' => 'Non vus',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Non lus',
'libraries.filterCategories.favorites' => 'Favoris',
'libraries.sortLabels.title' => 'Titre',
@@ -3802,8 +4046,6 @@ extension on TranslationsFr {
'explore.rows.watchlist' => 'Liste de suivi',
'explore.rows.recommendedMovies' => 'Films recommandés',
'explore.rows.recommendedShows' => 'Séries recommandées',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Films tendance',
'explore.rows.trendingShows' => 'Séries tendance',
'explore.rows.popularMovies' => 'Films populaires',
@@ -3924,7 +4166,7 @@ extension on TranslationsFr {
'explore.detail.budget' => 'Budget',
'explore.detail.revenue' => 'Box-office',
'explore.detail.contentAdvisory' => 'Recommandation d\'âge',
'explore.detail.tags' => 'Tags',
'explore.detail.tags' => 'Étiquettes',
'explore.detail.revealSpoilerTags' => 'Afficher les tags spoiler',
'explore.detail.links' => 'Liens',
'explore.detail.watchOn' => 'Regarder sur',
@@ -3956,6 +4198,11 @@ extension on TranslationsFr {
'liveTv.unknownChannel' => 'Chaîne inconnue',
'liveTv.live' => 'EN DIRECT',
'liveTv.reloadGuide' => 'Recharger le guide',
'liveTv.searchGuide' => 'Rechercher dans le guide',
'liveTv.searchHint' => 'Rechercher des chaînes et des programmes',
'liveTv.searchNoResults' => ({required Object query}) => 'Aucune correspondance pour "${query}"',
'liveTv.channelsSection' => 'Chaînes',
'liveTv.programsSection' => 'Programmes',
'liveTv.now' => 'Maintenant',
'liveTv.today' => 'Aujourd\'hui',
'liveTv.tomorrow' => 'Demain',
@@ -4093,6 +4340,8 @@ extension on TranslationsFr {
'music.repeatAll' => 'Tout répéter',
'music.repeatOne' => 'Répéter le titre',
'music.instantMixNoServer' => 'Aucun serveur n’est disponible pour créer un mix instantané',
'music.instantMixFailed' => 'Impossible de charger le mix instantané',
'music.instantMixEmpty' => 'Le mix instantané n\'a renvoyé aucun titre',
'music.noAudioUrl' => ({required Object track}) => 'Aucune URL audio n’est disponible pour ${track}',
'music.discography.singlesAndEps' => 'Singles et EP',
'music.discography.live' => 'Live',
@@ -4228,6 +4477,8 @@ extension on TranslationsFr {
'downloads.syncRuleCleanupUnavailable' => 'Les téléchargements associés n\'ont pas pu être identifiés en toute sécurité. Reconnectez le serveur et réessayez, ou retirez la règle sans supprimer les téléchargements.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => '${count} nouveaux épisodes synchronisés pour ${title}',
'downloads.activeSyncRules' => 'Règles de synchronisation',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Aucune règle de synchronisation',
'downloads.manageSyncRule' => 'Gérer la synchronisation',
'downloads.editEpisodeCount' => 'Nombre d’épisodes',
@@ -4316,8 +4567,6 @@ extension on TranslationsFr {
'companionRemote.pairing.availableDevices' => 'Appareils disponibles',
'companionRemote.pairing.manualConnection' => 'Connexion manuelle',
'companionRemote.pairing.cryptoInitFailed' => 'Impossible de démarrer la connexion sécurisée. Connectez-vous d\'abord à Plex.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Veuillez saisir l’adresse de l’hôte',
'companionRemote.pairing.validationHostFormat' => 'Le format doit être IP:port (p. ex. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Délai de connexion dépassé. Utilisez le même réseau sur les deux appareils.',
@@ -4577,6 +4826,8 @@ extension on TranslationsFr {
'seerr.statusBlocklisted' => 'Sur la liste de blocage',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Impossible de joindre ${url} : ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'Aucune instance Seerr à l’adresse ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Saisissez une adresse de serveur comme https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Cette instance Seerr ne prend pas en charge Quick Connect. Elle nécessite Seerr 3.4 ou version ultérieure.',
'seerr.notInitialized' => 'La configuration initiale de cette instance Seerr n’est pas terminée',
'seerr.noPlexTokenForReauth' => 'Aucun jeton Plex n’est disponible pour se reconnecter',
'seerr.noStoredCredentials' => 'Aucun identifiant enregistré n’est disponible pour se reconnecter',
@@ -4587,6 +4838,7 @@ extension on TranslationsFr {
'seerr.sessionRejectedAfterReauth' => 'La session a été refusée après la reconnexion',
'services.title' => 'Services',
'services.hubSubtitle' => 'Synchronisez votre progression et demandez de nouveaux titres.',
'services.integrations' => 'Intégrations',
'services.notConnected' => 'Non connecté',
'services.connectedAs' => ({required Object username}) => 'Connecté en tant que @${username}',
'services.scrobble' => 'Suivre la progression automatiquement',
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsHu extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$hu dialog = _Translations$dialog$hu._(_root);
@override late final _Translations$profiles$hu profiles = _Translations$profiles$hu._(_root);
@override late final _Translations$connections$hu connections = _Translations$connections$hu._(_root);
@override late final _Translations$accountPreferences$hu accountPreferences = _Translations$accountPreferences$hu._(_root);
@override late final _Translations$discover$hu discover = _Translations$discover$hu._(_root);
@override late final _Translations$errors$hu errors = _Translations$errors$hu._(_root);
@override late final _Translations$libraries$hu libraries = _Translations$libraries$hu._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$hu extends Translations$auth$en {
@override String get useBrowser => 'Böngésző használata';
@override String get or => 'vagy';
@override String connectToMediaBrowser({required Object product}) => 'Csatlakozás a(z) ${product} szolgáltatáshoz';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Quick Connect használata';
@override String get quickConnectInstructions => 'Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.';
@override String get quickConnectWaiting => 'Várakozás a jóváhagyásra…';
@@ -165,6 +167,7 @@ class _Translations$common$hu extends Translations$common$en {
@override String get mute => 'Némítás';
@override String get ok => 'OK';
@override String get off => 'Ki';
@override String get options => 'Lehetőségek';
@override String seasonNumber({required Object number}) => '${number}. évad';
@override String episodeNumberTitle({required Object number, required Object title}) => '${number}. epizód - ${title}';
@override String chapterNumber({required Object number}) => '${number}. fejezet';
@@ -249,6 +252,10 @@ class _Translations$settings$hu extends Translations$settings$en {
@override String get displayScale => 'Kijelző méretezése';
@override String get compact => 'Kompakt';
@override String get comfortable => 'Kényelmes';
@override String get gridSpacing => 'Rácstávolság';
@override String get gridSpacingTight => 'Sűrű';
@override String get gridSpacingNormal => 'Normál';
@override String get gridSpacingSpacious => 'Tágas';
@override String get tvCornerSpotlightBackdrop => 'Sarokban megjelenő kiemelt háttérkép';
@override String get tvCornerSpotlightBackdropDescription => 'A kiemelt háttérkép megjelenítése a jobb felső sarokban a teljes képernyő helyett';
@override String get viewMode => 'Nézetmód';
@@ -292,16 +299,14 @@ class _Translations$settings$hu extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Hardveres dekódolás';
@override String get hardwareDecodingDescription => 'Hardveres gyorsítás használata, ha elérhető';
@override String get bufferSize => 'Puffer mérete';
@override String bufferSizeMB({required Object size}) => '${size} MB';
@override String get bufferSizeAuto => 'Automatikus (ajánlott)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} MB memória érhető el. A(z) ${size} MB méretű puffer befolyásolhatja a lejátszást.';
@override String get playbackBuffer => 'Lejátszási puffer';
@override String get playbackBufferAuto => 'Automatikus (ajánlott)';
@override String get playbackBufferLarge => 'Nagy';
@override String get playbackBufferExtraLarge => 'Extra nagy';
@override String get playbackBufferDescription => 'Több puffert használ instabil kapcsolatok ellen. A pufferméret is korlátozza.';
@override String get defaultQualityTitle => 'Alapértelmezett minőség';
@override String get cellularQualityTitle => 'Alapértelmezett minőség mobilhálózaton';
@override String get cellularQualitySameAsDefault => 'Ugyanaz, mint az alapértelmezett minőség';
@override String get musicQualityTitle => 'Zene minősége';
@override String get subtitleStyling => 'Feliratok stílusa';
@override String get subtitleStylingDescription => 'Feliratok megjelenésének testreszabása';
@@ -315,6 +320,8 @@ class _Translations$settings$hu extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Hang- és feliratválasztások megjegyzése címenként';
@override String get followServerTrackSelections => 'A szerver epizódonkénti sávválasztásának használata';
@override String get followServerTrackSelectionsDescription => 'Epizódváltáskor a szerveren kiválasztott hang és felirat lép életbe az aktuális választás átvitele helyett';
@override String get resumeMusicOnLaunch => 'Zenei munkamenet megjegyzése';
@override String get resumeMusicOnLaunchDescription => 'Az alkalmazás indításakor az utolsó szám szüneteltetve nyílik meg ott, ahol abbamaradt';
@override String get showChapterMarkersOnTimeline => 'Fejezetjelölők megjelenítése az idősávon';
@override String get showChapterMarkersOnTimelineDescription => 'Az idősáv felosztása a fejezetek határainál';
@override String get specialsOrdering => 'Különkiadások epizódsorrendben';
@@ -368,7 +375,11 @@ class _Translations$settings$hu extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'A billentyűparancs már hozzá van rendelve a következőhöz: ${action}';
@override String shortcutUpdated({required Object action}) => 'Billentyűparancs frissítve a következőhöz: ${action}';
@override String get saveFailed => 'Nem sikerült menteni a módosításokat. Próbáld újra.';
@override String get autoSkip => 'Automatikus átugrás';
@override String get autoPlayAndSkip => 'Automatikus lejátszás és átugrás';
@override String get autoPlayNextEpisode => 'Következő epizód automatikus lejátszása';
@override String get autoPlayNextEpisodeDescription => 'A következő epizód automatikus elindítása, amikor az aktuális véget ér';
@override String get playNextCountdown => 'Visszaszámlálás a következő epizódig';
@override String get playNextCountdownImmediate => 'Azonnali lejátszás';
@override String get autoSkipIntro => 'Intró automatikus átugrása';
@override String get autoSkipIntroDescription => 'Az intrójelölők automatikus átugrása néhány másodperc után';
@override String get autoSkipCredits => 'Stáblista automatikus átugrása';
@@ -443,10 +454,8 @@ class _Translations$settings$hu extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'A natív DV7 kényszerítése és a DV-átalakítási újrapróbálkozás letiltása';
@override String get dvConversionDv81Description => 'A közvetlen RPU-átalakítás kényszerítése Dolby Vision Profile 8.1-re';
@override String get dvConversionHevcStripDescription => 'A Dolby Vision RPU/EL-rétegek eltávolítása és egyszerű HEVC-ként való megjelenítés';
@override String get demuxer => 'Konténer-demuxer';
@override String get demuxerDescription => 'Melyik demuxer dolgozza fel a közvetlenül lejátszott fájlokat. Válts media3-ra, ha egy fájl hibásan viselkedik.';
@override String get demuxerFfmpeg => 'FFmpeg (ajánlott)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Deinterlacing';
@override String get deinterlaceDescription => 'Fésűfog-műtermékek eltávolítása a váltott soros videóból (csak mpv lejátszóval)';
@override String get requireProfileSelectionOnOpen => 'Profil kérése az alkalmazás megnyitásakor';
@override String get requireProfileSelectionOnOpenDescription => 'Profilválasztó megjelenítése minden alkalommal, amikor az alkalmazást megnyitod';
@override String get forceTvMode => 'TV-mód kényszerítése';
@@ -464,15 +473,31 @@ class _Translations$settings$hu extends Translations$settings$en {
@override String get showExploreTabDescription => 'A Plex Discover és a csatlakoztatott követők tartalmait megjelenítő Felfedezés lap megjelenítése';
@override String get liveTvDefaultFavorites => 'Kedvenc csatornák alapértelmezés szerint';
@override String get liveTvDefaultFavoritesDescription => 'Csak a kedvenc csatornák megjelenítése az Élő TV megnyitásakor';
@override String get general => 'Általános';
@override String get generalDescription => 'Nyelv, indítás és ablakviselkedés';
@override String get languageAndRegion => 'Nyelv és régió';
@override String get startup => 'Indítás';
@override String get display => 'Kijelző';
@override String get libraryAndCards => 'Könyvtár és kártyák';
@override String get homeScreen => 'Kezdőképernyő';
@override String get navigation => 'Navigáció';
@override String get window => 'Ablak';
@override String get content => 'Tartalom';
@override String get liveTv => 'Élő TV';
@override String get player => 'Lejátszó';
@override String get subtitlesAndConfig => 'Feliratok és konfiguráció';
@override String get videoAndDisplay => 'Videó és kijelző';
@override String get audio => 'Hang';
@override String get quality => 'Minőség';
@override String get subtitles => 'Feliratok';
@override String get seekAndTiming => 'Tekerés és időzítés';
@override String get behavior => 'Viselkedés';
@override String get gestures => 'Mozdulatok';
@override String get gestureBrightnessSwipe => 'Fényerő-húzás';
@override String get gestureBrightnessSwipeDescription => 'Húzd felfelé vagy lefelé a bal szélén a fényerő beállításához';
@override String get gestureVolumeSwipe => 'Hangerő-húzás';
@override String get gestureVolumeSwipeDescription => 'Húzd felfelé vagy lefelé a jobb szélén a hangerő beállításához';
@override String get gesturePinchToZoom => 'Csípés a nagyításhoz';
@override String get gesturePinchToZoomDescription => 'Csípj a videóra a nagyításhoz vagy kicsinyítéshez';
@override String get controls => 'Vezérlők';
@override String get rememberPlayerChanges => 'Lejátszó módosításainak megjegyzése';
@override String get rememberPlayerChangesDescription => 'A lejátszás közben végzett módosítások mentési és újbóli alkalmazási helye';
@override String get scopePlaybackSpeed => 'Lejátszási sebesség';
@@ -1086,6 +1111,49 @@ class _Translations$connections$hu extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Adjon hozzá vagy távolítson el URL-eket a(z) ${serverName} esetén. A Plezy a legkisebb késleltetésű, elérhető URL-t fogja használni.';
}
// Path: accountPreferences
class _Translations$accountPreferences$hu extends Translations$accountPreferences$en {
_Translations$accountPreferences$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Fiókbeállítások';
@override String hubSubtitleSingle({required Object account}) => 'Hang-, felirat- és könyvtárbeállítások mentve a(z) ${account} fiókra';
@override String hubSubtitleMultiple({required Object count}) => 'Hang-, felirat- és könyvtárbeállítások mentve ${count} fiókra';
@override String get pickAccount => 'Minden fiók a saját beállításait tárolja. Válaszd ki a szerkesztendőt.';
@override String get storedOnAccount => 'Ezek a beállítások magán a fiókon tárolódnak, így minden bejelentkezett alkalmazás használja őket — beleértve a Plezyt a többi eszközödön.';
@override String get noAccounts => 'Nincs beállítható fiók';
@override String get noAccountsHint => 'Jelentkezz be a Plexbe, vagy csatlakoztass egy Jellyfin- vagy Emby-szervert, és az azon a fiókon tárolt beállítások itt jelennek meg.';
@override String get unavailable => 'Ez a fiók nem érhető el';
@override String get loadFailed => 'Nem sikerült betölteni ezeket a beállításokat';
@override String get noPreference => 'Nincs preferencia';
@override String get notSet => 'Nincs beállítva';
@override late final _Translations$accountPreferences$groups$hu groups = _Translations$accountPreferences$groups$hu._(_root);
@override String get preferredAudioLanguage => 'Elsődleges hangnyelv';
@override String get autoSelectAudio => 'Hangsáv kiválasztása nyelv alapján';
@override String get autoSelectAudioDescription => 'Ki kapcsolásnál a fájl által alapértelmezettként megjelölt hangsáv marad.';
@override String get preferredSubtitleLanguage => 'Elsődleges feliratnyelv';
@override String get subtitleMode => 'Feliratok bekapcsolása';
@override late final _Translations$accountPreferences$subtitleModes$hu subtitleModes = _Translations$accountPreferences$subtitleModes$hu._(_root);
@override String get subtitleAccessibility => 'SDH-feliratok';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$hu subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$hu._(_root);
@override String get forcedSubtitles => 'Kényszerített feliratok';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$hu forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$hu._(_root);
@override String get displayMissingEpisodes => 'Hiányzó epizódok megjelenítése';
@override String get displayMissingEpisodesDescription => 'Azoknak az epizódoknak a listázása, amelyeket a szerver ismer, de nincs hozzájuk fájl.';
@override String get hidePlayedInLatest => 'Megnézett elemek elrejtése a Legújabbak között';
@override String get hidePlayedInLatestDescription => 'A már megnézett elemek kimaradnak a szerver Legújabbak soraiból.';
@override String get displayCollectionsView => 'Gyűjtemények nézet megjelenítése';
@override String get displayCollectionsViewDescription => 'A szerver Gyűjtemények nézetének megjelenítése a könyvtárak mellett.';
@override String get rewatchingInNextUp => 'Újranézett sorozatok megtartása a Következikben';
@override String get rewatchingInNextUpDescription => 'Ha befejezel egy sorozatot, és újra elkezded, a Következik az újranézést követi, ahelyett hogy eldobná a sorozatot.';
@override String get watchedIndicator => 'Megtekintett jelzések';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$hu watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$hu._(_root);
@override String get mediaReviewsVisibility => 'Értékelések és kritikák';
@override late final _Translations$accountPreferences$mediaReviewsOptions$hu mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$hu._(_root);
}
// Path: discover
class _Translations$discover$hu extends Translations$discover$en {
_Translations$discover$hu._(TranslationsHu root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$hu extends Translations$liveTv$en {
@override String get unknownChannel => 'Ismeretlen csatorna';
@override String get live => 'ÉLŐ';
@override String get reloadGuide => 'Műsorújság újratöltése';
@override String get searchGuide => 'Keresés a műsorújságban';
@override String get searchHint => 'Csatornák és műsorok keresése';
@override String searchNoResults({required Object query}) => 'Nincs találat a következőre: "${query}"';
@override String get channelsSection => 'Csatornák';
@override String get programsSection => 'Műsorok';
@override String get now => 'Most';
@override String get today => 'Ma';
@override String get tomorrow => 'Holnap';
@@ -1545,6 +1618,8 @@ class _Translations$music$hu extends Translations$music$en {
@override String get repeatAll => 'Összes ismétlése';
@override String get repeatOne => 'Egy szám ismétlése';
@override String get instantMixNoServer => 'Nem érhető el szerver az azonnali mixhez';
@override String get instantMixFailed => 'Nem sikerült betölteni az azonnali mixet';
@override String get instantMixEmpty => 'Az azonnali mix nem adott vissza zeneszámokat';
@override String noAudioUrl({required Object track}) => 'Nem érhető el hang-URL ehhez: ${track}';
@override late final _Translations$music$discography$hu discography = _Translations$music$discography$hu._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$hu extends Translations$seerr$en {
@override String get statusBlocklisted => 'Tiltólistán';
@override String couldNotReach({required Object url, required Object error}) => 'Nem sikerült elérni ezt: ${url}: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'Nem található Seerr-példány ezen a címen: ${url} (HTTP ${status})';
@override String get invalidUrl => 'Adj meg egy szervercímet, például: https://seerr.example.com';
@override String get quickConnectUnsupported => 'Ez a Seerr-példány nem támogatja a Quick Connectet. Seerr 3.4 vagy újabb verzió szükséges.';
@override String get notInitialized => 'Ennek a Seerr-példánynak a kezdeti beállítása még nem fejeződött be';
@override String get noPlexTokenForReauth => 'Nem áll rendelkezésre Plex-token az újbóli bejelentkezéshez';
@override String get noStoredCredentials => 'Nem állnak rendelkezésre mentett hitelesítő adatok az újbóli bejelentkezéshez';
@@ -2066,6 +2143,7 @@ class _Translations$services$hu extends Translations$services$en {
// Translations
@override String get title => 'Szolgáltatások';
@override String get hubSubtitle => 'Megtekintési haladás szinkronizálása és új tartalmak igénylése.';
@override String get integrations => 'Integrációk';
@override String get notConnected => 'Nincs csatlakoztatva';
@override String connectedAs({required Object username}) => '@${username} néven csatlakoztatva';
@override String get scrobble => 'Haladás automatikus követése';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$hu extends Translations$videoControl
@override String unknown({required Object error}) => 'Hiba történt: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$hu extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Hang és feliratok';
@override String get libraryDisplay => 'Könyvtár';
@override String get personalMedia => 'Személyes média';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$hu extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get none => 'Kézzel kiválasztva';
@override String get noneDescription => 'Soha ne kapcsolja be magától a feliratokat.';
@override String get defaultMode => 'A sáv jelzőinek követése';
@override String get defaultModeDescription => 'Az egyes feliratsávokon tárolt alapértelmezett és kényszerített jelzők használata.';
@override String get always => 'Mindig engedélyezve';
@override String get alwaysDescription => 'Feliratsáv bekapcsolása az elsődleges nyelven, ha van ilyen.';
@override String get onlyForced => 'Csak kényszerített feliratok';
@override String get onlyForcedDescription => 'Csak a kényszerítettként megjelölt sávok betöltése.';
@override String get smart => 'Idegen nyelvű hang esetén megjelenítve';
@override String get smartDescription => 'Feliratok bekapcsolása csak akkor, ha a hang más nyelven szól.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$hu extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Nem SDH-feliratok előnyben részesítése';
@override String get preferSdh => 'SDH-feliratok előnyben részesítése';
@override String get onlySdh => 'Csak SDH-feliratok';
@override String get onlyNonSdh => 'Csak nem SDH-feliratok';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$hu extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Nem kényszerített feliratok előnyben részesítése';
@override String get preferForced => 'Kényszerített feliratok előnyben részesítése';
@override String get onlyForced => 'Csak kényszerített feliratok';
@override String get onlyNonForced => 'Csak nem kényszerített feliratok';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$hu extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get none => 'Soha';
@override String get moviesAndShows => 'Filmek és TV-sorozatok';
@override String get movies => 'Csak filmek';
@override String get shows => 'Csak TV-sorozatok';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$hu extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$hu._(TranslationsHu root) : this._root = root, super.internal(root);
final TranslationsHu _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Felhasználók és kritikusok';
@override String get usersOnly => 'Csak felhasználók';
@override String get criticsOnly => 'Csak kritikusok';
@override String get nobody => 'Rejtett';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$hu extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$hu._(TranslationsHu root) : this._root = root, super.internal(root);
@@ -2786,6 +2947,7 @@ extension on TranslationsHu {
'auth.useBrowser' => 'Böngésző használata',
'auth.or' => 'vagy',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Csatlakozás a(z) ${product} szolgáltatáshoz',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Quick Connect használata',
'auth.quickConnectInstructions' => 'Nyisd meg a Quick Connect-et a Jellyfinben, és add meg ezt a kódot.',
'auth.quickConnectWaiting' => 'Várakozás a jóváhagyásra…',
@@ -2825,6 +2987,7 @@ extension on TranslationsHu {
'common.mute' => 'Némítás',
'common.ok' => 'OK',
'common.off' => 'Ki',
'common.options' => 'Lehetőségek',
'common.seasonNumber' => ({required Object number}) => '${number}. évad',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => '${number}. epizód - ${title}',
'common.chapterNumber' => ({required Object number}) => '${number}. fejezet',
@@ -2903,6 +3066,10 @@ extension on TranslationsHu {
'settings.displayScale' => 'Kijelző méretezése',
'settings.compact' => 'Kompakt',
'settings.comfortable' => 'Kényelmes',
'settings.gridSpacing' => 'Rácstávolság',
'settings.gridSpacingTight' => 'Sűrű',
'settings.gridSpacingNormal' => 'Normál',
'settings.gridSpacingSpacious' => 'Tágas',
'settings.tvCornerSpotlightBackdrop' => 'Sarokban megjelenő kiemelt háttérkép',
'settings.tvCornerSpotlightBackdropDescription' => 'A kiemelt háttérkép megjelenítése a jobb felső sarokban a teljes képernyő helyett',
'settings.viewMode' => 'Nézetmód',
@@ -2946,16 +3113,14 @@ extension on TranslationsHu {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Hardveres dekódolás',
'settings.hardwareDecodingDescription' => 'Hardveres gyorsítás használata, ha elérhető',
'settings.bufferSize' => 'Puffer mérete',
'settings.bufferSizeMB' => ({required Object size}) => '${size} MB',
'settings.bufferSizeAuto' => 'Automatikus (ajánlott)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} MB memória érhető el. A(z) ${size} MB méretű puffer befolyásolhatja a lejátszást.',
'settings.playbackBuffer' => 'Lejátszási puffer',
'settings.playbackBufferAuto' => 'Automatikus (ajánlott)',
'settings.playbackBufferLarge' => 'Nagy',
'settings.playbackBufferExtraLarge' => 'Extra nagy',
'settings.playbackBufferDescription' => 'Több puffert használ instabil kapcsolatok ellen. A pufferméret is korlátozza.',
'settings.defaultQualityTitle' => 'Alapértelmezett minőség',
'settings.cellularQualityTitle' => 'Alapértelmezett minőség mobilhálózaton',
'settings.cellularQualitySameAsDefault' => 'Ugyanaz, mint az alapértelmezett minőség',
'settings.musicQualityTitle' => 'Zene minősége',
'settings.subtitleStyling' => 'Feliratok stílusa',
'settings.subtitleStylingDescription' => 'Feliratok megjelenésének testreszabása',
@@ -2969,6 +3134,8 @@ extension on TranslationsHu {
'settings.rememberTrackSelectionsDescription' => 'Hang- és feliratválasztások megjegyzése címenként',
'settings.followServerTrackSelections' => 'A szerver epizódonkénti sávválasztásának használata',
'settings.followServerTrackSelectionsDescription' => 'Epizódváltáskor a szerveren kiválasztott hang és felirat lép életbe az aktuális választás átvitele helyett',
'settings.resumeMusicOnLaunch' => 'Zenei munkamenet megjegyzése',
'settings.resumeMusicOnLaunchDescription' => 'Az alkalmazás indításakor az utolsó szám szüneteltetve nyílik meg ott, ahol abbamaradt',
'settings.showChapterMarkersOnTimeline' => 'Fejezetjelölők megjelenítése az idősávon',
'settings.showChapterMarkersOnTimelineDescription' => 'Az idősáv felosztása a fejezetek határainál',
'settings.specialsOrdering' => 'Különkiadások epizódsorrendben',
@@ -3022,7 +3189,11 @@ extension on TranslationsHu {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'A billentyűparancs már hozzá van rendelve a következőhöz: ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Billentyűparancs frissítve a következőhöz: ${action}',
'settings.saveFailed' => 'Nem sikerült menteni a módosításokat. Próbáld újra.',
'settings.autoSkip' => 'Automatikus átugrás',
'settings.autoPlayAndSkip' => 'Automatikus lejátszás és átugrás',
'settings.autoPlayNextEpisode' => 'Következő epizód automatikus lejátszása',
'settings.autoPlayNextEpisodeDescription' => 'A következő epizód automatikus elindítása, amikor az aktuális véget ér',
'settings.playNextCountdown' => 'Visszaszámlálás a következő epizódig',
'settings.playNextCountdownImmediate' => 'Azonnali lejátszás',
'settings.autoSkipIntro' => 'Intró automatikus átugrása',
'settings.autoSkipIntroDescription' => 'Az intrójelölők automatikus átugrása néhány másodperc után',
'settings.autoSkipCredits' => 'Stáblista automatikus átugrása',
@@ -3097,10 +3268,8 @@ extension on TranslationsHu {
'settings.dvConversionNativeDescription' => 'A natív DV7 kényszerítése és a DV-átalakítási újrapróbálkozás letiltása',
'settings.dvConversionDv81Description' => 'A közvetlen RPU-átalakítás kényszerítése Dolby Vision Profile 8.1-re',
'settings.dvConversionHevcStripDescription' => 'A Dolby Vision RPU/EL-rétegek eltávolítása és egyszerű HEVC-ként való megjelenítés',
'settings.demuxer' => 'Konténer-demuxer',
'settings.demuxerDescription' => 'Melyik demuxer dolgozza fel a közvetlenül lejátszott fájlokat. Válts media3-ra, ha egy fájl hibásan viselkedik.',
'settings.demuxerFfmpeg' => 'FFmpeg (ajánlott)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterlacing',
'settings.deinterlaceDescription' => 'Fésűfog-műtermékek eltávolítása a váltott soros videóból (csak mpv lejátszóval)',
'settings.requireProfileSelectionOnOpen' => 'Profil kérése az alkalmazás megnyitásakor',
'settings.requireProfileSelectionOnOpenDescription' => 'Profilválasztó megjelenítése minden alkalommal, amikor az alkalmazást megnyitod',
'settings.forceTvMode' => 'TV-mód kényszerítése',
@@ -3118,15 +3287,31 @@ extension on TranslationsHu {
'settings.showExploreTabDescription' => 'A Plex Discover és a csatlakoztatott követők tartalmait megjelenítő Felfedezés lap megjelenítése',
'settings.liveTvDefaultFavorites' => 'Kedvenc csatornák alapértelmezés szerint',
'settings.liveTvDefaultFavoritesDescription' => 'Csak a kedvenc csatornák megjelenítése az Élő TV megnyitásakor',
'settings.general' => 'Általános',
'settings.generalDescription' => 'Nyelv, indítás és ablakviselkedés',
'settings.languageAndRegion' => 'Nyelv és régió',
'settings.startup' => 'Indítás',
'settings.display' => 'Kijelző',
'settings.libraryAndCards' => 'Könyvtár és kártyák',
'settings.homeScreen' => 'Kezdőképernyő',
'settings.navigation' => 'Navigáció',
'settings.window' => 'Ablak',
'settings.content' => 'Tartalom',
'settings.liveTv' => 'Élő TV',
'settings.player' => 'Lejátszó',
'settings.subtitlesAndConfig' => 'Feliratok és konfiguráció',
'settings.videoAndDisplay' => 'Videó és kijelző',
'settings.audio' => 'Hang',
'settings.quality' => 'Minőség',
'settings.subtitles' => 'Feliratok',
'settings.seekAndTiming' => 'Tekerés és időzítés',
'settings.behavior' => 'Viselkedés',
'settings.gestures' => 'Mozdulatok',
'settings.gestureBrightnessSwipe' => 'Fényerő-húzás',
'settings.gestureBrightnessSwipeDescription' => 'Húzd felfelé vagy lefelé a bal szélén a fényerő beállításához',
'settings.gestureVolumeSwipe' => 'Hangerő-húzás',
'settings.gestureVolumeSwipeDescription' => 'Húzd felfelé vagy lefelé a jobb szélén a hangerő beállításához',
'settings.gesturePinchToZoom' => 'Csípés a nagyításhoz',
'settings.gesturePinchToZoomDescription' => 'Csípj a videóra a nagyításhoz vagy kicsinyítéshez',
'settings.controls' => 'Vezérlők',
'settings.rememberPlayerChanges' => 'Lejátszó módosításainak megjegyzése',
'settings.rememberPlayerChangesDescription' => 'A lejátszás közben végzett módosítások mentési és újbóli alkalmazási helye',
'settings.scopePlaybackSpeed' => 'Lejátszási sebesség',
@@ -3264,6 +3449,8 @@ extension on TranslationsHu {
'fileInfo.has64bitOffsets' => '64 bites eltolások',
'fileInfo.protocol' => 'Protokoll',
'fileInfo.mediaType' => 'Médiatípus',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Forrás típusa',
'fileInfo.optimizedVersion' => 'Optimalizált verzió',
'fileInfo.optimizationTarget' => 'Optimalizálási cél',
@@ -3288,8 +3475,6 @@ extension on TranslationsHu {
'fileInfo.flagOriginal' => 'Eredeti',
'fileInfo.channelsMono' => 'Monó',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => '${profile} profil',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Megjelölés megtekintettként',
'mediaMenu.markAsUnwatched' => 'Megjelölés nem megtekintettként',
'mediaMenu.removeFromContinueWatching' => 'Eltávolítás a folytatásból',
@@ -3624,6 +3809,63 @@ extension on TranslationsHu {
'connections.signInAgain' => 'Bejelentkezés újra',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'A(z) ${product} kapcsolat szerkesztése',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Adjon hozzá vagy távolítson el URL-eket a(z) ${serverName} esetén. A Plezy a legkisebb késleltetésű, elérhető URL-t fogja használni.',
'accountPreferences.sectionTitle' => 'Fiókbeállítások',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Hang-, felirat- és könyvtárbeállítások mentve a(z) ${account} fiókra',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Hang-, felirat- és könyvtárbeállítások mentve ${count} fiókra',
'accountPreferences.pickAccount' => 'Minden fiók a saját beállításait tárolja. Válaszd ki a szerkesztendőt.',
'accountPreferences.storedOnAccount' => 'Ezek a beállítások magán a fiókon tárolódnak, így minden bejelentkezett alkalmazás használja őket — beleértve a Plezyt a többi eszközödön.',
'accountPreferences.noAccounts' => 'Nincs beállítható fiók',
'accountPreferences.noAccountsHint' => 'Jelentkezz be a Plexbe, vagy csatlakoztass egy Jellyfin- vagy Emby-szervert, és az azon a fiókon tárolt beállítások itt jelennek meg.',
'accountPreferences.unavailable' => 'Ez a fiók nem érhető el',
'accountPreferences.loadFailed' => 'Nem sikerült betölteni ezeket a beállításokat',
'accountPreferences.noPreference' => 'Nincs preferencia',
'accountPreferences.notSet' => 'Nincs beállítva',
'accountPreferences.groups.audioAndSubtitles' => 'Hang és feliratok',
'accountPreferences.groups.libraryDisplay' => 'Könyvtár',
'accountPreferences.groups.personalMedia' => 'Személyes média',
'accountPreferences.preferredAudioLanguage' => 'Elsődleges hangnyelv',
'accountPreferences.autoSelectAudio' => 'Hangsáv kiválasztása nyelv alapján',
'accountPreferences.autoSelectAudioDescription' => 'Ki kapcsolásnál a fájl által alapértelmezettként megjelölt hangsáv marad.',
'accountPreferences.preferredSubtitleLanguage' => 'Elsődleges feliratnyelv',
'accountPreferences.subtitleMode' => 'Feliratok bekapcsolása',
'accountPreferences.subtitleModes.none' => 'Kézzel kiválasztva',
'accountPreferences.subtitleModes.noneDescription' => 'Soha ne kapcsolja be magától a feliratokat.',
'accountPreferences.subtitleModes.defaultMode' => 'A sáv jelzőinek követése',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Az egyes feliratsávokon tárolt alapértelmezett és kényszerített jelzők használata.',
'accountPreferences.subtitleModes.always' => 'Mindig engedélyezve',
'accountPreferences.subtitleModes.alwaysDescription' => 'Feliratsáv bekapcsolása az elsődleges nyelven, ha van ilyen.',
'accountPreferences.subtitleModes.onlyForced' => 'Csak kényszerített feliratok',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Csak a kényszerítettként megjelölt sávok betöltése.',
'accountPreferences.subtitleModes.smart' => 'Idegen nyelvű hang esetén megjelenítve',
'accountPreferences.subtitleModes.smartDescription' => 'Feliratok bekapcsolása csak akkor, ha a hang más nyelven szól.',
'accountPreferences.subtitleAccessibility' => 'SDH-feliratok',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Nem SDH-feliratok előnyben részesítése',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'SDH-feliratok előnyben részesítése',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Csak SDH-feliratok',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Csak nem SDH-feliratok',
'accountPreferences.forcedSubtitles' => 'Kényszerített feliratok',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Nem kényszerített feliratok előnyben részesítése',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Kényszerített feliratok előnyben részesítése',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Csak kényszerített feliratok',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Csak nem kényszerített feliratok',
'accountPreferences.displayMissingEpisodes' => 'Hiányzó epizódok megjelenítése',
'accountPreferences.displayMissingEpisodesDescription' => 'Azoknak az epizódoknak a listázása, amelyeket a szerver ismer, de nincs hozzájuk fájl.',
'accountPreferences.hidePlayedInLatest' => 'Megnézett elemek elrejtése a Legújabbak között',
'accountPreferences.hidePlayedInLatestDescription' => 'A már megnézett elemek kimaradnak a szerver Legújabbak soraiból.',
'accountPreferences.displayCollectionsView' => 'Gyűjtemények nézet megjelenítése',
'accountPreferences.displayCollectionsViewDescription' => 'A szerver Gyűjtemények nézetének megjelenítése a könyvtárak mellett.',
'accountPreferences.rewatchingInNextUp' => 'Újranézett sorozatok megtartása a Következikben',
'accountPreferences.rewatchingInNextUpDescription' => 'Ha befejezel egy sorozatot, és újra elkezded, a Következik az újranézést követi, ahelyett hogy eldobná a sorozatot.',
'accountPreferences.watchedIndicator' => 'Megtekintett jelzések',
'accountPreferences.watchedIndicatorOptions.none' => 'Soha',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Filmek és TV-sorozatok',
'accountPreferences.watchedIndicatorOptions.movies' => 'Csak filmek',
'accountPreferences.watchedIndicatorOptions.shows' => 'Csak TV-sorozatok',
'accountPreferences.mediaReviewsVisibility' => 'Értékelések és kritikák',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Felhasználók és kritikusok',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Csak felhasználók',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Csak kritikusok',
'accountPreferences.mediaReviewsOptions.nobody' => 'Rejtett',
'discover.title' => 'Felfedezés',
'discover.noContentAvailable' => 'Nincs elérhető tartalom',
'discover.addMediaToLibraries' => 'Adj hozzá médiát a könyvtáraidhoz',
@@ -3721,6 +3963,8 @@ extension on TranslationsHu {
'libraries.filterCategories.contentRating' => 'Korhatár-besorolás',
'libraries.filterCategories.tag' => 'Címke',
'libraries.filterCategories.unwatched' => 'Nem látott',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Nem lejátszott',
'libraries.filterCategories.favorites' => 'Kedvencek',
'libraries.sortLabels.title' => 'Cím',
@@ -3802,8 +4046,6 @@ extension on TranslationsHu {
'explore.rows.watchlist' => 'Néznivalók listája',
'explore.rows.recommendedMovies' => 'Ajánlott filmek',
'explore.rows.recommendedShows' => 'Ajánlott sorozatok',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Felkapott filmek',
'explore.rows.trendingShows' => 'Felkapott sorozatok',
'explore.rows.popularMovies' => 'Népszerű filmek',
@@ -3956,6 +4198,11 @@ extension on TranslationsHu {
'liveTv.unknownChannel' => 'Ismeretlen csatorna',
'liveTv.live' => 'ÉLŐ',
'liveTv.reloadGuide' => 'Műsorújság újratöltése',
'liveTv.searchGuide' => 'Keresés a műsorújságban',
'liveTv.searchHint' => 'Csatornák és műsorok keresése',
'liveTv.searchNoResults' => ({required Object query}) => 'Nincs találat a következőre: "${query}"',
'liveTv.channelsSection' => 'Csatornák',
'liveTv.programsSection' => 'Műsorok',
'liveTv.now' => 'Most',
'liveTv.today' => 'Ma',
'liveTv.tomorrow' => 'Holnap',
@@ -4093,6 +4340,8 @@ extension on TranslationsHu {
'music.repeatAll' => 'Összes ismétlése',
'music.repeatOne' => 'Egy szám ismétlése',
'music.instantMixNoServer' => 'Nem érhető el szerver az azonnali mixhez',
'music.instantMixFailed' => 'Nem sikerült betölteni az azonnali mixet',
'music.instantMixEmpty' => 'Az azonnali mix nem adott vissza zeneszámokat',
'music.noAudioUrl' => ({required Object track}) => 'Nem érhető el hang-URL ehhez: ${track}',
'music.discography.singlesAndEps' => 'Kislemezek és EP-k',
'music.discography.live' => 'Élő',
@@ -4228,6 +4477,8 @@ extension on TranslationsHu {
'downloads.syncRuleCleanupUnavailable' => 'A kapcsolódó letöltések nem azonosíthatók biztonságosan. Csatlakoztassa újra a szervert, és próbálja újra, vagy távolítsa el a szabályt a letöltések törlése nélkül.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => '${count} új epizód szinkronizálva a következőhöz: ${title}',
'downloads.activeSyncRules' => 'Szinkronizálási szabályok',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Nincsenek szinkronizálási szabályok',
'downloads.manageSyncRule' => 'Szinkronizálás kezelése',
'downloads.editEpisodeCount' => 'Epizódszám',
@@ -4316,8 +4567,6 @@ extension on TranslationsHu {
'companionRemote.pairing.availableDevices' => 'Elérhető eszközök',
'companionRemote.pairing.manualConnection' => 'Kézi csatlakozás',
'companionRemote.pairing.cryptoInitFailed' => 'Nem sikerült elindítani a biztonságos kapcsolatot. Először jelentkezz be a Plexbe.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Add meg a gazdagép címét',
'companionRemote.pairing.validationHostFormat' => 'A formátumnak IP:port kell lennie (pl. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'A csatlakozás túllépte az időkorlátot. Használd ugyanazt a hálózatot mindkét eszközön.',
@@ -4577,6 +4826,8 @@ extension on TranslationsHu {
'seerr.statusBlocklisted' => 'Tiltólistán',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Nem sikerült elérni ezt: ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'Nem található Seerr-példány ezen a címen: ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Adj meg egy szervercímet, például: https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Ez a Seerr-példány nem támogatja a Quick Connectet. Seerr 3.4 vagy újabb verzió szükséges.',
'seerr.notInitialized' => 'Ennek a Seerr-példánynak a kezdeti beállítása még nem fejeződött be',
'seerr.noPlexTokenForReauth' => 'Nem áll rendelkezésre Plex-token az újbóli bejelentkezéshez',
'seerr.noStoredCredentials' => 'Nem állnak rendelkezésre mentett hitelesítő adatok az újbóli bejelentkezéshez',
@@ -4587,6 +4838,7 @@ extension on TranslationsHu {
'seerr.sessionRejectedAfterReauth' => 'A munkamenetet az újbóli bejelentkezés után elutasították',
'services.title' => 'Szolgáltatások',
'services.hubSubtitle' => 'Megtekintési haladás szinkronizálása és új tartalmak igénylése.',
'services.integrations' => 'Integrációk',
'services.notConnected' => 'Nincs csatlakoztatva',
'services.connectedAs' => ({required Object username}) => '@${username} néven csatlakoztatva',
'services.scrobble' => 'Haladás automatikus követése',
+282 -30
View File
@@ -60,6 +60,7 @@ class TranslationsIt extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$it dialog = _Translations$dialog$it._(_root);
@override late final _Translations$profiles$it profiles = _Translations$profiles$it._(_root);
@override late final _Translations$connections$it connections = _Translations$connections$it._(_root);
@override late final _Translations$accountPreferences$it accountPreferences = _Translations$accountPreferences$it._(_root);
@override late final _Translations$discover$it discover = _Translations$discover$it._(_root);
@override late final _Translations$errors$it errors = _Translations$errors$it._(_root);
@override late final _Translations$libraries$it libraries = _Translations$libraries$it._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$it extends Translations$auth$en {
@override String get useBrowser => 'Usa il browser';
@override String get or => 'o';
@override String connectToMediaBrowser({required Object product}) => 'Connettiti a ${product}';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Usa Quick Connect';
@override String get quickConnectInstructions => 'Apri Quick Connect in Jellyfin e inserisci questo codice.';
@override String get quickConnectWaiting => 'In attesa di approvazione…';
@@ -165,6 +167,7 @@ class _Translations$common$it extends Translations$common$en {
@override String get mute => 'Disattiva audio';
@override String get ok => 'OK';
@override String get off => 'Disattivato';
@override String get options => 'Opzioni';
@override String seasonNumber({required Object number}) => 'Stagione ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Episodio ${number} - ${title}';
@override String chapterNumber({required Object number}) => 'Capitolo ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$it extends Translations$settings$en {
@override String get displayScale => 'Scala di visualizzazione';
@override String get compact => 'Compatta';
@override String get comfortable => 'Comoda';
@override String get gridSpacing => 'Spaziatura della griglia';
@override String get gridSpacingTight => 'Ristretta';
@override String get gridSpacingNormal => 'Normale';
@override String get gridSpacingSpacious => 'Ampia';
@override String get tvCornerSpotlightBackdrop => 'Sfondo in evidenza nell\'angolo';
@override String get tvCornerSpotlightBackdropDescription => 'Mostra l\'immagine in evidenza nell\'angolo in alto a destra anziché a schermo intero';
@override String get viewMode => 'Modalità di visualizzazione';
@@ -292,16 +299,14 @@ class _Translations$settings$it extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Decodifica hardware';
@override String get hardwareDecodingDescription => 'Utilizza l\'accelerazione hardware quando disponibile';
@override String get bufferSize => 'Dimensione buffer';
@override String bufferSizeMB({required Object size}) => '${size}MB';
@override String get bufferSizeAuto => 'Automatica (consigliata)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MB di memoria disponibile. Un buffer di ${size}MB può influire sulla riproduzione.';
@override String get playbackBuffer => 'Buffer di riproduzione';
@override String get playbackBufferAuto => 'Auto (consigliato)';
@override String get playbackBufferLarge => 'Grande';
@override String get playbackBufferExtraLarge => 'Extra grande';
@override String get playbackBufferDescription => 'Bufferizza di più contro connessioni instabili. Limitato anche dalla dimensione del buffer.';
@override String get defaultQualityTitle => 'Qualità predefinita';
@override String get cellularQualityTitle => 'Qualità predefinita sulla rete mobile';
@override String get cellularQualitySameAsDefault => 'Come la qualità predefinita';
@override String get musicQualityTitle => 'Qualità musicale';
@override String get subtitleStyling => 'Stile sottotitoli';
@override String get subtitleStylingDescription => 'Personalizza l\'aspetto dei sottotitoli';
@@ -315,6 +320,8 @@ class _Translations$settings$it extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Ricorda le scelte di audio e sottotitoli per ogni titolo';
@override String get followServerTrackSelections => 'Usa le tracce selezionate sul server per ogni episodio';
@override String get followServerTrackSelectionsDescription => 'Al cambio di episodio applica l\'audio e i sottotitoli selezionati sul server invece di mantenere la scelta corrente';
@override String get resumeMusicOnLaunch => 'Ricorda la sessione musicale';
@override String get resumeMusicOnLaunchDescription => 'All\'avvio dell\'app riapri l\'ultimo brano in pausa dal punto in cui era rimasto';
@override String get showChapterMarkersOnTimeline => 'Mostra i marcatori dei capitoli sulla barra di avanzamento';
@override String get showChapterMarkersOnTimelineDescription => 'Segmenta la barra di avanzamento ai confini dei capitoli';
@override String get specialsOrdering => 'Speciali nell\'ordine degli episodi';
@@ -368,7 +375,11 @@ class _Translations$settings$it extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Scorciatoia già assegnata a ${action}';
@override String shortcutUpdated({required Object action}) => 'Scorciatoia aggiornata per ${action}';
@override String get saveFailed => 'Impossibile salvare le modifiche. Riprova.';
@override String get autoSkip => 'Salto automatico';
@override String get autoPlayAndSkip => 'Riproduzione automatica e salti';
@override String get autoPlayNextEpisode => 'Riproduci automaticamente l\'episodio successivo';
@override String get autoPlayNextEpisodeDescription => 'Avvia automaticamente l\'episodio successivo quando termina quello in riproduzione';
@override String get playNextCountdown => 'Conto alla rovescia del successivo';
@override String get playNextCountdownImmediate => 'Riproduci subito';
@override String get autoSkipIntro => 'Salta automaticamente la sigla iniziale';
@override String get autoSkipIntroDescription => 'Salta automaticamente i marcatori della sigla iniziale dopo alcuni secondi';
@override String get autoSkipCredits => 'Salta automaticamente i titoli di coda';
@@ -443,10 +454,8 @@ class _Translations$settings$it extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Forza il DV7 nativo e impedisce nuovi tentativi di conversione DV';
@override String get dvConversionDv81Description => 'Forza la conversione RPU diretta al profilo Dolby Vision 8.1';
@override String get dvConversionHevcStripDescription => 'Rimuove i livelli RPU/EL di Dolby Vision e riproduce il video come semplice HEVC';
@override String get demuxer => 'Demuxer del contenitore';
@override String get demuxerDescription => 'Quale demuxer analizza i file riprodotti in diretta. Passa a media3 se un file dà problemi.';
@override String get demuxerFfmpeg => 'FFmpeg (consigliato)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Deinterlacciamento';
@override String get deinterlaceDescription => 'Rimuove gli artefatti a pettine dai video interlacciati (solo lettore mpv)';
@override String get requireProfileSelectionOnOpen => 'Chiedi di scegliere il profilo all\'apertura';
@override String get requireProfileSelectionOnOpenDescription => 'Mostra la selezione del profilo ogni volta che l\'app viene aperta';
@override String get forceTvMode => 'Forza modalità TV';
@@ -464,15 +473,31 @@ class _Translations$settings$it extends Translations$settings$en {
@override String get showExploreTabDescription => 'Visualizza la scheda Esplora con contenuti di Esplora di Plex e dei tracker collegati';
@override String get liveTvDefaultFavorites => 'Apri sui canali preferiti';
@override String get liveTvDefaultFavoritesDescription => 'Mostra solo i canali preferiti quando apri la TV in diretta';
@override String get general => 'Generali';
@override String get generalDescription => 'Lingua, avvio e comportamento della finestra';
@override String get languageAndRegion => 'Lingua e regione';
@override String get startup => 'Avvio';
@override String get display => 'Schermo';
@override String get libraryAndCards => 'Libreria e schede';
@override String get homeScreen => 'Schermata iniziale';
@override String get navigation => 'Navigazione';
@override String get window => 'Finestra';
@override String get content => 'Contenuti';
@override String get liveTv => 'TV in diretta';
@override String get player => 'Lettore';
@override String get subtitlesAndConfig => 'Sottotitoli e impostazioni';
@override String get videoAndDisplay => 'Video e schermo';
@override String get audio => 'Audio';
@override String get quality => 'Qualità';
@override String get subtitles => 'Sottotitoli';
@override String get seekAndTiming => 'Avanzamento e tempi';
@override String get behavior => 'Comportamento';
@override String get gestures => 'Gesti';
@override String get gestureBrightnessSwipe => 'Scorrimento per la luminosità';
@override String get gestureBrightnessSwipeDescription => 'Scorri verso l\'alto o il basso sul bordo sinistro per regolare la luminosità';
@override String get gestureVolumeSwipe => 'Scorrimento per il volume';
@override String get gestureVolumeSwipeDescription => 'Scorri verso l\'alto o il basso sul bordo destro per regolare il volume';
@override String get gesturePinchToZoom => 'Pizzica per lo zoom';
@override String get gesturePinchToZoomDescription => 'Pizzica il video per ingrandire o ridurre';
@override String get controls => 'Controlli';
@override String get rememberPlayerChanges => 'Ricorda le modifiche del lettore';
@override String get rememberPlayerChangesDescription => 'Dove viene salvata e riapplicata una modifica effettuata durante la riproduzione';
@override String get scopePlaybackSpeed => 'Velocità di riproduzione';
@@ -569,7 +594,7 @@ class _Translations$fileInfo$it extends Translations$fileInfo$en {
@override String get interlaced => 'Interlacciato';
@override String get anamorphic => 'Anamorfico';
@override String get referenceFrames => 'Frame di riferimento';
@override String get dynamicRange => 'Dynamic Range';
@override String get dynamicRange => 'Gamma dinamica';
@override String get dolbyVision => 'Dolby Vision';
@override String get dolbyVisionLevel => 'Dolby Vision Level';
@override String get dolbyVisionVersion => 'Dolby Vision Version';
@@ -1086,6 +1111,49 @@ class _Translations$connections$it extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Aggiungi o rimuovi gli URL per ${serverName}. Plezy userà l\'URL raggiungibile con la latenza più bassa.';
}
// Path: accountPreferences
class _Translations$accountPreferences$it extends Translations$accountPreferences$en {
_Translations$accountPreferences$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Preferenze dell\'account';
@override String hubSubtitleSingle({required Object account}) => 'Opzioni di audio, sottotitoli e libreria salvate su ${account}';
@override String hubSubtitleMultiple({required Object count}) => 'Opzioni di audio, sottotitoli e libreria salvate su ${count} account';
@override String get pickAccount => 'Ogni account conserva le proprie preferenze. Scegli quale modificare.';
@override String get storedOnAccount => 'Queste opzioni vengono salvate sull\'account stesso, quindi ogni app che vi accede le usa — inclusa Plezy sugli altri tuoi dispositivi.';
@override String get noAccounts => 'Nessun account da configurare';
@override String get noAccountsHint => 'Accedi a Plex oppure connetti un server Jellyfin o Emby: le preferenze salvate su quell\'account appariranno qui.';
@override String get unavailable => 'Impossibile raggiungere questo account';
@override String get loadFailed => 'Impossibile caricare queste preferenze';
@override String get noPreference => 'Nessuna preferenza';
@override String get notSet => 'Non impostato';
@override late final _Translations$accountPreferences$groups$it groups = _Translations$accountPreferences$groups$it._(_root);
@override String get preferredAudioLanguage => 'Lingua audio preferita';
@override String get autoSelectAudio => 'Scegli l\'audio in base alla lingua';
@override String get autoSelectAudioDescription => 'Se disattivata, viene mantenuta la traccia audio che il file indica come predefinita.';
@override String get preferredSubtitleLanguage => 'Lingua dei sottotitoli preferita';
@override String get subtitleMode => 'Attiva i sottotitoli';
@override late final _Translations$accountPreferences$subtitleModes$it subtitleModes = _Translations$accountPreferences$subtitleModes$it._(_root);
@override String get subtitleAccessibility => 'Sottotitoli SDH';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$it subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$it._(_root);
@override String get forcedSubtitles => 'Sottotitoli forzati';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$it forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$it._(_root);
@override String get displayMissingEpisodes => 'Mostra gli episodi mancanti';
@override String get displayMissingEpisodesDescription => 'Elenca gli episodi noti al server ma senza un file associato.';
@override String get hidePlayedInLatest => 'Nascondi gli elementi visti da Aggiunti di recente';
@override String get hidePlayedInLatestDescription => 'Esclude dagli Aggiunti di recente del server gli elementi che hai già visto.';
@override String get displayCollectionsView => 'Mostra la vista Raccolte';
@override String get displayCollectionsViewDescription => 'Mostra la vista Raccolte del server insieme alle tue librerie.';
@override String get rewatchingInNextUp => 'Mantieni le serie riviste in Prossimi episodi';
@override String get rewatchingInNextUpDescription => 'Quando finisci una serie e la ricominci, Prossimi episodi segue la nuova visione invece di rimuovere la serie.';
@override String get watchedIndicator => 'Indicatori di visione';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$it watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$it._(_root);
@override String get mediaReviewsVisibility => 'Valutazioni e recensioni';
@override late final _Translations$accountPreferences$mediaReviewsOptions$it mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$it._(_root);
}
// Path: discover
class _Translations$discover$it extends Translations$discover$en {
_Translations$discover$it._(TranslationsIt root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$it extends Translations$liveTv$en {
@override String get unknownChannel => 'Canale sconosciuto';
@override String get live => 'IN DIRETTA';
@override String get reloadGuide => 'Ricarica la guida';
@override String get searchGuide => 'Cerca nella guida';
@override String get searchHint => 'Cerca canali e programmi';
@override String searchNoResults({required Object query}) => 'Nessuna corrispondenza per "${query}"';
@override String get channelsSection => 'Canali';
@override String get programsSection => 'Programmi';
@override String get now => 'Ora';
@override String get today => 'Oggi';
@override String get tomorrow => 'Domani';
@@ -1545,6 +1618,8 @@ class _Translations$music$it extends Translations$music$en {
@override String get repeatAll => 'Ripeti tutto';
@override String get repeatOne => 'Ripeti il brano';
@override String get instantMixNoServer => 'Nessun server disponibile per un mix istantaneo';
@override String get instantMixFailed => 'Impossibile caricare il mix istantaneo';
@override String get instantMixEmpty => 'Il mix istantaneo non ha prodotto alcun brano';
@override String noAudioUrl({required Object track}) => 'Nessun URL audio disponibile per ${track}';
@override late final _Translations$music$discography$it discography = _Translations$music$discography$it._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$it extends Translations$seerr$en {
@override String get statusBlocklisted => 'Nella lista di blocco';
@override String couldNotReach({required Object url, required Object error}) => 'Impossibile raggiungere ${url}: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'Nessuna istanza Seerr all\'indirizzo ${url} (HTTP ${status})';
@override String get invalidUrl => 'Inserisci un indirizzo del server come https://seerr.example.com';
@override String get quickConnectUnsupported => 'Questa istanza Seerr non supporta Quick Connect. È necessaria la versione 3.4 o successiva di Seerr.';
@override String get notInitialized => 'Questa istanza Seerr non ha completato la configurazione iniziale';
@override String get noPlexTokenForReauth => 'Nessun token Plex disponibile per effettuare nuovamente l\'accesso';
@override String get noStoredCredentials => 'Nessuna credenziale salvata disponibile per effettuare nuovamente l\'accesso';
@@ -2066,6 +2143,7 @@ class _Translations$services$it extends Translations$services$en {
// Translations
@override String get title => 'Servizi';
@override String get hubSubtitle => 'Sincronizza i progressi di visione e richiedi nuovi titoli.';
@override String get integrations => 'Integrazioni';
@override String get notConnected => 'Non connesso';
@override String connectedAs({required Object username}) => 'Connesso come @${username}';
@override String get scrobble => 'Registra automaticamente i progressi';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$it extends Translations$videoControl
@override String unknown({required Object error}) => 'Si è verificato un errore: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$it extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Audio e sottotitoli';
@override String get libraryDisplay => 'Libreria';
@override String get personalMedia => 'Media personali';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$it extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get none => 'Selezionati manualmente';
@override String get noneDescription => 'Non attivare mai i sottotitoli automaticamente.';
@override String get defaultMode => 'Segui le flag della traccia';
@override String get defaultModeDescription => 'Usa le flag predefinite e forzate salvate su ogni traccia di sottotitoli.';
@override String get always => 'Sempre attivi';
@override String get alwaysDescription => 'Attiva una traccia di sottotitoli nella lingua preferita quando disponibile.';
@override String get onlyForced => 'Solo sottotitoli forzati';
@override String get onlyForcedDescription => 'Carica solo le tracce contrassegnate come forzate.';
@override String get smart => 'Mostrati con audio straniero';
@override String get smartDescription => 'Attiva i sottotitoli solo quando l\'audio è in un\'altra lingua.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$it extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Preferisci i sottotitoli non SDH';
@override String get preferSdh => 'Preferisci i sottotitoli SDH';
@override String get onlySdh => 'Solo sottotitoli SDH';
@override String get onlyNonSdh => 'Solo sottotitoli non SDH';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$it extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Preferisci i sottotitoli non forzati';
@override String get preferForced => 'Preferisci i sottotitoli forzati';
@override String get onlyForced => 'Solo sottotitoli forzati';
@override String get onlyNonForced => 'Solo sottotitoli non forzati';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$it extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get none => 'Mai';
@override String get moviesAndShows => 'Film e serie TV';
@override String get movies => 'Solo film';
@override String get shows => 'Solo serie TV';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$it extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$it._(TranslationsIt root) : this._root = root, super.internal(root);
final TranslationsIt _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Utenti e critici';
@override String get usersOnly => 'Solo utenti';
@override String get criticsOnly => 'Solo critici';
@override String get nobody => 'Nascoste';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$it extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$it._(TranslationsIt root) : this._root = root, super.internal(root);
@@ -2786,6 +2947,7 @@ extension on TranslationsIt {
'auth.useBrowser' => 'Usa il browser',
'auth.or' => 'o',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Connettiti a ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Usa Quick Connect',
'auth.quickConnectInstructions' => 'Apri Quick Connect in Jellyfin e inserisci questo codice.',
'auth.quickConnectWaiting' => 'In attesa di approvazione…',
@@ -2825,6 +2987,7 @@ extension on TranslationsIt {
'common.mute' => 'Disattiva audio',
'common.ok' => 'OK',
'common.off' => 'Disattivato',
'common.options' => 'Opzioni',
'common.seasonNumber' => ({required Object number}) => 'Stagione ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Episodio ${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'Capitolo ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsIt {
'settings.displayScale' => 'Scala di visualizzazione',
'settings.compact' => 'Compatta',
'settings.comfortable' => 'Comoda',
'settings.gridSpacing' => 'Spaziatura della griglia',
'settings.gridSpacingTight' => 'Ristretta',
'settings.gridSpacingNormal' => 'Normale',
'settings.gridSpacingSpacious' => 'Ampia',
'settings.tvCornerSpotlightBackdrop' => 'Sfondo in evidenza nell\'angolo',
'settings.tvCornerSpotlightBackdropDescription' => 'Mostra l\'immagine in evidenza nell\'angolo in alto a destra anziché a schermo intero',
'settings.viewMode' => 'Modalità di visualizzazione',
@@ -2946,16 +3113,14 @@ extension on TranslationsIt {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Decodifica hardware',
'settings.hardwareDecodingDescription' => 'Utilizza l\'accelerazione hardware quando disponibile',
'settings.bufferSize' => 'Dimensione buffer',
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
'settings.bufferSizeAuto' => 'Automatica (consigliata)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB di memoria disponibile. Un buffer di ${size}MB può influire sulla riproduzione.',
'settings.playbackBuffer' => 'Buffer di riproduzione',
'settings.playbackBufferAuto' => 'Auto (consigliato)',
'settings.playbackBufferLarge' => 'Grande',
'settings.playbackBufferExtraLarge' => 'Extra grande',
'settings.playbackBufferDescription' => 'Bufferizza di più contro connessioni instabili. Limitato anche dalla dimensione del buffer.',
'settings.defaultQualityTitle' => 'Qualità predefinita',
'settings.cellularQualityTitle' => 'Qualità predefinita sulla rete mobile',
'settings.cellularQualitySameAsDefault' => 'Come la qualità predefinita',
'settings.musicQualityTitle' => 'Qualità musicale',
'settings.subtitleStyling' => 'Stile sottotitoli',
'settings.subtitleStylingDescription' => 'Personalizza l\'aspetto dei sottotitoli',
@@ -2969,6 +3134,8 @@ extension on TranslationsIt {
'settings.rememberTrackSelectionsDescription' => 'Ricorda le scelte di audio e sottotitoli per ogni titolo',
'settings.followServerTrackSelections' => 'Usa le tracce selezionate sul server per ogni episodio',
'settings.followServerTrackSelectionsDescription' => 'Al cambio di episodio applica l\'audio e i sottotitoli selezionati sul server invece di mantenere la scelta corrente',
'settings.resumeMusicOnLaunch' => 'Ricorda la sessione musicale',
'settings.resumeMusicOnLaunchDescription' => 'All\'avvio dell\'app riapri l\'ultimo brano in pausa dal punto in cui era rimasto',
'settings.showChapterMarkersOnTimeline' => 'Mostra i marcatori dei capitoli sulla barra di avanzamento',
'settings.showChapterMarkersOnTimelineDescription' => 'Segmenta la barra di avanzamento ai confini dei capitoli',
'settings.specialsOrdering' => 'Speciali nell\'ordine degli episodi',
@@ -3022,7 +3189,11 @@ extension on TranslationsIt {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Scorciatoia già assegnata a ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Scorciatoia aggiornata per ${action}',
'settings.saveFailed' => 'Impossibile salvare le modifiche. Riprova.',
'settings.autoSkip' => 'Salto automatico',
'settings.autoPlayAndSkip' => 'Riproduzione automatica e salti',
'settings.autoPlayNextEpisode' => 'Riproduci automaticamente l\'episodio successivo',
'settings.autoPlayNextEpisodeDescription' => 'Avvia automaticamente l\'episodio successivo quando termina quello in riproduzione',
'settings.playNextCountdown' => 'Conto alla rovescia del successivo',
'settings.playNextCountdownImmediate' => 'Riproduci subito',
'settings.autoSkipIntro' => 'Salta automaticamente la sigla iniziale',
'settings.autoSkipIntroDescription' => 'Salta automaticamente i marcatori della sigla iniziale dopo alcuni secondi',
'settings.autoSkipCredits' => 'Salta automaticamente i titoli di coda',
@@ -3097,10 +3268,8 @@ extension on TranslationsIt {
'settings.dvConversionNativeDescription' => 'Forza il DV7 nativo e impedisce nuovi tentativi di conversione DV',
'settings.dvConversionDv81Description' => 'Forza la conversione RPU diretta al profilo Dolby Vision 8.1',
'settings.dvConversionHevcStripDescription' => 'Rimuove i livelli RPU/EL di Dolby Vision e riproduce il video come semplice HEVC',
'settings.demuxer' => 'Demuxer del contenitore',
'settings.demuxerDescription' => 'Quale demuxer analizza i file riprodotti in diretta. Passa a media3 se un file dà problemi.',
'settings.demuxerFfmpeg' => 'FFmpeg (consigliato)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterlacciamento',
'settings.deinterlaceDescription' => 'Rimuove gli artefatti a pettine dai video interlacciati (solo lettore mpv)',
'settings.requireProfileSelectionOnOpen' => 'Chiedi di scegliere il profilo all\'apertura',
'settings.requireProfileSelectionOnOpenDescription' => 'Mostra la selezione del profilo ogni volta che l\'app viene aperta',
'settings.forceTvMode' => 'Forza modalità TV',
@@ -3118,15 +3287,31 @@ extension on TranslationsIt {
'settings.showExploreTabDescription' => 'Visualizza la scheda Esplora con contenuti di Esplora di Plex e dei tracker collegati',
'settings.liveTvDefaultFavorites' => 'Apri sui canali preferiti',
'settings.liveTvDefaultFavoritesDescription' => 'Mostra solo i canali preferiti quando apri la TV in diretta',
'settings.general' => 'Generali',
'settings.generalDescription' => 'Lingua, avvio e comportamento della finestra',
'settings.languageAndRegion' => 'Lingua e regione',
'settings.startup' => 'Avvio',
'settings.display' => 'Schermo',
'settings.libraryAndCards' => 'Libreria e schede',
'settings.homeScreen' => 'Schermata iniziale',
'settings.navigation' => 'Navigazione',
'settings.window' => 'Finestra',
'settings.content' => 'Contenuti',
'settings.liveTv' => 'TV in diretta',
'settings.player' => 'Lettore',
'settings.subtitlesAndConfig' => 'Sottotitoli e impostazioni',
'settings.videoAndDisplay' => 'Video e schermo',
'settings.audio' => 'Audio',
'settings.quality' => 'Qualità',
'settings.subtitles' => 'Sottotitoli',
'settings.seekAndTiming' => 'Avanzamento e tempi',
'settings.behavior' => 'Comportamento',
'settings.gestures' => 'Gesti',
'settings.gestureBrightnessSwipe' => 'Scorrimento per la luminosità',
'settings.gestureBrightnessSwipeDescription' => 'Scorri verso l\'alto o il basso sul bordo sinistro per regolare la luminosità',
'settings.gestureVolumeSwipe' => 'Scorrimento per il volume',
'settings.gestureVolumeSwipeDescription' => 'Scorri verso l\'alto o il basso sul bordo destro per regolare il volume',
'settings.gesturePinchToZoom' => 'Pizzica per lo zoom',
'settings.gesturePinchToZoomDescription' => 'Pizzica il video per ingrandire o ridurre',
'settings.controls' => 'Controlli',
'settings.rememberPlayerChanges' => 'Ricorda le modifiche del lettore',
'settings.rememberPlayerChangesDescription' => 'Dove viene salvata e riapplicata una modifica effettuata durante la riproduzione',
'settings.scopePlaybackSpeed' => 'Velocità di riproduzione',
@@ -3220,7 +3405,7 @@ extension on TranslationsIt {
'fileInfo.interlaced' => 'Interlacciato',
'fileInfo.anamorphic' => 'Anamorfico',
'fileInfo.referenceFrames' => 'Frame di riferimento',
'fileInfo.dynamicRange' => 'Dynamic Range',
'fileInfo.dynamicRange' => 'Gamma dinamica',
'fileInfo.dolbyVision' => 'Dolby Vision',
'fileInfo.dolbyVisionLevel' => 'Dolby Vision Level',
'fileInfo.dolbyVisionVersion' => 'Dolby Vision Version',
@@ -3264,6 +3449,8 @@ extension on TranslationsIt {
'fileInfo.has64bitOffsets' => 'Offset a 64 bit',
'fileInfo.protocol' => 'Protocollo',
'fileInfo.mediaType' => 'Tipo di supporto',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Tipo di origine',
'fileInfo.optimizedVersion' => 'Versione ottimizzata',
'fileInfo.optimizationTarget' => 'Obiettivo ottimizzazione',
@@ -3288,8 +3475,6 @@ extension on TranslationsIt {
'fileInfo.flagOriginal' => 'Originale',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profilo ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Segna come visto',
'mediaMenu.markAsUnwatched' => 'Segna come non visto',
'mediaMenu.removeFromContinueWatching' => 'Rimuovi da Continua a guardare',
@@ -3624,6 +3809,63 @@ extension on TranslationsIt {
'connections.signInAgain' => 'Accedi di nuovo',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Modifica connessione ${product}',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Aggiungi o rimuovi gli URL per ${serverName}. Plezy userà l\'URL raggiungibile con la latenza più bassa.',
'accountPreferences.sectionTitle' => 'Preferenze dell\'account',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Opzioni di audio, sottotitoli e libreria salvate su ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Opzioni di audio, sottotitoli e libreria salvate su ${count} account',
'accountPreferences.pickAccount' => 'Ogni account conserva le proprie preferenze. Scegli quale modificare.',
'accountPreferences.storedOnAccount' => 'Queste opzioni vengono salvate sull\'account stesso, quindi ogni app che vi accede le usa — inclusa Plezy sugli altri tuoi dispositivi.',
'accountPreferences.noAccounts' => 'Nessun account da configurare',
'accountPreferences.noAccountsHint' => 'Accedi a Plex oppure connetti un server Jellyfin o Emby: le preferenze salvate su quell\'account appariranno qui.',
'accountPreferences.unavailable' => 'Impossibile raggiungere questo account',
'accountPreferences.loadFailed' => 'Impossibile caricare queste preferenze',
'accountPreferences.noPreference' => 'Nessuna preferenza',
'accountPreferences.notSet' => 'Non impostato',
'accountPreferences.groups.audioAndSubtitles' => 'Audio e sottotitoli',
'accountPreferences.groups.libraryDisplay' => 'Libreria',
'accountPreferences.groups.personalMedia' => 'Media personali',
'accountPreferences.preferredAudioLanguage' => 'Lingua audio preferita',
'accountPreferences.autoSelectAudio' => 'Scegli l\'audio in base alla lingua',
'accountPreferences.autoSelectAudioDescription' => 'Se disattivata, viene mantenuta la traccia audio che il file indica come predefinita.',
'accountPreferences.preferredSubtitleLanguage' => 'Lingua dei sottotitoli preferita',
'accountPreferences.subtitleMode' => 'Attiva i sottotitoli',
'accountPreferences.subtitleModes.none' => 'Selezionati manualmente',
'accountPreferences.subtitleModes.noneDescription' => 'Non attivare mai i sottotitoli automaticamente.',
'accountPreferences.subtitleModes.defaultMode' => 'Segui le flag della traccia',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Usa le flag predefinite e forzate salvate su ogni traccia di sottotitoli.',
'accountPreferences.subtitleModes.always' => 'Sempre attivi',
'accountPreferences.subtitleModes.alwaysDescription' => 'Attiva una traccia di sottotitoli nella lingua preferita quando disponibile.',
'accountPreferences.subtitleModes.onlyForced' => 'Solo sottotitoli forzati',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Carica solo le tracce contrassegnate come forzate.',
'accountPreferences.subtitleModes.smart' => 'Mostrati con audio straniero',
'accountPreferences.subtitleModes.smartDescription' => 'Attiva i sottotitoli solo quando l\'audio è in un\'altra lingua.',
'accountPreferences.subtitleAccessibility' => 'Sottotitoli SDH',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Preferisci i sottotitoli non SDH',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Preferisci i sottotitoli SDH',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Solo sottotitoli SDH',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Solo sottotitoli non SDH',
'accountPreferences.forcedSubtitles' => 'Sottotitoli forzati',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Preferisci i sottotitoli non forzati',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Preferisci i sottotitoli forzati',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Solo sottotitoli forzati',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Solo sottotitoli non forzati',
'accountPreferences.displayMissingEpisodes' => 'Mostra gli episodi mancanti',
'accountPreferences.displayMissingEpisodesDescription' => 'Elenca gli episodi noti al server ma senza un file associato.',
'accountPreferences.hidePlayedInLatest' => 'Nascondi gli elementi visti da Aggiunti di recente',
'accountPreferences.hidePlayedInLatestDescription' => 'Esclude dagli Aggiunti di recente del server gli elementi che hai già visto.',
'accountPreferences.displayCollectionsView' => 'Mostra la vista Raccolte',
'accountPreferences.displayCollectionsViewDescription' => 'Mostra la vista Raccolte del server insieme alle tue librerie.',
'accountPreferences.rewatchingInNextUp' => 'Mantieni le serie riviste in Prossimi episodi',
'accountPreferences.rewatchingInNextUpDescription' => 'Quando finisci una serie e la ricominci, Prossimi episodi segue la nuova visione invece di rimuovere la serie.',
'accountPreferences.watchedIndicator' => 'Indicatori di visione',
'accountPreferences.watchedIndicatorOptions.none' => 'Mai',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Film e serie TV',
'accountPreferences.watchedIndicatorOptions.movies' => 'Solo film',
'accountPreferences.watchedIndicatorOptions.shows' => 'Solo serie TV',
'accountPreferences.mediaReviewsVisibility' => 'Valutazioni e recensioni',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Utenti e critici',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Solo utenti',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Solo critici',
'accountPreferences.mediaReviewsOptions.nobody' => 'Nascoste',
'discover.title' => 'Esplora',
'discover.noContentAvailable' => 'Nessun contenuto disponibile',
'discover.addMediaToLibraries' => 'Aggiungi contenuti multimediali alle tue librerie',
@@ -3721,6 +3963,8 @@ extension on TranslationsIt {
'libraries.filterCategories.contentRating' => 'Classificazione per età',
'libraries.filterCategories.tag' => 'Tag',
'libraries.filterCategories.unwatched' => 'Non visti',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Non riprodotti',
'libraries.filterCategories.favorites' => 'Preferiti',
'libraries.sortLabels.title' => 'Titolo',
@@ -3802,8 +4046,6 @@ extension on TranslationsIt {
'explore.rows.watchlist' => 'Lista da guardare',
'explore.rows.recommendedMovies' => 'Film consigliati',
'explore.rows.recommendedShows' => 'Serie TV consigliate',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Film di tendenza',
'explore.rows.trendingShows' => 'Serie TV di tendenza',
'explore.rows.popularMovies' => 'Film popolari',
@@ -3956,6 +4198,11 @@ extension on TranslationsIt {
'liveTv.unknownChannel' => 'Canale sconosciuto',
'liveTv.live' => 'IN DIRETTA',
'liveTv.reloadGuide' => 'Ricarica la guida',
'liveTv.searchGuide' => 'Cerca nella guida',
'liveTv.searchHint' => 'Cerca canali e programmi',
'liveTv.searchNoResults' => ({required Object query}) => 'Nessuna corrispondenza per "${query}"',
'liveTv.channelsSection' => 'Canali',
'liveTv.programsSection' => 'Programmi',
'liveTv.now' => 'Ora',
'liveTv.today' => 'Oggi',
'liveTv.tomorrow' => 'Domani',
@@ -4093,6 +4340,8 @@ extension on TranslationsIt {
'music.repeatAll' => 'Ripeti tutto',
'music.repeatOne' => 'Ripeti il brano',
'music.instantMixNoServer' => 'Nessun server disponibile per un mix istantaneo',
'music.instantMixFailed' => 'Impossibile caricare il mix istantaneo',
'music.instantMixEmpty' => 'Il mix istantaneo non ha prodotto alcun brano',
'music.noAudioUrl' => ({required Object track}) => 'Nessun URL audio disponibile per ${track}',
'music.discography.singlesAndEps' => 'Singoli ed EP',
'music.discography.live' => 'Dal vivo',
@@ -4228,6 +4477,8 @@ extension on TranslationsIt {
'downloads.syncRuleCleanupUnavailable' => 'Non è stato possibile identificare in sicurezza i download associati. Riconnetti il server e riprova, oppure rimuovi la regola senza eliminare i download.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => '${count} nuovi episodi sincronizzati per ${title}',
'downloads.activeSyncRules' => 'Regole di sincronizzazione',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Nessuna regola di sincronizzazione',
'downloads.manageSyncRule' => 'Gestisci sincronizzazione',
'downloads.editEpisodeCount' => 'Numero di episodi',
@@ -4316,8 +4567,6 @@ extension on TranslationsIt {
'companionRemote.pairing.availableDevices' => 'Dispositivi disponibili',
'companionRemote.pairing.manualConnection' => 'Connessione manuale',
'companionRemote.pairing.cryptoInitFailed' => 'Impossibile avviare la connessione sicura. Accedi prima a Plex.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Inserisci l\'indirizzo host',
'companionRemote.pairing.validationHostFormat' => 'Il formato deve essere IP:porta (es. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Tempo scaduto per la connessione. Usa la stessa rete su entrambi i dispositivi.',
@@ -4577,6 +4826,8 @@ extension on TranslationsIt {
'seerr.statusBlocklisted' => 'Nella lista di blocco',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Impossibile raggiungere ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'Nessuna istanza Seerr all\'indirizzo ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Inserisci un indirizzo del server come https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Questa istanza Seerr non supporta Quick Connect. È necessaria la versione 3.4 o successiva di Seerr.',
'seerr.notInitialized' => 'Questa istanza Seerr non ha completato la configurazione iniziale',
'seerr.noPlexTokenForReauth' => 'Nessun token Plex disponibile per effettuare nuovamente l\'accesso',
'seerr.noStoredCredentials' => 'Nessuna credenziale salvata disponibile per effettuare nuovamente l\'accesso',
@@ -4587,6 +4838,7 @@ extension on TranslationsIt {
'seerr.sessionRejectedAfterReauth' => 'La sessione è stata rifiutata dopo aver effettuato nuovamente l\'accesso',
'services.title' => 'Servizi',
'services.hubSubtitle' => 'Sincronizza i progressi di visione e richiedi nuovi titoli.',
'services.integrations' => 'Integrazioni',
'services.notConnected' => 'Non connesso',
'services.connectedAs' => ({required Object username}) => 'Connesso come @${username}',
'services.scrobble' => 'Registra automaticamente i progressi',
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsJa extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$ja dialog = _Translations$dialog$ja._(_root);
@override late final _Translations$profiles$ja profiles = _Translations$profiles$ja._(_root);
@override late final _Translations$connections$ja connections = _Translations$connections$ja._(_root);
@override late final _Translations$accountPreferences$ja accountPreferences = _Translations$accountPreferences$ja._(_root);
@override late final _Translations$discover$ja discover = _Translations$discover$ja._(_root);
@override late final _Translations$errors$ja errors = _Translations$errors$ja._(_root);
@override late final _Translations$libraries$ja libraries = _Translations$libraries$ja._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$ja extends Translations$auth$en {
@override String get useBrowser => 'ブラウザを使用';
@override String get or => 'または';
@override String connectToMediaBrowser({required Object product}) => '${product}に接続';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Quick Connect を使う';
@override String get quickConnectInstructions => 'JellyfinでQuick Connectを開き、このコードを入力してください。';
@override String get quickConnectWaiting => '承認を待っています…';
@@ -165,6 +167,7 @@ class _Translations$common$ja extends Translations$common$en {
@override String get mute => 'ミュート';
@override String get ok => 'OK';
@override String get off => 'オフ';
@override String get options => 'オプション';
@override String seasonNumber({required Object number}) => 'シーズン${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'エピソード${number} - ${title}';
@override String chapterNumber({required Object number}) => 'チャプター${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$ja extends Translations$settings$en {
@override String get displayScale => '表示スケール';
@override String get compact => 'コンパクト';
@override String get comfortable => 'ゆったり';
@override String get gridSpacing => 'グリッド間隔';
@override String get gridSpacingTight => '密';
@override String get gridSpacingNormal => '標準';
@override String get gridSpacingSpacious => 'ゆったり';
@override String get tvCornerSpotlightBackdrop => '画面隅の注目コンテンツ背景';
@override String get tvCornerSpotlightBackdropDescription => '画面全体ではなく、右上隅に注目コンテンツのアートワークを表示します';
@override String get viewMode => '表示モード';
@@ -292,16 +299,14 @@ class _Translations$settings$ja extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'ハードウェアデコード';
@override String get hardwareDecodingDescription => '利用可能な場合にハードウェアアクセラレーションを使用';
@override String get bufferSize => 'バッファサイズ';
@override String bufferSizeMB({required Object size}) => '${size}MB';
@override String get bufferSizeAuto => '自動(推奨)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MBのメモリが利用可能です。${size}MBのバッファは再生に影響する可能性があります。';
@override String get playbackBuffer => '再生バッファー';
@override String get playbackBufferAuto => '自動(推奨)';
@override String get playbackBufferLarge => '大';
@override String get playbackBufferExtraLarge => '特大';
@override String get playbackBufferDescription => '不安定な接続に備えてより多くのバッファーを使用します。バッファーサイズによっても制限されます。';
@override String get defaultQualityTitle => 'デフォルト画質';
@override String get cellularQualityTitle => 'モバイルデータ通信時のデフォルト画質';
@override String get cellularQualitySameAsDefault => 'デフォルト画質と同じ';
@override String get musicQualityTitle => '音楽の音質';
@override String get subtitleStyling => '字幕スタイル';
@override String get subtitleStylingDescription => '字幕の外観をカスタマイズ';
@@ -315,6 +320,8 @@ class _Translations$settings$ja extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'タイトルごとに音声と字幕の選択を記憶します';
@override String get followServerTrackSelections => 'サーバーのエピソードごとのトラック選択を使用';
@override String get followServerTrackSelectionsDescription => 'エピソード切り替え時に、現在の選択を引き継ぐ代わりにサーバーで選択された音声と字幕を適用します';
@override String get resumeMusicOnLaunch => '音楽セッションを記憶';
@override String get resumeMusicOnLaunchDescription => 'アプリ起動時に最後の曲を停止位置で一時停止のまま開きます';
@override String get showChapterMarkersOnTimeline => 'シークバーにチャプターマーカーを表示';
@override String get showChapterMarkersOnTimelineDescription => 'チャプターの境界でシークバーを区切る';
@override String get specialsOrdering => 'エピソード順にスペシャルを表示';
@@ -368,7 +375,11 @@ class _Translations$settings$ja extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'ショートカットは既に${action}に割り当てられています';
@override String shortcutUpdated({required Object action}) => '${action}のショートカットを更新しました';
@override String get saveFailed => '変更を保存できませんでした。もう一度お試しください。';
@override String get autoSkip => '自動スキップ';
@override String get autoPlayAndSkip => '自動再生とスキップ';
@override String get autoPlayNextEpisode => '次のエピソードを自動再生';
@override String get autoPlayNextEpisodeDescription => 'エピソードが終了すると、次のエピソードを自動的に再生します';
@override String get playNextCountdown => '次回再生のカウントダウン';
@override String get playNextCountdownImmediate => 'すぐに再生';
@override String get autoSkipIntro => 'イントロを自動スキップ';
@override String get autoSkipIntroDescription => '数秒後にイントロマーカーを自動的にスキップ';
@override String get autoSkipCredits => 'クレジットを自動スキップ';
@@ -443,10 +454,8 @@ class _Translations$settings$ja extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'ネイティブ DV7 を強制し、DV 変換の再試行を抑制します';
@override String get dvConversionDv81Description => 'Dolby Vision プロファイル 8.1 へのインライン RPU 変換を強制します';
@override String get dvConversionHevcStripDescription => 'Dolby Vision の RPU/EL レイヤーを削除し、通常の HEVC として扱います';
@override String get demuxer => 'コンテナデマルチプレクサ';
@override String get demuxerDescription => 'ダイレクト再生のファイルを解析するデマルチプレクサ。ファイルの再生に問題がある場合は media3 に切り替えてください。';
@override String get demuxerFfmpeg => 'FFmpeg(推奨)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'デインターレース';
@override String get deinterlaceDescription => 'インターレース映像のくし型ノイズを除去します(mpvプレーヤーのみ)';
@override String get requireProfileSelectionOnOpen => 'アプリ起動時にプロフィールを確認';
@override String get requireProfileSelectionOnOpenDescription => 'アプリを開くたびにプロフィール選択を表示';
@override String get forceTvMode => 'TVモードを強制';
@@ -464,15 +473,31 @@ class _Translations$settings$ja extends Translations$settings$en {
@override String get showExploreTabDescription => 'Plex Discoverと接続済みのトラッカーのコンテンツを「探す」タブに表示します';
@override String get liveTvDefaultFavorites => 'お気に入りチャンネルをデフォルトに';
@override String get liveTvDefaultFavoritesDescription => 'ライブTVを開いたときにお気に入りチャンネルのみ表示';
@override String get general => '一般';
@override String get generalDescription => '言語、起動、ウィンドウの動作';
@override String get languageAndRegion => '言語と地域';
@override String get startup => '起動';
@override String get display => 'ディスプレイ';
@override String get libraryAndCards => 'ライブラリとカード';
@override String get homeScreen => 'ホーム画面';
@override String get navigation => 'ナビゲーション';
@override String get window => 'ウィンドウ';
@override String get content => 'コンテンツ';
@override String get liveTv => 'ライブTV';
@override String get player => 'プレーヤー';
@override String get subtitlesAndConfig => '字幕と設定';
@override String get videoAndDisplay => '映像とディスプレイ';
@override String get audio => '音声';
@override String get quality => '画質';
@override String get subtitles => '字幕';
@override String get seekAndTiming => 'シークとタイミング';
@override String get behavior => '動作';
@override String get gestures => 'ジェスチャー';
@override String get gestureBrightnessSwipe => '明るさスワイプ';
@override String get gestureBrightnessSwipeDescription => '左端を上下にスワイプして明るさを調整します';
@override String get gestureVolumeSwipe => '音量スワイプ';
@override String get gestureVolumeSwipeDescription => '右端を上下にスワイプして音量を調整します';
@override String get gesturePinchToZoom => 'ピンチでズーム';
@override String get gesturePinchToZoomDescription => '動画をピンチしてズームイン・アウト';
@override String get controls => 'コントロール';
@override String get rememberPlayerChanges => 'プレーヤーの変更を記憶';
@override String get rememberPlayerChangesDescription => '再生中に行った変更を保存し、再適用する場所';
@override String get scopePlaybackSpeed => '再生速度';
@@ -1082,6 +1107,49 @@ class _Translations$connections$ja extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => '${serverName}のURLを追加または削除します。Plezyは到達可能なURLのうち、レイテンシーが最も低いものを使用します。';
}
// Path: accountPreferences
class _Translations$accountPreferences$ja extends Translations$accountPreferences$en {
_Translations$accountPreferences$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'アカウント設定';
@override String hubSubtitleSingle({required Object account}) => '音声・字幕・ライブラリのオプションは${account}に保存されています';
@override String hubSubtitleMultiple({required Object count}) => '音声・字幕・ライブラリのオプションは${count}個のアカウントに保存されています';
@override String get pickAccount => '各アカウントに独自の設定が保存されています。編集するアカウントを選択してください。';
@override String get storedOnAccount => 'これらのオプションはアカウント自体に保存されるため、サインインしているすべてのアプリ(他のデバイスのPlezyを含む)で使用されます。';
@override String get noAccounts => '設定できるアカウントがありません';
@override String get noAccountsHint => 'Plexにサインインするか、JellyfinまたはEmbyサーバーに接続すると、そのアカウントに保存された設定がここに表示されます。';
@override String get unavailable => 'このアカウントにアクセスできません';
@override String get loadFailed => 'この設定を読み込めませんでした';
@override String get noPreference => '指定なし';
@override String get notSet => '未設定';
@override late final _Translations$accountPreferences$groups$ja groups = _Translations$accountPreferences$groups$ja._(_root);
@override String get preferredAudioLanguage => '優先音声言語';
@override String get autoSelectAudio => '言語で音声を選択';
@override String get autoSelectAudioDescription => 'オフにすると、ファイルでデフォルトとマークされた音声トラックがそのまま使われます。';
@override String get preferredSubtitleLanguage => '優先字幕言語';
@override String get subtitleMode => '字幕の自動表示';
@override late final _Translations$accountPreferences$subtitleModes$ja subtitleModes = _Translations$accountPreferences$subtitleModes$ja._(_root);
@override String get subtitleAccessibility => 'SDH字幕';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$ja subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$ja._(_root);
@override String get forcedSubtitles => '強制字幕';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$ja forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$ja._(_root);
@override String get displayMissingEpisodes => '欠落エピソードを表示';
@override String get displayMissingEpisodesDescription => 'サーバーが把握しているがファイルがないエピソードを一覧表示します。';
@override String get hidePlayedInLatest => '「最新」の視聴済みアイテムを非表示';
@override String get hidePlayedInLatestDescription => 'すでに視聴したアイテムは、サーバーの「最新」に表示されません。';
@override String get displayCollectionsView => 'コレクションビューを表示';
@override String get displayCollectionsViewDescription => 'ライブラリに加えて、サーバーのコレクションビューを表示します。';
@override String get rewatchingInNextUp => '再視聴した番組を「次のエピソード」に残す';
@override String get rewatchingInNextUpDescription => '番組を見終えた後に再び見始めると、「次のエピソード」は番組を外さずに再視聴に追従します。';
@override String get watchedIndicator => '視聴済みマーク';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$ja watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$ja._(_root);
@override String get mediaReviewsVisibility => '評価とレビュー';
@override late final _Translations$accountPreferences$mediaReviewsOptions$ja mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$ja._(_root);
}
// Path: discover
class _Translations$discover$ja extends Translations$discover$en {
_Translations$discover$ja._(TranslationsJa root) : this._root = root, super.internal(root);
@@ -1378,6 +1446,11 @@ class _Translations$liveTv$ja extends Translations$liveTv$en {
@override String get unknownChannel => '不明なチャンネル';
@override String get live => 'ライブ';
@override String get reloadGuide => '番組表を再読み込み';
@override String get searchGuide => '番組表を検索';
@override String get searchHint => 'チャンネル・番組を検索';
@override String searchNoResults({required Object query}) => '「${query}」に一致するものがありません';
@override String get channelsSection => 'チャンネル';
@override String get programsSection => '番組';
@override String get now => '現在';
@override String get today => '今日';
@override String get tomorrow => '明日';
@@ -1537,6 +1610,8 @@ class _Translations$music$ja extends Translations$music$en {
@override String get repeatAll => '全曲リピート';
@override String get repeatOne => '1曲リピート';
@override String get instantMixNoServer => 'インスタントミックスに利用できるサーバーがありません';
@override String get instantMixFailed => 'インスタントミックスを読み込めませんでした';
@override String get instantMixEmpty => 'インスタントミックスにトラックがありませんでした';
@override String noAudioUrl({required Object track}) => '${track}で利用可能な音声URLがありません';
@override late final _Translations$music$discography$ja discography = _Translations$music$discography$ja._(_root);
}
@@ -2039,6 +2114,8 @@ class _Translations$seerr$ja extends Translations$seerr$en {
@override String get statusBlocklisted => 'ブロックリスト登録済み';
@override String couldNotReach({required Object url, required Object error}) => '${url}に接続できませんでした: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => '${url}にSeerrインスタンスがありません(HTTP ${status})';
@override String get invalidUrl => 'https://seerr.example.comのようなサーバーアドレスを入力してください';
@override String get quickConnectUnsupported => 'このSeerrインスタンスはQuick Connectに対応していません。Seerr 3.4以降が必要です。';
@override String get notInitialized => 'このSeerrインスタンスでは初回セットアップが完了していません';
@override String get noPlexTokenForReauth => '再サインインに使用できるPlexトークンがありません';
@override String get noStoredCredentials => '再サインインに使用できる保存済みの認証情報がありません';
@@ -2058,6 +2135,7 @@ class _Translations$services$ja extends Translations$services$en {
// Translations
@override String get title => 'サービス';
@override String get hubSubtitle => '視聴の進捗を同期して、新しいタイトルをリクエストします。';
@override String get integrations => 'インテグレーション';
@override String get notConnected => '未接続';
@override String connectedAs({required Object username}) => '@${username} として接続済み';
@override String get scrobble => '進捗を自動で記録';
@@ -2220,6 +2298,89 @@ class _Translations$videoControls$pipErrors$ja extends Translations$videoControl
@override String unknown({required Object error}) => 'エラーが発生しました: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$ja extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => '音声と字幕';
@override String get libraryDisplay => 'ライブラリ';
@override String get personalMedia => '個人メディア';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$ja extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get none => '手動選択';
@override String get noneDescription => '自動では字幕をオンにしません。';
@override String get defaultMode => 'トラックのフラグに従う';
@override String get defaultModeDescription => '各字幕トラックに保存されたデフォルトと強制のフラグを使用します。';
@override String get always => '常に有効';
@override String get alwaysDescription => '優先言語の字幕トラックがあれば、常にオンにします。';
@override String get onlyForced => '強制字幕のみ';
@override String get onlyForcedDescription => '強制とマークされたトラックのみ読み込みます。';
@override String get smart => '外国語音声時に表示';
@override String get smartDescription => '音声が別の言語の場合にのみ字幕をオンにします。';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$ja extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => '非SDH字幕を優先';
@override String get preferSdh => 'SDH字幕を優先';
@override String get onlySdh => 'SDH字幕のみ';
@override String get onlyNonSdh => '非SDH字幕のみ';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$ja extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get preferNonForced => '非強制字幕を優先';
@override String get preferForced => '強制字幕を優先';
@override String get onlyForced => '強制字幕のみ';
@override String get onlyNonForced => '非強制字幕のみ';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$ja extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get none => 'しない';
@override String get moviesAndShows => '映画とテレビ番組';
@override String get movies => '映画のみ';
@override String get shows => 'テレビ番組のみ';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$ja extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$ja._(TranslationsJa root) : this._root = root, super.internal(root);
final TranslationsJa _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'ユーザーと批評家';
@override String get usersOnly => 'ユーザーのみ';
@override String get criticsOnly => '批評家のみ';
@override String get nobody => '非表示';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$ja extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$ja._(TranslationsJa root) : this._root = root, super.internal(root);
@@ -2776,6 +2937,7 @@ extension on TranslationsJa {
'auth.useBrowser' => 'ブラウザを使用',
'auth.or' => 'または',
'auth.connectToMediaBrowser' => ({required Object product}) => '${product}に接続',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Quick Connect を使う',
'auth.quickConnectInstructions' => 'JellyfinでQuick Connectを開き、このコードを入力してください。',
'auth.quickConnectWaiting' => '承認を待っています…',
@@ -2815,6 +2977,7 @@ extension on TranslationsJa {
'common.mute' => 'ミュート',
'common.ok' => 'OK',
'common.off' => 'オフ',
'common.options' => 'オプション',
'common.seasonNumber' => ({required Object number}) => 'シーズン${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'エピソード${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'チャプター${number}',
@@ -2893,6 +3056,10 @@ extension on TranslationsJa {
'settings.displayScale' => '表示スケール',
'settings.compact' => 'コンパクト',
'settings.comfortable' => 'ゆったり',
'settings.gridSpacing' => 'グリッド間隔',
'settings.gridSpacingTight' => '密',
'settings.gridSpacingNormal' => '標準',
'settings.gridSpacingSpacious' => 'ゆったり',
'settings.tvCornerSpotlightBackdrop' => '画面隅の注目コンテンツ背景',
'settings.tvCornerSpotlightBackdropDescription' => '画面全体ではなく、右上隅に注目コンテンツのアートワークを表示します',
'settings.viewMode' => '表示モード',
@@ -2936,16 +3103,14 @@ extension on TranslationsJa {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'ハードウェアデコード',
'settings.hardwareDecodingDescription' => '利用可能な場合にハードウェアアクセラレーションを使用',
'settings.bufferSize' => 'バッファサイズ',
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
'settings.bufferSizeAuto' => '自動(推奨)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MBのメモリが利用可能です。${size}MBのバッファは再生に影響する可能性があります。',
'settings.playbackBuffer' => '再生バッファー',
'settings.playbackBufferAuto' => '自動(推奨)',
'settings.playbackBufferLarge' => '大',
'settings.playbackBufferExtraLarge' => '特大',
'settings.playbackBufferDescription' => '不安定な接続に備えてより多くのバッファーを使用します。バッファーサイズによっても制限されます。',
'settings.defaultQualityTitle' => 'デフォルト画質',
'settings.cellularQualityTitle' => 'モバイルデータ通信時のデフォルト画質',
'settings.cellularQualitySameAsDefault' => 'デフォルト画質と同じ',
'settings.musicQualityTitle' => '音楽の音質',
'settings.subtitleStyling' => '字幕スタイル',
'settings.subtitleStylingDescription' => '字幕の外観をカスタマイズ',
@@ -2959,6 +3124,8 @@ extension on TranslationsJa {
'settings.rememberTrackSelectionsDescription' => 'タイトルごとに音声と字幕の選択を記憶します',
'settings.followServerTrackSelections' => 'サーバーのエピソードごとのトラック選択を使用',
'settings.followServerTrackSelectionsDescription' => 'エピソード切り替え時に、現在の選択を引き継ぐ代わりにサーバーで選択された音声と字幕を適用します',
'settings.resumeMusicOnLaunch' => '音楽セッションを記憶',
'settings.resumeMusicOnLaunchDescription' => 'アプリ起動時に最後の曲を停止位置で一時停止のまま開きます',
'settings.showChapterMarkersOnTimeline' => 'シークバーにチャプターマーカーを表示',
'settings.showChapterMarkersOnTimelineDescription' => 'チャプターの境界でシークバーを区切る',
'settings.specialsOrdering' => 'エピソード順にスペシャルを表示',
@@ -3012,7 +3179,11 @@ extension on TranslationsJa {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'ショートカットは既に${action}に割り当てられています',
'settings.shortcutUpdated' => ({required Object action}) => '${action}のショートカットを更新しました',
'settings.saveFailed' => '変更を保存できませんでした。もう一度お試しください。',
'settings.autoSkip' => '自動スキップ',
'settings.autoPlayAndSkip' => '自動再生とスキップ',
'settings.autoPlayNextEpisode' => '次のエピソードを自動再生',
'settings.autoPlayNextEpisodeDescription' => 'エピソードが終了すると、次のエピソードを自動的に再生します',
'settings.playNextCountdown' => '次回再生のカウントダウン',
'settings.playNextCountdownImmediate' => 'すぐに再生',
'settings.autoSkipIntro' => 'イントロを自動スキップ',
'settings.autoSkipIntroDescription' => '数秒後にイントロマーカーを自動的にスキップ',
'settings.autoSkipCredits' => 'クレジットを自動スキップ',
@@ -3087,10 +3258,8 @@ extension on TranslationsJa {
'settings.dvConversionNativeDescription' => 'ネイティブ DV7 を強制し、DV 変換の再試行を抑制します',
'settings.dvConversionDv81Description' => 'Dolby Vision プロファイル 8.1 へのインライン RPU 変換を強制します',
'settings.dvConversionHevcStripDescription' => 'Dolby Vision の RPU/EL レイヤーを削除し、通常の HEVC として扱います',
'settings.demuxer' => 'コンテナデマルチプレクサ',
'settings.demuxerDescription' => 'ダイレクト再生のファイルを解析するデマルチプレクサ。ファイルの再生に問題がある場合は media3 に切り替えてください。',
'settings.demuxerFfmpeg' => 'FFmpeg(推奨)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'デインターレース',
'settings.deinterlaceDescription' => 'インターレース映像のくし型ノイズを除去します(mpvプレーヤーのみ)',
'settings.requireProfileSelectionOnOpen' => 'アプリ起動時にプロフィールを確認',
'settings.requireProfileSelectionOnOpenDescription' => 'アプリを開くたびにプロフィール選択を表示',
'settings.forceTvMode' => 'TVモードを強制',
@@ -3108,15 +3277,31 @@ extension on TranslationsJa {
'settings.showExploreTabDescription' => 'Plex Discoverと接続済みのトラッカーのコンテンツを「探す」タブに表示します',
'settings.liveTvDefaultFavorites' => 'お気に入りチャンネルをデフォルトに',
'settings.liveTvDefaultFavoritesDescription' => 'ライブTVを開いたときにお気に入りチャンネルのみ表示',
'settings.general' => '一般',
'settings.generalDescription' => '言語、起動、ウィンドウの動作',
'settings.languageAndRegion' => '言語と地域',
'settings.startup' => '起動',
'settings.display' => 'ディスプレイ',
'settings.libraryAndCards' => 'ライブラリとカード',
'settings.homeScreen' => 'ホーム画面',
'settings.navigation' => 'ナビゲーション',
'settings.window' => 'ウィンドウ',
'settings.content' => 'コンテンツ',
'settings.liveTv' => 'ライブTV',
'settings.player' => 'プレーヤー',
'settings.subtitlesAndConfig' => '字幕と設定',
'settings.videoAndDisplay' => '映像とディスプレイ',
'settings.audio' => '音声',
'settings.quality' => '画質',
'settings.subtitles' => '字幕',
'settings.seekAndTiming' => 'シークとタイミング',
'settings.behavior' => '動作',
'settings.gestures' => 'ジェスチャー',
'settings.gestureBrightnessSwipe' => '明るさスワイプ',
'settings.gestureBrightnessSwipeDescription' => '左端を上下にスワイプして明るさを調整します',
'settings.gestureVolumeSwipe' => '音量スワイプ',
'settings.gestureVolumeSwipeDescription' => '右端を上下にスワイプして音量を調整します',
'settings.gesturePinchToZoom' => 'ピンチでズーム',
'settings.gesturePinchToZoomDescription' => '動画をピンチしてズームイン・アウト',
'settings.controls' => 'コントロール',
'settings.rememberPlayerChanges' => 'プレーヤーの変更を記憶',
'settings.rememberPlayerChangesDescription' => '再生中に行った変更を保存し、再適用する場所',
'settings.scopePlaybackSpeed' => '再生速度',
@@ -3254,6 +3439,8 @@ extension on TranslationsJa {
'fileInfo.has64bitOffsets' => '64ビットオフセット',
'fileInfo.protocol' => 'プロトコル',
'fileInfo.mediaType' => 'メディアタイプ',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'ソース種別',
'fileInfo.optimizedVersion' => '最適化バージョン',
'fileInfo.optimizationTarget' => '最適化ターゲット',
@@ -3278,8 +3465,6 @@ extension on TranslationsJa {
'fileInfo.flagOriginal' => 'オリジナル',
'fileInfo.channelsMono' => 'モノラル',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'プロファイル${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => '視聴済みにする',
'mediaMenu.markAsUnwatched' => '未視聴にする',
'mediaMenu.removeFromContinueWatching' => '視聴中から削除',
@@ -3614,6 +3799,63 @@ extension on TranslationsJa {
'connections.signInAgain' => '再度サインイン',
'connections.editMediaBrowserTitle' => ({required Object product}) => '${product}接続を編集',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => '${serverName}のURLを追加または削除します。Plezyは到達可能なURLのうち、レイテンシーが最も低いものを使用します。',
'accountPreferences.sectionTitle' => 'アカウント設定',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => '音声・字幕・ライブラリのオプションは${account}に保存されています',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => '音声・字幕・ライブラリのオプションは${count}個のアカウントに保存されています',
'accountPreferences.pickAccount' => '各アカウントに独自の設定が保存されています。編集するアカウントを選択してください。',
'accountPreferences.storedOnAccount' => 'これらのオプションはアカウント自体に保存されるため、サインインしているすべてのアプリ(他のデバイスのPlezyを含む)で使用されます。',
'accountPreferences.noAccounts' => '設定できるアカウントがありません',
'accountPreferences.noAccountsHint' => 'Plexにサインインするか、JellyfinまたはEmbyサーバーに接続すると、そのアカウントに保存された設定がここに表示されます。',
'accountPreferences.unavailable' => 'このアカウントにアクセスできません',
'accountPreferences.loadFailed' => 'この設定を読み込めませんでした',
'accountPreferences.noPreference' => '指定なし',
'accountPreferences.notSet' => '未設定',
'accountPreferences.groups.audioAndSubtitles' => '音声と字幕',
'accountPreferences.groups.libraryDisplay' => 'ライブラリ',
'accountPreferences.groups.personalMedia' => '個人メディア',
'accountPreferences.preferredAudioLanguage' => '優先音声言語',
'accountPreferences.autoSelectAudio' => '言語で音声を選択',
'accountPreferences.autoSelectAudioDescription' => 'オフにすると、ファイルでデフォルトとマークされた音声トラックがそのまま使われます。',
'accountPreferences.preferredSubtitleLanguage' => '優先字幕言語',
'accountPreferences.subtitleMode' => '字幕の自動表示',
'accountPreferences.subtitleModes.none' => '手動選択',
'accountPreferences.subtitleModes.noneDescription' => '自動では字幕をオンにしません。',
'accountPreferences.subtitleModes.defaultMode' => 'トラックのフラグに従う',
'accountPreferences.subtitleModes.defaultModeDescription' => '各字幕トラックに保存されたデフォルトと強制のフラグを使用します。',
'accountPreferences.subtitleModes.always' => '常に有効',
'accountPreferences.subtitleModes.alwaysDescription' => '優先言語の字幕トラックがあれば、常にオンにします。',
'accountPreferences.subtitleModes.onlyForced' => '強制字幕のみ',
'accountPreferences.subtitleModes.onlyForcedDescription' => '強制とマークされたトラックのみ読み込みます。',
'accountPreferences.subtitleModes.smart' => '外国語音声時に表示',
'accountPreferences.subtitleModes.smartDescription' => '音声が別の言語の場合にのみ字幕をオンにします。',
'accountPreferences.subtitleAccessibility' => 'SDH字幕',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => '非SDH字幕を優先',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'SDH字幕を優先',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'SDH字幕のみ',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => '非SDH字幕のみ',
'accountPreferences.forcedSubtitles' => '強制字幕',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => '非強制字幕を優先',
'accountPreferences.forcedSubtitleOptions.preferForced' => '強制字幕を優先',
'accountPreferences.forcedSubtitleOptions.onlyForced' => '強制字幕のみ',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => '非強制字幕のみ',
'accountPreferences.displayMissingEpisodes' => '欠落エピソードを表示',
'accountPreferences.displayMissingEpisodesDescription' => 'サーバーが把握しているがファイルがないエピソードを一覧表示します。',
'accountPreferences.hidePlayedInLatest' => '「最新」の視聴済みアイテムを非表示',
'accountPreferences.hidePlayedInLatestDescription' => 'すでに視聴したアイテムは、サーバーの「最新」に表示されません。',
'accountPreferences.displayCollectionsView' => 'コレクションビューを表示',
'accountPreferences.displayCollectionsViewDescription' => 'ライブラリに加えて、サーバーのコレクションビューを表示します。',
'accountPreferences.rewatchingInNextUp' => '再視聴した番組を「次のエピソード」に残す',
'accountPreferences.rewatchingInNextUpDescription' => '番組を見終えた後に再び見始めると、「次のエピソード」は番組を外さずに再視聴に追従します。',
'accountPreferences.watchedIndicator' => '視聴済みマーク',
'accountPreferences.watchedIndicatorOptions.none' => 'しない',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => '映画とテレビ番組',
'accountPreferences.watchedIndicatorOptions.movies' => '映画のみ',
'accountPreferences.watchedIndicatorOptions.shows' => 'テレビ番組のみ',
'accountPreferences.mediaReviewsVisibility' => '評価とレビュー',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'ユーザーと批評家',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'ユーザーのみ',
'accountPreferences.mediaReviewsOptions.criticsOnly' => '批評家のみ',
'accountPreferences.mediaReviewsOptions.nobody' => '非表示',
'discover.title' => '探す',
'discover.noContentAvailable' => 'コンテンツがありません',
'discover.addMediaToLibraries' => 'ライブラリにメディアを追加してください',
@@ -3711,6 +3953,8 @@ extension on TranslationsJa {
'libraries.filterCategories.contentRating' => '視聴年齢区分',
'libraries.filterCategories.tag' => 'タグ',
'libraries.filterCategories.unwatched' => '未視聴',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => '未再生',
'libraries.filterCategories.favorites' => 'お気に入り',
'libraries.sortLabels.title' => 'タイトル',
@@ -3792,8 +4036,6 @@ extension on TranslationsJa {
'explore.rows.watchlist' => 'ウォッチリスト',
'explore.rows.recommendedMovies' => 'おすすめの映画',
'explore.rows.recommendedShows' => 'おすすめのテレビ番組',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'トレンドの映画',
'explore.rows.trendingShows' => 'トレンドのテレビ番組',
'explore.rows.popularMovies' => '人気の映画',
@@ -3946,6 +4188,11 @@ extension on TranslationsJa {
'liveTv.unknownChannel' => '不明なチャンネル',
'liveTv.live' => 'ライブ',
'liveTv.reloadGuide' => '番組表を再読み込み',
'liveTv.searchGuide' => '番組表を検索',
'liveTv.searchHint' => 'チャンネル・番組を検索',
'liveTv.searchNoResults' => ({required Object query}) => '「${query}」に一致するものがありません',
'liveTv.channelsSection' => 'チャンネル',
'liveTv.programsSection' => '番組',
'liveTv.now' => '現在',
'liveTv.today' => '今日',
'liveTv.tomorrow' => '明日',
@@ -4083,6 +4330,8 @@ extension on TranslationsJa {
'music.repeatAll' => '全曲リピート',
'music.repeatOne' => '1曲リピート',
'music.instantMixNoServer' => 'インスタントミックスに利用できるサーバーがありません',
'music.instantMixFailed' => 'インスタントミックスを読み込めませんでした',
'music.instantMixEmpty' => 'インスタントミックスにトラックがありませんでした',
'music.noAudioUrl' => ({required Object track}) => '${track}で利用可能な音声URLがありません',
'music.discography.singlesAndEps' => 'シングル・EP',
'music.discography.live' => 'ライブ',
@@ -4218,6 +4467,8 @@ extension on TranslationsJa {
'downloads.syncRuleCleanupUnavailable' => '関連するダウンロードを安全に特定できませんでした。サーバーに再接続して再試行するか、ダウンロードを削除せずにルールを削除してください。',
'downloads.syncedNewEpisodes' => ({required Object title, required Object count}) => '${title}の新しいエピソードを${count}件同期しました',
'downloads.activeSyncRules' => '同期ルール',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => '同期ルールなし',
'downloads.manageSyncRule' => '同期を管理',
'downloads.editEpisodeCount' => 'エピソード数',
@@ -4306,8 +4557,6 @@ extension on TranslationsJa {
'companionRemote.pairing.availableDevices' => '利用可能なデバイス',
'companionRemote.pairing.manualConnection' => '手動接続',
'companionRemote.pairing.cryptoInitFailed' => '安全な接続を開始できませんでした。先にPlexにサインインしてください。',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'ホストアドレスを入力してください',
'companionRemote.pairing.validationHostFormat' => '形式はIP:ポートである必要があります(例: 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => '接続がタイムアウトしました。両方のデバイスで同じネットワークを使用してください。',
@@ -4567,6 +4816,8 @@ extension on TranslationsJa {
'seerr.statusBlocklisted' => 'ブロックリスト登録済み',
'seerr.couldNotReach' => ({required Object url, required Object error}) => '${url}に接続できませんでした: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => '${url}にSeerrインスタンスがありません(HTTP ${status})',
'seerr.invalidUrl' => 'https://seerr.example.comのようなサーバーアドレスを入力してください',
'seerr.quickConnectUnsupported' => 'このSeerrインスタンスはQuick Connectに対応していません。Seerr 3.4以降が必要です。',
'seerr.notInitialized' => 'このSeerrインスタンスでは初回セットアップが完了していません',
'seerr.noPlexTokenForReauth' => '再サインインに使用できるPlexトークンがありません',
'seerr.noStoredCredentials' => '再サインインに使用できる保存済みの認証情報がありません',
@@ -4577,6 +4828,7 @@ extension on TranslationsJa {
'seerr.sessionRejectedAfterReauth' => '再サインイン後にセッションが拒否されました',
'services.title' => 'サービス',
'services.hubSubtitle' => '視聴の進捗を同期して、新しいタイトルをリクエストします。',
'services.integrations' => 'インテグレーション',
'services.notConnected' => '未接続',
'services.connectedAs' => ({required Object username}) => '@${username} として接続済み',
'services.scrobble' => '進捗を自動で記録',
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsKk extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$kk dialog = _Translations$dialog$kk._(_root);
@override late final _Translations$profiles$kk profiles = _Translations$profiles$kk._(_root);
@override late final _Translations$connections$kk connections = _Translations$connections$kk._(_root);
@override late final _Translations$accountPreferences$kk accountPreferences = _Translations$accountPreferences$kk._(_root);
@override late final _Translations$discover$kk discover = _Translations$discover$kk._(_root);
@override late final _Translations$errors$kk errors = _Translations$errors$kk._(_root);
@override late final _Translations$libraries$kk libraries = _Translations$libraries$kk._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$kk extends Translations$auth$en {
@override String get useBrowser => 'Браузерді пайдалану';
@override String get or => 'немесе';
@override String connectToMediaBrowser({required Object product}) => '${product} қосылу';
@override String get quickConnect => 'Жылдам қосылу';
@override String get useQuickConnect => 'Жылдам қосылуды пайдалану';
@override String get quickConnectInstructions => 'Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.';
@override String get quickConnectWaiting => 'Растау күтілуде…';
@@ -165,6 +167,7 @@ class _Translations$common$kk extends Translations$common$en {
@override String get mute => 'Дыбысты өшіру';
@override String get ok => 'Түсінікті';
@override String get off => 'Өшірулі';
@override String get options => 'Параметрлер';
@override String seasonNumber({required Object number}) => '${number}-маусым';
@override String episodeNumberTitle({required Object number, required Object title}) => '${number}-бөлім - ${title}';
@override String chapterNumber({required Object number}) => '${number}-бөлім';
@@ -249,6 +252,10 @@ class _Translations$settings$kk extends Translations$settings$en {
@override String get displayScale => 'Дисплей масштабы';
@override String get compact => 'Тығыз';
@override String get comfortable => 'Ыңғайлы';
@override String get gridSpacing => 'Тор аралығы';
@override String get gridSpacingTight => 'Тығыз';
@override String get gridSpacingNormal => 'Қалыпты';
@override String get gridSpacingSpacious => 'Кең';
@override String get tvCornerSpotlightBackdrop => 'Бұрыштық жарық фоны';
@override String get tvCornerSpotlightBackdropDescription => 'Фонды толық экран орнына жоғарғы оң жақ бұрышта көрсету';
@override String get viewMode => 'Көрініс режимі';
@@ -292,16 +299,14 @@ class _Translations$settings$kk extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Аппараттық декодтау';
@override String get hardwareDecodingDescription => 'Мүмкіндігінше аппараттық үдетуді пайдалану';
@override String get bufferSize => 'Буфер көлемі';
@override String bufferSizeMB({required Object size}) => '${size} МБ';
@override String get bufferSizeAuto => 'Автоматты (Ұсынылатын)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} МБ ЖҰД қолжетімді. ${size} МБ буфер ойнатуға әсер етуі мүмкін.';
@override String get playbackBuffer => 'Ойнату буфері';
@override String get playbackBufferAuto => 'Авто (ұсынылады)';
@override String get playbackBufferLarge => 'Үлкен';
@override String get playbackBufferExtraLarge => 'Өте үлкен';
@override String get playbackBufferDescription => 'Тұрақсыз қосылыстарға қарсы көбірек буферлейді. Буфер өлшемімен де шектеледі.';
@override String get defaultQualityTitle => 'Әдепкі сапа';
@override String get cellularQualityTitle => 'Мобильді желідегі әдепкі сапа';
@override String get cellularQualitySameAsDefault => 'Әдепкі сапамен бірдей';
@override String get musicQualityTitle => 'Музыка сапасы';
@override String get subtitleStyling => 'Субтитр баптаулары';
@override String get subtitleStylingDescription => 'Субтитрлердің сыртқы келбетін теңшеу';
@@ -315,6 +320,8 @@ class _Translations$settings$kk extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Әр медиа үшін дыбыс пен субтитр таңдауын сақтау';
@override String get followServerTrackSelections => 'Әр бөлім үшін сервердегі жолдар таңдауын қолдану';
@override String get followServerTrackSelectionsDescription => 'Бөлім ауысқанда ағымдағы таңдауды көшірудің орнына серверде таңдалған дыбыс пен субтитрлерді қолдану';
@override String get resumeMusicOnLaunch => 'Музыка сессиясын есте сақтау';
@override String get resumeMusicOnLaunchDescription => 'Қолданба ашылғанда соңғы әнді тоқтаған жерінен кідіртілген күйде ашу';
@override String get showChapterMarkersOnTimeline => 'Уақыт шкаласында бөлім белгілерін көрсету';
@override String get showChapterMarkersOnTimelineDescription => 'Уақыт шкаласын бөлімдерге бөлу';
@override String get specialsOrdering => 'Арнайы бөлімдер эпизодтар ретімен';
@@ -368,7 +375,11 @@ class _Translations$settings$kk extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Бұл тіркес ${action} әрекетіне тағайындалған';
@override String shortcutUpdated({required Object action}) => '${action} үшін пернелер тіркесі жаңартылды';
@override String get saveFailed => 'Өзгерістер сақталмады. Қайтадан байқап көріңіз.';
@override String get autoSkip => 'Автоматты өткізу';
@override String get autoPlayAndSkip => 'Автоойнату және өткізу';
@override String get autoPlayNextEpisode => 'Келесі бөлімді автоматты ойнату';
@override String get autoPlayNextEpisodeDescription => 'Бір бөлім аяқталғанда келесі бөлімді автоматты түрде бастау';
@override String get playNextCountdown => 'Келесіні ойнату санағы';
@override String get playNextCountdownImmediate => 'Дереу ойнату';
@override String get autoSkipIntro => 'Киріс бөлімді (Intro) автоматты өткізу';
@override String get autoSkipIntroDescription => 'Бірнеше секундтан кейін киріс белгілерін автоматты өткізу';
@override String get autoSkipCredits => 'Титрлерді автоматты өткізу';
@@ -443,10 +454,8 @@ class _Translations$settings$kk extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Ішкі DV7 режимін мәжбүрлеу';
@override String get dvConversionDv81Description => 'Dolby Vision profile 8.1 форматына түрлендіру';
@override String get dvConversionHevcStripDescription => 'Dolby Vision қабаттарын алып тастап, HEVC ретінде көрсету';
@override String get demuxer => 'Контейнер демультиплексоры';
@override String get demuxerDescription => 'Тікелей ойнатылатын файлдарды қай демультиплексор талдайды. Файл дұрыс ойнатылмаса, media3-ке ауысыңыз.';
@override String get demuxerFfmpeg => 'FFmpeg (ұсынылады)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Деинтерлейсинг';
@override String get deinterlaceDescription => 'Жоларалық видеодағы тарақ тәрізді артефактілерді жою (тек mpv ойнатқышында)';
@override String get requireProfileSelectionOnOpen => 'Ашқанда профильді сұрау';
@override String get requireProfileSelectionOnOpenDescription => 'Қолданба ашылған сайын профильді таңдауды көрсету';
@override String get forceTvMode => 'TV режимін мәжбүрлеу';
@@ -464,15 +473,31 @@ class _Translations$settings$kk extends Translations$settings$en {
@override String get showExploreTabDescription => 'Plex шолуынан және қосылған трекерлерден алынған контенті бар «Шолу» қойындысын көрсету';
@override String get liveTvDefaultFavorites => 'Тікелей TV-де таңдаулы арналарды әдепкі ету';
@override String get liveTvDefaultFavoritesDescription => 'Тікелей TV ашылғанда тек таңдаулы арналарды көрсету';
@override String get general => 'Жалпы';
@override String get generalDescription => 'Тіл, іске қосу және терезе мінез-құлқы';
@override String get languageAndRegion => 'Тіл және аймақ';
@override String get startup => 'Іске қосу';
@override String get display => 'Дисплей';
@override String get libraryAndCards => 'Кітапхана және карточкалар';
@override String get homeScreen => 'Басты экран';
@override String get navigation => 'Навигация';
@override String get window => 'Терезе';
@override String get content => 'Мазмұн';
@override String get liveTv => 'Тікелей TV';
@override String get player => 'Ойнатқыш';
@override String get subtitlesAndConfig => 'Субтитрлер мен конфигурация';
@override String get videoAndDisplay => 'Видео және дисплей';
@override String get audio => 'Аудио';
@override String get quality => 'Сапа';
@override String get subtitles => 'Субтитрлер';
@override String get seekAndTiming => 'Айналдыру және уақытты реттеу';
@override String get behavior => 'Мінез-құлық';
@override String get gestures => 'Ым-ишаралар';
@override String get gestureBrightnessSwipe => 'Жарықтық сырғытуы';
@override String get gestureBrightnessSwipeDescription => 'Сол жақ шетінде жоғары немесе төмен сырғытып, жарықтықты реттеу';
@override String get gestureVolumeSwipe => 'Дыбыс сырғытуы';
@override String get gestureVolumeSwipeDescription => 'Оң жақ шетінде жоғары немесе төмен сырғытып, дыбыс деңгейін реттеу';
@override String get gesturePinchToZoom => 'Шымшу арқылы масштабтау';
@override String get gesturePinchToZoomDescription => 'Видеода шымшып, жақындату немесе алыстату';
@override String get controls => 'Басқару элементтері';
@override String get rememberPlayerChanges => 'Ойнатқыш өзгерістерін есте сақтау';
@override String get rememberPlayerChangesDescription => 'Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын';
@override String get scopePlaybackSpeed => 'Ойнату жылдамдығы';
@@ -1086,6 +1111,49 @@ class _Translations$connections$kk extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => '${serverName} үшін URL мекенжайларды қосыңыз немесе өшіріңіз. Plezy ең төмен кідірісі бар қолжетімді URL мекенжайды пайдаланады.';
}
// Path: accountPreferences
class _Translations$accountPreferences$kk extends Translations$accountPreferences$en {
_Translations$accountPreferences$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Тіркелгі баптаулары';
@override String hubSubtitleSingle({required Object account}) => '${account} тіркелгісінде сақталған аудио, субтитр және кітапхана баптаулары';
@override String hubSubtitleMultiple({required Object count}) => '${count} тіркелгіде сақталған аудио, субтитр және кітапхана баптаулары';
@override String get pickAccount => 'Әр тіркелгі өз баптауларын сақтайды. Өңдеу үшін біреуін таңдаңыз.';
@override String get storedOnAccount => 'Бұл баптаулар тіркелгінің өзінде сақталады, сондықтан оған кірген әрбір қолданба оларды пайдаланады — басқа құрылғыларыңыздағы Plezy қоса.';
@override String get noAccounts => 'Баптауға тіркелгілер жоқ';
@override String get noAccountsHint => 'Plex-ке кіріңіз немесе Jellyfin не Emby серверін қосыңыз, сонда сол тіркелгіде сақталған баптаулар осында көрсетіледі.';
@override String get unavailable => 'Бұл тіркелгіге қол жеткізу мүмкін емес';
@override String get loadFailed => 'Бұл баптауларды жүктеу мүмкін болмады';
@override String get noPreference => 'Таңдау жоқ';
@override String get notSet => 'Орнатылмаған';
@override late final _Translations$accountPreferences$groups$kk groups = _Translations$accountPreferences$groups$kk._(_root);
@override String get preferredAudioLanguage => 'Қалаулы аудио тілі';
@override String get autoSelectAudio => 'Аудионы тілі бойынша таңдау';
@override String get autoSelectAudioDescription => 'Өшірулі күйде файл әдепкі деп белгілеген аудио трек қолданылады.';
@override String get preferredSubtitleLanguage => 'Қалаулы субтитр тілі';
@override String get subtitleMode => 'Субтитрлерді қосу';
@override late final _Translations$accountPreferences$subtitleModes$kk subtitleModes = _Translations$accountPreferences$subtitleModes$kk._(_root);
@override String get subtitleAccessibility => 'SDH субтитрлері';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$kk subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$kk._(_root);
@override String get forcedSubtitles => 'Мәжбүрлі субтитрлер';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$kk forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$kk._(_root);
@override String get displayMissingEpisodes => 'Жоқ бөлімдерді көрсету';
@override String get displayMissingEpisodesDescription => 'Сервер білетін, бірақ файлы жоқ бөлімдерді тізімдеу.';
@override String get hidePlayedInLatest => '«Latest» айдарында көрілген элементтерді жасыру';
@override String get hidePlayedInLatestDescription => 'Сервердің «Latest» қатарларында сіз көрген элементтерді көрсетпеу.';
@override String get displayCollectionsView => 'Топтамалар көрінісін көрсету';
@override String get displayCollectionsViewDescription => 'Кітапханаларыңызбен қатар сервердің Топтамалар көрінісін ұсыну.';
@override String get rewatchingInNextUp => 'Қайта көрілген сериалдарды «Next Up» айдарында қалдыру';
@override String get rewatchingInNextUpDescription => 'Сериалды аяқтаған соң оны қайта бастасаңыз, «Next Up» сериалды тастамай, қайта көруді жалғастырады.';
@override String get watchedIndicator => 'Көрілген белгілері';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$kk watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$kk._(_root);
@override String get mediaReviewsVisibility => 'Бағалар мен пікірлер';
@override late final _Translations$accountPreferences$mediaReviewsOptions$kk mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$kk._(_root);
}
// Path: discover
class _Translations$discover$kk extends Translations$discover$en {
_Translations$discover$kk._(TranslationsKk root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$kk extends Translations$liveTv$en {
@override String get unknownChannel => 'Белгісіз арна';
@override String get live => 'ТІКЕЛЕЙ ЭФИР';
@override String get reloadGuide => 'Бағдарламаны жаңарту';
@override String get searchGuide => 'Телебағдарламадан іздеу';
@override String get searchHint => 'Арналар мен бағдарламаларды іздеу';
@override String searchNoResults({required Object query}) => '"${query}" бойынша сәйкестік табылмады';
@override String get channelsSection => 'Арналар';
@override String get programsSection => 'Бағдарламалар';
@override String get now => 'Қазір';
@override String get today => 'Бүгін';
@override String get tomorrow => 'Ертең';
@@ -1545,6 +1618,8 @@ class _Translations$music$kk extends Translations$music$en {
@override String get repeatAll => 'Барлығын қайталау';
@override String get repeatOne => 'Біреуін қайталау';
@override String get instantMixNoServer => 'Жедел микс үшін қолжетімді сервер жоқ';
@override String get instantMixFailed => 'Сәттік микс жүктелмеді';
@override String get instantMixEmpty => 'Сәттік микс ешбір тректі қамтымады';
@override String noAudioUrl({required Object track}) => '${track} үшін аудио URL-і қолжетімді емес';
@override late final _Translations$music$discography$kk discography = _Translations$music$discography$kk._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$kk extends Translations$seerr$en {
@override String get statusBlocklisted => 'Бұғаттау тізімінде';
@override String couldNotReach({required Object url, required Object error}) => '${url} мекенжайына қосылу мүмкін болмады: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => '${url} мекенжайында Seerr данасы жоқ (HTTP ${status})';
@override String get invalidUrl => 'Сервер мекенжайын енгізіңіз, мысалы: https://seerr.example.com';
@override String get quickConnectUnsupported => 'Бұл Seerr данасы Жылдам қосылуды қолдамайды. Оған Seerr 3.4 немесе одан жаңарақ нұсқа қажет.';
@override String get notInitialized => 'Бұл Seerr данасының бастапқы баптауы аяқталмаған';
@override String get noPlexTokenForReauth => 'Қайта кіру үшін Plex токені қолжетімді емес';
@override String get noStoredCredentials => 'Қайта кіру үшін сақталған тіркелгі деректері жоқ';
@@ -2066,6 +2143,7 @@ class _Translations$services$kk extends Translations$services$en {
// Translations
@override String get title => 'Қызметтер';
@override String get hubSubtitle => 'Көру барысын синхрондаңыз және жаңа мазмұн сұраңыз.';
@override String get integrations => 'Интеграциялар';
@override String get notConnected => 'Қосылмаған';
@override String connectedAs({required Object username}) => '@${username} ретінде қосылды';
@override String get scrobble => 'Барысты автоматты бақылау';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$kk extends Translations$videoControl
@override String unknown({required Object error}) => 'Қате орын алды: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$kk extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Аудио және субтитрлер';
@override String get libraryDisplay => 'Кітапхана';
@override String get personalMedia => 'Жеке медиа';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$kk extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get none => 'Қолмен таңдалған';
@override String get noneDescription => 'Субтитрлерді өзінен-өзі ешқашан қоспау.';
@override String get defaultMode => 'Трек белгілерін қолдану';
@override String get defaultModeDescription => 'Әр субтитр трегінде сақталған әдепкі және мәжбүрлі белгілерді пайдалану.';
@override String get always => 'Әрдайым қосылған';
@override String get alwaysDescription => 'Қалаулы тілдегі субтитр трегі бар болса, оны қосу.';
@override String get onlyForced => 'Тек мәжбүрлі субтитрлер';
@override String get onlyForcedDescription => 'Тек мәжбүрлі деп белгіленген тректерді жүктеу.';
@override String get smart => 'Шет тіліндегі аудио кезінде көрсету';
@override String get smartDescription => 'Субтитрлерді тек аудио басқа тілде болғанда қосу.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$kk extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'SDH емес субтитрлерді артық көру';
@override String get preferSdh => 'SDH субтитрлерін артық көру';
@override String get onlySdh => 'Тек SDH субтитрлері';
@override String get onlyNonSdh => 'Тек SDH емес субтитрлер';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$kk extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Мәжбүрлі емес субтитрлерді артық көру';
@override String get preferForced => 'Мәжбүрлі субтитрлерді артық көру';
@override String get onlyForced => 'Тек мәжбүрлі субтитрлер';
@override String get onlyNonForced => 'Тек мәжбүрлі емес субтитрлер';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$kk extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get none => 'Ешқашан';
@override String get moviesAndShows => 'Фильмдер және сериалдар';
@override String get movies => 'Тек фильмдер';
@override String get shows => 'Тек сериалдар';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$kk extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$kk._(TranslationsKk root) : this._root = root, super.internal(root);
final TranslationsKk _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Пайдаланушылар және сыншылар';
@override String get usersOnly => 'Тек пайдаланушылар';
@override String get criticsOnly => 'Тек сыншылар';
@override String get nobody => 'Жасырылған';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$kk extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$kk._(TranslationsKk root) : this._root = root, super.internal(root);
@@ -2786,6 +2947,7 @@ extension on TranslationsKk {
'auth.useBrowser' => 'Браузерді пайдалану',
'auth.or' => 'немесе',
'auth.connectToMediaBrowser' => ({required Object product}) => '${product} қосылу',
'auth.quickConnect' => 'Жылдам қосылу',
'auth.useQuickConnect' => 'Жылдам қосылуды пайдалану',
'auth.quickConnectInstructions' => 'Jellyfin-де Жылдам қосылуды ашып, осы кодты енгізіңіз.',
'auth.quickConnectWaiting' => 'Растау күтілуде…',
@@ -2825,6 +2987,7 @@ extension on TranslationsKk {
'common.mute' => 'Дыбысты өшіру',
'common.ok' => 'Түсінікті',
'common.off' => 'Өшірулі',
'common.options' => 'Параметрлер',
'common.seasonNumber' => ({required Object number}) => '${number}-маусым',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => '${number}-бөлім - ${title}',
'common.chapterNumber' => ({required Object number}) => '${number}-бөлім',
@@ -2903,6 +3066,10 @@ extension on TranslationsKk {
'settings.displayScale' => 'Дисплей масштабы',
'settings.compact' => 'Тығыз',
'settings.comfortable' => 'Ыңғайлы',
'settings.gridSpacing' => 'Тор аралығы',
'settings.gridSpacingTight' => 'Тығыз',
'settings.gridSpacingNormal' => 'Қалыпты',
'settings.gridSpacingSpacious' => 'Кең',
'settings.tvCornerSpotlightBackdrop' => 'Бұрыштық жарық фоны',
'settings.tvCornerSpotlightBackdropDescription' => 'Фонды толық экран орнына жоғарғы оң жақ бұрышта көрсету',
'settings.viewMode' => 'Көрініс режимі',
@@ -2946,16 +3113,14 @@ extension on TranslationsKk {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Аппараттық декодтау',
'settings.hardwareDecodingDescription' => 'Мүмкіндігінше аппараттық үдетуді пайдалану',
'settings.bufferSize' => 'Буфер көлемі',
'settings.bufferSizeMB' => ({required Object size}) => '${size} МБ',
'settings.bufferSizeAuto' => 'Автоматты (Ұсынылатын)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} МБ ЖҰД қолжетімді. ${size} МБ буфер ойнатуға әсер етуі мүмкін.',
'settings.playbackBuffer' => 'Ойнату буфері',
'settings.playbackBufferAuto' => 'Авто (ұсынылады)',
'settings.playbackBufferLarge' => 'Үлкен',
'settings.playbackBufferExtraLarge' => 'Өте үлкен',
'settings.playbackBufferDescription' => 'Тұрақсыз қосылыстарға қарсы көбірек буферлейді. Буфер өлшемімен де шектеледі.',
'settings.defaultQualityTitle' => 'Әдепкі сапа',
'settings.cellularQualityTitle' => 'Мобильді желідегі әдепкі сапа',
'settings.cellularQualitySameAsDefault' => 'Әдепкі сапамен бірдей',
'settings.musicQualityTitle' => 'Музыка сапасы',
'settings.subtitleStyling' => 'Субтитр баптаулары',
'settings.subtitleStylingDescription' => 'Субтитрлердің сыртқы келбетін теңшеу',
@@ -2969,6 +3134,8 @@ extension on TranslationsKk {
'settings.rememberTrackSelectionsDescription' => 'Әр медиа үшін дыбыс пен субтитр таңдауын сақтау',
'settings.followServerTrackSelections' => 'Әр бөлім үшін сервердегі жолдар таңдауын қолдану',
'settings.followServerTrackSelectionsDescription' => 'Бөлім ауысқанда ағымдағы таңдауды көшірудің орнына серверде таңдалған дыбыс пен субтитрлерді қолдану',
'settings.resumeMusicOnLaunch' => 'Музыка сессиясын есте сақтау',
'settings.resumeMusicOnLaunchDescription' => 'Қолданба ашылғанда соңғы әнді тоқтаған жерінен кідіртілген күйде ашу',
'settings.showChapterMarkersOnTimeline' => 'Уақыт шкаласында бөлім белгілерін көрсету',
'settings.showChapterMarkersOnTimelineDescription' => 'Уақыт шкаласын бөлімдерге бөлу',
'settings.specialsOrdering' => 'Арнайы бөлімдер эпизодтар ретімен',
@@ -3022,7 +3189,11 @@ extension on TranslationsKk {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Бұл тіркес ${action} әрекетіне тағайындалған',
'settings.shortcutUpdated' => ({required Object action}) => '${action} үшін пернелер тіркесі жаңартылды',
'settings.saveFailed' => 'Өзгерістер сақталмады. Қайтадан байқап көріңіз.',
'settings.autoSkip' => 'Автоматты өткізу',
'settings.autoPlayAndSkip' => 'Автоойнату және өткізу',
'settings.autoPlayNextEpisode' => 'Келесі бөлімді автоматты ойнату',
'settings.autoPlayNextEpisodeDescription' => 'Бір бөлім аяқталғанда келесі бөлімді автоматты түрде бастау',
'settings.playNextCountdown' => 'Келесіні ойнату санағы',
'settings.playNextCountdownImmediate' => 'Дереу ойнату',
'settings.autoSkipIntro' => 'Киріс бөлімді (Intro) автоматты өткізу',
'settings.autoSkipIntroDescription' => 'Бірнеше секундтан кейін киріс белгілерін автоматты өткізу',
'settings.autoSkipCredits' => 'Титрлерді автоматты өткізу',
@@ -3097,10 +3268,8 @@ extension on TranslationsKk {
'settings.dvConversionNativeDescription' => 'Ішкі DV7 режимін мәжбүрлеу',
'settings.dvConversionDv81Description' => 'Dolby Vision profile 8.1 форматына түрлендіру',
'settings.dvConversionHevcStripDescription' => 'Dolby Vision қабаттарын алып тастап, HEVC ретінде көрсету',
'settings.demuxer' => 'Контейнер демультиплексоры',
'settings.demuxerDescription' => 'Тікелей ойнатылатын файлдарды қай демультиплексор талдайды. Файл дұрыс ойнатылмаса, media3-ке ауысыңыз.',
'settings.demuxerFfmpeg' => 'FFmpeg (ұсынылады)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Деинтерлейсинг',
'settings.deinterlaceDescription' => 'Жоларалық видеодағы тарақ тәрізді артефактілерді жою (тек mpv ойнатқышында)',
'settings.requireProfileSelectionOnOpen' => 'Ашқанда профильді сұрау',
'settings.requireProfileSelectionOnOpenDescription' => 'Қолданба ашылған сайын профильді таңдауды көрсету',
'settings.forceTvMode' => 'TV режимін мәжбүрлеу',
@@ -3118,15 +3287,31 @@ extension on TranslationsKk {
'settings.showExploreTabDescription' => 'Plex шолуынан және қосылған трекерлерден алынған контенті бар «Шолу» қойындысын көрсету',
'settings.liveTvDefaultFavorites' => 'Тікелей TV-де таңдаулы арналарды әдепкі ету',
'settings.liveTvDefaultFavoritesDescription' => 'Тікелей TV ашылғанда тек таңдаулы арналарды көрсету',
'settings.general' => 'Жалпы',
'settings.generalDescription' => 'Тіл, іске қосу және терезе мінез-құлқы',
'settings.languageAndRegion' => 'Тіл және аймақ',
'settings.startup' => 'Іске қосу',
'settings.display' => 'Дисплей',
'settings.libraryAndCards' => 'Кітапхана және карточкалар',
'settings.homeScreen' => 'Басты экран',
'settings.navigation' => 'Навигация',
'settings.window' => 'Терезе',
'settings.content' => 'Мазмұн',
'settings.liveTv' => 'Тікелей TV',
'settings.player' => 'Ойнатқыш',
'settings.subtitlesAndConfig' => 'Субтитрлер мен конфигурация',
'settings.videoAndDisplay' => 'Видео және дисплей',
'settings.audio' => 'Аудио',
'settings.quality' => 'Сапа',
'settings.subtitles' => 'Субтитрлер',
'settings.seekAndTiming' => 'Айналдыру және уақытты реттеу',
'settings.behavior' => 'Мінез-құлық',
'settings.gestures' => 'Ым-ишаралар',
'settings.gestureBrightnessSwipe' => 'Жарықтық сырғытуы',
'settings.gestureBrightnessSwipeDescription' => 'Сол жақ шетінде жоғары немесе төмен сырғытып, жарықтықты реттеу',
'settings.gestureVolumeSwipe' => 'Дыбыс сырғытуы',
'settings.gestureVolumeSwipeDescription' => 'Оң жақ шетінде жоғары немесе төмен сырғытып, дыбыс деңгейін реттеу',
'settings.gesturePinchToZoom' => 'Шымшу арқылы масштабтау',
'settings.gesturePinchToZoomDescription' => 'Видеода шымшып, жақындату немесе алыстату',
'settings.controls' => 'Басқару элементтері',
'settings.rememberPlayerChanges' => 'Ойнатқыш өзгерістерін есте сақтау',
'settings.rememberPlayerChangesDescription' => 'Ойнату кезінде жасалған өзгеріс сақталатын және қайта қолданылатын орын',
'settings.scopePlaybackSpeed' => 'Ойнату жылдамдығы',
@@ -3264,6 +3449,8 @@ extension on TranslationsKk {
'fileInfo.has64bitOffsets' => '64-биттік ығысулар',
'fileInfo.protocol' => 'Протокол',
'fileInfo.mediaType' => 'Медиа түрі',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Дереккөз түрі',
'fileInfo.optimizedVersion' => 'Оңтайландырылған нұсқа',
'fileInfo.optimizationTarget' => 'Оңтайландыру мақсаты',
@@ -3288,8 +3475,6 @@ extension on TranslationsKk {
'fileInfo.flagOriginal' => 'Түпнұсқа',
'fileInfo.channelsMono' => 'Моно',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => '${profile} профилі',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Көрілді деп белгілеу',
'mediaMenu.markAsUnwatched' => 'Көрілмеді деп белгілеу',
'mediaMenu.removeFromContinueWatching' => '"Көруді жалғастыру" тізімінен өшіру',
@@ -3624,6 +3809,63 @@ extension on TranslationsKk {
'connections.signInAgain' => 'Қайтадан кіру',
'connections.editMediaBrowserTitle' => ({required Object product}) => '${product} қосылымын өңдеу',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => '${serverName} үшін URL мекенжайларды қосыңыз немесе өшіріңіз. Plezy ең төмен кідірісі бар қолжетімді URL мекенжайды пайдаланады.',
'accountPreferences.sectionTitle' => 'Тіркелгі баптаулары',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => '${account} тіркелгісінде сақталған аудио, субтитр және кітапхана баптаулары',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => '${count} тіркелгіде сақталған аудио, субтитр және кітапхана баптаулары',
'accountPreferences.pickAccount' => 'Әр тіркелгі өз баптауларын сақтайды. Өңдеу үшін біреуін таңдаңыз.',
'accountPreferences.storedOnAccount' => 'Бұл баптаулар тіркелгінің өзінде сақталады, сондықтан оған кірген әрбір қолданба оларды пайдаланады — басқа құрылғыларыңыздағы Plezy қоса.',
'accountPreferences.noAccounts' => 'Баптауға тіркелгілер жоқ',
'accountPreferences.noAccountsHint' => 'Plex-ке кіріңіз немесе Jellyfin не Emby серверін қосыңыз, сонда сол тіркелгіде сақталған баптаулар осында көрсетіледі.',
'accountPreferences.unavailable' => 'Бұл тіркелгіге қол жеткізу мүмкін емес',
'accountPreferences.loadFailed' => 'Бұл баптауларды жүктеу мүмкін болмады',
'accountPreferences.noPreference' => 'Таңдау жоқ',
'accountPreferences.notSet' => 'Орнатылмаған',
'accountPreferences.groups.audioAndSubtitles' => 'Аудио және субтитрлер',
'accountPreferences.groups.libraryDisplay' => 'Кітапхана',
'accountPreferences.groups.personalMedia' => 'Жеке медиа',
'accountPreferences.preferredAudioLanguage' => 'Қалаулы аудио тілі',
'accountPreferences.autoSelectAudio' => 'Аудионы тілі бойынша таңдау',
'accountPreferences.autoSelectAudioDescription' => 'Өшірулі күйде файл әдепкі деп белгілеген аудио трек қолданылады.',
'accountPreferences.preferredSubtitleLanguage' => 'Қалаулы субтитр тілі',
'accountPreferences.subtitleMode' => 'Субтитрлерді қосу',
'accountPreferences.subtitleModes.none' => 'Қолмен таңдалған',
'accountPreferences.subtitleModes.noneDescription' => 'Субтитрлерді өзінен-өзі ешқашан қоспау.',
'accountPreferences.subtitleModes.defaultMode' => 'Трек белгілерін қолдану',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Әр субтитр трегінде сақталған әдепкі және мәжбүрлі белгілерді пайдалану.',
'accountPreferences.subtitleModes.always' => 'Әрдайым қосылған',
'accountPreferences.subtitleModes.alwaysDescription' => 'Қалаулы тілдегі субтитр трегі бар болса, оны қосу.',
'accountPreferences.subtitleModes.onlyForced' => 'Тек мәжбүрлі субтитрлер',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Тек мәжбүрлі деп белгіленген тректерді жүктеу.',
'accountPreferences.subtitleModes.smart' => 'Шет тіліндегі аудио кезінде көрсету',
'accountPreferences.subtitleModes.smartDescription' => 'Субтитрлерді тек аудио басқа тілде болғанда қосу.',
'accountPreferences.subtitleAccessibility' => 'SDH субтитрлері',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'SDH емес субтитрлерді артық көру',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'SDH субтитрлерін артық көру',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Тек SDH субтитрлері',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Тек SDH емес субтитрлер',
'accountPreferences.forcedSubtitles' => 'Мәжбүрлі субтитрлер',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Мәжбүрлі емес субтитрлерді артық көру',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Мәжбүрлі субтитрлерді артық көру',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Тек мәжбүрлі субтитрлер',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Тек мәжбүрлі емес субтитрлер',
'accountPreferences.displayMissingEpisodes' => 'Жоқ бөлімдерді көрсету',
'accountPreferences.displayMissingEpisodesDescription' => 'Сервер білетін, бірақ файлы жоқ бөлімдерді тізімдеу.',
'accountPreferences.hidePlayedInLatest' => '«Latest» айдарында көрілген элементтерді жасыру',
'accountPreferences.hidePlayedInLatestDescription' => 'Сервердің «Latest» қатарларында сіз көрген элементтерді көрсетпеу.',
'accountPreferences.displayCollectionsView' => 'Топтамалар көрінісін көрсету',
'accountPreferences.displayCollectionsViewDescription' => 'Кітапханаларыңызбен қатар сервердің Топтамалар көрінісін ұсыну.',
'accountPreferences.rewatchingInNextUp' => 'Қайта көрілген сериалдарды «Next Up» айдарында қалдыру',
'accountPreferences.rewatchingInNextUpDescription' => 'Сериалды аяқтаған соң оны қайта бастасаңыз, «Next Up» сериалды тастамай, қайта көруді жалғастырады.',
'accountPreferences.watchedIndicator' => 'Көрілген белгілері',
'accountPreferences.watchedIndicatorOptions.none' => 'Ешқашан',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Фильмдер және сериалдар',
'accountPreferences.watchedIndicatorOptions.movies' => 'Тек фильмдер',
'accountPreferences.watchedIndicatorOptions.shows' => 'Тек сериалдар',
'accountPreferences.mediaReviewsVisibility' => 'Бағалар мен пікірлер',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Пайдаланушылар және сыншылар',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Тек пайдаланушылар',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Тек сыншылар',
'accountPreferences.mediaReviewsOptions.nobody' => 'Жасырылған',
'discover.title' => 'Шолу',
'discover.noContentAvailable' => 'Мазмұн қолжетімсіз',
'discover.addMediaToLibraries' => 'Кітапханаларыңызға медиа қосыңыз',
@@ -3721,6 +3963,8 @@ extension on TranslationsKk {
'libraries.filterCategories.contentRating' => 'Мазмұн рейтингі',
'libraries.filterCategories.tag' => 'Тег',
'libraries.filterCategories.unwatched' => 'Көрілмеген',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Ойнатылмаған',
'libraries.filterCategories.favorites' => 'Таңдаулылар',
'libraries.sortLabels.title' => 'Атауы',
@@ -3802,8 +4046,6 @@ extension on TranslationsKk {
'explore.rows.watchlist' => 'Көру тізімі',
'explore.rows.recommendedMovies' => 'Ұсынылатын фильмдер',
'explore.rows.recommendedShows' => 'Ұсынылатын сериалдар',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Трендтегі фильмдер',
'explore.rows.trendingShows' => 'Трендтегі сериалдар',
'explore.rows.popularMovies' => 'Танымал фильмдер',
@@ -3956,6 +4198,11 @@ extension on TranslationsKk {
'liveTv.unknownChannel' => 'Белгісіз арна',
'liveTv.live' => 'ТІКЕЛЕЙ ЭФИР',
'liveTv.reloadGuide' => 'Бағдарламаны жаңарту',
'liveTv.searchGuide' => 'Телебағдарламадан іздеу',
'liveTv.searchHint' => 'Арналар мен бағдарламаларды іздеу',
'liveTv.searchNoResults' => ({required Object query}) => '"${query}" бойынша сәйкестік табылмады',
'liveTv.channelsSection' => 'Арналар',
'liveTv.programsSection' => 'Бағдарламалар',
'liveTv.now' => 'Қазір',
'liveTv.today' => 'Бүгін',
'liveTv.tomorrow' => 'Ертең',
@@ -4093,6 +4340,8 @@ extension on TranslationsKk {
'music.repeatAll' => 'Барлығын қайталау',
'music.repeatOne' => 'Біреуін қайталау',
'music.instantMixNoServer' => 'Жедел микс үшін қолжетімді сервер жоқ',
'music.instantMixFailed' => 'Сәттік микс жүктелмеді',
'music.instantMixEmpty' => 'Сәттік микс ешбір тректі қамтымады',
'music.noAudioUrl' => ({required Object track}) => '${track} үшін аудио URL-і қолжетімді емес',
'music.discography.singlesAndEps' => 'Синглдер және EP',
'music.discography.live' => 'Жанды',
@@ -4228,6 +4477,8 @@ extension on TranslationsKk {
'downloads.syncRuleCleanupUnavailable' => 'Байланысты жүктеулерді қауіпсіз анықтау мүмкін болмады. Серверге қайта қосылып көріңіз немесе ережені жүктеулерді жоймай өшіріңіз.',
'downloads.syncedNewEpisodes' => ({required Object title, required Object count}) => '${title} үшін ${count} жаңа бөлім синхрондалды',
'downloads.activeSyncRules' => 'Белсенді синхрондау ережелері',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Синхрондау ережелері жоқ',
'downloads.manageSyncRule' => 'Синхрондауды басқару',
'downloads.editEpisodeCount' => 'Бөлімдер саны',
@@ -4316,8 +4567,6 @@ extension on TranslationsKk {
'companionRemote.pairing.availableDevices' => 'Қолжетімді құрылғылар',
'companionRemote.pairing.manualConnection' => 'Қолмен қосылу',
'companionRemote.pairing.cryptoInitFailed' => 'Қауіпсіз қосылым қатесі. Алдымен Plex-ке кіріңіз.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Хост мекенжайын енгізіңіз',
'companionRemote.pairing.validationHostFormat' => 'Формат IP:порт түрінде болуы керек',
'companionRemote.pairing.connectionTimedOut' => 'Қосылу уақыты өтті.',
@@ -4577,6 +4826,8 @@ extension on TranslationsKk {
'seerr.statusBlocklisted' => 'Бұғаттау тізімінде',
'seerr.couldNotReach' => ({required Object url, required Object error}) => '${url} мекенжайына қосылу мүмкін болмады: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => '${url} мекенжайында Seerr данасы жоқ (HTTP ${status})',
'seerr.invalidUrl' => 'Сервер мекенжайын енгізіңіз, мысалы: https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Бұл Seerr данасы Жылдам қосылуды қолдамайды. Оған Seerr 3.4 немесе одан жаңарақ нұсқа қажет.',
'seerr.notInitialized' => 'Бұл Seerr данасының бастапқы баптауы аяқталмаған',
'seerr.noPlexTokenForReauth' => 'Қайта кіру үшін Plex токені қолжетімді емес',
'seerr.noStoredCredentials' => 'Қайта кіру үшін сақталған тіркелгі деректері жоқ',
@@ -4587,6 +4838,7 @@ extension on TranslationsKk {
'seerr.sessionRejectedAfterReauth' => 'Қайта кіргеннен кейін сеанс қабылданбады',
'services.title' => 'Қызметтер',
'services.hubSubtitle' => 'Көру барысын синхрондаңыз және жаңа мазмұн сұраңыз.',
'services.integrations' => 'Интеграциялар',
'services.notConnected' => 'Қосылмаған',
'services.connectedAs' => ({required Object username}) => '@${username} ретінде қосылды',
'services.scrobble' => 'Барысты автоматты бақылау',
+280 -28
View File
@@ -60,6 +60,7 @@ class TranslationsKo extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$ko dialog = _Translations$dialog$ko._(_root);
@override late final _Translations$profiles$ko profiles = _Translations$profiles$ko._(_root);
@override late final _Translations$connections$ko connections = _Translations$connections$ko._(_root);
@override late final _Translations$accountPreferences$ko accountPreferences = _Translations$accountPreferences$ko._(_root);
@override late final _Translations$discover$ko discover = _Translations$discover$ko._(_root);
@override late final _Translations$errors$ko errors = _Translations$errors$ko._(_root);
@override late final _Translations$libraries$ko libraries = _Translations$libraries$ko._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$ko extends Translations$auth$en {
@override String get useBrowser => '브라우저 사용';
@override String get or => '또는';
@override String connectToMediaBrowser({required Object product}) => '${product}에 연결';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Quick Connect 사용';
@override String get quickConnectInstructions => 'Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.';
@override String get quickConnectWaiting => '승인 대기 중…';
@@ -165,6 +167,7 @@ class _Translations$common$ko extends Translations$common$en {
@override String get mute => '음소거';
@override String get ok => '확인';
@override String get off => '꺼짐';
@override String get options => '옵션';
@override String seasonNumber({required Object number}) => '시즌 ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => '${number}화 - ${title}';
@override String chapterNumber({required Object number}) => '챕터 ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$ko extends Translations$settings$en {
@override String get displayScale => '표시 배율';
@override String get compact => '조밀하게';
@override String get comfortable => '여유롭게';
@override String get gridSpacing => '그리드 간격';
@override String get gridSpacingTight => '조밀하게';
@override String get gridSpacingNormal => '보통';
@override String get gridSpacingSpacious => '여유롭게';
@override String get tvCornerSpotlightBackdrop => '모서리 스포트라이트 배경';
@override String get tvCornerSpotlightBackdropDescription => '화면 전체를 채우는 대신 오른쪽 상단에 스포트라이트 아트워크를 표시합니다';
@override String get viewMode => '보기 모드';
@@ -292,16 +299,14 @@ class _Translations$settings$ko extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => '하드웨어 디코딩';
@override String get hardwareDecodingDescription => '가능한 경우 하드웨어 가속을 사용합니다';
@override String get bufferSize => '버퍼 크기';
@override String bufferSizeMB({required Object size}) => '${size}MB';
@override String get bufferSizeAuto => '자동 (권장)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap}MB 메모리를 사용할 수 있습니다. ${size}MB 버퍼는 재생에 영향을 줄 수 있습니다.';
@override String get playbackBuffer => '재생 버퍼';
@override String get playbackBufferAuto => '자동 (권장)';
@override String get playbackBufferLarge => '대형';
@override String get playbackBufferExtraLarge => '초대형';
@override String get playbackBufferDescription => '불안정한 연결에 대비해 더 많은 버퍼를 사용합니다. 버퍼 크기에 의해서도 제한됩니다.';
@override String get defaultQualityTitle => '기본 화질';
@override String get cellularQualityTitle => '셀룰러에서 기본 화질';
@override String get cellularQualitySameAsDefault => '기본 화질과 동일';
@override String get musicQualityTitle => '음악 음질';
@override String get subtitleStyling => '자막 스타일';
@override String get subtitleStylingDescription => '자막 모양을 사용자 지정합니다';
@@ -315,6 +320,8 @@ class _Translations$settings$ko extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => '작품별 오디오 및 자막 선택 기억';
@override String get followServerTrackSelections => '서버의 에피소드별 트랙 선택 사용';
@override String get followServerTrackSelectionsDescription => '에피소드 전환 시 현재 선택을 유지하는 대신 서버에서 선택된 오디오와 자막을 적용합니다';
@override String get resumeMusicOnLaunch => '음악 세션 기억';
@override String get resumeMusicOnLaunchDescription => '앱 시작 시 마지막 곡을 멈춘 위치에서 일시정지 상태로 엽니다';
@override String get showChapterMarkersOnTimeline => '탐색 막대에 챕터 마커 표시';
@override String get showChapterMarkersOnTimelineDescription => '챕터 경계에서 탐색 막대 구분';
@override String get specialsOrdering => '에피소드 순서로 스페셜 표시';
@@ -368,7 +375,11 @@ class _Translations$settings$ko extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => '단축키가 이미 ${action}에 할당되었습니다';
@override String shortcutUpdated({required Object action}) => '${action} 단축키가 업데이트되었습니다';
@override String get saveFailed => '변경 사항을 저장하지 못했습니다. 다시 시도하세요.';
@override String get autoSkip => '자동 건너뛰기';
@override String get autoPlayAndSkip => '자동 재생 및 건너뛰기';
@override String get autoPlayNextEpisode => '다음 에피소드 자동 재생';
@override String get autoPlayNextEpisodeDescription => '에피소드가 끝나면 다음 에피소드를 자동으로 재생';
@override String get playNextCountdown => '다음 재생 카운트다운';
@override String get playNextCountdownImmediate => '즉시 재생';
@override String get autoSkipIntro => '자동으로 오프닝 건너뛰기';
@override String get autoSkipIntroDescription => '몇 초 후 오프닝을 자동으로 건너뛰기';
@override String get autoSkipCredits => '자동으로 엔딩 건너뛰기';
@@ -443,10 +454,8 @@ class _Translations$settings$ko extends Translations$settings$en {
@override String get dvConversionNativeDescription => '네이티브 DV7을 강제하고 DV 변환 재시도를 억제합니다';
@override String get dvConversionDv81Description => 'Dolby Vision 프로필 8.1로 인라인 RPU 변환을 강제합니다';
@override String get dvConversionHevcStripDescription => 'Dolby Vision RPU/EL 레이어를 제거하고 일반 HEVC로 제공합니다';
@override String get demuxer => '컨테이너 디먹서';
@override String get demuxerDescription => '다이렉트 재생 파일을 분석할 디먹서입니다. 파일이 제대로 재생되지 않으면 media3으로 전환하세요.';
@override String get demuxerFfmpeg => 'FFmpeg (권장)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => '디인터레이싱';
@override String get deinterlaceDescription => '인터레이스 비디오의 빗살 무늬 아티팩트를 제거합니다 (mpv 플레이어만 해당)';
@override String get requireProfileSelectionOnOpen => '앱 실행 시 프로필 선택';
@override String get requireProfileSelectionOnOpenDescription => '앱을 열 때마다 프로필 선택 화면을 표시합니다';
@override String get forceTvMode => 'TV 모드 강제 사용';
@@ -464,15 +473,31 @@ class _Translations$settings$ko extends Translations$settings$en {
@override String get showExploreTabDescription => 'Plex Discover와 연결된 트래커의 콘텐츠가 포함된 둘러보기 탭을 표시합니다';
@override String get liveTvDefaultFavorites => '즐겨찾기 채널 기본 설정';
@override String get liveTvDefaultFavoritesDescription => '라이브 TV를 열 때 즐겨찾기 채널만 표시';
@override String get general => '일반';
@override String get generalDescription => '언어, 시작 및 창 동작';
@override String get languageAndRegion => '언어 및 지역';
@override String get startup => '시작';
@override String get display => '디스플레이';
@override String get libraryAndCards => '라이브러리 및 카드';
@override String get homeScreen => '홈 화면';
@override String get navigation => '탐색';
@override String get window => '창';
@override String get content => '콘텐츠';
@override String get liveTv => '라이브 TV';
@override String get player => '플레이어';
@override String get subtitlesAndConfig => '자막 및 구성';
@override String get videoAndDisplay => '비디오 및 디스플레이';
@override String get audio => '오디오';
@override String get quality => '화질';
@override String get subtitles => '자막';
@override String get seekAndTiming => '탐색 및 타이밍';
@override String get behavior => '동작';
@override String get gestures => '제스처';
@override String get gestureBrightnessSwipe => '밝기 스와이프';
@override String get gestureBrightnessSwipeDescription => '왼쪽 가장자리에서 위아래로 스와이프하여 밝기를 조절합니다';
@override String get gestureVolumeSwipe => '볼륨 스와이프';
@override String get gestureVolumeSwipeDescription => '오른쪽 가장자리에서 위아래로 스와이프하여 볼륨을 조절합니다';
@override String get gesturePinchToZoom => '핀치 줌';
@override String get gesturePinchToZoomDescription => '비디오에서 핀치하여 확대하거나 축소합니다';
@override String get controls => '컨트롤';
@override String get rememberPlayerChanges => '플레이어 변경 사항 기억';
@override String get rememberPlayerChangesDescription => '재생 중 변경한 사항을 저장하고 다시 적용할 위치';
@override String get scopePlaybackSpeed => '재생 속도';
@@ -1082,6 +1107,49 @@ class _Translations$connections$ko extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => '${serverName}의 URL을 추가하거나 제거하세요. Plezy는 연결 가능한 URL 중 지연 시간이 가장 낮은 URL을 사용합니다.';
}
// Path: accountPreferences
class _Translations$accountPreferences$ko extends Translations$accountPreferences$en {
_Translations$accountPreferences$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get sectionTitle => '계정 기본 설정';
@override String hubSubtitleSingle({required Object account}) => '${account}에 저장된 오디오, 자막 및 라이브러리 옵션';
@override String hubSubtitleMultiple({required Object count}) => '${count}개 계정에 저장된 오디오, 자막 및 라이브러리 옵션';
@override String get pickAccount => '각 계정은 고유한 기본 설정을 저장합니다. 편집할 계정을 선택하세요.';
@override String get storedOnAccount => '이 옵션은 계정 자체에 저장되므로 해당 계정으로 로그인한 모든 앱에서 사용됩니다 — 다른 기기의 Plezy를 포함합니다.';
@override String get noAccounts => '구성할 계정이 없습니다';
@override String get noAccountsHint => 'Plex에 로그인하거나 Jellyfin 또는 Emby 서버에 연결하면 해당 계정에 저장된 기본 설정이 여기에 표시됩니다.';
@override String get unavailable => '이 계정에 연결할 수 없습니다';
@override String get loadFailed => '이 기본 설정을 불러올 수 없습니다';
@override String get noPreference => '기본 설정 없음';
@override String get notSet => '설정되지 않음';
@override late final _Translations$accountPreferences$groups$ko groups = _Translations$accountPreferences$groups$ko._(_root);
@override String get preferredAudioLanguage => '선호 오디오 언어';
@override String get autoSelectAudio => '언어별로 오디오 선택';
@override String get autoSelectAudioDescription => '끄면 파일이 기본으로 표시한 오디오 트랙을 유지합니다.';
@override String get preferredSubtitleLanguage => '선호 자막 언어';
@override String get subtitleMode => '자막 자동 켜기';
@override late final _Translations$accountPreferences$subtitleModes$ko subtitleModes = _Translations$accountPreferences$subtitleModes$ko._(_root);
@override String get subtitleAccessibility => 'SDH 자막';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$ko subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$ko._(_root);
@override String get forcedSubtitles => '강제 자막';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$ko forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$ko._(_root);
@override String get displayMissingEpisodes => '누락된 에피소드 표시';
@override String get displayMissingEpisodesDescription => '서버가 알고 있지만 파일이 없는 에피소드를 나열합니다.';
@override String get hidePlayedInLatest => '최신 항목에서 시청한 항목 숨기기';
@override String get hidePlayedInLatestDescription => '이미 시청한 항목은 서버의 최신 행에서 제외합니다.';
@override String get displayCollectionsView => '컬렉션 보기 표시';
@override String get displayCollectionsViewDescription => '라이브러리와 함께 서버의 컬렉션 보기를 제공합니다.';
@override String get rewatchingInNextUp => '재시청한 프로그램을 다음 에피소드에 유지';
@override String get rewatchingInNextUpDescription => '프로그램을 끝까지 본 뒤 다시 시작하면, 목록에서 제거하는 대신 다음 에피소드가 재시청을 따라갑니다.';
@override String get watchedIndicator => '시청 표시';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$ko watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$ko._(_root);
@override String get mediaReviewsVisibility => '평점 및 리뷰';
@override late final _Translations$accountPreferences$mediaReviewsOptions$ko mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$ko._(_root);
}
// Path: discover
class _Translations$discover$ko extends Translations$discover$en {
_Translations$discover$ko._(TranslationsKo root) : this._root = root, super.internal(root);
@@ -1378,6 +1446,11 @@ class _Translations$liveTv$ko extends Translations$liveTv$en {
@override String get unknownChannel => '알 수 없는 채널';
@override String get live => '실시간';
@override String get reloadGuide => '편성표 새로고침';
@override String get searchGuide => '편성표 검색';
@override String get searchHint => '채널 및 프로그램 검색';
@override String searchNoResults({required Object query}) => '「${query}」와(과) 일치하는 항목이 없습니다';
@override String get channelsSection => '채널';
@override String get programsSection => '프로그램';
@override String get now => '지금';
@override String get today => '오늘';
@override String get tomorrow => '내일';
@@ -1537,6 +1610,8 @@ class _Translations$music$ko extends Translations$music$en {
@override String get repeatAll => '전체 반복';
@override String get repeatOne => '한 곡 반복';
@override String get instantMixNoServer => '즉석 믹스에 사용할 수 있는 서버가 없습니다';
@override String get instantMixFailed => '인스턴트 믹스를 불러오지 못했습니다';
@override String get instantMixEmpty => '인스턴트 믹스에 트랙이 없습니다';
@override String noAudioUrl({required Object track}) => '${track}에 사용할 수 있는 오디오 URL이 없습니다';
@override late final _Translations$music$discography$ko discography = _Translations$music$discography$ko._(_root);
}
@@ -2039,6 +2114,8 @@ class _Translations$seerr$ko extends Translations$seerr$en {
@override String get statusBlocklisted => '차단 목록에 있음';
@override String couldNotReach({required Object url, required Object error}) => '${url}에 연결할 수 없습니다: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => '${url}에 Seerr 인스턴스가 없습니다(HTTP ${status})';
@override String get invalidUrl => 'https://seerr.example.com과 같은 서버 주소를 입력하세요';
@override String get quickConnectUnsupported => '이 Seerr 인스턴스는 Quick Connect를 지원하지 않습니다. Seerr 3.4 이상이 필요합니다.';
@override String get notInitialized => '이 Seerr 인스턴스는 최초 실행 설정을 완료하지 않았습니다';
@override String get noPlexTokenForReauth => '다시 로그인하는 데 사용할 수 있는 Plex 토큰이 없습니다';
@override String get noStoredCredentials => '다시 로그인하는 데 사용할 수 있는 저장된 인증 정보가 없습니다';
@@ -2058,6 +2135,7 @@ class _Translations$services$ko extends Translations$services$en {
// Translations
@override String get title => '서비스';
@override String get hubSubtitle => '시청 진행률을 동기화하고 새 작품을 요청하세요.';
@override String get integrations => '연동';
@override String get notConnected => '연결되지 않음';
@override String connectedAs({required Object username}) => '@${username}(으)로 연결됨';
@override String get scrobble => '진행률 자동 추적';
@@ -2220,6 +2298,89 @@ class _Translations$videoControls$pipErrors$ko extends Translations$videoControl
@override String unknown({required Object error}) => '오류가 발생했습니다: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$ko extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => '오디오 및 자막';
@override String get libraryDisplay => '라이브러리';
@override String get personalMedia => '개인 미디어';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$ko extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get none => '수동 선택';
@override String get noneDescription => '자동으로 자막을 켜지 않습니다.';
@override String get defaultMode => '트랙 플래그 따르기';
@override String get defaultModeDescription => '각 자막 트랙에 저장된 기본 및 강제 플래그를 사용합니다.';
@override String get always => '항상 활성화';
@override String get alwaysDescription => '선호 언어의 자막 트랙이 있으면 항상 켭니다.';
@override String get onlyForced => '강제 자막만';
@override String get onlyForcedDescription => '강제로 표시된 트랙만 로드합니다.';
@override String get smart => '외국어 오디오 시 표시';
@override String get smartDescription => '오디오가 다른 언어일 때만 자막을 켭니다.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$ko extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'SDH가 아닌 자막 선호';
@override String get preferSdh => 'SDH 자막 선호';
@override String get onlySdh => 'SDH 자막만';
@override String get onlyNonSdh => 'SDH가 아닌 자막만';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$ko extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get preferNonForced => '강제가 아닌 자막 선호';
@override String get preferForced => '강제 자막 선호';
@override String get onlyForced => '강제 자막만';
@override String get onlyNonForced => '강제가 아닌 자막만';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$ko extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get none => '안 함';
@override String get moviesAndShows => '영화 및 TV 프로그램';
@override String get movies => '영화만';
@override String get shows => 'TV 프로그램만';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$ko extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$ko._(TranslationsKo root) : this._root = root, super.internal(root);
final TranslationsKo _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => '사용자 및 평론가';
@override String get usersOnly => '사용자만';
@override String get criticsOnly => '평론가만';
@override String get nobody => '숨김';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$ko extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$ko._(TranslationsKo root) : this._root = root, super.internal(root);
@@ -2776,6 +2937,7 @@ extension on TranslationsKo {
'auth.useBrowser' => '브라우저 사용',
'auth.or' => '또는',
'auth.connectToMediaBrowser' => ({required Object product}) => '${product}에 연결',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Quick Connect 사용',
'auth.quickConnectInstructions' => 'Jellyfin에서 Quick Connect를 열고 이 코드를 입력하세요.',
'auth.quickConnectWaiting' => '승인 대기 중…',
@@ -2815,6 +2977,7 @@ extension on TranslationsKo {
'common.mute' => '음소거',
'common.ok' => '확인',
'common.off' => '꺼짐',
'common.options' => '옵션',
'common.seasonNumber' => ({required Object number}) => '시즌 ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => '${number}화 - ${title}',
'common.chapterNumber' => ({required Object number}) => '챕터 ${number}',
@@ -2893,6 +3056,10 @@ extension on TranslationsKo {
'settings.displayScale' => '표시 배율',
'settings.compact' => '조밀하게',
'settings.comfortable' => '여유롭게',
'settings.gridSpacing' => '그리드 간격',
'settings.gridSpacingTight' => '조밀하게',
'settings.gridSpacingNormal' => '보통',
'settings.gridSpacingSpacious' => '여유롭게',
'settings.tvCornerSpotlightBackdrop' => '모서리 스포트라이트 배경',
'settings.tvCornerSpotlightBackdropDescription' => '화면 전체를 채우는 대신 오른쪽 상단에 스포트라이트 아트워크를 표시합니다',
'settings.viewMode' => '보기 모드',
@@ -2936,16 +3103,14 @@ extension on TranslationsKo {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => '하드웨어 디코딩',
'settings.hardwareDecodingDescription' => '가능한 경우 하드웨어 가속을 사용합니다',
'settings.bufferSize' => '버퍼 크기',
'settings.bufferSizeMB' => ({required Object size}) => '${size}MB',
'settings.bufferSizeAuto' => '자동 (권장)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap}MB 메모리를 사용할 수 있습니다. ${size}MB 버퍼는 재생에 영향을 줄 수 있습니다.',
'settings.playbackBuffer' => '재생 버퍼',
'settings.playbackBufferAuto' => '자동 (권장)',
'settings.playbackBufferLarge' => '대형',
'settings.playbackBufferExtraLarge' => '초대형',
'settings.playbackBufferDescription' => '불안정한 연결에 대비해 더 많은 버퍼를 사용합니다. 버퍼 크기에 의해서도 제한됩니다.',
'settings.defaultQualityTitle' => '기본 화질',
'settings.cellularQualityTitle' => '셀룰러에서 기본 화질',
'settings.cellularQualitySameAsDefault' => '기본 화질과 동일',
'settings.musicQualityTitle' => '음악 음질',
'settings.subtitleStyling' => '자막 스타일',
'settings.subtitleStylingDescription' => '자막 모양을 사용자 지정합니다',
@@ -2959,6 +3124,8 @@ extension on TranslationsKo {
'settings.rememberTrackSelectionsDescription' => '작품별 오디오 및 자막 선택 기억',
'settings.followServerTrackSelections' => '서버의 에피소드별 트랙 선택 사용',
'settings.followServerTrackSelectionsDescription' => '에피소드 전환 시 현재 선택을 유지하는 대신 서버에서 선택된 오디오와 자막을 적용합니다',
'settings.resumeMusicOnLaunch' => '음악 세션 기억',
'settings.resumeMusicOnLaunchDescription' => '앱 시작 시 마지막 곡을 멈춘 위치에서 일시정지 상태로 엽니다',
'settings.showChapterMarkersOnTimeline' => '탐색 막대에 챕터 마커 표시',
'settings.showChapterMarkersOnTimelineDescription' => '챕터 경계에서 탐색 막대 구분',
'settings.specialsOrdering' => '에피소드 순서로 스페셜 표시',
@@ -3012,7 +3179,11 @@ extension on TranslationsKo {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => '단축키가 이미 ${action}에 할당되었습니다',
'settings.shortcutUpdated' => ({required Object action}) => '${action} 단축키가 업데이트되었습니다',
'settings.saveFailed' => '변경 사항을 저장하지 못했습니다. 다시 시도하세요.',
'settings.autoSkip' => '자동 건너뛰기',
'settings.autoPlayAndSkip' => '자동 재생 및 건너뛰기',
'settings.autoPlayNextEpisode' => '다음 에피소드 자동 재생',
'settings.autoPlayNextEpisodeDescription' => '에피소드가 끝나면 다음 에피소드를 자동으로 재생',
'settings.playNextCountdown' => '다음 재생 카운트다운',
'settings.playNextCountdownImmediate' => '즉시 재생',
'settings.autoSkipIntro' => '자동으로 오프닝 건너뛰기',
'settings.autoSkipIntroDescription' => '몇 초 후 오프닝을 자동으로 건너뛰기',
'settings.autoSkipCredits' => '자동으로 엔딩 건너뛰기',
@@ -3087,10 +3258,8 @@ extension on TranslationsKo {
'settings.dvConversionNativeDescription' => '네이티브 DV7을 강제하고 DV 변환 재시도를 억제합니다',
'settings.dvConversionDv81Description' => 'Dolby Vision 프로필 8.1로 인라인 RPU 변환을 강제합니다',
'settings.dvConversionHevcStripDescription' => 'Dolby Vision RPU/EL 레이어를 제거하고 일반 HEVC로 제공합니다',
'settings.demuxer' => '컨테이너 디먹서',
'settings.demuxerDescription' => '다이렉트 재생 파일을 분석할 디먹서입니다. 파일이 제대로 재생되지 않으면 media3으로 전환하세요.',
'settings.demuxerFfmpeg' => 'FFmpeg (권장)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => '디인터레이싱',
'settings.deinterlaceDescription' => '인터레이스 비디오의 빗살 무늬 아티팩트를 제거합니다 (mpv 플레이어만 해당)',
'settings.requireProfileSelectionOnOpen' => '앱 실행 시 프로필 선택',
'settings.requireProfileSelectionOnOpenDescription' => '앱을 열 때마다 프로필 선택 화면을 표시합니다',
'settings.forceTvMode' => 'TV 모드 강제 사용',
@@ -3108,15 +3277,31 @@ extension on TranslationsKo {
'settings.showExploreTabDescription' => 'Plex Discover와 연결된 트래커의 콘텐츠가 포함된 둘러보기 탭을 표시합니다',
'settings.liveTvDefaultFavorites' => '즐겨찾기 채널 기본 설정',
'settings.liveTvDefaultFavoritesDescription' => '라이브 TV를 열 때 즐겨찾기 채널만 표시',
'settings.general' => '일반',
'settings.generalDescription' => '언어, 시작 및 창 동작',
'settings.languageAndRegion' => '언어 및 지역',
'settings.startup' => '시작',
'settings.display' => '디스플레이',
'settings.libraryAndCards' => '라이브러리 및 카드',
'settings.homeScreen' => '홈 화면',
'settings.navigation' => '탐색',
'settings.window' => '창',
'settings.content' => '콘텐츠',
'settings.liveTv' => '라이브 TV',
'settings.player' => '플레이어',
'settings.subtitlesAndConfig' => '자막 및 구성',
'settings.videoAndDisplay' => '비디오 및 디스플레이',
'settings.audio' => '오디오',
'settings.quality' => '화질',
'settings.subtitles' => '자막',
'settings.seekAndTiming' => '탐색 및 타이밍',
'settings.behavior' => '동작',
'settings.gestures' => '제스처',
'settings.gestureBrightnessSwipe' => '밝기 스와이프',
'settings.gestureBrightnessSwipeDescription' => '왼쪽 가장자리에서 위아래로 스와이프하여 밝기를 조절합니다',
'settings.gestureVolumeSwipe' => '볼륨 스와이프',
'settings.gestureVolumeSwipeDescription' => '오른쪽 가장자리에서 위아래로 스와이프하여 볼륨을 조절합니다',
'settings.gesturePinchToZoom' => '핀치 줌',
'settings.gesturePinchToZoomDescription' => '비디오에서 핀치하여 확대하거나 축소합니다',
'settings.controls' => '컨트롤',
'settings.rememberPlayerChanges' => '플레이어 변경 사항 기억',
'settings.rememberPlayerChangesDescription' => '재생 중 변경한 사항을 저장하고 다시 적용할 위치',
'settings.scopePlaybackSpeed' => '재생 속도',
@@ -3254,6 +3439,8 @@ extension on TranslationsKo {
'fileInfo.has64bitOffsets' => '64비트 오프셋',
'fileInfo.protocol' => '프로토콜',
'fileInfo.mediaType' => '미디어 유형',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => '소스 종류',
'fileInfo.optimizedVersion' => '최적화 버전',
'fileInfo.optimizationTarget' => '최적화 대상',
@@ -3278,8 +3465,6 @@ extension on TranslationsKo {
'fileInfo.flagOriginal' => '원본',
'fileInfo.channelsMono' => '모노',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => '프로파일 ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => '시청 완료로 표시',
'mediaMenu.markAsUnwatched' => '미시청으로 표시',
'mediaMenu.removeFromContinueWatching' => '계속 보기에서 제거',
@@ -3614,6 +3799,63 @@ extension on TranslationsKo {
'connections.signInAgain' => '다시 로그인',
'connections.editMediaBrowserTitle' => ({required Object product}) => '${product} 연결 편집',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => '${serverName}의 URL을 추가하거나 제거하세요. Plezy는 연결 가능한 URL 중 지연 시간이 가장 낮은 URL을 사용합니다.',
'accountPreferences.sectionTitle' => '계정 기본 설정',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => '${account}에 저장된 오디오, 자막 및 라이브러리 옵션',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => '${count}개 계정에 저장된 오디오, 자막 및 라이브러리 옵션',
'accountPreferences.pickAccount' => '각 계정은 고유한 기본 설정을 저장합니다. 편집할 계정을 선택하세요.',
'accountPreferences.storedOnAccount' => '이 옵션은 계정 자체에 저장되므로 해당 계정으로 로그인한 모든 앱에서 사용됩니다 — 다른 기기의 Plezy를 포함합니다.',
'accountPreferences.noAccounts' => '구성할 계정이 없습니다',
'accountPreferences.noAccountsHint' => 'Plex에 로그인하거나 Jellyfin 또는 Emby 서버에 연결하면 해당 계정에 저장된 기본 설정이 여기에 표시됩니다.',
'accountPreferences.unavailable' => '이 계정에 연결할 수 없습니다',
'accountPreferences.loadFailed' => '이 기본 설정을 불러올 수 없습니다',
'accountPreferences.noPreference' => '기본 설정 없음',
'accountPreferences.notSet' => '설정되지 않음',
'accountPreferences.groups.audioAndSubtitles' => '오디오 및 자막',
'accountPreferences.groups.libraryDisplay' => '라이브러리',
'accountPreferences.groups.personalMedia' => '개인 미디어',
'accountPreferences.preferredAudioLanguage' => '선호 오디오 언어',
'accountPreferences.autoSelectAudio' => '언어별로 오디오 선택',
'accountPreferences.autoSelectAudioDescription' => '끄면 파일이 기본으로 표시한 오디오 트랙을 유지합니다.',
'accountPreferences.preferredSubtitleLanguage' => '선호 자막 언어',
'accountPreferences.subtitleMode' => '자막 자동 켜기',
'accountPreferences.subtitleModes.none' => '수동 선택',
'accountPreferences.subtitleModes.noneDescription' => '자동으로 자막을 켜지 않습니다.',
'accountPreferences.subtitleModes.defaultMode' => '트랙 플래그 따르기',
'accountPreferences.subtitleModes.defaultModeDescription' => '각 자막 트랙에 저장된 기본 및 강제 플래그를 사용합니다.',
'accountPreferences.subtitleModes.always' => '항상 활성화',
'accountPreferences.subtitleModes.alwaysDescription' => '선호 언어의 자막 트랙이 있으면 항상 켭니다.',
'accountPreferences.subtitleModes.onlyForced' => '강제 자막만',
'accountPreferences.subtitleModes.onlyForcedDescription' => '강제로 표시된 트랙만 로드합니다.',
'accountPreferences.subtitleModes.smart' => '외국어 오디오 시 표시',
'accountPreferences.subtitleModes.smartDescription' => '오디오가 다른 언어일 때만 자막을 켭니다.',
'accountPreferences.subtitleAccessibility' => 'SDH 자막',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'SDH가 아닌 자막 선호',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'SDH 자막 선호',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'SDH 자막만',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'SDH가 아닌 자막만',
'accountPreferences.forcedSubtitles' => '강제 자막',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => '강제가 아닌 자막 선호',
'accountPreferences.forcedSubtitleOptions.preferForced' => '강제 자막 선호',
'accountPreferences.forcedSubtitleOptions.onlyForced' => '강제 자막만',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => '강제가 아닌 자막만',
'accountPreferences.displayMissingEpisodes' => '누락된 에피소드 표시',
'accountPreferences.displayMissingEpisodesDescription' => '서버가 알고 있지만 파일이 없는 에피소드를 나열합니다.',
'accountPreferences.hidePlayedInLatest' => '최신 항목에서 시청한 항목 숨기기',
'accountPreferences.hidePlayedInLatestDescription' => '이미 시청한 항목은 서버의 최신 행에서 제외합니다.',
'accountPreferences.displayCollectionsView' => '컬렉션 보기 표시',
'accountPreferences.displayCollectionsViewDescription' => '라이브러리와 함께 서버의 컬렉션 보기를 제공합니다.',
'accountPreferences.rewatchingInNextUp' => '재시청한 프로그램을 다음 에피소드에 유지',
'accountPreferences.rewatchingInNextUpDescription' => '프로그램을 끝까지 본 뒤 다시 시작하면, 목록에서 제거하는 대신 다음 에피소드가 재시청을 따라갑니다.',
'accountPreferences.watchedIndicator' => '시청 표시',
'accountPreferences.watchedIndicatorOptions.none' => '안 함',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => '영화 및 TV 프로그램',
'accountPreferences.watchedIndicatorOptions.movies' => '영화만',
'accountPreferences.watchedIndicatorOptions.shows' => 'TV 프로그램만',
'accountPreferences.mediaReviewsVisibility' => '평점 및 리뷰',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => '사용자 및 평론가',
'accountPreferences.mediaReviewsOptions.usersOnly' => '사용자만',
'accountPreferences.mediaReviewsOptions.criticsOnly' => '평론가만',
'accountPreferences.mediaReviewsOptions.nobody' => '숨김',
'discover.title' => '둘러보기',
'discover.noContentAvailable' => '사용 가능한 콘텐츠가 없습니다',
'discover.addMediaToLibraries' => '미디어 라이브러리에 미디어를 추가해 주세요',
@@ -3711,6 +3953,8 @@ extension on TranslationsKo {
'libraries.filterCategories.contentRating' => '시청 등급',
'libraries.filterCategories.tag' => '태그',
'libraries.filterCategories.unwatched' => '미시청',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => '재생하지 않음',
'libraries.filterCategories.favorites' => '즐겨찾기',
'libraries.sortLabels.title' => '제목',
@@ -3792,8 +4036,6 @@ extension on TranslationsKo {
'explore.rows.watchlist' => '관심 목록',
'explore.rows.recommendedMovies' => '추천 영화',
'explore.rows.recommendedShows' => '추천 TV 프로그램',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => '지금 뜨는 영화',
'explore.rows.trendingShows' => '지금 뜨는 TV 프로그램',
'explore.rows.popularMovies' => '인기 영화',
@@ -3946,6 +4188,11 @@ extension on TranslationsKo {
'liveTv.unknownChannel' => '알 수 없는 채널',
'liveTv.live' => '실시간',
'liveTv.reloadGuide' => '편성표 새로고침',
'liveTv.searchGuide' => '편성표 검색',
'liveTv.searchHint' => '채널 및 프로그램 검색',
'liveTv.searchNoResults' => ({required Object query}) => '「${query}」와(과) 일치하는 항목이 없습니다',
'liveTv.channelsSection' => '채널',
'liveTv.programsSection' => '프로그램',
'liveTv.now' => '지금',
'liveTv.today' => '오늘',
'liveTv.tomorrow' => '내일',
@@ -4083,6 +4330,8 @@ extension on TranslationsKo {
'music.repeatAll' => '전체 반복',
'music.repeatOne' => '한 곡 반복',
'music.instantMixNoServer' => '즉석 믹스에 사용할 수 있는 서버가 없습니다',
'music.instantMixFailed' => '인스턴트 믹스를 불러오지 못했습니다',
'music.instantMixEmpty' => '인스턴트 믹스에 트랙이 없습니다',
'music.noAudioUrl' => ({required Object track}) => '${track}에 사용할 수 있는 오디오 URL이 없습니다',
'music.discography.singlesAndEps' => '싱글 및 EP',
'music.discography.live' => '라이브',
@@ -4218,6 +4467,8 @@ extension on TranslationsKo {
'downloads.syncRuleCleanupUnavailable' => '연결된 다운로드를 안전하게 확인할 수 없습니다. 서버에 다시 연결한 후 시도하거나, 다운로드를 삭제하지 않고 규칙만 제거하세요.',
'downloads.syncedNewEpisodes' => ({required Object title, required Object count}) => '${title}의 새 에피소드 ${count}개 동기화됨',
'downloads.activeSyncRules' => '동기화 규칙',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => '동기화 규칙 없음',
'downloads.manageSyncRule' => '동기화 관리',
'downloads.editEpisodeCount' => '에피소드 수',
@@ -4306,8 +4557,6 @@ extension on TranslationsKo {
'companionRemote.pairing.availableDevices' => '사용 가능한 기기',
'companionRemote.pairing.manualConnection' => '수동 연결',
'companionRemote.pairing.cryptoInitFailed' => '보안 연결을 시작할 수 없습니다. 먼저 Plex에 로그인하세요.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => '호스트 주소를 입력하세요',
'companionRemote.pairing.validationHostFormat' => '형식은 IP:포트여야 합니다 (예: 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => '연결 시간이 초과되었습니다. 두 기기에서 같은 네트워크를 사용하세요.',
@@ -4567,6 +4816,8 @@ extension on TranslationsKo {
'seerr.statusBlocklisted' => '차단 목록에 있음',
'seerr.couldNotReach' => ({required Object url, required Object error}) => '${url}에 연결할 수 없습니다: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => '${url}에 Seerr 인스턴스가 없습니다(HTTP ${status})',
'seerr.invalidUrl' => 'https://seerr.example.com과 같은 서버 주소를 입력하세요',
'seerr.quickConnectUnsupported' => '이 Seerr 인스턴스는 Quick Connect를 지원하지 않습니다. Seerr 3.4 이상이 필요합니다.',
'seerr.notInitialized' => '이 Seerr 인스턴스는 최초 실행 설정을 완료하지 않았습니다',
'seerr.noPlexTokenForReauth' => '다시 로그인하는 데 사용할 수 있는 Plex 토큰이 없습니다',
'seerr.noStoredCredentials' => '다시 로그인하는 데 사용할 수 있는 저장된 인증 정보가 없습니다',
@@ -4577,6 +4828,7 @@ extension on TranslationsKo {
'seerr.sessionRejectedAfterReauth' => '다시 로그인한 후 세션이 거부되었습니다',
'services.title' => '서비스',
'services.hubSubtitle' => '시청 진행률을 동기화하고 새 작품을 요청하세요.',
'services.integrations' => '연동',
'services.notConnected' => '연결되지 않음',
'services.connectedAs' => ({required Object username}) => '@${username}(으)로 연결됨',
'services.scrobble' => '진행률 자동 추적',
+282 -30
View File
@@ -60,6 +60,7 @@ class TranslationsNb extends Translations with BaseTranslations<AppLocale, Trans
@override late final _Translations$dialog$nb dialog = _Translations$dialog$nb._(_root);
@override late final _Translations$profiles$nb profiles = _Translations$profiles$nb._(_root);
@override late final _Translations$connections$nb connections = _Translations$connections$nb._(_root);
@override late final _Translations$accountPreferences$nb accountPreferences = _Translations$accountPreferences$nb._(_root);
@override late final _Translations$discover$nb discover = _Translations$discover$nb._(_root);
@override late final _Translations$errors$nb errors = _Translations$errors$nb._(_root);
@override late final _Translations$libraries$nb libraries = _Translations$libraries$nb._(_root);
@@ -117,6 +118,7 @@ class _Translations$auth$nb extends Translations$auth$en {
@override String get useBrowser => 'Bruk nettleser';
@override String get or => 'eller';
@override String connectToMediaBrowser({required Object product}) => 'Koble til ${product}';
@override String get quickConnect => 'Quick Connect';
@override String get useQuickConnect => 'Bruk Quick Connect';
@override String get quickConnectInstructions => 'Åpne Quick Connect i Jellyfin og skriv inn denne koden.';
@override String get quickConnectWaiting => 'Venter på godkjenning…';
@@ -165,6 +167,7 @@ class _Translations$common$nb extends Translations$common$en {
@override String get mute => 'Demp';
@override String get ok => 'OK';
@override String get off => 'Av';
@override String get options => 'Valg';
@override String seasonNumber({required Object number}) => 'Sesong ${number}';
@override String episodeNumberTitle({required Object number, required Object title}) => 'Episode ${number} - ${title}';
@override String chapterNumber({required Object number}) => 'Kapittel ${number}';
@@ -249,6 +252,10 @@ class _Translations$settings$nb extends Translations$settings$en {
@override String get displayScale => 'Skalering';
@override String get compact => 'Kompakt';
@override String get comfortable => 'Komfortabel';
@override String get gridSpacing => 'Rutenettavstand';
@override String get gridSpacingTight => 'Tett';
@override String get gridSpacingNormal => 'Normal';
@override String get gridSpacingSpacious => 'Rommelig';
@override String get tvCornerSpotlightBackdrop => 'Fremhevet bakgrunn i hjørnet';
@override String get tvCornerSpotlightBackdropDescription => 'Vis fremhevet grafikk øverst til høyre i stedet for å fylle skjermen';
@override String get viewMode => 'Visningsmodus';
@@ -292,16 +299,14 @@ class _Translations$settings$nb extends Translations$settings$en {
@override String get mpv => 'mpv';
@override String get hardwareDecoding => 'Maskinvaredekoding';
@override String get hardwareDecodingDescription => 'Bruk maskinvareakselerasjon når tilgjengelig';
@override String get bufferSize => 'Bufferstørrelse';
@override String bufferSizeMB({required Object size}) => '${size} MB';
@override String get bufferSizeAuto => 'Automatisk (anbefalt)';
@override String bufferSizeWarning({required Object heap, required Object size}) => '${heap} MB minne tilgjengelig. En buffer på ${size} MB kan påvirke avspillingen.';
@override String get playbackBuffer => 'Avspillingsbuffer';
@override String get playbackBufferAuto => 'Auto (anbefalt)';
@override String get playbackBufferLarge => 'Stor';
@override String get playbackBufferExtraLarge => 'Ekstra stor';
@override String get playbackBufferDescription => 'Bufrer mer mot ustabile tilkoblinger. Begrenses også av bufferstørrelsen.';
@override String get defaultQualityTitle => 'Standardkvalitet';
@override String get cellularQualityTitle => 'Standardkvalitet på mobilnett';
@override String get cellularQualitySameAsDefault => 'Samme som standardkvalitet';
@override String get musicQualityTitle => 'Musikkvalitet';
@override String get subtitleStyling => 'Undertekststil';
@override String get subtitleStylingDescription => 'Tilpass utseendet på undertekster';
@@ -315,6 +320,8 @@ class _Translations$settings$nb extends Translations$settings$en {
@override String get rememberTrackSelectionsDescription => 'Husk lyd- og undertekstvalg per tittel';
@override String get followServerTrackSelections => 'Bruk serverens sporvalg per episode';
@override String get followServerTrackSelectionsDescription => 'Ved episodebytte brukes lyden og undertekstene som er valgt på serveren, i stedet for å videreføre gjeldende valg';
@override String get resumeMusicOnLaunch => 'Husk musikkøkt';
@override String get resumeMusicOnLaunchDescription => 'Åpne den siste sangen på pause der den slapp når appen starter';
@override String get showChapterMarkersOnTimeline => 'Vis kapittelmarkører på tidslinjen';
@override String get showChapterMarkersOnTimelineDescription => 'Del tidslinjen ved kapittelgrenser';
@override String get specialsOrdering => 'Spesialepisoder i episoderekkefølge';
@@ -368,7 +375,11 @@ class _Translations$settings$nb extends Translations$settings$en {
@override String shortcutAlreadyAssigned({required Object action}) => 'Snarvei allerede tilordnet til ${action}';
@override String shortcutUpdated({required Object action}) => 'Snarvei oppdatert for ${action}';
@override String get saveFailed => 'Kunne ikke lagre endringene. Prøv igjen.';
@override String get autoSkip => 'Automatisk hopp';
@override String get autoPlayAndSkip => 'Autospill og hopp';
@override String get autoPlayNextEpisode => 'Spill neste episode automatisk';
@override String get autoPlayNextEpisodeDescription => 'Start neste episode automatisk når en episode er ferdig';
@override String get playNextCountdown => 'Nedtelling for neste episode';
@override String get playNextCountdownImmediate => 'Spill umiddelbart';
@override String get autoSkipIntro => 'Hopp over intro automatisk';
@override String get autoSkipIntroDescription => 'Hopp automatisk over intromarkører etter noen sekunder';
@override String get autoSkipCredits => 'Hopp over rulletekst automatisk';
@@ -443,10 +454,8 @@ class _Translations$settings$nb extends Translations$settings$en {
@override String get dvConversionNativeDescription => 'Tving opprinnelig DV7-avspilling og ikke prøv DV-konvertering på nytt';
@override String get dvConversionDv81Description => 'Tving direkte RPU-konvertering til Dolby Vision-profil 8.1';
@override String get dvConversionHevcStripDescription => 'Fjern Dolby Vision RPU/EL-lag og lever som vanlig HEVC';
@override String get demuxer => 'Container-demukser';
@override String get demuxerDescription => 'Hvilken demukser som leser direkteavspilte filer. Bytt til media3 hvis en fil oppfører seg rart.';
@override String get demuxerFfmpeg => 'FFmpeg (anbefalt)';
@override String get demuxerMedia3 => 'media3';
@override String get deinterlace => 'Deinterlacing';
@override String get deinterlaceDescription => 'Fjern combing-artefakter fra interlaced video (kun mpv-spiller)';
@override String get requireProfileSelectionOnOpen => 'Spør om profil ved appåpning';
@override String get requireProfileSelectionOnOpenDescription => 'Vis profilvalg hver gang appen åpnes';
@override String get forceTvMode => 'Tving TV-modus';
@@ -464,15 +473,31 @@ class _Translations$settings$nb extends Translations$settings$en {
@override String get showExploreTabDescription => 'Vis Oppdag-fanen med innhold fra Plex Discover og tilkoblede sporingstjenester';
@override String get liveTvDefaultFavorites => 'Vis favorittkanaler som standard';
@override String get liveTvDefaultFavoritesDescription => 'Vis bare favorittkanaler når du åpner Direkte-TV';
@override String get general => 'Generelt';
@override String get generalDescription => 'Språk, oppstart og vindusatferd';
@override String get languageAndRegion => 'Språk og region';
@override String get startup => 'Oppstart';
@override String get display => 'Skjerm';
@override String get libraryAndCards => 'Bibliotek og kort';
@override String get homeScreen => 'Hjemmeskjerm';
@override String get navigation => 'Navigering';
@override String get window => 'Vindu';
@override String get content => 'Innhold';
@override String get liveTv => 'Direkte-TV';
@override String get player => 'Spiller';
@override String get subtitlesAndConfig => 'Undertekster og konfigurasjon';
@override String get videoAndDisplay => 'Video og skjerm';
@override String get audio => 'Lyd';
@override String get quality => 'Kvalitet';
@override String get subtitles => 'Undertekster';
@override String get seekAndTiming => 'Spoling og tidsinnstillinger';
@override String get behavior => 'Oppførsel';
@override String get gestures => 'Bevegelser';
@override String get gestureBrightnessSwipe => 'Lysstyrke-sveip';
@override String get gestureBrightnessSwipeDescription => 'Sveip opp eller ned på venstre kant for å justere lysstyrken';
@override String get gestureVolumeSwipe => 'Volum-sveip';
@override String get gestureVolumeSwipeDescription => 'Sveip opp eller ned på høyre kant for å justere volumet';
@override String get gesturePinchToZoom => 'Klyp for å zoome';
@override String get gesturePinchToZoomDescription => 'Klyp på videoen for å zoome inn eller ut';
@override String get controls => 'Kontroller';
@override String get rememberPlayerChanges => 'Husk endringer i spilleren';
@override String get rememberPlayerChangesDescription => 'Hvor en endring under avspilling lagres og brukes på nytt';
@override String get scopePlaybackSpeed => 'Avspillingshastighet';
@@ -1086,6 +1111,49 @@ class _Translations$connections$nb extends Translations$connections$en {
@override String editMediaBrowserIntro({required Object serverName}) => 'Legg til eller fjern URL-er for ${serverName}. Plezy vil bruke den nåbare URL-en med lavest ventetid.';
}
// Path: accountPreferences
class _Translations$accountPreferences$nb extends Translations$accountPreferences$en {
_Translations$accountPreferences$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get sectionTitle => 'Kontoinnstillinger';
@override String hubSubtitleSingle({required Object account}) => 'Lyd-, undertekst- og bibliotekalternativer lagret på ${account}';
@override String hubSubtitleMultiple({required Object count}) => 'Lyd-, undertekst- og bibliotekalternativer lagret på ${count} kontoer';
@override String get pickAccount => 'Hver konto lagrer sine egne innstillinger. Velg hvilken du vil redigere.';
@override String get storedOnAccount => 'Disse alternativene lagres på selve kontoen, så alle apper som er logget på den, bruker dem — inkludert Plezy på de andre enhetene dine.';
@override String get noAccounts => 'Ingen kontoer å konfigurere';
@override String get noAccountsHint => 'Logg inn på Plex, eller koble til en Jellyfin- eller Emby-server, så vises innstillingene som er lagret på den kontoen, her.';
@override String get unavailable => 'Kan ikke nå denne kontoen';
@override String get loadFailed => 'Kunne ikke laste inn disse innstillingene';
@override String get noPreference => 'Ingen preferanse';
@override String get notSet => 'Ikke angitt';
@override late final _Translations$accountPreferences$groups$nb groups = _Translations$accountPreferences$groups$nb._(_root);
@override String get preferredAudioLanguage => 'Foretrukket lydspråk';
@override String get autoSelectAudio => 'Velg lyd etter språk';
@override String get autoSelectAudioDescription => 'Av beholder det lydsporet filen markerer som standard.';
@override String get preferredSubtitleLanguage => 'Foretrukket undertekstspråk';
@override String get subtitleMode => 'Slå på undertekster';
@override late final _Translations$accountPreferences$subtitleModes$nb subtitleModes = _Translations$accountPreferences$subtitleModes$nb._(_root);
@override String get subtitleAccessibility => 'SDH-undertekster';
@override late final _Translations$accountPreferences$subtitleAccessibilityOptions$nb subtitleAccessibilityOptions = _Translations$accountPreferences$subtitleAccessibilityOptions$nb._(_root);
@override String get forcedSubtitles => 'Tvungne undertekster';
@override late final _Translations$accountPreferences$forcedSubtitleOptions$nb forcedSubtitleOptions = _Translations$accountPreferences$forcedSubtitleOptions$nb._(_root);
@override String get displayMissingEpisodes => 'Vis manglende episoder';
@override String get displayMissingEpisodesDescription => 'Vis episoder serveren kjenner til, men som ikke har noen fil.';
@override String get hidePlayedInLatest => 'Skjul sette elementer i Siste';
@override String get hidePlayedInLatestDescription => 'Hold elementer du allerede har sett utenfor serverens Siste-rader.';
@override String get displayCollectionsView => 'Vis samlingsvisningen';
@override String get displayCollectionsViewDescription => 'Vis serverens samlingsvisning sammen med bibliotekene dine.';
@override String get rewatchingInNextUp => 'Behold serier du ser på nytt i Neste opp';
@override String get rewatchingInNextUpDescription => 'Når du er ferdig med en serie og starter den på nytt, følger Neste opp den nye avspillingen i stedet for å fjerne serien.';
@override String get watchedIndicator => 'Sett-indikatorer';
@override late final _Translations$accountPreferences$watchedIndicatorOptions$nb watchedIndicatorOptions = _Translations$accountPreferences$watchedIndicatorOptions$nb._(_root);
@override String get mediaReviewsVisibility => 'Vurderinger og anmeldelser';
@override late final _Translations$accountPreferences$mediaReviewsOptions$nb mediaReviewsOptions = _Translations$accountPreferences$mediaReviewsOptions$nb._(_root);
}
// Path: discover
class _Translations$discover$nb extends Translations$discover$en {
_Translations$discover$nb._(TranslationsNb root) : this._root = root, super.internal(root);
@@ -1385,6 +1453,11 @@ class _Translations$liveTv$nb extends Translations$liveTv$en {
@override String get unknownChannel => 'Ukjent kanal';
@override String get live => 'DIREKTE';
@override String get reloadGuide => 'Last inn programguide på nytt';
@override String get searchGuide => 'Søk i programguiden';
@override String get searchHint => 'Søk etter kanaler og programmer';
@override String searchNoResults({required Object query}) => 'Ingen treff for "${query}"';
@override String get channelsSection => 'Kanaler';
@override String get programsSection => 'Programmer';
@override String get now => 'Nå';
@override String get today => 'I dag';
@override String get tomorrow => 'I morgen';
@@ -1545,6 +1618,8 @@ class _Translations$music$nb extends Translations$music$en {
@override String get repeatAll => 'Gjenta alle';
@override String get repeatOne => 'Gjenta ett spor';
@override String get instantMixNoServer => 'Ingen server er tilgjengelig for en hurtigmiks';
@override String get instantMixFailed => 'Kunne ikke laste inn direktemiksen';
@override String get instantMixEmpty => 'Direktemiksen ga ingen spor';
@override String noAudioUrl({required Object track}) => 'Ingen lyd-URL er tilgjengelig for ${track}';
@override late final _Translations$music$discography$nb discography = _Translations$music$discography$nb._(_root);
}
@@ -2047,6 +2122,8 @@ class _Translations$seerr$nb extends Translations$seerr$en {
@override String get statusBlocklisted => 'På blokkeringslisten';
@override String couldNotReach({required Object url, required Object error}) => 'Kunne ikke nå ${url}: ${error}';
@override String noInstanceAtUrl({required Object url, required Object status}) => 'Ingen Seerr-instans på ${url} (HTTP ${status})';
@override String get invalidUrl => 'Skriv inn en serveradresse som https://seerr.example.com';
@override String get quickConnectUnsupported => 'Denne Seerr-instansen støtter ikke Quick Connect. Den krever Seerr 3.4 eller nyere.';
@override String get notInitialized => 'Denne Seerr-instansen har ikke fullført førstegangsoppsettet';
@override String get noPlexTokenForReauth => 'Ingen Plex-token er tilgjengelig for å logge inn på nytt';
@override String get noStoredCredentials => 'Ingen lagret påloggingsinformasjon er tilgjengelig for å logge inn på nytt';
@@ -2066,6 +2143,7 @@ class _Translations$services$nb extends Translations$services$en {
// Translations
@override String get title => 'Tjenester';
@override String get hubSubtitle => 'Synkroniser fremdrift og forespør nye titler.';
@override String get integrations => 'Integrasjoner';
@override String get notConnected => 'Ikke tilkoblet';
@override String connectedAs({required Object username}) => 'Tilkoblet som @${username}';
@override String get scrobble => 'Registrer fremdrift automatisk';
@@ -2228,6 +2306,89 @@ class _Translations$videoControls$pipErrors$nb extends Translations$videoControl
@override String unknown({required Object error}) => 'En feil oppstod: ${error}';
}
// Path: accountPreferences.groups
class _Translations$accountPreferences$groups$nb extends Translations$accountPreferences$groups$en {
_Translations$accountPreferences$groups$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get audioAndSubtitles => 'Lyd og undertekster';
@override String get libraryDisplay => 'Bibliotek';
@override String get personalMedia => 'Personlige medier';
}
// Path: accountPreferences.subtitleModes
class _Translations$accountPreferences$subtitleModes$nb extends Translations$accountPreferences$subtitleModes$en {
_Translations$accountPreferences$subtitleModes$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get none => 'Manuelt valgt';
@override String get noneDescription => 'Slå aldri på undertekster av seg selv.';
@override String get defaultMode => 'Følg sporflaggene';
@override String get defaultModeDescription => 'Bruk standard- og tvunget-flaggene som er lagret på hvert undertekstspor.';
@override String get always => 'Alltid aktivert';
@override String get alwaysDescription => 'Slå på et undertekstspor på foretrukket språk når det finnes et.';
@override String get onlyForced => 'Kun tvungne undertekster';
@override String get onlyForcedDescription => 'Last inn bare sporene som er merket som tvungne.';
@override String get smart => 'Vist med fremmedspråklig lyd';
@override String get smartDescription => 'Slå på undertekster bare når lyden er på et annet språk.';
}
// Path: accountPreferences.subtitleAccessibilityOptions
class _Translations$accountPreferences$subtitleAccessibilityOptions$nb extends Translations$accountPreferences$subtitleAccessibilityOptions$en {
_Translations$accountPreferences$subtitleAccessibilityOptions$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get preferNonSdh => 'Foretrekk ikke-SDH-undertekster';
@override String get preferSdh => 'Foretrekk SDH-undertekster';
@override String get onlySdh => 'Kun SDH-undertekster';
@override String get onlyNonSdh => 'Kun ikke-SDH-undertekster';
}
// Path: accountPreferences.forcedSubtitleOptions
class _Translations$accountPreferences$forcedSubtitleOptions$nb extends Translations$accountPreferences$forcedSubtitleOptions$en {
_Translations$accountPreferences$forcedSubtitleOptions$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get preferNonForced => 'Foretrekk ikke-tvungne undertekster';
@override String get preferForced => 'Foretrekk tvungne undertekster';
@override String get onlyForced => 'Kun tvungne undertekster';
@override String get onlyNonForced => 'Kun ikke-tvungne undertekster';
}
// Path: accountPreferences.watchedIndicatorOptions
class _Translations$accountPreferences$watchedIndicatorOptions$nb extends Translations$accountPreferences$watchedIndicatorOptions$en {
_Translations$accountPreferences$watchedIndicatorOptions$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get none => 'Aldri';
@override String get moviesAndShows => 'Filmer og TV-serier';
@override String get movies => 'Kun filmer';
@override String get shows => 'Kun TV-serier';
}
// Path: accountPreferences.mediaReviewsOptions
class _Translations$accountPreferences$mediaReviewsOptions$nb extends Translations$accountPreferences$mediaReviewsOptions$en {
_Translations$accountPreferences$mediaReviewsOptions$nb._(TranslationsNb root) : this._root = root, super.internal(root);
final TranslationsNb _root; // ignore: unused_field
// Translations
@override String get usersAndCritics => 'Brukere og kritikere';
@override String get usersOnly => 'Kun brukere';
@override String get criticsOnly => 'Kun kritikere';
@override String get nobody => 'Skjult';
}
// Path: libraries.tabs
class _Translations$libraries$tabs$nb extends Translations$libraries$tabs$en {
_Translations$libraries$tabs$nb._(TranslationsNb root) : this._root = root, super.internal(root);
@@ -2270,7 +2431,7 @@ class _Translations$libraries$filterCategories$nb extends Translations$libraries
@override String get genre => 'Sjanger';
@override String get year => 'År';
@override String get contentRating => 'Aldersgrense';
@override String get tag => 'Tag';
@override String get tag => 'Tagg';
@override String get unwatched => 'Usette';
@override String get unplayed => 'Ikke avspilt';
@override String get favorites => 'Favoritter';
@@ -2786,6 +2947,7 @@ extension on TranslationsNb {
'auth.useBrowser' => 'Bruk nettleser',
'auth.or' => 'eller',
'auth.connectToMediaBrowser' => ({required Object product}) => 'Koble til ${product}',
'auth.quickConnect' => 'Quick Connect',
'auth.useQuickConnect' => 'Bruk Quick Connect',
'auth.quickConnectInstructions' => 'Åpne Quick Connect i Jellyfin og skriv inn denne koden.',
'auth.quickConnectWaiting' => 'Venter på godkjenning…',
@@ -2825,6 +2987,7 @@ extension on TranslationsNb {
'common.mute' => 'Demp',
'common.ok' => 'OK',
'common.off' => 'Av',
'common.options' => 'Valg',
'common.seasonNumber' => ({required Object number}) => 'Sesong ${number}',
'common.episodeNumberTitle' => ({required Object number, required Object title}) => 'Episode ${number} - ${title}',
'common.chapterNumber' => ({required Object number}) => 'Kapittel ${number}',
@@ -2903,6 +3066,10 @@ extension on TranslationsNb {
'settings.displayScale' => 'Skalering',
'settings.compact' => 'Kompakt',
'settings.comfortable' => 'Komfortabel',
'settings.gridSpacing' => 'Rutenettavstand',
'settings.gridSpacingTight' => 'Tett',
'settings.gridSpacingNormal' => 'Normal',
'settings.gridSpacingSpacious' => 'Rommelig',
'settings.tvCornerSpotlightBackdrop' => 'Fremhevet bakgrunn i hjørnet',
'settings.tvCornerSpotlightBackdropDescription' => 'Vis fremhevet grafikk øverst til høyre i stedet for å fylle skjermen',
'settings.viewMode' => 'Visningsmodus',
@@ -2946,16 +3113,14 @@ extension on TranslationsNb {
'settings.mpv' => 'mpv',
'settings.hardwareDecoding' => 'Maskinvaredekoding',
'settings.hardwareDecodingDescription' => 'Bruk maskinvareakselerasjon når tilgjengelig',
'settings.bufferSize' => 'Bufferstørrelse',
'settings.bufferSizeMB' => ({required Object size}) => '${size} MB',
'settings.bufferSizeAuto' => 'Automatisk (anbefalt)',
'settings.bufferSizeWarning' => ({required Object heap, required Object size}) => '${heap} MB minne tilgjengelig. En buffer på ${size} MB kan påvirke avspillingen.',
'settings.playbackBuffer' => 'Avspillingsbuffer',
'settings.playbackBufferAuto' => 'Auto (anbefalt)',
'settings.playbackBufferLarge' => 'Stor',
'settings.playbackBufferExtraLarge' => 'Ekstra stor',
'settings.playbackBufferDescription' => 'Bufrer mer mot ustabile tilkoblinger. Begrenses også av bufferstørrelsen.',
'settings.defaultQualityTitle' => 'Standardkvalitet',
'settings.cellularQualityTitle' => 'Standardkvalitet på mobilnett',
'settings.cellularQualitySameAsDefault' => 'Samme som standardkvalitet',
'settings.musicQualityTitle' => 'Musikkvalitet',
'settings.subtitleStyling' => 'Undertekststil',
'settings.subtitleStylingDescription' => 'Tilpass utseendet på undertekster',
@@ -2969,6 +3134,8 @@ extension on TranslationsNb {
'settings.rememberTrackSelectionsDescription' => 'Husk lyd- og undertekstvalg per tittel',
'settings.followServerTrackSelections' => 'Bruk serverens sporvalg per episode',
'settings.followServerTrackSelectionsDescription' => 'Ved episodebytte brukes lyden og undertekstene som er valgt på serveren, i stedet for å videreføre gjeldende valg',
'settings.resumeMusicOnLaunch' => 'Husk musikkøkt',
'settings.resumeMusicOnLaunchDescription' => 'Åpne den siste sangen på pause der den slapp når appen starter',
'settings.showChapterMarkersOnTimeline' => 'Vis kapittelmarkører på tidslinjen',
'settings.showChapterMarkersOnTimelineDescription' => 'Del tidslinjen ved kapittelgrenser',
'settings.specialsOrdering' => 'Spesialepisoder i episoderekkefølge',
@@ -3022,7 +3189,11 @@ extension on TranslationsNb {
'settings.shortcutAlreadyAssigned' => ({required Object action}) => 'Snarvei allerede tilordnet til ${action}',
'settings.shortcutUpdated' => ({required Object action}) => 'Snarvei oppdatert for ${action}',
'settings.saveFailed' => 'Kunne ikke lagre endringene. Prøv igjen.',
'settings.autoSkip' => 'Automatisk hopp',
'settings.autoPlayAndSkip' => 'Autospill og hopp',
'settings.autoPlayNextEpisode' => 'Spill neste episode automatisk',
'settings.autoPlayNextEpisodeDescription' => 'Start neste episode automatisk når en episode er ferdig',
'settings.playNextCountdown' => 'Nedtelling for neste episode',
'settings.playNextCountdownImmediate' => 'Spill umiddelbart',
'settings.autoSkipIntro' => 'Hopp over intro automatisk',
'settings.autoSkipIntroDescription' => 'Hopp automatisk over intromarkører etter noen sekunder',
'settings.autoSkipCredits' => 'Hopp over rulletekst automatisk',
@@ -3097,10 +3268,8 @@ extension on TranslationsNb {
'settings.dvConversionNativeDescription' => 'Tving opprinnelig DV7-avspilling og ikke prøv DV-konvertering på nytt',
'settings.dvConversionDv81Description' => 'Tving direkte RPU-konvertering til Dolby Vision-profil 8.1',
'settings.dvConversionHevcStripDescription' => 'Fjern Dolby Vision RPU/EL-lag og lever som vanlig HEVC',
'settings.demuxer' => 'Container-demukser',
'settings.demuxerDescription' => 'Hvilken demukser som leser direkteavspilte filer. Bytt til media3 hvis en fil oppfører seg rart.',
'settings.demuxerFfmpeg' => 'FFmpeg (anbefalt)',
'settings.demuxerMedia3' => 'media3',
'settings.deinterlace' => 'Deinterlacing',
'settings.deinterlaceDescription' => 'Fjern combing-artefakter fra interlaced video (kun mpv-spiller)',
'settings.requireProfileSelectionOnOpen' => 'Spør om profil ved appåpning',
'settings.requireProfileSelectionOnOpenDescription' => 'Vis profilvalg hver gang appen åpnes',
'settings.forceTvMode' => 'Tving TV-modus',
@@ -3118,15 +3287,31 @@ extension on TranslationsNb {
'settings.showExploreTabDescription' => 'Vis Oppdag-fanen med innhold fra Plex Discover og tilkoblede sporingstjenester',
'settings.liveTvDefaultFavorites' => 'Vis favorittkanaler som standard',
'settings.liveTvDefaultFavoritesDescription' => 'Vis bare favorittkanaler når du åpner Direkte-TV',
'settings.general' => 'Generelt',
'settings.generalDescription' => 'Språk, oppstart og vindusatferd',
'settings.languageAndRegion' => 'Språk og region',
'settings.startup' => 'Oppstart',
'settings.display' => 'Skjerm',
'settings.libraryAndCards' => 'Bibliotek og kort',
'settings.homeScreen' => 'Hjemmeskjerm',
'settings.navigation' => 'Navigering',
'settings.window' => 'Vindu',
'settings.content' => 'Innhold',
'settings.liveTv' => 'Direkte-TV',
'settings.player' => 'Spiller',
'settings.subtitlesAndConfig' => 'Undertekster og konfigurasjon',
'settings.videoAndDisplay' => 'Video og skjerm',
'settings.audio' => 'Lyd',
'settings.quality' => 'Kvalitet',
'settings.subtitles' => 'Undertekster',
'settings.seekAndTiming' => 'Spoling og tidsinnstillinger',
'settings.behavior' => 'Oppførsel',
'settings.gestures' => 'Bevegelser',
'settings.gestureBrightnessSwipe' => 'Lysstyrke-sveip',
'settings.gestureBrightnessSwipeDescription' => 'Sveip opp eller ned på venstre kant for å justere lysstyrken',
'settings.gestureVolumeSwipe' => 'Volum-sveip',
'settings.gestureVolumeSwipeDescription' => 'Sveip opp eller ned på høyre kant for å justere volumet',
'settings.gesturePinchToZoom' => 'Klyp for å zoome',
'settings.gesturePinchToZoomDescription' => 'Klyp på videoen for å zoome inn eller ut',
'settings.controls' => 'Kontroller',
'settings.rememberPlayerChanges' => 'Husk endringer i spilleren',
'settings.rememberPlayerChangesDescription' => 'Hvor en endring under avspilling lagres og brukes på nytt',
'settings.scopePlaybackSpeed' => 'Avspillingshastighet',
@@ -3264,6 +3449,8 @@ extension on TranslationsNb {
'fileInfo.has64bitOffsets' => '64-biters forskyvninger',
'fileInfo.protocol' => 'Protokoll',
'fileInfo.mediaType' => 'Mediatype',
_ => null,
} ?? switch (path) {
'fileInfo.sourceKind' => 'Kildetype',
'fileInfo.optimizedVersion' => 'Optimalisert versjon',
'fileInfo.optimizationTarget' => 'Optimaliseringsmål',
@@ -3288,8 +3475,6 @@ extension on TranslationsNb {
'fileInfo.flagOriginal' => 'Original',
'fileInfo.channelsMono' => 'Mono',
'fileInfo.dolbyVisionProfile' => ({required Object profile}) => 'Profil ${profile}',
_ => null,
} ?? switch (path) {
'mediaMenu.markAsWatched' => 'Merk som sett',
'mediaMenu.markAsUnwatched' => 'Merk som usett',
'mediaMenu.removeFromContinueWatching' => 'Fjern fra Fortsett å se',
@@ -3624,6 +3809,63 @@ extension on TranslationsNb {
'connections.signInAgain' => 'Logg inn igjen',
'connections.editMediaBrowserTitle' => ({required Object product}) => 'Rediger ${product}-tilkobling',
'connections.editMediaBrowserIntro' => ({required Object serverName}) => 'Legg til eller fjern URL-er for ${serverName}. Plezy vil bruke den nåbare URL-en med lavest ventetid.',
'accountPreferences.sectionTitle' => 'Kontoinnstillinger',
'accountPreferences.hubSubtitleSingle' => ({required Object account}) => 'Lyd-, undertekst- og bibliotekalternativer lagret på ${account}',
'accountPreferences.hubSubtitleMultiple' => ({required Object count}) => 'Lyd-, undertekst- og bibliotekalternativer lagret på ${count} kontoer',
'accountPreferences.pickAccount' => 'Hver konto lagrer sine egne innstillinger. Velg hvilken du vil redigere.',
'accountPreferences.storedOnAccount' => 'Disse alternativene lagres på selve kontoen, så alle apper som er logget på den, bruker dem — inkludert Plezy på de andre enhetene dine.',
'accountPreferences.noAccounts' => 'Ingen kontoer å konfigurere',
'accountPreferences.noAccountsHint' => 'Logg inn på Plex, eller koble til en Jellyfin- eller Emby-server, så vises innstillingene som er lagret på den kontoen, her.',
'accountPreferences.unavailable' => 'Kan ikke nå denne kontoen',
'accountPreferences.loadFailed' => 'Kunne ikke laste inn disse innstillingene',
'accountPreferences.noPreference' => 'Ingen preferanse',
'accountPreferences.notSet' => 'Ikke angitt',
'accountPreferences.groups.audioAndSubtitles' => 'Lyd og undertekster',
'accountPreferences.groups.libraryDisplay' => 'Bibliotek',
'accountPreferences.groups.personalMedia' => 'Personlige medier',
'accountPreferences.preferredAudioLanguage' => 'Foretrukket lydspråk',
'accountPreferences.autoSelectAudio' => 'Velg lyd etter språk',
'accountPreferences.autoSelectAudioDescription' => 'Av beholder det lydsporet filen markerer som standard.',
'accountPreferences.preferredSubtitleLanguage' => 'Foretrukket undertekstspråk',
'accountPreferences.subtitleMode' => 'Slå på undertekster',
'accountPreferences.subtitleModes.none' => 'Manuelt valgt',
'accountPreferences.subtitleModes.noneDescription' => 'Slå aldri på undertekster av seg selv.',
'accountPreferences.subtitleModes.defaultMode' => 'Følg sporflaggene',
'accountPreferences.subtitleModes.defaultModeDescription' => 'Bruk standard- og tvunget-flaggene som er lagret på hvert undertekstspor.',
'accountPreferences.subtitleModes.always' => 'Alltid aktivert',
'accountPreferences.subtitleModes.alwaysDescription' => 'Slå på et undertekstspor på foretrukket språk når det finnes et.',
'accountPreferences.subtitleModes.onlyForced' => 'Kun tvungne undertekster',
'accountPreferences.subtitleModes.onlyForcedDescription' => 'Last inn bare sporene som er merket som tvungne.',
'accountPreferences.subtitleModes.smart' => 'Vist med fremmedspråklig lyd',
'accountPreferences.subtitleModes.smartDescription' => 'Slå på undertekster bare når lyden er på et annet språk.',
'accountPreferences.subtitleAccessibility' => 'SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.preferNonSdh' => 'Foretrekk ikke-SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.preferSdh' => 'Foretrekk SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.onlySdh' => 'Kun SDH-undertekster',
'accountPreferences.subtitleAccessibilityOptions.onlyNonSdh' => 'Kun ikke-SDH-undertekster',
'accountPreferences.forcedSubtitles' => 'Tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.preferNonForced' => 'Foretrekk ikke-tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.preferForced' => 'Foretrekk tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.onlyForced' => 'Kun tvungne undertekster',
'accountPreferences.forcedSubtitleOptions.onlyNonForced' => 'Kun ikke-tvungne undertekster',
'accountPreferences.displayMissingEpisodes' => 'Vis manglende episoder',
'accountPreferences.displayMissingEpisodesDescription' => 'Vis episoder serveren kjenner til, men som ikke har noen fil.',
'accountPreferences.hidePlayedInLatest' => 'Skjul sette elementer i Siste',
'accountPreferences.hidePlayedInLatestDescription' => 'Hold elementer du allerede har sett utenfor serverens Siste-rader.',
'accountPreferences.displayCollectionsView' => 'Vis samlingsvisningen',
'accountPreferences.displayCollectionsViewDescription' => 'Vis serverens samlingsvisning sammen med bibliotekene dine.',
'accountPreferences.rewatchingInNextUp' => 'Behold serier du ser på nytt i Neste opp',
'accountPreferences.rewatchingInNextUpDescription' => 'Når du er ferdig med en serie og starter den på nytt, følger Neste opp den nye avspillingen i stedet for å fjerne serien.',
'accountPreferences.watchedIndicator' => 'Sett-indikatorer',
'accountPreferences.watchedIndicatorOptions.none' => 'Aldri',
'accountPreferences.watchedIndicatorOptions.moviesAndShows' => 'Filmer og TV-serier',
'accountPreferences.watchedIndicatorOptions.movies' => 'Kun filmer',
'accountPreferences.watchedIndicatorOptions.shows' => 'Kun TV-serier',
'accountPreferences.mediaReviewsVisibility' => 'Vurderinger og anmeldelser',
'accountPreferences.mediaReviewsOptions.usersAndCritics' => 'Brukere og kritikere',
'accountPreferences.mediaReviewsOptions.usersOnly' => 'Kun brukere',
'accountPreferences.mediaReviewsOptions.criticsOnly' => 'Kun kritikere',
'accountPreferences.mediaReviewsOptions.nobody' => 'Skjult',
'discover.title' => 'Oppdag',
'discover.noContentAvailable' => 'Ikke noe innhold tilgjengelig',
'discover.addMediaToLibraries' => 'Legg til medier i bibliotekene dine',
@@ -3719,8 +3961,10 @@ extension on TranslationsNb {
'libraries.filterCategories.genre' => 'Sjanger',
'libraries.filterCategories.year' => 'År',
'libraries.filterCategories.contentRating' => 'Aldersgrense',
'libraries.filterCategories.tag' => 'Tag',
'libraries.filterCategories.tag' => 'Tagg',
'libraries.filterCategories.unwatched' => 'Usette',
_ => null,
} ?? switch (path) {
'libraries.filterCategories.unplayed' => 'Ikke avspilt',
'libraries.filterCategories.favorites' => 'Favoritter',
'libraries.sortLabels.title' => 'Tittel',
@@ -3802,8 +4046,6 @@ extension on TranslationsNb {
'explore.rows.watchlist' => 'Ønskeliste',
'explore.rows.recommendedMovies' => 'Anbefalte filmer',
'explore.rows.recommendedShows' => 'Anbefalte serier',
_ => null,
} ?? switch (path) {
'explore.rows.trendingMovies' => 'Populære filmer nå',
'explore.rows.trendingShows' => 'Populære serier nå',
'explore.rows.popularMovies' => 'Populære filmer',
@@ -3956,6 +4198,11 @@ extension on TranslationsNb {
'liveTv.unknownChannel' => 'Ukjent kanal',
'liveTv.live' => 'DIREKTE',
'liveTv.reloadGuide' => 'Last inn programguide på nytt',
'liveTv.searchGuide' => 'Søk i programguiden',
'liveTv.searchHint' => 'Søk etter kanaler og programmer',
'liveTv.searchNoResults' => ({required Object query}) => 'Ingen treff for "${query}"',
'liveTv.channelsSection' => 'Kanaler',
'liveTv.programsSection' => 'Programmer',
'liveTv.now' => 'Nå',
'liveTv.today' => 'I dag',
'liveTv.tomorrow' => 'I morgen',
@@ -4093,6 +4340,8 @@ extension on TranslationsNb {
'music.repeatAll' => 'Gjenta alle',
'music.repeatOne' => 'Gjenta ett spor',
'music.instantMixNoServer' => 'Ingen server er tilgjengelig for en hurtigmiks',
'music.instantMixFailed' => 'Kunne ikke laste inn direktemiksen',
'music.instantMixEmpty' => 'Direktemiksen ga ingen spor',
'music.noAudioUrl' => ({required Object track}) => 'Ingen lyd-URL er tilgjengelig for ${track}',
'music.discography.singlesAndEps' => 'Singler og EP-er',
'music.discography.live' => 'Live',
@@ -4228,6 +4477,8 @@ extension on TranslationsNb {
'downloads.syncRuleCleanupUnavailable' => 'Tilknyttede nedlastinger kunne ikke identifiseres på en trygg måte. Koble til serveren på nytt og prøv igjen, eller fjern regelen uten å slette nedlastingene.',
'downloads.syncedNewEpisodes' => ({required Object count, required Object title}) => 'Synkroniserte ${count} nye episoder for ${title}',
'downloads.activeSyncRules' => 'Synkroniseringsregler',
_ => null,
} ?? switch (path) {
'downloads.noSyncRules' => 'Ingen synkroniseringsregler',
'downloads.manageSyncRule' => 'Administrer synkronisering',
'downloads.editEpisodeCount' => 'Antall episoder',
@@ -4316,8 +4567,6 @@ extension on TranslationsNb {
'companionRemote.pairing.availableDevices' => 'Tilgjengelige enheter',
'companionRemote.pairing.manualConnection' => 'Manuell tilkobling',
'companionRemote.pairing.cryptoInitFailed' => 'Kunne ikke starte sikker tilkobling. Logg inn på Plex først.',
_ => null,
} ?? switch (path) {
'companionRemote.pairing.validationHostRequired' => 'Vennligst oppgi vertsadresse',
'companionRemote.pairing.validationHostFormat' => 'Format må være IP:port (f.eks. 192.168.1.100:48632)',
'companionRemote.pairing.connectionTimedOut' => 'Tilkoblingen fikk tidsavbrudd. Bruk samme nettverk på begge enheter.',
@@ -4577,6 +4826,8 @@ extension on TranslationsNb {
'seerr.statusBlocklisted' => 'På blokkeringslisten',
'seerr.couldNotReach' => ({required Object url, required Object error}) => 'Kunne ikke nå ${url}: ${error}',
'seerr.noInstanceAtUrl' => ({required Object url, required Object status}) => 'Ingen Seerr-instans på ${url} (HTTP ${status})',
'seerr.invalidUrl' => 'Skriv inn en serveradresse som https://seerr.example.com',
'seerr.quickConnectUnsupported' => 'Denne Seerr-instansen støtter ikke Quick Connect. Den krever Seerr 3.4 eller nyere.',
'seerr.notInitialized' => 'Denne Seerr-instansen har ikke fullført førstegangsoppsettet',
'seerr.noPlexTokenForReauth' => 'Ingen Plex-token er tilgjengelig for å logge inn på nytt',
'seerr.noStoredCredentials' => 'Ingen lagret påloggingsinformasjon er tilgjengelig for å logge inn på nytt',
@@ -4587,6 +4838,7 @@ extension on TranslationsNb {
'seerr.sessionRejectedAfterReauth' => 'Økten ble avvist etter ny innlogging',
'services.title' => 'Tjenester',
'services.hubSubtitle' => 'Synkroniser fremdrift og forespør nye titler.',
'services.integrations' => 'Integrasjoner',
'services.notConnected' => 'Ikke tilkoblet',
'services.connectedAs' => ({required Object username}) => 'Tilkoblet som @${username}',
'services.scrobble' => 'Registrer fremdrift automatisk',

Some files were not shown because too many files have changed in this diff Show More