Files
pipu-music/docs/mobile.md
T
mrleo1nidandClaude Opus 5.5 ebf296542c
ci / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m55s
ci / api (push) Successful in 51s
ci / mobile (Metro-бандл Android) (push) Canceled after 0s
release / images (services/api/Dockerfile, storage-agent, storage-agent) (push) Canceled after 0s
release / images (services/recommender/Dockerfile, recommender, ) (push) Canceled after 0s
ci / recommender (push) Successful in 1m11s
ci / ts (packages + web) (push) Canceled after 3s
release / verify (push) Successful in 1m16s
release / images (apps/web/Dockerfile, web, ) (push) Failing after 2m40s
release / android + release (push) Skipped
release / images (services/api/Dockerfile, api, api) (push) Canceled after 1s
feat: M0 monorepo skeleton with services, clients, deploy and CI
- api/openapi.yaml: contract with /healthz, /readyz, /api/v1/server-info
- services/api (Go): api with oapi-codegen strict server, goose migrations,
  readiness checks for Postgres/Redis; storage-agent stub with internal token
- services/recommender (Python, uv): FastAPI health/readiness/internal status,
  `pipu-rec serve` and `pipu-rec analyze` entrypoints
- packages: generated api-client, core (server URL + server check),
  i18n (ru/en + dictionary check), design-tokens (CSS vars + TS from tokens.json)
- apps/web: Vite + React shell showing server status, language switch
- apps/mobile: Expo (SDK 53) Android app with server connect screen
- deploy: Dockerfiles, compose (prod + dev), nginx template, install/update scripts
- .gitea/workflows: ci.yml and tag-based release.yml (images, signed APK, release)
- scripts/version.mjs: version and Android versionCode from git tag
- tests/fixtures/music/generate.sh: synthetic test library via ffmpeg
- docs: versionCode formula, OpenAPI 3.0.3, dev instructions in README

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 23:55:11 +03:00

9.4 KiB
Raw Blame History

Android-приложение (apps/mobile)

Стек

Задача Выбор
Фреймворк React Native (New Architecture) + Expo SDK, prebuild / dev client (нативный код под контролем)
Навигация expo-router (файловые маршруты)
Аудио react-native-track-player v4 — ExoPlayer, MediaSession, уведомление, экран блокировки, Android Auto-совместимость
Анимация волны @shopify/react-native-skia (порт wave.js, общие формулы в packages/core/wave-anim) + react-native-reanimated
Серверное состояние TanStack Query (+ persist в MMKV)
Клиентское состояние Zustand (общие сторы из packages/core)
Хранилище react-native-mmkv (настройки, кэш), expo-sqlite (офлайн-каталог, очередь событий), expo-secure-store (refresh-токен)
Загрузки expo-file-system (фоновые загрузки, resumable)
Сеть @react-native-community/netinfo (Wi-Fi / сотовая → качество)
i18n i18next, словари из packages/i18n
Минимальная версия Android 8.0 (API 26), target — актуальный API

Экраны

Макет — Pipu Android.dc.html, сводка решений — design.md.

  • Подключение и вход: адрес сервера (проверка GET /server-info: версия сервера, HTTPS, поддерживается ли эта версия приложения), список сохранённых серверов, затем логин, пароль, язык. Далее те же setup-экраны, что и в вебе.
  • Нижняя навигация: Главная · Поиск · Коллекция · Профиль. Мини-плеер над навигацией; свайп вверх — полноэкранный плеер в одном из двух видов (выбор в профиле): cover — крупная обложка (по умолчанию) или wave — визуализация волны; прогресс, лайк / дизлайк, скачать, очередь (нижний лист).
  • Поиск: недавние запросы (хранятся локально), подсказка про опечатки и раскладку, значок «скачано» у треков.
  • Главная: кнопка «Моя волна» (orb или liquid по настройке, тот же алгоритм, что в вебе, на Skia; пульс в такт BPM / energy из features трека — на Android спектр недоступен из ExoPlayer без нативного модуля, поэтому анимация управляется метаданными и позицией трека), выбор занятия / настроения.
  • Коллекция: История, Лайки, Дизлайки, Скачанное.
  • Коллекция → Скачанное: занято на устройстве / свободно в телефоне, «Скачивать новые лайки», «Скачать все лайки», очередь загрузок с пометкой «по Wi-Fi».
  • Профиль: смена пароля (лист), язык, тема, вид волны, вид плеера, качество звука (лист: Wi-Fi / сотовая / загрузки), «только Wi-Fi для загрузок», кэш недавних треков (размер + «Очистить»), проверка обновлений (точка, если есть новая версия), текущий сервер, «Админка — откроется в браузере» (для admin / moderator), выход.
  • Офлайн-баннер вверху, когда нет связи с сервером.
  • Админку в мобильном не делаем на первом этапе (ссылка «открыть в браузере»).

