Files
lampac/mods/Music
..

Music

Музыкальный модуль для [Lampac] — превращает Lampa в полноценный музыкальный сервис: главный экран с полками и чартами, поиск, экраны артистов и альбомов, пользовательские плейлисты, импорт из Spotify / Apple Music / SoundCloud, синхронизированные тексты песен и отдельный музыкальный плеер для iOS, Android и Windows.

Модуль серверный (C# / ASP.NET, динамическая компиляция при старте Lampac) с единым клиентом для Lampa (plugin.js), который отдаётся как /music.js.

Возможности

  • Главный экран — недавно прослушанное, недавние альбомы/артисты/поиски, пользовательские плейлисты и discovery-полки (Apple Music, Spotify, VK Top, SoundCloud).
  • Поиск — артисты, альбомы, треки; каноническая база метаданных MusicBrainz, дополнительные секции из YouTube Music.
  • Воспроизведение — автоматический подбор playable-источника (YouTube, Sefon, SoundCloud, опционально Z3Fm) с точным поиском по ISRC, когда каталог его предоставляет, и fallback по названию/артисту; выбранный вручную источник закрепляется (pinned) и переживает перезапуски.
  • Пользовательские плейлисты — создание, наполнение из любого экрана, перестановка треков, хранение на сервере с привязкой к профилю браузера.
  • Импорт плейлистов и альбомов по ссылке — Spotify, Apple Music, SoundCloud; без API-ключей и без логина. Повторная синхронизация — слияние: локальный порядок и ручные правки сохраняются, новые треки источника добавляются в начало.
  • Управление очередью отовсюду — лонг-тап по треку в любом списке: «Играть следующим», «Добавить в очередь», «Убрать из очереди»; перестановка и удаление также из шита очереди плеера. Очередь переживает перезапуск клиента (снапшот с восстановлением позиции).
  • Радио — «Радио от трека» из меню любого трека (волна от выбранной песни) и автоподборка в конец очереди; движок похожести — рекомендации SoundCloud (related/station) со слоистым пулом и жёстким дедупом.
  • Статистика прослушиваний — «Твой топ»: играбельный альбом из самых прослушиваемых треков, появляется после накопления данных; счётчики по дням в SQLite.
  • Выбор плеера для музыки — отдельная настройка в «Настройки → Плеер», независимая от глобального плеера Lampa (встроенный, Music Player на Android/Windows, iOS, VLC и другие внешние).
  • Тексты песен — синхронизированная (караоке) и обычная лирика через lrclib с фоллбеком на YouTube Music; ручная подгонка тайминга на телефоне и с пульта телевизора.
  • iOS lock screen — в standalone-режиме плеера: play/pause, next/prev, перемотка скраббером, корректные метаданные и обложка, автопереход треков, устойчивое возобновление на заблокированном экране.
  • Стриминг через сервер — клиент никогда не ходит на внешние CDN напрямую: единый relay /music/stream с поддержкой Range-запросов, тикетами и автоматическим повторным резолвом протухших ссылок.

Архитектура

Модуль не пытается сделать «универсальный парсер любого сайта». Каждый внешний источник подключается в одной или нескольких строго разделённых ролях:

Роль Ответственность Текущие источники
Metadata канонические артисты, альбомы, треки MusicBrainz
Discovery полки, чарты, подборки, секции поиска Apple Music, Spotify, VK Top, SoundCloud, YouTube Music
Audio playable match и stream sources YouTube (основной), Sefon, SoundCloud, Z3Fm (опц.)
Auth внешние пользовательские аккаунты SoundCloud (каркас, выключен по умолчанию)
Images обогащение картинок Discogs (фото артистов)
Import импорт плейлистов по ссылке Spotify, Apple Music, SoundCloud

Ключевые принципы:

  • Metadata-база одна — MusicBrainz. Discovery- и audio-источники не становятся источником правды о каталоге.
  • Home всегда быстрый — discovery-провайдеры опрашиваются параллельно с бюджетом времени; медленный или упавший источник не блокирует ответ, его секция догревается в фоне.
  • Durable отделён от volatile — SQLite хранит только результат действий пользователя (история, плейлисты, pinned-источники, учётные данные); все кэши живут в HybridFileCache и памяти.
flowchart TD
  Client["plugin.js / Lampa UI"] --> Api["Controllers"]
  Api --> Catalog["MusicCatalogService"]
  Api --> Playback["MusicPlaybackService / MusicResolver"]
  Api --> Playlists["MusicUserPlaylistService"]
  Api --> Lyrics["MusicLyricsService"]
  Catalog --> Metadata["Metadata providers"]
  Catalog --> Discovery["Discovery providers"]
  Playback --> Audio["Audio providers"]
  Playback --> Relay["/music/stream relay"]
  Playlists --> DB["Music.sql (SQLite)"]
  Lyrics --> Cache["HybridFileCache"]
  Metadata --> Cache
  Audio --> Cache

Структура каталога

Controllers/        HTTP API: ApiController, SearchController, PlaybackController, AuthController
Models/             Contracts.cs — DTO контрактов API
Providers/
  Abstractions/     интерфейсы провайдеров
  Metadata/         MusicBrainz
  Discovery/        AppleMusic, Spotify, VkTopChart, SoundCloud, YouTubeMusic
  Audio/            YouTube, Sefon, Z3Fm
  SoundCloud/       общий site-support (partial: Catalog / Audio / Import / Auth)
  Spotify/          импорт публичных плейлистов/альбомов (анонимный embed-токен)
  AppleMusic/       импорт публичных плейлистов/альбомов (анонимный web-JWT)
Services/
  Catalog/          оркестрация home / search / секций, реестр провайдеров
  Playback/         резолвер источников, stream relay, тикеты, история
  Playlists/        пользовательские и импортированные плейлисты
  Lyrics/           lrclib + YouTube Music fallback
  Images/           image proxy, Discogs
  Cache/            metadata cache, source-match cache
SQL/                MusicContext.cs — durable SQLite с self-healing и safe backup
plugin.js           весь клиент Lampa, отдаётся как /music.js
ModInit.cs          регистрация модуля и значения конфигурации по умолчанию
ModuleConf.cs       схема настроек
manifest.json       dynamic: true, зависимость YoutubeExplode

Установка

  1. Поместите каталог Music в Modules/ вашей установки Lampac. Модуль объявлен как dynamic и компилируется автоматически при старте.
  2. Перезапустите Lampac. При первом старте создаётся database/music/ с SQLite-базой.
  3. Клиентский плагин доступен по адресу {host}/music.js — подключите его в Lampa штатным способом (initPlugins в конфигурации LampaWeb либо вручную как плагин).

Проверка, что сервер поднялся:

curl -fsS 'http://127.0.0.1:9118/music/home'
curl -fsS 'http://127.0.0.1:9118/music/providers'

Конфигурация

Настройки задаются в init.conf Lampac под ключом Music и подхватываются без перезапуска. Пример:

{
  "Music": {
    "useproxy": false,
    "useproxystream": false,
    "applemusic_country": "ru",
    "applemusic_album_resolver": "auto",
    "spotify_discovery_enabled": true,
    "spotify_country": "ua",
    "soundcloud_country": "UK",
    "z3fm_enabled": true,
    "z3fm_audio_enabled": true
  }
}

Полная схема (ModuleConf.cs) со значениями по умолчанию (ModInit.cs):

Параметр По умолчанию Описание
default_metadata_provider musicbrainz metadata-база каталога
default_audio_provider youtubeaudio основной audio-провайдер
default_auth_provider (пусто) auth-провайдер по умолчанию
useproxy false прокси для API, поиска и discovery; используется стандартный proxy Lampac
useproxystream false прокси для получения manifest и передачи audio-stream
globalnameproxy (пусто) имя proxy из глобального globalproxy Lampac
proxy (пусто) локальная настройка proxy Music; имеет приоритет над глобальной
client_debug_enabled false приём клиентских трейсов на /music/clientlog (только для отладки)
stats_clear_enabled false показывать сервисную кнопку очистки статистики «Твоего топа» в фильтре
youtube_audio_enabled true YouTube как audio-источник (через YoutubeExplode)
spotify_search_fallback_enabled false дополнительные результаты поиска Spotify
spotify_discovery_enabled true региональная полка плейлистов Spotify на главном экране
spotify_country us профиль плейлистов Spotify: UA, CA, US, RU, PL, FR, DE; для остальных ISO-кодов используется явно подписанный Global-профиль
sefon_audio_enabled true Sefon как дополнительный audio-источник
soundcloud_enabled true SoundCloud целиком
soundcloud_discovery_enabled true полки/подборки SoundCloud
soundcloud_audio_enabled true SoundCloud как audio-источник
soundcloud_auth_enabled false вход в аккаунт SoundCloud (OAuth)
applemusic_country us регион Apple Music Charts (ISO 3166-1 alpha-2)
applemusic_album_resolver auto открытие альбомов из Apple-полок: auto, applemusic, spotify, soundcloud или musicbrainz
soundcloud_client_id (пусто) собственный client_id; без него скрапится публичный
soundcloud_client_secret (пусто) для OAuth-потока
soundcloud_redirect_uri (пусто) для OAuth-потока
soundcloud_country US регион SoundCloud Charts: US или UK (GB нормализуется в UK)
z3fm_enabled false Z3Fm целиком (требует Playwright для антибота)
z3fm_audio_enabled false Z3Fm как audio-источник
z3fm_proxy_enabled / z3fm_proxy_url / z3fm_proxy_username / z3fm_proxy_password false / (пусто) прокси для Z3Fm
limit_map ^/music: 15 req/s WAF rate-limit для эндпоинтов модуля

Каждый профиль Spotify содержит одну полку с 15 плейлистами: пять основных региональных, пять дополнительных региональных тематических и пять глобальных тематических. Для CA, US, PL, FR, DE используются официальные подборки Spotify; профили UA и RU частично состоят из публичных локальных плейлистов. После смены spotify_country нужен перезапуск Lampac; недоступный плейлист пропускается, не блокируя остальные discovery-источники.

applemusic_album_resolver задаёт приоритет открытия карточек Apple Music Charts. auto использует цепочку Apple ID → Spotify → SoundCloud → MusicBrainz; applemusic — Apple ID → MusicBrainz; spotify — Spotify → MusicBrainz; soundcloud — SoundCloud → MusicBrainz; musicbrainz сохраняет старое поведение. В SoundCloud учитываются только результаты из секции альбомов, пользовательские плейлисты исключены. Spotify участвует при включённом spotify_search_fallback_enabled, SoundCloud — при включённом soundcloud_discovery_enabled; неизвестное значение строго нормализуется в auto.

HTTP API

Все эндпоинты живут под префиксом /music. Внешние картинки в ответах проксируются через image-proxy, stream-ссылки — через relay: клиенту не нужны прямые запросы к внешним сервисам.

Клиент и служебные

Метод Эндпоинт Назначение
GET /music.js, /music/js/{token} клиентский плагин
GET /music/providers список активных провайдеров
POST /music/clientlog приём клиентских трейсов (при client_debug_enabled)

Каталог и поиск

Метод Эндпоинт Назначение
GET /music, /music/home главный экран
GET /music/section?id= полный список секции («Ещё» / пагинация)
GET /music/search?q= поиск
GET /music/artist?id= экран артиста
GET /music/artistsection догрузка секций артиста
GET /music/artistimg lazy-обогащение фото артиста (Discogs)
GET /music/album?id= экран альбома
GET /music/track данные трека

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

Метод Эндпоинт Назначение
GET /music/play playable-ответ для трека (источники + stream URL)
GET /music/stream relay стрима (Range, тикеты, авто re-resolve)
GET /music/matches кандидаты audio-источников для трека
POST /music/match/select закрепить ручной выбор источника (pinned)
POST /music/match/reset сбросить закреплённый выбор
GET /music/playlist.m3u плейлист очереди в формате M3U
POST /music/history/mark обновить историю; count_play=true инкрементит счётчик статистики, played_ms добавляет фактически прослушанное время
POST /music/history/remove удалить запись истории
GET /music/stats/top топ треков профиля + условия разблокировки «Твоего топа»
POST /music/stats/clear очистить дневную статистику текущего профиля, не трогая историю прослушиваний

Плейлисты

Метод Эндпоинт Назначение
GET /music/playlists список плейлистов профиля
GET /music/playlists/tracks треки плейлиста
POST /music/playlists/create создать плейлист
POST /music/playlists/delete удалить плейлист
POST /music/playlists/import импорт по ссылке (Spotify / Apple Music / SoundCloud)
POST /music/playlists/sync пересинхронизировать импортированный плейлист
POST /music/playlists/track/add добавить трек
POST /music/playlists/track/remove убрать трек
POST /music/playlists/track/move переставить трек внутри плейлиста (position — финальный индекс)
POST /music/radio подобрать похожие треки (автоподборка очереди и «Радио от трека»)
GET /music/daily микс дня недели (day 1–7, опц. salt — счётчик сброса): треки двух артистов из истории + похожего

Прочее

Метод Эндпоинт Назначение
GET /music/lyrics текст песни (synced/plain)
GET /music/auth/state состояние внешней авторизации
POST /music/auth/save сохранить учётные данные
POST /music/auth/logout выйти

Как это работает

Резолв и воспроизведение трека

  1. Клиент вызывает /music/play с идентификатором трека и fallback-параметрами (артист, название, длительность).
  2. MusicPlaybackService собирает track DTO, MusicResolver подбирает audio match: сначала закреплённый пользователем pinned-источник, затем кэшированный авто-матч, затем поиск по провайдерам с ранжированием.
  3. Выбранный провайдер возвращает stream sources; сервер превращает их в тикетные URL /music/stream?... — прямые ссылки на CDN клиенту не отдаются.
  4. Relay отдаёт поток с поддержкой Range. Если тикет жив, а upstream-ссылка умерла (403/404/410), выполняется прозрачный повторный резолв по fallback-параметрам — трек не прерывается ни при протухании ссылки, ни после перезапуска сервера.

Ручной выбор источника («Источники» в интерфейсе, включая поиск произвольным запросом) сохраняется как pinned в SQLite и имеет безусловный приоритет над эвристиками — это спасает треки с «грязными» метаданными из чартов.

Режимы плеера

У клиента две принципиально разные реальности воспроизведения:

  • inner — встроенный плеер Lampa. На iOS lock screen поддерживает play/pause, next/prev, метаданные и обложку; перемотка скраббером в этом режиме сознательно не поддерживается (подробный разбор: docs/ios-embedded-lockscreen-seek.md).
  • ios — standalone audio-плеер модуля для iPhone: собственный полноэкранный интерфейс (очередь, shuffle/repeat, таймер сна, лирика, свайпы), полный Media Session (включая скраббер перемотки), keep-alive аудиосессии и восстановление воспроизведения на заблокированном экране.

Эти режимы не следует смешивать — у них разная логика Media Session и жизненного цикла audio-элемента.

Плейлисты и импорт

Плейлисты хранятся на сервере (SQLite, таблица user_playlists) с привязкой к профилю браузера — той же identity, что и история прослушиваний. Payload плейлиста содержит полные объекты треков, поэтому плейлист самодостаточен и не зависит от доступности внешних каталогов.

Импорт по ссылке работает без ключей и логина:

  • Spotify — анонимный токен со страницы embed-плеера + внутренний GraphQL API с пагинацией; импорт атомарный (сбой любой страницы отменяет импорт целиком).
  • Apple Music — анонимный developer-JWT из веб-бандла music.apple.com + публичный catalog API; поддержаны плейлисты (включая пользовательские pl.u-…) и альбомы, все storefront'ы.
  • SoundCloud — публичный client_id (скрапится автоматически либо задаётся в конфиге).

Импортированный плейлист хранит source и может быть пересинхронизирован. Синхронизация зеркалит источник: ручные изменения в таком плейлисте будут перезаписаны.

Тексты песен

/music/lyrics возвращает синхронизированный (LRC, построчный тайминг) или обычный текст. Основной источник — lrclib.net; при отсутствии synced-варианта пробуется fallback через YouTube Music. Кэширование дифференцированное: synced-результаты живут долго, plain-only и «не найдено» — коротко, сетевые сбои не кэшируются вовсе и помечаются retry для клиента.

Для синхронизированного текста клиент позволяет подогнать тайминг в диапазоне от −10 до +10 секунд с шагом 0,5 секунды. + показывает текст позже, − — раньше; нажатие на значение сбрасывает сдвиг в ноль. В standalone-плеере управление находится в окне «Текст», во встроенном плеере — над лирикой и отдельными кнопками − / + на панели для управления пультом. Сдвиг действует только на текущий трек, сбрасывается при его смене и не показывается для обычного текста без таймингов.

Радио

У радио два входа:

  • «Радио от трека» — пункт в лонг-тап меню любого трека: выбранный трек стартует первым, дальше — волна из ~20 подобранных. При неудаче подбора текущая очередь не меняется.
  • Автоподборка — opt-in тумблер («Автоподборка треков» в фильтре и чип «Подборка» в фулл-плеере — одна настройка, выключена по умолчанию): когда очередь подходит к концу, в неё догружаются похожие треки. В шите очереди граница отмечается разделителем «Дальше автоподборка».

Движок похожести — слоистый пул на сервере (MusicRadioService):

  1. Рекомендации SoundCloud по треку — related и track-station (их коллаборативная фильтрация), основной слой;
  2. Топ-треки сид-артистов (YouTube Music / SoundCloud поиск) — только backfill при недоборе.

Пулы чередуются round-robin, чтобы выдача не вырождалась в одного исполнителя.

Контракт:

  • работает только для управляемой очереди модуля (ios / inner), не для внешних m3u-плееров;
  • POST /music/radio принимает seed-треки и явный exclude текущей очереди; сиды всегда исключаются из выдачи;
  • дедуп: track.id, нормализованные artist + title, история прослушиваний за сутки;
  • шумофильтр: миксы/подкасты/реакции, треки короче 45с и длиннее 12мин; cover/karaoke/sped up/slowed/nightcore/remix — только если этих слов не было в сиде; при полностью кириллическом сид-контексте латинский мусор отсекается;
  • триггер автоподборки — предпоследний трек по порядку воспроизведения (с учётом shuffle), не чаще одного запроса на поколение очереди;
  • repeat=all отключает автоподборку: зацикленная очередь — осознанный выбор;
  • если кандидатов мало, сервер честно отвечает available:false и клиент ничего не добавляет.

Главный риск фичи — не код, а качество рекомендаций; настройка ведётся по живым промахам, не «по формуле». SponsorBlock для YouTube-треков сознательно не в roadmap (нет реальной боли, внешний skip-сервис рискует ложными пропусками); YouTube Mix как движок рекомендаций проверен и отвергнут — анонимный доступ закрыт YouTube («Войдите в аккаунт»), логин-пути модуль не использует.

Статистика: «Твой топ»

Статистика подаётся не экраном с цифрами, а играбельным альбомом: карточка «Твой топ» появляется на главном экране рядом с плейлистами только после накопления данных (10 разных треков и либо фаворит с 10 прослушиваниями, либо 100 прослушиваний суммарно). Внутри — топ треков с настоящими «Слушать»/«Перемешать», числом прослушиваний у каждой строки и сухой метой в духе «247 прослушиваний · примерно 18 ч · артисты заработали ≈ $0.99 · почти кофе».

Данные: таблица track_stats_daily (профиль, трек, день, счётчики, last_played) — только счётчики, витрина треков джойнится из playback_history. play_count инкрементится исключительно на честном play-пути (count_play=true после реальной задержки воспроизведения), а total_ms пополняется фактическими кусками played_ms без повторного увеличения счётчика; обновления payload статистику не трогают. Дневная гранулярность оставляет пространство для будущих окон («за месяц», «На повторе», «Вернуть в ротацию»).

Хранение данных

Durable (SQLite, database/music/Music.sql) — только то, что выражает действие пользователя или долгоживущую identity: история прослушиваний, дневная статистика (track_stats_daily), пользовательские плейлисты, pinned-выборы источников, учётные данные внешних сервисов. База защищена self-healing (карантин повреждённого файла вместе с WAL/SHM-сайдкарами, восстановление из последнего валидного бэкапа) и безопасным бэкапом через SQLite backup API.

Volatile (HybridFileCache + память) — кэши метаданных, авто-матчей, лирики, картинок, stream-тикеты. Волатильный кэш никогда не попадает в SQLite.

Расширение: новый источник

Источник подключается не «сайтом целиком», а конкретной ролью. Рекомендуемый порядок внедрения незнакомого сервиса: Discovery → Audio → Auth — сначала источник безопасно появляется в интерфейсе, затем добавляется воспроизведение.

Общий порядок:

  1. Определить, что источник реально даёт (полки? стримы? аккаунт?).
  2. Реализовать провайдер под соответствующий интерфейс из Providers/Abstractions.
  3. Добавить флаги в ModuleConf.cs и значения по умолчанию в ModInit.cs (для audio-источников default — false, пока не проверено качество матчей).
  4. Зарегистрировать провайдер в MusicProviderRegistry.cs.
  5. Проверить через API, не трогая остальную архитектуру.

Контракты по ролям

Discovery (IMusicDiscoveryProvider): GetHomeSectionsAsync — короткие секции для home, GetSectionAsync — полный список для «Ещё»/пагинации.

Audio (IMusicAudioProvider): SearchAsync — кандидаты MusicAudioMatch по MusicTrack; GetStreamsAsync — playable MusicPlaybackSource; GetPreferredStreamAsync — быстрый exact-id путь, если он есть, иначе null.

Проверка:

curl -fsS 'http://127.0.0.1:9118/music/home'
curl -fsS 'http://127.0.0.1:9118/music/section?id=examplemusic:top'
curl -fsS 'http://127.0.0.1:9118/music/matches?title=Song&artist_name=Artist&audio_provider=examplemusicaudio'

Обязательные требования к провайдерам

  • Не делать новый источник metadata-базой без отдельного архитектурного решения.
  • Не блокировать /music/home долгими запросами — уважать бюджет времени.
  • Не кэшировать сетевой сбой как «пусто навсегда»; пустые результаты кэшировать только коротко.
  • Stream отдавать через /music/stream, а не напрямую клиенту.
  • Уважать pinned-выбор пользователя.
  • Внешние картинки возвращать в обычных полях ответа, чтобы их подхватил MusicImageProxyService.

Разработка

После изменения серверного кода — пересборка (модуль компилируется при старте контейнера):

docker compose up -d --build lampac

После изменения только plugin.js пересборка не нужна (файл монтируется), но клиент Lampa кэширует плагин — нужно поднять revision в конфигурации подключения плагина и полностью перезапустить клиент.

Smoke-проверка:

curl -fsS 'http://127.0.0.1:9118/music/home'
curl -fsS 'http://127.0.0.1:9118/music/search?q=nirvana'
curl -fsS 'http://127.0.0.1:9118/music/providers'

Клиентская отладка: включить client_debug_enabled, трейсы клиента будут приходить на /music/clientlog и попадать в логи сервера. После отладки — выключить.

Известные ограничения

  • Discovery/импорт Spotify, импорт Apple Music и авто-получение SoundCloud client_id опираются на недокументированные публичные механизмы этих сервисов; при изменениях на их стороне интеграция может временно ломаться до обновления модуля (симптом для Spotify — PersistedQueryNotFound в логах).
  • Перемотка скраббером на iOS lock screen поддерживается только в standalone-режиме плеера (ios), не во встроенном (inner).
  • Синхронизация импортированного плейлиста — недеструктивное слияние: порядок и ручные правки сохраняются, новинки добавляются в начало; но треки, удалённые в источнике, локально не удаляются (убрать можно только вручную).
  • Управление очередью (играть следующим, убрать, радио-догрузка) работает только для управляемой очереди модуля (ios / inner); внешние плееры получают разовый плейлист без возможности менять его после запуска.
  • Статистика накапливается с момента включения фичи: прослушивания до неё известны только как «последний раз слушал», без счётчиков.
  • Лайки/библиотека внешних сервисов переносятся через публичный плейлист (скопировать Liked Songs в публичный плейлист и импортировать по ссылке); прямой доступ к библиотеке требует логина и не реализован.