Files
psysonic/src/locales/ro/settings.ts
T
cucadmuh 5bf2441ccf feat(library): local library index and search (preview) (#846)
* feat(library): scaffold psysonic-library crate with v1 schema and store (#791)

Adds a new workspace crate that will host the unified track store and the
upcoming sync engine. PR-1a covers spec phases A1–A6:

- migrations/001_initial.sql: full v1 schema — sync_state, track, album,
  artist, track_fts (+ ai/ad/au triggers), track_extension, track_offline,
  track_id_history, track_fact, track_artifact, canonical_track,
  canonical_identity, track_canonical_link, canonical_enrichment_link, and
  all §5.2 partial indexes.
- store::LibraryStore: WAL + foreign_keys=ON SQLite connection rooted at
  app_data_dir/library.sqlite (distinct from the analysis cache, which
  uses app_config_dir). schema_migrations table + idempotent embedded
  migration runner; LIBRARY_DB_SCHEMA_VERSION = 1.
- repos::TrackRepository::upsert_batch: 35-column transactional upsert
  with ON CONFLICT(server_id, id) all-fields rewrite; FTS rows follow via
  the triggers.
- search::search_tracks: minimal bm25-ordered FTS5 helper scoped to a
  single server_id, filtering deleted rows.
- filter::FilterFieldRegistry: static v1 registry (text, genre, year,
  starred = V1; bpm = SchemaV1UiLater; user_rating/suffix/bit_rate =
  Planned). Entity routing is a silent skip per §5.13.3.

No Tauri commands, no frontend, no sync — those land in PR-2..PR-7.

PR-1b will follow with the migration-runner edge-case tests, the
initial_sync_cursor_json read/write API, and the breaking-migration hook
stub (P22).

* feat(library): A7 migration-runner safety net + initial-sync cursor API (PR-1b) (#792)

* feat(library): wire migration-runner safety net and initial-sync cursor API

PR-1b — Phase A7 infrastructure on top of PR-1a. Production behaviour
is unchanged at v1 launch; everything here is plumbing that PR-3 will
consume.

- store::run_migrations_with: testable entry point that takes an explicit
  migration slice, a min-compatible-version threshold, and a breaking-bump
  hook. The prod `run_migrations` fixes those to MIGRATIONS,
  LIBRARY_DB_MIN_COMPATIBLE_VERSION, and the no-op stub. The slice is now
  sorted defensively before applying.
- store::LIBRARY_DB_MIN_COMPATIBLE_VERSION: new public constant (currently
  equal to LIBRARY_DB_SCHEMA_VERSION). When a future release needs to
  invalidate v1 data, bumping this above the max applied version trips
  the hook on next open per spec §5.7 / P22.
- store::MigrationOutcome (Applied | BreakingBump): crate-internal signal
  callers can branch on. PR-1b consumers ignore it; PR-3 / Settings will
  surface the "library rebuilt after update" toast when it surfaces.
- store::handle_breaking_schema_bump: documented no-op stub. The drop +
  resync logic lands with the first real breaking bump.
- repos::SyncStateRepository: ensure(server_id, scope) idempotently
  inserts a default row; get_initial_sync_cursor / set_initial_sync_cursor
  read and write sync_state.initial_sync_cursor_json via
  serde_json::Value. The set uses ON CONFLICT … DO UPDATE scoped to the
  cursor column only, so phase / poll-stats / tier survive cursor writes
  intact.
- Tests cover: additive 002-style migration preserves prior data
  (spec §5.7 explicit integration test), runner sorts an unsorted source
  slice, breaking-bump hook fires when max applied < min_compatible,
  hook does not fire on a fresh DB, cursor round-trips a nested
  serde_json::Value, ON CONFLICT preserves sibling columns, library_scope
  separates rows per server.

End-to-end "kill mid-500k-sync → resume same cursor" stays out of scope
per the kickoff answer — it belongs to PR-3 / C2 where the
InitialSyncRunner lives.

* test(library): cover AC A3 — 500-row upsert_batch under perf budget

* feat(library): Subsonic REST client for the sync engine (Phase B, PR-2) (#793)

Phase B (B1-B9 per spec §10) — pure-Rust Subsonic client that the
library-sync engine (PR-3) will drive. No Tauri commands, no events;
the surface is added internally to psysonic-integration as a sibling
of the existing navidrome native-REST module.

- B1 — SubsonicClient + ping over /rest/{method}.view. Auth via the
  legacy salted-md5 token (spec v1.13+, advertised as 1.16.1). New
  SubsonicCredentials helper computes token = md5(password || salt)
  and ships a per-process unique salt nonce so back-to-back calls
  don't repeat.
- B2 — get_scan_status → ScanStatus { scanning, count, folder_count,
  last_scan }. Lightweight poll for the Huge-tier path (§6.2.2).
- B3 — get_album_list2(type, size, offset, musicFolderId?) +
  get_album(id). The two-call pattern the sync engine walks during
  initial ingest (§6.3).
- B4 — search3(query, songCount, songOffset, musicFolderId?). Empty
  query → all songs paged (Navidrome quirk, spec §2.4).
- B5 — get_indexes(musicFolderId?, ifModifiedSince?). Conditional
  fetch for file-tree fallback (S3 / §3.1).
- B6 — get_song(id). Error code 70 maps to the dedicated
  SubsonicError::NotFound variant so the tombstone reconciler can
  match on the variant instead of parsing strings.
- B8 — get_artists(musicFolderId?). ID3-path artist index; clients
  compare ArtistIndex.last_modified_ms against the local watermark
  to decide if a delta pass is needed (§2.2.1).
- B9 — fingerprint_sample helper picks every-Nth track id for the
  server-fingerprint verify pass. Sampling is deterministic so
  reruns probe the same tracks. The verify-and-compare glue itself
  is library-side (PR-3 territory, deps on the store).

Tests cover envelope parsing (status=ok/failed, code 70 → NotFound,
missing body key), credentials (md5 vectors, salt uniqueness across
1k rapid calls, salt differs per from_password call), each endpoint
end-to-end through wiremock with query-param matchers, OpenSubsonic
forward-compat (unknown fields ignored on Song), and the trailing-slash
base-URL normalisation.

Cargo.toml — adds query + form + multipart to psysonic-integration's
reqwest feature set. PR-2's client needs `query`; the other two were
already used by existing navidrome::covers / remote::lastfm code and
only worked via top-crate feature unification. Aligning the crate's
own deps means `cargo test -p psysonic-integration` now compiles
without depending on the workspace build.

Out of scope: capability detection (C1 / PR-3), Navidrome native
bulk path (uses existing psysonic-integration::navidrome::queries),
fixtures harness expansion (G1).

* feat(library): subsonic client follow-ups from PR-2 review (PR-2b) (#794)

Picks up the three non-blocking items from cucadmuh's PR-793 review
(handoffs/2026-05-19-pr-793-review.md) before PR-3 starts on top.

- Fresh `(token, salt)` per request. `SubsonicClient` now caches the
  plaintext username + password and derives a new `SubsonicCredentials`
  inside `send()` for every endpoint call — matches the frontend's
  `subsonicClient.ts` `getAuthParams()` lifecycle and follows Subsonic
  replay-resistance guidance. Test path keeps a `with_static_credentials`
  constructor so wiremock matchers stay deterministic. New
  `build_credentials` (`pub(crate)`) routes the two modes.
- `SUBSONIC_CLIENT_ID` now carries the crate version
  (`psysonic/<CARGO_PKG_VERSION>`) — aligns with the frontend's
  `psysonic/${version}` so Navidrome log lines correlate across the
  WebView and Rust sync paths.
- `Song.mbid_recording` gains the `musicBrainzId` serde alias (plus the
  schema-column spelling) so the OpenSubsonic field lands on the same
  hot column the §5.1 schema names. P13 strong-key matching can now key
  off it on ingest.
- `get_song_with_raw` / `get_album_with_raw` return both the typed
  projection and the raw `serde_json::Value` body sub-tree. PR-3 ingest
  will write that raw value verbatim into `track.raw_json`, so
  OpenSubsonic extensions (`contributors`, `replayGain`, future fields)
  survive without manual field mirroring. Internal `parse_envelope_body`
  extracts the validation + body-key lookup once; `parse_envelope` and
  the new `parse_envelope_with_raw` share it.

Tests cover: `from_password` produces unique salt/token across two
back-to-back calls (direct + over-the-wire via wiremock
`received_requests`), static mode returns the same triple,
`c` query param starts with `psysonic/` and equals `SUBSONIC_CLIENT_ID`,
`get_song_with_raw` preserves untyped fields (`replayGain`,
`contributors`) in the raw value, `get_album_with_raw` keeps per-track
extensions in `raw.song[i]`, error 70 still maps to `NotFound` on the
raw variant, and `Song` deserializes `musicBrainzId` and
`mbid_recording` interchangeably.

B9 fingerprint-verify glue and the wider raw-ingest call sites stay
with PR-3 / C2 as the review's §5 / §7 checklist directs.

* feat(library): capability probe + sync_state accessors (Phase C1+C7, PR-3a) (#795)

First sub-PR of Phase C (sync orchestrator). Lands the foundation that
PR-3b's InitialSyncRunner consumes — pure plumbing, no runners or
background tasks yet.

- C1 capability probe. `psysonic_library::sync::CapabilityProbe::run`
  drives the §6.1 probe chain: Subsonic ping (captures `ServerInfo`
  envelope metadata for server-type / OpenSubsonic detection), then
  best-effort probes for search3 / getScanStatus / getIndexes, plus an
  optional Navidrome native bulk probe (caller passes
  `NavidromeProbeCredentials`). `CapabilityFlags(u32)` matches the
  §6.1.1 bitfield: NavidromeNativeBulk / SubsonicSearch3Bulk /
  ScanStatusAvailable / OpenSubsonic / UnstableTrackIds / FileTreeBrowse.
- C7 sync_state accessors. `SyncStateRepository` gains get/set
  capability_flags, get/set sync_phase (idle / probing / initial_sync
  / ready / error), and column-scoped setters for server_last_scan_iso,
  indexes_last_modified_ms, artists_last_modified_ms, library_tier.
  Every setter uses `ON CONFLICT … DO UPDATE` scoped to its own column
  so concurrent watermark writes don't clobber each other.
- Supporting additions in `psysonic-integration`:
  - `subsonic::SubsonicClient::server_info()` extracts `ServerInfo`
    from the ping envelope (server_type, server_version, api_version,
    open_subsonic). Re-uses `send()` so auth lifecycle is the same.
  - `navidrome::probe::native_bulk_available(url, token)` does the
    `GET /api/song?_start=0&_end=1` Bearer-auth probe. Returns
    Ok(true) on 2xx, Ok(false) on 4xx (auth ok but endpoint missing),
    Err on 5xx. Probe-only — full nd_list_songs port is PR-3b.
- `psysonic-library/Cargo.toml` gains a `psysonic-integration`
  dependency (sync calls into Subsonic + Navidrome probes). DAG stays
  acyclic: integration does not depend on library.

Per cucadmuh's PR-3 kickoff answer (handoff `2026-05-19-pr3-kickoff.md`):
- Crate placement: option A — sync lives in `psysonic-library/src/sync/`,
  no new psysonic-sync crate.
- N1 gate: probe is `/api/song?_start=0&_end=1` only; `nd_list_artists_by_role`
  is NOT required (Q3 answer + N1 ingest port lands in PR-3b).
- UnstableTrackIds: set for Navidrome via `ServerInfo.server_type`,
  cleared for generic Subsonic.

Tests added: 23 across library/sync, library/repos/sync_state,
integration/subsonic, integration/navidrome/probe. Cover bitfield
contains/insert/remove + spec bit values, probe across mixed-capability
servers (full Navidrome, minimal Subsonic, broken endpoints), ping-failure
short-circuit, optional Navidrome creds gating N1, sync_state column-scoped
upserts (capability_flags / sync_phase / watermarks / library_tier),
cross-column independence (capability writes don't reset cursor),
ServerInfo extraction from ping envelope, Navidrome bulk probe across
2xx/4xx/5xx.

* feat(library): InitialSyncRunner + C12 backoff + C13 id remap (Phase C2/C12/C13, PR-3b) (#796)

Second sub-PR of Phase C — wires the actual ingest path on top of
PR-3a's capability + sync_state foundation. Runner is pure async Rust:
PR-3d will spawn it inside a tokio task and emit Tauri progress events
on top.

- C2 InitialSyncRunner. Drives spec §6.3 IS-1 → IS-6: probe-derived
  IngestStrategy (enum N1/S1/S2/S3, selector picks N1 → S1 → S2 chain
  per kickoff Q3), per-page upsert loop, cursor flush after every
  successful batch, IS-4 best-effort getArtists watermark, IS-5
  getScanStatus.lastScan capture, IS-6 phase=ready + cursor cleared.
  Resume is automatic: a non-empty initial_sync_cursor_json restarts
  at the persisted offset; a strategy mismatch between cursor and
  capability flags surfaces as SyncError::CursorIncompatible.
- C12 backoff. sync::backoff::Backoff implements the §6.8 schedule
  (2s → 4s → … cap 120s) with ±25% jitter via deterministic salt.
  retry_with_backoff wraps every endpoint call: transport / Navidrome
  failures retry up to MAX_ATTEMPTS_PER_BATCH (5), the cursor never
  advances on failure, success resets the counter. Cancellation
  AtomicBool is checked between attempts.
- C13 id remap. TrackRepository::upsert_batch_with_remap performs the
  §6.9 detect-and-rebind pass inside the same SQLite transaction as
  the upsert: a content_hash or server_path collision on a different
  existing id triggers UPDATE of child tables (track_offline,
  track_extension, track_fact, track_artifact, track_canonical_link),
  INSERT INTO track_id_history, DELETE old track row. Off when
  UnstableTrackIds is clear (generic Subsonic). New
  TrackIdHistoryRepository read-side helper for forward lookups
  (analysis cache reuse, Phase E).
- IngestStrategy enum + selector (sync::strategy) — N1 → S1 → S2;
  N1 requires Navidrome bearer credentials at runtime (skipped when
  None). S3 is enumerated for future file-tree fallback but returns
  StrategyUnsupported in v1 per kickoff Q3.
- InitialSyncCursor (sync::cursor) — JSON-serialisable
  { strategy, phase, library_scope, ingested_count, strategy_state }.
  StrategyState tagged enum: LinearOffset { offset } for N1/S1,
  AlbumCrawl { album_offset, current_album_id } for S2.
- mapping::subsonic_song_to_track_row + navidrome_song_to_track_row
  centralise the JSON → TrackRow projection. Subsonic path also reads
  replayGain.{trackGain,albumGain} from the raw value so PR-3b doesn't
  drop the columns that PR-2b reserves on TrackRow.
- Supporting bits in psysonic-integration:
  - subsonic types now derive Serialize so the runner can round-trip
    a typed Song back into raw JSON when feeding upsert.
  - navidrome::queries gains nd_list_songs_internal — pure async
    function (no #[tauri::command] decorator) that the N1 ingest
    loop calls directly. The existing Tauri command wraps it.

Tests added across sync::* and repos::track_id_history. Wiremock
covers S1 happy-path, mid-cursor resume from a persisted offset,
strategy mismatch → CursorIncompatible, 503 transient → retry-then-
succeed, AtomicBool cancellation → Cancelled, N1 paginated /api/song
ingest, S2 album crawl, and §6.9 remap firing under UnstableTrackIds
during an actual sync. Backoff schedule + jitter formula pinned.
TrackRepository remap path covered by content_hash collision,
server_path collision, hash+path-missing skip, identity-noop, and
remap-off compatibility with the existing upsert_batch contract.

Also fixes cucadmuh's PR-3a review minor 1: drops the dead
`mount_ok` scaffolding from sync::capability tests.

Out of scope per kickoff Q2:
- DeltaSyncRunner + tombstones → PR-3c
- Background task lifecycle, cancellation wiring, progress emit
  throttle, adaptive scheduler, request budget, bandwidth lane → PR-3d
- Tauri command surface for "sync now" / progress events → PR-5

* feat(library): search3 raw envelope fidelity for S1 ingest (PR-3b follow-up) (#797)

Picks up cucadmuh's PR-3b review minor 1: the S1 path in
InitialSyncRunner was reserialising the typed `Song` for
`track.raw_json`, dropping unknown OpenSubsonic extensions
(`replayGain`, `contributors`, …). N1 and S2 already carry the raw
sub-tree verbatim through `nd_list_songs_internal` and
`get_album_with_raw`; S1 now matches via the new
`SubsonicClient::search3_with_raw` mirror of the PR-2b pattern.

- subsonic::SubsonicClient::search3_with_raw — returns
  `(SearchResult, serde_json::Value)`; uses the existing
  `parse_envelope_with_raw` so error 70 / `Api { code, .. }` mapping
  stays consistent.
- sync::initial::run_s1 now calls `search3_with_raw` and feeds the
  per-song raw sub-tree (`raw_body.song[i]`) into
  `subsonic_song_to_track_row` instead of a typed reserialise.

Tests cover `search3_with_raw` round-trip on a payload with
`replayGain` + `contributors` (verifies the raw value preserves both)
and the empty-result case where the body is `searchResult3: {}`.
Plus an end-to-end S1 ingest test that asserts the persisted
`track.raw_json` column contains the OpenSubsonic extensions after a
full runner pass, and that `replay_gain_track_db` / `_album_db` still
land on the typed columns via the mapping helper.

Full review: psysonic-workdocs/internal/collaboration/handoffs/2026-05-19-pr-796-review.md

* feat(library): DeltaSyncRunner + TombstoneReconciler (Phase C3/C4, PR-3c) (#798)

Third sub-PR of Phase C — drives targeted delta passes on top of
PR-3a/b's foundation. Pure async; PR-3d will spawn it inside the
background scheduler.

- C3 DeltaSyncRunner. Walks spec §6.4 DS-0 … DS-9:
  - DS-0/1/2/3 cheap probe via `getArtists` (small/medium tier) or
    `getScanStatus` (huge tier when `ScanStatusAvailable`). Server
    watermark match → up_to_date short-circuit, scan-in-progress →
    deferred_scanning report; zero further requests in either case.
  - DS-4 targeted ingest. Strategy from capability_flags: N1-delta
    when NavidromeNativeBulk is set, otherwise S2-delta. S1 has no
    delta semantic so it's not used here.
    - N1-delta: GET /api/song _sort=updated_at _order=DESC, pages
      until rows fall under the local `MAX(server_updated_at)`
      watermark; out-of-band rows in the same page are dropped.
    - S2-delta: getAlbumList2 type=newest then type=recent, up to
      a small page cap; getAlbum is fetched only for album_ids the
      local store doesn't already have. Known albums are skipped
      so a play-bump under "recent" doesn't re-ingest the whole
      tracklist.
  - DS-6 id remap reuses TrackRepository::upsert_batch_with_remap.
  - DS-9 stamps next watermark (artists_last_modified_ms or
    server_last_scan_iso) + last_delta_sync_at.
  - DS-5 canonical matcher (Phase H) and DS-7 starred delta are out
    of scope for PR-3c.
- C4 TombstoneReconciler. Caller-driven streaming: each
  `reconcile_chunk(budget)` picks the next `budget` ids ordered by
  synced_at ASC, calls getSong, marks deleted=1 on code 70, and
  refreshes synced_at on every checked id so the queue rotates.
  Mode A (manual integrity) loops until checked == 0; Mode B
  (auto-threshold) tests `should_auto_reconcile(local, server, pct)`
  per delta tick and runs a small budgeted chunk. Memory bounded —
  no full local-id list ever held in RAM.

- SyncStateRepository: new getters for artists_last_modified_ms,
  server_last_scan_iso, library_tier; new
  set_last_delta_sync_at stamp helper. All column-scoped upserts
  preserve neighbouring fields.

Tests cover DS-2 short-circuit (watermark match), DS-3 defer
(scanning=true), N1-delta watermark cutoff (3 fresh + 2 stale rows →
only 3 upserted), S2-delta known-album skip (mock 404 on al_known
guards the assertion), DS-9 watermark + last_delta stamping,
should_auto_reconcile threshold cases (gap, tolerance, server=0,
local<=server), reconcile_chunk code-70 → deleted=1, budget +
ordering (oldest first, newest untouched), empty-store noop, and
cancellation.

PR-3d (background task, probe→flags wiring, progress emit, adaptive
scheduler, request budget, bandwidth throttle) lands next on the
same integration branch.

* feat(library): sync supervisor + progress channel + DS-8 wiring (Phase C5/C6, PR-3d1) (#799)

First half of PR-3d (cucadmuh-approved split per kickoff Q2).
Pure-Rust lifecycle + progress infrastructure on top of the runners
from PR-3a/b/c. Tauri events stay in the top crate (PR-5); this PR
only ships the channel the top crate will subscribe to.

- C5 SyncSupervisor. Spawns a sync workload inside a tokio task,
  owns the cancellation AtomicBool, and exposes a single-consumer
  mpsc receiver for ProgressEvent. join() returns the inner
  Result<(), SyncError>; panics surface as Storage so callers
  never need to know about tokio internals.
- C6 progress channel. New sync::progress module:
  - ProgressEvent enum — lean variants
    (PhaseChanged / IngestPage / Remapped / Tombstoned /
    Completed / Error). Server / scope context lives on the
    channel side (one supervisor = one scope).
  - Progress trait + NoopProgress default + ChannelProgress
    forwarding through tokio mpsc. Throttle is the simple
    last-emit-timestamp gate; terminal events (Completed /
    Error) bypass it.
- InitialSyncRunner + DeltaSyncRunner gain with_progress(...)
  builders. IS-1 / IS-6 emit PhaseChanged + Completed; delta
  emits PhaseChanged at strategy pick, Tombstoned at DS-8, and
  Completed at DS-9. Defaults to NoopProgress so existing call
  sites keep working.
- DS-8 wired. DeltaSyncRunner::with_tombstone_budget(n) drives
  TombstoneReconciler::reconcile_chunk(n) after DS-4 ingest;
  shares the runner's cancellation flag + sleep override. The
  DeltaSyncReport gains tombstones_checked / tombstones_deleted
  so callers can act on the counts.
- capability::probe_and_persist helper. Chains
  CapabilityProbe::run with sync_state writes: sets phase to
  "probing" before the probe, persists capability_flags, then
  drops back to "idle". PR-3d2 (the scheduler) will call this in
  front of every initial / delta run so the stored flags reflect
  the live server.

Tests cover: ChannelProgress throttle (zero-interval pass-through,
terminal bypass, non-terminal collapse, sender alive after
receiver drop), SyncSupervisor task completion + cancel +
panic-as-Storage + receiver-take-once, probe_and_persist
round-trip through SyncStateRepository (flags persisted, phase ends
at "idle"), DS-8 reconcile-after-ingest landing tombstones on
code 70 returns.

PR-3d2 follows with the adaptive scheduler (C8), request budget
(C9), poll EWMA (C10), and the bandwidth / queue priority lane
(C11).

* feat(library): adaptive scheduler + request budget + EWMA poll + bandwidth (Phase C8/C9/C10/C11, PR-3d2) (#800)

Second half of PR-3d per cucadmuh's kickoff-Q2 split. Wraps the
runners + supervisor from PR-3a/b/c/d1 into a tick-driven background
scheduler. Top crate (PR-5) plumbs the timer.

- C8 BackgroundScheduler. Tick-based — caller drives the interval,
  scheduler decides whether the tick should run. is_due(now_ms)
  checks sync_state.next_poll_at; tick(now_ms) either skips
  (not due / PrefetchActive pause), or runs a DeltaSyncRunner with
  the right budget + tombstone trigger, then stamps the next
  poll_at via the adaptive formula. No tokio task ownership —
  tests stay deterministic, PR-5 plugs spawn behaviour to taste.
- C9 RequestBudget. PassKind enum (PollTick / DeltaLight /
  DeltaMismatch / InitialSync) with caps per spec §6.2.5
  (1 / 50 / 200 / unlimited). RequestBudget::has_room(used) gates
  the runner; PR-3d2 ships the data type, runner enforcement of
  the cap is a future tightening (DeltaSyncRunner already has its
  own page cap so the soft cap mostly informs Settings).
- C10 PollStats EWMA. New sync::poll_stats with PollStats
  (artist_count, ewma_bytes, ewma_duration_ms, library_tier),
  observe()/set_artist_count()/reclassify() helpers, the §6.2.2
  tier table (<2k / 2k-15k / >15k or ewma_bytes >2MB), and
  next_interval_ms following the spec formula
  (base * load_factor * artist_factor, load_factor clamped
  [1, 10]).
- C11 PlaybackHint + ParallelismBudget. PlaybackHint enum
  (Idle / Playing / PrefetchActive) resolved to a
  ParallelismBudget { max_concurrent, min_request_gap_ms }.
  PrefetchActive pauses bulk (`max_concurrent = 0`) per
  §6.2.4; the scheduler honours it via tick short-circuit.
- Auto-tombstone wire. Before running the DeltaSyncRunner the
  scheduler tests `should_auto_reconcile(local, server, pct)`
  against the persisted counts; on threshold trip it sets
  `with_tombstone_budget(200)` (the §6.2.5 DeltaMismatch cap).
- SyncStateRepository gains poll_stats_json get/set,
  next_poll_at get/set, local_track_count get/set, and
  server_track_count get/set — all column-scoped upserts.

Tests: ~30 new across poll_stats / budget / bandwidth / scheduler.
EWMA seed + smoothing, tier-classification edges (artist + size
overrides), next-interval formula bounds (idle base, slow-network
load_factor clamp), RequestBudget caps per pass, ParallelismBudget
resolution, scheduler is_due (no schedule / future schedule),
tick short-circuit (not due, PrefetchActive pause), tick runs
delta and persists next_poll_at, auto-tombstone trigger above
5 % threshold, PollStats round-trip through SQLite.

Together with PR-3d1 this finishes Phase C — Tauri command surface
(D1-D4) lands with PR-5.

* feat(library): read-only Tauri command surface (Phase D1 part 1, PR-5a) (#801)

First sub-PR of Phase D per cucadmuh's kickoff Q1 split. Lands the
LibraryRuntime Tauri State plus the 8 read-only library commands
from spec §7.1. No SyncSupervisor spawn, no sync lifecycle commands,
no credentials store — those land in PR-5b.

- New psysonic_library::runtime::LibraryRuntime — Tauri State
  wrapping Arc<LibraryStore>. Top crate's lib.rs setup() now calls
  LibraryStore::init(app), wraps the result in the runtime, and
  app.manage's it. Mirrors the AnalysisCache wiring above it.
- New psysonic_library::dto module — camelCase wire DTOs per
  src-tauri/CLAUDE.md: SyncStateDto, LibraryTrackDto (flat
  projection over the track hot columns + raw_json sub-tree),
  LibraryTracksEnvelope, TrackArtifactDto, TrackFactDto,
  OfflinePathDto, TrackRefDto. local_tracks_max_updated_ms helper
  surfaces the implicit N1-delta watermark on the SyncStateDto.
- New psysonic_library::payload module — pure
  ProgressEvent → LibrarySyncProgressPayload mapper (the
  payload Tauri events carry once PR-5b plugs the supervisor's
  mpsc receiver into AppHandle::emit). Constants for the event
  names too. Unit-testable without Tauri runtime.
- New psysonic_library::commands module with 8 #[tauri::command]
  handlers:
  - library_get_status — joins the sync_state row + the
    track-watermark MAX query into one SyncStateDto.
  - library_search — FTS5 via the existing search_tracks helper,
    paginated; hydrates hits to full LibraryTrackDto.
  - library_get_track — single SELECT through new
    TrackRepository::find_one.
  - library_get_tracks_batch — capped at 100 refs/call per spec,
    preserves caller-supplied order, drops unknowns silently.
  - library_get_tracks_by_album — ordered by
    disc/track/id via new TrackRepository::find_by_album.
  - library_get_artifact — flexible WHERE over track_artifact
    (artifact_kind required, source/format optional), latest
    fetched_at wins.
  - library_get_facts — fact_kinds filter optional;
    returns all rows for the (server_id, track_id) pair when
    none specified, sorted by fact_kind + fetched_at DESC.
  - library_get_offline_path — returns local_path with a
    `missing: true` flag when the row is absent.
- TrackRepository gains find_one / find_batch / find_by_album
  with a shared row-to-TrackRow mapper. SQL constants pinned next
  to the existing UPSERT_SQL so a schema change touches one file.
- src-tauri/src/lib.rs: LibraryStore::init in setup(), the eight
  command handlers added to invoke_handler!.

Tests cover: DTO field-name camelCase (IPC contract guard),
LibraryTrackDto round-trip through TrackRow, raw_json fallback to
Value::Null on bad input, local_tracks_max_updated_ms ignores
deleted rows, TrackRepository::find_one / find_batch / find_by_album
ordering + unknown-ref drop, ProgressEvent mapper across all six
variants + serialization keys camelCase. Library tests at 166;
workspace stays green.

Out of scope per kickoff Q1:
- Mutating commands (library_sync_*, library_patch_*,
  library_put_*, library_purge_*, library_delete_*) → PR-5b
- SyncSupervisor spawn + background scheduler tick loop +
  progress emit → PR-5b
- library_sync_bind_session / clear_session credentials → PR-5b
- TS wrappers + Settings UI + server-remove modal → PR-5c
- library_advanced_search / library_search_cross_server SQL
  builders → PR-5d

* feat(library): sync lifecycle + mutate + purge Tauri surface (Phase D1 part 2, PR-5b) (#802)

Second sub-PR of Phase D per cucadmuh's kickoff Q1 split. Adds the
mutating side of §7.1 plus the SyncSession credentials store, the
PlaybackHint setter, the orchestrator that runs InitialSyncRunner /
DeltaSyncRunner under a Tauri AppHandle and emits library:sync-progress
and library:sync-idle events, and the top-crate scheduler tick task
that sweeps every bound session through BackgroundScheduler::tick.

- LibraryRuntime extended per kickoff Q2: sync_sessions HashMap,
  playback_hint cell, current_job (cancel handle + identity), and
  scheduler_cancel flag the tick task watches. Kickoff sketch said
  Mutex<Option<SyncSupervisor>> — supervisor's join() consumes self,
  so holding it in the mutex would block library_sync_cancel behind
  the orchestrator's join; CurrentJob carries the Arc<AtomicBool>
  cancel + metadata instead, orchestrator task owns supervisor /
  receiver / join.
- New commands (spec §7.1):
  - library_sync_bind_session — caches Subsonic creds in memory,
    tries navidrome_token once for bearer cache, runs
    probe_and_persist so capability_flags reflect the live server.
  - library_sync_clear_session — drops cached credentials.
  - library_set_playback_hint — JS pushes idle / playing /
    prefetch_active from existing audio listeners.
  - library_sync_start — dispatches InitialSyncRunner (mode='full')
    or DeltaSyncRunner (mode='delta', with auto-tombstone budget
    when local/server count gap exceeds threshold). Spawns runner
    + orchestrator task that drains the progress mpsc into
    library:sync-progress emits and emits library:sync-idle when
    the runner exits.
  - library_sync_cancel — trips the current job's cancel flag.
  - library_patch_track — sparse JSON patch (starredAt, userRating,
    playCount, playedAt) per §6.5.
  - library_put_artifact / library_put_fact — upserts with
    ON CONFLICT scoped to the PK so lyrics / BPM writes survive
    re-fetches.
  - library_purge_server — transactional DELETE across the v1
    schema tables for this server_id. include_offline (default
    false) controls track_offline + bytes_freed.
  - library_delete_server_data — alias that always purges offline
    too (logout flow).
- src-tauri/src/lib.rs setup() spawns a 30 s
  MissedTickBehavior::Skip task that snapshots bound sessions and
  drives BackgroundScheduler::tick(now_ms) for each. Honours
  runtime.scheduler_cancel + the current PlaybackHint. Background
  ticks stay silent (NoopProgress) — Tauri emit for the
  scheduler path lands when Settings (PR-5c) surfaces it.
- psysonic-integration::navidrome re-exports navidrome_token so the
  bind_session command can drive the bearer cache without making
  the client module pub.

Tests cover: LibraryRuntime session round-trip (set/get/clear
scopes per server), playback_hint default + setter, snapshot
returns clones so callers can mutate freely. Existing library tests
stay green (171 → 171; new code paths under the Tauri command
surface — devtools integration smoke is PR-5c's job).

Out of scope per kickoff Q1:
- src/library/ TS wrappers + Settings UI subsection + server-remove
  modal → PR-5c
- library_advanced_search / library_search_cross_server SQL
  builders → PR-5d
- Background-tick Tauri emit (NoopProgress today) → PR-5c
- analysis_cache cross-purge in library_purge_server → PR-6

* feat(library): typed invoke wrappers + verify_integrity command (Phase D2 + part of D1, PR-5c) (#803)

Frontend-facing slice of Phase D. Ships the typed src/api/library.ts
wrapper layer that any Settings / browse code will import from, plus
the manual-integrity backend command PR-5b's review §5 note 2 called
out as missing.

Scope cut from cucadmuh's PR-5 kickoff Q1 split: that proposal had
PR-5c = D2 + D3 + D4 (wrappers + Settings subsection + server-remove
modal). The Settings UI + server-remove + audio playback-hint
wiring + authStore extensions + i18n strings turn into a thick frontend
patch in their own right; landing them in one PR with the wrappers
would mix Tauri-surface review with Settings UX review. The split:

- PR-5c (this PR) — D2 wrappers + library_sync_verify_integrity.
- PR-5c-ui (follow-up) — D3 Library Settings subsection, D4
  server-remove modal contract, playback hint feed, authStore /
  i18n.

Per kickoff exit clause ("Do not split 5c unless review size
forces it"). Reviewable as a clean Tauri-surface vs UX boundary.

- Backend: `library_sync_verify_integrity { serverId, libraryScope? }`
  command — same dispatch shape as `library_sync_start { mode:'delta' }`
  but always forces the full `DELTA_MISMATCH_CAP` tombstone budget
  regardless of the local/server count gap. Spec §6.7 Mode A user-
  initiated full reconcile bypasses the threshold check that
  governs background ticks.
  `library_sync_start` itself is refactored to delegate to a private
  `library_sync_start_inner(force_full_tombstone)` so both entry
  points share the runner-spawn + orchestrator + emit code.

- Frontend `src/api/library.ts`: full typed wrapper layer over the
  19 `library_*` Tauri commands. DTO mirrors carry the camelCase
  wire shape (`SyncStateDto`, `LibraryTrackDto`, `TrackArtifactDto`,
  `TrackFactDto`, `OfflinePathDto`, `PurgeReportDto`, `SyncJobDto`,
  `TrackRefDto`, `ArtifactInputDto`, `FactInputDto`). Plus the
  `LibrarySyncProgressPayload` / `LibrarySyncIdlePayload` interfaces
  and `subscribeLibrarySyncProgress` / `subscribeLibrarySyncIdle`
  helpers that wrap `@tauri-apps/api/event` listen. PlaybackHint
  literal type lives here too (`'idle' | 'playing' | 'prefetch_active'`)
  so the audio listeners in PR-5c-ui can import a single source of
  truth.

- `src-tauri/src/lib.rs` adds the new verify_integrity handler to
  the `invoke_handler!` aggregate.

Tests: library tests stay at 171 — verify_integrity is exercised
through the existing `sync_start_inner` paths; the wrapper layer is
trivial passthrough that TypeScript types already check. Vitest
coverage for the typed wrappers belongs with PR-5c-ui where there
are real consumers (LibraryTab) to drive integration tests.

PR-5c-ui (next) lands:
- Library Settings subsection (§7.3 minus advanced toggles)
- ServerRemoveModal extension (keep vs delete local index per §5.6)
- authStore: libraryIndexEnabledByServer + auto-reconcile toggle
- src/store/audioListenerSetup audio:playing / ended /
  setDeferHotCachePrefetch → library_set_playback_hint
- i18n keys for the new strings

* feat(library): Settings library index UI + playback hint + purge-on-remove (Phase D3/D4, PR-5c-ui) (#804)

* feat(library): Settings library index UI + playback hint + purge-on-remove (Phase D3/D4, PR-5c-ui)

Frontend half of Phase D, on top of PR-5c's typed wrappers. Wires the
Settings → Library subsection (§7.3), the audio playback-hint feed
(§6.2.4), and the server-remove keep-vs-delete choice (§5.6).

- New libraryIndexStore (Zustand, persisted) — per-server enable flag
  + auto-reconcile toggle. Kept out of authStore so the index feature
  evolves independently and the persisted blob stays small.
- New LibraryIndexSection in Settings → Library:
  - Per-server "Enable local library index" toggle → binds /
    clears the Rust sync session with the active server's
    credentials. Off by default (P6).
  - Read-only status (Idle / Checking / Initial sync / Ready (n) /
    Error) polled from library_get_status every 3 s, overlaid with
    live library:sync-progress events.
  - Sync now / Verify integrity / Cancel buttons. Verify runs one
    §6.7 pass (budget 200) per click; the status line shows the
    checked/removed counts so large libraries can be continued with
    another click (auto-resume loop is a follow-up).
  - Auto-reconcile toggle.
  - Subscribes to library:sync-progress + library:sync-idle for the
    active server; errors surface as a toast.
- Audio playback hint: handleAudioPlaying → 'playing',
  handleAudioEnded → 'idle' via notifyLibraryPlaybackHint, which
  gates on the per-server index toggle + dedupes repeated hints so
  the IPC boundary isn't spammed on every progress tick.
- ServersTab delete flow: when a server with an enabled index is
  removed, a second confirm offers keep-vs-delete of the local
  library cache (OK = library_delete_server_data, Cancel = retain
  for offline). Always clears the sync session.
- i18n: en + de keys for the new strings; other locales fall back
  to en via i18next (later sweep).

Per PR-803 review §5: verify-integrity resume UX is one-pass-per-click
with a visible counter; sync_start idempotency (replaces in-flight)
is surfaced via the Cancel button appearing while busy.

Out of scope:
- VirtualSongList / playerStore local-mode consumers → PR-7 (F1/F3/F5)
- library_advanced_search / cross-server UI → PR-5d + PR-7 F2
- Auto-resume loop for very large verify-integrity runs → follow-up
- Search-all-servers + threshold input (advanced §7.3) → later

* fix(library): normalize server base URL before bind probe

The bind toggle threw "subsonic transport: builder error | relative
URL without a base" — `server.url` is stored bare (e.g.
`nas.example.com`) and reqwest needs a scheme. Two-sided fix:

- Frontend: LibraryIndexSection passes `authStore.getBaseUrl()`
  (adds http:// + strips trailing slash) instead of the raw
  `server.url`, matching the existing `subsonic.ts` convention.
- Backend: `library_sync_bind_session` normalizes the incoming
  `base_url` defensively so the stored session + every downstream
  caller (sync_start, scheduler tick, navidrome_token) gets a
  scheme-qualified URL regardless of what the WebView sends.

Tests: normalize_base_url covers bare host, trailing slash, existing
http/https scheme, and whitespace.

* fix(library): re-bind sync session on startup + server switch

"Library sync failed: no bound session" — the per-server index toggle
persists in localStorage but the Rust sync session (credentials +
bearer) lives in process memory and is gone after an app restart, so
the toggle showed "on" while no session existed. Per PR-5 kickoff Q5
("on server connect if index already on").

- New `ensureActiveServerSessionBound()` helper: re-binds the active
  server's session when its index toggle is enabled. Best-effort —
  silent on failure (Settings surfaces the real error on explicit
  toggle).
- MainApp re-binds on every `activeServerId` change (covers app
  startup + server switch — `setActiveServer` drives the effect).
- LibraryIndexSection re-binds on mount before the first status poll,
  so Sync now / Verify integrity work immediately even when the
  toggle was already on from a previous run.

* fix(library): trigger initial full sync on first enable (PR-804 review §5.1)

cucadmuh's PR-804 review flagged this as release-blocking: the toggle
only bound the session and «Sync now» / the background tick ran
delta-only, so a fresh enable left the index empty — delta can't
populate a never-synced library.

- On first enable, after bind, fetch status and dispatch
  `library_sync_start { mode: 'full' }` when `lastFullSyncAt` is null
  (matches spec §6.2 "initial sync always background").
- «Sync now» now picks mode adaptively: `full` until a full sync has
  completed, `delta` afterward — so the button works both for the
  initial population and incremental updates.

Other PR-804 review notes (auto-reconcile toggle → backend wiring,
prefetch_active hint, clear-old-session-on-switch) stay as documented
non-blocking follow-ups.

* feat(library): advanced search + cross-server SQL builders (Phase D-search, PR-5d) (#806)

* feat(library): advanced search + cross-server SQL builders + commands (Phase D-search, PR-5d)

- FilterFieldRegistry SQL resolution: SqlFragment, compare_fragment, validate_for_entity (§5.13.5)
- Advanced Search builder: per-entity track/album/artist queries; genre (case-insensitive), year, starred, bpm filters; bpm dual-storage resolution (§5.13.4); libraryScope; sort allowlist; full-match totals
- Cross-server FTS union (§5.5B / §5.9 A') with canonical-id dedup
- library_advanced_search + library_search_cross_server commands, registered in the shell

* feat(library): typed advanced search / cross-server invoke wrappers (PR-5d)

Mirror request/response DTOs and add libraryAdvancedSearch / librarySearchCrossServer
in src/api/library.ts. UI parity (AdvancedSearch.tsx) stays PR-7.

* fix(library): self-heal stale/unreadable initial-sync cursor instead of bricking (#807)

The initial-sync cursor records the ingest strategy it was created under.
When a re-probe later selects a different strategy (e.g. the Navidrome
native bearer is briefly unavailable, downgrading N1->S2), the cursor guard
returned a hard error — and since nothing clears the cursor, every later
full sync failed with no recovery path.

Reset the stale (or unreadable) cursor and start fresh under the selected
strategy instead of erroring. Re-ingest is idempotent (upsert); the
tombstone pass reconciles leftovers.

* fix(library): emit per-batch progress during initial sync (#808)

The initial-sync runner only emitted PhaseChanged (start) and Completed
(end), so the Settings status sat at "initial_sync" with no count for the
entire ingest — looking stuck on large libraries even while rows landed.

Emit IngestPage per batch from the N1/S1/S2 loops with the running ingested
total; the existing <=2 Hz throttle paces it. The frontend already renders
the count from these events.

* feat(library): Advanced Search reads the local index when ready (Phase F2, PR-7a) (#811)

When the active server's index is fully synced, Advanced Search serves
query / genre / year / result-type from library_advanced_search (instant +
offline) and pages songs locally. On not-ready or any failure it falls back
to the existing network path unchanged (spec 5.13.6). Results map from each
entity's stored Subsonic rawJson, with the flat hot columns as a fallback.

* feat(library): canonical matcher — link tracks by ISRC/MBID on ingest (Phase H1/H2, PR-4a) (#812)

Adds the strong-key cross-server matcher (spec §5.5A): on every track upsert,
link (server_id, track_id) to a canonical id derived from its ISRC (preferred)
or MBID recording. Deterministic id (`{kind}:{value}`) keeps it O(1) and
idempotent — no lookup-then-create race, no fuzzy loop on the bulk path.
Tracks without a strong key stay standalone (fuzzy/search-time matching is H3).

* feat(library): cross-server fuzzy fallback in search (Phase H3, PR-4b) (#813)

library_search_cross_server now returns a `fuzzy` list alongside the exact
FTS `hits` (spec §5.9): per-server `title LIKE %query%` for matches the exact
pass missed (diacritics, partial words), capped per server, excluding exact
hits and deduped by canonical id against them. Shared `like_contains` moved
to the `search` module.

* feat(library): FactRepository with TTL + provenance rules (Phase E4, PR-6a) (#814)

Typed CRUD over track_fact behind library_get_facts / library_put_fact
(spec §5.12): get lazily deletes the track's expired facts then returns the
survivors (no background GC, P34); a `user` bpm fact also writes the hot
track.bpm column so the override wins and survives a resync (R6-3.4). The
commands now delegate here instead of inlining the SQL.

* feat(library): ArtifactRepository with TTL + 512KB cap (Phase E4, PR-6b) (#815)

* fix(integration): decode OpenSubsonic isrc string-array on Song (#818)

OpenSubsonic types `isrc` as `string[]`; Navidrome 0.61.2 ships it as
`isrc: []` or `["USRC…"]`. The typed `Song.isrc: Option<String>` could
not decode either form, which broke the S1 (`search3`) and S2
(`getAlbum`) ingest paths on real Navidrome libraries — initial sync
could not complete past the first array-valued track.

Add a tolerant `de_string_or_seq` deserializer: plain string →
`Some`, non-empty array → first usable value (string element, or an
object element's `name` for the `[{ "name": … }]` shape), `[]`/null →
`None`. The full multi-value set still survives verbatim in
`track.raw_json` (ADR-7). Applied to `Song.isrc`.

Per maintainer policy R7-15 (workdocs question
2026-05-20-large-library-ingest-client-only, checklist item 1):
treat Navidrome as a black box, harden the client decode.

Tests cover `isrc: []` → None, populated array, and the legacy
single-string form.

* feat(library): large-library ingest strategy — S1 over N1 (R7-15) (#819)

Per maintainer policy R7-15 (large-library ingest, client-only): very
large Navidrome catalogs must not start initial sync on N1 — its native
`/api/song` returns HTTP 500 beyond a deep offset and can never finish.
S1 (`search3`) does not hit that wall.

- Add `IngestStrategy::select_initial_strategy(flags, server_track_count,
  n1_bulk_unreliable)`. Large libraries (count > LARGE_LIBRARY_THRESHOLD,
  default 40_000) or servers flagged `n1_bulk_unreliable` route to S1 — or
  S2 when search3 bulk is absent. Normal-size libraries keep the cheapest
  N1 → S1 → S2 chain unchanged.
- Persist the learned per-server `n1_bulk_unreliable` flag on `sync_state`
  (additive migration 002, DEFAULT 0). The mid-run N1→S1 fallback that
  sets it lands in a follow-up.
- Capture `getScanStatus.count` in the capability probe and persist it as
  the `server_track_count` watermark, so the threshold applies from the
  first sync rather than only after N1 hits the wall once. A count-less
  probe never clobbers a watermark from a prior run.
- The initial-sync runner now selects via the new policy.

Tests: selector table (all branches incl. threshold boundary and the
search3-absent fallback), repo flag roundtrip, probe count capture +
watermark-preservation, migration head-version bookkeeping.

* feat(library): freeze ingest strategy on resume (R7-15 Q3) (#820)

A persisted initial-sync cursor that has already made progress must resume
under its own strategy and ignore what a fresh capability probe would now
pick. Previously any strategy mismatch reset the cursor to a fresh one — so
a flapping Navidrome bearer (N1 flag toggling between probes) restarted
ingest from offset 0 on every launch, which is why large initial syncs
never completed across restarts.

`load_or_init_cursor` now:
- resumes the cursor's strategy when it has progress (`ingested_count > 0`
  or `phase != Ingest`), regardless of the re-selected strategy;
- adopts the freshly-selected strategy only when there is no resumable
  progress (offset 0), where re-selecting costs nothing;
- still resets a corrupt/unreadable cursor rather than hard-erroring.

One guarded exception: a cursor still on N1 after the server was learned
`n1_bulk_unreliable` is known-broken and re-selects onto the non-N1 path
instead of resuming a wall-bound N1 loop (the mid-run N1→S1 fallback that
preserves progress lands next).

Tests: resume-with-progress freezes strategy and keeps the count;
no-progress cursor adopts the re-selected strategy; known-broken N1 cursor
re-selects; unreadable cursor still resets.

* feat(library): one-way N1→S1 fallback on deep-offset 500 (R7-15 Q5) (#821)

When the N1 ingest loop hits a persistent HTTP 500 at or beyond the
deep-offset safety line (`N1_DEEP_OFFSET_SAFE`, 50_000) it now treats it
as Navidrome's server-side deep-offset wall rather than a transient error:
it learns `n1_bulk_unreliable` for the server and finishes the sync on S1.

- `run_n1` catches the wall after retry exhaustion (`n1_hit_deep_offset_wall`:
  HTTP 500 AND offset >= the safety line) and hands off to `fall_back_n1_to_s1`.
  A 500 below the line stays a propagated error — no silent downgrade.
- The fallback flags the server, then restarts S1 from offset 0. N1 (`id ASC`)
  and S1 (`search3` default order) don't share an offset space, so resuming
  from the N1 offset would skip songs; re-ingest is idempotent (PK upsert),
  duplicate work over the rows N1 already wrote is acceptable for v1. The
  cursor is rewritten in place, never zeroed.
- One-way only: S1 never flips back to N1 mid-run. Combined with the
  persisted flag and the resume freeze, a future sync selects S1 directly.
- `N1_DEEP_OFFSET_SAFE` is overridable on the runner so the fallback is
  testable without 50k rows of fixture data.

Tests: deep-offset 500 falls back to S1, ingests the full set without
duplicating N1's rows, and persists the flag; a shallow 500 propagates and
does not flag the server.

* feat(library): cache + retry Navidrome bearer, keep N1 flag on transient loss (R7-15 Q3) (#822)

A flaky `/auth/login` previously stripped N1 for a whole bind: the bearer
was fetched once, best-effort, and a single miss dropped to Subsonic-only.
Per R7-15 Q3 a transient `navidrome_token` failure must not drop the
`NavidromeNativeBulk` capability.

- `bind_session` fetches the bearer with `navidrome_token_with_retry`
  (3 attempts, short backoff); if it still fails, it keeps the bearer
  cached from a prior bind instead of overwriting it with `None`. The token
  / credentials are never logged.
- `probe_and_persist` preserves a previously-learned `NavidromeNativeBulk`
  flag when it probes without a token — the server still supports
  `/api/song`; only the bearer is missing this bind. The capability is a
  stable server property, so a token-less probe must not clear it.
- `library_sync_start_inner` masks `NavidromeNativeBulk` from *this run's*
  strategy selection when the session has no token, so the run proceeds
  Subsonic-only (S1/S2) instead of selecting N1 with no creds. The
  persisted capability stays intact for a later bind that recovers the
  token. The in-flight cursor is already protected by the resume freeze.

Tests: token retry yields the token on success and `None` after exhausting
attempts; the probe keeps a learned N1 flag across a token-less re-probe.

* feat(library): mid-run S1→S2 fallback on persistent S1 failure (R7-15 Q8) (#823)

The N1→S1 fallback (#821) had no analogue when S1 itself fails on a server.
Per R7-15 Q8, a persistent S1 failure (C12 retries already exhausted) must
fall back to the universal S2 album crawl — no new artist-walk strategy.

- `run_s1` catches a persistent fetch failure from the `search3` retry loop
  (`is_fetch_failure`: transport / HTTP / decode / Subsonic API / not-found)
  and hands off to `fall_back_s1_to_s2`. Cancellation and storage errors
  propagate untouched.
- The fallback restarts S2 from scratch. S1 (`search3` order) and S2
  (album-list order) don't share an offset space, so resuming from the S1
  offset would skip songs; re-ingest is idempotent (PK upsert). The cursor is
  rewritten in place, never zeroed — the resume freeze then keeps the run on
  S2 across restarts.

This completes the ingest fallback chain N1→S1→S2 from the §6.3 strategy
order; the start-time "no search3 → S2" selection was already covered (#819).

Tests: a persistent S1 500 falls back to S2 and the album crawl ingests the
track.

* fix(library): resume interrupted initial sync on startup (#824)

* fix(library): resume interrupted initial sync on startup

An initial sync killed mid-run (app restart) sat at `idle` until the user
clicked «Sync now» — the background scheduler is delta-only and the
auto-full-sync only fired on the index toggle, not on the startup re-bind.

`resumeInitialSyncIfIncomplete` runs after the active server's session is
re-bound (startup + server switch): if no full sync has completed yet
(`!lastFullSyncAt`) it dispatches `library_sync_start { mode: 'full' }`,
which resumes from the persisted cursor instead of restarting from zero.
Once a full sync has landed it is a no-op, so delta stays the scheduler's
job. Best-effort — errors stay silent (Settings surfaces them on explicit
action).

Tests: starts a full sync when none has completed, no-ops once a full sync
has landed, stays silent when the status lookup fails.

* fix(library): silence cancelled-sync toast, de-dupe startup resume

Two rough edges from the startup resume:

- A cancelled sync surfaced as «Library sync failed: sync cancelled». The
  orchestrator emitted the runner's `Cancelled` result as an error on the
  sync-idle event. Cancellation is expected — the user cancelled, or a newer
  `library_sync_start` superseded the job (server switch / startup resume) —
  and is documented as silent. `sync_outcome_to_result` now maps
  `SyncError::Cancelled` to a clean idle, only real errors toast.
- `resumeInitialSyncIfIncomplete` is now de-duped per server. React
  StrictMode fires the startup effect twice, so a second `library_sync_start`
  cancelled the first (`set_current_job` is cancel-and-replace) — harmless
  with the fix above, but the dedupe avoids the wasted job + probe entirely.

Tests: `sync_outcome_to_result` keeps `Cancelled` silent and forwards real
errors; concurrent resume calls start a single full sync.

* fix(library): run DB read commands off the main thread (async) (#825)

The 10 library read commands were synchronous (`pub fn`). Per the Tauri v2
docs, commands without `async` run on the main thread — so a read that
blocks freezes the UI. During an initial sync the runner holds the single
`Mutex<Connection>` for a whole batch write (500 rows × per-row remap on
Navidrome + upsert + FTS, one transaction), and the Settings library
section polls `library_get_status` on an interval. Each batch write blocked
that polled read on the main thread → the window greyed out until the batch
finished, with the freeze growing as the DB grew.

Make the DB-touching read commands `async` so they run off the main thread:
`library_get_status`, `library_search`, `library_get_track`,
`library_get_tracks_batch`, `library_get_tracks_by_album`,
`library_get_artifact`, `library_get_facts`, `library_get_offline_path`,
`library_advanced_search`, `library_search_cross_server`. Reads still
serialize behind the writer (the single connection is intentional — the
schema mirrors `analysis_cache`, spec §5.1), but the wait no longer blocks
the UI. State-only commands stay sync. Invoke names / payloads are
unchanged, so the frontend is unaffected.

Spec §15 R7-15 follow-up — surfaced in live QA on a 170k library.

* feat(library): scope analysis cache by server_id (E1, schema only) (#826)

Add a versioned migration to audio-analysis.sqlite so waveform/loudness
rows are keyed per server. This is the schema-only step (PR-6c-1): every
existing row migrates to server_id='' and behaviour is unchanged. The
server_id write/read wiring, legacy fallback and lazy re-tag follow in 6c-2.

- migrations 001 (baseline = the pre-versioning schema) + 002 (rebuild the
  three tables with server_id; PK (server_id, track_id, md5_16kb), loudness
  + target_lufs)
- versioned runner mirroring the library store; each migration commits its
  schema change and version marker in one transaction, so a failure or crash
  rolls the whole migration back and retries cleanly
- VACUUM INTO snapshot before the table rewrite as a safety net beyond the
  transaction (disk-full at COMMIT, FS corruption)
- TrackKey gains server_id; all callers pass "" for now

* feat(library): analysis cache server_id wiring (E1, 6c-2) (#827)

* feat(library): scope analysis cache writes/reads/deletes by server_id (E1 wiring)

Build on the 6c-1 schema migration: thread the playback server scope
(playbackServerId ?? activeServerId) through the analysis cache so a server
switch can no longer surface another server's waveform/loudness for the same
bare track_id.

- Write: seed_from_bytes_* and the CPU-seed / HTTP-backfill queues carry a
  server_id; every audio write path (in-memory, ranged, legacy stream, local
  file, spill, preload), the syncfs offline/hot caches, and the backfill
  command write under the playback server (empty = legacy '').
- Read: get_latest_*_for_track and the exact-key lookup try the server scope
  first, then fall back to the legacy '' rows; a legacy hit is re-tagged onto
  the server scope (INSERT OR IGNORE, never clobbers a precise row). No bulk
  backfill — existing caches re-tag lazily on play, so they are not
  re-analysed wholesale.
- The backend gain-resolution path (loudness normalization, replay-gain
  updates, device resume) is scoped via a pinned current_playback_server_id on
  the audio engine, so normalization keeps working for server-scoped rows.
- Delete: delete_*_for_track_id scope to (server + legacy ''); reseed on one
  server no longer wipes another server's analysis. delete_all_waveforms stays
  global (Settings -> Storage).

Tauri boundary: analysis_get_waveform(_for_track), analysis_get_loudness_for_track,
analysis_delete_waveform/loudness_for_track and analysis_enqueue_seed_from_url
gain an optional serverId; audio_play and audio_preload gain an optional
serverId. All additive (absent = legacy '').

* feat(library): pass playback serverId to analysis IPC (E1 wiring)

Send getPlaybackServerId() (queueServerId ?? activeServerId) with every
analysis-cache call so reads/writes/deletes scope to the right server:

- audio_play / audio_preload (playTrack, resume, queue-undo restore, gapless
  byte-preload)
- analysis_get_waveform_for_track / analysis_get_loudness_for_track (waveform +
  loudness refresh)
- analysis_delete_waveform/loudness_for_track + analysis_enqueue_seed_from_url
  (reseed + loudness backfill)

Absent serverId stays backward-compatible (legacy '' scope).

* feat(library): content_hash from playback (E2, 6d) (#828)

* feat(library): record playback content_hash into the track store (E2)

Bridge the playback-derived md5_16kb into library `track.content_hash` (R7-16 Q4)
so id-remap can rebind a track when the server reassigns ids (§6.9).

- New `ContentHashSink` port in psysonic-core (closure handle, mirrors
  PlaybackQueryHandle): keeps psysonic-analysis decoupled from psysonic-library.
- `seed_from_bytes_into_cache` returns the computed md5; `seed_from_bytes_execute`
  fires the sink after a successful seed (Upserted or cache-hit) when a real
  server is known. The shell crate registers the sink to patch the library.
- `patch_content_hash` + `library_patch_track`'s new optional `contentHash`
  field write it; both no-op when the library has no row for (server_id, id),
  i.e. the index is off for that server.
- Sync upsert no longer clobbers it: `content_hash = COALESCE(NULLIF(
  excluded.content_hash,''), track.content_hash)` — a sync (which passes NULL)
  preserves the playback hash, a non-empty incoming hash still wins.

No schema migration — the `content_hash` column already exists. Tauri boundary:
`library_patch_track` gains optional `contentHash` (additive).

* feat(library): expose contentHash on libraryPatchTrack wrapper (E2)

Add optional `contentHash` to the `libraryPatchTrack` patch type so the TS
contract matches the extended Rust command. Normally written by the Rust
analysis bridge; exposed for completeness.

* feat(library): enrichment summary on library_get_track (E3, 6e) (#829)

* feat(library): enrichment summary on library_get_track (E3)

Add an optional `enrichment { waveformReady, loudnessReady, lyricsCached }` to
the single-track `library_get_track` read (R7-16 Q5). Read-only, per-server,
never blocks on the network; list/batch projections leave it unset.

- New `AnalysisReadinessQuery` port in psysonic-core (closure handle, mirrors
  ContentHashSink) keeps psysonic-library decoupled from psysonic-analysis. The
  shell crate registers it to probe the analysis cache by exact
  (server_id, track_id, content_hash) key with legacy '' fallback — read-only,
  no re-tag. waveform/loudness readiness is gated on a known content_hash (E2).
- `lyricsCached` from a new pure-read `ArtifactRepository::lyrics_cached`
  (valid, non-expired, non-not_found lyrics row).
- `library_purge_server`'s `includeAnalysis` documented as a deliberate v1
  no-op (R7-16 Q7): analysis is never deleted on purge / server remove.

Tauri boundary: `LibraryTrackDto` gains optional `enrichment` (additive).

* feat(library): mirror enrichment on LibraryTrackDto wrapper (E3)

Add `TrackEnrichmentDto` + optional `enrichment` to the TS `LibraryTrackDto` so
the contract matches the extended `library_get_track` response.

* feat(library): VirtualSongList browses the local index when ready (F1) (#830)

The all-songs browse now serves pages from the local library index when it is
ready for the active server, falling back to the unchanged network path
otherwise.

- `runLocalSongBrowse` (reuses the F2 local-read adapters): empty-query
  browse-all via `library_advanced_search`, whose default track order
  (`t.title COLLATE NOCASE ASC`) matches the network `ndListSongs('title','ASC')`
  path, so paging stays coherent across a local↔network boundary.
- Gated per page on `libraryIsReady` + `source === 'local'`; any miss / failure
  returns null → VirtualSongList uses the existing browse path unchanged.
- Search (non-empty query) stays on the network path for now; rich search is
  already covered by Advanced Search (F2).

* feat(library): patch-on-use for star/rating/scrobble (PR-7 F3) (#831)

* feat(library): library_patch_track clears nullable fields on explicit null (F3)

Extract the patch logic into a testable `apply_track_patch`. Nullable integer
fields (`starredAt` / `userRating` / `playCount` / `playedAt`) now distinguish an
absent key (leave untouched) from an explicit `null` (clear the column), so
`unstar` ({ starredAt: null }) actually un-stars the local row. `.map` keeps the
present/absent distinction; `as_i64()` yields the value or `None` → bound as SQL
NULL. F3 is the first caller that sends null, so no existing behaviour changes.

* feat(library): patch-on-use wiring for star / rating / scrobble (F3)

After a successful star/unstar, setRating, or play scrobble, mirror the change
into the local library index via `library_patch_track` so its reads (browse F1,
advanced search F2) reflect the action immediately — no stale list after a rate,
no full resync.

- `patchLibraryTrackOnUse` helper: fire-and-forget, gated on the index being
  enabled for the server; the Rust command additionally no-ops when no row
  matches (album/artist id, or index off).
- Wired at the central API chokepoints: `star`/`unstar` (song only) →
  `starredAt`, `setRating` → `userRating`, `scrobbleSong` → `playedAt`.
- `play_count` is left to the next sync (the patch sets absolute values; a
  correct increment needs the current base).

F4 (deprecating the player-store override maps) is intentionally separate —
removing them would break instant star feedback when the index is off.

* feat(library): full-queue restore from the index on startup (PR-7 F5) (#832)

Persist the whole queue as a lightweight ref list and rehydrate it from the
local index on startup, so the entire queue survives a restart instead of only
the windowed slice (R7-17 / §8.6).

- Persist adds `queueRefs` (full ordered ids) + `queueRefsIndex` alongside the
  existing windowed `queue`. Ids are tiny; the windowed objects stay as the
  no-index fallback.
- `hydrateQueueFromIndex` (startup, after session bind): when the library index
  is ready for the queue's server, hydrate the full queue via
  `library_get_tracks_batch` (batched ≤100), map `songToTrack ∘ trackToSong`,
  re-locate the current track so `queueIndex` stays aligned, then clear the refs.
- Index not ready / missing rows / current track not found → keep the windowed
  fallback (queue never empty when the index is off, the P6 default). Old
  persisted shape without refs loads unchanged.
- `trackToSong` exported from the F2 local-read adapters (one mapper).

Kept the windowed-objects persist (did not drop the cap per R7-17 note): the
index-off default needs the embedded fallback or the queue would restore empty.

* feat(library): pending-sync for song star/rating (PR-7 F4) (#833)

* feat(library): central pending-sync helper for song star/rating (PR-7 F4)

`queueSongStar` / `queueSongRating` (spec §6.5 / R7-18): set the player-store
override optimistically, retry the Subsonic API with exponential backoff (flush
on `online` / window focus), and on success clear the override + patch the
in-memory Track so the UI stays correct without it. The F3 index patch-on-use
runs inside the API layer, unchanged.

- No rollback on the first network error (the override survives until the retry
  succeeds or the app restarts; overrides are session-only, not persisted).
- Latest-toggle-wins coalescing + an identity guard so a fast re-toggle while a
  request is in flight can't retire the newer task.
- v1: songs only.

* feat(library): route song star/rating through the pending-sync helper (PR-7 F4)

Replace the scattered optimistic-set + API-call + rollback logic with the single
`queueSongStar` / `queueSongRating` helper across cucadmuh's named v1 surfaces:
PlayerBar, FullscreenPlayer, MobilePlayerView, both context menus (song + queue
row), both shortcut paths, the song-rating hook + player-bar stars, skip→1★, and
AlbumDetail (song star + rating). The 30+ override read sites are unchanged —
they already read `override ?? track`, and the override now clears on success.

Standalone page toggles (Favorites, RandomMix, NowPlaying star) and the separate
mini-player webview keep their existing path — no regression (a non-migrated
override simply lingers as before) — and move to a follow-up.

* feat(library): route remaining song star/rating sites through pending-sync (F4 follow-up) (#834)

Migrate the three standalone song write sites left out of #833 onto the
central queueSongStar / queueSongRating helper:

- Favorites: handleRate + removeSong (un-star)
- RandomMix: toggleSongStar (drops local try/catch rollback per no-rollback policy)
- useNowPlayingStarLove: toggleStar (keeps local view state, helper owns override + retried sync)

MiniContextMenu stays on its direct path (separate webview, no shared store).
No behaviour change for album/artist rating paths.

* feat(library): route playlist song star/rating through pending-sync (F4 follow-up) (#835)

The playlist-detail star/rating hook was the last shared-store song write
site still calling the Subsonic API directly. Route handleRate +
handleToggleStar through queueSongRating / queueSongStar, matching the
Favorites and RandomMix follow-ups; keep the local ratings/starredSongs
view state, drop the inline override.

MiniContextMenu remains on its direct path (separate webview).

* feat(library): BPM range filter UI in Advanced Search (PR-7 F6) (#836)

* feat(library-sync): parallel initial ingest (S2 + N1/S1 prefetch)

Wire C11 ParallelismBudget (max 4 when idle) into InitialSyncRunner:
parallel getAlbum for S2, up to 4 in-flight pages for N1/S1, and persist
S2 cursor once per album-list page instead of per album.

* fix(library-sync): defer scheduler during initial sync and improve ingest diagnostics

Background delta/tombstone ticks every 30s were competing with IS-3 bulk ingest
for the write mutex (20–60s lock waits on large libraries). Skip scheduler while
sync_phase is initial_sync/probing or bulk ingest is active.

Serialize ingest batch metrics as camelCase for DevTools, add bulk-ingest FTS/index
suspension, combined cursor persist, write-op tracing, live local search, and
library dev logging helpers.

* fix(library-search): scoped FTS, cancel stale live search, skip 1-char queries

Use column-scoped FTS for artists/albums/songs, min two graphemes for local
FTS, capped match counts in Advanced Search, and title browse index (m004).

Live Search aborts superseded network requests, passes requestEpoch to drop
stale Rust FTS, and avoids search3 fallback for too-short queries.

* fix(library-search): prefix FTS, fast subquery joins, hide BPM in Advanced Search

Live and Advanced Search now use FTS5 prefix tokens ("metal"*) and limit
bm25 ranking inside rowid subqueries so large libraries stay in the ms range.
Advanced Search BPM filter is removed from the UI until enrichment ships.

* feat(library-search): race local index vs search3, show first result

Live Search and Advanced Search text queries run library and network
backends in parallel; the faster source wins. Adds searchRace helper
and search_race dev logging.

* feat(library-index): multi-server UI, serial sync queue, scoped local search

Add master library index toggle with per-server rows, offline retry, and a
frontend sync queue so initial ingest runs one server at a time. Scope Live
Search and Advanced Search to the sidebar music library filter via library_id
and raw_json fallbacks; coerce numeric libraryId on ingest. Promote idle sync
state to ready when a full sync stamp exists and block cross-server initial
sync starts in Rust.

* chore(library-store): compliance — clippy, i18n, CHANGELOG

Fix clippy/tsc blockers (request structs, IngestPageCtx, type aliases),
add library index strings to all 9 locales, and document the preview
feature in CHANGELOG [1.47.0] with Psychotoxical + cucadmuh attribution.

* docs(credits): library index preview contributions

Credit Psychotoxical for the local library store foundation and cucadmuh
for multi-server UI, scoped search, and i18n. Drop removed scan-trigger
wording from PR #780 entry.

* docs(release): link library index preview to PR #846

* docs(changelog): sort [1.47.0] entries by ascending PR number

* docs(changelog): mark library index as Added in [1.47.0]

* docs(changelog): restructure [1.47.0] into Added/Changed/Fixed

Match 1.46.0 layout: new features in Added (incl. library index),
enhancements in Changed, bug fixes in Fixed — PR ascending within each block.

* fix(library): address PR #846 review — delta guard + FTS order

Skip background scheduler delta when LibraryRuntime already has a
foreground sync job for the same server. Preserve bm25 rowid ordering
in live search track/artist/album fetches.

* fix(library): address remaining PR #846 review items

S2 resume persists current_album_id per album; same-server resync awaits
the previous runner. N1 delta watermark uses strict less-than; Navidrome
HTTP 500 detection is structured. Adds genre/year indexes, backoff jitter
salt, LiveSearch failure toast, and user-facing search badge copy.

* fix(library): close resync notify race and tighten FTS trigger test

Use notify_one() so an early runner completion cannot lose the drain
signal before same-server full resync awaits. FTS test now compares
normalized trigger bodies from migration vs suspend/restore roundtrip.

---------

Co-authored-by: Frank Stellmacher <171614930+Psychotoxical@users.noreply.github.com>
2026-05-22 00:33:09 +02:00

508 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export const settings = {
title: 'Setări',
language: 'Limbă',
languageEn: 'Engleză',
languageDe: 'Germană',
languageFr: 'Franceză',
languageNl: 'Olandeză',
languageZh: 'Chineză',
languageNb: 'Norvegiană',
languageRu: 'Rusă',
languageEs: 'Spaniolă',
languageRo: 'Română',
font: 'Font',
fontHintOpenDyslexic: 'Prietenos cu dislexia · Chineza nu este suportată',
theme: 'Temă',
appearance: 'Aparență',
servers: 'Servere',
serverName: 'Nume Server',
serverUrl: 'URL Server',
serverUrlPlaceholder: '192.168.1.100:4533 sau https://music.example.com',
serverUsername: 'Nume utilizator',
serverPassword: 'Parolă',
addServer: 'Adaugă Server',
addServerTitle: 'Adaugă Server nou',
editServer: 'Editează',
editServerTitle: 'Editează Server',
useServer: 'Folosește',
deleteServer: 'Șterge',
noServers: 'Niciun server salvat.',
serverActive: 'Activ',
confirmDeleteServer: 'Șterge server "{{name}}"?',
serverConnecting: 'Se conectează…',
serverConnected: 'Conectat!',
serverFailed: 'Conexiune eșuată.',
testBtn: 'Testează Conexiunea',
testingBtn: 'Se testează…',
serverCompatible: 'Făcut pentru Navidrome. Alte servere compatibile cu Subsonic (Gonic, Airsonic, …) pot rula cu funcționalitate redusă, deoarece Psysonic folosește multe endpointuri API specifice Navidrome.',
userMgmtTitle: 'Gestionarea Utilizatorilor',
userMgmtDesc: 'Gestionează utilizatorii de pe acest server. Necesită privilegii admin.',
userMgmtNoAdmin: 'Ai nevoie de privilegii de admin pentru a gestiona utilizatorii de pe acest server.',
userMgmtLoadError: 'Eroare la încărcarea utilizatorilor.',
userMgmtLoadFriendly: 'Serverul nu a răspuns — acesta este de obicei un caz singular.',
userMgmtRetry: 'Reîncearcă',
userMgmtEmpty: 'Niciun utilizator găsit.',
userMgmtYouBadge: 'Tu',
userMgmtAdminBadge: 'Admin',
userMgmtAddUser: 'Adaugă Utilizator',
userMgmtAddUserTitle: 'Adaugă Nou Utilizator',
userMgmtEditUserTitle: 'Editează Utilizatorul',
userMgmtUsername: 'Nume utilizator',
userMgmtName: 'Nume afișaj',
userMgmtEmail: 'Email',
userMgmtPassword: 'Parolă',
userMgmtPasswordEditHint: 'Introdu o parolă nouă pentru a o actualiza.',
userMgmtRoleAdmin: 'Admin',
userMgmtLibraries: 'Librării',
userMgmtLibrariesAdminHint: 'Utilizatorii Admin au acces automat la toate librăriile.',
userMgmtLibrariesEmpty: 'Nicio librărie disponibilă pe acest server.',
userMgmtLibrariesValidation: 'Alege cel puțin o librărie.',
userMgmtLibrariesUpdateError: 'Utilizator salvat, dar atribuirea librăriilor a eșuat',
userMgmtNoLibraries: 'Nicio librărie asignată',
userMgmtNeverSeen: 'Niciodată',
userMgmtSave: 'Salvează',
userMgmtCancel: 'Anulează',
userMgmtDelete: 'Șterge',
userMgmtEdit: 'Editează',
userMgmtConfirmDelete: 'Șterge utilizatorul "{{username}}"? Acestă acțiune nu poate fi anulată.',
userMgmtCreateError: 'Nu s-a putut crea utilizatorul.',
userMgmtUpdateError: 'Nu s-a putut actualiza utilizatorul.',
userMgmtDeleteError: 'Nu s-a putut șterge utilizatorul.',
userMgmtCreated: 'Utilizator creat.',
userMgmtUpdated: 'Utilizator actualizat.',
userMgmtDeleted: 'Utilizator șters.',
userMgmtValidationMissing: 'Numele de utilizator, numele de afișaj și parola sunt necesare.',
userMgmtValidationMissingIdentity: 'Numele de utilizator și numele de afișaj sunt necesare.',
userMgmtMagicStringGenerate: 'Generează string-ul magic',
userMgmtSaveAndMagicString: 'Salvează și obține string-ul magic',
userMgmtMagicStringPasswordNavHint:
'Navidrome va salva această parolă pentru utilizator. Dacă diferă de cea curentă, serverul va actualiza parola de login.',
userMgmtMagicStringPlaintextWarning:
'Distribuie string-ul magic cu grijă: conține o parolă necriptată (codificarea nu este criptare). Oricine cu string-ul complet se poate conecta ca acest utilizator.',
userMgmtMagicStringCopied: 'String-ul magic copiat în clipboard.',
userMgmtMagicStringCopyFailed: 'Nu s-a putut copia în clipboard.',
userMgmtMagicStringLoginFailed: 'Verificarea parolei a eșuat — credențialele nu au putut fi verificate.',
userMgmtMagicStringModalTitle: 'Generează string-ul magic',
userMgmtMagicStringModalDesc: 'Introdu parola Subsonic pentru "{{username}}". Este inclusă în string-ul magic copiat.',
userMgmtMagicStringModalConfirm: 'Copiază string-ul',
audiomuseTitle: 'AudioMuse-AI (Navidrome)',
audiomuseDesc:
'Pornește dacă acest server are <pluginLink>AudioMuse-AI Navidrome plugin</pluginLink> configurat. Activează Mixul Instant din piese si folosește artiști similari de pe partea de server în loc de Last.fm pe paginile de artiști.',
audiomuseIssueHint:
'Mixul Instant a eșuat recent — verifică plugin-ul Navidrome și API-ul AudioMuse. Artiștii similari sunt bazați pe Last.fm când serverul nu returnează nimic.',
connected: 'Conectat',
failed: 'Eșuat',
eqTitle: 'Egalizator',
eqEnabled: 'Activează Egalizatorul',
eqPreset: 'Preset',
eqPresetCustom: 'Personalizat',
eqPresetBuiltin: 'Preseturi încorporate',
eqPresetCustomGroup: 'Preseturile mele',
eqSavePreset: 'Salvează ca Preset',
eqPresetName: 'Nume preset…',
eqDeletePreset: 'Șterge preset',
eqResetBands: 'Resetează la Plat',
eqPreGain: 'Pre-gain',
eqResetPreGain: 'Resetează pre-gain',
eqAutoEqTitle: 'Căutare Căști AutoEQ',
eqAutoEqPlaceholder: 'Caută model căști / IEM…',
eqAutoEqSearching: 'Se caută…',
eqAutoEqNoResults: 'Niciun rezultat găsit',
eqAutoEqError: 'Căutarea a eșuat',
eqAutoEqRateLimit: 'Limita ratei GitHub a fost atinsă — încearcă din nou într-un minut',
eqAutoEqFetchError: 'Nu s-a putut lua profilul EQ',
lfmTitle: 'Last.fm',
lfmConnect: 'Conectează-te cu Last.fm',
lfmConnecting: 'Se așteaptă autorizarea…',
lfmConfirm: 'Am autorizat aplicația',
lfmConnected: 'Conectat ca',
lfmDisconnect: 'Deconectează-te',
lfmConnectDesc: 'Conectează contul tău Last.fm pentru a pornit scrobbling și update-uri Now Playing direct din Psysonic — nicio nevoie configurare Navidrome necesară.',
lfmOpenBrowser: 'O fereastră browser se va deschide. Autorizează Psysonic în Last.fm, apoi apasă pe butonul de mai jos.',
lfmScrobbles: '{{n}} scrobble-uri',
lfmMemberSince: 'Membru din {{year}}',
scrobbleEnabled: 'Scrobbling dezactivat',
scrobbleDesc: 'Trimite piese la Last.fm după 50% timp de redare',
behavior: 'Comportamentul Aplicației',
cacheTitle: 'Spațiu de stocare maxim',
cacheDesc: 'Artă copertă și imagini artist. Când este plin, cele mai vechi intrări sunt șterse automat. Albumele offline nu sunt șterse automat, dar vor fi șterse când memoria cache este curățată manual.',
cacheUsedImages: 'Imagini:',
cacheUsedOffline: 'Piese offline:',
cacheUsedHot: 'Mărimea pe disc:',
hotCacheTrackCount: 'Piese în memoria cache:',
cacheMaxLabel: 'Mărimea maximă',
cacheClearBtn: 'Golește memoria cache',
waveformCacheClearBtn: 'Golește cache-ul formelor de undă',
cacheClearWarning: 'Aceasta va înlătura de asemenea toate albumele offline din librărie.',
cacheClearConfirm: 'Golește totul',
cacheClearCancel: 'Anulează',
waveformCacheCleared: 'Cache-ul formelor de undă golit ({{count}} rows).',
waveformCacheClearFailed: 'Golirea cache-ului formelor de undă a eșuat.',
offlineDirTitle: 'Librărie offline (În Aplicație)',
offlineDirDesc: 'Locația stocării pieselor pe care le faci disponibile offline în Psysonic.',
offlineDirDefault: 'Implicit (App Data)',
offlineDirChange: 'Schimbă directorul',
offlineDirClear: 'Resetează la implicit',
offlineDirHint: 'Noile descărcări vor folosi această locație. Descărcările existente rămân la adresa originală.',
hotCacheTitle: 'Cache-ul hot playback',
hotCacheDisclaimer: 'Preîncarcă piesele ce urmează din coadă și păstrează anterioarele. Când este pornit, folosește spațiu pe disc și rețea.',
hotCacheDirDefault: 'Implicit (App Data)',
hotCacheDirChange: 'Schimbă folderul',
hotCacheDirClear: 'Resetează la implicit',
hotCacheDirHint: 'Schimbarea folderului resetează indexul din aplicație; fișierele aflate deja pe disc stau unde sunt până când le ștergi.',
hotCacheEnabled: 'Activează cache-ul hot playback cache',
hotCacheMaxMb: 'Mărimea maximă cache',
hotCacheDebounce: 'Debounce la schimbarea cozii',
hotCacheDebounceImmediate: 'Imediat',
hotCacheDebounceSeconds: '{{n}} s',
hotCacheClearBtn: 'Golește hot cache',
audioOutputDevice: 'Dispozitiv de ieșire audio',
audioOutputDeviceDesc: 'Alege prin ce dispozitiv audio să redea Psysonic. Schimbările au efect imediat și re-încep piesa curentă.',
audioOutputDeviceDefault: 'Implicit sistem',
audioOutputDeviceRefresh: 'Reîmprospătează lista dispozitivelor',
audioOutputDeviceOsDefaultNow: 'ieșirea curentă a sistemului',
audioOutputDeviceListError: 'Nu s-a putut încărca lista dispozitivelor audio',
audioOutputDeviceNotInCurrentList: 'nu există în lista curentă',
audioOutputDeviceMacNotice: 'În macOS, redarea urmărește mereu dispozitivul audio de ieșire al sistemului din motive tehnice. Schimbă ținta în Setări → Sunet sau iconița difuzor din bara de meniu. Fundal: CoreAudio declanșează un prompt de permisiune de microfon când se deschide un stream non-implicit — îl evităm folosind opțiunea implicită a sistemului mereu.',
hiResTitle: 'Playback Hi-Res nativ',
hiResEnabled: 'Pornește playback-ul hi-res nativ',
hiResDesc: "Forțează ieșire implicită de 44.1 kHz pentru stabilitate maximă. Pornește doar dacă sistemul hardware și rețeaua suport fiabil rate mari de eșantionare (88.2 kHz+).",
showArtistImages: 'Afișează Imagini Artist',
showArtistImagesDesc: 'Încarcă și afișează imagini artist în Prezentarea generală a Artiștilor. Oprit implicit pentru a reduce I/O pe server și încărcarea rețelei pe librării mari.',
showOrbitTrigger: 'Afișează "Orbit" în antet',
showOrbitTriggerDesc: 'Declanșatorul din bara de sus pentru a începe sau intra într-o sesiune de ascultare distribuită. Ascunde-l dacă nu folosești Orbit - îl poți porni înapoi aici.',
showTrayIcon: 'Afișează Iconița Tavă',
showTrayIconDesc: 'Afișează iconița Psysonic în zona notificărilor de sistem / bara de meniu.',
minimizeToTray: 'Minimizează în Tavă',
minimizeToTrayDesc: 'La închiderea ferestrei, continuă rularea Psysonic în tava de sistem în loc de ieșire',
clockFormat: 'Format Oră',
clockFormatDesc: 'Format al ceasului folosit pentru ETA-ul cozii și previzualizarea cronometrului de somn.',
clockFormatAuto: 'Automat (limba sistemului)',
clockFormatTwentyFour: '24 de ore',
clockFormatTwelve: '12 ore (AM/PM)',
preloadMiniPlayer: 'Preîncarcă mini player',
preloadMiniPlayerDesc: 'Crează fereastra mini player în fundal la deschiderea aplicației pentru a arăta conținut instantaneu la prima deschidere. Folosește puțină extra memorie.',
discordRichPresence: 'Prezență Discord Rich',
discordRichPresenceDesc: 'Arată piesa redată curent pe profilul tău de Discord. Necesită Discord să ruleze.',
useCustomTitlebar: 'Bară de titlu personalizată',
useCustomTitlebarDesc: 'Înlocuiește bara de titlu a sistemului cu una care corespunde cu tema aplicației. Dezactivează pentru a folosi bara de titlu nativ GNOME/GTK.',
linuxWebkitSmoothScroll: 'Rotiță lină (Linux)',
linuxWebkitSmoothScrollDesc: 'Pornit: scroll inert. Oprit: linie cu linie, în stil GTK.',
linuxWaylandTextRender: 'Randare text Wayland (Linux)',
linuxWaylandTextRenderDesc:
'Netezirea în interfață se aplică imediat. Accelerația hardware WebKit (ascuțit/GPU) este salvată și aplicată la următoarea pornire a aplicației — comutarea în timpul rulării poate îngheța WebKitGTK.',
linuxWaylandTextRenderBalanced: 'Echilibrat',
linuxWaylandTextRenderSharp: 'Ascuțit (prietenos cu CPU)',
linuxWaylandTextRenderGpu: 'Prioritate GPU',
linuxWaylandTextRenderMinimal: 'Minim (netezire CSS implicită)',
discordCoverSource: 'Sursa artei de copertă',
discordCoverSourceDesc: 'De unde să fie preluată arta de album afișată pe profilul tău de Discord.',
discordCoverNone: 'Niciuna (doar iconița aplicației)',
discordCoverServer: 'Server (prin informații album)',
discordCoverApple: 'Apple Music',
discordOptions: 'Opțiuni avansate Discord',
discordTemplates: 'Șablon text personalizat',
discordTemplatesDesc: 'Personalizează ce informații sunt afișate pe profilul tău de Discord. Variabile: {title}, {artist}, {album}',
discordTemplateDetails: 'Linia principală (detalii)',
discordTemplateState: 'Linia secundară (status)',
discordTemplateLargeText: 'Tooltip album (largeText)',
nowPlayingEnabled: 'Se afișează în Now Playing',
nowPlayingEnabledDesc: 'Difuzează piesa redată curent către vizualizatorul de ascultare live a serverului. Dezactivează pentru a opri trimiterea datelor de redare.',
enableBandsintown: 'Date de turneu Bandsintown',
enableBandsintownDesc: 'Arată concertele următoare pentru artisul curent în tab-ul Informații. Datele sunt preluate din API-ul public Bandsintown.',
lyricsServerFirst: 'Preferă versurile de pe server',
lyricsServerFirstDesc: 'Verifică versurile furnizate de server (tag-uri încorporate, fișiere sidecar) înainte de interogarea LRCLIB. Dezactivează pentru a folosi LRCLIB primul.',
enableNeteaselyrics: 'Versuri Netease Cloud Music',
enableNeteaselyricsDesc: 'Folosește Netease Cloud Music ca ultimă soluție pentru versuri când serverul și LRCLIB nu returnează nimic. Cea mai bună acoperire pentru muzică Asiatică și internațională.',
lyricsSourcesTitle: 'Sursă versuri',
lyricsSourcesDesc: 'Alege ce surse se interoghează pentru versuri și în ce ordine. Trage pentru a reordona. Sursele dezactivate sunt ignorate complet.',
lyricsSourceServer: 'Server',
lyricsSourceLrclib: 'LRCLIB',
lyricsSourceNetease: 'Netease Cloud Music',
lyricsModeStandard: 'Standard',
lyricsModeStandardDesc: 'Trei surse în o listă ordonabilă: tag-uri server, LRCLIB, Netease.',
lyricsModeLyricsplus: 'YouLyPlus (Karaoke)',
lyricsModeLyricsplusDesc: 'Sincronizare cuvânt-cu-cuvânt preluată din Apple Music, Spotify, Musixmatch & QQ (backend comunitate). Recurge în tăcere la sursele standard când nimic nu este găsit.',
lyricsStaticOnly: 'Afișează versurile doar ca text static',
lyricsStaticOnlyDesc: 'Redă versurile sincronizate fără scroll automat și fără evidențierea cuvintelor.',
downloadsTitle: 'Export ZIP & Arhivare',
downloadsFolderDesc: 'Folder destinație pentru albumele pe care le descarci ca fișiere ZIP în calculator.',
downloadsDefault: 'Folderul Download Implicit',
pickFolder: 'Selectează',
pickFolderTitle: 'Selectează Folderul de Download',
clearFolder: 'Golește folderul de download',
logout: 'Deconectare',
aboutTitle: 'Despre Psysonic',
aboutDesc: 'Un player muzical desktop modern făcut pentru Navidrome. Folosește API-ul Subsonic plus extensiile specifice Navidrome. Construit cu Tauri v2 cu un motor audio nativ Rust — ușor și rapid, dar împachetat cu funcționalități: bară de redare cu forme de undă, versuri sincronizate, integrare Last.fm, EQ 10 benzi, crossfade, redare fără spațiu între piese, Replay Gain, navigarea pe gen, și o librărie largă de teme.',
aboutLicense: 'Licență',
aboutLicenseText: 'GNU GPL v3 — liber de utilizat, modificat, și distribuit sub aceeași licență.',
aboutRepo: 'Cod Sursă pe GitHub',
aboutVersion: 'Versiune',
aboutBuiltWith: 'Construit cu Tauri · React · TypeScript · Rust/rodio',
aboutMaintainersLabel: 'Dezvoltatori',
aboutReleaseNotesLabel: 'Notițe lansare',
aboutReleaseNotesLink: "Deschide ce e nou pentru această versiune",
aboutContributorsLabel: 'Contribuabili',
showChangelogOnUpdate: "Arată 'Ce este nou' în update",
showChangelogOnUpdateDesc: "Arată un banner discret cu lista modificărilor deasupra Now Playing după un update. Apasă pentru a deschide lista modificărilor; X îl respinge.",
libraryGridMaxColumnsTitle: 'Grile cu carduri în bibliotecă',
libraryGridMaxColumnsPerfHint: 'Mai multe coloane înseamnă mai multe plăcuțe pe rând și mai multă muncă de layout și desen — se simte pe biblioteci foarte mari sau hardware lent.',
libraryGridMaxColumnsRangeLabel: 'Număr maxim de coloane ({{min}}{{max}})',
libraryGridMaxColumnsDesc: 'Se aplică la albume, artiști, liste de redare, radio, offline și alte ecrane cu carduri. Mai puține coloane = plăcuțe mai mari și de obicei mai puțină încărcare pe CPU.',
libraryIndexTitle: 'Index local al bibliotecii (previzualizare)',
libraryIndexDesc: 'Păstrează o copie locală a bazei de piese a fiecărui server, astfel încât navigarea și căutarea rămân rapide și funcționează offline. Sincronizarea inițială rulează pentru toate serverele configurate; serverele offline sunt reîncercate automat.',
libraryIndexDeltaHint: 'Sincronizarea delta în fundal verifică serverele legate la fiecare 30 de secunde și rulează când este cazul — de obicei la 545 de minute, în funcție de dimensiunea bibliotecii și încărcare.',
libraryIndexEnable: 'Activează indexul local al bibliotecii',
libraryIndexEnableAllDesc: 'Indexează și sincronizează toate serverele configurate în fundal.',
libraryIndexNoServer: 'Adaugă mai întâi un server.',
libraryIndexServerListTitle: 'Servere indexate',
libraryIndexAllExcluded: 'Toate serverele sunt excluse din sincronizare. Reactivează indexul sau adaugă un server.',
libraryIndexServerOffline: 'Server offline — sincronizare amânată',
libraryIndexServerDeferred: 'Amânată',
libraryIndexServerSyncing: 'Se sincronizează…',
libraryIndexFullResync: 'Resincronizare completă',
libraryIndexDeltaSync: 'Sincronizare delta',
libraryIndexExcludeServer: 'Exclude din sincronizare',
libraryIndexExcludedTitle: 'Excluse din sincronizare',
libraryIndexIncludeServer: 'Include din nou',
libraryIndexStatus: 'Stare',
libraryIndexStatusIdle: 'Inactiv',
libraryIndexStatusProbing: 'Se verifică serverul…',
libraryIndexStatusInitial: 'Sincronizare inițială…',
libraryIndexStatusReady: 'Gata ({{count}} piese)',
libraryIndexStatusError: 'Eroare — vezi jurnalele',
libraryIndexProgressIngest: '{{count}} piese indexate…',
libraryIndexProgressVerify: '{{checked}} verificate ({{deleted}} eliminate)',
libraryIndexSyncNow: 'Sincronizează acum',
libraryIndexVerify: 'Verifică integritatea bibliotecii',
libraryIndexCancel: 'Anulează',
libraryIndexAutoReconcile: 'Reconciliere automată la scăderea numărului',
libraryIndexAutoReconcileDesc: 'Verifică automat piesele eliminate când serverul raportează mai puține decât se așteaptă.',
libraryIndexSyncError: 'Sincronizarea bibliotecii a eșuat: {{error}}',
libraryIndexBindError: 'Indexul nu a putut fi activat: {{error}}',
randomMixTitle: 'Lista neagră Mix Aleatoriu',
luckyMixMenuTitle: 'Arată Mixul Norocos în meniu',
luckyMixMenuDesc: 'Activează Mixul Norocos în Construiește un Mix și ca un element separat în meniu când navigarea împărțită este pornită. Vizibil doar când AudioMuse este activat pe serverul activ.',
randomMixBlacklistTitle: 'Cuvinte cheie personalizate pentru filtre',
randomMixBlacklistDesc: 'Piesele sunt excluse când orice cuvânt cheie corespunde cu genul, titlul, albumul sau artistul lor (activ când checkbox-ul de deasupra este pornit).',
randomMixBlacklistPlaceholder: 'Adaugă cuvânt cheie…',
randomMixBlacklistAdd: 'Adaugă',
randomMixBlacklistEmpty: 'Niciun cuvânt cheie adăugat încă.',
randomMixHardcodedTitle: 'Cuvinte cheie predefinite (activ când checkbox-ul este pornit)',
tabAudio: 'Audio',
tabStorage: 'Offline & Cache',
tabAppearance: 'Aparență',
tabLibrary: 'Librărie',
tabServers: 'Servere',
tabLyrics: 'Versuri',
tabPersonalisation: 'Personalizare',
tabIntegrations: 'Integrări',
inputKeybindingsTitle: 'Scurtături tastatură',
aboutContributorsCount_one: '{{count}} contribuție',
aboutContributorsCount_other: '{{count}} contribuții',
searchPlaceholder: 'Caută setări…',
searchNoResults: 'Nicio setare nu corespunde cu ce ai căutat.',
integrationsPrivacyTitle: 'Notificare de confidențialitate',
integrationsPrivacyBody: 'Toate integrările din acest tab sunt <strong>opționale</strong> și trimit date către servicii externe sau server-ul tău Navidrome când sunt activate. Last.fm primește istoricul ascultărilor tale, Discord arată piesa redată curent în profilul tău, Bandsintown e interogat per artist pentru a aduce date de turnee, și distribuirea Now-Playing publică piesa curentă către alți useri din server-ul tău Navidrome. Dacă nu vrei vreo opțiune, doar lasă secțiunea corespunzătoare dezactivată',
homeCustomizerTitle: 'Pagina Principală',
queueToolbarTitle: 'Toolbar Coadă',
queueToolbarReset: 'Resetează la implicit',
queueToolbarSeparator: 'Separator',
sidebarTitle: 'Bara laterală',
sidebarReset: 'Resetează la implicit',
artistLayoutTitle: 'Secțiunile paginii artistului',
artistLayoutDesc: 'Trage pentru a reordona, comutați pentru a ascunde secțiuni individuale ale paginii de artist. Secțiunile fără date sunt ignorate automat.',
artistLayoutReset: 'Resetează la implicit',
artistLayoutBio: 'Biografie artist',
artistLayoutTopTracks: 'Top piese',
artistLayoutSimilar: 'Artiști similari',
artistLayoutAlbums: 'Albume',
artistLayoutFeatured: 'Prezentat și pe',
playlistLayoutTitle: 'Aspect pagină playlist',
playlistLayoutDesc: 'Afișează sau ascunde elemente individuale pe pagina playlistului.',
playlistLayoutReset: 'Resetare la implicit',
playerBarTitle: 'Bara de redare',
playerBarReset: 'Resetare la implicit',
playerBarStarRating: 'Evaluare cu stele',
playerBarFavorite: 'Favorit (inimă)',
playerBarLastfmLove: 'Last.fm love',
playerBarEqualizer: 'Egalizator',
playerBarMiniPlayer: 'Mini player',
advancedMode: 'Avansat',
advancedModeTooltip: 'Afișează opțiuni avansate în toate filele de setări. Contribuții ale comunității care nu reflectă neapărat filozofia de design a mentenanților Psysonic.',
advancedBadge: 'Avansat',
sidebarDrag: 'Trage pentru a reordona',
sidebarFixed: 'Mereu vizibil',
randomNavSplitTitle: 'Navigare Mix împărțită',
randomNavSplitDesc: 'Arată "Mix Aleatoriu", "Albume Aleatorii", și "Mix Norocos" ca intrări separate în bara laterală în loc de hub-ul "Construiește un Mix".',
tabInput: 'Input',
tabUsers: 'Useri',
tabSystem: 'Sistem',
loggingTitle: 'Logging',
loggingModeDesc: 'Controlează verbozitatea log-urilor din backend în terminal.',
loggingModeOff: 'Oprit',
loggingModeNormal: 'Normal',
loggingModeDebug: 'Debug',
loggingExport: 'Exportă log-uri',
loggingExportSuccess: 'Log-uri exportate ({{count}} linii).',
loggingExportError: 'Nu s-au putut exporta log-urile.',
ratingsSectionTitle: 'Ratinguri',
ratingsSkipStarTitle: 'Sari peste 1 stea',
ratingsSkipStarDesc:
'După mai multe salturi la rând, setează ratingul piesei la 1★. Doar pentru piesele care nu au fost deja evaluate.',
ratingsSkipStarThresholdLabel: 'Salturi',
ratingsMixFilterTitle: 'Filtrează după rating',
ratingsMixFilterDesc:
'Filtrează elementele cu scor mic în {{mix}} și {{albums}}. Apasă pe steaua selectată din nou pentru a opri limita.',
ratingsMixMinSong: 'Piese',
ratingsMixMinAlbum: 'Albume',
ratingsMixMinArtist: 'Artiști',
ratingsMixMinThresholdAria: 'Stele minime: {{label}}',
backupTitle: 'Backup & Restaurare',
backupExport: 'Setări de export',
backupExportDesc: 'Salvează toate setările, profilele de server, configurări Last.fm, teme, EQ și combinații de taste către un fișier .psybkp. Parolele sunt stocate ca text — păstrează fișierul în siguranță.',
backupImport: 'Setări de Import',
backupImportDesc: 'Restaurează setări dintr-in fișier .psybkp. Aplicația se va reîncărca după import.',
backupImportConfirm: 'Aceasta va suprascrie toate setările curente. Continuați?',
backupSuccess: 'Backup salvat',
backupImportSuccess: 'Setări restaurate — se reîncarcă…',
backupImportError: 'Fișier de backup invalid sau corupt.',
shortcutsReset: 'Resetează la implicite',
shortcutListening: 'Apasă o tastă…',
shortcutUnbound: '—',
globalShortcutsTitle: 'Scurtături globale',
globalShortcutsNote: 'Funcționează la nivel de sistem și când Psysonic este în fundal. Necesită Ctrl, Alt, sau Super ca un modificator.',
shortcutClear: 'Golește',
shortcutPlayPause: 'Redă / Pauză',
shortcutNext: 'Următoarea piesă',
shortcutPrev: 'Piesa anterioară',
shortcutVolumeUp: 'Ridică volumul',
shortcutVolumeDown: 'Coboară volumul',
shortcutSeekForward: 'Dă înainte 10s',
shortcutSeekBackward: 'Dă înapoi 10s',
shortcutToggleQueue: 'Comutați coada',
shortcutOpenFolderBrowser: 'Deschide {{folderBrowser}}',
shortcutFullscreenPlayer: 'Player pe ecran complet',
shortcutNativeFullscreen: 'Ecran complet nativ',
shortcutOpenMiniPlayer: 'Deschide mini player',
shortcutStartSearch: 'Începe o căutare',
shortcutStartAdvancedSearch: 'Începe o căutare avansată',
shortcutToggleSidebar: 'Comutați bara laterală',
shortcutMuteSound: 'Opriți sunetul',
shortcutToggleEqualizer: 'Porniți / Comutați Egalizatorul',
shortcutToggleRepeat: 'Comutați repetarea',
shortcutOpenNowPlaying: 'Deschide "Now Playing"',
shortcutShowLyrics: 'Arată Versurile',
shortcutFavoriteCurrentTrack: 'Adaugă piesa curentă la favorite',
shortcutOpenHelp: 'Ajutor',
playbackTitle: 'Redare',
replayGain: 'Replay Gain',
replayGainDesc: 'Normalizează volumul pieselor folosind metadata ReplayGain',
replayGainMode: 'Mod',
replayGainAuto: 'Auto',
replayGainAutoDesc: 'Alege volumul albumului când piesele vecine din coadă sunt din același album, altfel volumul piesei.',
replayGainTrack: 'Piesă',
replayGainAlbum: 'Album',
replayGainPreGain: 'Pre-Gain (fișiere etichetate)',
replayGainPreGainDesc: 'Boost universal adăugat peste fiecare calculare ReplayGain. Folosește pentru a compensa dacă librăria ta se simte prea liniștită per total.',
replayGainFallback: 'Rezervă (neetichetat / radio)',
replayGainFallbackDesc: 'Aplicat la piesele (și stream-urile radio) care nu au tag-uri ReplayGain deloc, pentru ca conținutul neetichetat să nu se audă mai gălăgios decât restul.',
normalization: 'Normalizare',
normalizationDesc: 'Aliniază volumul perceput peste piese, albume și radio.',
normalizationOff: 'Oprit',
normalizationReplayGain: 'ReplayGain',
normalizationLufs: 'LUFS',
loudnessTargetLufs: 'Țintă LUFS',
loudnessTargetLufsDesc: 'Volumul de referință la care toate piesele sunt ajustate. Numere mai mici = volum mai mare. Serviciile de streaming sunt setate în mod obișnuit la -14 LUFS.',
loudnessPreAnalysisAttenuation: 'Atenuare pre-analiză',
loudnessPreAnalysisAttenuationDesc:
'Extra coborâre a volumului până când nivelul pentru această piesă este salvat. Streaming apoi folosește presupuneri aproximative, nu o măsurătoare completă. 0 dB = oprit; mai jos = mai încet. Numărul din dreapta este nivelul dB efectif pentru ținta curentă.',
loudnessPreAnalysisAttenuationRef: '{{eff}} dB efectiv cu ținta {{tgt}} LUFS.',
loudnessPreAnalysisAttenuationReset: 'Resetează la implicit',
loudnessFirstPlayNote:
'Prima redate a unei piese noi își poate schimba brusc volumul cât este măsurată. Următoarea redare folosește măsurătoarea stocată și este stabilă. Piesele următoare din coadă sunt de obicei pre-analizate în timpul piesei anterioare, deci acest fenomen se întâmplă rar în practică.',
crossfade: 'Crossfade',
crossfadeDesc: 'Estompează între piese',
crossfadeSecs: '{{n}} s',
notWithGapless: 'Nu este valabil cât Gapless este activ',
notWithCrossfade: 'Nu este valabil cât Crossfade este activ',
gapless: 'Playback Gapless',
gaplessDesc: 'Pre-încarcă următoarea piesă pentru a elimina spațiile dintre piese',
preservePlayNextOrder: 'Prezervă ordinea "Redă următoarea"',
preservePlayNextOrderDesc: 'Elementele Redă Următoarea noi adăugate sunt puse după cele adăugate mai devreme în loc să sară în față.',
trackPreviewsTitle: 'Previzualizări Piese',
trackPreviewsToggle: 'Pornește previzualizările pieselor',
trackPreviewsDesc: 'Arată butoanele de Redare și Pre-vizualizare în rând în listele de piese pentru o mostră rapidă din mijlocul piesei.',
trackPreviewStart: 'Poziția de începere',
trackPreviewStartDesc: 'Cât de departe în piesă să înceapă pre-vizualizarea (% din lungimea piesei).',
trackPreviewDuration: 'Durată',
trackPreviewDurationDesc: 'Cât de mult se redă fiecare pre-vizualizare până se oprește.',
trackPreviewDurationSecs: '{{n}} s',
trackPreviewLocationsTitle: 'Unde apar pre-vizualizările',
trackPreviewLocationsDesc: 'Alege ce liste arată butoanele de pre-vizualizare în linie.',
trackPreviewLocation_suggestions: 'Sugestii în playlist',
trackPreviewLocation_albums: 'Liste de piese în album',
trackPreviewLocation_playlists: 'În playlist-uri',
trackPreviewLocation_favorites: 'În favorite',
trackPreviewLocation_artist: 'În top piese ale artistului',
trackPreviewLocation_randomMix: 'În Mixul Aleatoriu',
preloadMode: 'Preîncarcă Următoarea Piesă',
preloadModeDesc: 'Când să înceapă preîncărcarea următoarei piese din coadă',
nextTrackBufferingTitle: 'Preîncarcă',
preloadHotCacheMutualExclusive: 'Preîncarcarea și Hot Cache au același scop — doar una poate fi activată deodată. Pornirea unei opțiuni o dezactivează pe cealaltă.',
preloadOff: 'Oprit',
preloadBalanced: 'Balansat (30 s înainte de sfârșit)',
preloadEarly: 'Devreme (după 5 s de redare)',
preloadCustom: 'Personalizat',
preloadCustomSeconds: 'Secunde înainte de sfârșit: {{n}}',
infiniteQueue: 'Coadă Infinită',
infiniteQueueDesc: 'Adaugă automat piese aleatorii când coada se golește',
experimental: 'Experimental',
fsPlayerSection: 'Player pe ecran complet',
fsLyricsStyle: 'Stilul versurilor',
fsLyricsStyleRail: 'Șină',
fsLyricsStyleRailDesc: 'Șină clasica pe 5 linii.',
fsLyricsStyleApple: 'Derulare',
fsLyricsStyleAppleDesc: 'Listă în derulare pe ecran complet.',
sidebarLyricsStyle: 'Stilul derulării versurilor',
sidebarLyricsStyleClassic: 'Clasic',
sidebarLyricsStyleClassicDesc: 'Derulează linia activă în centru.',
sidebarLyricsStyleApple: 'Ca Apple Music',
sidebarLyricsStyleAppleDesc: 'Linia activă este ancorată aproape de vârf cu animații netede.',
fsShowArtistPortrait: 'Arată poza artisului',
fsShowArtistPortraitDesc: 'Arată poza artistului (sau arta albumului) în partea dreaptă a playerului pe ecran complet.',
fsPortraitDim: 'Estomparea fotografiei',
seekbarStyle: 'Stilul barei de redare',
seekbarStyleDesc: 'Alege aspectul barei de redare a playerului',
seekbarTruewave: 'Truewave',
seekbarPseudowave: 'Pseudowave',
seekbarLinedot: 'Linie & Punct',
seekbarBar: 'Bară',
seekbarThick: 'Bară groasă',
seekbarSegmented: 'Segmentat',
seekbarNeon: 'Strălucire Neon',
seekbarPulsewave: 'Undă de Puls',
seekbarParticletrail: 'Urmă de Particule',
seekbarLiquidfill: 'Lichid',
seekbarRetrotape: 'Bandă Retro',
themeSchedulerTitle: 'Schimbă Tema automat',
themeSchedulerEnable: 'Pornește temele programate',
themeSchedulerEnableSub: 'Schimbă automat între două teme pe baza orei din zi',
themeSchedulerDayTheme: 'Temă de zi',
themeSchedulerDayStart: 'Ziua începe la',
themeSchedulerNightTheme: 'Temă de noapte',
themeSchedulerNightStart: 'Noaptea începe la',
themeSchedulerActiveHint: 'Temele programate sunt active - aceste schimbări de teme sunt gestionate automat.',
visualOptionsTitle: 'Opțiuni vizuale',
coverArtBackground: 'Arta de fundal a copertei',
coverArtBackgroundSub: 'Arată arta de copertă blurată ca fundal în antetele albumelor/playlisturilor',
playlistCoverPhoto: 'Poza de copertă a playlistului',
playlistCoverPhotoSub: 'Arată poza grilă de copertă în afișajul de detalii al playlistului',
showBitrate: 'Arată Bitrate',
showBitrateSub: 'Arată bitrate-ul audio în listările de piese',
floatingPlayerBar: 'Bară Player plutitoare',
floatingPlayerBarSub: 'Păstrează bara playerului plutind deasupra conținutului',
uiScaleTitle: 'Scara interfaței',
uiScaleLabel: 'Zoom',
};