HomeMusic — личный музыкальный сервис: Navidrome + Хаб (FastAPI, React PWA) + Android-приложение
  • Python 70.6%
  • TypeScript 27.2%
  • PowerShell 1.1%
  • JavaScript 0.4%
  • Shell 0.3%
  • Other 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-12 11:07:34 +03:00
embedder HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
eval HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
gateway HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
hub Hub: optional Soulseek source for downloads, Spotify export import 2026-11-12 11:07:34 +03:00
navidrome HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
scripts HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
web Hub: optional Soulseek source for downloads, Spotify export import 2026-11-12 11:07:34 +03:00
.dockerignore HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
.env.example Hub: optional Soulseek source for downloads, Spotify export import 2026-11-12 11:07:34 +03:00
.gitattributes HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
.gitignore HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
config.example.yaml Hub: optional Soulseek source for downloads, Spotify export import 2026-11-12 11:07:34 +03:00
docker-compose.yml Hub: optional Soulseek source for downloads, Spotify export import 2026-11-12 11:07:34 +03:00
HomeMusic.bat HomeMusic snapshot (7ba7c35) 2026-11-08 17:20:26 +03:00
README.md Hub: optional Soulseek source for downloads, Spotify export import 2026-11-12 11:07:34 +03:00

HomeMusic

Личный музыкальный сервис на домашнем ПК: библиотека и стриминг — Navidrome, плеер на телефоне — Tempus, поиск, скачивание, загрузка своих файлов и ИИ-ассистент — свой сервис Хаб (FastAPI + PWA). API Хаба — hub/API.md.

Скачать Android-приложение: HomeMusic.apk (все версии — Releases). Приложению нужен свой сервер HomeMusic: вход только по приглашению его владельца.

