Как стать автором
Обновить

Скачиваем аудио вконтакте через клиентский js или расширение файлов .m3u8

Время на прочтение 11 мин
Количество просмотров 21K

Как все начиналось...


Как всегда, зависая вконтакте, я решил скачать пару новых аудиозаписей на комп. Но меня ждало разочарование: аудиозаписи возвращались в каком-то странном формате: m3u8. Этот формат даже vlc media pleyer не воспроизводил, и я стал думать, что делать…

Что собственно то делать?


Погуглив, что это собственно за формат такой .m3u8, я понял, что это аудио в формате .m3u. Отлично, скачиваем этот файл .m3u8, открываем с помощью текстового редактора и видим примерно вот такой текст:

Текст
#EXTM3U
#EXT-X-TARGETDURATION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:1.000,
6cfGpgIDcrZjA.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:2.000,
c2d2tpKzIsYzM.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:3.000,
a3fWlvLDwmZD4.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:3.000,
edeWZhKTUnazE.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:3.000,
9df2NqJzcmZj0.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXT-X-ENDLIST


Дальше понимаем, что

9df2NqJzcmZj0.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh

— путь к аудиозаписи. То есть надо подставить к каждому пути хост и, скорее всего, это аудио станет проигрываемым. Быстренько набросав на питоне пару строк кода реализуем это:

Код
import re
text='''#EXTM3U
#EXT-X-TARGETDURATION:3
#EXT-X-ALLOW-CACHE:YES
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:1
#EXTINF:1.000,
6cfGpgIDcrZjA.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:2.000,
c2d2tpKzIsYzM.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:3.000,
a3fWlvLDwmZD4.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:3.000,
edeWZhKTUnazE.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXTINF:3.000,
9df2NqJzcmZj0.ts?extra=0F4d1n-wWV6igsS5Ji7x6gYIbtU_aRzsiByqvrumv4W1iznLLoiC552LnsmyKeuuOtw70WTqfYdDCir-nmlL3VlLR9i2Y6IPOudQxWPbZjlslXE7prmIvdLyoLxb3A9NFnHo2KR5NStPg1sk6ZVXrYBh
#EXT-X-ENDLIST'''
host='https://cs9-5v4.vkuseraudio.net/p16/d5fce44eae6dbc/'
al = re.findall('\n.+?\.ts\?extra\=.+?\n',text)
for r in al:
    text=text.replace(r,'\n'+host+r.strip('\n')+'\n')

print(text)
input()


и получаем уже играбельный .m3u файл.

НО! Проблема в том, что:

  • Этот файл можно проигрывать только пока ссылки на промежутки аудио действительны
  • Конвертировать файл так же можно только пока ссылки действительны
  • Конвертировать файл можно только на том компьютере, на котором он был получен т.к. ссылки привязана к ip адресу пк.

Поняв все это я решил написать программу на клиентском js, чтобы ее можно было исполнять в командной строке. Вот что у меня получилось:

