В прошлой статье мы рассказывали, к чему приводят неистраченные токены в конце месяца: мы собрали открытый Mac-плеер Kinrox (GitHub), который жрёт 196 МБ оперативки вместо 2 ГБ у официального клиента Spotify, крутит фильмы с управлением через пульт на iPhone и включает скринсейвер «Матрицы» с дивана.
Казалось бы, память спасена, процессор в простое показывает 0%, можно спокойно слушать музыку. Но стоило попользоваться собственным плеером пару вечеров подряд, как вылезла проблема, знакомая почти каждому подписчику стримингов в 2026 году.
Включаешь любимый трек на стыке psybient, organic electronic или downtempo, хочешь найти что-то в том же духе, чего ты ещё не слышал, - а штатные алгоритмы Spotify радостно подсовывают тебе твои же собственные залайканные песни трёхлетней давности вперемешку с попсовыми сборниками для кофеен.
Мы посмотрели на верхнюю панель нашего плеера, переименовали скромную вкладку Music прямо в Spotify, добавили в левую колонку раздел Similar & discovery и решили разобраться: почему родной поиск похожей музыки в Spotify стал таким унылым и КАК собрать собственный прозрачный движок дискавери, если сама Spotify в 2025-2026 годах демонстративно отрубила разработчикам старые рекомендательные API?
Поехали.
Что не так с рекомендациями самого Spotify (и какой сюрприз ждёт в API 2026 года)
Если вы пользуетесь Spotify больше пары лет, то наверняка замечали, как изменилось поведение «Радио по треку», автоплея и «Миксов дня». Вместо музыкальных находок алгоритм запирает слушателя в тесном стеклянном стакане по трём причинам:
Эффект шарманки из собственных лайков. До 60-80% выдачи в стандартных миксах и радио по песне составляют треки, которые уже лежат в ваших
Liked tracksили крутились на прошлой неделе. Алгоритм оптимизирует удержание (чтобы вы не нажалиSkip), а самый безопасный способ избежать скипа - скормить человеку то, что он уже заведомо лайкнул.Стягивание к коммерческому мейнстриму. Стоит шагнуть в нишевую электронику, этнику,
handpanили редкий эмбиент, как через три-четыре песни радио плавно съезжает к самым популярным трекам смежного макрожанра. Настоящий андеграунд и малые лейблы тонут на дне.Абсолютный «чёрный ящик». Официальный клиент никогда не объясняет, почему конкретный трек оказался в списке рекомендаций: то ли у него общий сплит-релиз с вашим любимым артистом, то ли он просто попал в витринный промо-хаб.
Ну а теперь начнём разбираться, что происходит, когда ты открываешь документацию Spotify Web API в 2026 году с наивной мыслью: «Сейчас дёрнем /v1/recommendations, передадим seed_tracks, подкрутим target_popularity и сделаем красиво».
Стучимся в API - и получаем HTTP 404 / HTTP 403.
Очень короткий пересказ. В рамках закручивания гаек для приложений в режиме Development Mode (волна ограничений ноября 2024 - февраля 2026 года) Spotify отключила для сторонних разработчиков сразу весь классический стек рекомендаций: эндпоинты GET /v1/recommendations, GET /v1/audio-features, GET /v1/audio-analysis, GET /v1/artists/{id}/related-artists, витринные редакторские подборки и даже чтение треков чужих публичных плейлистов через GET /v1/playlists/{id}/items, если пользователь не владелец и не соавтор этого плейлиста. Конец пересказа.
Интересно. Почти все эти закрытые ручки (audio-features, related-artists, параметрический recommendations) были прямым наследием легендарного музыкального стартапа The Echo Nest, выросшего из MIT Media Lab. В 2014 году Spotify купила The Echo Nest примерно за 100 миллионов долларов. Десять лет открытое API Echo Nest позволяло тысячам инди-разработчиков строить музыкальные карты, генераторы плейлистов по BPM и поисковики редких сцен, пока сама платформа не решила прикрыть лавочку, чтобы на её графе не обучали внешние ИИ-модели. Спасибо за открытую экосистему, было весело.
Итак, прямых кнопок «дай похожих артистов» и «дай рекомендации по треку» в Web API больше нет. Но прежде чем изобретать обходной манёвр для релиза Kinrox 0.5.5, нам пришлось вытащить две собственные занозы в коде плеера.
Разминка перед дискавери: куда пропали чужие плейлисты и почему 30-минутный трек вешал альбом
Когда мы начали готовить интерфейс к новой трёхколоночной навигации (Spotify v3), вскрылись две прекрасные истории.
История первая. Как мы сами спрятали от пользователя все подписанные плейлисты
Открываем левую колонку библиотеки: лайкнутые треки на месте, сохранённые альбомы на месте, а в списке Playlists зияет пустота - видны только личные плейлисты пользователя, тогда как все жанровые подборки, на которые он подписан в Spotify (YOGA | Rhythm & Flow, Guqin/Relaxing Music, Calming Music), бесследно исчезли!
Лезем в SpotifyLibraryStore+Loading.swift и на 54-й строчке находим шедевр собственной перестраховки:
// Как было до 0.5.5:
let editable = remotePlaylists.filter { $0.isEditable(by: library.userID) }
Откуда взялся этот фильтр? В Kinrox работает удобное правило FavoritesPolicy: если ты снимаешь сердечко лайка с песни, плеер автоматически вычищает её и из твоих собственных плейлистов, чтобы она больше не всплывала при шаффле. Поскольку удалять песни из чужих плейлистов нельзя (Spotify вернёт 403 Forbidden), первая версия загрузчика просто... выкидывала все чужие плейлисты прямо на этапе чтения списка GET /v1/me/playlists! Причём в модели LibrarySnapshot уже лежало отдельное вычисляемое свойство editablePlaylists, но кто бы о нём вспомнил.
В 0.5.5 мы разделили сам факт существования плейлиста в медиатеке и доступность его содержимого (itemsAvailable + restrictedPlaylistIDs), а метод ensurePlaylistLoaded(_:requireEditable:) научили различать чтение и запись:
при обычном просмотре (
requireEditable: false) в левой колонке сохраняются вообще все плейлисты (с фильтрамиAll | Mine | Subscribedи разворачиванием секции на всю высоту сайдбара по кнопкеShow all →);если Spotify в Development Mode отдаёт
403на попытку прочитать треки чужого закрытого плейлиста, плеер не падает и не прячет подборку, а помечаетitemsAvailable = falseи выводит спокойную плашку с прямой кнопкой открытия в официальном клиенте;а для поиска живых тематических подборок прямо в сайдбаре появились быстрые чипсы (
#Cosmic,#Harp,#Psybient,#Techno), которые догружают доступные плейлисты через поисковый эндпоинт.
История вторая (0.5.4). Детектив с треком на 33 минуты и гонкой на 200 миллисекунд
Посмотрите на верхний левый угол заглавного скриншота: там играет трек Spiritual Ascension (963 Hz) от Meditative Mind общей длительностью 33 минуты 33 секунды. Именно на таких длинных медитативных и эмбиент-альбомах перед релизом 0.5.4 мы поймали фантомный баг: иногда после окончания длинной дорожки плеер не переходил к следующей песне, а молча замирал.
Расследование в SoloistStateReducer и SoloistProcessSupervisor выявило сразу три сцепленных фактора. Например:
Раннее опустошение буфера на длинных треках. Консольный движок
Spotify Soloistдочитывает последние секунды длинного потока в память заранее, переходит в состояние финального буфера за до 5 секунд до номинальной длительности (duration), а в самом конце присылает кадр со сброшенной позицией0:00и статусомpausedилиidle. Наш старый редьюсер ждал конца трека в более узком коридоре, принимал этот финальныйpausedна нулевой секунде за ручную паузу пользователя и не вызывал переход к следующему элементу очереди! Расширили окноnaturalEndдо 5 секунд и добавили защёлку (latch) терминального завершения.Гонка между закрытием WebSocket и
exit(0). В режиме--single-trackдочерний процессsoloistпосле окончания песни штатно завершается с кодом0. Но ядро macOS закрывает TCP-сокет локального WebSocket на долю секунды раньше, чем доставляет родителю сигнал завершения процесса. В результате наш 200-миллисекундный таймер обрыва связи иногда успевал сработать раньшеterminationHandlerи отменял штатное переключение трека. Научили супервайзер перед отправкой ошибки сокета проверять, не находится ли процесс в стадии штатного выхода.Усыпление через macOS App Nap. Когда 33-минутный трек играет в фоне при погашенном экране, macOS норовит притормозить таймеры приложения в самый момент склейки между песнями. Добавили удержание системной активности (
ProcessInfo.processInfo.beginActivity) на время воспроизведения и переходов, а заодно научили очередь автоматически перешагивать через недоступные в регионе треки (isPlayable == false), чтобы одна заблокированная дорожка в середине альбома не тормозила всю пластинку.
Вывод. Когда базовый плеер перестал спотыкаться на часовых альбомах и вернул на место все плейлисты, руки наконец дошли до главного блюда - собственного поисковика похожей музыки.
КАК мы сделали Similar & Discovery без закрытых эндпоинтов Spotify

Итак, условие задачи: пользователь слушает песню, открывает альбом, выбирает плейлист или кликает на артиста и нажимает кнопку Similar (в верхней деке, в шапке коллекции или переходит во вкладку Similar & discovery слева). Нам нужно выдать ему список реально близких по духу композиций, андеграундных находок и свежих релизов, не имея доступа ни к /v1/recommendations, ни к /v1/related-artists, ни к /v1/audio-features.
Зачем мы вообще решили строить свой прозрачный граф вместо того, чтобы смириться с ограничениями API? Чтобы писать системы, которые понятно поддерживать через два года и которые не разваливаются под реальной нагрузкой или очередной сменой правил платформы.
На пальцах. Если у вас отобрали готовую кнопку «сделай мне хорошо», нужно посмотреть, какие фундаментальные данные Spotify не может закрыть, не сломав собственный поиск и каталог. Таких открытых источников четыре, и на их пересечении мы построили движок MusicDiscoveryEngine (Packages/KinroxMusic/Sources/MusicCore/MusicDiscovery.swift) в связке с SpotifyLibraryStore+Discovery.swift.
Сигнал №1. Совместная встречаемость в плейлистах (playlistCooccurrence, вес 90 + 10 × N)
Кто лучше любого алгоритма знает, какие два трека идеально звучат рядом в ночном сете? Живые люди, которые годами собирали тематические плейлисты руками.
Когда вы ищете похожую музыку по треку или артисту (на скриншоте это David Hohme, Waxman (CA), Slow Ted - With Me - Slow Ted Radio Edit), Kinrox формирует поисковый запрос GET /v1/search?q=<артист + название>&type=playlist,track&limit=5, а параллельно сканирует граф всех ваших локальных и доступных плейлистов в library.playlists.
Если плейлист содержит исходный трек или треки того же артиста, движок вытаскивает его соседей по списку (до 25 треков из каждого найденного читаемого плейлиста) и считает частоту пересечений cooccurrenceCounts[track.id]:
Попался трек в одном общем плейлисте с вашим сидом → получает базовый скор
100баллов (90 + 1 × 10, либо70 + 5 × Nдля чисто локального фоллбэка).Встретился сразу в двух или трёх независимых плейлистах → взлетает на самый верх таблицы с весом
110-120баллов!Сами найденные подборки не пропадают зря: до 3 лучших из них превращаются в готовые карточки секции
MIXES & COMPILATIONSнад таблицей треков (на скриншоте - карточкаslowс кнопкамиPlayи+ Queue).
Сигнал №2. Граф сплитов, ремиксов и лейбловых сборников (compilation, вес 80)
В электронной, инструментальной и инди-музыке артисты постоянно выпускают совместные EP, ремиксы друг на друга (как Fear Less - Hraach Remix у того же David Hohme в шестой строчке нашего скриншота) и попадают на сборники родственных лейблов.
Эндпоинт /v1/artists/{id}/related-artists закрыт, но базовый каталог альбомов артиста открыт! Мы делаем запрос с фильтром групп включения:
GET /v1/artists/{id}/albums?include_groups=appears_on,compilation
Параметры appears_on и compilation игнорируют сольные номерные пластинки самого музыканта и возвращают ровно то, что нужно для дискавери: гостевые участия и сборники, где наш артист засветился плечом к плечу с другими музыкантами той же сцены. Мы загружаем треклисты первых трёх таких релизов через GET /v1/albums/{id}/tracks, начисляем им по 80 баллов и выводим с пометкой Compilation: <название релиза>.
Сигнал №3. Скрытые поисковые операторы tag:hipster и tag:new (вес от 95 до 75)
А вот здесь кроется самая крутая фишка, о которой не подозревают 99% обычных пользователей Spotify.
В верхнем ряду фильтров под заголовком Similar & discovery мы сделали три переключателя режимов (MusicDiscoveryMode): Same spirit, Underground и Fresh.
Откуда берутся данные для Underground и Fresh? В поисковом движке Spotify (GET /v1/search?type=album) до сих пор живут два специальных серверных оператора фильтрации альбомов:
tag:hipster- инструктирует сервер Spotify вернуть только те альбомы, которые входят в нижние 10% по общей популярности во всём каталоге! Когда вы нажимаете таблеткуUnderground, Kinrox берёт имя вашего артиста или выбранный жанровый тег, дописывает к немуtag:hipster, забирает найденные малоизвестные альбомы и разворачивает их треки с приоритетным весом95 - offset. Никакой попсы и миллионников - только настоящий андеграунд, который в обычном интерфейсе Spotify не всплывёт никогда.tag:new- фильтрует каталог по альбомам, вышедшим за последние две недели. НажимаетеFresh- и получаете свежие релизы по вашей нише с тем же приоритетным весом95 - offset.
Причём тут мы сразу поставили жёсткий предохранитель в коде и закрепили его тестом freshAndUndergroundOnlyLabelConfirmedTracks (BrowserSelectionTests.swift):
// Треки из локального кэша никогда не получают фальшивый бейдж андеграунда или новинки:
// метки .underground и .freshRelease выдаются только трекам из подтверждённого ответа с tag:hipster / tag:new
#expect(freshItem?.reasonKind == .freshRelease)
#expect(cachedItem?.reasonKind != .freshRelease)
#expect(cachedItem?.reasonKind != .underground)
Если в процессе поиска в выдачу подмешались просто похожие треки из локального каталога, движок никогда не приклеит к ним обманчивый ярлык Underground · tag:hipster или Fresh release · tag:new.
Сигнал №4. Жанровые чипсы артиста (sharedGenre и sharedArtist, вес 85 - offset)
Четвёртый слой - детальная жанровая таксономия Spotify. Карточка артиста GET /v1/artists/{id} по-прежнему отдаёт массив узких жанровых тегов genres: [String] (именно оттуда на скриншоте подтянулись кнопки #psybient, #organic electronic, #downtempo, #handpan, #ambient, #melodic techno).
В режиме Same spirit мы используем строгий поисковый оператор genre:"<выбранный жанр>" по типу track, а клик по любому жанровому чипсу мгновенно перестраивает выборку вокруг конкретного оттенка звучания.
Почему это не превращается в кашу: фильтр Hide liked, двухфазный отклик и колонка WHY THIS TRACK
Собрать кандидатов из четырёх источников - полдела. Чтобы выдача радовала каждый день, мы добавили три правила, которых катастрофически не хватает в официальном клиенте:
Галочка
Hide likedвключена по умолчанию. Первая же строчка вMusicDiscoveryEngine.buildформирует множество исключений:let excludedIDs = seedTrackIDs.union(hideLiked ? library.likedTrackIDs : []). Все 760 треков, которые уже лежат в ваших лайках на скриншоте, автоматически вырезаются из выдачи! Вы видите только 394 новых кандидата, а не собственную заезженную пластинку.Колонка
WHY THIS TRACKнапротив каждой песни. Никаких секретов от слушателя. У каждого рекомендованного трека справа горит янтарный бейдж, объясняющий, за какие заслуги он сюда попал:In 1 playlists: David Hohme- найден по пересечению в плейлистах;Compilation: ...- вытащен из совместного сборника илиappears_on;Underground · tag:hipster- альбом из нижних 10% популярности Spotify;Fresh release · tag:new- релиз последних двух недель;Artist: David Hohme, Waxman (CA)илиGenre: Mid-Atlantic Dub- совпадение по коллабораторам или жанровому оператору.
Двухфазное обновление за
0 мс+ сетевое дообогащение. Когда вы кликаетеSimilarили переключаете фильтр, никто не заставляет вас смотреть в пустой экран со спиннером. ВMusicViewModel+Discovery.swiftметодrefreshDiscovery()сначала синхронно за единицы миллисекунд строит локальный снимок по вашей SQLite-базе и графу плейлистов, сразу отрисовывает таблицу, а параллельно запускает асинхронную задачу с проверкойdiscoveryRequestIDиsessionGeneration. Если вы быстро прощёлкали три жанра подряд, старые сетевые запросы мгновенно отменяются черезTask.checkCancellation(), не устраивая гонок в UI.
Попутно в 0.5.5 мы перестроили и правую часть окна: теперь там постоянно живёт компактная очередь CompactQueueColumn (Queue). Нашли в Similar & discovery крутую песню, целый микс или альбом? Просто перетаскиваете его мышкой (track:<id>, album:<id>, playlist:<id>) между любыми двумя строчками очереди справа - и альбом сам разворачивается в список треков ровно в указанной позиции.
Сравнение в цифрах: штатный рекомендатор Spotify против Kinrox 0.5.5
Сведём отличия и инженерные параметры в одну таблицу.
Параметр | Официальный клиент | Сторонние клиенты на старом Web API |
| Уровень уверенности |
|---|---|---|---|---|
Потребление RAM на macOS ( | ~2000 МБ (Electron / Chromium) | Зависит от оболочки | ~180-196 МБ ( | Твёрдо ( |
Работа рекомендаций в Development Mode (после февраля 2026) | Внутри закрытого бэкенда | Сломано ( | Работает через 4 открытых сигнала каталога и поиска | Твёрдо (проверено в |
Защита от старых лайков в выдаче | Нет (постоянно крутит уже лайкнутые треки) | Ручная фильтрация, если реализована | Встроенный тумблер | Твёрдо |
Поиск редкого андеграунда (нижние 10% популярности) | Нет кнопки в UI | Почти никто не выводит в интерфейс | Отдельный режим | Твёрдо |
Фильтр только свежих релизов (за 14 дней) | Размыт в общем | Редко используется | Отдельный режим | Твёрдо |
Объяснение причины рекомендации каждого трека | Нет («чёрный ящик») | Нет | Явный бейдж | Твёрдо |
Время первого отклика при открытии | Сетевой запрос к бэкенду | Сетевой запрос | Мгновенно (<5 мс) из локального SQLite-графа + фоновое дообогащение | Твёрдо |
Куда этот движок может расти дальше (и почему здесь огромный простор для экспериментов)
То, что вышло в Kinrox 0.5.5, - это первый рабочий фундамент, который уже сейчас находит музыку интереснее штатного радио. Но самое приятное в собственной нативной архитектуре то, что мы вообще не ограничены фантазией продуктовых менеджеров стриминга. Вот четыре направления, куда мы планируем развивать дискавери дальше:
Локальная обратная связь по дослушиваниям и скипам в SQLite. У нас уже работает быстрый локальный кэш
SpotifyCacheStoreнаlibsqlite3. Если локально (без всякой отправки данных на внешние серверы!) считать, какие рекомендованные треки вы дослушали до конца или добавили в очередь, а какие переключили на первых 15 секундах, скоринг сможет сам подстраивать веса конкретных плейлистов-доноров, лейблов и тегов под ваше настроение. Уровень уверенности: Похоже (вся инфраструктура БД и событий конца трека вSoloistStateReducerуже готова).Мульти-сиды («скрестить два трека или двух артистов»). В текущей версии
openSimilarотталкивается от одного выбранного объекта (трека, альбома, плейлиста или исполнителя). Но что мешает выделить в таблице сразу два-три трека из разных жанров и найти пересечение их графов компиляций и пользовательских плейлистов? Так можно находить идеальные мосты между, скажем, неоклассикой и даунтемпо. Уровень уверенности: Похоже (MusicDiscoveryEngine.buildуже принимает множестваseedTrackIDs: Set<String>иseedArtistIDs: Set<String>).Обогащение через открытые музыкальные базы (MusicBrainz, Discogs, Bandcamp). Spotify знает далеко не всё о реальных связях музыкантов. Поскольку Kinrox - обычное десктопное приложение на macOS, к его графу можно подключить открытое API
MusicBrainzилиDiscogs: узнавать, кто сводил альбом, на каком микро-лейбле выходил винил, в каких сайд-проектах играет перкуссионист, и искать эти имена обратно в каталоге Spotify. Уровень уверенности: Гипотеза (нужно аккуратно сматчить сущности по ISRC и названиям, чтобы не наплодить ложных совпадений).Управление волной открытий прямо с iPhone-пульта. Сейчас с айфона можно управлять очередью, громкостью и лайками, а в следующих версиях хочется добавить на экран пульта быстрые кнопки
Similar,UndergroundиFresh- чтобы менять музыкальную волну, вообще не подходя к ноутбуку.
Где это не сработает и о чём стоит помнить
Обозначим реальные ограничения текущего подхода, чтобы никто не ждал чуда там, где его нет:
Если вы слушаете совсем изолированного исполнителя, у которого в Spotify нет ни одного совместного сборника (
appears_on/compilation), ни одного жанрового тега в профиле и который не попал ни в один читаемый плейлист, сетевой граф вернёт минимум пересечений и движок мягко откатится на локальный каталог вашей медиатеки.Ограничение Spotify Development Mode на чтение чужих плейлистов (
GET /v1/playlists/{id}/items→403 Forbiddenдля не-владельцев) означает, что для ко-оккурентности по сети движок может заглядывать внутрь ваших собственных и коллаборативных плейлистов, а также альбомов и поисковой выдачи треков, но не внутрь закрытых редакционных плейлистов самой Spotify.Как и прежде, для работы музыкального режима нужен активный аккаунт Spotify Premium, свой
Client IDиз кабинета разработчика и скачиваемый в один клик официальный движокSpotify Soloist.
ИТОГ. В чём вообще кайф Kinrox и почему мы зовём вас в Open Source
Для тех, кто заглянул сразу во вторую серию или читал по диагонали, соберём на одной ладони, зачем вообще существует это приложение и почему мы сами кайфуем от него каждый день.
Во-первых, музыка играет напрямую из самого Spotify. Вам не нужно ничего парсить со сторонних сайтов, скачивать гигабайты MP3 на диск или возиться с пиратскими раздачами. Вы просто подключаете свой аккаунт с платной подпиской Spotify, закрываете прожорливый официальный клиент - и вся ваша медиатека, поиск и умный дискавери работают в нативном маковском окне, занимая в 10 раз меньше памяти.
Во-вторых, единый пульт на iPhone для музыки и сериалов. Когда MacBook подключён к проектору или большому экрану по HDMI, вы лежите на диване и с телефона в локальной сети спокойно листаете треки из Spotify. А когда включили кино или сериал - с того же айфона перематываете заставку на 10 или 30 секунд, меняете аудиодорожку с субтитрами или в одно касание переходите к новой серии.
И в-третьих, самое главное - это стопроцентный Open Source:
Скачать готовый нотаризованный билд
Kinrox 0.5.5для Mac (Apple Silicon, macOS 14+): https://kinrox.app/Исходники на Swift 6, движок
MusicDiscoveryEngineи все расследования вdocs/RESEARCHES: https://github.com/awaik/Kinrox
Весь код открыт специально для того, чтобы над плеером можно было прикалываться и развивать его вместе. Хотите подкрутить формулу скоринга в Similar & discovery или прицепить к ней MusicBrainz? Сделали форк, написали фичу, закинули Pull Request. Считаете, что одного скринсейвера «Матрицы» мало и рядом обязательно должен быть трёхмерный лабиринт из Windows 95, трубы или полёт сквозь звёзды? Присылайте PR - и ваш скринсейвер или ваш алгоритм поиска музыки поедет в следующий релиз приложения!
А в комментариях расскажите: как вы сами сейчас ищете новую музыку, когда рекомендации стримингов начинают крутить одно и то же по десятому кругу?

Комментарии 0