Битва: Яндекс.Музыка vs Marshall или как лайкать треки с помощью кнопок наушников

от автора

На повестке дня — сервис Яндекс.Музыки. Я меломан, у меня куча лайкнутых треков, и знаете что? Я устала. Устала их лайкать. На телефоне-то всё удобно: сверху висит плашка с названием трека и сердечком, и всё делается за секунду.

А вот десктопное приложение — это просто до свидания. Я амбассадор мака (это слово нынче тренд), и меня дико бесит, что для лайка трека мне приходится сворачивать вкладку или уводить фокус со страницы, на которой я работаю.

Но мы упрямые. (и немного наивные)

Идеальная постановка задачи: нажатием кнопки на наушниках ставить лайк на играющий трек Яндекс.Музыки, если он ещё не лайкнут, и показывать overlay с его названием. А если уже лайкнут — просто написать, что вы его уже лайкнули.

Реалии, к которым я пришла: слёзы, куча матов, экзистенциальный кризис от невозможности переопределить кнопку M на Marshall и, конечно же, гора костылей.

Если интересно — читайте дальше.

Состав преступления

  • вайбкодер (это я);

  • его мудрый агент — генератор безумных идей (это Claude);

  • наушники Marshall, кнопки которых были изнасилованы;

  • Яндекс.Музыка в роли злого полицейского;

  • Hammerspoon в роли ниндзя;

  • неофициальное API Яндекса в роли спасителя;

  • демон на macOS в роли наглого шпиона, сломавшего весь процесс медиа-сессий.

Суть уловили? Если коротко — было очень интересно, но неожиданно безвыходно.


Акт 0. Искренняя надежда, что решение кроется в самой Яндекс.Музыке

Сразу договоримся: мы рассматриваем десктопное приложение, а не браузер. Потому что если вы слушаете музыку из браузера — вы не умеете слушать музыку.

У приложения есть горячие клавиши — это здорово.

Горячие клавиши яндекс музыки

Но на маке они почему-то работают, только когда приложение в фокусе. То есть ровно в тот момент, когда мне это не нужно: если Яндекс и так в фокусе, я и мышкой дотянусь.

Я поискала похожие проблемы по маку, нашла какой-то плагин — но ему лет десять, и я не уверена, что он ещё живой. (Но не думайте, что я не пыталась решить всё без вайбкодинга. Попытки были, просто кончились они грустно, да и статьи бы тогда не было)

Что говорит поисковик

Акт 1. Притча о том, что утопия возможна только в книгах

Жил-был разработчик и считал, что всё в этом мире ему подвластно. Что можно взять и вклинить своё произведение искусства прямо в экосистему Яндекса — просто распарсив кнопки Electron-приложения.

Идея такая: у десктопа под капотом — обычный веб. Значит, можно найти в дереве доступности (Accessibility) кнопку лайка и «нажать» её программно:

-- достаём дерево элементов приложения через Accessibility APIlocal app = hs.application.get("ru.yandex.desktop.music")local ax  = hs.axuielement.applicationElement(app)-- ...и ищем среди сотен безымянных AXButton ту самую, что отвечает за лайк

Но злой Яндекс кнопки не подписал (AXTitle пустой) — отличить лайк от соседних можно только по позиции. Плюс приложение часто обновляется и перетасовывает вёрстку. Такая тактика — безумна и ненадёжна.

Вывод: в помойку.

(Но почему-то это первая идея, которую подкинул мне Claude. Иногда мне кажется что агенты специально подсовывают самые извращенские решения в первую очередь, чтобы кожаные человеки спустили на них больше токенов, но у меня нет доказательств)


Акт 2. Анекдот о том, как мы воровали свой собственный токен

Легальными официальными средствами не вышло — пошли к неофициальным.

У Яндекс.Музыки есть неофициальное API, которое живёт на уровне официального (спасибо python-либе MarshalX). Чтобы им пользоваться, нужен токен доступа — который пришлось стырить прямо с сайта и периодически обновлять, как только он протухнет.

Токен кладём в файл, в репозиторий он не попадает:

from pathlib import PathTOKEN_PATH = Path.home() / ".config" / "yandex_music" / "token"token = TOKEN_PATH.read_text().strip()

Надёжно? Не очень. Работает как швейцарские часы? Да.

Бонус: теперь Claude может анализировать моё состояние за неделю по лайкнутым трекам через простенький MCP. Зачем? Ну потому что приятно знать, что ты… В общем, пока решала насущную проблему — получила психологический разбор и рекомендатор песен в лице Claude. Почему бы и нет. (Это не реклама, это я хвастаюсь.)

Сlaude проанализировал мои музыкальные предпочтения

Сlaude проанализировал мои музыкальные предпочтения

И да — теперь можно собрать плейлист из всех песен, в названии которых есть слово «любовь». Как вам? Ну круто же… Правда не совсем законно.


Акт 3. Красивое решение — это простое решение

