vk_api icon indicating copy to clipboard operation
vk_api copied to clipboard

Проблема получения аудиозаписей (m3u8)

Open qwertyadrian opened this issue 5 years ago • 78 comments

Я столкнулся с проблемой получения аудиозаписей. ВК явно что-то поменял. 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), он имеет следующее содержимое: ссылка

На данный момент, я знаю, что делать с этим файлом дальше.

qwertyadrian avatar Oct 22 '19 10:10 qwertyadrian

Ну, возможно стоит конвертировать в другой формат :thinking:

prostomarkeloff avatar Oct 22 '19 10:10 prostomarkeloff

Подвох в том, что теперь аудиозапись разделена на фрагменты, и каждый из них зашифрован с помощью алгоритма AES-128.

qwertyadrian avatar Oct 22 '19 12:10 qwertyadrian

Предлагаю https://gist.github.com/grwlf/e1876f5d78cb6e66791809771d7bf36b . Пожалуйста, сообщите, если есть нормальная программа, которая понимает такие плейлисты.

sergei-mironov avatar Oct 27 '19 14:10 sergei-mironov

@grwlf Я также переписал ваш скрипт на Python для возможности его использования в Windows: https://gist.github.com/qwertyadrian/a57edb869d3e3c5e01abf04a744a62f0

Пожалуйста, сообщите, если есть нормальная программа, которая понимает такие плейлисты.

ffplay может воспроизводить такие плейлисты, но тут уже вопрос удобства

qwertyadrian avatar Oct 28 '19 10:10 qwertyadrian

ffplay может воспроизводить такие плейлисты, но тут уже вопрос удобства

ОК. Пока не проверял, но есть сомнения: если ffplay - из пакета ffmpeg, то наверное там поддержка такая же как и в ffmpeg, а там она недостаточна (поэтому и скрипт понадобился). Например, ffmpeg похоже непонимает, что в плейлисте урлы сегментов указаны относительно урла плейлиста

sergei-mironov avatar Oct 28 '19 11:10 sergei-mironov

ffmpeg похоже непонимает, что в плейлисте урлы сегментов указаны относительно урла плейлиста

Да, ffplay может воспроизводить плейлисты только после обработки вашим скриптом, то есть файл index_local.m3u8

upd: Я сейчас проверил и оказалось, что ffplay спокойно воспроизводит плейлист, если ему передать ссылку на него. Правда есть небольшие заикания.

qwertyadrian avatar Oct 28 '19 11:10 qwertyadrian

Оказалось все еще проще. ffmpeg скачивает плейлист, если использовать следующую команду: ffmpeg -i <ссылка на плейлист index.m3u8> -c copy out.ts

qwertyadrian avatar Oct 28 '19 11:10 qwertyadrian

Удивительно, я точно так пробовал, не работало. Может плохо пробовал) Ах, да, понял. Именно ссылку нужно было давать. Я давал скачанный файл

sergei-mironov avatar Oct 28 '19 13:10 sergei-mironov

Ещё такое соображение: меня ВК вчера забанил (не знаю, на долго ли), когда я попытался скачать много песен последовательно. Наверное должна помочь задержка, имитируюшая проигрывание в плеере. Такого ffmpeg наверное не сумеет прямо сходу.

sergei-mironov avatar Oct 28 '19 13:10 sergei-mironov

@grwlf можно так: крон-такси на определенное кол-во запусков, с каким-то промежутком @qwertyadrian указывайте, пожалуйста, версию python.

Enziferum avatar Nov 03 '19 16:11 Enziferum

меня ВК вчера забанил

можно так: крон-такси на определенное кол-во запусков, с каким-то промежутком

Оказалось, забанил ненадолго. Сейчас ставлю задержку размером в длину аудиозаписи после каждого скачивания. Пока работает без сбоев. Видимо, банхаммер не слишком избирательный.

sergei-mironov avatar Nov 03 '19 17:11 sergei-mironov

