Как все начиналось
Дело было вечером, делать было нечего… Точнее, я просто хотел загрузить аудиокнигу перед парами и тут меня ждал сюрприз. Кэш в кейт мобайле отключили. Как так? Что делать? Конечно же писать свое приложение с кэшем и аудиозаписями. Но для начала нужно понять, как вк превращает ссылки вида audio%user_id%_%track_id% в прямые ссылки на mp3. Что из этого вышло
Дебажим js
Начинаем с очевидного — открываем вкладку аудиозаписей и смотрим код. Видим onclick на кнопке у каждой аудиозаписи:
onclick="return getAudioPlayer().toggleAudio(this, event)"Открываем audioplayer.js, включаем Pretty print и ищем функцию toggleAudio().
toggleAudio()
AudioPlayer.prototype.toggleAudio = function(t, e) {
if (vk && vk.widget && !vk.id && window.Widgets)
return Widgets.oauth(),
!1;
if (domClosest("_audio_row__tt", e.target))
return cancelEvent(e);
var i = domClosest("_audio_row", t)
, o = AudioUtils.getAudioFromEl(i, !0);
if (window.getSelection && window.getSelection().rangeCount) {
var a = window.getSelection().getRangeAt(0);
if (a && a.startOffset != a.endOffset)
return !1
}
if (e && hasClass(e.target, "mem_link"))
return nav.go(attr(e.target, "href"), e, {
navigateToUploader: !0
}),
cancelEvent(e);
if (hasClass(e.target, "_audio_row__title_inner") && o.lyrics && !o.isInAttach)
return AudioUtils.toggleAudioLyrics(i, o),
cancelEvent(e);
if (hasClass(e.target, "audio_row__performer"))
return checkEvent(e) || vk.widget ? !0 : (AudioUtils.audioSearchPerformer(e.target, o.performer, e),
cancelEvent(e));
var s = cur.cancelClick || e && (hasClass(e.target, "audio_lyrics") || domClosest("_audio_duration_wrap", e.target) || domClosest("_audio_inline_player", e.target) || domClosest("audio_performer", e.target));
if (cur._sliderMouseUpNowEl && cur._sliderMouseUpNowEl == geByClass1("audio_inline_player_progress", i) && (s = !0),
delete cur.cancelClick,
delete cur._sliderMouseUpNowEl,
s)
return !0;
if (AudioUtils.isClaimedAudio(o) || o.isReplaceable) {
var r = AudioUtils.getAudioExtra(o)
, l = r.claim;
if (l)
return void (hasClass(i, "no_actions") || o.isInEditBox || showAudioClaimWarning(o, l, AudioUtils.replaceWithOriginal.bind(AudioUtils, i, o)))
}
if (o.isPlaying)
this.pause();
else {
var n = AudioUtils.getContextPlaylist(i);
this.play(o.fullId, n.playlist, o.context || n.context),
cur.audioPage && cur.audioPage.onUserAction(o, n.playlist)
}
AudioUtils.onRowOver(i, !1, !0)
}Ставим брейкпоинт на первую инструкцию и нажимаем на кнопку. Входим в режим отладки и начинаем выполнять код по шагам.
Видим, что переменная o через несколько шагов содержит кучу полезной информации: исполнитель, название, хэши, всякие айди. Но не содержит главного свойства — url.
Идем дальше, выходим из toggleAudio(), попадаем в файл common.js в функцию, работающую с эвентами. Ага, значит ссылку мы получим асинхронно, надо бы посматривать на сетевую активность, что ж, шагаем дальше. Через n шагов замечаем, что во вкладке network появляется интересный запрос:
Request URL:https://vk.com/al_audio.php
Request Method:POST
Form Data:
act:reload_audio
al:1
ids: список id вида %user_id%_%track_id%, которые нужно обновитьИ не менее интересный ответ:
4089188939145<!><!>0<!>6854<!>0<!><!json>[[456239119,%my_id%,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=ofvLohaZvtDKnOPmEtHWl2rJCLLMrJiZy1i4D3blohn4AZLflLqZtMn3utbJmeTrCdq2Be1LyJ1HDvqTBKnUne8YrJfzzKrVENKXzL9JmMHgEgz3u3nbDwfuBMDiywLJBOrfl2fQwJDRmvrFzwDbwtGWvwThDxy6lxLHt1KOlvDODhbTAgjOzffOzdvTBOvOms9nvO9Ix1bxrNqUwgnfAfLWnwfLDLb0CLLxAvCVr2r4ExbHzhPTlKS2p3zcodu3yxm4Ea#CWS1mZi","Сон смешного человека ","Ф.М. Достоевский",4705,0,0,"",0,2,"","[]","6adb4186ee0c1d3ad0\/5102a312745ae505a7\/08eb0e4bd407e74e76\/d313ec4b6051942649\/","",[]],[456239118,%my_id%,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=AdbkuuLOywjuou0Zme1yrdzJsI9Tm2qYvIO2AJaWnMnIztKOzgLxmMfbu3rgyJ8Tme5MBNrKlMLpChPIBLLFngjvu3zZAxztuOXJztfgrK9vq1rznLCYDJznBMrMCMT4rO9PytD3oc5kAhyYm1jMmZeZlxqZsKfWyKO9DNGUmvfuBtfOudjXrvbmyZLVltvICvPIver5zg5Zm2jWmJvWsue5nuXWzgPnBZq6l2n3x30OCgCVC2SVmxjqzvD6Egrlnc1zyq#CWSYmdC","автостопом в москву","црвених цветова",359,0,0,"",0,18,"","[]","57c59cffa93d47effa\/836d457cff34e02fa0\/6374a8e457c763a8c6\/96db3ddc8c210b1fb0\/","",[]],[456239117,%my_id%,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=AgDYnY1TmwmOm2vTos5Ylu9Juwzkugv4zMzZyJbVlKjcqI9LB2vdzhe6DufVqY9KA1uZy3bfnhm4AgDOAwHIzMDztMTxsLjHn2K9veXuttvKq2fxogTWxY1fzJvumJrADLDlCdnLAv8UAfPiB2zxEKTrttLTrtK2ntrLCLnOww5rtI1Rq2vzsgr2vMnYp1P5BeOOx3rIEs8WBI10yLntCNzOCKrLBtznBMzRC29ewMOOC3uTueuYl29OztnWDhCXoffz#CWSYntC","Sal sér hon standa (Völuspá, 64-66)","Nytt Land",272,0,0,"",0,82,"","[]","4881448c55978a3374\/40a707901f551a4572\/778fe59467e6a629a9\/02a308905303098496\/","https:\/\/pp.userapi.com\/c837628\/v837628453\/829d0\/kLoB-0G_r78.jpg,https:\/\/pp.userapi.com\/c837628\/v837628453\/829cf\/C39pJ5b-t-w.jpg",80],[456239116,%my_id%,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=l1yUmI1MCc8Vsxq2qxHYoM1Ku2i9C1y\/EdzWzZfADhrpDgGYrwvJwwTRtZzHDwL3A2HZqKXXrOfHtZDVDKzZn1zIrKLJD3PVBKqOzxLylKDjvZuYqKf5qLG3rhn4nJnHs1rlt1PUy29mCvLuuxD6DJHJAtGXvNfjrMzkt29Wme93mKuWze55x1PotO01lMqZnuHfzJLgBM1Jluzqogvkr3KYs1fNmtn6qODYyx0XnxDZDNnvvO9Lsgn0tdrKDc9Kowm2#CWSOmJy","After Dark "," Mr.Kitty",351,0,0,"",0,18,"","[]","353d934506a14abed5\/5745fb63e4e5f4abb4\/2ed8c9b81df35317d7\/01404a5db986cf16b8\/","",[]],[456239115,%my_id%,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=mJfWyY1SBMm\/m1HYDc5YzgO5BwvdndC5nZDyvNzWyMvsEgfwyMvmsJzXm2fqEhq5mJvXvw50qIO4xZKTEefVsxq3yMfkltLpnJvWzfDrCZHgywiXEgzZqwntsJvMn21Nss9psKDkBKHKlY1LzI1vlJPyEuu3l3nyEhjsBNuWAhrlq3rezZfxmJn4A2zwtZbowhq3B1CWlZe4ytHZnhzhu19Lt29fvJDkCfLOzgvewI43rtjWmd1YBKLysOe2uMfKztbr#CWSZmJG","Storm","Godspeed You! Black Emperor",1352,0,0,"",0,34,"","[]","81b9d46c10d9df8d03\/5299d303c627944df7\/5ec696a0453d27253e\/ce7a1e0600cff40c53\/","",[]]]<!><!bool><!>7212f741260c90ab47Понимаем, что это json, распаковываем

Отлично, теперь у нас есть хоть какая-то ссылка, но выглядит она не очень рабочей, что делать дальше?
Расшифровываем ссылку
Шагаем по скрипту дальше и понимаем, что ничего не выходит, на каком-то этапе ссылка просто превращается из audio_api_unavailable в ссылку на mp3. Значит где-то что-то происходит!
Но где именно? В одной из функций setUrl() вызывается функция с говорящим именем audioUnmaskSource()
Код функции
function i() {
return window.wbopen && ~(window.open + "").indexOf("wbopen")
}
function o(t) {
if (!i() && ~t.indexOf("audio_api_unavailable")) {
var e = t.split("?extra=")[1].split("#")
, o = "" === e[1] ? "" : a(e[1]);
if (e = a(e[0]),
"string" != typeof o || !e)
return t;
o = o ? o.split(String.fromCharCode(9)) : [];
for (var s, r, n = o.length; n--; ) {
if (r = o[n].split(String.fromCharCode(11)),
s = r.splice(0, 1, e)[0],
!l[s])
return t;
e = l[s].apply(null, r)
}
if (e && "http" === e.substr(0, 4))
return e
}
return t
}
function a(t) {
if (!t || t.length % 4 == 1)
return !1;
for (var e, i, o = 0, a = 0, s = ""; i = t.charAt(a++); )
i = r.indexOf(i),
~i && (e = o % 4 ? 64 * e + i : i,
o++ % 4) && (s += String.fromCharCode(255 & e >> (-2 * o & 6)));
return s
}
function s(t, e) {
var i = t.length
, o = [];
if (i) {
var a = i;
for (e = Math.abs(e); a--; )
o[a] = (e += e * (a + i) / e) % i | 0
}
return o
}
Object.defineProperty(e, "__esModule", {
value: !0
}),
e.audioUnmaskSource = o;
var r = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/="
, l = {
v: function(t) {
return t.split("").reverse().join("")
},
r: function(t, e) {
t = t.split("");
for (var i, o = r + r, a = t.length; a--; )
i = o.indexOf(t[a]),
~i && (t[a] = o.substr(i - e, 1));
return t.join("")
},
s: function(t, e) {
var i = t.length;
if (i) {
var o = s(t, e)
, a = 0;
for (t = t.split(""); ++a < i; )
t[a] = t.splice(o[i - 1 - a], 1, t[a])[0];
t = t.join("")
}
return t
},
x: function(t, e) {
var i = [];
return e = e.charCodeAt(0),
each(t.split(""), function(t, o) {
i.push(String.fromCharCode(o.charCodeAt(0) ^ e))
}),
i.join("")
}
}
}Ставим брейкпоинт на o(t) и смотрим, что приходит и что уходит. t — наша ссылка вида audio_api..., в ?extra= содержатся два параметра. Насколько я понял, один содержит зашифрованную ссылку, а второй — это что-то вроде ключа. Можно зареверсить алгоритм, понять, как именно он все это шифрует, а можно просто вызвать o('https://...audio_api_...'). Так я и решил сделать, и получил на выходе прямую ссылку на mp3
Получаем зашифрованные ссылки
Расшифровывать ссылки научились, метод для получения зашифрованн��й ссылки знаем. Как же нам теперь получить айди, которые нужно передать методу, возвращающему зашифрованные ссылки? Идем смотреть сетевую активность. Как мы поняли ранее, взаимодействие с audio API происходит через al_audio.php. Ставим фильтр на данный запрос, загружаем страницу с аудиозаписями заново и видим уже новый запрос
Request URL:https://vk.com/al_audio.php
Request Method:POST
Status Code:200
Form Data:
access_hash:
act:load_section
al:1
claim:0
offset:30
owner_id:my_id
playlist_id:-1
type:playlistИ в ответ получаем большой json в том же виде, что и раньше, который содержит данные о плейлисте. Поле offset отвечает за смещение, начиная с которого, мы будем получать данные о плейлисте. Кроме того, ответ содержит полезные данные: поля hasMore и nextOffset.
Собираем все вместе
Мы знаем как получить данные о плейлисте, как получить зашифрованные ссылки и как их расшифровать. Осталось собрать все воедино. Вот пример того, что получилось у меня. Проверил работоспособность на node.js v8.1.3.
UPD:
Как заметил Veber, с Оперой вк работает как-то по-особому, поэтому куки лучше брать из хрома/огнелиса.