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

    Добрый день, Хабравчане! Статья для новичков, каких то сверх новых идей вы здесь не увидите. Да и данный функционал, скорее всего, реализовывался десятки раз на различных языках. Идея состоит в том, что бы получив ссылку на пост в твиттере в котором содержится видео, забрать это видео и конвертировать в 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

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


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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 0

    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

    Самое читаемое