На повестке дня — сервис Яндекс.Музыки. Я меломан, у меня куча лайкнутых треков, и знаете что? Я устала. Устала их лайкать. На телефоне-то всё удобно: сверху висит плашка с названием трека и сердечком, и всё делается за секунду.
А вот десктопное приложение — это просто до свидания. Я амбассадор мака (это слово нынче тренд), и меня дико бесит, что для лайка трека мне приходится сворачивать вкладку или уводить фокус со страницы, на которой я работаю.
Но мы упрямые. (и немного наивные)
Идеальная постановка задачи: нажатием кнопки на наушниках ставить лайк на играющий трек Яндекс.Музыки, если он ещё не лайкнут, и показывать 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. Почему бы и нет. (Это не реклама, это я хвастаюсь.)
И да — теперь можно собрать плейлист из всех песен, в названии которых есть слово «любовь». Как вам? Ну круто же… Правда не совсем законно.
Акт 3. Красивое решение — это простое решение
Берём Hammerspoon для отлавливания нажатий (и надеемся, что никто не узнает, сколько непечатных слов вы на самом деле набираете на клавиатуре).
Задумка простая: по сочетанию клавиш отправлять запрос на лайк играющего трека. Из большой либы нам нужны всего два действия:
-
узнать, что играет прямо сейчас, и лайкнут ли трек;
-
поставить лайк.
Как узнать, что играет, если приложение снаружи молчит? Через 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 (на другом ухе), на неё можно назначить:
-
вызов голосового помощника (презираю голосовых помощников);
-
открытие Spotify (недоступного в России);
-
эквалайзер (вау, спасибо, для истинных ценителей) — этим и пользуюсь;
-
ничего не делать.
И кнопка M натерпелась от меня многого:
-
потому что она бесполезна;
-
потому что её невозможно сделать полезной;
-
вам серьёзно нужна третья причина?
Нажатие 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 это сразу тоже не дошло.
Вывод: иногда простое решение проще, чем кажется.
Раскладка хоткеев самой Яндекс.Музыки:
|
Клавиша |
Действие |
|---|---|
|
|
play / pause |
|
|
следующий трек |
|
|
предыдущий |
|
|
лайк |
|
|
перемотка |
|
|
громкость |
Ключевой трюк: 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,next→N,prev→P; -
двойной
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/