The library page compares listen-through, early-skip and like rates per
impression model version, so v1 and the trained ranker can be compared online.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GET /onboarding offers the library's top artists (with album covers)
and genres and says whether to show it (not done and < 10 plays);
PUT /me/onboarding stores the choice or a skip (migration 00009).
- The recommender seeds the taste from it while the user has fewer than
30 scored tracks: chosen artists' tracks +1, chosen genres +0.5.
- Web: onboarding replaces the wave on the home page until done or
skipped; artist grid with covers, genre chips, at least 3 artists.
- e2e journey and the recommender DB test cover it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Ambient background in the app shell: three drifting blurred blobs,
the third one tinted with the current track's cover; can be turned off
in settings and stays still with reduced motion.
- Island player variant (floating glass panel) next to the bar, chosen
in settings; player source line ("Album · …", "My Wave"), reactions
around the transport controls, queue drawer.
- Sidebar: like / dislike counters, pending uploads badge on Queue,
admin Settings page holding the upload queue limits.
- Collection: header tile with kicker, underline tabs, time / number
lead column with an equalizer for the current track, album column,
dimmed dislikes with undo, Play all / Shuffle pills.
- Album: cover-tinted header and Play / Shuffle; artist: artist wave,
Play and popular tracks (new GET /artists/{id}/top-tracks).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Suggest tracks: drag-and-drop of files and folders with relative
paths, streaming SHA-256 in a Web Worker (hash-wasm), duplicate
precheck, batch reservation and tus-js-client upload (8 MB chunks,
4 in parallel, token refresh on 401, resume of failed files).
- My uploads: statuses, rejection reasons, withdraw, quota meter.
- Upload queue (admin): status tabs, possible-duplicate filter, batches,
metadata and storage editing, bulk approve / reject with a reason,
import retry, and the queue settings form.
- Sidebar section for moderators and admins; ru / en strings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Migration 00008: upload_batches, upload_files, upload_quota.
- Quota reserved under a row lock when a batch is created; every status
change goes through one transition that frees space in the same
transaction. SHA-256 duplicates are rejected before any upload.
- tus 1.0.0 core (HEAD/PATCH) for the author only; offset is the real
staging file size, data beyond the declared size is refused (ADR-015).
- Processing worker: ffprobe checks, SHA-256 verification, exact and
Chromaprint duplicates against library and queue, metadata from tags
or the dropped folder path, possible_duplicate on metadata match, and
the target path from write_template reusing existing folder spelling.
- Import worker: edited tags written with ffmpeg stream copy, file
written by storage-agent, point scan of the new file, imported status;
idempotent and resumed after restart. Stale uploads expire after 24 h,
orphan staging files are removed at startup.
- API: quota, precheck, batches, my uploads, withdraw, admin list,
metadata edit, bulk review, retry and queue settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- storage-agent: POST /internal/files copies a staged file into a source
(temp file, fsync, SHA-256 check, hard link so nothing is overwritten,
" (2)" suffix on conflict, owner PIPU_FILE_UID/GID) and
POST /internal/files/test. NFS sources that accept uploads are mounted
rw in an agent-only /library-rw and bind-mounted read-only for others;
local sources are written through /library-host-rw.
- meta: write templates ({track:02}, {disc:1-}, [optional]) with name
sanitising and length limits, suggestion from the source's regexes
and a reversibility check against 50 catalogue tracks.
- API and admin UI: accepts_uploads / write_template on sources,
write-template suggest and check endpoints, write test result.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- My Wave is rendered with react-native-skia on the UI thread; physics
and colour helpers moved to @pipu/core (wave-anim) and shared with web.
- Full-screen player honours ui_prefs.mobile_player (cover / wave).
- Theme switching (dark / light / system) via a theme context and
useColors() across all screens.
- Profile: theme, wave variant, player view, sound quality sheet
(Wi-Fi / mobile / downloads), recent-tracks cache clearing on next
cold start, and a change-password sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Downloads queue (2 parallel, Wi-Fi only by default) stores files under
the app's document directory and track metadata in SQLite; Collection
gets a Downloads tab, album and player screens get download buttons.
- Player plays downloaded files and local covers; My Wave falls back to
a shuffle of downloaded tracks when the server is unreachable.
- Playback events that fail on the network are kept in SQLite and sent
when the connection returns.
- AuthSession can open offline with the cached user and fetches an
access token before the first request once online.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Checks the server for a newer release at most every 6 hours and on
demand in Profile, shows release notes, downloads the APK with progress,
verifies SHA-256 in 1 MB chunks and hands it to the system installer.
Updates below min_supported cannot be dismissed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/v1/app/android/latest reads the latest Gitea release with the
server token (cached for 10 minutes); the download endpoint caches the
APK under /updates, verifies its SHA-256 and serves it with Range.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- expo-router app in src/app: connect (server check, recent servers),
login, mandatory credential / password change, tabs Home / Search /
Collection / Profile, album and artist pages, full-screen player
- AuthSession per server with the refresh token in expo-secure-store
(Android Keystore); language from the profile
- player on react-native-track-player: ExoPlayer queue with a
notification and lock-screen controls via a playback service, 500 MB
stream cache, quality by network (Wi-Fi high, cellular medium),
play events with played time, reactions, playback state restore and
the endless My Wave with feedback
- home: pulsing wave circle driven by BPM / energy in the cover
palette, activity and mood chips that switch the wave on the fly
- New Architecture is off until react-native-track-player supports it;
the release build runs codegen only when it is on; query-string is a
direct dependency because expo-router 5.1 imports it undeclared
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- WaveButton: TypeScript port of the prototype's canvas animation
(orb and liquid), breathing at rest, pulsing to the track's BPM and
energy in the cover palette, following the cursor, pressing with a
spring; reduced motion and hidden tabs slow or stop it
- home: the wave in the centre with activity and mood chips; changing
them while playing rebuilds the upcoming tracks without stopping
- player wave mode: asks for the next batch when two tracks are left,
sends complete / skip / early skip / like / dislike as feedback and
refreshes the queue right after a reaction or an early skip
- tracks expose bpm, energy and loudness once analysed; library page
shows analysis progress and 7-day wave metrics; wave style setting
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /wave/options, POST /wave/sessions (activity, mood, seed),
/next with reactions, PATCH to switch the context on the fly, DELETE
- the recommender is called with an 800 ms budget; on failure the api
serves a fallback wave of likes, favourite artists and random
available tracks with the same dislike / recent / served filters
- every served track is an impression with its source and model; wave
events (context_type wave) set its outcome, reactions win
- library status shows the analysis queue and 7-day wave metrics:
listen-through, early skip and like rate
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- core: PlayQueue with shuffle and repeat, PlayAttempt counting only
time actually played, EventBuffer with batching and backoff, uuidv7
- web: Player over HTMLAudioElement with Media Session, preload of the
next track, expired-link refresh, local and server playback state,
hotkeys; PlayerProvider is not wired into the shell yet
- ru/en strings and icons for the player and Collection
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tracks carry a stream_url signed for the user for 12 h and the
user's reaction; /stream checks the signature, expiry and that the
user is not blocked, then hands the file to nginx by X-Accel-Redirect
(or serves it with Range support when X_ACCEL=false)
- original by default; formats browsers can't play and explicit
high/medium/low go through Opus transcoding, never upwards: a lossy
source under the target bitrate stays original; transcodes are
cached per file and size, deduplicated in flight, limited by
TRANSCODE_CONCURRENCY and trimmed hourly by LRU to the size limit
- /events takes up to 500 events with client UUIDs, drops repeats and
events older than 30 days, counts plays (≥ half or 4 min, or
finished), early skips and history entries (≥ 30 s)
- history with clearing, likes and dislikes with cursors and totals,
track reactions, playback state with last-write-wins, tracks batch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- covers are taken from the embedded picture of the first track or a
cover/folder/front image in the album folder, converted by ffmpeg to
WebP 64/300/1200 and named by content hash, so albums sharing a cover
share the files; a full scan looks again for albums without one
- a 3-colour palette (dominant, second, most saturated accent) from a
deterministic k-means feeds placeholders, page tints and the wave
- /covers/{id} is public but HMAC-signed and immutable; nginx serves
the file via X-Accel-Redirect, X_ACCEL=false serves it from api
- the web client picks the size by display size and pixel density
- install.sh and update.sh generate STREAM_SIGNING_KEY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Storage admin: local folders with a folder picker and NFS sources,
connection test, scan and full scan with live progress, enable,
disable, delete
- path pattern editor with reorder, examples and a preview table that
shows which pattern matched and what came from tags; compile errors
land under the broken pattern
- Library page with counters, scans and unreadable files; search
reindex for admins
- search box in the top bar: combobox with keyboard navigation, 150 ms
debounce, cancelled stale requests, "/" and Ctrl+K, layout fix hint
- search page with tabs and paging, artist and album pages, album
cards and track lists; unavailable tracks are dimmed
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- artists, albums and tracks indexes are rebuilt from PostgreSQL next
to the live ones and swapped in, so search keeps working; a rebuild
runs on start, after every scan and when a source is removed
- transliteration fields (Кино ↔ kino) and a retry in the other
keyboard layout (ghbdtn → привет) reported as corrected_query
- /search/suggest (3 artists, 3 albums, 5 tracks), /search by type
with cursor pages, /admin/search/reindex; Meilisearch is part of
/readyz
- found ids are hydrated from the catalog, so a lagging index never
returns deleted entries
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- schema for sources, path patterns, media files, artists, albums,
tracks, genres (migration 00003)
- path patterns (RE2 with named groups, skip), merge with tags by
source priority, fallbacks from the file name and folder, name
normalization and multi-artist split
- scanner: walk, skip unchanged files by size and mtime, ffprobe in 8
workers with a single catalog writer, renames by content hash keep
the track, duplicates fold into one track with the best file as
primary, missing files are kept 30 days
- storage-agent mounts NFS with validated server, export and an option
whitelist, no shell; test mount counts audio files
- admin API: sources CRUD, connection test, patterns with preview,
scan, library status, file errors, host folder picker; scheduled
scans and health checks; mounts are restored on start
- catalog API: artist, artist albums, album with tracks, track
- api image is alpine with ffmpeg for ffprobe
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- core: AuthSession keeps the access token in memory, refreshes it
once for all parallel requests (single-flight) and retries them;
refresh races between tabs are retried, reuse signs out; ApiError
carries problem+json codes and field errors
- web: TanStack Router with guards — guests go to /login, the bootstrap
admin to /setup/admin, temporary passwords to /setup/password, admin
pages by role
- login, setup screens, Classic shell (top bar, role-based sidebar,
empty player), settings: name, theme, language, password, devices
- users admin: search, role filter, create with a one-time temporary
password, reset, block, role change, delete as the policy allows;
audit log for admins
- ru/en strings for auth, admin and error codes; language and theme
are saved in the profile
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /admin/users: list with search, role filter and cursor pages; create
with a one-time temporary password; rename, change role, block;
reset password; delete
- every change is checked against the role policy, a forbidden field
rejects the whole patch; the last active admin is locked in the
transaction and cannot be demoted, blocked or deleted
- role change and blocking revoke access tokens at once; blocking and
password reset also end all sessions
- /admin/audit: newest-first journal with actor names
- path and query parameter errors are problem+json now
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- users, refresh_tokens and audit_log schema (migration 00002)
- argon2id passwords with rehash on login, password policy, lockout
with exponential back-off and a per-IP rate limit in Redis
- Ed25519 JWT access tokens with kid-based key rotation; rotating
refresh tokens with reuse detection (cookie for web, body for Android)
- bootstrap admin on first start; must_change_credentials and
must_change_password gate every other endpoint
- /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password,
/me/bootstrap-credentials, /me/sessions; server-info reports
bootstrap_required
- role policy from docs/auth-and-roles.md with table tests
- problem+json errors with field errors; ru/en translations of the codes
- install.sh and update.sh generate JWT_PRIVATE_KEY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ci / docker (образы собираются — их сервер собирает сам при обновлении) (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
Security and robustness:
- storage-agent rejects an empty INTERNAL_TOKEN (config notEmpty + middleware)
- api takes the client IP only from X-Real-IP (chi RealIP trusted True-Client-IP)
- /readyz reports ok/fail and logs the cause; checks run in parallel with a timeout
- storage-agent shuts down gracefully; shared httpx.NewServer/Serve with IdleTimeout
- goose migrations take a Postgres advisory lock
- recommender readyz: per-check timeout, redis socket timeouts
Clients:
- Android release allows cleartext HTTP for LAN servers (expo-build-properties)
- checkServer times out after 10s; connect button is disabled while checking
Deploy and CI:
- install.sh hands cache/uploads/updates/models to the non-root service users
- update.sh refuses downgrades unless PIPU_ALLOW_DOWNGRADE=1
- recommender/analyzer get only the env they need; required secrets fail fast
- nginx security headers and CSP; strip True-Client-IP
- dev compose binds ports to 127.0.0.1; postgres/redis images pinned
- version.mjs fails on a malformed explicit tag instead of releasing 0.0.0-dev
- contract check catches untracked generated files
- final releases fail without a release keystore; changelog skips rc tags
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>