GAPI

Скорость скачивания

Мы отдаём ссылки, а не файлы, поэтому скорость решает то, как вы их забираете. Разница между одним соединением и шестнадцатью настоящая, и она не одинакова на разных площадках — здесь написано, какая ссылка что позволяет, по замерам, а не по догадкам.

Какие ссылки можно делить

Ссылку можно тянуть параллельно, только если сервер за ней принимает Range-запрос. Мы попросили у каждой байты с 1024 по 2047 и записали, что пришло.

ПлощадкаRange-запросыЧто это значит
InstagramДаОтвечает 206 и полным размером в Content-Range. Делите на сколько угодно соединений и продолжайте с места обрыва.
TikTokДаОтвечает 206 и полным размером. Делить и докачивать можно, но не через несколько дней: ссылка на видео подписана примерно на шесть часов, на фото — примерно на сорок восемь.
FacebookДаОтвечает 206 и полным размером, как Instagram. Подпись держалась около тридцати четырёх часов.
ThreadsДаОтвечает 206 и полным размером. Картинки Threads лежат на том же CDN, что и фото Instagram, и подписаны примерно на сто часов.
VKНетОтвечает 400 на Range-запрос. Одно соединение с начала, обрыв — качать заново.
YouTubeНетЭто вообще не ссылка на CDN: файл идёт потоком с нашего домена одним проходом, и заголовок Range игнорируется. И живёт она меньше всех здесь, около 75 секунд.

Разбить скачивание на соединения

Там, где Range работает, это даёт больше всего. Запрашивайте по куску файла на соединение и склеивайте в конце. Шестнадцать — разумное число, дальше вы обычно стоите в очереди, а не качаете.

aria2c -x 16 -s 16 -k 1M \
  -o photo.jpg 'https://scontent.cdninstagram.com/…'
import concurrent.futures as cf
import httpx

def segment(url, path, parts=16):
    """Fetch one file over several connections, then join the pieces."""
    head = httpx.head(url, follow_redirects=True)
    if head.headers.get("Accept-Ranges") != "bytes":
        # No ranges: one connection is the only option, so take it.
        with open(path, "wb") as f, httpx.stream("GET", url, follow_redirects=True) as r:
            for chunk in r.iter_bytes():
                f.write(chunk)
        return

    total = int(head.headers["Content-Length"])
    span = total // parts

    def piece(i):
        first = i * span
        last = total - 1 if i == parts - 1 else first + span - 1
        r = httpx.get(url, headers={"Range": f"bytes={first}-{last}"},
                      follow_redirects=True, timeout=120)
        return i, r.content

    with cf.ThreadPoolExecutor(max_workers=parts) as pool:
        chunks = dict(pool.map(piece, range(parts)))

    with open(path, "wb") as f:
        for i in range(parts):
            f.write(chunks[i])

Где соединение только одно

VK и YouTube приходится забирать за один раз. Начинайте скачивание сразу, как получили ссылку, а не кладите её в очередь, и никогда не сохраняйте ссылку YouTube на потом: секунд через семьдесят пять она отвечает 410, и лечится это только новым вызовом.

curl -L --fail -o video.mp4 \
  'https://api.gapi.dev/dl/dc2cdb0cf9874b9cbdcff5b249b5c054'

Узнать размер заранее

В ответе лежат ссылки, а не размеры. Для YouTube /v2/youtube/info перечисляет все качества с размером в байтах: один дешёвый вызов, и человек выбирает до того, как что-то скачается. В остальных случаях HEAD по ссылке отдаёт Content-Length, и на четырёх площадках, которые принимают Range-запросы, это число точное.