Оказалось все еще проще. ffmpeg скачивает плейлист, если использовать следующую команду: ffmpeg -i <ссылка на плейлист index.m3u8> -c copy out.ts

Если ffmpeg скормить ссылку на m3u8, то он собирает отрывки в один файл, но есть заикания. Причина заиканий мне не ясна. Это очень странно потому что результирующая продолжительность файла оказывается правильной.

Я скачивал части из плейлиста вручную и некоторые .ts проигрываются без расшифровки (те что METHOD=NONE), а те части, что METHOD=AES-128 не проигрываются без расшифровки.

Не знаю, может заикания из-за того, что отрывки в хаотичном порядке склеиваются?

DaveScream avatar Nov 23 '19 21:11 DaveScream

@grwlf и у вас ffmpeg бесшовно склеивает?

DaveScream avatar Nov 24 '19 08:11 DaveScream

Я научился расшифровывать зашифрованные .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, поэтому просто бинарно склеивать файлы нельзя.

DaveScream avatar Nov 24 '19 09:11 DaveScream

В общем переписал парсер и расшировывальщик заширофанных частей https://pastebin.com/jaFpvxM3 , но заикания после склеивания никуда не ушли(((

Я попытался перекодировать файл, а не просто склеить из частей, при перекодировании ffmpeg стал выдавать ошибки timestamp. Похоже, заикания из-за этого. Нужно разбираться откуда берется DTS и почему между кусками DTS перепрыгивает на несколько фреймов вперед-назад image

DaveScream avatar Nov 24 '19 12:11 DaveScream

Вот так выглядит список файла очереди для ffmpeg: image

Я оставил только 4 файла (4*3 = 12 секунд аудио) image

и склеил. в 12 секундном файле слышно только один артефакт. между 2 и 3 отрезком. И в окне FFMPEG видно только одну ошибку синхронизации: image

это значит, что какие-то файлы склеиваются бесшовно, но какие-то имеют проблемы склеивания.

DaveScream avatar Nov 24 '19 13:11 DaveScream

Сейчас я заметил, что если в VLC открыть ссылку на аудиофайл, то он воспроизводится (а также и сохраняется) без заиканий.

qwertyadrian avatar Nov 24 '19 13:11 qwertyadrian

Сейчас я заметил, что если в VLC открыть ссылку на аудиофайл, то он воспроизводится (а также и сохраняется) без заиканий.

Сейчас я заметил, что если в VLC открыть ссылку на аудиофайл, то он воспроизводится (а также и сохраняется) без заиканий.

Наконец-то хоть кто-то ожил. И у меня победа)) У меня была ошибка, парсер пропускал файлы плейлиста в которых были спецсимволы. Теперь файл склеивается без нареканий и заиканий. Что с перекодированием, что без перекодирования -c copy

Я думаю, для vk_api лучше вариант не через VLC, а через python + ffmpeg все таки? Он более гибкий и на выходе можно сконвертить во что угодно.

Это первый раз, когда я могу сделать какой-то вклад в уже существующий и кем-то написанный проект!

вот рабочий файл, который распаковывает m3u8, скачивает и расшифровывает зашифрованные части и склеивает их потом https://pastebin.com/ACBA7kWW

Как писал выше, для работы расшифровщика нужна библиотека pip install pycryptodome при этом нужно удалить pip uninstall pycrypto потому что они мешают друг другу pycrypto можно удалять, это устаревший проект + он не имел бинарных библиотек, их нужно было компилировать самому - гемморой

DaveScream avatar Nov 24 '19 13:11 DaveScream

Ещё нужно решить что теперь делать с полученным файлом, ведь это не mp3, в foobar он не воспроизводится. Это контейнер MPEG-TS, с mp3 потоком внутри. Как-то нужно научиться его доставать из MPEG-TS и без перекодирования запихивать в родной контейнер mp3. Простое переименование .ts в .mp3 ничего не делает

