среда, 6 июня 2018 г.

И снова музыка VK

  1. Предварительные замечания
  2. Получение данных о треке
  3. Получение прямой ссылки
  4. Скрипт для Tampermonkey

Предварительные замечания



На тему скачивания музыки из социальной сети "В контакте" я уже писал ранее, но с тех пор кое-что изменилось, так что скрипты, актуальные для того времени - больше не работают. Мало того, "вконтактик" стал "шифроваться": если раньше страница содержала прямые ссылки на аудиофайлы, то теперь их там нет и процесс существенно усложнился.

Далее следует упомянуть, что для скачивания музыки из "вконтактика" есть расширения для браузеров, так что, если нужно просто что-то скачать, то лучше использовать именно их, а то, что я опишу здесь, будет интересно скорее, если надо сделать что-то нестандартное. Например, ранее я приводил скрипт для скачивания видео оттуда же, так там в браузере просто формировался bash-скрипт для скачивания, после чего его можно разместить на какой-то другой машине, которая будет все это дело качать. Для музыки это, возможно, и не так актуально, поскольку музыкальные файлы меньше по размеру и качаются быстро, но, тем не менее сценарии, в которых может потребоваться получение списка ссылок на аудиофайлы - вполне могут быть востребованными. Хотя, лично для меня весь этот процесс представляет сугубо "научный" интерес.


Получение данных о треке



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

