Это руководство для того, кто решил поднять всё сам. От пустого 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/configmkdir -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)"} >> .envchmod 600 .env
Отдельно — приватный ключ WireGuard от вашего VPN-провайдера. Его вставляем через невидимый ввод, чтобы он не попал ни на экран, ни в history:
read -rs WGKEYprintf '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: musicnetworks: default: name: music ipam: config: - subnet: 172.28.0.0/24 # фиксируем подсеть, чтобы не пересечься с диапазоном VPNx-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,slskd.music,caddy,172.28.0.0/16,.music # внутреннее → напрямую 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=oneshotRemainAfterExit=yesWorkingDirectory=/home/<user>/music-appExecStart=/usr/bin/docker compose --profile torrents up -dUser=<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 многое могут).
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.
Грабли
Места, где я потратил время, чтобы вы не потратили.
-
Сетевая ловушка 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. Всем хорошей музыки.
ссылка на оригинал статьи https://habr.com/ru/articles/1060002/