DaveScream avatar Nov 24 '19 13:11 DaveScream

Так. проблема тоже решилась, нужно просто указывать выходной файл .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 будет...

DaveScream avatar Nov 24 '19 13:11 DaveScream

вот рабочий файл, который распаковывает m3u8, скачивает и расшифровывает зашифрованные части и склеивает их потом

Я в ближайшее время подправлю его для работы в linux, так как по завершению работы программы временная папка удаляется вместе с полученным аудиофайлом. И еще у меня выполнение программы завершается с ошибкой, так как вы используется для путей обратный слеш (который используется только в Windows), для работы с путями лучше использовать модуть os.path. И по умолчанию возвращается список, который нельзя перенаправить в stdout.

qwertyadrian avatar Nov 24 '19 13:11 qwertyadrian

Здесь уже довольно давно нашел решение: https://habr.com/ru/post/457438/#comment_20319286 Регулярки достаточно чтобы из m3u8-ссылки получить mp3 В бою откатано, отправил PR #315

bakatrouble avatar Nov 24 '19 14:11 bakatrouble

Здесь уже довольно давно нашел решение: https://habr.com/ru/post/457438/#comment_20319286 Регулярки достаточно чтобы из m3u8-ссылки получить mp3 В бою откатано, отправил PR #315

у меня не сработало напрямую брать mp3

DaveScream avatar Nov 24 '19 14:11 DaveScream

вот рабочий файл, который распаковывает m3u8, скачивает и расшифровывает зашифрованные части и склеивает их потом

Я в ближайшее время подправлю его для работы в linux, так как по завершению работы программы временная папка удаляется вместе с полученным аудиофайлом. И еще у меня выполнение программы завершается с ошибкой, так как вы используется для путей обратный слеш (который используется только в Windows), для работы с путями лучше использовать модуть os.path. И по умолчанию возвращается список, который нельзя перенаправить в stdout.

написал комменты к коду, вывод в stdout и пути для linux https://pastebin.com/gvU6PQwQ

DaveScream avatar Nov 24 '19 14:11 DaveScream

у меня не сработало напрямую брать mp3

image

Получилось не сразу, иногда отдает немного другую ссылку Но работает у меня уже довольно давно и без сбоев

bakatrouble avatar Nov 24 '19 14:11 bakatrouble

Получилось не сразу, иногда отдает немного другую ссылку Но работает у меня уже довольно давно и без сбоев

я не так менял, и правда работает. спасибо. оставлю и тот и этот варианты на будущее.

DaveScream avatar Nov 24 '19 15:11 DaveScream

На данный момент решение, предложенное @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" тут точно не получится.

arkhipovkm avatar May 02 '20 19:05 arkhipovkm

Замечены новые m3u8-ссылки вида https://psv4.vkuseraudio.net/audio/ee/Ip2PDLQ0hcCVf66TIwU_96K18de69W5zQ0r0hg/5fNzQ4MDsxMjE8/fafls0f242Pz5rc10/index.m3u8?extra=V68TVWsJa742IUTDElHRDutcQq0TIDk36a5PJQpufpinJu9K1LwHeNIRNnOoRgwWNmLbU0mq8KgoiE_6PL0UsboDARvakwEgxJo5dkAutSYg40Ny15N-FPt_9bYEz4l2G6GM5xClzvSp9atAN_Fu_JbxuQ С которыми пока непонятно что делать

bakatrouble avatar Dec 13 '20 10:12 bakatrouble

Получилось не сразу, иногда отдает немного другую ссылку Но работает у меня уже довольно давно и без сбоев

я не так менял, и правда работает. спасибо. оставлю и тот и этот варианты на будущее.

iv находится внутри зашифрованного файла?

tparser avatar Feb 26 '21 06:02 tparser

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

Ключ подходит, расшифровка идет, но сам файл битый, не воспроизводится.

tparser avatar Feb 26 '21 07:02 tparser