Программа
class music_get{
    constructor(){
        this.el=document.getElementsByClassName("audio_row");
        this.str_param=[];
        this.json=[];
        this.last_len=this.el.length;
        this.make_str(this.el);    
        this.load_audio_to_json(0,this.str_param[0]);
    }
    parse(element_){
        //функция, возвращающая id аудио, который надо передать в запросе, чтобы получить ссылку.
        //let i=JSON.parse(element_.attributes['data-audio'].nodeValue),s1=i[13].split("/");
        //return i[1]+"_"+i[0]+"_"+s1[2]+"_"+s1[s1.length-2];
        let i = AudioUtils.asObject(JSON.parse(element_.getAttribute('data-audio')));
        return i.fullId+"_"+i.actionHash+"_"+i.urlHash;
    }
    encode_url(t) {
        //функция, декодирующая ссылку на аудиозапись
        let c = {v:(t)=> { return t.split('').reverse().join('')},r: (t, e) => {t = t.split('');for (let i, o = _ + _, a = t.length; a--; ) ~(i = o.indexOf(t[a])) && (t[a] = o.substr(i - e, 1));return t.join('')},
             s: (t,e)=> { let i = t.length;if (i) { let o = function(t, e) {let i = t.length,o = [];if (i) {let a = i;for (e = Math.abs(e); a--; ) e = (i * (a + 1) ^ e + a) % i,o[a] = e }return o}(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},
             i:(t, e)=> {return c.s(t, e ^ vk.id)},x: (t, e)=> {let i = [];return e = e.charCodeAt(0),each(t.split(''), (t, o) => {i.push(String.fromCharCode(o.charCodeAt(0) ^ e))}),i.join('')}
        },_ = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=',h=(t)=>{ if (!t || t.length % 4 == 1) return !1;for (var e, i, o = 0, a = 0, s = ''; i = t.charAt(a++); ) ~(i = _.indexOf(i)) && (e = o % 4 ? 64 * e + i : i, o++ % 4) && (s += String.fromCharCode(255 & e >> ( - 2 * o & 6)));return s};
        if ((!window.wbopen || !~(window.open + '').indexOf('wbopen')) && ~t.indexOf('audio_api_unavailable')) { 
        let e = t.split('?extra=')[1].split('#'),i=''===e[1]?'':h(e[1]);
        if (e = h(e[0]), 'string' != typeof i || !e) return t;for (var o, a, s = (i = i ? i.split(String.fromCharCode(9))  : []).length; s--; ) {if (o = (a = i[s].split(String.fromCharCode(11))).splice(0, 1, e) [0], !c[o]) return t; e = c[o].apply(null, a)}if (e && 'http' === e.substr(0, 4)) return e}return t
    }
    end(){
        //для каждой аудиозаписи в html код добавляем кнопку
        each(this.json,(_,item)=>{
            let els = document.querySelectorAll('[data-full-id="'+item.fullId+'"]')[0];
            if(els.children[0].children[6].children.length===3)return;
            els.children[0].children[6].innerHTML+="<div onclick='new music_download().download(this);' style='float:right;height:40px;width:40px;background:url(/doc472427950_504561254) no-repeat 5px 5px;'></div>"            
            els.children[0].children[6].children[2].attributes.info=item;
            
        });
    }
    make_str(mass){
        //функция, добавляющая в массив str_param строки с id аудио, которые будут передаваться в запросе.
        each(mass,(i,e)=>{
            if(Math.floor(i/10)===i/10)
                this.str_param.push(this.parse(e));
            else
                this.str_param[this.str_param.length-1]+=","+this.parse(e);
        });
    }
    load_audio_to_json(i,l){
        //посылаем запрос на сервер вк, в котором в ответ приходит массив с аудио,
        //каждый элемент которого мы добавляем в массив this.json
        ajax.post("/al_audio.php",{act:'reload_audio',al:'1',ids:l},{onDone:(a)=>{
            //each - функция, которая есть на сайте vk.com - похожа на array.forEach
            each(a,(_,c)=>{
                c=AudioUtils.asObject(c);
                //ну естественно декодируем ссылку, как же без этого)
                c.url = this.encode_url(c.url);
                this.json.push(c);
            });
            //рекурсия
            if(this.str_param.length-1===i) this.end();
            else this.load_audio_to_json(i+1,this.str_param[i+1]);

        }});
    }
    _update_scroll(){    
        //функция, вызывающаяся при скролле страницы.
        if(this.el.length===this.last_len)return;
        
        let c = this.el.length,offset=c-this.last_len;
        this.last_len=c;
        let arr = Array.from(this.el).splice(-offset);
        this._load_button(arr);
        
    }
    _load_button(list){
        //функция, которая подгружает новые кнопки.
        let leng=this.str_param.length-1;
        this.make_str(list);
        this.load_audio_to_json(leng,this.str_param[leng]);
    }
} 
class music_download{
    //constructor(){}
    download(e){    
        this.info = e.attributes.info;
        //если формат аудио - .mp3, то просто открываем ссылку в новом окне
        if(this.info.url.indexOf(".mp3?")!==-1)
            window.open(this.info.url);
        else 
            //с недавнего времени вк стало поддерживать формат .m3u8, который является аудиоплейлистом(текстом), 
            //в котором содержатся ссылки на промежутки аудио .ts, но ссылки без хоста. 
            //Исправим это следуюшей функцией response:
            fetch(this.info.url).then((e)=>e.text().then((e)=>this.response(e)));
    }
    response(data){
        let alls = data.match(/\n.+?\.ts\?/ig), host=this.info.url.split("index.m3u8")[0];
        each(alls,(_,e)=>data=data.replace(e,"\n"+host+e.replace('\n','')));
        //скачиваем полученный файл
        this.download_data(this.info.title.replace(/[-\/\\:*?"<>|]/gim,'')+".m3u8",data);
    
    }
    download_data(f_n, t) {
        let e = document.createElement('a');
        e.setAttribute('href', //'data:text/plain;charset=utf-8,'
                'data:text/html;base64,'+ btoa(t));
        e.setAttribute('download', f_n);
        e.style.display = 'none';
        document.body.appendChild(e);
        e.click();
        document.body.removeChild(e);
    }

}
var mus = new music_get();
//функция скролла
window.onscroll=()=>mus._update_scroll();

В результате получается примерно вот, что:


Дальше, скачав все аудио в одну папку, я написал следующий код на питоне, чтобы конвертировать все аудио .m3u8 в .ts:

Код
import requests,re,os
def convert_mp3(f):
    z=open(f).read()
    con=list(map(lambda e: e.rstrip('\n').rstrip('#EXT-X-ENDLIST').rstrip("\n") if '#EXTM3U' not in e else '' ,re.split('#EXTINF:\d+.\d+,\n',z)))
    z = b''
    for r in con:
        if(r==''):continue;
        z+=requests.get(r).content
    open(f.strip(".m3u8")+".ts",'bw').write(z)
z=set()
for file in os.listdir():
    if file.endswith(".m3u8"):
        z.add(file)
        convert_mp3(file)
z=',\n'.join(z)
input(f"Файлы:{z} переконвертированны.\nНажмите Enter, чтобы выйти!")


Впринципе, можно попытаться соединить отрывки .ts на js и потом скачать весь файл, но у меня не получилось (

P.S. у кого получится — пишите в комментарии
P.P.S. Забыл сказать, что яндекс.браузер до сих пор возвращает ссылки на .mp3)

Update:


Как правильно заметил nokimaro можно скачивать сразу mp3:
код
let el,dataItems,
    id=window['vk']['id'],
    e=window['each'], asObj=window['AudioUtils']['asObject'], ajax=window['ajax'];
function update(){
    el=document.querySelectorAll(".audio_row");
    dataItems=[].map.call(el,(element_)=>{
        //функция, возвращающая id аудио, который надо передать в запросе, чтобы получить ссылку.

        let i = asObj(JSON.parse(element_.dataset['audio']));
        return `${i['fullId']}_${i['actionHash']}_${i['urlHash']}`;
    });
    e(el,(index, el)=>{
        el = el.querySelector(".audio_row__info");
        let appendEl = document.createElement('a');
        appendEl.onclick=onElClick.bind(appendEl,dataItems[index]);
        appendEl.target='_blank';
        appendEl.setAttribute('style','float: left;height: 30px;width: 30px;background: url(/doc472427950_504561254) no-repeat;background-size: 100%;z-index: 10000;position: absolute;transform: translateX(-31px);');
        el.appendChild(appendEl)
    });
}
update();

function encode_url(t) {
        //функция, декодирующая ссылку на аудиозапись
        let c = {
            'v': t=> t.split('').reverse().join(''),
            'r': (t, e) => {
                t = t.split('');
                for (let i, o = _ + _, a = t.length; a--; )
                    ~(i = o.indexOf(t[a])) && (t[a] = o.substr(i - e, 1));
                return t.join('')
            },
            's': (t,e)=> {
                let i = t.length;
                if (i) {
                    let o = ((t, e)=> {
                        let i = t.length,o = [];
                        if (i) {let a = i;
                        for (e = Math.abs(e); a--; )
                            e = (i * (a + 1) ^ e + a) % i,o[a] = e
                        }
                        return o
                    })(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
            },
            'i': (t, e) => c['s'](t, e ^ id),
            'x': (t, e,i=[])=> {
                return e = e.charCodeAt(0),
                    e(t.split(''), (t, o) =>
                    {i.push(String.fromCharCode(o.charCodeAt(0) ^ e))}),
                    i.join('')
            }
        },_ = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=',
            h=t=>{
            if (!t || t.length % 4 == 1)
                return !1;
            for (var e, i, o = 0, a = 0, s = ''; i = t.charAt(a++); )
                ~(i = _.indexOf(i)) &&
                (e = o % 4 ? 64 * e + i : i, o++ % 4) &&
                (s += String.fromCharCode(255 & e >> ( - 2 * o & 6)));
            return s
        };
        if ((!window['wbopen'] || !~(window.open + '').indexOf('wbopen')) && ~t.indexOf('audio_api_unavailable')) {
            let e = t.split('?extra=')[1].split('#'),i=''===e[1]?'':h(e[1]);
            if (e = h(e[0]), 'string' != typeof i || !e)
                return t;
            for (var o, a, s = (i = i ? i.split(String.fromCharCode(9))  : []).length; s--; ) {
                if (o = (a = i[s].split(String.fromCharCode(11))).splice(0, 1, e) [0], !c[o])
                    return t; e = c[o].apply(null, a)}if (e && 'http' === e.substr(0, 4)) return e
        }
        return t
}
function onElClick(audio_id,event) {
    event.preventDefault();
    event.stopPropagation();
    get_data(audio_id).then(el=>{
        window.open(this.href=el['url'])
    });
}
function _g(url){
    if(url.indexOf(".mp3?")!==-1)
        return url;
    else
        return url.replace("/index.m3u8",".mp3").replace(/\/\w{11}\//,'/');
}
function get_data(audio_id){
    return new Promise(onSuccess => {
            //посылаем запрос на сервер вк, в котором в ответ приходит массив с аудио,
            //каждый элемент которого мы добавляем в массив j
            let index = dataItems.findIndex(e=>audio_id===e||audio_id.startsWith(e['fullId']));
            if(typeof dataItems[index]!="string") onSuccess(dataItems[index]);
            else {
                let datas=dataItems.slice(index).filter(el=>typeof el =="string").slice(0,10);
                ajax.post("/al_audio.php", {'act': 'reload_audio', 'al': '1', 'ids': datas + ""}, {
                    'onDone': a => {
                        //each - функция, которая есть на сайте vk.com - похожа на array.forEach
                        e(a, (i, c) => {
                            c = asObj(c);
                            //ну естественно декодируем ссылку, как же без этого)
                            c['url'] = _g(encode_url(c['url']));
                            dataItems[dataItems.indexOf(datas[i])] = c
                        });
                        onSuccess(dataItems[index])
                        //рекурсия
                    }
                })
            }
        }
    )
}
function _update_scroll(){
    //функция, вызывающаяся при скролле страницы.
    if(!el[el.length-1].nextElementSibling)return;
    update()
}
window.addEventListener("scroll",_update_scroll);

сжатый код:
~function(){'use strict';var k,p,q=vk.id,r=each,t=AudioUtils.asObject;function v(A){k=document.querySelectorAll(".audio_row");p=[].map.call(k,d=>`${(A=t(JSON.parse(d.dataset.audio))).fullId}_${A.actionHash}_${A.urlHash}`);r(k,(d,g,e)=>{g=g.querySelector(".audio_row__info");e=document.createElement("a");e.onclick=w.bind(e,p[d]);e.target="_blank";e.setAttribute("style","float: left;height: 30px;width: 30px;background: url(/doc472427950_504561254) no-repeat;background-size: 100%;z-index: 10000;position: absolute;transform: translateX(-31px);");g.appendChild(e)})}v();function x(d,g){g={v:a=>a.split("").reverse().join(""),r:(a,b)=>{a=a.split("");for(var f,c="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=",h=a.length;h--;)~(f=c.indexOf(a[h]))&&(a[h]=c.substr(f-b,1));return a.join("")},s:(a,b,m)=>{var f=a.length;if(f){var c=a.length,h=[];if(c){m=c;for(b=Math.abs(b);m--;)b=(c*(m+1)^b+m)%c,h[m]=b}b=h;c=0;for(a=a.split("");++c<f;)a[c]=a.splice(b[f-1-c],1,a[c])[0];a=a.join("")}return a},i:(a,b)=>g.s(a,b^q),x:(a,b,f=[])=>(b=b.charCodeAt(0),b(a.split(""),(c,h)=>{f.push(String.fromCharCode(h.charCodeAt(0)^b))}),f.join(""))};var e=a=>{if(!a||1==a.length%4)return!1;for(var b,f,c=0,h=0,m="";f=a.charAt(h++);)~(f="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789+/=".indexOf(f))&&(b=c%4?64*b+f:f,c++%4)&&(m+=String.fromCharCode(255&b>>(-2*c&6)));return m};if((!window.wbopen||!~(window.open+"").indexOf("wbopen"))&&~d.indexOf("audio_api_unavailable")){var a=d.split("?extra=")[1].split("#"),b=""===a[1]?"":e(a[1]);if(a=e(a[0]),"string"!=typeof b||!a)return d;for(var l,n=(b=b?b.split(String.fromCharCode(9)):[]).length;n--;){if(l=(e=b[n].split(String.fromCharCode(11))).splice(0,1,a)[0],!g[l])return d;a=g[l].apply(null,e)}if(a&&a.startsWith("http"))return a}return d}function w(d,g){g.preventDefault();g.stopPropagation();new Promise((g,e)=>{e=p.findIndex(l=>d===l||d.startsWith(l.fullId));if("string"!=typeof p[e])g(p[e]);else{var l=p.slice(e).filter(n=>"string"==typeof n).slice(0,10);ajax.post("/al_audio.php",{act:"reload_audio",al:1,ids:l+""},{onDone:n=>{r(n,(a,b)=>{var f=b=t(b),c=x(b.url);c=-1!==c.indexOf(".mp3?")?c:c.replace("/index.m3u8",".mp3").replace(/\/\w{11}\//,"/");f.url=c;p[p.indexOf(l[a])]=b});g(p[e])}})}}).then(e=>{window.open(this.href=e.url)})}window.addEventListener("scroll",a=>k[k.length-1].nextElementSibling&&v());}()

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Как вы скачиваете музыку с вконтакте?
70.65% через дополнение для браузера 65
17.39% через специальные сайты 16
7.61% через js 7
11.96% через чат-бота VkMusicBot 11
Проголосовали 92 пользователя. Воздержались 78 пользователей.
Теги:
Хабы:
+8
Комментарии 20
Комментарии Комментарии 20

Публикации

Истории

Работа

Data Scientist
66 вакансий
React разработчик
65 вакансий
Python разработчик
136 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн