Как все начиналось...
Как всегда, зависая вконтакте, я решил скачать пару новых аудиозаписей на комп. Но меня ждало разочарование: аудиозаписи возвращались в каком-то странном формате: 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:
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
window.open(this.info.url.replace("/index.m3u8",".mp3").replace(//w{11}//,'/'));
}
}
var mus = new music_get();
//функция скролла
window.onscroll=()=>mus._update_scroll();
сжатый код:
eval(function(p,a,c,k,e,r){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--)r[e(c)]=k[c]||e(c);k=[function(e){return r[e]}];e=function(){return'\w+'};c=1};while(c--)if(k[c])p=p.replace(new RegExp('\b'+e(c)+'\b','g'),k[c]);return p}('8 o=7(){5.z=S.1i("1j");5.p=[];5.M=[];5.F=5.z.n;5.N(5.z);5.G(0,5.p[0])};o.q.H=7(a){a=T.U(1k.H(a.1l("V-1m")));j a.W+"X"+a.1n+"X"+a.1o};o.q.Y=7(a){8 b={v:7(a){j a.t("").1p().I("")},r:7(a,b){a=a.t("");C(8 h=1q 0,e=c+c,g=a.n;g--;)~(h=e.D(a[g]))&&(a[g]=e.Z(h-b,1));j a.I("")},s:7(a,b){8 c=a.n;u(c){8 e=b,g=a.n,d=[];u(g){8 h=g;C(e=12.1r(e);h--;)e=(g*(h+1)^e+h)%g,d[h]=e}e=0;C(a=a.t("");++e<c;)a[e]=a.O(d[c-1-e],1,a[e])[0];a=a.I("")}j a},i:7(a,c){j b.s(a,c^1s.13)},x:7(a,b){8 c=[];j b=b.14(0),J(a.t(""),7(a,d){c.P(K.L(d.14(0)^b))}),c.I("")}},c="1t+/=",f=7(a){u(!a||1==a.n%4)j!1;C(8 b,d,e=0,k=0,f="";d=a.1u(k++);)~(d=c.D(d))&&(b=e%4?1v*b+d:d,e++%4)&&(f+=K.L(1w&b>>(-2*e&6)));j f};u((!E.15||!~(E.Q+"").D("15"))&&~a.D("1x")){8 d=a.t("?1y=")[1].t("#"),k=""===d[1]?"":f(d[1]);u(d=f(d[0]),"1z"!=1A k||!d)j a;C(8 l,m=(k=k?k.t(K.L(9)):[]).n;m--;){u(l=(f=k[m].t(K.L(11))).O(0,1,d)[0],!b[l])j a;d=b[l].1B(1C,f)}u(d&&"1D"===d.Z(0,4))j d}j a};o.q.16=7(){J(5.M,7(a,b){8 c=S.1E('[V-1F-13="'+b.W+'"]')[0];3!==c.y[0].y[6].y.n&&(c.y[0].y[6].1G+="<17 1H='18 R().19(5);' 1I='1J:1K;1L:1a;1M:1a;1N:A(/1O) 1P-1Q 1b 1b;'></17>",c.y[0].y[6].y[2].1c.B=b)})};o.q.N=7(a){8 b=5;J(a,7(a,f){12.1R(a/10)===a/10?b.p.P(b.H(f)):b.p[b.p.n-1]+=","+b.H(f)})};o.q.G=7(a,b){8 c=5;1S.1T("/1U.1V",{1W:"1X",1Y:"1",1Z:b},{20:7(b){J(b,7(a,b){(b=T.U(b)).A=c.Y(b.A);c.M.P(b)});c.p.n-1===a?c.16():c.G(a+1,c.p[a+1])}})};o.q.1d=7(){u(5.z.n!==5.F){8 a=5.z.n,b=a-5.F;5.F=a;a=21.22(5.z).O(-b);5.1e(a)}};o.q.1e=7(a){8 b=5.p.n-1;5.N(a);5.G(b,5.p[b])};8 R=7(){};R.q.19=7(a){5.B=a.1c.B;-1!==5.B.A.D(".1f?")?E.Q(5.B.A):E.Q(5.B.A.1g("/23.24",".1f").1g(/\/\w{11}\//,"/"))};8 1h=18 o;E.25=7(){j 1h.1d()};',62,130,'|||||this||function|var|||||||||||return||||length|music_get|str_param|prototype|||split|if||||children|el|url|info|for|indexOf|window|last_len|load_audio_to_json|parse|join|each|String|fromCharCode|json|make_str|splice|push|open|music_download|document|AudioUtils|asObject|data|fullId|_|encode_url|substr|||Math|id|charCodeAt|wbopen|end|div|new|download|40px|5px|attributes|_update_scroll|_load_button|mp3|replace|mus|getElementsByClassName|audio_row|JSON|getAttribute|audio|actionHash|urlHash|reverse|void|abs|vk|abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN0PQRSTUVWXYZO123456789|charAt|64|255|audio_api_unavailable|extra|string|typeof|apply|null|http|querySelectorAll|full|innerHTML|onclick|style|float|right|height|width|background|doc472427950_504561254|no|repeat|floor|ajax|post|al_audio|php|act|reload_audio|al|ids|onDone|Array|from|index|m3u8|onscroll'.split('|'),0,{}))
Автор: SuperHackerVk