Телефон ──HTTPS──> VPS 203.0.113.10 (Caddy, сертификат Let's Encrypt на IP)
                     :8444 → 127.0.0.1:4533 ─┐
                     :8445 → 127.0.0.1:8080 ─┤ обратный SSH-туннель (пользователь homemusic-tunnel)
                                             ▼
             ПК (Docker Desktop): homemusic_tunnel → navidrome:4533, hub:8080
                                  MUSIC_ROOT = B:\Music (library/, staging/, .trash/, .backups/)
Что Адрес снаружи Адрес дома
Navidrome (Tempus, веб-плеер) https://203.0.113.10:8444 http://192.168.1.10:4533
Хаб (PWA) https://203.0.113.10:8445 http://192.168.1.10:8080

В обычном браузере Хаб — весь сервис без приложения: Главная, библиотека Navidrome («Понравившиеся», плейлисты, исполнители, альбомы) и плеер с очередью. Внутри Android-приложения он открывается только своими разделами.

Логин и пароль везде одни — учётка Navidrome.

Запуск с нуля

  1. Docker Desktop (WSL2), Git. Docker Desktop должен стартовать при входе в Windows (сейчас так и есть). Сон ПК отключён (питание от сети → «никогда»).
  2. Папка музыки: B:\Music с подпапками library, staging, .trash, .backups.
  3. Настройки:
    copy .env.example .env          # заполнить секреты (см. комментарии в файле)
    copy config.example.yaml config.yaml
    
    LLM_API_KEY — ключ OpenRouter для ИИ-ассистента (без него работают ссылки и текстовые списки, но не скриншоты). LASTFM_API_KEY — ключ Last.fm для «Микса дня» (похожие треки и исполнители, теги для названия; без него микс собирается из остальных источников).
  4. Ключ туннеля (один раз):
    ssh-keygen -t ed25519 -N "" -C homemusic-tunnel -f gateway\tunnel\tunnel_key
    ssh-keyscan -t ed25519 203.0.113.10 > gateway\tunnel\known_hosts
    
  5. VPS (один раз, идемпотентно; другие сервисы на VPS не трогает, все изменённые файлы бэкапит в /root/homemusic-gateway-backup/):
    scp gateway\vps\setup-vps.sh myvps:/root/homemusic-setup-vps.sh
    ssh myvps "bash /root/homemusic-setup-vps.sh 203.0.113.10 '<содержимое gateway\tunnel\tunnel_key.pub>'"
    
  6. Запуск:
    docker compose up -d --build
    
  7. Первый запуск Navidrome: админ создаётся в веб-интерфейсе http://localhost:4533 (или его уже создал установщик — логин/пароль в .env: NAVIDROME_ADMIN_USER / NAVIDROME_ADMIN_PASSWORD). Остальных пользователей заводит админ в Navidrome → «Пользователи».
  8. Телефон:
    • Tempus (https://github.com/eddyizm/tempus/releases, app-tempus-release.apk): сервер https://203.0.113.10:8444.
    • Хаб: открыть https://203.0.113.10:8445 в Chrome → меню → «Установить приложение». После этого Хаб есть в меню «Поделиться» Android: скриншот → ассистент, аудиофайл → загрузка в библиотеку.

Android-приложение HomeMusic (форк Tempus)

Исходники — android/ (отдельный git-репозиторий, ветка homemusic; в этот репозиторий не входит). Это Tempus с вкладкой «Хаб» (Каталог, Загрузки, Ассистент, Мои файлы), «Поделиться» → Хаб, уведомлениями ассистента и обновлениями из Хаба. Статусы загрузок на сервер видны в разделе «Загрузки». Android показывает прогресс по трекам и итог при ручном скачивании на телефон. Во время автосинхронизации на телефон служба Android может показать общий статус синхронизации. Ставится рядом с оригинальным Tempus (app.homemusic).

  • Установить на телефон: открыть https://203.0.113.10:8445/app/latest.apk в браузере телефона, разрешить установку. Вход: сервер (предложен https://203.0.113.10:8444) → имя и пароль, или «Регистрация по коду» (приглашение), или Google (см. «Аккаунты» ниже). Адрес Хаба по умолчанию уже прописан (настройки → «HomeMusic»).
  • Выпустить обновление: из корня проекта на чистых master и Android homemusic выполнить одну команду:
    pwsh -File .\scripts\release-android.ps1 -Notes 'Что нового по-русски'
    
    Она сама выбирает следующую patch-версию и versionCode, собирает release (тесты — см. ниже: не повторяются для коммита, который уже прошёл полный прогон; сама сборка — из кеша Gradle, если задачу закончили предсборкой, см. «Время»), находит APK по output-metadata.json этой сборки и проверяет пакет, версию, одну подпись (тот же сертификат, что у прежнего релиза) и ровно одну универсальную запись. Дальше по неизменяемой копии APK она строит bsdiff40-v1-патчи от последних шести версий и каждый проверяет восстановлением точного APK, кладёт файлы в B:\Music\.apk\artifacts\ (адреса по SHA-256), скачивает их через Хаб, ещё раз восстанавливает APK из принятых байтов и только потом атомарно заменяет latest.json. Хаб должен быть уже выкачан с маршрутом /app/artifacts (иначе команда остановится до переключения). Для minor/major версии допускается -VersionName '1.15.0'.
    • Тесты. testHomemusicDebugUnitTest не нужно гонять дважды. Перед слиянием вместо голого запуска Gradle выполняют предсборку (см. «Время»): она гоняет те же тесты. Полный прогон на чистом дереве записывает отметку для коммита (recordHomeMusicTestsPassed в app/build.gradle, файл homemusic-tests-passed/<коммит> в общем git-каталоге android/.git, он общий для всех worktree). Это прогон из правил проекта (после rebase): fast-forward в homemusic сохраняет коммит, поэтому отметка доходит до выпуска. Команда пропускает тесты, только если отметка есть для точного HEAD и дерево чистое; иначе (другой коммит, грязное дерево, прогон с --tests, красные тесты) запускает их сама.
    • Время. В конце команда печатает таблицу времени по этапам. Патчи и их проверка (в том числе повторное восстановление из байтов Хаба) идут параллельно, по процессу на каждую базу: байты патчей от этого не меняются. Результат aapt и apksigner для прежних APK кешируется по SHA-256 в B:\AndroidEmu\release-cache\apk-info.json (файл можно удалить — проверки просто выполнятся заново). Без кеша Gradle занимал 3,5–4 мин (R8 около 2,5 мин и lintVital около 2,3 мин идут одновременно; ПК владельца). Теперь включён кеш сборки Gradle (org.gradle.caching=true, хранилище в B:\AndroidEmu\gradle-home\caches\build-cache-1; вход javac для Room сделан переносимым — RoomSchemaArgs в app/build.gradle, поэтому ключ кеша не зависит от папки). Предсборка — последний шаг задачи перед слиянием, из корня worktree задачи на закоммиченном чистом дереве после rebase:
      pwsh -File .\scripts\release-android.ps1 -Prebuild -AndroidPath '<Android worktree>'
      
      Она берёт из живого B:\Music\.apk (только чтение, ничего не пишет и не блокирует) ту же следующую версию, что получит выпуск, гоняет testHomemusicDebugUnitTest assembleHomemusicRelease с этой версией и печатает версию и таблицу времени. После merge --ff-only дерево не меняется, поэтому настоящий выпуск берёт Kotlin, javac, R8, lint и упаковку из кеша, а APK получается побайтно тем же. Если между предсборкой и выпуском вышла другая версия или изменился коммит, кеш промахивается: выпуск идёт обычным путём (дольше, но верно) и пишет подсказку, когда Gradle-этап занял больше минуты. Измерено на ПК владельца (R8, lint, Kotlin и javac целиком из кеша): предсборка с холодным кешем — 4 мин 4 с (тесты и сборка); сборка той же версии и коммита в другой папке — 13–25 с вместо 3,5–4 мин; вся команда выпуска вместе с шестью патчами — около 40 с. APK, собранный в worktree, отличается от собранного в основной папке одним файлом (META-INF/version-control-info.textproto, его пишет Gradle по git-каталогу папки), на работу не влияет. Если Gradle-этап выпуска всё равно длинный (скрипт напишет подсказку), а версия и коммит те же — ключ кеша разошёлся из-за содержимого файлов: например, в основной папке остался файл с CRLF, а git считает дерево чистым (core.autocrlf=input). Сравнить размеры отслеживаемых файлов основной папки и worktree; такие файлы пересоздать из HEAD (удалить и git checkout -- <файл>). Так нашлись и исправлены nav_graph.xml, nav_drawer.xml, NavigationHelper.java.
    • Повтор. Исходники уже опубликованы — команда только сверяет файлы с latest.json и ничего не меняет. После сбоя latest.json остаётся прежним (клиенты ничего не заметили); повтор убирает непубликованный HomeMusic-*.apk сбойного запуска, берёт тот же versionCode и достраивает выпуск. Второй параллельный запуск останавливается на блокировке. Файлы в artifacts\, не нужные текущему релизу, удаляются через 7 дней.
    • Нужны Android SDK и JDK по путям из android/CLAUDE.md, ключ android/keystore/homemusic-release.jks и android/keystore.properties (вне git). Сохраните копию ключа: без него установить обновление поверх приложения нельзя.
    • Что получает телефон. Старые клиенты и первая установка — полный APK (/app/latest.apk). Новые выбирают самый маленький проверенный патч для точного хеша установленного APK, без подходящего — полный; системная установка остаётся. На лимитной сети при сбое патча полный APK скачивается только после согласия. Размер патча зависит от изменений кода: DEX хранится без сжатия (packaging.dex в app/build.gradle), патч на соседнюю версию 1,2–1,8 МБ (со сжатым DEX было 3,7 МБ), сборка без изменений кода — около 16 КиБ; полный APK 15,0 МБ (был 11,4 МБ). 10 КиБ — ориентир, не порог публикации.
  • Изолированный пробный выпуск: pwsh -File .\scripts\release-android.ps1 -Notes 'Проверка' -StagePath '<отдельная папка>' -AndroidPath '<Android worktree>' [-HubUrl http://127.0.0.1:18080]. В папку staging можно заранее скопировать архивные APK и latest.json для проверки перехода (с -Prebuild -StagePath версия для предсборки берётся из этой же папки). Живой каталог команда не использует; с -HubUrl раздачу проверяет указанный Хаб (например, временный контейнер с маршрутами system.py).

Аккаунты: приглашения, регистрация по почте и вход через Google

Аккаунт — пользователь Navidrome. Новые аккаунты появляются по приглашению или, если настроена почта, по email (ниже, «Регистрация по почте»). Приглашение:

  1. Админ: Хаб → «Ещё» → «Пользователи и приглашения» → «Пригласить», вписать, для кого (имя попадёт в Navidrome), выбрать срок. Появится одноразовый код вида ABCD-EFGH — его можно скопировать или отправить кнопкой «Поделиться».
  2. Человек ставит приложение, на экране входа оставляет предложенный сервер (https://203.0.113.10:8444), нажимает «Далее», затем «Регистрация по коду», вводит код, имя и пароль. Хаб создаёт пользователя в Navidrome (через NAVIDROME_ADMIN_USER/PASSWORD) и сразу входит. Код одноразовый, срок по умолчанию — INVITE_DAYS (7 дней).

Там же, в «Пользователях», админ видит каждого: сколько потратил на ИИ (всего и за 30 дней), сколько песен добавил и скачал, ♥ и плейлисты, когда был в Хабе и в плеере; по нажатию — расходы по месяцам и последние запросы.

Для обычных пользователей ассистент принимает 5 сообщений в день; для администратора лимита нет. Лимит общий для всех чатов, скриншотов и ссылок и обновляется в полночь по часовому поясу Хаба (TIMEZONE, по умолчанию Europe/Moscow). Новый чат, удаление истории и перезапуск Хаба лимит не сбрасывают. Принятый запрос считается и при последующей ошибке ИИ; подтверждение готовых действий и просмотр истории бесплатны. В одном сообщении — до 15 фотографий (каждая до 15 МБ) и 5000 символов текста, включая пробелы; эти ограничения действуют и для администратора. Под полем ввода видны счётчики, превышение не расходует дневную квоту.

Регистрация по почте (по желанию, разовая настройка ~5 минут)

Без настройки её нет. С ней любой, кто знает адрес сервера, сам заводит аккаунт: «Регистрация по почте» на экране входа (приложение и PWA) → почта → «Получить код» → код из письма (6 цифр, 15 минут), имя и пароль. Админ ничего не подтверждает; потом можно входить и по имени, и по почте. Письма уходят через SMTP любого ящика — бесплатно через Gmail (до ~500 писем в сутки):

  1. Завести для сервера отдельный ящик Gmail (например, homemusic.noreply@gmail.com) и включить в нём двухэтапную проверку: https://myaccount.google.com/security.
  2. https://myaccount.google.com/apppasswords → имя HomeMusic → «Создать» → скопировать пароль из 16 букв.
  3. В .env: SMTP_HOST=smtp.gmail.com, SMTP_PORT=587, SMTP_USER=<ящик>, SMTP_PASSWORD=<пароль из шага 2>, затем docker compose up -d hub.

Подойдёт и другой ящик (Яндекс: smtp.yandex.ru, порт 465, пароль приложения) или бесплатный тариф Brevo (smtp-relay.brevo.com, 587, 300 писем в сутки). Защита: на один адрес — не чаще письма в минуту и 5 в час, 5 неверных кодов гасят письмо, всего за сутки — не больше EMAIL_DAILY_LIMIT (450, чтобы не упереться в лимит Gmail). Если писем нет — docker logs homemusic_hub | Select-String letter покажет ответ почтового сервера.

Вход через Google (по желанию, разовая настройка ~15 минут)

Без настройки кнопки Google нет, остальное работает. Google не пускает вход на сайт по голому IP, поэтому вход через Google есть только в приложении; Хаб лишь проверяет выданный Google токен (секрета клиента нет).

  1. https://console.cloud.google.com → создать проект HomeMusic.
  2. «Google Auth Platform» (APIs & Services → OAuth consent screen) → «Get started»: имя HomeMusic, ваша почта, Audience — External. Затем «Audience» → «Publish app» (для входа с почтой и именем проверка Google не нужна; иначе войти смогут только адреса из списка «Test users»).
  3. «Clients» → «Create client» → Web application, имя HomeMusic Hub, адреса не нужны → скопировать Client ID.
  4. «Clients» → «Create client» → Android: package name app.homemusic, SHA-1 28:F1:56:76:47:1B:30:9F:69:AB:86:BF:13:87:28:29:61:2A:BC:E3 (отпечаток ключа подписи релизов; проверить: apksigner verify --print-certs <apk>). Этот клиент ничего не выдаёт — по нему Google узнаёт приложение.
  5. В .env: GOOGLE_CLIENT_ID=<Client ID из шага 3>, затем docker compose up -d hub.

Приложение берёт ID из GET /api/auth/options — пересобирать его не нужно. Первый вход через Google просит код приглашения (новый аккаунт) — или войти паролем и привязать Google в настройках приложения («HomeMusic» → «Вход через Google»). Ошибка «Developer console is not set up correctly» значит, что не совпали package name или SHA-1 из шага 4.

Перенос музыки из Яндекс Музыки и Spotify

В приложении — вкладка «Файлы» (библиотека): значок ⇄ в шапке или карточка «Перенести музыку из Яндекс Музыки»; в браузере — ещё и «Ещё» → «Перенести музыку». Дальше «Войти через Яндекс»: Хаб показывает код, его вводят на https://ya.ru/device под своим Яндекс ID (подписка Плюс не нужна). Хаб читает только списки: «Мне нравится», плейлисты (и закрытые), любимые альбомы и исполнители, «Не нравится». Дальше — галочки, что перенести, и режим:

  • Экономно (по умолчанию): что уже есть в библиотеке, сразу получает ♥ и встаёт в плейлисты Navidrome в порядке Яндекса; остальное слушается со страницы импорта и скачивается, когда трек послушали. «Скачать всё сейчас» — в любой момент.
  • Скачать всё: всё качается в фоне (около 9 МБ на трек). Обычные загрузки и прослушивание идут первыми: импорт не занимает последний воркер очереди.

Без входа — ссылка на открытый профиль (music.yandex.ru/users/<логин>/playlists/3, без закрытых плейлистов и дизлайков) или токен вручную. Вход (токен) хранится зашифрованным и удаляется после переноса, если не включить «Запомнить вход» (тогда «Подтянуть новое из Яндекса» добирает новые лайки и треки плейлистов); кнопка «Удалить вход» — на странице импорта. Ненайденное выбирается вручную в отчёте («Выбрать файл» → страница загрузки). Вход идёт от имени приложения Яндекс Музыки (своего API у Яндекса нет): если Яндекс это закроет, останутся ссылка на профиль и токен.

Spotify — без входа, по файлам экспорта: в настройках приватности Spotify запросить данные аккаунта (архив приходит на почту, обычно за несколько дней), затем «Перенести музыку» → «Загрузить экспорт Spotify» и выбрать из архива Playlist*.json и/или YourLibrary.json (до 32 файлов, 8 МБ). Хаб берёт из них плейлисты, понравившиеся треки, сохранённые альбомы и исполнителей (подкасты пропускает), дальше — тот же предпросмотр, режимы и отчёт, что у Яндекса. В экспорте только названия (без длительности), поэтому сомнительных совпадений в отчёте может быть больше. «Обновить» у такого переноса нет: новое подтягивается новым экспортом.

Микс дня

Каждое утро (по умолчанию в 5:00 по TIMEZONE, Europe/Moscow) Хаб собирает каждому пользователю с историей прослушиваний плейлист Navidrome «Микс дня» из 50 песен: часть знакомого (любимое, давно не слушанное, свои ♥ и загрузки), часть нового — похожего на вкус (радио YouTube Music, Last.fm, Deezer, похожие Яндекса, новинки любимых исполнителей, «Моя волна» Яндекса, если при импорте включён «Запомнить вход»). Нескачанные песни качаются тихо, в фоне (в «Загрузках» их нет) и встают в плейлист по мере импорта; не скачалась — её место занимает запасная. Плейлист один и тот же (id не меняется) — в приложении его можно держать офлайн. При следующем миксе песни, скачанные только ради старого и никем не прослушанные (не ♥, не в плейлисте, не скачанные явно), уходят в .trash.

  • Режим (PUT /api/mixes/settings): «Обычный» — 40 % знакомого / 60 % нового, «Любимое» — 80/20, «Незнакомое» — 10/90; там же час сборки. Новому пользователю без истории — выбор 3–5 исполнителей (/api/taste/*); страница микса в PWA и карточка в приложении — следующие задачи (T5, T7).
  • Ключи и настройки (.env): LASTFM_API_KEY (бесплатный ключ: https://www.last.fm/api/account/create), EMBEDDER_URL (сервис векторов звука, по умолчанию http://embedder:8090; не отвечает — микс без «похожести по звуку»), TIMEZONE. Тонкие настройки — config.yaml, раздел mixes: (размер, запас, веса источников).
  • Собрать заново сейчас (админ): POST /api/mixes/rebuild ({"user_id": N} — другому пользователю). Отчёт сборки (источники, сколько дали, ошибки, время) — GET /api/mixes/today → mix.generators и лог Хаба (mix N of user M).

Лёгкий звук для мобильной сети

Приложение в лимитной сети (настройка «Экономия мобильного трафика») просит у Хаба лёгкий вариант звука: GET /api/listen/stream?…&q=mobile. Файл тяжелее 192 кбит/с (mp3 320, flac) Хаб перекодирует ffmpeg в Ogg Opus 160 кбит/с (~вдвое меньше, на слух почти то же), файл легче отдаёт как есть. Копии лежат в staging/listen-light (на диске B:\Music\staging, до 2,5 ГБ, лишние стираются по давности использования — можно очистить папку руками, копии сделаются заново), делаются заранее — для песен «Микса дня» и треков «Моей волны» — и по первому запросу. Пока копии нет, поток идёт оригиналом (ответ помечен X-HomeMusic-Variant). Перекодирование идёт в фоне, с низким приоритетом, не больше трёх ffmpeg сразу. Подробности — hub/API.md, PROGRESS.md.

Экономия мобильного трафика в приложении

Настройки → «HomeMusic» → «Экономия мобильного трафика» (по умолчанию включена). «Экономить» значит: настройка включена И активная сеть лимитная по меткам Android (isActiveNetworkMetered, не по транспорту: Wi-Fi-точка другого телефона — лимитная, мобильный безлимит — нет). В приложении это HomeTraffic.economy(); её же спрашивает «лёгкий звук» выше. Что меняется в такой сети:

  • играющий трек, пока проиграно меньше 30 с, подгружается не больше чем на 40 с вперёд (ранний пропуск стоит ~2 МБ вместо всего файла: у mp3 320 кбит/с это было ~8 МБ); после 30-й секунды трек грузится целиком, как раньше (туннели, обрывы);
  • предзагрузка — начало (10 с) одного следующего трека, а не трёх; в «Моей волне» — только после ответа Хаба на текущий трек (волна пересобирает очередь вперёд, и раньше начала заменённых треков выбрасывались);
  • перед обновлением приложения показывается размер, первая кнопка — «Дождусь Wi-Fi».

Независимо от сети: кэш стриминга по умолчанию 1–2 ГБ (не больше десятой части свободного места; кто менял — не тронут), обложки Navidrome 500 px вместо исходных. «Микс дня на телефон» (настройка рядом, по умолчанию включена): когда телефон на зарядке и в безлимитном Wi-Fi, песни сегодняшнего микса (плейлист Navidrome) скачиваются в «Скачанное» по одной; при смене микса убираются скачанные ради прошлого микса, кроме ♥ и скачанного вами самим; свободного места остаётся не меньше 2 ГБ. Микс из загрузок играет без интернета. Там же счётчик «Мобильный трафик плеера»: сколько трафика в лимитной сети ушло на песни и на начала следующих сегодня и за 7 дней (плюс «всё приложение» по статистике Android, если она отдаётся).

Soulseek (по желанию)

Источник lossless-файлов для загрузок в библиотеку через slskd. По умолчанию выключен: Хаб включает его, только когда в .env заданы все три значения — SLSKD_API_KEY (любая случайная строка, например openssl rand -hex 24), SLSKD_SLSK_USERNAME и SLSKD_SLSK_PASSWORD (новый логин регистрируется сам при первом входе). Контейнер slskd запускается с профилем: COMPOSE_PROFILES=soulseek в .env, затем docker compose up -d.

  • Каждая загрузка в библиотеку (кнопка, ♥, импорт, Микс дня, Моя волна) сначала ищет файл на Soulseek и берёт его, только если он лучше обычных источников: по умолчанию только FLAC (soulseek.format в config.yaml). Не нашёл, нет сети Soulseek или пир не отдал файл за soulseek.transfer_timeout_s (90 с, с очередью) — загрузка идёт дальше обычным порядком (hitmoz → … → YouTube), Soulseek не ждут.
  • Прослушивание Soulseek не использует никогда (файл у пира качается целиком, с его очередью), вкладка «Файлы» — тоже (поиск там занимает секунды; по имени — provider=soulseek в API).
  • Файл проверяется по имени и папкам у пира (исполнитель, название, версия, длительность) и после загрузки — по формату. Для русских треков с матом, где важна версия без цензуры, файл Soulseek без пометки «без цензуры» не берётся.
  • Готовый файл переносится в библиотеку и удаляется из тома slskd. Свои файлы в сеть не раздаются. Проверка — «Ещё» → «Проверить Soulseek» (у админа, когда источник включён).

Обновление

git pull
docker compose up -d --build          # пересобрать Хаб (yt-dlp внутри образа обновляется пересборкой)
docker compose pull navidrome         # после смены версии в docker-compose.yml
docker image prune -f                 # убрать прежние версии образов, оставшиеся без имени (<none>) после пересборки

Пересборка под тем же тегом не удаляет прежний образ: он остаётся без имени и занимает место в диске Docker (образ embedder — около 7 ГБ). prune -f (без -a) удаляет только такие образы, у которых нет контейнера; образы с тегом, контейнеры и тома не трогает. Запускайте после каждого docker compose build / up -d --build.

yt-dlp ломается после изменений YouTube — пересобирайте образ Хаба примерно раз в неделю (версия yt-dlp закреплена в hub/pyproject.toml: поднять её и выполнить uv lock в hub/). При обновлении bgutil-ytdlp-pot-provider одновременно меняйте тег сервиса youtube-pot в docker-compose.yml: плагин и сервер должны иметь одинаковую версию.

YouTube просит проверку «не робот»

Sign in to confirm you’re not a bot означает, что YouTube отказал в выдаче файла: песня может находиться поиском, но скачивание и прослушивание недоступны. Это не ошибка распознавания ассистента. Можно повторить позже или выбрать файл из другого источника; повторный поиск того же ролика сам по себе проверку не снимает.

Хаб использует клиент mweb и автоматически получает PO-токены через bgutil-ytdlp-pot-provider, как рекомендует руководство yt-dlp. Сервис youtube-pot запускается в Compose вместе с Хабом, доступен только внутри Docker и не получает файл cookies. Для диагностики доступности: docker compose exec hub python -c "import urllib.request; print(urllib.request.urlopen('http://youtube-pot:4416/ping', timeout=5).status)". PO-токены не гарантируют обход проверки входа, HTTP 403 и ограничений аккаунта/IP.

Обычный YouTube и поиск перезаливов делят два слота; yt-dlp делает паузу 1 с между запросами и 5 с перед скачиванием, повторяет сетевые ошибки один раз. Успешные метаданные ролика переиспользуются 10 минут, одновременные запросы одного ролика объединяются; существующий кэш прослушивания сохраняется. После проверки «не робот», недействительной сессии или HTTP 429 новые обращения YouTube прекращаются на 15 минут; загрузки могут перейти к резервному SoundCloud. Параметры — ytdlp в config.example.yaml.

Если ошибка сохраняется, владелец сервера может подключить cookies YouTube:

  1. Экспортировать cookies только youtube.com в формате Netscape по инструкции yt-dlp: войти в YouTube в отдельном приватном окне, в той же вкладке открыть https://www.youtube.com/robots.txt, экспортировать cookies и закрыть приватное окно. yt-dlp предупреждает о риске блокировки аккаунта; используйте отдельный аккаунт и подключайте его только при необходимости.
  2. Сохранить файл на серверном ПК как B:\Music\.youtube-cookies.txt (при другом MUSIC_ROOT — в его корне). Cookies дают доступ к сессии: не отправлять их в чат и не добавлять в git.
  3. В .env добавить YTDLP_COOKIES_FILE=/music/.youtube-cookies.txt — это путь внутри контейнера. Применить окружение командой docker compose up -d hub, дождавшись завершения активных загрузок.
  4. Если yt-dlp сообщает cookies are no longer valid / rotated, заново выполнить шаг 1; продление дат в старом файле сессию не восстанавливает. Сначала полностью сохранить новый экспорт в отдельный локальный файл вне репозитория и закрыть приватное окно, затем заменить B:\Music\.youtube-cookies.txt готовым файлом (не редактировать его по строкам во время работы загрузчика). Ручной вход и CAPTCHA выполняет владелец; содержимое экспорта не отправлять агенту и не печатать в логах.
  5. Повторить загрузку. Хаб замечает замену файла, сбрасывает паузу после отказа и кэш метаданных; перезапуск не нужен. Он читает cookies в приватную копию и не перезаписывает экспорт; cookies других доменов и SoundCloud не используются. Не экспортировать все cookies обычного браузера через --cookies-from-browser. Для отключения убрать переменную из .env и снова выполнить команду шага 3.

Cookies не гарантируют снятие ограничений YouTube. Вход в Google в приложении HomeMusic авторизует пользователя Хаба и не передаёт YouTube-сессию серверному загрузчику.

Исходящий прокси

В .env укажите OUTBOUND_PROXY и список доменов через запятую в OUTBOUND_PROXY_HOSTS. Общий HTTP-клиент Хаба направляет через прокси перечисленные домены и их поддомены. youtube.com включает прокси для YouTube и YouTube Music, soundcloud.com — для SoundCloud. Для каждого из этих источников поиск, извлечение ссылок и получение аудио идут через один прокси, включая CDN. Настройка применяется после пересоздания контейнера.

Если прокси запущен на машине с Docker, используйте, например, http://host.docker.internal:3128. На Linux прокси должен слушать на интерфейсе, доступном из сети Docker. Compose добавляет адрес хоста для Хаба и сервиса PO-токенов.

Полезное

HomeMusic.bat (ярлык на рабочем столе) включает и выключает HomeMusic одним запуском: если работают все контейнеры — спрашивает подтверждение, предупреждает о загрузках в очереди и делает docker compose stop; иначе запускает Docker Desktop (если он не запущен), docker compose up -d и ждёт ответа Хаба. Docker Desktop не выключает (в нём могут работать и другие контейнеры). Выключенный так HomeMusic не поднимается сам после перезагрузки ПК.

docker compose ps
docker logs --tail 50 homemusic_hub          # JSON-логи Хаба
docker logs --tail 20 homemusic_tunnel       # подключения туннеля
docker compose restart tunnel
curl http://localhost:8080/healthz
  • Бэкапы: база Хаба — B:\Music\.backups\hub\ (каждый день в 4:00, 7 копий), база Navidrome — B:\Music\.backups\navidrome\ (в 4:30, 7 копий).
  • Удалённое из Хаба лежит в B:\Music\.trash\<дата>\ 30 дней.
  • ID треков Navidrome стабильны: Хаб пишет в файлы теги HOMEMUSIC_TRACK_ID / HOMEMUSIC_ALBUM_ID, из них Navidrome строит свои ID (navidrome/navidrome.toml). Не меняйте PID.* без бэкапа базы Navidrome.
  • Источники и лимиты — config.yaml (порядок источников для русской и остальной музыки — для русской по умолчанию hitmoz → hitmos.fm → SoundCloud → перезаливы YouTube → YouTube Music; sources.uploaded_before — файлы SoundCloud, загруженные с 2026-05-01, не берутся; источники перезаливов без цензуры sources.reuploads — YouTube и SoundCloud, домены hitmoz, пороги сопоставления). sources.download_fallback (по умолчанию [soundcloud], [] отключает) — резерв полной записи для загрузки и прослушивания после отказа обычных источников, в том числе проверки «не робот» YouTube. Действуют те же проверки исполнителя, версии и длительности; короткое превью полной песней не становится. Приложение с 1.14.1 запрашивает только полную запись (full=1): при её недоступности показывает ошибку вместо 30-секундного превью, а старый кэш превью больше не использует. ИИ-провайдер — .env (LLM_BASE_URL, LLM_MODEL; запасной вариант — Ollama qwen3-vl:8b).
  • Оценка распознавания скриншотов — eval/README.md.

Если в вашем config.yaml явно перечислены зеркала hitmoz.domains, добавьте ru.hitmox.net после ru.hitmoz.org, чтобы использовать его при недоступности основного адреса.

Сервис embedder (звуковые векторы для миксов)

Нейросеть MuQ-MuLan-large (embedder/, контейнер homemusic_embedder) превращает фрагмент песни (30 с из середины файла библиотеки или превью Deezer) и текст в векторы одного пространства; Хаб сравнивает по ним кандидатов миксов и «Моей волны» со вкусом пользователя. API — hub/API.md.

  • Запуск: docker compose up -d --build embedder, затем docker image prune -f (прежний образ — ~7 ГБ — иначе остаётся без имени). Порт наружу не открыт: Хаб ходит в http://embedder:8090 (EMBEDDER_URL в .env; EMBEDDER_TOKEN — по желанию, тот же в обоих сервисах).
  • Первая загрузка весов: при первом задании (или POST /text) сервис скачивает с Hugging Face ~2,5 ГБ в том homemusic_embedder-models — это несколько минут; дальше модель грузится из тома прямо на видеокарту за ~12 с (пик RAM контейнера ~3,4 ГБ, лимит mem_limit: 4g). Без работы 5 минут модель выгружается из памяти (EMBEDDER_IDLE_UNLOAD_S) — видеокарта общая. Веса подмоделей (models--OpenMuQ--MuQ-large-msd-iter, models--xlm-roberta-base, ~2,4 ГБ) сервису больше не нужны — из них берутся только конфиги и токенизатор.
  • Видеокарта: нужен Docker Desktop с WSL2 и драйвер NVIDIA на Windows. Проверка, что GPU виден в контейнере:
    docker run --rm --gpus all nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi
    
    Если на видеокарте меньше 5 ГБ свободно (по nvidia-smi, EMBEDDER_MIN_FREE_GPU_MB), модель не грузится и ждёт карту: в /healthz — state: error с GpuBusy, очередь стоит, попытка раз в минуту; Хаб тем временем строит миксы и волну без звука. Принудительно — EMBEDDER_DEVICE=cuda|cpu; на CPU модели нужно ~7 ГБ RAM (≈ 5 с на песню) — сначала поднять mem_limit/memswap_limit в docker-compose.yml.
  • Состояние: docker exec homemusic_embedder python -c "import urllib.request as u; print(u.urlopen('http://localhost:8090/healthz').read().decode())" — модель, где она (device), очередь, скорость последней пачки. Замер на файлах библиотеки: docker compose run --rm embedder python -m app.bench --songs 64.
  • Тесты — в образе сервиса (модель в них — фейк):
    docker run --rm -v "${PWD}\embedder:/src" -w /src homemusic-embedder:local bash -c "uv sync -q --frozen --extra gpu && uv run --no-sync pytest -q -p no:cacheprovider -n 6"
    
    С настоящей моделью (медленно): добавить --gpus all -v homemusic_embedder-models:/models -e EMBEDDER_REAL_MODEL_TESTS=1 и -m gpu к pytest.

Разработка

cd hub
uv sync
uv run pytest -q                    # тесты; тестам с ffmpeg нужен Docker (см. ниже)
uv run uvicorn app.main:app --reload --port 8080
cd ..\web
npm install
npm run dev                         # PWA на :5173 с прокси на :8080

Тесты, которым нужен ffmpeg (теги, проверка файлов, полный сценарий), — в образе Хаба (docker compose build hub, затем docker image prune -f), из cmd:

docker run --rm -v homemusic-test-venv:/tmp/venv -e UV_PROJECT_ENVIRONMENT=/tmp/venv -e UV_LINK_MODE=copy -e PYTHONDONTWRITEBYTECODE= -v "%cd%\hub:/src" -w /src homemusic-hub:local bash -c "uv sync -q --frozen && uv run --no-sync pytest -q -p no:cacheprovider -n 6"

Откат шлюза на VPS

ssh myvps
systemctl disable --now homemusic-tunnel-watchdog homemusic-guard-firewall
rm -f /etc/systemd/system/homemusic-tunnel-watchdog.service /etc/systemd/system/homemusic-guard-firewall.service /usr/local/sbin/homemusic-tunnel-watchdog
rm -rf /etc/homemusic; nft delete table inet homemusic_guard 2>/dev/null; systemctl daemon-reload
rm -f /etc/ssh/sshd_config.d/61-homemusic-tunnel.conf && sshd -t && systemctl reload ssh
pkill -u homemusic-tunnel; userdel -r homemusic-tunnel
# в /etc/caddy/Caddyfile удалить блок между "# >>> homemusic" и "# <<< homemusic", затем: systemctl reload caddy