xima.music - персональный музыкальный плеер для Android. Полностью офлайн: без аккаунтов, серверов и сетевых запросов. Музыка лежит в памяти телефона, а не внутри APK.

Главное архитектурное решение видно сразу: в вебе нет ни одного <audio>. Звук, нотификация, экран блокировки и Bluetooth - целиком на стороне Android.

proxima812/music.ximaИсходники: Tauri 2, Rust, Kotlin, SolidJS.

Три слоя и жесткие границы

TXT
SolidJS UI
    |  Tauri IPC (invoke / events)
Rust Core  (domain / application / infrastructure)
    |  tauri-plugin-player
Kotlin / Android  (Media3, ExoPlayer, MediaSessionService)
Слой Технологии
Оболочка Tauri 2 (Android)
UI SolidJS, TypeScript strict, Tailwind v4, HeroUI, Kobalte
Ядро Rust: домен, сервисы, репозитории
БД SQLite через sqlx, FTS5, версионированные миграции
Звук Kotlin, Media3/ExoPlayer, MediaSessionService

Кто чем владеет:

  • Kotlin и только Kotlin владеет воспроизведением: play, pause, seek, next, prev, shuffle, repeat, очередь на уровне плеера, нотификация, аудиофокус.
  • Rust владеет данными: библиотека, плейлисты, история, статистика, поиск.
  • Solid владеет только представлением и локальным состоянием интерфейса. Источник истины по треку и прогрессу - нативный плеер, он пробрасывает события.

Прямо запрещено: <audio>, HTMLAudioElement и Web Audio API для воспроизведения библиотеки; SQL-запросы из фронтенда напрямую; хранение библиотеки в Tauri Store; складывать mp3 в ассеты APK.

В цифрах слои выглядят так: 64 файла на Rust, 60 компонентов на Solid, 15 файлов на Kotlin в отдельном плагине tauri-plugin-player.

Что умеет

Библиотека собирается из общей медиатеки (MediaStore) и своих папок (SAF): песни, альбомы, исполнители, жанры, папки, 11 сортировок, виртуализованные списки. Очередь с «играть следующим», перестановкой и восстановлением после перезапуска.

Умные плейлисты - самая интересная часть: правила хранятся как данные и компилируются в SQL. 11 типов правил, 7 пресетов из коробки. Поиск через SQLite FTS5, история прослушиваний, счетчики, избранное.

Отдельная деталь: если у альбома нет обложки, рисуется детерминированный glow-градиент - один и тот же для одного альбома. Палитра взята из color.xima.work.

Тема только темная. Статистики, Android Auto, сон-таймера, эквалайзера и текстов песен нет сознательно.

Баги, которые построили этот проект

В репозитории лежит docs/BUGS.md, разделенный на три части: починенное, открытое и непроверенное - то есть места, где багов еще никто не искал. Такая третья категория встречается редко, а честности в ней больше, чем в первых двух.

Приложение не запускалось, а все проверки были зелеными

Функция run() в src-tauri/src/lib.rs не была помечена #[cfg_attr(mobile, tauri::mobile_entry_point)]. На Android приложение стартует из JVM, а не из main, и без этого макроса в .so нет JNI-точки входа, которую грузит MainActivity.

Коварство в том, что cargo check, cargo test и cargo clippy проходили полностью зелеными, и библиотека собиралась под aarch64-linux-android без единого предупреждения. Ошибка вылезала только при упаковке APK.

Комментарий на одну строку съел половину файла

Строка в Kotlin выглядела безобидно:

KOTLIN
/** `audio/*`, но не музыка: плейлисты. */

Блочные комментарии в Kotlin вложенные. Последовательность /* внутри текста открыла вложенный комментарий, закрывающая */ закрыла только его, а внешний остался открытым до конца файла - и проглотил две функции и три константы. Итог: 7 ошибок «unresolved reference» в трех разных файлах из одной опечатки.

Обратные кавычки не спасают, лексер их не смотрит. Лечение одно: не писать пару символов / и * внутри блочного комментария. Проверка баланса по всем Kotlin-файлам делается одной строкой на awk.

Кириллица в пути к проекту

Проект лежал в папке с русским названием, и это дало три разных симптома одной причины. Tauri CLI падал с паникой, читая собственный путь: кириллица приезжала в Rust невалидным UTF-8, и current_exe().unwrap() паниковал. tauri android init не мог определить путь к себе и писал в Gradle-таску node tauri вместо абсолютного пути. Экспорт LANG и LC_ALL не помогает: искажение происходит в мосте Node в Rust внутри самого CLI, а не в шелле.

Текущий обход - симлинк с латинским именем, из которого выполняются все команды. Обход, а не решение.

Конфликт версий Kotlin

Сгенерированный Tauri Android-проект пинит Kotlin Gradle plugin 1.9.25, а kotlinx-coroutines-android тянет kotlin-stdlib до 2.2.10. Компилятор 1.9 не читает метаданные 2.2, поэтому «не найденными» оказывались trim, takeIf, emptyList, let - весь stdlib разом.

Папка, которая перезаписывается

src-tauri/gen/android/ живет в .gitignore и перезаписывается при каждом tauri android init. При этом в ней лежат две правки, без которых сборка не проходит. То есть после любой регенерации сборка ломается двумя разными способами, и оба выглядят как загадочные ошибки Kotlin.

По этой же причине подпись APK вынесена отдельным шагом, а не в signingConfig Gradle: конфиг пришлось бы держать в папке, которая перезаписывается.

Про ключ подписи

Самая дорогая строчка в документации проекта: хранилище ключей и пароль в репозиторий не попадают и восстановлению не подлежат. Потеряли пароль - обновить установленное приложение уже нечем. Новый ключ означает новую подпись, поверх старой сборки она не встает, и пользователям придется удалять приложение вместе с плейлистами и историей.

Это уже случилось между версиями 1.0.1 и 1.1.0.

Сборка

Нужны Node 22+, Rust с таргетом aarch64-linux-android, Android SDK, NDK 27 и JDK 21.

Bash
npm install
npm run typecheck                 # tsc --noEmit
npm run rust:test                 # тесты ядра
npm run rust:lint                 # clippy -D warnings
npm run android:dev               # дев-сборка с hot reload на устройстве
npm run android:build -- --apk    # release, все ABI, без подписи
npm run release:sign              # выравнивание, подпись, проверка

Иконка приложения собирается из одного xima.music.png в корне, все остальное производное. После генерации фон адаптивной иконки приходится возвращать руками: шаблон Tauri кладет туда белый.

Документация как часть проекта

Файл О чем
AGENTS.md как писать код в этом проекте
docs/CONTRACTS.md типы, команды, события, схема БД - источник истины
docs/FEATURES.md что реализовано и в каком статусе
docs/BUGS.md починенное, открытое и непроверенное
docs/PROPOSALS.md что делать дальше

Разделение на «контракты» и «фичи» тут не формальность. Контракты - это то, о чем договорились три слоя между собой, и менять их в одном слое молча нельзя.

Релизы xima.musicОдин APK на все архитектуры: arm64, arm32, x86, x86_64.