Вот я взял для примера один трек, запустил его и среди прочих ответов нашел один следующего содержания
Код:
3555327223824<!><!>0<!>6864<!>0<!><!json>[[456239027,236521131,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=yvHQuL0Tn1DYsZiUu3boDgvJsuLLCgzHq2vUnMXkCvv1D105uOzyuOu5otvSvxjuvO9tBxqWmhDYqKq5AZDRlMm5DeTSChbnqs8WDujVCMvtrw12CdDwELzIncOXwu1zyMvZq3GWAwjgnODZmez6qZfdsermoc9sse8ZnfbFBvHAlJjdCdboogK5AsO9AdHqCMvsnwrZm24VC3q5zZHJBNy4AuHXEhaVsxq5zun3nJLSyZm4p286Eerpsu9TmfvdA1DrmLvOCW#CWS2mde","Toxicity","SOAD",221,0,0,"",0,66,"","[]","87ef4ee798155bb9c7\/5af6233c4ebfce2bc7\/dbcca94dfef4740bcb\/1b09969c356c6cd7b7\/","https:\/\/pp.userapi.com\/c637525\/v637525450\/43363\/fq34v93oQ-E.jpg,https:\/\/pp.userapi.com\/c637525\/v637525450\/43362\/Empa6PNfdd4.jpg",13],[456239026,236521131,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=Ady1DZHZyxa3mOTcAY5tlKTem3iYrtqZEdi3CtDLvvvFyNfHvLj2BtfSmgKWqNnWzxmOB1DHBI5KodmZELKWuxPdr2mVytrdqN0Wus9bBMH1ANfrs2DRohnZAZuWrxbHyvjwB3POwxu1AMTWCMLPxOPiy2e2uhHTnJiXqtPzBdqVuxbZtKTrmN09qwTOsJKWrK9kttHzDOjVBM1yDxvNvMOYA20YpZHYufu3A3GTmg9Vy3qTC3LcBwvJss9tDLzJvMmVBZu1x3rertnMsfH4vtzsls9zuffY#CWS5nJG","Bohemian Raphsody","Queen",367,0,0,"",0,2,"","[]","de85fcf68b66fdfc72\/20835a3ec770f404a1\/143a1ba9f6cf77e927\/98f642833f224eebc4\/","",[]],[456239021,236521131,"https:\/\/vk.com\/mp3\/audio_api_unavailable.mp3?extra=qL05v1btyxb4lZzWAwnMzfi5CvrLutrrmhrpz3qXnhjLt3zkrM54tNmOwMvslNPMufy9B1D4EwnoCOH1BxfiEdbixZi2CNrOs1bwAfLNqxr4BOjkzK84CvvHmdD1ttq2Cxe\/Bhe3Cfn0CY4TlZn2Es8WAtzJEwe6mfbqzeTjoejZuKrdovvcmLu5DdfkvLPyoc53m2HIlwXjCePZuuPTAM44z3bUAxHTyvDLmfrinKrVCMnjvxLSwezJrM5Yx2vXrOy1lZu1vq#CWS4mJC","Прелюдия op.23 N5 g-moll (C. Рихтер)","C. Рахманинов",224,0,0,"",0,66,"","[]","72dd9492b75b10c6d2\/cb7082fdf361019a23\/aedd4d728dcd893ab6\/ccb81055a794889803\/","https:\/\/pp.userapi.com\/c604720\/v604720447\/4d59e\/3FEenUEGjcc.jpg,https:\/\/pp.userapi.com\/c604720\/v604720447\/4d59d\/mK7PAW1l6_8.jpg",32]]<!><!json>{"236521131":"1"}<!>970abaeddddd19fa50
Здесь мы видим JSON-код, обрамленный некими дополнительными маркерами, представляющий массив массивов, где каждый внутренний массив содержит информацию об отдельном треке, в том числе адрес некоего файла с расширением mp3, хоть и с очень подозрительным именем файла audio_api_unavailable, напоминающем нам о недоступности аудио-апи. Но поскольку у адреса есть еще параметр, то остается надежда, что это то, что нам нужно. Но, к сожалению, эта ссылка ведет к файлу, который теперь уже голосом рассказывает нам о том, что мы пытаемся прослушать файл не тем приложением или что-то в этом роде.

Несмотря на то, что везде в этих примерах мы получаем один и тот же файл, все-таки параметр extra у каждого адреса свой, поэтому, как показали дальнейшие исследования, вожделенную прямую ссылку мы, в конце концов, будем получать именно из него. Стало быть для начала было бы совсем не лишним разобраться как получить от сервера такой же ответ.

Для получения такого ответа был отправлен POST-запрос на адрес https://vk.com/al_audio.php, со следующими параметрами
act: reload_audio
al: 1
ids: 236521131_456239027,236521131_456239026,236521131_456239025,236521131_456239024,236521131_456239021,236521131_456239027

Первые два параметра всегда одинаковые, что касается третьего, то нетрудно догадаться, что здесь записаны через запятую уникальные идентификаторы треков. Здесь указано пять идентификаторов, хотя мы получили информацию только о трех треках, но на странице, с которой я это взял в списке среди первых пяти треков третий и четвертый стали доступны только по подписке, поэтому информация о них и не пришла.

Кроме изложенного следует упомянуть о cookies. Если запрос отправляется из браузера с учетом контекста страницы, как это происходит, если выполнять все в скрипте GreaseMonkey или ему подобных, то можно об этом не думать. В то же время, если надо отправить запрос из стороннего приложения, или даже из браузера, но когда не пройдена авторизация в vk, то вразумительного ответа от сервера не будет. Пока мы на этом сосредотачиваться не будем, а займемся идентификаторами.

Открываем страничку с аудиозаписями берем любой трек и просматриваем блок с записью с помощью инструментов разработчика. Я смотрю в файрфоксе, там в контекстном меню есть пункт "Исследовать элемент". В одном из контейнеров, открытого таким образом элемента можно обнаружить атрибут data-full-id в котором как раз и содержится нужный нам id. Этой информации вполне достаточно для того, чтобы собрать ids со страницы.
Код javascript Выделить
function GetVisibleIds()
{
  var audiorows = document.querySelectorAll("div[data-full-id]");
  return Array.prototype.map.call(audiorows, function(e){return e.getAttribute("data-full-id")});
}
Для получения данных с сервера по идентификаторам треков напишем пару фунций: синхронную и асинхронную. На синхронные браузер не очень хорошо реагирует и пишет в консоли, что не надо так делать, хотя и выполняет все как надо. А если выполнять параллельно несколько таких запросов, то сервер может плохо отреагировать, хотя это только предположение. Поэтому пишу в двух вариантах, хотя делают эти функции одну и ту же работу, а что использовать - каждый решит сам.
Кликните здесь для просмотра всего текста
Код javascript Выделить
function GetTrackDataAsync(ids, callback)
{
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, true);
  xhr.onreadystatechange = function() 
  {
      if (xhr.readyState != 4) return;
 
      if (xhr.status != 200) 
      {
        console.log(xhr.status + ': ' + xhr.statusText);
      } 
      else 
      {
        //console.log("response");
        var re = /\<\!json\>([^<]+)\<!>/;
        var resp = re.exec( xhr.responseText)[1];
        callback(JSON.parse(resp));
      }
 
  }
  xhr.send();
  
}
 
function GetTrackData(ids)
{
  var xhr = new XMLHttpRequest();
  xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, false);
  xhr.send();
  var re = /\<\!json\>([^<]+)\<!>/;
  var resp = re.exec( xhr.responseText)[1];
  return JSON.parse(resp);
}
Далее следует сказать, что при отправке более пяти идентификаторов одновременно адекватного ответа ждать не стоит. Кроме того надо помнить, что некоторые треки доступны только по подписке или недоступны по другим причинам и информация о них не приходит. Так что надо доступные id разделить на группы по 5 элементов и смотреть, что возвратил сервер, благо ответ содержит информацию о треке, которая не исчерпывается ссылкой на файл. Идентификатор полученного трека можно получить объединив первые два элемента массива знаком подчеркивания, третий элемент - url файла с которым будем дальше работать для получения прямой ссылки, далее идут: название композиции, автор, продолжительность в секундах. В остальном не разбирался, поскольку решил, что это вряд ли понадобится.

Получение прямой ссылки



Теперь рассмотрим как из полученного негодного адреса файла получить ссылку на файл, который мы ищем. Получить информацию о том, как на странице обрабатываются данные - задача непростая, поскольку придется перелопатить очень много всего. Я пошел по более простому пути и воспользовался результатом работы проделанной другими людьми. А именно: установил расширение для браузера Firefox, предназначенное скачивания аудиофайлов оттуда же, откуда качаем и мы и поискал решение там. Ссылка на это расширение здесь.VK Universal Downloader.

Исследование кода этого расширения привело к тому, что нам понадобится следующий фрагмент.
Кликните здесь для просмотра всего текста
Код javascript Выделить
function fix_direct_url(t) {
    if (~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 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
}
 
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("")
    }
}
 
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
}
Единственная неприятность, что там используется некая функция each, которая нигде не определена, и по всей видимости импортируется в этот код, но откуда - я не разбирался. В то же время, несложно догадаться, что именно делает эта функция, поэтому я ее добавил и реализовал следующим образом.
Код javascript Выделить
        function each(arr, f)
        {
            for (var i = 0; i < arr.length; i++)
            {
                f(arr, arr[i]);
            }
        }


Скрипт для Tampermonkey


Теперь можно все вышесказанное объединить во что-то более-менее работающее и способное проиллюстрировать все вышесказанное. Скрипт запускался в браузере Google Chrome с расширением Tampermonkey. В Firefox, к сожалению на vk.com это расширение по непонятной причине не хочет реагировать на GM_registerMenuCommand и не создает пункт меню. Я написал об этом в комментариях на странице расширения, разработчик в ответ предложил скачать бета-версию "from here", но где это "here" указать, видимо, забыл. Кроме того попросил, напсать работет или нет, но куда писать - тоже неизвестно. )) Поэтому пока тестировалось только на хроме.
Кликните здесь для просмотра всего текста
Код javascript Выделить
// ==UserScript==
// @name        DownloadVkMusic
// @namespace   https://vk.com/
// @match       https://vk.com/*
// @version     1
// @description  Получает данные о музыкальных треках на vk.com
// @author       diadiavova
// @grant       GM_registerMenuCommand
// ==/UserScript==
 
(function(){
    GM_registerMenuCommand("Показать данные о треках", function(){
        TextInNewPage(JSON.stringify(GetDataFromPage().map(GetObjFromArray)));
    });
    function GetVisibleIds()
    {
        var audiorows = document.querySelectorAll("div[data-full-id]");
        return Array.prototype.map.call(audiorows, function(e){return e.getAttribute("data-full-id");});
    }
 
    function GetTrackDataAsync(ids, callback)
    {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, true);
        xhr.onreadystatechange = function()
        {
            if (xhr.readyState != 4) return;
 
            if (xhr.status != 200)
            {
                console.log(xhr.status + ': ' + xhr.statusText);
            }
            else
            {
                var re = /<\!json\>([^<]+)<!>/;
                var resp = re.exec( xhr.responseText)[1];
                callback(JSON.parse(resp));
            }
        };
        xhr.send();
 
    }
 
    function GetTrackData(ids)
    {
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "https://vk.com/al_audio.php?act=reload_audio&al=1&ids=" + ids, false);
        xhr.send();
        var re = /<\!json\>([^<]+)<!>/;
        var resp = re.exec( xhr.responseText)[1];
        return JSON.parse(resp);
    }
 
 
    function GetIdRanges()
    {
        var idarray = GetVisibleIds();
        var result = [];
        for(var i = 0; i < idarray.length; i += 5)
        {
            result.push(idarray.slice(i, i + 5).join(","));
        }
        return result;
    }
 
    function GetDataFromPage()
    {
        var result = [];
        for(var ids of GetIdRanges())
        {
            result = result.concat(GetTrackData(ids));
        }
        return result;
    }
 
    function GetObjFromArray(a)
    {
        return {author: a[4], track: a[3], url: fix_direct_url(a[2]), duration: a[5]};
    }
 
    function TextInNewPage(txt)
    {
 
        var newwin = unsafeWindow.open("about:blank");
        setTimeout(function(){newwin.document.body.appendChild(document.createTextNode(txt));}, 1000);
 
    }
 
 
 
 
    // Честно стыренный код расшифровки имен файлов
 
    function fix_direct_url(t) {
        if (~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 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;
    }
 
    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("");
        }
    };
 
    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;
    }
})();


Данный скрипт создает пункт меню "Показать данные о треках" в меню расширения на странице вконтктика. Если на странице отображены треки, то в результате работы скрипта откроется новая вкладка и через секунду в ней отобразится JSON с данными о треках, включая прямые ссылки. Для отображения данных пришлось использовать setTimeout при открытии нового окна, поскольку обработка DOMContentLoaded в хроме почему-то не работает.

Полученный документ надо использовать сразу, поскольку прямые ссылки на файлы со временем устаревают.

Комментариев нет :

Отправить комментарий