# Music Музыкальный модуль для [Lampac] — превращает Lampa в полноценный музыкальный сервис: главный экран с полками и чартами, поиск, экраны артистов и альбомов, пользовательские плейлисты, импорт из Spotify / Apple Music / SoundCloud, синхронизированные тексты песен и отдельный музыкальный плеер для iOS, Android и Windows. Модуль серверный (C# / ASP.NET, динамическая компиляция при старте Lampac) с единым клиентом для Lampa ([plugin.js](./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` и памяти. ```mermaid 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 либо вручную как плагин). Проверка, что сервер поднялся: ```bash curl -fsS 'http://127.0.0.1:9118/music/home' curl -fsS 'http://127.0.0.1:9118/music/providers' ``` ## Конфигурация Настройки задаются в `init.conf` Lampac под ключом `Music` и подхватываются без перезапуска. Пример: ```jsonc { "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](./ModuleConf.cs)) со значениями по умолчанию ([ModInit.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](./Services/Playback/MusicPlaybackService.cs) собирает track DTO, [MusicResolver](./Services/Playback/MusicResolver.cs) подбирает 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](./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](https://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](./Providers/Abstractions). 3. Добавить флаги в [ModuleConf.cs](./ModuleConf.cs) и значения по умолчанию в [ModInit.cs](./ModInit.cs) (для audio-источников default — `false`, пока не проверено качество матчей). 4. Зарегистрировать провайдер в [MusicProviderRegistry.cs](./Services/Catalog/MusicProviderRegistry.cs). 5. Проверить через API, не трогая остальную архитектуру. ### Контракты по ролям **Discovery** (`IMusicDiscoveryProvider`): `GetHomeSectionsAsync` — короткие секции для home, `GetSectionAsync` — полный список для «Ещё»/пагинации. **Audio** (`IMusicAudioProvider`): `SearchAsync` — кандидаты `MusicAudioMatch` по `MusicTrack`; `GetStreamsAsync` — playable `MusicPlaybackSource`; `GetPreferredStreamAsync` — быстрый exact-id путь, если он есть, иначе `null`. Проверка: ```bash 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`. ## Разработка После изменения серверного кода — пересборка (модуль компилируется при старте контейнера): ```bash docker compose up -d --build lampac ``` После изменения только `plugin.js` пересборка не нужна (файл монтируется), но клиент Lampa кэширует плагин — нужно поднять revision в конфигурации подключения плагина и полностью перезапустить клиент. Smoke-проверка: ```bash 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 в публичный плейлист и импортировать по ссылке); прямой доступ к библиотеке требует логина и не реализован.