Это руководство для того, кто решил поднять всё сам. От пустого LXC-контейнера до работающего стека: свой стриминг, поиск и скачивание музыки с телефона, приватность через VPN и рекомендации «по звуку». Разбираем каждый из двенадцати сервисов — что ставим, зачем и как настроить.
С чего всё началось
YouTube Music я использую довольно давно и с подпиской, полез чистить «Понравившееся» — а часть треков оттуда просто исчезла: где-то удалили клип, где-то канал в бане, где-то правообладатель передумал. Плейлист, который я собирал годами, тихо усыхал, и меня не спросили. Причем у меня есть подписка. Ну и обойдусь.
Заменить стриминг «включил — играет любой трек мира» самохостинг не может, каталог из десятков миллионов треков дома не поднять. Задача другая — забрать под контроль то, что реально ценно: свою коллекцию, свою курацию и механику «услышал → добавил → получил похожее» в пару кликов. Ну и, конечно, мы не пираты.
Чего мы хотим
Я не слежу за артистами и не качаю дискографии — мне нужно, чтобы играла моя коллекция, чтобы услышанный трек добавлялся одной кнопкой, и чтобы система подсовывала похожее. Отсюда цели:
Своя коллекция на своём диске. Ядро — самохостовый аудио-сервер (Navidrome, протокол OpenSubsonic): стриминг, плейлисты, рейтинги. Файлы у меня и не исчезают от чужих решений.
Поиск и добавление трека с телефона. Услышал — за пару тапов в коллекции, теггировано и разложено. Без «подойди к ноутбуку, запусти Х».
Приватность, домашний IP не светится. Всё внешнее приобретение — через VPN с fail-closed killswitch. Soulseek (P2P-раздача — главный DMCA-вектор) живёт целиком внутри сетевого namespace VPN.
Перенос курации. Годы лайков, рейтингов, плейлистов и «мёртвые» треки (те, что Google уже удалил) — не потерять, а перенести в нативные механизмы Navidrome.
Sonic-рекомендации. Локальный анализ звука («дай похожее на этот трек») плюс weekly-рекомендации по вкусу для открытия нового.
В сумме — замена YTM. На телефоне один клиент (Symfonium под Android), в нём моя музыка, лайки, поиск-добавление и радио. Zero-subscription — ни одной активной подписки.
Архитектура
Одна мысль, из которой растёт весь стек:
Navidrome читает каталог
/musicи строит библиотеку по тегам внутри файлов. SoulSync — это то, что/musicнаполняет. Остальное — обвязка вокруг этих двух ролей.
Держа это в голове, двенадцать контейнеров складываются в понятную трубу источники → SoulSync → /music → Navidrome → телефон. Ядро — ровно два сервиса; прочие десять — либо приватность, либо «умные» фичи, либо клей.
ТЕЛЕФОН — Symfonium + клиентский серт .p12 │ HTTPS :443, mTLS-гейт (нет серта → connection refused) ▼ edge-Caddy: tls { client_auth trust music-CA } nvd.example.com │ reverse_proxy → внутренний caddy (Host сохраняется) ══════ LXC «music» (unprivileged, Docker) ══════ ▼ caddy (внутренний ingress :80) — Host-роутинг локальных UI ▼ navidrome :4533 ◄── плагин audiomuseai.ndp (sonicSimilarity) ЧИТАЕТ /music ПО ТЕГАМ ▲ анализ звука ▲ audiomuse ×4 (flask+worker+postgres+redis) │ читает /music (ro) │ ПИШЕТ (file_organization=ON) SoulSync :8008 (+ PWA на телефон) │ всё внешнее — через VPN ▼ gluetun (WireGuard-VPN, killswitch fail-closed) ├─ HTTP-прокси :8888 ← YouTube/Deezer └─ netns: slskd (Soulseek P2P), prowlarr+qbit (торренты, opt-in) egress = VPN. Домашний/VPS IP наружу не светится. explo — раз в неделю пишет плейлист-рекомендацию в Navidrome
Со схемы важно прочитать две вещи. Первое: телефон ходит только К Navidrome, «сверху вниз» — у VPN нет входящего, приватность телефонного трафика держит mTLS на edge, а VPN закрывает исходящий трафик качалок. Второе: /music — одна физическая папка (bind-mount), в которую SoulSync пишет, а Navidrome читает; никакого копирования, никакого промежуточного каталога. Про mTLS важно отметить, что это Caddy reverse proxy и в моих прошлых статьях описано, что это самый край домашней инфры, торчащий в интернет, второй Caddy из текста ниже, это внутри LXC просто для удобства. Внешний Caddy заставить просить mTLS мне нужно было потому, что дырявым музыкальным веб-серверам я не доверяю, а проигрыватель Symfonium как раз поддерживает аутентификацию по сертификату и делает это более надежный Caddy. Не забудьте только в телефон сертификат установить, на андроиде, в настройках. Расписывать как сгенерировать сертификат и установить я не буду, разберетесь. Тогда какие бы дырявые ни были музыкальные сервисы, защищает их внешний reverse proxy.
Контейнер | Образ | Роль |
|---|---|---|
navidrome |
| Ядро. OpenSubsonic-сервер: |
soulsync |
| Ядро. Поиск+скачивание (YouTube/Soulseek/торренты/Deezer/…), пишет в |
slskd |
| Soulseek P2P — источник редкого лосслесса. В netns gluetun |
gluetun |
| VPN-шлюз (WireGuard) + killswitch. Оборачивает качалки; HTTP-прокси |
prowlarr / qbittorrent |
| Торрент-индексеры + клиент. Профиль |
audiomuse (flask + worker) |
| Sonic-анализ звука (Essentia + CLAP-эмбеддинги): похожие треки, плейлисты |
audiomuse-db / -redis |
| Хранилище эмбеддингов и очередь задач анализа |
explo |
| Weekly-рекомендации ListenBrainz → плейлист в Navidrome |
caddy |
| Внутренний reverse-proxy: Host-роутинг локальных UI, единая точка за edge-mTLS |
Установка
Шаг 0. LXC — то, что делается только с хоста
Стек живёт в отдельном unprivileged-LXC. Gluetun нужен туннельный интерфейс, который LXC по умолчанию не отдаёт:
# на хосте Proxmox, под root (117 — номер вашего CT) pct set 117 --dev0 /dev/net/tun
Дальше всё внутри контейнера и под обычным пользователем (у меня user, uid 1000) — root понадобится только для установки docker и systemd-юнита автозапуска. Это осознанное решение: чем меньше крутится под root, тем меньше поверхность, если один из образов окажется недобросовестным.
Шаг 1. Каталоги — одно дерево, один владелец
# внутри CT, под обычным пользователем mkdir -p ~/music-app/{navidrome/plugins,soulsync/{config,logs},slskd,prowlarr,qbit,caddy} mkdir -p ~/music-app/audiomuse/{pgdata,redis-data,temp-flask,temp-worker,plugins-flask,plugins-worker} mkdir -p ~/music-app/explo/config mkdir -p ~/music-app/data/{media/music,soulseek/downloads,torrents} cd ~/music-app
data/media/music — это и есть тот самый /music, единственная папка с музыкой. Всё дерево принадлежит uid 1000; за этим стоит следить (см. грабли про root-owned каталоги).
Шаг 2. Секреты — .env с chmod 600
Пароли и ключи генерируем на месте и держим только в .env, не в compose и не в образах:
{ echo "TZ=Europe/Amsterdam" echo "PUID=1000"; echo "PGID=1000"; echo "UMASK=002" echo "AM_DB_PASSWORD=$(openssl rand -hex 24)" # Postgres AudioMuse echo "ND_PASSWORDENCRYPTIONKEY=$(openssl rand -hex 24)" # шифрование паролей Navidrome echo "SLSK_USER=music-$(openssl rand -hex 4)" # Soulseek-аккаунт заводится первым логином echo "SLSK_PASS=$(openssl rand -hex 16)" echo "EXPLO_UI_PASSWORD=$(openssl rand -hex 8)" } >> .env chmod 600 .env
Отдельно — приватный ключ WireGuard от вашего VPN-провайдера. Его вставляем через невидимый ввод, чтобы он не попал ни на экран, ни в history:
read -rs WGKEY printf 'WG_PRIVATE_KEY=%s\n' "$WGKEY" >> .env
Шаг 3. docker-compose.yml — весь стек
Здесь есть одна сетевая ловушка, на которой легко потерять полдня. Контейнеры за network_mode: service:gluetun наследуют DNS gluetun и перестают резолвить docker-имена — SoulSync теряет связь с navidrome и slskd. Поэтому в туннель VPN мы сажаем только качалки (slskd, prowlarr, qbit), а SoulSync живёт в обычной сети (docker-DNS работает) и гонит наружу через HTTP-прокси gluetun. Ниже это уже учтено.
name: music networks: default: name: music ipam: config: - subnet: 172.28.0.0/24 # фиксируем подсеть, чтобы не пересечься с диапазоном VPN x-env: &common-env TZ: ${TZ} PUID: ${PUID} PGID: ${PGID} UMASK: ${UMASK} services: # ── музыкальный сервер (наружу через edge-Caddy с mTLS) ── navidrome: image: deluan/navidrome:0.63.2 container_name: navidrome user: "1000:1000" environment: ND_SCANSCHEDULE: 1h ND_PLUGINS_ENABLED: "true" # нужно для плагина AudioMuse ND_PLUGINS_AUTORELOAD: "true" ND_AGENTS: "audiomuseai,deezer" # audiomuseai первым — sonic-похожесть ND_DEVARTISTINFOTIMETOLIVE: "1s" # не отдавать similar/radio из кэша ND_PASSWORDENCRYPTIONKEY: ${ND_PASSWORDENCRYPTIONKEY} TZ: ${TZ} volumes: - ./navidrome:/data - ./data/media/music:/music:ro # библиотека — только на чтение restart: unless-stopped # ── AudioMuse-AI: sonic-анализ (flask + worker + postgres + redis) ── audiomuse: image: ghcr.io/neptunehub/audiomuse-ai:2.6.2 container_name: audiomuse-ai-flask-app user: "1000:1000" environment: &am-env SERVICE_TYPE: "flask" POSTGRES_HOST: audiomuse-db POSTGRES_USER: audiomuse POSTGRES_PASSWORD: ${AM_DB_PASSWORD} POSTGRES_DB: audiomusedb REDIS_URL: redis://audiomuse-redis:6379/0 TEMP_DIR: /app/temp_audio TZ: ${TZ} volumes: - ./audiomuse/temp-flask:/app/temp_audio - ./audiomuse/plugins-flask:/app/plugin/installed depends_on: [audiomuse-db, audiomuse-redis] restart: unless-stopped audiomuse-worker: image: ghcr.io/neptunehub/audiomuse-ai:2.6.2 container_name: audiomuse-ai-worker user: "1000:1000" environment: <<: *am-env SERVICE_TYPE: "worker" volumes: - ./audiomuse/temp-worker:/app/temp_audio - ./audiomuse/plugins-worker:/app/plugin/installed - ./data/media/music:/music:ro # воркер читает аудио для анализа depends_on: [audiomuse-db, audiomuse-redis] cpus: "4.0" # анализ тяжёлый — ограничиваем, чтобы не душить соседей на хосте mem_limit: 4g restart: unless-stopped audiomuse-redis: image: redis:7-alpine container_name: audiomuse-redis user: "1000:1000" volumes: [./audiomuse/redis-data:/data] restart: unless-stopped audiomuse-db: image: postgres:15-alpine container_name: audiomuse-db user: "1000:1000" command: ["postgres", "-c", "unix_socket_directories=/tmp"] environment: POSTGRES_USER: audiomuse POSTGRES_PASSWORD: ${AM_DB_PASSWORD} POSTGRES_DB: audiomusedb volumes: [./audiomuse/pgdata:/var/lib/postgresql/data] restart: unless-stopped # ── VPN-шлюз: WireGuard + killswitch ── gluetun: image: qmcgaw/gluetun:latest container_name: gluetun cap_add: [NET_ADMIN] devices: ["/dev/net/tun:/dev/net/tun"] ports: - "5030:5030" # slskd Web UI - "8080:8080" # qBittorrent (профиль torrents) - "9696:9696" # Prowlarr (профиль torrents) environment: VPN_SERVICE_PROVIDER: nordvpn # ваш провайдер VPN_TYPE: wireguard WIREGUARD_PRIVATE_KEY: ${WG_PRIVATE_KEY} SERVER_COUNTRIES: Switzerland # DMCA-safe юрисдикция SERVER_CATEGORIES: P2P FIREWALL: "on" # killswitch fail-closed: туннель упал → трафик дропается HTTPPROXY: "on" # :8888 для HTTP-качалок (SoulSync) TZ: ${TZ} restart: unless-stopped # ── Soulseek-демон (целиком в туннеле VPN) ── slskd: image: slskd/slskd:latest container_name: slskd user: "1000:1000" network_mode: "service:gluetun" depends_on: gluetun: { condition: service_healthy } environment: SLSKD_SLSK_USERNAME: ${SLSK_USER} SLSKD_SLSK_PASSWORD: ${SLSK_PASS} SLSKD_SLSK_LISTEN_PORT: "50300" # входящий порт для раздачи (иначе uploads=0) TZ: ${TZ} volumes: - ./slskd:/app - ./data:/data restart: unless-stopped # ── SoulSync: поиск+скачивание (наружу через edge-Caddy) ── soulsync: image: boulderbadgedad/soulsync:3.0.1 # конкретный тег, НЕ latest (проект очень активен — см. грабли) container_name: soulsync-webui environment: - PUID=1000 - PGID=1000 - UMASK=022 - TZ=${TZ} - HTTP_PROXY=http://gluetun:8888 # внешнее (YouTube/Deezer) → VPN - HTTPS_PROXY=http://gluetun:8888 - NO_PROXY=localhost,127.0.0.1,navidrome,gluetun,slskd.music,caddy,172.28.0.0/16,.music # внутреннее → напрямую; gluetun обязателен — иначе запросы к Prowlarr/qBit уйдут в прокси volumes: - ./soulsync/config:/app/config - ./soulsync/logs:/app/logs - ./data/soulseek/downloads:/app/downloads - ./data/media/music:/app/Transfer # SoulSync пишет ПРЯМО в /music - ./data/torrents:/data/torrents - soulsync_database:/app/data depends_on: [slskd] restart: unless-stopped # ── Торренты (опционально, профиль torrents; в туннеле VPN) ── prowlarr: image: lscr.io/linuxserver/prowlarr:latest container_name: prowlarr profiles: ["torrents"] network_mode: "service:gluetun" depends_on: { gluetun: { condition: service_healthy } } environment: *common-env volumes: [./prowlarr:/config] restart: unless-stopped qbittorrent: image: lscr.io/linuxserver/qbittorrent:latest container_name: qbittorrent profiles: ["torrents"] network_mode: "service:gluetun" depends_on: { gluetun: { condition: service_healthy } } environment: <<: *common-env WEBUI_PORT: "8080" volumes: - ./qbit:/config - ./data:/data restart: unless-stopped # ── Explo: weekly-рекомендации ListenBrainz → плейлист ── explo: image: ghcr.io/lumepart/explo:latest container_name: explo user: "1000:1000" environment: TZ: ${TZ} WEB_UI: "true" UI_USERNAME: admin UI_PASSWORD: ${EXPLO_UI_PASSWORD} HTTP_PROXY: http://gluetun:8888 HTTPS_PROXY: http://gluetun:8888 NO_PROXY: localhost,127.0.0.1,navidrome,gluetun,slskd.music,caddy,172.28.0.0/16,.music volumes: - ./explo/config:/opt/explo/config - ./data/media/music/Explo:/data/ - ./data/soulseek/downloads:/slskd/ depends_on: gluetun: { condition: service_healthy } navidrome: { condition: service_started } restart: unless-stopped # ── внутренний reverse-proxy (Host-роутинг локальных UI) ── caddy: image: caddy:2-alpine container_name: caddy user: "1000:1000" cap_add: [NET_BIND_SERVICE] ports: ["80:80"] volumes: [./caddy/Caddyfile:/etc/caddy/Caddyfile:ro] depends_on: { gluetun: { condition: service_healthy } } restart: unless-stopped networks: default: aliases: [nvd.music, ss.music, slskd.music, audiomuse.music] volumes: soulsync_database:
Шаг 4. Внутренний Caddy — маршрутизация локальных UI
Внутренний caddy отдаёт веб-морды сервисов по внутренним именам (снаружи их закрывает edge-Caddy с mTLS). Минимальный caddy/Caddyfile:
nvd.example.com { reverse_proxy navidrome:4533 } ss.example.com { reverse_proxy soulsync-webui:8008 } audiomuse.example.com { reverse_proxy audiomuse-ai-flask-app:8000 } slskd.example.com { reverse_proxy gluetun:5030 }
Наружу эти хосты проксирует ваша edge-нода, добавляя mTLS (клиентский сертификат). Как устроен edge-слой с mTLS — тема отдельная; здесь важно, что телефон ходит на nvd.example.com за сертификатом, а всё остальное недоступно снаружи вовсе.
Шаг 5. Запуск и проверка, что VPN держит
docker compose up -d # ядро docker compose --profile torrents up -d # плюс торренты, если нужны
Прежде чем что-то качать — убедимся, что качалки реально ходят через VPN, а не мимо. Спрашиваем публичный IP изнутри туннеля (из контейнера, который в netns gluetun):
docker exec qbittorrent sh -c 'wget -qO- https://ipinfo.io/ip' # → адрес вашего VPN-сервера. С реальным IP хоста совпасть НЕ должен.
Это принципиально: спросив IP с хоста, вы увидите egress хоста и ничего не поймёте. Проверять надо именно из namespace VPN. Убьёте туннель (docker stop gluetun) — у slskd/qbit пропадёт сеть целиком: killswitch fail-closed, утечь нечему.
Шаг 6. Автозапуск после ребута
Чтобы стек поднимался сам, вешаем на хост CT systemd-юнит:
# /etc/systemd/system/music-app.service (под root) [Service] Type=oneshot RemainAfterExit=yes WorkingDirectory=/home/<user>/music-app ExecStart=/usr/bin/docker compose --profile torrents up -d User=<user> [Install] WantedBy=multi-user.target
systemctl enable --now music-app. Вместе с restart: unless-stopped на контейнерах и onboot=1 на самом LXC — три уровня «поднимись сам», потому что за «после ребута ничего не играет» никто не любит просыпаться.
Настройка сервисов
Compose поднял контейнеры — теперь пройдёмся по веб-мордам и включим то, что не задаётся через окружение. Никаких ручных запросов к API: всё это тумблеры и поля в интерфейсах (но я это почти все не нажимал, только юзеров создавал, я попросил клода и он через API все сам нажал, рекомендую, а потом я его же попросил написать кнопки в текст статьи, они примерные).
Navidrome (nvd.example.com). При первом входе создаём администратора. Библиотека уже примонтирована (/music), скан идёт по расписанию. Для sonic-похожести нужен плагин AudioMuse: кладём файл audiomuseai.ndp в navidrome/plugins/ и в его настройках указываем адрес AudioMuse (http://audiomuse-ai-flask-app:8000). В compose агент уже включён первым (ND_AGENTS=audiomuseai,deezer) — этого достаточно, чтобы Navidrome начал отдавать «похожие треки».
Soulseek (slskd.example.com). Аккаунт Soulseek заводится сам фактом первого логина с логином/паролем из .env. Проверяем, что демон подключился и что открыт порт раздачи — без входящего порта вы только качаете, но не раздаёте, а на Soulseek это влияет на репутацию.
SoulSync (ss.example.com) — центральный пульт. Здесь всё через интерфейс:
Источники. Включаем нужные: Soulseek (подхватывается автоматически), YouTube (работает из коробки, качает через VPN-прокси). Для торрентов в настройках указываем адреса нашего Prowlarr и qBittorrent — SoulSync будет искать и там.
Профиль качества. Ставим режим best_quality (пулит кандидатов со всех источников и берёт лучший по факту, а не первый), cutoff — FLAC 16-bit, и включаем replace lower quality (замена файла на лучший на месте).
Обслуживание библиотеки (repair-джобы). Включаем те, что чинят метадату скачанного: Unknown Artist Fixer, Metadata Gap Filler (дотягивает MBID из MusicBrainz), Fake Lossless Detector (спектрально ловит псевдо-FLAC), Quality Upgrade (движок апгрейда до FLAC). Про важный нюанс их применения — в разделе «как работает».
Раскладка. Включаем file organization (
Артист/Альбом/Трек) и отмечаем/musicкак постоянную библиотеку — тогда SoulSync пишет прямо в неё и не пытается растащить уже разложенное.Телефон. SoulSync — это PWA: открываем
ss.example.comв мобильном браузере и добавляем на домашний экран. Дальше поиск и скачивание — прямо из кармана. Интерфейс там так себе, но привыкнуть можно, а мне надо раз в месяц.
AudioMuse (audiomuse.example.com). Проходим setup-wizard (указываем Navidrome и креды), запускаем первичный анализ библиотеки — воркер посчитает звуковые эмбеддинги, это разово и небыстро. Похожесть работает только по проанализированным трекам, так что дайте анализу закончиться. Чтобы плейлисты обновлялись сами — на странице Cron заводим две ночные задачи: analysis (проанализировать новьё) и clustering (пересобрать sonic-плейлисты и записать их прямо в Navidrome). После этого никаких действий от вас не требуется — плейлисты появляются в Symfonium как обычные.
Explo. Указываем токен ListenBrainz, задаём расписание — раз в неделю он собирает рекомендацию «по вкусу» и кладёт плейлистом в Navidrome.
Повторю, я только в navidrome пользователя создал и попросил нейронку все через API сервисов настроить, она сама все и настроила за пару итерация проверок, что работает.
Как это работает
SoulSync: от «услышал» до «играет»
Вы ищете трек в PWA, SoulSync опрашивает источники и в режиме best_quality берёт лучшую версию, скачивает её, тегирует и раскладывает в Артист/Альбом/Трек прямо в /music. Через минуту Navidrome его проиндексировал — и он играет в наушниках.
Тонкость, которую стоит знать: у SoulSync два пути добавления. Основной, через поиск-и-подтверждение в интерфейсе, приносит трек с полными тегами, обложкой и текстом. Есть и «сырой» путь (скачать по прямой ссылке) — он кладёт файл без обогащения, плоско. Спасает то, что Navidrome браузит по тегам, а не по папкам: даже плоский-но-тегированный файл виден в библиотеке правильно, а безтеговые дочищают repair-джобы поиском по названию (джобы в soulsync многое могут).
Источники под капотом: YouTube и торрентная связка
YouTube — источник «по умолчанию», настройки не требует вовсе. SoulSync ищет прямо по YouTube, качает лучшую аудиодорожку (на выходе .opus, обычно ~130–140 kbps) и весь этот трафик гонит через HTTP-прокси gluetun — в compose выше это уже задано переменными HTTP_PROXY/HTTPS_PROXY, со своего IP SoulSync наружу не ходит. Дальше обычный конвейер: теги и обложка из метадата-источника, раскладка в /music. Полезная деталь — встроенная проверка целостности: длительность скачанного сверяется с ожидаемой (допуск порядка 15 секунд), и «не то» отбрасывается. Это спасает от классической подставы YouTube-поиска, когда вместо студийной версии прилетает лайв, ремикс или десятиминутный «full album» — я на этом допуске живьём видел, как отсеялся файл с дрейфом в 31 секунду: пришла другая редакция трека, и система её честно не взяла.
По качеству без иллюзий: opus с YouTube — это пол, а не потолок. Тут у SoulSync два поведения, и их стоит различать. Простое «скачай сейчас» идёт по цепочке приоритетов источников — это настройка, у меня youtube → soulseek → torrent (YouTube первым: отвечает быстрее всех, для «услышал — добавил» это то, что нужно). А вот автодобор — wishlist, апгрейд качества — собирает кандидатов со всех включённых источников разом и берёт лучшего по качеству: нашёлся FLAC на Soulseek или торрентах — возьмёт его, не нашёлся — YouTube закроет дырку хоть чем-то. Поэтому Soulseek стоит держать включённым, даже если кажется, что «всё равно всё едет с ютуба».
Торренты — это профиль torrents из compose и связка трёх ролей: Prowlarr — «адресная книга» трекеров, qBittorrent — качалка, SoulSync — заказчик. Первая пара живёт в сетевом namespace gluetun (network_mode: service:gluetun), и отсюда неочевидное следствие: у этих контейнеров нет собственного сетевого имени, их веб-морды и API видны по имени VPN-шлюза. То есть в интеграциях указывается не prowlarr:9696, а http://gluetun:9696 и http://gluetun:8080 — это второе после docker-DNS место (грабля №1), где namespace ломает интуицию, закладывайте.
Настройка руками (или инструктажем ИИ — все три пункта делаются через их API):
Prowlarr (
:9696). Добавляем индексаторы: открытые трекеры заводятся логином/паролем — Prowlarr сам логинится и держит сессию; полузакрытые — пасскеем из профиля на трекере. Тут же берём Settings → General → API Key, он понадобится SoulSync.qBittorrent (
:8080). Здесь ровно одно действие: задать постоянный пароль (грабля №4: временный слетает на рестарте — и интеграция молча отваливается). Каталог загрузок в самом qBit настраивать не нужно — SoulSync передаёт свойsave_pathс каждой раздачей; важно лишь, чтобы у обоих контейнеров был примонтирован один и тот же./data(в compose выше так и есть).SoulSync → Settings: адрес Prowlarr + его API key, адрес qBittorrent + логин/пароль (оба адреса — через
gluetun:, см. выше), путь загрузок/data/torrentsи категория (у меняsoulsync— по ней закачки SoulSync отличимы от всего остального в qBit). Тест-кнопки должны позеленеть.
Как идёт закачка целиком: SoulSync спрашивает Prowlarr → тот веером опрашивает все заведённые трекеры и возвращает раздачи с сидами → SoulSync выбирает подходящую и отдаёт её qBittorrent с категорией soulsync и путём /data/torrents → qBit качает через туннель → SoulSync видит завершение, вынимает из раздачи нужные файлы, тегирует и раскладывает в /music. Раздачу, которая не двигается (нулевые сиды), SoulSync бросает по stall-таймауту — у меня 10 минут, — так что мёртвые торренты не висят в очереди вечно. Специфика музыкальных торрентов, о которую спотыкаешься сразу: раздача — почти всегда альбом или дискография, а не отдельный трек. Для «услышал — добавил» этот канал избыточен (там отрабатывают Soulseek и YouTube), зато для FLAC-альбомов и редкого русского каталога он основной — тот самый добор «мёртвых» из главы про миграцию ехал именно здесь.
Проверить, что цепочка жива, можно одним запросом к Prowlarr, не тыкая интерфейсы (3000 — категория «музыка»):
curl -s -H "X-Api-Key: $PROWLARR_KEY" \ 'http://localhost:9696/api/v1/search?query=<артист>&categories=3000' # → JSON с релизами и числом сидов. Релизы пришли — трекеры отвечают, дальше дело qBit.
FLAC-апгрейд на месте
Quality Upgrade — это движок повышения качества. Раз в неделю он читает каждый файл, сравнивает с профилем (cutoff = FLAC), для трека ниже профиля ищет лучшую версию и показывает находку. Подтверждаете — трек качается из общего пула источников, и включённый replace lower quality заменяет старый файл по тому же пути; Navidrome пересканирует, дублей нет.
Важно, почему это ручное подтверждение, а не автомат: AcoustID и матчинг по названию регулярно путают каверы, лайв и ремиксы. Живой пример из практики — «Die Antwoord — Baby’s on Fire» опознаётся по звуку как «Goat». Автозамена «на похожее» тихо угробит библиотеку неправильными версиями. Поэтому машина ищет — человек подтверждает. Тот же принцип у всех джобов, которые могут подменить файл: они по умолчанию только показывают находки, применяете вы.
Но и тут я просто попросил ИИ через API все самому разгрести, а то там поначалу были какие-то тысячи находок. Сам я пару песен искал, нашел, которые нужны были и скачал, а все разгребание это puthon скрипты, которые ИИ себе написал и сам все сделал. Тратить на такую ерунду жизнь смысла нет больше.
Sonic-рекомендации на телефоне
AudioMuse анализирует звук (Essentia + CLAP-эмбеддинги), а не теги, — поэтому находит похожее даже для обскура, которого нет ни в одной метадата-базе. Плагин audiomuseai.ndp добавляет Navidrome стандартное OpenSubsonic-расширение sonic-похожести, и оно уезжает на телефон.
В Symfonium это оживает как Radio Mix «похожее по звуку» — в настройках клиента включается опция использовать серверную похожесть для радио (иначе радио крутит по артисту, а не по звуку). А вот «умные потоки» Smart Flow (режимы вроде Echo Match) для Subsonic-сервера требуют более свежей версии Symfonium — там завезли поддержку нужного расширения; на более старой они серые. Сервер отдаёт всё — упирается в клиент. Ставьте бэту 15.0 и будет работать.
Второй слой рекомендаций — Explo с ListenBrainz — работает по другой оси: не «похожее по звуку из того, что есть», а «чего у тебя нет» по коллаборативному вкусу. Но я пока проверил, что прослушанное правда уезжает в ListenBraiz, рекомендации оно вроде как генерит раз в неделю, проверю еще.
Удаление трека с телефона
Symfonium не умеет слать серверу команду «удали». Обходим просто: заводим на сервере плейлист «Удалить», а на хосте — таймер, который раз в 10 минут переносит помеченные треки в корзину (обратимо, с манифестом). Кинул трек в плейлист с телефона — через несколько минут он уехал из библиотеки, но не пропал безвозвратно. Скрипт тоже написал ИИ, я проверил, что работает.
Миграция и добор «мёртвых» треков
Отдельный сюжет — как перетащить коллекцию из YTM и вернуть удалённое. Из моей миграции осталось 487 «мёртвых» треков — тех, что Google уже удалил (unavailable/copyright), преимущественно русское и нишевое. Первое, что нужно сделать, — зафиксировать их намертво списком (videoId, артист, название), пока не потерялись окончательно; этот список потом не трогаем.
Soulseek по такому нишевому вывозит процентов двадцать, поэтому основным каналом добора стали торренты — открытые и полу-закрытые трекеры через тот же Prowlarr+qBit под VPN. Тонкость русских трекеров: они индексят дискографиями, а не треками, поэтому искать надо по артисту, тянуть архив, вынимать из него нужные треки по совпадению имени файла и удалять балласт — диск не резиновый.
Часть «мёртвых» была голыми videoId без названия. Их можно опознать через публичный YouTube oEmbed — он отдаёт автора и заголовок ролика без единой куки, по одному videoId. То, чего не нашлось на торрентах, добирается YouTube-альтернативами через SoulSync. В сумме у меня вернулось около 372 из 487.
Дальше чистка. Скачанное с YouTube приходит без тегов, и [Unknown Artist] быстро расплодился. Чинить это надо по звуку: AcoustID-фингерпринт опознаёт запись по фактическому содержимому файла, а не по кривому заголовку вида «ARTIST - Title (Official Video) [4K]» — матчинг по такому заголовку ушёл бы не туда. Плюс дедуп по фингерпринту (один трек в разных качествах → оставить лучшее). Честный потолок: то, чего нет ни во FLAC, ни в альтернативной заливке нигде, останется в opus или потеряется — самохостинг не печатает записи, которых не существует.
Главное конечно, а где все это взять? Плейлисты то где? Это в takeout google. Я там достал не только плейлисты, но и всю свою активность начиная с 2009 года, по которой восстановил где и чем я занимался вплоть до дней, включая геолокацию с точностью до города, но это другая история. По активности можно достать и что я слушал, потому, что в плейлистах Гугла останется только то, что он не удалил, а в активности есть все. Я просто попросил ИИ провести анализ и он нарыл ~600 треков из плейлистов и ~2500 треков по косвенным данным активности на “сайте гугла”, а на деле YTM YT.
Плейлисты из Google Takeout — по шагам
Takeout — это исходник, но из коробки он даёт не плейлисты, а сырьё. Покажу конкретно, как превратить его в рабочие плейлисты в своём стеке, на тех же ручках, что и всё остальное. Сам я это, понятно, руками не собирал: скинул ИИ папку выгрузки и задачу, он написал пачку python-скриптов и прогнал их через API SoulSync и Navidrome прямо в контейнере. Ниже — механика, чтобы повторить (или чтобы отдать этот раздел своему Клоду и не объяснять на пальцах).
Что внутри Takeout. Заказываете выгрузку «YouTube and YouTube Music». Плейлисты лежат в Takeout/YouTube and YouTube Music/playlists/ — по одному CSV на плейлист (лайки — отдельным файлом). Нюанс, который экономит полдня недоумения: в CSV только videoId и метка времени, без названий. Гугл отдаёт голые идентификаторы, названия достаём сами. И то же, что уже сказано выше про мёртвых: в плейлистах Takeout остаётся лишь то, что Гугл ещё не удалил; полную картину «что я вообще слушал» даёт Takeout/My Activity/YouTube/ (там сразу с заголовками), но это шумный источник — туда падает вообще всё.
Шаг 1. videoId → артист и название. По голому id тянем метаданные публичным YouTube oEmbed — без единой куки, по одному id:
curl -s 'https://www.youtube.com/oembed?url=https://youtu.be/<videoId>&format=json' # → {"title":"...","author_name":"Аквариум - Topic", ...}
Канал вида «Артист - Topic» — официальная музыкальная заливка, author_name из него даёт чистого артиста; для обычных каналов заголовок парсим по «Артист — Название». oEmbed вернул 404 — трек Гугл удалил, это «мёртвый» на добор с торрентов (см. выше).
Шаг 2. Разделяем: что уже в библиотеке, что добирать. Для каждой пары (артист, название) спрашиваем Navidrome субсоник-поиском, есть ли трек. Токен субсоника — md5(пароль + соль), считаем его локально, наружу уходит только хеш, не пароль:
curl -s -H 'Host: nvd.music' \ 'http://localhost/rest/search3?query=Аквариум+Город+золотой&u=<user>&t=<md5>&s=<salt>&v=1.16.1&c=cc&f=json' # song[].id в выдаче → трек в библиотеке; пусто → в добор
song.id из выдачи — это и есть идентификатор для плейлиста.
Шаг 3. Недостающее — в добор через SoulSync. Прогоняем через тот же движок, что наполняет всю библиотеку: сперва резолвим кандидата на Deezer (нужны точные длительность и альбом — страховка от «не той версии»), кладём в wishlist полной карточкой, запускаем обработку. SoulSync качает в лучшем качестве и раскладывает в /music:
# найти кандидата на Deezer (ручка SoulSync, ключ не нужен) curl -s -H 'Host: ss.music' --data-urlencode 'query=Аквариум Город золотой' \ -G 'http://localhost/api/deezer/search_tracks' # → tracks[].id / name / artists / duration_ms # положить в wishlist полной карточкой (id, имя, артист, альбом, длительность) # $SS_KEY — разовый ключ SoulSync API из его настроек curl -s -H 'Host: ss.music' -H "Authorization: Bearer $SS_KEY" \ -H 'Content-Type: application/json' -X POST 'http://localhost/api/v1/wishlist' \ -d '{"track_data":{"id":"deezer_123","name":"Город золотой","artists":[{"name":"Аквариум"}],"album":{"name":"…"},"duration_ms":222000},"source_type":"manual"}' # запустить обработку (авто-обработка wishlist и так висит по расписанию, но можно пнуть) curl -s -H 'Host: ss.music' -H "Authorization: Bearer $SS_KEY" -X POST \ 'http://localhost/api/v1/wishlist/process'
Место, где легко налажать на пакетном доборе, — сверка длительности. Deezer по «артист + название» охотно отдаёт ремикс, лайв или кавер той же длины плюс-минус пара минут; добавишь вслепую — в плейлист приедет не та версия. Берём кандидата, только если имя совпало И длительность в пределах ±5–6 секунд, остальное — на ручную сверку. Тот же принцип «машина ищет — человек подтверждает», что и у апгрейда качества.
Шаг 4. Собираем плейлист в Navidrome. Когда добор осел и Navidrome пересканировал /music (новым трекам присвоены id), собираем плейлист субсоник-ручкой createPlaylist — имя и список songId:
curl -s -H 'Host: nvd.music' \ 'http://localhost/rest/createPlaylist?name=Мой+перенос&songId=<id1>&songId=<id2>&u=<user>&t=<md5>&s=<salt>&v=1.16.1&c=cc&f=json'
Грабля напоследок: songId, которых нет в текущем индексе Navidrome, ручка молча пропускает. Если songCount в ответе меньше, чем вы слали, — часть треков ещё не доехала из добора или не нашлась вовсе; дособрать позже той же updatePlaylist.
Как это в жизни. Я отдал ИИ папку playlists/ и сказал «собери в Navidrome, чего нет — в добор». Он распарсил CSV, прогнал id через oEmbed, разделил «есть/нет», недостающее с проверкой длительности покидал в wishlist, а из имеющегося собрал плейлист; мёртвое (oEmbed 404) — отдельным списком на торренты. Всё теми же ручками, что выше. Повторить с другим Клодом — дать ему этот раздел и путь к папке Takeout, дальше он допишет скрипты сам.
Грабли
Места, где я потратил время, чтобы вы не потратили.
Сетевая ловушка namespace. Контейнер за
network_mode: service:gluetunтеряет docker-DNS и не видит соседей по имени. Поэтому в туннель сажаем только качалки, а SoulSync держим в обычной сети с выходом через прокси VPN. В compose выше это уже разведено.root-owned каталоги и тихо дохнущие сервисы. Всё крутится под uid 1000; если какой-то каталог остался root-owned от прежнего запуска, systemd-таймеры под обычным пользователем молча падают — юнит зелёный, работа не делается. Если «настроено, но не срабатывает» — первым делом проверяйте владельца каталогов, а не логику.
AcoustID путает каверы. Поэтому все операции, которые могут подменить файл (апгрейд качества, ретег по звуку), — на ручном подтверждении. Не автоматизируйте, каким бы заманчивым это ни было. Но я автоматизировал, это всего лишь музыка :)
Пароль qBittorrent слетает на рестарте (образ генерит временный при старте) — интеграция с SoulSync отваливается. Лечится тем, что после первого входа задаёте постоянный пароль в веб-морде qBit, тогда хэш ложится в конфиг и переживает рестарт. Делал ИИ.
Killswitch проверяйте IP изнутри namespace, а не с хоста — иначе увидите egress хоста и сделаете неверный вывод об утечке.
Пин версии SoulSync. Проект очень активный, релизы частые; держите конкретный проверенный тег, а не
latest, — ломающий апдейт прилетит ровно тогда, когда раз в неделю зайдёте что-то скачать.
Что в итоге
Своё ядро. Navidrome отдаёт мою коллекцию по OpenSubsonic, клиент — Symfonium через mTLS. Стек переживает ребут и не требует няньки в обычный день.
Добавление с телефона. SoulSync-PWA: услышал → нашёл → скачал в лучшем качестве → легло в библиотеку с тегами и обложкой.
Приватность проверена, а не заявлена. Внешнее приобретение — через VPN; Soulseek-P2P в netns с fail-closed killswitch; домашний IP не светится, наружу только
:443за mTLS.Коллекция наполнена и курируется. Теги причёсаны, обслуживание — на repair-джобах, а не ручном труде.
Рекомендации замкнуты. Explo/ListenBrainz даёт «чего нет», AudioMuse — «похожее по звуку»; sonic доехал до телефона, плейлисты пересобираются каждую ночь сами.
И всё это zero-sub — ни одной активной подписки. Если бы они не хозяйничали в моих плейлистах я так бы и платил, но теперь с ИИ, он настроил все это за три вечера пока я его ругал, что все не так. Самое сложное было подобрать хорошо притертые сервисы и удобные что бы на телефоне было два приложения, одно для поиска (к сожалению PWA лучшее, что найдено) и одно играть. При этом ИИ сам лазит в SoulSync и другие сервисы, я просто в чате пишу “подними качество” или что там у нас по статусу джобов, лазить в PWA SoulSync или еще куда мне не хочется, а в Symfonium на телефоне просто есть огромная коллекция музыки с рекомендациями и просто работает. Как заработало, я так на ковыряниеяи и забил. Symfonium, кстити, не забудьте включить кэширование, что бы задержек не было, оно там выключено по умолчанию. Все вместе даже умные плейлисты строит по спектральному анализу и прочее подобное, все работает и притерто.
P.S. Всем хорошей музыки.
