📜 ⬆️ ⬇️

Dragging music from VK without public music API

How it all began


It was evening, there was nothing to do ... More precisely, I just wanted to download an audiobook in front of couples and a surprise was waiting for me. Cache in kate mobile disabled. How so? What to do? Of course, write your application with cache and audio recordings. But first you need to understand how VC turns links like audio% user_id% _% track_id% into direct links to mp3. I did not write what came out of this application and how to download a specific playlist can be read under the cut.

Debasim js


We start with the obvious - open the audio recordings tab and look at the code. We see onclick on the button for each audio recording:

onclick="return getAudioPlayer().toggleAudio(this, event)" 

Open audioplayer.js, turn on Pretty print and look for the toggleAudio () function.

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) } 


We set a breakpoint on the first instruction and press the button. Enter debug mode and begin to execute the code step by step.
')
We see that the variable o in a few steps contains a bunch of useful information: artist, name, hashes, all sorts of IDs. But does not contain the main property - url.
We go further, we leave from toggleAudio (), we get to the common.js file in the function working with events. Yeah, then we get the link asynchronously, we should look at the network activity, well, we go further. After n steps, we notice that an interesting query appears in the network tab:

Request URL:https://vk.com/al_audio.php
Request Method:POST

Form Data:
act:reload_audio
al:1
ids: id %user_id%_%track_id%,


And no less interesting answer:

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-tw.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

We understand that it is json, we unpack

JSON of one of the elements

Ok, now we have at least some link, but it looks not very working, what to do next?

Decipher link


We step on the script further and we understand that nothing comes out, at some stage the link simply turns from audio_api_unavailable into a link to mp3. It means that something is happening somewhere!
But where exactly? In one of the setUrl () functions, a function is called with the speaker name audioUnmaskSource ()

Function code
  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("") } } } 


We set a breakpoint on o (t) and see what comes and what goes. t is our link of the form audio_api ...,? extra = contains two parameters. As I understand it, one contains an encrypted link, and the second is something like a key. You can override the algorithm, figure out exactly how it encrypts all this, or you can simply call o ('https: // ... audio_api _...'). So I decided to do it, and at the output I got a direct link to mp3

We get encrypted links


We learned to decipher the links, we know the method for obtaining the encrypted link. How do we now get the IDs that need to be passed to the method that returns the encrypted links? We go to watch network activity. As we understood earlier, interaction with the audio API takes place via al_audio.php. Put a filter on this request, load the page with audio recordings again and see a new request

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


And in response we get a big json in the same form as before, which contains data about the playlist. The offset field is responsible for the offset from which we will receive data about the playlist. In addition, the response contains useful data: the hasMore and nextOffset fields.

Putting it all together


We know how to get data about the playlist, how to get encrypted links and how to decrypt them. It remains to collect all together. Here is an example of what happened with me. Checked performance on node.js v8.1.3.

UPD:


As Veber noted, with Opera VC it ​​works somehow in a special way, therefore it is better to take cookies from chrome / firestone.

UPD 2: VC has updated the way of the masking of the link, now there is a code that rushes from the ID, more details in the committer of 12.17.17

Source: https://habr.com/ru/post/340810/


All Articles