vk_api
vk_api copied to clipboard
Проблема получения аудиозаписей (m3u8)
Я столкнулся с проблемой получения аудиозаписей. ВК явно что-то поменял. Python 3.7
Функция decode_audio_url стала возвращать ссылки вида:
https://cs7-4v4.vkuseraudio.net/p8/cd058a65959/c6ad6ed7935e91/index.m3u8?extra=CfH-LPRW3C8vvJEmWLtzlL_-WaCw9VB5kTeKLm54_4kOhnGRo8gkLLdo7OG6nruYv-HstmX2-4Mant54JzDivp-Ebm1x_RuIhLUnHwlfFRubKYpOqxQbcmzbl1MYneotQVEG7AOFzRh5a3HvPJI7PL4eMg
Скачав файл плейлиста (файл index.m3u8), он имеет следующее содержимое: ссылка
На данный момент, я знаю, что делать с этим файлом дальше.
Ну, возможно стоит конвертировать в другой формат :thinking:
Подвох в том, что теперь аудиозапись разделена на фрагменты, и каждый из них зашифрован с помощью алгоритма AES-128.
Предлагаю https://gist.github.com/grwlf/e1876f5d78cb6e66791809771d7bf36b . Пожалуйста, сообщите, если есть нормальная программа, которая понимает такие плейлисты.
@grwlf Я также переписал ваш скрипт на Python для возможности его использования в Windows: https://gist.github.com/qwertyadrian/a57edb869d3e3c5e01abf04a744a62f0
Пожалуйста, сообщите, если есть нормальная программа, которая понимает такие плейлисты.
ffplay может воспроизводить такие плейлисты, но тут уже вопрос удобства
ffplay может воспроизводить такие плейлисты, но тут уже вопрос удобства
ОК. Пока не проверял, но есть сомнения: если ffplay - из пакета ffmpeg, то наверное там поддержка такая же как и в ffmpeg, а там она недостаточна (поэтому и скрипт понадобился). Например, ffmpeg похоже непонимает, что в плейлисте урлы сегментов указаны относительно урла плейлиста
ffmpeg похоже непонимает, что в плейлисте урлы сегментов указаны относительно урла плейлиста
Да, ffplay может воспроизводить плейлисты только после обработки вашим скриптом, то есть файл index_local.m3u8
upd: Я сейчас проверил и оказалось, что ffplay спокойно воспроизводит плейлист, если ему передать ссылку на него. Правда есть небольшие заикания.
Оказалось все еще проще. ffmpeg скачивает плейлист, если использовать следующую команду: ffmpeg -i <ссылка на плейлист index.m3u8> -c copy out.ts
Удивительно, я точно так пробовал, не работало. Может плохо пробовал) Ах, да, понял. Именно ссылку нужно было давать. Я давал скачанный файл
Ещё такое соображение: меня ВК вчера забанил (не знаю, на долго ли), когда я попытался скачать много песен последовательно. Наверное должна помочь задержка, имитируюшая проигрывание в плеере. Такого ffmpeg наверное не сумеет прямо сходу.
@grwlf можно так: крон-такси на определенное кол-во запусков, с каким-то промежутком @qwertyadrian указывайте, пожалуйста, версию python.
меня ВК вчера забанил
можно так: крон-такси на определенное кол-во запусков, с каким-то промежутком
Оказалось, забанил ненадолго. Сейчас ставлю задержку размером в длину аудиозаписи после каждого скачивания. Пока работает без сбоев. Видимо, банхаммер не слишком избирательный.
Оказалось все еще проще. ffmpeg скачивает плейлист, если использовать следующую команду:
ffmpeg -i <ссылка на плейлист index.m3u8> -c copy out.ts
Если ffmpeg скормить ссылку на m3u8, то он собирает отрывки в один файл, но есть заикания. Причина заиканий мне не ясна. Это очень странно потому что результирующая продолжительность файла оказывается правильной.
Я скачивал части из плейлиста вручную и некоторые .ts проигрываются без расшифровки (те что METHOD=NONE), а те части, что METHOD=AES-128 не проигрываются без расшифровки.
Не знаю, может заикания из-за того, что отрывки в хаотичном порядке склеиваются?
@grwlf и у вас ffmpeg бесшовно склеивает?
Я научился расшифровывать зашифрованные .ts части. Файлы проигрываются MPC-HC проигрывателем. Расшифровка с помощью такого кода:
from Crypto.Cipher import AES from Crypto.Util.Padding import unpad def main(): input_file = 'enc2.ts' # Input file key = b'5bfa50cb41ebfee1' # Ключ взятый из скачанного текстового файла key.pub
# Read the data from the file
file_in = open(input_file, 'rb') # Open the file to read bytes
iv = file_in.read(16) # Read the iv out - this is 16 bytes long
ciphered_data = file_in.read() # Read the rest of the data
file_in.close()
cipher = AES.new(key, AES.MODE_CBC, iv=iv) # Setup cipher
original_data = unpad(cipher.decrypt(ciphered_data), AES.block_size) # Decrypt and then up-pad the result
with open(input_file + ".dec", 'wb') as out:
out.write(original_data)
print(original_data)
if name == 'main': main()
- для работы нужна библиотека pip install pycryptodome
- при этом нужно удалить pip uninstall pycrypto потому что они мешают друг другу
- pycrypto мёртв, + он не имел бинарных библиотек, их нужно было компилировать самому - гемморой
Теперь у меня такой ход мыслей - зашифрованные части расшифровать, приложить к ним не зашифрованные, создать текстовый список файлов и подать этот список на вход ffmpeg, чтобы он склеил все части файла в один.
ffmpeg нужен, т.к. каждая часть содержит свой заголовок ID3, поэтому просто бинарно склеивать файлы нельзя.
В общем переписал парсер и расшировывальщик заширофанных частей https://pastebin.com/jaFpvxM3 , но заикания после склеивания никуда не ушли(((
Я попытался перекодировать файл, а не просто склеить из частей, при перекодировании ffmpeg стал выдавать ошибки timestamp. Похоже, заикания из-за этого. Нужно разбираться откуда берется DTS и почему между кусками DTS перепрыгивает на несколько фреймов вперед-назад
Вот так выглядит список файла очереди для ffmpeg:
Я оставил только 4 файла (4*3 = 12 секунд аудио)
и склеил. в 12 секундном файле слышно только один артефакт. между 2 и 3 отрезком. И в окне FFMPEG видно только одну ошибку синхронизации:
это значит, что какие-то файлы склеиваются бесшовно, но какие-то имеют проблемы склеивания.
Сейчас я заметил, что если в VLC открыть ссылку на аудиофайл, то он воспроизводится (а также и сохраняется) без заиканий.
Сейчас я заметил, что если в VLC открыть ссылку на аудиофайл, то он воспроизводится (а также и сохраняется) без заиканий.
Сейчас я заметил, что если в VLC открыть ссылку на аудиофайл, то он воспроизводится (а также и сохраняется) без заиканий.
Наконец-то хоть кто-то ожил. И у меня победа)) У меня была ошибка, парсер пропускал файлы плейлиста в которых были спецсимволы. Теперь файл склеивается без нареканий и заиканий. Что с перекодированием, что без перекодирования -c copy
Я думаю, для vk_api лучше вариант не через VLC, а через python + ffmpeg все таки? Он более гибкий и на выходе можно сконвертить во что угодно.
Это первый раз, когда я могу сделать какой-то вклад в уже существующий и кем-то написанный проект!
вот рабочий файл, который распаковывает m3u8, скачивает и расшифровывает зашифрованные части и склеивает их потом https://pastebin.com/ACBA7kWW
Как писал выше, для работы расшифровщика нужна библиотека pip install pycryptodome при этом нужно удалить pip uninstall pycrypto потому что они мешают друг другу pycrypto можно удалять, это устаревший проект + он не имел бинарных библиотек, их нужно было компилировать самому - гемморой
Ещё нужно решить что теперь делать с полученным файлом, ведь это не mp3, в foobar он не воспроизводится. Это контейнер MPEG-TS, с mp3 потоком внутри. Как-то нужно научиться его доставать из MPEG-TS и без перекодирования запихивать в родной контейнер mp3. Простое переименование .ts в .mp3 ничего не делает
Так. проблема тоже решилась, нужно просто указывать выходной файл .mp3
ffmpeg -f concat -segment_time_metadata 1 -safe 0 -analyzeduration 100M -probesize 100M -i C:\Users\F2\AppData\Local\Temp\tmpom4h2pf7\queue.txt -c copy _out.mp3
тогда и поток будет без перекодирования в mp3 и проигрываться в плеерах
выходит, чтобы работало скачивание аудио в vk_api, туда нужно включить ffmpeg как это правильнее сделать?
в imageio включен пакет ffmpeg, может сделать зависимость vk_api от imageio, тогда ffmpeg будет...
вот рабочий файл, который распаковывает m3u8, скачивает и расшифровывает зашифрованные части и склеивает их потом
Я в ближайшее время подправлю его для работы в linux, так как по завершению работы программы временная папка удаляется вместе с полученным аудиофайлом. И еще у меня выполнение программы завершается с ошибкой, так как вы используется для путей обратный слеш (который используется только в Windows), для работы с путями лучше использовать модуть os.path. И по умолчанию возвращается список, который нельзя перенаправить в stdout.
Здесь уже довольно давно нашел решение: https://habr.com/ru/post/457438/#comment_20319286 Регулярки достаточно чтобы из m3u8-ссылки получить mp3 В бою откатано, отправил PR #315
Здесь уже довольно давно нашел решение: https://habr.com/ru/post/457438/#comment_20319286 Регулярки достаточно чтобы из m3u8-ссылки получить mp3 В бою откатано, отправил PR #315
у меня не сработало напрямую брать mp3
вот рабочий файл, который распаковывает m3u8, скачивает и расшифровывает зашифрованные части и склеивает их потом
Я в ближайшее время подправлю его для работы в linux, так как по завершению работы программы временная папка удаляется вместе с полученным аудиофайлом. И еще у меня выполнение программы завершается с ошибкой, так как вы используется для путей обратный слеш (который используется только в Windows), для работы с путями лучше использовать модуть os.path. И по умолчанию возвращается список, который нельзя перенаправить в stdout.
написал комменты к коду, вывод в stdout и пути для linux https://pastebin.com/gvU6PQwQ
у меня не сработало напрямую брать mp3
Получилось не сразу, иногда отдает немного другую ссылку Но работает у меня уже довольно давно и без сбоев
Получилось не сразу, иногда отдает немного другую ссылку Но работает у меня уже довольно давно и без сбоев
я не так менял, и правда работает. спасибо. оставлю и тот и этот варианты на будущее.
На данный момент решение, предложенное @bakatrouble работает и ссылки на .mp3 файлы успешно конвертируются простым regexp'ом.
Тем не менее, я бы хотел предложить решение проблемы скачивания аудиозаписей используя протокол HLS, если вдруг ВК полностью закроют доступ на .mp3
Скачивание сегментов один за другим по очереди (обычно 60-80 сегментов на трек) может быть очень долгим и даже близко не будет использовать возможности сети, поэтому в этом примере все сегменты скачиваются асинхронно с помощью asyncio.gather()
. По этой причине все предлагаемые ниже функции - асинхронный код.
import asyncio
import aiofiles
import aiohttp
import m3u8
import pathlib
import shutil
import uuid
try:
from cryptography.hazmat.primitives.ciphers.algorithms import AES
except:
pass
from .utils import getLogger, handler
logger = getLogger(name=__name__)
logger.add_handler(handler)
async def fetch_playlist(url: str, session: aiohttp.ClientSession) -> (bytes, str):
'''Fetches contents of .m3u8 HLS playlist by its url'''
async with session.get(url, allow_redirects=False) as response:
if response.status == 302:
# Redirect occurs when trying to fetch the playlist using other IP address than the one which was used to get the URLs
# We cannot allow the library to just follow redirects automatically,
# because we'll need the final URL to make up absolute URLs out from relative ones in the .m3u8 playlist
# This function returns the actual url after redirection if it occurs.
await logger.debug('Redirect-Location: {}'.format(response.headers['Location']))
# Recursion. Normally 2 redirects occur: 302 Moved Temporarily and 302 Found
return await fetch_playlist(response.headers['Location'], session)
elif response.status == 200:
data = await response.text()
return data, url
async def fetch_segment(i: int, s: m3u8.Segment, path: pathlib.Path) -> None:
'''Fetches the transport stream (.ts) segment into a file with *path*.
Decyphers if needed. Segment should already contain the actual key, not only its URL'''
async with aiohttp.ClientSession() as session:
async with session.get(s.absolute_uri) as response:
if response.status != 200:
raise Exception(response)
content = await response.read()
# Decypher content with the key if needed. Segment's number is used as the Initialization Vector (IV)
if s.key.uri:
try:
cipher = AES.new(
s.key._value, AES.MODE_CBC, iv=i.to_bytes(length=16, byteorder='big')
)
content = cipher.decrypt(content)
except ValueError as e:
raise Exception(f'{e}, content length: {len(content)}')
async with aiofiles.open(path/f'{i}.ts', 'wb') as f:
await f.write(content)
await logger.debug(f'Fetched {i}-th segment..')
async def fetch_track(url: str, path: pathlib.Path):
'''Fetches the whole track (audio file) into a directory with *path*.
Saves transport streams (.ts) files into the directory, as well as the myslist.txt file to be used inFFmpeg command'''
async with aiohttp.ClientSession() as session:
# The actual URL is returned alongside playlist so that absolute URLs could be made up
playlist, url = await fetch_playlist(url, session)
m = m3u8.loads(playlist, uri=url)
# Normally, only 1-2 unique public keys are present in .m3u8 playlist and 7-12 segments are encoded.
# We can reuse these unique key for all the encoded segments
for key in m.keys:
if key.uri:
async with session.get(key.uri) as response:
key._value = await response.read()
# mylist.txt file is written alongside the .ts files to be used as input in FFmpeg concat command
async with aiofiles.open(path/'tslist.txt', 'w') as f:
await f.write('\n'.join(f'file \'{i}.ts\'' for i, s in enumerate(m.segments)))
# Segments (.ts files) are fetched concurrently for performance
tasks = []
for i, s in enumerate(m.segments):
tasks.append(asyncio.ensure_future(fetch_segment(i, s, path)))
await asyncio.gather(*tasks)
async def download_hls(url: str, path: pathlib.Path) -> bytes:
# Prepare a directory for incoming .ts segments
ts_path = path / uuid.uuid4().hex
ts_path.mkdir(exist_ok=True)
try:
# Fetch all the .ts files and write "tslist.txt"
await hlscore.fetch_track(url, ts_path)
# .ts segments should be concatenated using FFmpeg demuxer
# Simple concatenation will lead to "tape jamming" between segments
# ffmpeg: main executable
# -hide_banner: does not show general FFmpeg's information on startup
# -loglevel panic no logging (only in fatal events)
# -y overwrite if exist (not used here because we pipe output)
# -f concat function to use - concat demuxer
# -i <path> input path (tslist.txt file lisitng .ts segments), e.g.
# '0.ts'
# '1.ts'
# ... etc
# Note: single quotes are important (double quotes will not work).
# Note: all the listed segments must be present
# -c copy Codec, simple copy
# -map 0:a:0 Extract only audio stream (a) , no video (0) and no text (0)
# -f mp3 Format mp3
# - output, hyphen means to pipe output to STDOUT, otherwise a filename
p = await asyncio.create_subprocess_shell(
f'ffmpeg -hide_banner -loglevel panic -y -f concat -i {ts_path / "tslist.txt"} -c copy -map 0:a:0 -f mp3 -',
stdout=asyncio.subprocess.PIPE
)
# Return bytes
return await p.stdout.read()
finally:
# Cleanup
shutil.rmtree(ts_path)
Как я понял, vk_api - "синхронная" библиотека, поэтому вызвать функцию нужно будет так:
import asyncio
body = asyncio.get_event_loop().run_until_complete(asyncio.ensure_future(download_hls(url, path)))
FFmpeg, хоть и понимает HLS плейлисты, скачивает сегменты один за одним, это просто очень долго.
Данный кусок кода требует присутствия одной внешней библиотеки для парсинга m3u8 файла - https://pypi.org/project/m3u8/. В принципе, пропарсить файл можно и "вручную" итерацией по линиям, если сторонние библиотеки не приветствуются в проекте. Как бы там ни было, от FFmpeg все равно не избавиться, так что "pure python" тут точно не получится.
Замечены новые m3u8-ссылки вида https://psv4.vkuseraudio.net/audio/ee/Ip2PDLQ0hcCVf66TIwU_96K18de69W5zQ0r0hg/5fNzQ4MDsxMjE8/fafls0f242Pz5rc10/index.m3u8?extra=V68TVWsJa742IUTDElHRDutcQq0TIDk36a5PJQpufpinJu9K1LwHeNIRNnOoRgwWNmLbU0mq8KgoiE_6PL0UsboDARvakwEgxJo5dkAutSYg40Ny15N-FPt_9bYEz4l2G6GM5xClzvSp9atAN_Fu_JbxuQ С которыми пока непонятно что делать
Получилось не сразу, иногда отдает немного другую ссылку Но работает у меня уже довольно давно и без сбоев
я не так менял, и правда работает. спасибо. оставлю и тот и этот варианты на будущее.
iv находится внутри зашифрованного файла?
php
$file = '1aY29mcXp0eSx0b24sPCZuOA.ts'; # имя зашифрованного файла $data = file_get_contents($file, false, null, 16); # считываем файл с 17 символа $iv = file_get_contents($file, false, null, 0, 16); # считываем первые 16 символов $method = "AES128"; $key = "c71207e178f614ef"; # ключ для расшифровки $chunkResponseDecrypt = openssl_decrypt($data, $method, $key, OPENSSL_RAW_DATA, $iv); # расшифровка ? fwrite(fopen('save.ts', 'w'), $chunkResponseDecrypt); # записываем результат в save.ts
Ключ подходит, расшифровка идет, но сам файл битый, не воспроизводится.