Берём Hammerspoon для отлавливания нажатий (и надеемся, что никто не узнает, сколько непечатных слов вы на самом деле набираете на клавиатуре).

Задумка простая: по сочетанию клавиш отправлять запрос на лайк играющего трека. Из большой либы нам нужны всего два действия:

  1. узнать, что играет прямо сейчас, и лайкнут ли трек;

  2. поставить лайк.

Как узнать, что играет, если приложение снаружи молчит? Через Ynison — это real-time протокол синхронизации плеера Яндекса между устройствами. Он по WebSocket отдаёт текущий трек:

from yandex_music import Clientfrom yandex_music.ynison import simple# что играет прямо сейчас (real-time sync)playable = simple.get_current_track(token, device_id="headset-bridge-01")track_id = str(playable.playable_id)client = Client(token).init()# уже лайкнут? (идемпотентность: чтобы не снять лайк по ошибке)likes = client.users_likes_tracks().tracks_idsliked = any(str(t.id) == track_id for t in likes)if not liked:    client.users_likes_tracks_add(track_ids=[track_id])   # ♥

Теперь навешиваем это на глобальный хоткей в Hammerspoon и рисуем overlay, не трогая фокус:

hs.hotkey.bind({"cmd", "alt", "ctrl"}, "L", function()  hs.task.new("/bin/sh", function(_exit, stdout)    local title = (stdout or ""):match(":(.+)$")    -- hs.alert — это полупрозрачная плашка поверх всех окон, фокус остаётся    hs.alert.show("♥  " .. (title or ""), 1.0)  end, {"-c", PYTHON .. " " .. API_SCRIPT}):start()end)

И бинго: в любой момент, пока я печатаю, могу зажать четыре клавиши — ⌃⌥⌘L (L — потому что like, остальные — потому что мне нравится ощущение, будто я в эту секунду играю на пианино) — и увидеть красивый overlay с нужной информацией.

Ура, полдела позади.

БОНУС: теперь я могу с компьютера лайкать треки, которые играют на телефоне. Видели бы вы моё лицо, когда я это обнаружила при тесте. Не баг, а фича — причём удобная: не надо знать, где играет музыка, просто слышишь, что нравится, жмёшь клавиши и живёшь счастливо.


Акт 4. А если вы всё-таки не за компьютером…

А теперь начинается грустная теория, которая привела меня к тому, что даже Claude сказал мне успокоиться.

ИДЕЯ: нажимать кнопку на наушниках → ловить её системой macOS → делать нужное действие. Если по-простому — заменить прослойку «сочетание клавиш» на «клик по кнопке наушников».

Вот тут и зарыта подстава, с которой, наверное, стоило начинать рассказ.

На Marshall у меня две кнопки:

Кнопка медиа-сессий:

  • одно нажатие — play/pause;

  • два — подключение по Bluetooth;

  • вправо — следующий трек;

  • влево — предыдущий;

  • вверх/вниз — громче/тише.

Других состояний нет.

Кнопка M (на другом ухе), на неё можно назначить:

  1. вызов голосового помощника (презираю голосовых помощников);

  2. открытие Spotify (недоступного в России);

  3. эквалайзер (вау, спасибо, для истинных ценителей) — этим и пользуюсь;

  4. ничего не делать.

И кнопка M натерпелась от меня многого:

  1. потому что она бесполезна;

  2. потому что её невозможно сделать полезной;

  3. вам серьёзно нужна третья причина?

Нажатие M обрабатывается прошивкой наушников и до устройства не доходит — кроме случая с голосовым помощником. Единственный способ её приспособить — повесить на M вызов ассистента, а на Siri написать хук, который лайкает трек. Но я. Ненавижу. Голосовых. Помощников.

Вывод: ну пожалуйста, сделайте кто-нибудь что-нибудь, это же так грустно — иметь бесполезную кнопку.

Путь номер 2: затесаться шпионом через медиа-сессии

Первым делом я проверила: а ловятся ли кнопки наушников вообще как события macOS?

-- слушаем системные медиа-клавишиprobe = hs.eventtap.new({hs.eventtap.event.types.systemDefined}, function(e)  local d = e:systemKey()  if d and d.key then print("media key:", d.key) end  return falseend):start()-- ...нажимаю play на наушниках. Тишина.

Тишина. Оказалось: Marshall шлёт команды по AVRCP (это Bluetooth-профиль управления), а не как HID-клавиши. До слоя eventtap они не долетают — идут напрямую в MediaRemote, тому приложению, которое сейчас «Now Playing».

Отсюда — дилемма номер 1: либо мы лишаем какую-то кнопку её родной логики, либо выдумываем свой тайный символ.

В ходе дебага выяснилось: частые нажатия play/pause дебаунсятся системой и ничего не дают, а вот prev/next летят без дебаунса — прекрасно. Значит, двойное «вправо» можно превратить в новое событие ЛАЙК, не теряя старый функционал. Думала я — пока не столкнулась с экосистемой macOS.

Тебя никто не будет слушать, если ты ничего из себя не представляешь.

В macOS активный источник медиа-сессий — ровно один, и только им можно управлять. Пока играет Яндекс.Музыка — все события уходят ей напрямую, и ты просто не в состоянии их перехватить.

НО есть выход. Нам нужно прикинуться Яндекс.Музыкой — самим стать плеером.

Ловим кнопки через MPRemoteCommandCenter, но, чтобы система вообще начала отдавать их нам, надо заявить, что играем именно мы:

from MediaPlayer import (    MPRemoteCommandCenter, MPNowPlayingInfoCenter,    MPNowPlayingPlaybackStatePlaying,)cc = MPRemoteCommandCenter.sharedCommandCenter()def on_next(_event):    print("NEXT с наушников прилетел нам!")    return 0  # successcc.nextTrackCommand().addTargetWithHandler_(on_next)# без этого команды к нам не пойдут — заявляем, что МЫ теперь Now Playinginfo = MPNowPlayingInfoCenter.defaultCenter()info.setPlaybackState_(MPNowPlayingPlaybackStatePlaying)

И это сработало: next/prev/play с наушников теперь прилетают нам. Кроме кнопки M — она, как мы помним, живёт в прошивке и в macOS не приходит вообще.

Теперь мы сами — плеер, который вещает какой-то поток (никакой) и отправляет команды через API из Акта 2.

Минусы:

  • искусственно воспроизведённые события;

  • зависимость от интернета;

  • а лайк по двойному next всё равно ещё не работает как надо.

Плюсы: звучит как какое-то решение. На которое Claude сказал мне остановиться.


Акт 5. Смириться с безысходностью… или нет

Представьте: даже ваш умный агент, тот самый мощный генератор безумных идей, говорит вам — ну всё, давай закругляться.

В общем, он сдался. А я — нет.

У меня уже работает Hammerspoon и сочетание клавиш из Акта 3. И у Яндекс.Музыки тоже есть горячие клавиши. Вы сейчас подумаете: «О НЕТ, до неё дошло, что это можно объединить». И да — дошло. Почему-то в самом конце, когда я уже познакомилась со всем, с чем знакомиться не хотела. И почему-то до Claude это сразу тоже не дошло.

Вывод: иногда простое решение проще, чем кажется.

Раскладка хоткеев самой Яндекс.Музыки:

Клавиша

Действие

K

play / pause

N

следующий трек

P

предыдущий

F

лайк

L / J

перемотка

/

громкость

Ключевой трюк: Hammerspoon умеет слать клавишу конкретному приложению, не активируя его окно. То есть управлять плеером, не теряя фокус:

local app = hs.application.get("ru.yandex.desktop.music")-- шлём хоткей Яндекса ПРЯМО приложению, окно не всплывает, фокус мой:hs.eventtap.event.newKeyEvent({}, "k", true):post(app)   -- K = play/pausehs.eventtap.event.newKeyEvent({}, "k", false):post(app)

Мы немного подумали и пришли к выводу, что Ynison нам, по сути, нужен только чтобы прочитать текущий трек под лайк. Всё остальное — это Hammerspoon и горячие клавиши мака.

Итоговая схема демона-гибрида:

  • захватываем Now Playing (MPRemoteCommandCenter) → ловим кнопки гарнитуры;

  • одиночный play/pause → инъекция K, nextN, prevP;

  • двойной next (в окне ~700 мс) → лайк играющего трека через API + overlay ♥.

def on_next():    # накапливаем нажатия; два за 700 мс = ЛАЙК, одно = обычный next    with lock:        count += 1        if count == 1:            Timer(0.7, lambda: inject_key("n") if count == 1 else None).start()        elif count >= 2:            do_like()   # api_like.py: читаем трек + ставим лайк + overlay

И оно работает. С наушников, без потери фокуса, с красивым сердечком.

…но всё-таки безысходность рядом

А потом пришли Bluetooth и экосистема мака, погладили меня по голове и сказали:

Ты не можешь просто взять и перехватывать события, идущие к Яндекс.Музыке. Либо ты плеер — либо ты никому не нужен.

Мой демон, чтобы ловить кнопки, вынужден постоянно держать единственный системный слот Now Playing и притворяться играющим. А это значит: он монополизирует медиа-клавиши даже когда Яндекс молчит, мешает другим приложениям, а стоит Яндексу снова реально зазвучать после паузы — система отдаёт слот ему, и мой шпион теряет захват.

Так что финал — честный: лайк и управление с наушников работают, но демон ведёт себя как наглый сосед, занявший общую розетку. Следующая серия — научить его вежливости: держать слот, только пока музыка реально играет.

Но это уже другая история. А пока я лайкаю треки с наушников, не трогая мышку. Ради этого всё и затевалось.

ссылка на оригинал статьи https://habr.com/ru/articles/1058178/