Как бы ни ругали Apple за закрытость платформы и самой экосистемы, некоторые их решения являются исключением. На рынке много стриминговых сервисов, но предоставляющих полноценный SDK для стриминга в сторонних продуктах крайне мало, для российского рынка список официально доступных SDK ограничивается Deezer и Apple Music. Конечно, когда на наш рынок придёт Spotify, на один доступный SDK станет больше, но пока есть два игрока и только один из них имеет широкую пользовательскую базу. Так получилось, что я имел опыт работы с Deezer SDK под Android и прямо сейчас активно работаю с SDK Apple Music (MusicKit) под iOS. И главное отличие от опыта с Deezer состоит в том, что MusicKit — это верхушка айсберга, она же публично доступный API. В отличие от Deezer, где повторить большую часть функциональности официального приложения — это просто длительный процесс, повторить функциональность даже веб-страницы Apple Music, используя только публичный API, невозможно. Даже если Apple использует MusicKit в своих решениях, то кроме него она использует ещё массу недокументированных API запросов и приватных API, которые простым смертным использовать запрещено. В статье я расскажу про работу с MusicKit с точки зрения разработчика в контексте реализации достаточно "простых задач": поискать в каталоге, показать картинки в результатах поиска, получить песни, рекомендации и даже проиграть какую-то музыку. Забегая вперёд скажу, что многое из рассказанного будет справедливо и для работы с Apple Music в Android и Javascript. Если понадобится, готов дать детальные ответы с кодом на вопросы в комментариях.  При работе с MusicKit под iOS придётся взаимодействовать со следующими сущностями: Так же при подключении к Affiliate программе можно запросить доступ к дампу метаданных Enterprise Partner Feed, который Apple советует использовать вместо частых запросов к iTunes Search API. И, на всякий случай, Music.app — это приложение "Музыка" на iOS устройствах, которое является официальным плеером Apple Music. Источники сведений, в порядке убывания их полезности: Самым очевидным способом проверить можно ли что-то сделать в своём приложении является факт того, что что-то подобное имеется в официальном приложении. Вот возьмём, например, поиск. Ищем группу "The Police" и имеем в выдаче Music.app фотографию группы с улыбающимся Стингом: Прекрасно, воспользуемся API поиска. В выдаче имеем ключ artwork с вложенной ссылкой url для всего, кроме артистов. Странно. Проверяем ещё раз, в Music.app всё есть. Более того, в веб-интерфейсе фотография тоже есть, а ссылка на веб-страницу имеется в json-выдаче API поиска. iTunes Search API картинку так же не отдаёт, но так же включает в выдачу адрес веб-странице по ключу artistLinkUrl. Ну что поделать, придётся скачивать web-страницу и доставать из неё адрес картинки, благо разработчики позаботились и прописали адрес картинки в мета-информацию: Формально говоря, мы уже нарушили правила Apple, которые запрещают "scraping" их веб-страниц. Фактически, так делают все сторонние приложения за исключением тех, что ищут картинки с использованием сторонних сервисов. Есть и ещё один минус: мы запрашиваем страницу размером в несколько сотен килобайт. Для приложения и без того гоняющего большие объёмы данных (проигрывающего музыку), терпимо (особенно при учёте, что данные передаются в сжатом виде), но в общем случае не очень. Размеры картинок можно подгонять под свои нужды: в url имеются шаблоны для замены ({w}, {h}), а на веб-странице ссылка на картинку имеет тот же формат, так что сложностей получить картинку нужного размера нет. Если хочется получить закруглённую картинку, то нужно добавить cw перед расширением. Во время проигрывания плеер из Media Player оперирует конструкцией MPMediaItem, которая, в частности, предоставляет объект Artwork. Очень удобно для отображения картинки трека в плеере. Проблема лишь в том, что на практике данный объект, как его ни крути, не даёт картинки для "стримящейся" музыки (а это любая музыка из Apple Music), поэтому приходится "матчить" проигрываемый MPMediaItem c песнями, которые добавлялись в очередь и скачивать изображение аналогично альбомам. В iTunes и приложении Apple Music имеются списки жанров и топы песен в оных. Найти жанр не должно составить труда, ведь мы можем искать по всему чему угодно? Это далеко не так. API не даёт искать по жанрам и не даёт списка жанров. Есть возможность запросить информацию (из полезного — название) жанра по его идентификатору,
но большая часть сведений в API содержит как раз имя жанра, а не идентификатор.  Благо для iTunes предоставляется выгрузка всех жанров (совсем всех, включая "жанры" мобильных приложений). Идентификаторы жанров (как и прочие идентификаторы из iTunes) полностью соответствуют идентификаторам в Apple Music. Выгрузка доступна для всех регионов (регион указывается в ключе cc, а не storefront как в Apple Music API), но фактически отличаются лишь названия (они переведены). Структура жанров иерархична и имеются повторения названий жанров. В общем, поиск по жанрам необходимо делать локально, причём учитывать, что "фанк джаз", "джаз-фанк" и "jazz funk" — это одно и тоже. Не сложно, но и не так просто, как попросить API поиска искать по жанрам. Найдя жанр, можно запросить чарт (список самых популярных песен) для данного жанра. Для редких жанров вполне можно получить в выдаче всего 3 песни. Забавный факт: для некоторых альбомов и артистов (Ed Sheeran из известных) в качестве одного из жанров указывается просто верхнеуровневый жанр "Музыка". Если хочется делать рекомендации на базе жанров, то данный жанр стоит игнорировать. Когда приложению необходимо проиграть песни артиста (музыкальной группы) чаще всего это означает, что необходимо проиграть популярные песни данной группы. Apple Music даёт возможность запросить песни как связь типа "songs" 2 для артиста. На выходе имеем максимум 20 песен 3, явно отсортированных по популярности. Иногда всё же хочется получить чуть больше композиций. С очень популярными группами пройдёт вариант получения плейлистов, типа "[Artist Name]: главное" ("[Artist Name] Essentials") и "[Artist Name]: в деталях" ("[Artist Name]: Next Steps"), но в общем случае такой вариант не подходит. Другой вариант: взять 20 популярных песен, а остальное добить из альбомов, благо Apple Music даёт возможность получить сразу все альбомы артиста (связи типа albums) и все песни в них (include="tracks") за один запрос.  Но если получить все песни артиста, то возникает другая проблема: необходимо из всех этих песен отобрать популярные. К сожалению, API информации о популярности не предоставляет 4. Брать случайные песни из всего списка можно, но на практике такой подход приводит к частому проигрыванию совсем неинтересных записей (проходных лайвов, записей репетиций, скитов и прочего), что особо заметно на очень популярных группах 5. Попробуем получить популярные песни другим образом. Ещё до появления MusicKit компания Apple начала предоставлять API поиска в iTunes Store. Как уже говорилось, каталог в Apple Music и iTunes Store одинаков 6 и, соответственно, идентификаторы песен тоже одинаковы. Данный API возвращает по умолчанию 50 песен, но допустимо запросить до 200 песен. Результаты в выдаче так же отсортированы по популярности.  Интересно, что сама компания Apple не использует ни один из этих вариантов в собственном веб-интерфейсе. Они используют недокументированный (читай, приватный) API 7, в выдаче которого есть всё то, чего не хватает, включая идентификатор (а не только название) жанра и индекс популярности. Использовать данный API на практике я не решился 8, поскольку идентификаторы жанров можно получить обратным поиском по именам жанров 9, а численный индекс популярности не так уж и важен, когда получаешь отсортированный по популярности список композиций. Забавный факт, который я называю "проблемой Хью Лори" заключается в том, что в каталоге Apple Music имеется масса немузыкального контента. В частности, различные компиляции из комедийных шоу и стэндапы. Это с одной стороны прекрасно (мы платили за музыку, а получили ещё и смешные диалоги со Стивеном Фраем, которые можно послушать перед сном), а с другой приводит к прослушиванию какой-то болтовни, когда просил включить музыку. Соответственно, когда контекст работы приложения прямо говорит о том, что пользователь хочет слушать музыку, приходится самостоятельно убирать из полученного списка "песен" те, чьи жанры обозначены как "spoken word" и "comedy". Проигрывание рекомендаций (они же "похожие песни", "радиостанции", "вам понравится" и т.д.) стало базовой функциональностью стриминговых плееров. Условно рекомендации можно разделить на два вида 10: для пользователя (на базе истории прослушивания, покупок и лайков) и для музыкального объекта (артиста, трека, реже альбома и плейлиста).  С предпочтениями пользователя в Apple Music формально 11 всё в порядке: можно запросить список персональных рекомендаций, в который включаются "персональные миксы" (плейлисты) и рекомендуемые альбомы. Из альбомов список песен можно получить, используя API запроса треков (связь tracks), для плейлистов аналогично. Следует учитывать, что треки могут включать не только песни (type = "songs"), но и видео-клипы, потому дополнительная фильтрация полученных данных не повредит. С другими же рекомендациями всё чуть сложнее. Если посмотреть в Music.app, то там отображаются "похожие артисты" и можно включить "радиостанцию" для данного артиста. Последняя проигрывает микс из артиста и рекомендаций к нему. Кажется, всё просто: наверняка для артистов есть связь "похожие артисты". Спешу расстроить, такой связи нет. В схеме данных Enterprise Partner Feed сведения о похожести так же отсутствуют. Хорошо, значит мы можем запросить "радиостанцию" для артиста и получить список песен? И снова нет, радиостанцию запросить мы действительно можем, её идентификатор находится в выдаче связей артиста. Но в сведениях о станции нет сведений о проигрываемых песнях, альбомах или артистах, есть только служебный объект с информацией для проигрывания playParams. Идея с playParams простая: скармливаем данный словарь 12 в очередь проигрывания и слушаем.  Это всё хорошо, если хочется играть только радиостанцию (как делает Music.app), но что если хочется смешать песни из радиостанции 13 с чем-то ещё? Очевидно, мы каким-то образом должны получить список похожих артистов. В Media Player важную роль играют очереди проигрывания, в частности MPMusicPlayerControllerMutableQueue. Очередь позволяет: вставить дескрипторы после определённого проигрываемого объекта, удалить указанные объекты, а так же получить список всеъ объектов в очереди. При запросе объектов на выходе мы должны получить массив из MPMediaItem. А MPMediaItem, в частности включает идентификатор песни. Вот есть только одна проблема: конструкторы очередей не являются публичным API, переведу: Media Player может дать готовую очередь (иногда редактируемую), а самому создать её невозможно 14. Хорошо, немного копнуть в сторону костылестроения может быть полезно. Если Media Player может дать мне изменяемую очередь, то её нужно взять. Для этого придётся работать с текущим (синглтон) плеером, так что действовать нужно быстро, иначе пользователю может проиграться что-то не то, а плеер успеет ещё и предзагрузить ненужные пока песни. Изменяем текущую очередь проигрывания (добавляем дескриптор станции), смотрим что получилось, откатываем измения в очереди новой транзакцией (удаляем то, что фактически добавилось в очередь).  К сожалению обнаруживаем добавление только двух объектов MPMediaItem в очередь. Оба выглядят как песни (есть имя артиста, название трека, длина трека), но не включают идентификатор песни. Если посмотреть в Music.app, то обнаружим, что там всё сходится: включив радиостанцию будет проигрываться песня, а в очереди будет только одна песня к проигрыванию. При переходе к следующей песне очередь обновляется. Дальше пытаться строить костыли с радиостанциями смысла нет, приходится использовать другие варианты. Вспомним, что, в Music.app и веб-интерфейсе отображаются похожие артисты. Похожие артисты доступны только для самых популярных групп 15. Опять приходится прибегать к запрещённому приёму парсинга веб-страницы. Из плюсов: мы уже скачиваем веб-страницу для получения картинки, значит мы можем получить из неё сразу и похожих артистов.  В коде html странице видим большой json-объект, из которого забираем сначала идентификаторы похожих артистов ():  а потом по этим идентификаторам получаем данные артистов из того же json ( с типом lockup/artist): Имея идентификаторы артистов мы можем запросить их треки. API позволяет запросить несколько артистов по их идентификаторам за один запрос с включёнными связями. Увы, на практике API возвращает ошибку, сообщающую о невозможности добавления списка треков к результату. Так можно делать только при запросе артистов по одному или при прямом запросе песен для отдельно взятого артиста. В этом случае можно воспользоваться уже упомянутым iTunes Search API, который отдаст чуть больше полезной информации. Списки "похожих артистов" в Apple Music достаточно хороши, однако на практике часто оказываются пустыми. В ситуации пустого списка рекомендаций приходится использовать сторонние сервисы. Классика — Last.fm, чуть изощерённее — Spotify. Результаты обоих нужно матчить с объектами в каталоге Apple Music. Решение "в лоб" заключается в поиске каждого артиста по имени с получением ровно одного результата с последующим получением песен как описано выше. Решения чуть интереснее, но накладнее состоят в использовании musicbrainz для матчинга. Musicbrainz предоставляет информацию о идентификаторах 16 в других сервисах, т.е. можно связать идентификатор в Spotify с идентификатором в Apple Music. В ситуации с Last.fm всё ещё лучше: Last.fm возвращает в списке рекомендаций идентификаторы из musicbrainz. Накладность заключается в лимитах на использование musicbrainz, при большом трафике придётся поднимать собственный клон musicbrainz (благо, это возможно) или разрабатывать собственный API вокруг базы данных, дампы которой можно спокойно скачать. Для новых групп даже Last.fm не даст рекомендаций, запасным вариантом будут "жанровые" рекомендации. Берём жанры артиста (на базе названия получаем идентификаторы из уже загруженного списка всех жанров), получаем для каждого жанра по чарту, смешиваем полученные песни. Это лучше, чем ничего. За проигрывание в iOS из Apple Music отвечает Media Player, он позволяет работать с музыкой из Apple Music и персональным каталогом пользователя 17. Фреймворк высокоуровневый и в ситуации работы со стриминговым контентом спуститься на уровень ниже 18 не получится. Фреймворк предоставляет два плеера с общим программным интерфейсом: один играет музыку непосредственно в приложении Music.app, а другой работает более-менее независимо. Использовать первый для чего-то серьёзного бессмысленно 19. А документация нагло лжёт 20, что второй плеер не работает в фоновом режиме С архитектурной стороны работать с этим делом просто: говорим плееру что играть (например, заполняя очередь, на основе массива идентификаторов песен) и управляем проигрыванием (играть, пауза, следующий, предыдущий). Прямого управления очередью проигрывания нет, нужно выполнять транзакционное изменение, которое занимает время. При этом достаточно часто обработчик completionHandler вызывается с ошибкой-таймаутом, которая переводится как "я не знаю смог ли я что-то сделать или делаю я ли ещё что-то, но мне надоело самого себя ждать". Чаще всего транзакция проходит нормально, а ошибки, приходящие в обработчик приходится игнорировать. Часть изменений можно вносить и чуть более высокоуровнего, однако если уже пришлось использовать транзакционные изменения, то лучше использовать их для всего. Вообще очереди достаточно гибкие и, если нет вероятности, что ваше приложение будет когда-то работать и с другими стриминговыми сервисами 21, то с очередями можно делать много интересного, в частности указывать в какое время какой элемент начинает играть и как долго. Из других "минусов" такого высокоуровнего подхода стоит отметить невозможность обработки действий пользователя. Нажатие пользователя на гарнитуре кнопки "следующий трек" не приведёт к запросу программы "что мне делать", плеер лучше знает что делать и сам переключит песню. Так что можно только отлавливать фактически свершившиеся изменения в состоянии плеера и принимать какие-то решения постфактум. С другой стороны, много всего делать не нужно: информация о проигрывании на экране блокировки просто есть и все кнопки в ней работают, Apple Watch и Siri умеют управлять проигрыванием, а стриминг через AirPlay на Apple TV работает из коробки 22. Повторить функциональность и интерфейс очереди Music.app в своём приложении возможно, но кажущаяся простота drag'n'drop-а элементов очереди на деле оказывается жонглированием транзакциями изменения очереди и синхронизацией желаемой очереди и реальной очереди 23 плеера. Фактически, работа с API ничем не отличается на других платформах, а вот SDK сделаны не совсем под копирку с iOS и, местами, более удобны 24, хотя общая идея тандема очереди и плеера сохраняется.  Так, под Android можно получить актуальную очередь и двигать элементы очереди без ухищрений с транзакционным изменением оной (удаления из очереди и добавления в нужное место, скрестив пальцы). MusicKit JS же предоставляет специальные методы-обёртки для работы с API Apple Music,
но при использовании обёрток без чтения документации по самому API не обойтись. Работа с очередью и плеером не удобнее iOS SDK, но с небольшими надстройками может стать терпимой . Работать в симуляторе с MusicKit можно только в разрезе общения с HTTP API, но даже в этом случае придётся стабить токен пользователя для проведения запросов, а не получать его через StoreKit. Проигрывать музыку из Apple Music в симуляторе вовсе невозможно, ибо отсутствует приложение Music.app. На деле приходится разрабатывать собственные надстройки над SDK и при запуске в симуляторе использовать альтернативные (mock) реализации. Фактически, при разработке симулятор оказывается полезен только для работы над пользовательским интерфейсом. Собственная абстракция будет полезна и при автоматизированном создании скриншотов для приложения (да и вообще при создании скриншотов с помощью симулятора), поскольку иметь в арсенале оба поколения "больших" iPad Pro только для того, чтобы сделать на обоих скриншоты своего плеера, крайне накладно и глупо. В качестве интересного, но не очень полезного, эксперимента можно сделать альтернативную реализацию собственного высокоуровневого плеера на базе MusicKit JS. С одной стороны Apple предоставляет по запросу дамп метаданных и говорит использовать его, дабы снизить нагрузку на API и получить сведения, которых в API нет. С другой стороны, мы имеем явную проблему курицы и яйца: доступ к Affiliate программе предоставляется при указании ссылки на приложение. Достаточно сложно разрабатывать приложение, используя данные, которые можно получить, только имея опубликованное в App Store приложение. И, конечно, данные не дают первому встречному. Каждое заявление проверяют в течение пяти рабочих дней. На момент написания данной статьи получить дамп метаданных в свои руки мне так и не удалось. Apple завернуло моё заявление, не указав точных причин. Скорее всего, на промо-сайте приложения они увидели словосочетание "Apple Music" без упоминания о том, что я к Apple не имею отношения. Или они где-то увидели, что я использую сведения с веб-страниц, хотя при проверке приложения это проблем не вызывало. Конечно, попытки мои не закончились и сама Apple поощряет повторную подачу заявления, но факт в том, что дампа на руках нет. Можно пофантазировать и даже сделать тестовую базу, поскольку секрета из схемы данных никто не делает, однако само наличие дампа на руках жизнь не упростит. Разрабатывать свой бэкенд и отправлять все запросы из приложения к нему вместо Apple Music не только затратно (как с точки зрения времени на разработку, так и на финансирование инфраструктуры серверной части), но и имеет скрытые проблемы. Так, фид обновляется раз в неделю, а новая музыка в Apple Music появляется ежедневно. Как участнику одного из музыкальных коллективов мне бы хотелось, чтобы свежий релиз появился сразу везде, а не только в официальных приложениях. Вообще использование Enterprise Partner Feed видится полезным в следующих случаях: Приведу несколько банальных советов, которые кому-то могут пригодиться: Не смотря на шероховатости SDK, недостаточность сведений и отсутствие по настоящему полезных batch запросов в API, имеющихся в арсенале средств достаточно для разработки интересных программных продуктов, выходящих за рамки «альтернативных плееров» для Apple Music. Хочется верить, что и другие игроки рынка, включая Яндекс и Гугл начнут предоставлять публичные SDK для музыкальных сервисов. Будем надеяться на светлое будущее с большим выбором музыкальных SDK, а пока закатаем рукава и будем делать интересные вещи в рамках имеющихся ограничений.  Говорят, ограничения даже помогают. 1 Он же метод (научного тыка) 2 Заметьте, что документация о такой связи умалчивает 3 Конечно, в Music.app и веб-интерфейсе при просмотре альбомов есть индикация популярности песен (популярные песни отмечены звёздочками), но в публичном API эта информация (даже простой флаг "популярно") отсутствует 4 Играться с параметром limit не получится: Specified limit for relationship 'songs' exceeds maximum of 20 5 The Beatles 6 Если быть более точным, то всё что есть в Apple Music есть в iTunes Store. В выдаче API iTunes Store присутствует флаг `isStreamablez, по которому можно понять есть объект в Apple Music или нет. 7 Пример запроса: https://itunes.apple.com/WebObjects/MZStore.woa/wa/viewArtistSeeAll?cc=ru&ids=424279384&section=0 8 Хотя уверен, что он достаточно стабилен и, скорее всего, используется в декстопных приложениях iTunes и на устаревших (необновляемых) устройствах с iTunes Store 9 А список жанров нам в любом случае нужен для поиска по ним 10 Классифицируем классификаторы, всё как в лучших университетах нашей страны!  11 Формально, поскольку рекомендации в Apple Music среди пользовательской базы часто воспринимаются негативно: они игнорируют дизлайки, пропуски песен и не всегда похожи на то, что хотел бы услышать пользователь 12 Естественно, предварительно обернув в MPMusicPlayerPlayParametersQueueDescriptor 13 Станции в Apple Music бывают разных типов, в том числе (живые) с настоящими ведущими. Такие нас не очень интересуют в контексте рекомендаций. 14 Если, конечно, хочется попасть в App Store 15 Не ожидайте увидеть (Триаду) в рекомендациях для (Нигатива) и (Сансару) в рекомендациях для (Курары). Вообще не ожидайте увидеть для них рекомендации. 16 Чаще всего это просто ссылки на веб-страницы, но из них достаточно просто достать настоящие идентификаторы 17 Персональный каталог состоит из того, что пользователь купил в iTunes Store и загрузил в iTunes Match 18 И тем более получить с помощью публичного API URL для стриминга 19 Исключение — игры или программы-утилиты, где проигрывание музыки не является основной задачей. Делегировать, так делегировать. 20 Документация говорит: (When your app moves to the background, the music player stops playing the current media.). В действительности под капотом плеер использует AVPlayer, который может работать в фоне, достаточно только указать необходимые флаги в Info.plist или просто поставить галочку в нужном месте и всё будет сделано автоматически. 21 Тогда всё же хочется делать чуть более общую реализацию и свою надстройку над очередью. 22 При этом везде отображаются правильные обложки, которые самому из MPMediaItem получить не удаётся 23 Которую так же приходится получать с помощью транзакции изменения очереди 24 Скорее всего это вызвано тем, что данные SDK подключаются как внешние библиотеки, что несколько развязывает руки разработчикам, позволяя сильно не заботиться об обратной совместимости. В ситуации с iOS Media Player является частью операционной системы, соответственно требования по стабильности API более высокие. 25 Не буду тыкать пальцами в конкретные конкурирующие операционные системы, в которых работа с медиа — это игра в прятки с демонами, а с каждой версией правила игры меняются. 26 Фактически раньше тем же SDK можно было управлять только локальной библиотекой пользователя, теперь добавилась поддержка Apple Music. Apple не создала новый фреймворк, а улучшила существующий. 27 Дорого не только с точки зрения железа, но и самой дорогой части — стоимости труда программиста. Конечно, можно взять сведения из Enterprise Partner Feed, musicbrainz, всё это обработать и на каждый случай предоставлять свой API. Вполне вероятно, что это будет экономить трафик пользователя (хотя в музыкальном приложении это вопрос не первостепенный) и вообще работать быстрее. Однако стоит подождать до того момента, когда наберётся критическая масса пользователей и реальных их проблем. 28 R.I.P. 29 С помощью API поиска Bing 30 Оберегая нас от исков со стороны правообладателей Разработчик