Воспроизведение

  • packages/core содержит общую логику очереди, событий и сохранения позиции; мобильный реализует адаптер AudioEngine поверх react-native-track-player.
  • Фоновый сервис плеера (PlaybackService) обрабатывает кнопки уведомления / гарнитуры и шлёт события, даже когда UI выгружен.
  • Если трек скачан — играется локальный файл, иначе стрим с качеством по типу сети.
  • Встроенный кэш ExoPlayer (maxCacheSize 500 МБ) для недавних стримов.
  • Аудиофокус: пауза при звонке, приглушение при навигационных подсказках.

Офлайн

  • Пользователь скачивает трек, альбом, «все лайки» (опционально — авто-загрузка новых лайков).
  • Файлы — во внутреннем хранилище приложения (documentDirectory/offline/<trackId>.opus), не видны другим приложениям.
  • SQLite хранит метаданные скачанных треков (название, исполнитель, обложка-файл, длительность, features) — раздел «Скачанное» и плеер работают полностью без сети.
  • Очередь загрузок: не более 2 параллельно, возобновление после обрыва, «только по Wi-Fi» по умолчанию.
  • Без сети:
    • доступны «Скачанное» и офлайн-волна (локальный упрощённый алгоритм: перемешивание скачанных с учётом лайков и энергии под выбранный контекст);
    • события пишутся в SQLite-очередь и отправляются при появлении сети (POST /events, идемпотентно);
    • реакции (лайк / дизлайк) — оптимистично локально + очередь мутаций.
  • Подписанные ссылки не кэшируются — при восстановлении сети запрашиваются заново.

Самообновление

Приложение распространяется только через Gitea Releases (не Google Play).

Поток:

  1. При запуске (не чаще раза в 6 ч) и вручную в профиле: GET /api/v1/app/android/latest на сервере Pipu. Сервер сам опрашивает Gitea API (GET /api/v1/repos/{owner}/{repo}/releases/latest) с серверным токеном, кэширует ответ на 10 мин и APK на диске. Так:
    • мобильному приложению не нужен доступ к Gitea и токен (репозиторий может быть приватным);
    • версия приложения согласована с версией сервера (min_supported из server-info).
  2. Если version_code новее — диалог с changelog (Markdown из релиза), кнопки «Обновить» / «Позже». Если текущая версия ниже min_supported — обновление обязательно.
  3. Загрузка APK через expo-file-system в cacheDirectory с прогрессом; проверка SHA-256 (значение из релиза).
  4. Установка: Intent.ACTION_VIEW с content:// URI через FileProvider и флагом FLAG_GRANT_READ_URI_PERMISSION (небольшой локальный Expo-модуль modules/apk-installer на Kotlin; при необходимости — PackageInstaller session API).
  5. Если нет разрешения «установка из неизвестных источников» — открываем Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES для нашего пакета с пояснением.

Требования:

  • В манифесте: REQUEST_INSTALL_PACKAGES, FileProvider.
  • Все релизы подписываются одним и тем же ключом (keystore в секретах CI) — иначе Android откажет в обновлении.
  • versionCode монотонно растёт: major·1 000 000 + minor·10 000 + patch·100 + (99 | rc) (см. ci-cd.md).
  • Ассеты релиза: pipu-music-<version>.apk, pipu-music-<version>.apk.sha256.

Безопасность

  • Refresh-токен — expo-secure-store (Android Keystore).
  • HTTPS обязателен; для self-signed сертификатов — возможность добавить пользовательский CA через network_security_config (собирается в CI по флагу) или пиннинг отпечатка, введённого при подключении к серверу.
  • android:allowBackup="false" для данных с токенами.