Pull to refresh

Микросервис на GO для граббинга видео из твитов

Reading time4 min
Views6.9K

Добрый день, Хабравчане! Статья для новичков, каких то сверх новых идей вы здесь не увидите. Да и данный функционал, скорее всего, реализовывался десятки раз на различных языках. Идея состоит в том, что бы получив ссылку на пост в твиттере в котором содержится видео, забрать это видео и конвертировать в mkv.


К делу!


Что нам понадобится:


  • GO
  • ffmpeg
  • docker (хотя можно и без него. Однако, куда без него в наши дни?!; )

У нас есть ссылка на твит с видео:


https://twitter.com/FunnyVines/status/1101196533830041600

Из всей ссылки нас интересует только ID состоящий из цифр, поэтому элементарной регуляркой выдергиваем всю цифровую подстроку:


var reurl = regexp.MustCompile(`\/(\d*)$`)
// тут еще какой-то код
e.GET("/*video", func(c *fasthttp.RequestCtx) {
  // регэкспим то что мы получили
  url := reurl.FindSubmatch([]byte(c.UserValue("video").(string)))

С полученным ID идем по адресу:


resp, err := client.Get("https://twitter.com/i/videos/tweet/" + id)

Где получаем ссылку на JS код видеоплейра:


src="https://abs.twimg.com/web-video-player/TwitterVideoPlayerIframe.f52b5b572446290e.js"
Из этого js файла нам нужна одна очень важная вещь — bearer для авторизации в api twitter'а.


Regex'пим его!


re, _ := regexp.Compile(`(?m)authorization:\"Bearer (.*)\",\"x-csrf`)

Этого недостаточно для доступа к api, еще нужен guest_token. Его можно получить обратившись POST запросом по адресу — "https://api.twitter.com/1.1/guest/activate.json", передав туда: personalization_id и guest_id из файла cookie(который мы получили в ответе от сервера при обращении к предыдущему URL) :


    var personalization_id, guest_id string
    cookies := resp.Cookies()
    for _, cookie := range cookies {
        if cookie.Name == "personalization_id" {
            personalization_id = cookie.Value
        }
        if cookie.Name == "guest_id" {
            guest_id = cookie.Value
        }
    }
    // // Get Activation
    url, _ := url.Parse("https://api.twitter.com/1.1/guest/activate.json")

    request := &http.Request{
        Method: "POST",
        URL:    url,
        Header: http.Header{
            "user-agent": []string{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"},
             "accept-encoding": []string{"gzip", "deflate", "br"}, 
             "authorization": []string{"Bearer " + bearer},
             "cookie": []string{"personalization_id=\"" + personalization_id + "\"; guest_id=" + guest_id}
        },
    }
    resp, err = client.Do(request)

guest_token

Он периодически меняется, я не стал разбираться как часто его вызывать (скорее он меняется по таймеру — 15и минутный интервал), но вроде как, регулярная активация /1.1/guest/activate.json, позволяет обойти ограничение api на 300 запросов.


Ответ gzip'нутый, в Go распаковать его можно, примерно так:


res, err := gzip.NewReader(resp.Body)
    if err != nil {
        return "", err
    }
    defer res.Close()
    r, err := ioutil.ReadAll(res)

Ну все! Теперь у нас есть все что нужно, что бы вызвать API:


    url, _ = url.Parse("https://api.twitter.com/1.1/videos/tweet/config/" + id + ".json")
    request = &http.Request{
        Method: "GET",
        URL:    url,
        Header: http.Header{
            "user-agent": []string{"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.77 Safari/537.36"},
            "accept-encoding": []string{"gzip", "deflate", "br"}, "origin": []string{"https://twitter.com"},
            "x-guest-token": []string{gt.GuestToken}, 
            "referer": []string{"https://twitter.com/i/videos/tweet/" + id},
            "authorization": []string{"Bearer " + bearer}},
    }
    resp, err = client.Do(request)

Ответом от API будет Json с описанием видео и самое главное — URL адресом для его получения(playbackUrl):


{"contentType":"media_entity","publisherId":"4888096512","contentId":"1096941371649347584","durationMs":11201,"playbackUrl":"https:\/\/video.twimg.com\/ext_tw_video\/1096941371649347584\/pu\/pl\/xcBvPmwAmKckck-F.m3u8?tag=6","playbackType"

И наконец, у нас есть адрес видео, отправляем его в ffmpeg, при этом проверив в каком формате видео, я увидел 2 возможных формата, первый — mp4:


if strings.Contains(videoURL.Track.PlaybackURL, ".mp4") {
        convert := exec.Command("ffmpeg", "-i", videoURL.Track.PlaybackURL, "-c", "copy", "./videos/"+id+".mkv")
        convert.Stdout = os.Stdout
        convert.Stderr = os.Stderr
        if convert.Run() != nil {
            return "", err
        }
        return id, nil
    }

И второй — m3u8 файл плейлиста, для этого варианта нужен еще один шаг — GET'ом получаем
его, и берем URL контента в нужном разрешении:


#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=256000,RESOLUTION=180x316,CODECS="mp4a.40.2,avc1.4d0015"
/ext_tw_video/1039516210948333568/pu/pl/180x316/x0HWMgnbSJ9y6NFL.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=832000,RESOLUTION=464x816,CODECS="mp4a.40.2,avc1.4d001f"
/ext_tw_video/1039516210948333568/pu/pl/464x816/Z58__ptq1xBk8CIV.m3u8

И все так же — ffmpeg.


А теперь, немного про HTTP сервер


Я использовал:



Логика следующая, стартуем сервер:


cfg := tcplisten.Config{
        ReusePort:   true,
        FastOpen:    true,
        DeferAccept: true,
        Backlog:     1024,
    }
    ln, err := cfg.NewListener("tcp4", ":8080")
    if err != nil {
        log.Fatalf("error in reuseport listener: %s\n", err)
    }
    serv := fasthttp.Server{Handler: e.Handler, ReduceMemoryUsage: false, Name: "highload", Concurrency: 2 * 1024, DisableHeaderNamesNormalizing: true}
    if err := serv.Serve(ln); err != nil {
        log.Fatalf("error in fasthttp Server: %s", err)
    }

И обрабатываем всего один роут /*video :


// сюда прокатит:
// это http://localhost:8080/https://twitter.com/FunnyVines/status/1101196533830041600
// и это http://localhost:8080/1101196533830041600
e.GET("/*video", func(c *fasthttp.RequestCtx) {

Как все это можно собирать?


К примеру, простой Makefile (make build, make run...):


build:
    go build -o main 
    docker build -t tvideo .
run:
    go build -o main 
    docker build -t tvideo .
    docker kill tvideo
    docker run -d --rm --name tvideo -v /etc/ssl:/etc/ssl:ro -v videos:/opt/videos -p 8080:8080 tvideo
    docker logs -f tvideo

Обратите внимание на флаг "-v /etc/ssl:/etc/ssl:ro", в базовом image ubuntu не оказалось корневых сертификатов и http клиент не признавал https twitter'а, прокинул с хостовой машины через --volume (сейчас, вроде как, правильнее использовать --mount).


Dockerfile


FROM ubuntu
// кладем бинарник приложения в docker image
COPY main /opt/app
RUN apt-get update && \
    // устанавливаем вундервафлю
    apt-get install -y ffmpeg && \ 
    chmod +x /opt/app

EXPOSE 8080

WORKDIR /opt
CMD ./app

Несомненно, Америку в данной статье я не открыл, но вдруг кому-то пригодится.


Исходники доступны здесь.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+11
Comments0

Articles