├── .dockerignore ├── .gitignore ├── packages ├── transcoder │ └── transcoder.go ├── database │ ├── migration.go │ └── postgres.go ├── mediaInfo │ └── medfiainfo.go ├── ffmpeg │ ├── executer.go │ ├── core.go │ ├── tools.go │ └── ffmpeg.go ├── s3 │ └── minio.go └── cache │ └── redis.go ├── wait-for-it ├── .gitignore ├── test │ ├── requirements.txt │ ├── README.md │ ├── container-runners.py │ └── wait-for-it.py ├── .travis.yml ├── composer.json ├── LICENSE ├── README.md └── wait-for-it.sh ├── .vscode └── settings.json ├── images ├── logo.png └── cover.png ├── transcoder ├── config.go ├── options.go ├── ffmpeg │ ├── config.go │ ├── progress.go │ ├── options.go │ ├── metadata.go │ └── ffmpeg.go ├── progress.go ├── transcoder.go ├── utils │ └── utils.go └── metadata.go ├── .env ├── Dockerfile ├── models ├── frame-types.go ├── file-types.go ├── splash-type.go └── api-types.go ├── interfaces ├── file-manager │ └── filemanager.go ├── splash │ └── splash.go └── frame │ └── frame.go ├── docker-compose.DEV.yml ├── config └── config.go ├── go.mod ├── docker-compose.yml ├── main.go ├── controller ├── frame.controller.go ├── generator-controller.go └── splash-controller.go ├── README.md └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | data 2 | sample-files/*.* 3 | .vscode -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | data/* 2 | sample-files/*.* 3 | .vscode 4 | -------------------------------------------------------------------------------- /packages/transcoder/transcoder.go: -------------------------------------------------------------------------------- 1 | package executer 2 | -------------------------------------------------------------------------------- /wait-for-it/.gitignore: -------------------------------------------------------------------------------- 1 | **/*.pyc 2 | .pydevproject 3 | /vendor/ 4 | -------------------------------------------------------------------------------- /wait-for-it/test/requirements.txt: -------------------------------------------------------------------------------- 1 | docker>=4.0.0 2 | parameterized>=0.7.0 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "luma" 4 | ] 5 | } -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amupxm/go-video-concat/HEAD/images/logo.png -------------------------------------------------------------------------------- /images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amupxm/go-video-concat/HEAD/images/cover.png -------------------------------------------------------------------------------- /transcoder/config.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | // Config ... 4 | type Config interface{} 5 | -------------------------------------------------------------------------------- /wait-for-it/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | script: 6 | - python test/wait-for-it.py 7 | 8 | -------------------------------------------------------------------------------- /transcoder/options.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | // Options ... 4 | type Options interface { 5 | GetStrArguments() []string 6 | } 7 | -------------------------------------------------------------------------------- /transcoder/ffmpeg/config.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | // Config ... 4 | type Config struct { 5 | FfmpegBinPath string 6 | FfprobeBinPath string 7 | ProgressEnabled bool 8 | Verbose bool 9 | } 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | dbhost=127.0.0.1 2 | dbname=postgres 3 | dbpass=1202212022AaAa 4 | dbport=5432 5 | dbusername=postgres 6 | Minio_user=AKIAIOSFODNN7EXAMPLE 7 | Minio_host=127.0.0.1:9000 8 | Minio_password=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -------------------------------------------------------------------------------- /transcoder/progress.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | // Progress ... 4 | type Progress interface { 5 | GetFramesProcessed() string 6 | GetCurrentTime() string 7 | GetCurrentBitrate() string 8 | GetProgress() float64 9 | GetSpeed() string 10 | } 11 | -------------------------------------------------------------------------------- /wait-for-it/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vishnubob/wait-for-it", 3 | "description": "Pure bash script to test and wait on the availability of a TCP host and port", 4 | "type": "library", 5 | "license": "MIT", 6 | "bin": ["wait-for-it.sh"] 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16.4 2 | 3 | WORKDIR /go/src/app/amupxm 4 | 5 | RUN apt-get update && apt-get install -y \ 6 | ffmpeg \ 7 | libmediainfo-dev \ 8 | zlib* \ 9 | gcc && rm -rf /var/lib/apt/lists/* 10 | COPY . . 11 | 12 | RUN go get . 13 | 14 | 15 | RUN go build -v . 16 | 17 | CMD [ "./go-video-concat" ] -------------------------------------------------------------------------------- /packages/database/migration.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/amupxm/go-video-concat/models" 7 | ) 8 | 9 | func AutoMigration() { 10 | if err := PostgresConnection.DBCli.AutoMigrate( 11 | &models.Frame{}, 12 | &models.Splash{}, 13 | ); err != nil { 14 | log.Panicln(err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /models/frame-types.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ( 4 | Frame struct { 5 | ID uint `gorm:"primaryKey;index"` 6 | Name string `json:"name" binding:"required"` 7 | FileCode string `json:"file_code" binding:"required" gorm:"uniqueIndex;not null"` 8 | Height int `json:"height" binding:"required"` 9 | Width int `json:"width" binding:"required"` 10 | StartOffset int `json:"start_offset" binding:"required"` 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /models/file-types.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Suite int 4 | 5 | type ( 6 | fileType uint 7 | ) 8 | 9 | const ( 10 | _ fileType = iota 11 | uploadFile 12 | downloadFile 13 | chunkFile 14 | templatesFrameFile 15 | outputFile 16 | ) 17 | 18 | var ( 19 | fileTypes = map[fileType]string{ 20 | uploadFile: "upload", 21 | downloadFile: "download", 22 | chunkFile: "chunk", 23 | templatesFrameFile: "template", 24 | outputFile: "output", 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /transcoder/transcoder.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // Transcoder ... 8 | type Transcoder interface { 9 | Start(opts Options) (<-chan Progress, error) 10 | Input(i string) Transcoder 11 | InputPipe(w *io.WriteCloser, r *io.ReadCloser) Transcoder 12 | Output(o string) Transcoder 13 | OutputPipe(w *io.WriteCloser, r *io.ReadCloser) Transcoder 14 | WithOptions(opts Options) Transcoder 15 | WithAdditionalOptions(opts Options) Transcoder 16 | GetMetadata() (Metadata, error) 17 | } 18 | -------------------------------------------------------------------------------- /transcoder/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // DurToSec ... 9 | func DurToSec(dur string) (sec float64) { 10 | durAry := strings.Split(dur, ":") 11 | var secs float64 12 | if len(durAry) != 3 { 13 | return 14 | } 15 | hr, _ := strconv.ParseFloat(durAry[0], 64) 16 | secs = hr * (60 * 60) 17 | min, _ := strconv.ParseFloat(durAry[1], 64) 18 | secs += min * (60) 19 | second, _ := strconv.ParseFloat(durAry[2], 64) 20 | secs += second 21 | return secs 22 | } 23 | -------------------------------------------------------------------------------- /wait-for-it/test/README.md: -------------------------------------------------------------------------------- 1 | # Tests for wait-for-it 2 | 3 | * wait-for-it.py - pytests for wait-for-it.sh 4 | * container-runners.py - Runs wait-for-it.py tests in multiple containers 5 | * requirements.txt - pip requirements for container-runners.py 6 | 7 | To run the basic tests: 8 | 9 | ``` 10 | python wait-for-it.py 11 | ``` 12 | 13 | Many of the issues encountered have been related to differences between operating system versions. The container-runners.py script provides an easy way to run the python wait-for-it.py tests against multiple system configurations: 14 | 15 | ``` 16 | pip install -r requirements.txt 17 | python container-runners.py 18 | ``` 19 | -------------------------------------------------------------------------------- /packages/mediaInfo/medfiainfo.go: -------------------------------------------------------------------------------- 1 | package mediainfo 2 | 3 | import mediainfo "github.com/lbryio/go_mediainfo" 4 | 5 | type ( 6 | MediaInfoStruct struct { 7 | Height int64 8 | Width int64 9 | Duration int64 10 | } 11 | MediaInfoInterface interface { 12 | getVideoInfo() error 13 | } 14 | ) 15 | 16 | func (m *MediaInfoStruct) GetVideoInfo(path string) error { 17 | mi := mediainfo.NewMediaInfo() 18 | err := mi.OpenFile(path) 19 | if err != nil { 20 | return err 21 | } 22 | defer mi.Close() 23 | 24 | m.Duration = mi.GetInt(mediainfo.MediaInfo_Stream_General, "Duration") 25 | m.Width = mi.GetInt(mediainfo.MediaInfo_Stream_Video, "Width") 26 | m.Height = mi.GetInt(mediainfo.MediaInfo_Stream_Video, "Height") 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /models/splash-type.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type SplashBaseType int 4 | 5 | const ( 6 | Image SplashBaseType = iota 7 | Video 8 | ) 9 | 10 | func (splash SplashBaseType) String() string { 11 | return [...]string{"Image", "Video"}[splash] 12 | } 13 | 14 | type ( 15 | Splash struct { 16 | ID uint `gorm:"primaryKey;index"` 17 | FileCode string `gorm:"uniqueIndex;not null"` 18 | Status bool 19 | // requirements as json input 20 | BaseFile string `json:"base_file" binding:"required"` 21 | BaseAudio string `json:"base_audio" binding:"required"` 22 | SplashBase SplashBaseType 23 | Name string `json:"name" binding:"required"` 24 | MaxLength uint `json:"max_length" binding:"required"` 25 | BaseColor string `json:"base_color" binding:"required"` 26 | } 27 | ) 28 | -------------------------------------------------------------------------------- /transcoder/ffmpeg/progress.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | // Progress ... 4 | type Progress struct { 5 | FramesProcessed string 6 | CurrentTime string 7 | CurrentBitrate string 8 | Progress float64 9 | Speed string 10 | } 11 | 12 | // GetFramesProcessed ... 13 | func (p Progress) GetFramesProcessed() string { 14 | return p.FramesProcessed 15 | } 16 | 17 | // GetCurrentTime ... 18 | func (p Progress) GetCurrentTime() string { 19 | return p.CurrentTime 20 | } 21 | 22 | // GetCurrentBitrate ... 23 | func (p Progress) GetCurrentBitrate() string { 24 | return p.CurrentBitrate 25 | } 26 | 27 | // GetProgress ... 28 | func (p Progress) GetProgress() float64 { 29 | return p.Progress 30 | } 31 | 32 | // GetSpeed ... 33 | func (p Progress) GetSpeed() string { 34 | return p.Speed 35 | } 36 | -------------------------------------------------------------------------------- /interfaces/file-manager/filemanager.go: -------------------------------------------------------------------------------- 1 | package filemanager 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | 7 | "github.com/cavaliercoder/grab" 8 | ) 9 | 10 | type ( 11 | Temperory struct { 12 | DirName string 13 | } 14 | FileManager interface { 15 | CreateTempDir() error 16 | DeleteTempDir() error 17 | } 18 | ) 19 | 20 | func (t *Temperory) CreateTempDir() error { 21 | err := os.Mkdir("/tmp/"+t.DirName, 0777) 22 | return err 23 | } 24 | 25 | func (t *Temperory) DeleteTempDir() error { 26 | err := os.Remove("/tmp/" + t.DirName) 27 | return err 28 | } 29 | 30 | func DownloadFile(wg *sync.WaitGroup, url string, path string) (bool, string) { 31 | client := grab.NewClient() 32 | req, err := grab.NewRequest(path, url) 33 | resp := client.Do(req) 34 | defer wg.Done() 35 | return err == nil, resp.HTTPResponse.Status 36 | } 37 | -------------------------------------------------------------------------------- /packages/database/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/amupxm/go-video-concat/config" 8 | "gorm.io/driver/postgres" 9 | "gorm.io/gorm" 10 | ) 11 | 12 | type Postgres struct { 13 | DBCli *gorm.DB 14 | } 15 | 16 | var PostgresConnection = Postgres{} 17 | 18 | func (d *Postgres) ConnectDatabase(config *config.ConfigStruct) { 19 | dsn := fmt.Sprintf("host=%v user=%v password=%v dbname=%v port=%v sslmode=%v TimeZone=%v", 20 | config.Database.Postgres_host, 21 | config.Database.Postgres_username, 22 | config.Database.Postgres_password, 23 | config.Database.Postgres_databasename, 24 | config.Database.Postgres_port, 25 | "disable", 26 | "Asia/Tehran", 27 | ) 28 | 29 | db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | d.DBCli = db 35 | } 36 | -------------------------------------------------------------------------------- /packages/ffmpeg/executer.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | ) 7 | 8 | type Config struct { 9 | FfmpegBinPath string 10 | FfprobeBinPath string 11 | ProgressEnabled bool 12 | Verbose bool 13 | } 14 | type Progress struct { 15 | FramesProcessed string 16 | CurrentTime string 17 | CurrentBitrate string 18 | Progress float64 19 | Speed string 20 | } 21 | 22 | const ( 23 | FfmpegBinPath = "/usr/bin/ffmpeg" 24 | FfprobeBinPath = "/usr/bin/ffprobe" 25 | ProgressEnabled = true 26 | Verbose = true 27 | ) 28 | 29 | // Spawn chield proccess 30 | func Execute(args ...string) error { 31 | cmd := exec.Command(FfmpegBinPath, args...) 32 | if Verbose { 33 | cmd.Stderr = os.Stdout 34 | } 35 | err := cmd.Start() 36 | if err != nil { 37 | return err 38 | } 39 | err = cmd.Wait() 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | // TODO : add transcoder for executable 47 | -------------------------------------------------------------------------------- /models/api-types.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type ( 6 | VideoRecipe struct { 7 | VideoName string `json:"video_name" binding:"required"` 8 | FrameCode string `json:"frame_code" binding:"required"` 9 | CallBackAddress string `json:"call_back_address" binding:"required"` 10 | SoundLevel uint `json:"sound_level" binding:"required"` 11 | Audio string `json:"audio_file" binding:"required" ` 12 | Splash string `json:"splash_file" binding:"required" ` 13 | ExternalAudio bool `json:"has_audio" binding:"required"` 14 | Chunks []RecipeChunks `json:"inputs"` 15 | Status bool 16 | Message string 17 | UUID string 18 | StartedAt time.Time 19 | EndedAt *time.Time 20 | } 21 | RecipeChunks struct { 22 | FinalFile string 23 | Type string `json:"type" binding:"required"` 24 | Url string `json:"path" binding:"required"` 25 | Start int `json:"start" binding:"required"` 26 | Name string 27 | End int `json:"end" binding:"required"` 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /wait-for-it/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016 Giles Hall 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /packages/s3/minio.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/amupxm/go-video-concat/config" 8 | "github.com/minio/minio-go/v7" 9 | "github.com/minio/minio-go/v7/pkg/credentials" 10 | ) 11 | 12 | type ObjectStorageStruct struct { 13 | Client *minio.Client 14 | } 15 | 16 | var ObjectStorage = ObjectStorageStruct{} 17 | 18 | func (object *ObjectStorageStruct) Connect(cfg *config.ConfigStruct) { 19 | 20 | m, err := minio.New(cfg.Storage.Minio_host, &minio.Options{ 21 | Creds: credentials.NewStaticV4(cfg.Storage.Minio_user, cfg.Storage.Minio_password, ""), 22 | Secure: false, 23 | }) 24 | if err != nil { 25 | log.Fatalln(err) 26 | } 27 | ObjectStorage.Client = m 28 | } 29 | 30 | func InitBuckets(buckets []string) { 31 | for _, bucket := range buckets { 32 | s, err := ObjectStorage.Client.BucketExists(context.Background(), bucket) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | if !s { 37 | if err := ObjectStorage.Client.MakeBucket(context.Background(), 38 | bucket, 39 | minio.MakeBucketOptions{Region: "us-east-1", ObjectLocking: false}); err != nil { 40 | log.Fatal(err) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.DEV.yml: -------------------------------------------------------------------------------- 1 | version : "3" 2 | 3 | services: 4 | redis : 5 | image : redis 6 | ports: 7 | - 6379:6379 8 | postgres: 9 | image: postgres:latest 10 | environment: 11 | POSTGRES_PASSWORD: 1202212022AaAa 12 | PGDATA : /var/lib/postgresql/data/pgdata 13 | ports: 14 | - 5432:5432 15 | volumes: 16 | - ./data:/var/lib/postgresql/data:Z 17 | nginx: 18 | image : nginx 19 | volumes: 20 | - ./sample-files:/usr/share/nginx/html:ro 21 | ports: 22 | - "8081:80" 23 | environment: 24 | - NGINX_PORT=80 25 | minio: 26 | image : minio/minio 27 | ports: 28 | - "9000:9000" 29 | volumes: 30 | - ./data/minio:/data:Z 31 | healthcheck: 32 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 33 | interval: 30s 34 | timeout: 20s 35 | retries: 3 36 | command: server /data 37 | environment: 38 | - MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE 39 | - MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -------------------------------------------------------------------------------- /wait-for-it/test/container-runners.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # Unit tests to run wait-for-it.py unit tests in several different docker images 4 | 5 | import unittest 6 | import os 7 | import docker 8 | from parameterized import parameterized 9 | 10 | client = docker.from_env() 11 | app_path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..')) 12 | volumes = {app_path: {'bind': '/app', 'mode': 'ro'}} 13 | 14 | class TestContainers(unittest.TestCase): 15 | """ 16 | Test multiple container types with the test cases in wait-for-it.py 17 | """ 18 | 19 | @parameterized.expand([ 20 | "python:3.5-buster", 21 | "python:3.5-stretch", 22 | "dougg/alpine-busybox:alpine-3.11.3_busybox-1.30.1", 23 | "dougg/alpine-busybox:alpine-3.11.3_busybox-1.31.1" 24 | ]) 25 | def test_image(self, image): 26 | print(image) 27 | command="/app/test/wait-for-it.py" 28 | container = client.containers.run(image, command=command, volumes=volumes, detach=True) 29 | result = container.wait() 30 | logs = container.logs() 31 | container.remove() 32 | self.assertEqual(result["StatusCode"], 0) 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | ) 9 | 10 | type ( 11 | ConfigStruct struct { 12 | Database DatabaseConfig 13 | Storage StorageConfig 14 | Redis RedisConfig 15 | } 16 | DatabaseConfig struct { 17 | Postgres_host string 18 | Postgres_username string 19 | Postgres_password string 20 | Postgres_databasename string 21 | Postgres_port string 22 | } 23 | StorageConfig struct { 24 | Minio_user string 25 | Minio_password string 26 | Minio_host string 27 | } 28 | 29 | RedisConfig struct { 30 | Host string 31 | Password string 32 | DB uint 33 | } 34 | ) 35 | 36 | var config *ConfigStruct 37 | 38 | func (cfg *ConfigStruct) LoadConfigs() { 39 | err := godotenv.Load() 40 | if err != nil { 41 | log.Fatal("Error loading .env file") 42 | } 43 | cfg.Database.Postgres_databasename = os.Getenv("dbname") 44 | cfg.Database.Postgres_host = os.Getenv("dbhost") 45 | cfg.Database.Postgres_password = os.Getenv("dbpass") 46 | cfg.Database.Postgres_port = os.Getenv("dbport") 47 | cfg.Database.Postgres_username = os.Getenv("dbusername") 48 | cfg.Storage.Minio_host = os.Getenv("Minio_host") 49 | cfg.Storage.Minio_password = os.Getenv("Minio_password") 50 | cfg.Storage.Minio_user = os.Getenv("Minio_user") 51 | 52 | cfg.Redis.Host = os.Getenv("Redis_host") 53 | cfg.Redis.Password = os.Getenv("Redis_password") 54 | cfg.Redis.DB = 0 55 | 56 | config = cfg 57 | } 58 | -------------------------------------------------------------------------------- /packages/cache/redis.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/amupxm/go-video-concat/config" 8 | "github.com/go-redis/redis/v8" 9 | ) 10 | 11 | type CacheRedisContext struct { 12 | Cli *redis.Client 13 | } 14 | 15 | var ctx = context.Background() 16 | var CacheRedis = CacheRedisContext{} 17 | 18 | func Init(config *config.ConfigStruct) { 19 | db := redis.NewClient(&redis.Options{ 20 | Addr: config.Redis.Host, 21 | Password: config.Redis.Password, 22 | DB: int(config.Redis.DB), 23 | }) 24 | CacheRedis.Cli = db 25 | } 26 | 27 | func NewProccess(uuid string, sectionsCount int) bool { 28 | var r = &CacheRedis 29 | errors := make([]error, 4) 30 | errors[1] = r.Cli.Set(ctx, uuid+":status", "started", 0).Err() 31 | for _, e := range errors { 32 | if e != nil { 33 | return false 34 | } 35 | } 36 | return true 37 | } 38 | 39 | func UpdateStatus(uuid, message string, status bool) bool { 40 | var r = &CacheRedis 41 | errors := make([]error, 2) 42 | errors[1] = r.Cli.Set(ctx, uuid+":status", status, 0).Err() 43 | errors[0] = r.Cli.Set(ctx, uuid+":message", message, 0).Err() 44 | 45 | for _, e := range errors { 46 | if e == nil { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | func GetStatus(code string) (bool, string) { 54 | var r = &CacheRedis 55 | log.Println(code + ":status") 56 | respStatus := r.Cli.Get(ctx, code+":status") 57 | respMessage := r.Cli.Get(ctx, code+":message") 58 | message := respMessage.Val() 59 | status, err := respStatus.Bool() 60 | if err == nil { 61 | return status, message 62 | } 63 | return false, "invalid code" 64 | } 65 | -------------------------------------------------------------------------------- /transcoder/metadata.go: -------------------------------------------------------------------------------- 1 | package transcoder 2 | 3 | // Metadata ... 4 | type Metadata interface { 5 | GetFormat() Format 6 | GetStreams() []Streams 7 | } 8 | 9 | // Format ... 10 | type Format interface { 11 | GetFilename() string 12 | GetNbStreams() int 13 | GetNbPrograms() int 14 | GetFormatName() string 15 | GetFormatLongName() string 16 | GetDuration() string 17 | GetSize() string 18 | GetBitRate() string 19 | GetProbeScore() int 20 | GetTags() Tags 21 | } 22 | 23 | // Streams ... 24 | type Streams interface { 25 | GetIndex() int 26 | GetID() string 27 | GetCodecName() string 28 | GetCodecLongName() string 29 | GetProfile() string 30 | GetCodecType() string 31 | GetCodecTimeBase() string 32 | GetCodecTagString() string 33 | GetCodecTag() string 34 | GetWidth() int 35 | GetHeight() int 36 | GetCodedWidth() int 37 | GetCodedHeight() int 38 | GetHasBFrames() int 39 | GetSampleAspectRatio() string 40 | GetDisplayAspectRatio() string 41 | GetPixFmt() string 42 | GetLevel() int 43 | GetChromaLocation() string 44 | GetRefs() int 45 | GetQuarterSample() string 46 | GetDivxPacked() string 47 | GetRFrameRrate() string 48 | GetAvgFrameRate() string 49 | GetTimeBase() string 50 | GetDurationTs() int 51 | GetDuration() string 52 | GetDisposition() Disposition 53 | GetBitRate() string 54 | } 55 | 56 | // Tags ... 57 | type Tags interface { 58 | GetEncoder() string 59 | } 60 | 61 | // Disposition ... 62 | type Disposition interface { 63 | GetDefault() int 64 | GetDub() int 65 | GetOriginal() int 66 | GetComment() int 67 | GetLyrics() int 68 | GetKaraoke() int 69 | GetForced() int 70 | GetHearingImpaired() int 71 | GetVisualImpaired() int 72 | GetCleanEffects() int 73 | } 74 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/amupxm/go-video-concat 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/cavaliercoder/grab v2.0.0+incompatible 7 | github.com/floostack/transcoder v1.1.1 8 | github.com/gin-gonic/gin v1.7.1 9 | github.com/go-ini/ini v1.62.0 // indirect 10 | github.com/go-playground/validator/v10 v10.5.0 // indirect 11 | github.com/go-redis/redis/v8 v8.8.2 12 | github.com/gofrs/uuid v3.2.0+incompatible 13 | github.com/golang/protobuf v1.5.2 // indirect 14 | github.com/google/uuid v1.2.0 // indirect 15 | github.com/joho/godotenv v1.3.0 16 | github.com/json-iterator/go v1.1.11 // indirect 17 | github.com/klauspost/cpuid/v2 v2.0.6 // indirect 18 | github.com/lbryio/go_mediainfo v0.0.0-20200109212001-4c7318fd92ad 19 | github.com/leodido/go-urn v1.2.1 // indirect 20 | github.com/minio/md5-simd v1.1.2 // indirect 21 | github.com/minio/minio-go v6.0.14+incompatible // indirect 22 | github.com/minio/minio-go/v7 v7.0.10 23 | github.com/minio/sha256-simd v1.0.0 // indirect 24 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 25 | github.com/modern-go/reflect2 v1.0.1 // indirect 26 | github.com/rs/xid v1.3.0 // indirect 27 | github.com/ugorji/go v1.2.5 // indirect 28 | golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf // indirect 29 | golang.org/x/net v0.0.0-20210508051633-16afe75a6701 // indirect 30 | golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096 // indirect 31 | golang.org/x/text v0.3.6 // indirect 32 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 33 | gopkg.in/ini.v1 v1.62.0 // indirect 34 | gopkg.in/yaml.v2 v2.4.0 // indirect 35 | gorm.io/driver/postgres v1.1.0 36 | gorm.io/gorm v1.21.9 37 | honnef.co/go/tools v0.0.1-2019.2.3 // indirect 38 | ) 39 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version : "3" 2 | 3 | services: 4 | app: 5 | build: . 6 | env_file: .env 7 | ports: 8 | - 8080:8080 9 | depends_on: 10 | - "redis" 11 | - "postgres" 12 | - "minio" 13 | environment: 14 | GIN_MODE : release 15 | networks: 16 | - inner 17 | 18 | redis : 19 | image : redis 20 | ports: 21 | - 6379:6379 22 | networks: 23 | - inner 24 | postgres: 25 | image: postgres:latest 26 | environment: 27 | POSTGRES_PASSWORD: 1202212022AaAa 28 | PGDATA : /var/lib/postgresql/data/pgdata 29 | ports: 30 | - 5432:5432 31 | volumes: 32 | - ./data:/var/lib/postgresql/data:Z 33 | networks: 34 | - inner 35 | nginx: 36 | image : nginx 37 | volumes: 38 | - ./sample-files:/usr/share/nginx/html:ro 39 | ports: 40 | - "8081:80" 41 | environment: 42 | - NGINX_PORT=80 43 | networks: 44 | - inner 45 | minio: 46 | image : minio/minio 47 | ports: 48 | - "9000:9000" 49 | volumes: 50 | - ./data/minio:/data:Z 51 | healthcheck: 52 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 53 | interval: 30s 54 | timeout: 20s 55 | retries: 3 56 | command: server /export 57 | environment: 58 | - MINIO_ROOT_USER=AKIAIOSFODNN7EXAMPLE 59 | - MINIO_ROOT_PASSWORD=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY 60 | networks: 61 | - inner 62 | networks: 63 | inner: -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/amupxm/go-video-concat/packages/cache" 5 | postgres "github.com/amupxm/go-video-concat/packages/database" 6 | 7 | ApiController "github.com/amupxm/go-video-concat/controller" 8 | 9 | s3 "github.com/amupxm/go-video-concat/packages/s3" 10 | 11 | "github.com/amupxm/go-video-concat/config" 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | var AppConfig = &config.ConfigStruct{} 16 | 17 | func main() { 18 | router := gin.Default() 19 | AppConfig.LoadConfigs() 20 | // ======= Database and Storage ========= 21 | postgres.PostgresConnection.ConnectDatabase(AppConfig) 22 | postgres.AutoMigration() 23 | buckets := []string{"frame", "amupxm", "thumbnails", "splash", "upload", "splash-base", "splash-audio", "outputs"} 24 | s3.ObjectStorage.Connect(AppConfig) 25 | s3.InitBuckets(buckets) 26 | cache.Init(AppConfig) 27 | // ======================================= 28 | 29 | // ============ Controllers ============ 30 | 31 | // 1 - frame 32 | router.POST("/frame/upload", ApiController.Frame_Upload) 33 | router.POST("/frame", ApiController.Frame_Add) 34 | router.GET("/frame", ApiController.Frame_list) 35 | router.GET("/frame/:code/file", ApiController.Frame_File) 36 | router.GET("/frame/:code", ApiController.Frame_Single) 37 | 38 | // 2 - splash video (splash) 39 | router.POST("/splash/base", ApiController.Splash_Base) 40 | router.POST("/splash/audio", ApiController.Splash_Audio) 41 | router.POST("/splash", ApiController.Splash_Add) 42 | router.GET("/splash/:code", ApiController.Splash_file) 43 | 44 | // 3 - generator 45 | router.POST("/generator/upload", ApiController.Generator_Upload) 46 | router.POST("/generator", ApiController.Generator_Generate) 47 | router.GET("/generator/:code/file", ApiController.Generator_file) 48 | router.GET("/generator/:code", ApiController.Generator_Status) 49 | 50 | router.Run() 51 | } 52 | -------------------------------------------------------------------------------- /interfaces/splash/splash.go: -------------------------------------------------------------------------------- 1 | package splash 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | 7 | "github.com/amupxm/go-video-concat/models" 8 | "gorm.io/gorm" 9 | 10 | postgres "github.com/amupxm/go-video-concat/packages/database" 11 | "github.com/amupxm/go-video-concat/packages/s3" 12 | "github.com/minio/minio-go/v7" 13 | ) 14 | 15 | type ( 16 | Splash struct{ *models.Splash } 17 | SplashMessageChannel struct { 18 | Message string 19 | Status bool 20 | } 21 | SplashOperation interface { 22 | AddSplash() bool 23 | } 24 | ) 25 | 26 | func (splash *Splash) AddSplash(uuid string) (bool, string) { 27 | var Database = &postgres.PostgresConnection 28 | var ObjectStorage = &s3.ObjectStorage 29 | if splash.BaseFile == "" || splash.BaseAudio == "" { 30 | return false, "object name should not be empty" 31 | } 32 | _, err := ObjectStorage.Client.StatObject(context.Background(), "splash-base", splash.BaseFile, minio.StatObjectOptions{}) 33 | if err != nil { 34 | return false, "base file dont exits in storage" 35 | } 36 | _, err = ObjectStorage.Client.StatObject(context.Background(), "splash-audio", splash.BaseAudio, minio.StatObjectOptions{}) 37 | if err != nil { 38 | return false, "audio file dont exits in storage" 39 | } 40 | splash.FileCode = uuid 41 | splash.Status = false 42 | result := Database.DBCli.Create(splash) 43 | return result.Error == nil, "done" 44 | } 45 | 46 | func (splash *Splash) FindSplash(code string) (Splash, bool) { 47 | var resultFrame Splash 48 | var Database = &postgres.PostgresConnection 49 | result := Database.DBCli.First(&resultFrame, "file_code = ?", code) 50 | return resultFrame, !errors.Is(result.Error, gorm.ErrRecordNotFound) 51 | } 52 | 53 | func (splash *Splash) GetFile(splashCode string) (*minio.Object, bool) { 54 | var tmp *minio.Object 55 | _, status := splash.FindSplash(splashCode) 56 | if !status { 57 | return tmp, status 58 | } 59 | 60 | var ObjectStorage = &s3.ObjectStorage 61 | reader, err := ObjectStorage.Client.GetObject(context.Background(), "splash", splashCode, minio.GetObjectOptions{}) 62 | if err != nil { 63 | return tmp, false 64 | } 65 | return reader, true 66 | } 67 | -------------------------------------------------------------------------------- /interfaces/frame/frame.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "log" 7 | 8 | "github.com/amupxm/go-video-concat/models" 9 | postgres "github.com/amupxm/go-video-concat/packages/database" 10 | "github.com/amupxm/go-video-concat/packages/s3" 11 | "github.com/minio/minio-go/v7" 12 | 13 | "gorm.io/gorm" 14 | ) 15 | 16 | type ( 17 | Frame struct{ *models.Frame } 18 | FrameOperation interface { 19 | AddFrame() bool 20 | ListFrame() []Frame 21 | FindFrame(code string) (Frame, bool) 22 | GetFile(frameCode string) (Frame, bool) 23 | } 24 | ) 25 | 26 | // add frame to database and s3. 27 | func (frame *Frame) AddFrame() bool { 28 | var Database = &postgres.PostgresConnection 29 | var ObjectStorage = &s3.ObjectStorage 30 | // check file code exists in storage 31 | _, err := ObjectStorage.Client.StatObject(context.Background(), "frame", frame.FileCode, minio.StatObjectOptions{}) 32 | if err != nil { 33 | log.Fatal(err) 34 | return false 35 | 36 | } 37 | result := Database.DBCli.Create(frame) 38 | return result.Error == nil 39 | } 40 | 41 | // get all frames and return as array frames 42 | func (frame *Frame) ListFrame() []Frame { 43 | var Database = &postgres.PostgresConnection 44 | var respone []Frame 45 | Database.DBCli.Find(&respone) 46 | return respone 47 | } 48 | 49 | // use frame unique uuid to find frame and returns single frame 50 | func (frame *Frame) FindFrame(code string) (Frame, bool) { 51 | var resultFrame Frame 52 | var Database = &postgres.PostgresConnection 53 | result := Database.DBCli.First(&resultFrame, "file_code = ?", code) 54 | return resultFrame, !errors.Is(result.Error, gorm.ErrRecordNotFound) 55 | } 56 | 57 | // get frame file from s3 and returns s3 object( read io ). 58 | func (frame *Frame) GetFile(frameCode string) (*minio.Object, bool) { 59 | var tmp *minio.Object 60 | _, status := frame.FindFrame(frameCode) 61 | if !status { 62 | return tmp, status 63 | } 64 | var ObjectStorage = &s3.ObjectStorage 65 | reader, err := ObjectStorage.Client.GetObject(context.Background(), "frame", frameCode, minio.GetObjectOptions{}) 66 | if err != nil { 67 | return tmp, false 68 | } 69 | return reader, true 70 | } 71 | -------------------------------------------------------------------------------- /wait-for-it/README.md: -------------------------------------------------------------------------------- 1 | # wait-for-it 2 | 3 | `wait-for-it.sh` is a pure bash script that will wait on the availability of a 4 | host and TCP port. It is useful for synchronizing the spin-up of 5 | interdependent services, such as linked docker containers. Since it is a pure 6 | bash script, it does not have any external dependencies. 7 | 8 | ## Usage 9 | 10 | ```text 11 | wait-for-it.sh host:port [-s] [-t timeout] [-- command args] 12 | -h HOST | --host=HOST Host or IP under test 13 | -p PORT | --port=PORT TCP port under test 14 | Alternatively, you specify the host and port as host:port 15 | -s | --strict Only execute subcommand if the test succeeds 16 | -q | --quiet Don't output any status messages 17 | -t TIMEOUT | --timeout=TIMEOUT 18 | Timeout in seconds, zero for no timeout 19 | -- COMMAND ARGS Execute command with args after the test finishes 20 | ``` 21 | 22 | ## Examples 23 | 24 | For example, let's test to see if we can access port 80 on `www.google.com`, 25 | and if it is available, echo the message `google is up`. 26 | 27 | ```text 28 | $ ./wait-for-it.sh www.google.com:80 -- echo "google is up" 29 | wait-for-it.sh: waiting 15 seconds for www.google.com:80 30 | wait-for-it.sh: www.google.com:80 is available after 0 seconds 31 | google is up 32 | ``` 33 | 34 | You can set your own timeout with the `-t` or `--timeout=` option. Setting 35 | the timeout value to 0 will disable the timeout: 36 | 37 | ```text 38 | $ ./wait-for-it.sh -t 0 www.google.com:80 -- echo "google is up" 39 | wait-for-it.sh: waiting for www.google.com:80 without a timeout 40 | wait-for-it.sh: www.google.com:80 is available after 0 seconds 41 | google is up 42 | ``` 43 | 44 | The subcommand will be executed regardless if the service is up or not. If you 45 | wish to execute the subcommand only if the service is up, add the `--strict` 46 | argument. In this example, we will test port 81 on `www.google.com` which will 47 | fail: 48 | 49 | ```text 50 | $ ./wait-for-it.sh www.google.com:81 --timeout=1 --strict -- echo "google is up" 51 | wait-for-it.sh: waiting 1 seconds for www.google.com:81 52 | wait-for-it.sh: timeout occurred after waiting 1 seconds for www.google.com:81 53 | wait-for-it.sh: strict mode, refusing to execute subprocess 54 | ``` 55 | 56 | If you don't want to execute a subcommand, leave off the `--` argument. This 57 | way, you can test the exit condition of `wait-for-it.sh` in your own scripts, 58 | and determine how to proceed: 59 | 60 | ```text 61 | $ ./wait-for-it.sh www.google.com:80 62 | wait-for-it.sh: waiting 15 seconds for www.google.com:80 63 | wait-for-it.sh: www.google.com:80 is available after 0 seconds 64 | $ echo $? 65 | 0 66 | $ ./wait-for-it.sh www.google.com:81 67 | wait-for-it.sh: waiting 15 seconds for www.google.com:81 68 | wait-for-it.sh: timeout occurred after waiting 15 seconds for www.google.com:81 69 | $ echo $? 70 | 124 71 | ``` 72 | 73 | ## Community 74 | 75 | *Debian*: There is a [Debian package](https://tracker.debian.org/pkg/wait-for-it). 76 | -------------------------------------------------------------------------------- /controller/frame.controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | AppContext "context" 5 | "fmt" 6 | 7 | "log" 8 | "net/http" 9 | 10 | frame "github.com/amupxm/go-video-concat/interfaces/frame" 11 | "github.com/amupxm/go-video-concat/packages/s3" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/gofrs/uuid" 15 | "github.com/minio/minio-go/v7" 16 | ) 17 | 18 | // upload add 19 | 20 | func Frame_Upload(context *gin.Context) { 21 | file, err := context.FormFile("upload") 22 | // if no file contained 23 | if err != nil { 24 | context.JSON(http.StatusInternalServerError, gin.H{ 25 | "ok": false, 26 | "message": "invalid file input", 27 | }) 28 | return 29 | } 30 | 31 | uuid, _ := uuid.NewV4() 32 | 33 | fileIOReader, err := file.Open() 34 | 35 | if err != nil { 36 | context.JSON(http.StatusInternalServerError, gin.H{ 37 | "ok": false, 38 | "message": "file error", 39 | }) 40 | } 41 | var storage = &s3.ObjectStorage 42 | 43 | uploadInformation, err := storage.Client.PutObject( 44 | AppContext.Background(), 45 | "frame", 46 | uuid.String(), 47 | fileIOReader, 48 | file.Size, 49 | minio.PutObjectOptions{ContentType: file.Header.Get("Content-Type")}) 50 | if err != nil { 51 | context.JSON(http.StatusInternalServerError, gin.H{ 52 | "ok": false, 53 | "message": err, 54 | }) 55 | } 56 | log.Print(uploadInformation.LastModified.Second()) 57 | context.JSON(200, gin.H{ 58 | "ok": true, 59 | "message": "file uploaded successfully", 60 | "file_name": uuid.String(), 61 | }) 62 | } 63 | 64 | // add 65 | func Frame_Add(context *gin.Context) { 66 | var frame frame.Frame 67 | if err := context.ShouldBindJSON(&frame); err != nil { 68 | context.JSON(http.StatusNotAcceptable, gin.H{ 69 | "ok": false, 70 | "message": "invalid json structure", 71 | }) 72 | } 73 | status := frame.AddFrame() 74 | 75 | context.JSON(http.StatusAccepted, gin.H{ 76 | "ok": status, 77 | "data": frame, 78 | }) 79 | } 80 | 81 | // get all 82 | 83 | func Frame_list(context *gin.Context) { 84 | var frame frame.Frame 85 | frameList := frame.ListFrame() 86 | context.JSON(http.StatusAccepted, gin.H{ 87 | "ok": true, 88 | "data": frameList, 89 | }) 90 | } 91 | 92 | // TODO single view 93 | func Frame_Single(context *gin.Context) { 94 | frameCode := context.Param("code") 95 | var frame frame.Frame 96 | frameList, status := frame.FindFrame(frameCode) 97 | context.JSON(http.StatusAccepted, gin.H{ 98 | "ok": status, 99 | "data": frameList, 100 | }) 101 | } 102 | 103 | // TODO single file view 104 | func Frame_File(context *gin.Context) { 105 | frameCode := context.Param("code") 106 | var frame frame.Frame 107 | file, status := frame.GetFile(frameCode) 108 | stat, err := file.Stat() 109 | if err != nil || !status { 110 | context.JSON(http.StatusAccepted, gin.H{ 111 | "ok": false, 112 | }) 113 | } 114 | extraHeaders := map[string]string{} 115 | fmt.Print(stat) 116 | context.DataFromReader(http.StatusOK, stat.Size, stat.ContentType, file, extraHeaders) 117 | } 118 | 119 | // TODO remove 120 | -------------------------------------------------------------------------------- /controller/generator-controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | AppContext "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/amupxm/go-video-concat/models" 10 | "github.com/amupxm/go-video-concat/packages/cache" 11 | "github.com/amupxm/go-video-concat/packages/ffmpeg" 12 | "github.com/amupxm/go-video-concat/packages/s3" 13 | "github.com/gin-gonic/gin" 14 | "github.com/gofrs/uuid" 15 | "github.com/minio/minio-go/v7" 16 | ) 17 | 18 | func Generator_Upload(context *gin.Context) { 19 | file, err := context.FormFile("upload") 20 | // if no file contained 21 | if err != nil { 22 | context.JSON(http.StatusInternalServerError, gin.H{ 23 | "ok": false, 24 | "message": "invalid file input", 25 | }) 26 | return 27 | } 28 | 29 | uuid, _ := uuid.NewV4() 30 | 31 | fileIOReader, err := file.Open() 32 | 33 | if err != nil { 34 | context.JSON(http.StatusInternalServerError, gin.H{ 35 | "ok": false, 36 | "message": "file error", 37 | }) 38 | } 39 | var storage = &s3.ObjectStorage 40 | 41 | uploadInformation, err := storage.Client.PutObject( 42 | AppContext.Background(), 43 | "upload", 44 | uuid.String(), 45 | fileIOReader, 46 | file.Size, 47 | minio.PutObjectOptions{ContentType: file.Header.Get("Content-Type")}) 48 | if err != nil { 49 | context.JSON(http.StatusInternalServerError, gin.H{ 50 | "ok": false, 51 | "message": err, 52 | }) 53 | } 54 | log.Print(uploadInformation.LastModified.Second()) 55 | context.JSON(200, gin.H{ 56 | "ok": true, 57 | "message": "file uploaded successfully", 58 | "file_name": uuid.String(), 59 | }) 60 | 61 | } 62 | 63 | func Generator_Generate(context *gin.Context) { 64 | var recipe models.VideoRecipe 65 | if err := context.ShouldBindJSON(&recipe); err != nil { 66 | context.JSON(http.StatusNotAcceptable, gin.H{ 67 | "ok": false, 68 | "message": "invalid structure", 69 | }) 70 | fmt.Print(err) 71 | return 72 | } 73 | var f ffmpeg.FFmpeg_Generator 74 | f.Recipe = &recipe 75 | f.Error = &ffmpeg.FFmpeg_Message{} 76 | processId, _ := uuid.NewV4() 77 | f.UUID = processId.String() 78 | context.JSON(http.StatusAccepted, gin.H{ 79 | "ok": true, 80 | "message": "please wait", 81 | "code": f.UUID, 82 | }) 83 | go func() { 84 | ffmpeg.Generator(&f) 85 | }() 86 | } 87 | 88 | func Generator_Status(context *gin.Context) { 89 | code := context.Param("code") 90 | status, message := cache.GetStatus(code) 91 | context.JSON(200, gin.H{ 92 | "status": status, 93 | "message": message, 94 | }) 95 | } 96 | 97 | func Generator_file(context *gin.Context) { 98 | code := context.Param("code") 99 | var gen ffmpeg.FFmpeg_Generator 100 | file, _, status := gen.GetFromS3(code) 101 | if status != nil { 102 | log.Println(status) 103 | context.JSON(http.StatusAccepted, gin.H{ 104 | "ok": false, 105 | }) 106 | return 107 | } 108 | stat, err := file.Stat() 109 | if err != nil { 110 | context.JSON(http.StatusAccepted, gin.H{ 111 | "ok": false, 112 | }) 113 | return 114 | } 115 | defer file.Close() 116 | extraHeaders := map[string]string{} 117 | fmt.Print(stat) 118 | context.DataFromReader(http.StatusOK, stat.Size, stat.ContentType, file, extraHeaders) 119 | 120 | } 121 | -------------------------------------------------------------------------------- /packages/ffmpeg/core.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/amupxm/go-video-concat/interfaces/frame" 7 | "github.com/amupxm/go-video-concat/interfaces/splash" 8 | "github.com/amupxm/go-video-concat/models" 9 | "github.com/amupxm/go-video-concat/packages/cache" 10 | ) 11 | 12 | type ( 13 | FFmpeg_Audio struct { 14 | Format string 15 | Input string 16 | Output string 17 | Duration int 18 | } 19 | FFmpeg_Splash struct { 20 | Audio string 21 | Video string 22 | Output string 23 | } 24 | FFmpeg_Generator struct { 25 | Recipe *models.VideoRecipe 26 | Frame *frame.Frame 27 | Splash *splash.Splash 28 | Error *FFmpeg_Message 29 | UUID string 30 | Dir string 31 | } 32 | FFmpeg_Message struct { 33 | Status bool 34 | Message string 35 | UUID *string 36 | } 37 | FFmpegInterface interface { 38 | Generator(f *FFmpeg_Generator, responseChannel chan *FFmpeg_Message) 39 | GenerateSplash(splash *splash.Splash) *FFmpeg_Message 40 | img2vid(index int, wg *sync.WaitGroup) 41 | trimVid(index int, wg *sync.WaitGroup) 42 | CreateSplash(splash FFmpeg_Splash) 43 | GetSplashFile(wg *sync.WaitGroup) 44 | GetFRameFile(wg *sync.WaitGroup) 45 | TrimAudio(audio FFmpeg_Audio) 46 | Execute(args ...string) error 47 | MakeEvenNumber(i int) int 48 | ValidateSplash() 49 | GenerateChunks() 50 | ResponseError() 51 | DownloadFiles() 52 | FitSplash() 53 | Overlay() 54 | Concat() 55 | TmpDir() 56 | } 57 | ) 58 | 59 | func Generator(f *FFmpeg_Generator) { 60 | // init redis status 61 | cache.NewProccess(f.UUID, len(f.Recipe.Chunks)) 62 | f.Error.Status = false 63 | f.Error.Message = "" 64 | 65 | // validate frame 66 | cache.UpdateStatus(f.UUID, "validate frame", true) 67 | f.ValidateFrame() 68 | if f.Error.Status { 69 | f.ResponseError() 70 | return 71 | } 72 | // validate splash 73 | cache.UpdateStatus(f.UUID, "validate Splash", true) 74 | f.ValidateSplash() 75 | if f.Error.Status { 76 | f.ResponseError() 77 | return 78 | } 79 | 80 | // create tmp file 81 | cache.UpdateStatus(f.UUID, "init directory", true) 82 | 83 | f.TmpDir("init") 84 | if f.Error.Status { 85 | f.ResponseError() 86 | return 87 | } 88 | 89 | // download files 90 | cache.UpdateStatus(f.UUID, "download files", true) 91 | f.DownloadFiles() 92 | if f.Error.Status { 93 | f.ResponseError() 94 | return 95 | } 96 | 97 | // get splash and frame file 98 | cache.UpdateStatus(f.UUID, "connecting to s3", true) 99 | var fileControllerWaitGroup sync.WaitGroup 100 | fileControllerWaitGroup.Add(2) 101 | go f.GetSplashFile(&fileControllerWaitGroup) 102 | go f.GetFrameFile(&fileControllerWaitGroup) 103 | fileControllerWaitGroup.Wait() 104 | if f.Error.Status { 105 | f.ResponseError() 106 | return 107 | } 108 | // make splash in correct size 109 | cache.UpdateStatus(f.UUID, "generate splash", true) 110 | f.FitSplash() 111 | // trim video chunks in correct duration 112 | cache.UpdateStatus(f.UUID, "prepare", true) 113 | f.GenerateChunks() 114 | // connect chunks (+ sound overlay) 115 | cache.UpdateStatus(f.UUID, "concatting", true) 116 | f.Concat() 117 | //overLay 118 | cache.UpdateStatus(f.UUID, "overlaying", true) 119 | f.OverLay() 120 | cache.UpdateStatus(f.UUID, "done", true) 121 | f.WriteTos3() 122 | defer f.SendResponseCallback() 123 | } 124 | -------------------------------------------------------------------------------- /controller/splash-controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | AppContext "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/amupxm/go-video-concat/interfaces/splash" 10 | "github.com/amupxm/go-video-concat/packages/ffmpeg" 11 | "github.com/amupxm/go-video-concat/packages/s3" 12 | "github.com/gin-gonic/gin" 13 | "github.com/gofrs/uuid" 14 | "github.com/minio/minio-go/v7" 15 | ) 16 | 17 | func Splash_Audio(context *gin.Context) { 18 | file, err := context.FormFile("upload") 19 | // if no file contained 20 | if err != nil { 21 | context.JSON(http.StatusInternalServerError, gin.H{ 22 | "ok": false, 23 | "message": "invalid file input", 24 | }) 25 | return 26 | } 27 | 28 | uuid, _ := uuid.NewV4() 29 | 30 | fileIOReader, err := file.Open() 31 | 32 | if err != nil { 33 | context.JSON(http.StatusInternalServerError, gin.H{ 34 | "ok": false, 35 | "message": "file error", 36 | }) 37 | } 38 | var storage = &s3.ObjectStorage 39 | 40 | uploadInformation, err := storage.Client.PutObject( 41 | AppContext.Background(), 42 | "splash-audio", 43 | uuid.String(), 44 | fileIOReader, 45 | file.Size, 46 | minio.PutObjectOptions{ContentType: file.Header.Get("Content-Type")}) 47 | if err != nil { 48 | context.JSON(http.StatusInternalServerError, gin.H{ 49 | "ok": false, 50 | "message": err, 51 | }) 52 | } 53 | log.Print(uploadInformation.LastModified.Second()) 54 | context.JSON(200, gin.H{ 55 | "ok": true, 56 | "message": "file uploaded successfully", 57 | "file_name": uuid.String(), 58 | }) 59 | } 60 | 61 | func Splash_Base(context *gin.Context) { 62 | file, err := context.FormFile("upload") 63 | // if no file contained 64 | if err != nil { 65 | context.JSON(http.StatusInternalServerError, gin.H{ 66 | "ok": false, 67 | "message": "invalid file input", 68 | }) 69 | return 70 | } 71 | 72 | uuid, _ := uuid.NewV4() 73 | 74 | fileIOReader, err := file.Open() 75 | 76 | if err != nil { 77 | context.JSON(http.StatusInternalServerError, gin.H{ 78 | "ok": false, 79 | "message": "file error", 80 | }) 81 | } 82 | var storage = &s3.ObjectStorage 83 | 84 | uploadInformation, err := storage.Client.PutObject( 85 | AppContext.Background(), 86 | "splash-base", 87 | uuid.String(), 88 | fileIOReader, 89 | file.Size, 90 | minio.PutObjectOptions{ContentType: file.Header.Get("Content-Type")}) 91 | if err != nil { 92 | context.JSON(http.StatusInternalServerError, gin.H{ 93 | "ok": false, 94 | "message": err, 95 | }) 96 | } 97 | log.Print(uploadInformation.LastModified.Second()) 98 | context.JSON(200, gin.H{ 99 | "ok": true, 100 | "message": "file uploaded successfully", 101 | "file_name": uuid.String(), 102 | }) 103 | } 104 | 105 | func Splash_Add(context *gin.Context) { 106 | var Splash splash.Splash 107 | 108 | if err := context.ShouldBindJSON(&Splash); err != nil { 109 | context.JSON(http.StatusNotAcceptable, gin.H{ 110 | "ok": false, 111 | "message": "invalid requires structure", 112 | }) 113 | return 114 | } 115 | 116 | uuid, _ := uuid.NewV4() 117 | 118 | status, message := Splash.AddSplash(uuid.String()) 119 | context.JSON(http.StatusAccepted, gin.H{ 120 | "ok": status, 121 | "message": message, 122 | "code": uuid.String(), 123 | }) 124 | // now should start proccessing 125 | res := ffmpeg.GenerateSplash(&Splash) 126 | fmt.Print(res) 127 | 128 | } 129 | 130 | func Splash_file(context *gin.Context) { 131 | splashCode := context.Param("code") 132 | var splash splash.Splash 133 | file, status := splash.GetFile(splashCode) 134 | stat, err := file.Stat() 135 | if err != nil || !status { 136 | context.JSON(http.StatusAccepted, gin.H{ 137 | "ok": false, 138 | }) 139 | } 140 | extraHeaders := map[string]string{} 141 | context.DataFromReader(http.StatusOK, stat.Size, stat.ContentType, file, extraHeaders) 142 | } 143 | -------------------------------------------------------------------------------- /transcoder/ffmpeg/options.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | // Options defines allowed FFmpeg arguments 9 | type Options struct { 10 | Aspect *string `flag:"-aspect"` 11 | Resolution *string `flag:"-s"` 12 | VideoBitRate *string `flag:"-b:v"` 13 | VideoBitRateTolerance *int `flag:"-bt"` 14 | VideoMaxBitRate *int `flag:"-maxrate"` 15 | VideoMinBitrate *int `flag:"-minrate"` 16 | VideoCodec *string `flag:"-c:v"` 17 | Vframes *int `flag:"-vframes"` 18 | FrameRate *int `flag:"-r"` 19 | AudioRate *int `flag:"-ar"` 20 | KeyframeInterval *int `flag:"-g"` 21 | AudioCodec *string `flag:"-c:a"` 22 | AudioBitrate *string `flag:"-ab"` 23 | AudioChannels *int `flag:"-ac"` 24 | AudioVariableBitrate *bool `flag:"-q:a"` 25 | BufferSize *int `flag:"-bufsize"` 26 | Threadset *bool `flag:"-threads"` 27 | Threads *int `flag:"-threads"` 28 | Preset *string `flag:"-preset"` 29 | Tune *string `flag:"-tune"` 30 | AudioProfile *string `flag:"-profile:a"` 31 | VideoProfile *string `flag:"-profile:v"` 32 | Target *string `flag:"-target"` 33 | Duration *string `flag:"-t"` 34 | Qscale *uint32 `flag:"-qscale"` 35 | Crf *uint32 `flag:"-crf"` 36 | Strict *int `flag:"-strict"` 37 | MuxDelay *string `flag:"-muxdelay"` 38 | SeekTime *string `flag:"-ss"` 39 | SeekUsingTimestamp *bool `flag:"-seek_timestamp"` 40 | MovFlags *string `flag:"-movflags"` 41 | HideBanner *bool `flag:"-hide_banner"` 42 | OutputFormat *string `flag:"-f"` 43 | CopyTs *bool `flag:"-copyts"` 44 | NativeFramerateInput *bool `flag:"-re"` 45 | InputInitialOffset *string `flag:"-itsoffset"` 46 | RtmpLive *string `flag:"-rtmp_live"` 47 | HlsPlaylistType *string `flag:"-hls_playlist_type"` 48 | HlsListSize *int `flag:"-hls_list_size"` 49 | HlsSegmentDuration *int `flag:"-hls_time"` 50 | HlsMasterPlaylistName *string `flag:"-master_pl_name"` 51 | HlsSegmentFilename *string `flag:"-hls_segment_filename"` 52 | HTTPMethod *string `flag:"-method"` 53 | HTTPKeepAlive *bool `flag:"-multiple_requests"` 54 | Hwaccel *string `flag:"-hwaccel"` 55 | StreamIds map[string]string `flag:"-streamid"` 56 | VideoFilter *string `flag:"-vf"` 57 | AudioFilter *string `flag:"-af"` 58 | SkipVideo *bool `flag:"-vn"` 59 | SkipAudio *bool `flag:"-an"` 60 | CompressionLevel *int `flag:"-compression_level"` 61 | MapMetadata *string `flag:"-map_metadata"` 62 | Metadata map[string]string `flag:"-metadata"` 63 | EncryptionKey *string `flag:"-hls_key_info_file"` 64 | Bframe *int `flag:"-bf"` 65 | PixFmt *string `flag:"-pix_fmt"` 66 | WhiteListProtocols []string `flag:"-protocol_whitelist"` 67 | Overwrite *bool `flag:"-y"` 68 | Shortest *bool `flag:"-shortest"` 69 | MapVideo *string `flag:"-map"` 70 | MapAudio *string `flag:"-map"` 71 | ExtraArgs map[string]interface{} 72 | } 73 | 74 | // GetStrArguments ... 75 | func (opts Options) GetStrArguments() []string { 76 | f := reflect.TypeOf(opts) 77 | v := reflect.ValueOf(opts) 78 | 79 | values := []string{} 80 | 81 | for i := 0; i < f.NumField(); i++ { 82 | flag := f.Field(i).Tag.Get("flag") 83 | value := v.Field(i).Interface() 84 | 85 | if !v.Field(i).IsNil() { 86 | 87 | if _, ok := value.(*bool); ok { 88 | values = append(values, flag) 89 | } 90 | 91 | if vs, ok := value.(*string); ok { 92 | values = append(values, flag, *vs) 93 | } 94 | 95 | if va, ok := value.([]string); ok { 96 | 97 | for i := 0; i < len(va); i++ { 98 | item := va[i] 99 | values = append(values, flag, item) 100 | } 101 | } 102 | 103 | if vm, ok := value.(map[string]interface{}); ok { 104 | for k, v := range vm { 105 | values = append(values, k, fmt.Sprintf("%v", v)) 106 | } 107 | } 108 | 109 | if vi, ok := value.(*int); ok { 110 | values = append(values, flag, fmt.Sprintf("%d", *vi)) 111 | } 112 | 113 | } 114 | } 115 | 116 | return values 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![LinkedIn][linkedin-shield]][linkedin-url] 3 | 4 | 5 | 6 | 7 |
8 |

9 | 10 | Logo 11 | 12 | 13 |

simple Golang video concat api

14 | 15 |

16 | Extremely fast API for editing social media suitable contents. 17 |
18 | 19 |

20 |

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ## Getting Started 29 | 30 | Just start app with docker compsoe. 31 | ### Prerequisites 32 | 33 | if you want to serve app outside of dockerd use script below : 34 | * debian based linux : 35 | ```sh 36 | apt-get update && apt-get install -y \ 37 | ffmpeg \ 38 | libmediainfo-dev \ 39 | zlib* \ 40 | gcc \ 41 | go && rm -rf /var/lib/apt/lists/* 42 | go get . && go build -v 43 | ``` 44 | * openSUSE : 45 | ```sh 46 | zypper in -y \ 47 | libmediainfo-devel\ 48 | zlib-devel\ 49 | zlib*\ 50 | ffmpeg\ 51 | gcc \ 52 | go && rm -rf /var/lib/apt/lists/* 53 | go get . && go build -v 54 | ``` 55 | ### Usage 56 | 57 | #### 1. add frame : 58 | A frame is an image which your video will centered inside it. 59 | for example : 60 | 61 |
62 |

63 | Logo 64 |

65 | 66 | First simply upload your frame to s3 : 67 | ```sh 68 | curl --location --request POST 'localhost:8080/frame/upload' \ 69 | --form 'upload=@"cover.png"' 70 | ``` 71 | Which will return an unique uuid code. Then send post requst to `localhost:8080/frame` like example : 72 | 73 | ``` json 74 | { 75 | "name" : "template name" , 76 | "file_code" : "8a19d34e-ce11-4730-8853-9b2ec3a21ec4", 77 | "height": 303, 78 | "width": 540, 79 | "start_offset": 300 80 | } 81 | ``` 82 | Start_offset is Y coordination of template.This request will return an uuid code.You can use below end-point to ger frames: 83 | 84 | ``` sh 85 | # get list of frames : 86 | curl --location --request GET 'localhost:8080/frame' 87 | 88 | # get single frame information (height , offests and ...) : 89 | curl --location --request GET 'localhost:8080/frame/:UUID' 90 | 91 | # get frame file 92 | curl --location --request GET 'localhost:8080/frame/:UUID/file' 93 | ``` 94 | 95 | 96 | #### 2. Create splash 97 | A splash is a required short video (something like a logo) which shown in start of your video.You can merge any video with any voice ( or music). 98 | 99 | ``` sh 100 | # add base file ( video file ) 101 | curl --location --request POST 'localhost:8080/splash/base' \ 102 | --form 'upload=@"video.mp4"' 103 | 104 | # add voice (music file) 105 | curl --location --request POST 'localhost:8080/splash/audio' \ 106 | --form 'upload=@"video.mp3"' 107 | ``` 108 | Then merge them together : 109 | 110 | ``` json 111 | { 112 | "base_file": "c7d6d339-6668-42a8-b569-da7ab101e5a6", 113 | "base_audio": "db20ab4d-59e1-45fb-a9c5-0df2b4603d0e", 114 | "name": "splash for test", 115 | "max_length": 7 , 116 | "base_color" : "#FF6600" 117 | } 118 | ``` 119 | 120 | In this example max_length is duration of your splash file. It will generate empty file parts if the max_length be longer than video or audio length.The base_color will be a background for splash if you use transparent videos or when you are using splash file in different alignment frame size. 121 |
122 | You can check generated splash file using `localhost:8080/splash/:code` end point. 123 | 124 | 125 | #### 3. Concator: 126 | Concator is a process include merge / trim and overlaying media contents on frame.Like below example : 127 | 128 | ``` json 129 | { 130 | "video_name": "video name", 131 | "sound_level": 70, 132 | "call_back_address": "https://google.com", 133 | "splash_file": "8fe439c1-8f15-4950-8c52-48838acedbdd", 134 | "frame_code": "8a19d34e-ce11-4730-8853-9b2ec3a21ec4", 135 | "audio_file":"http://localhost:8081/voice.mp3", 136 | "has_audio" : true , 137 | "inputs": [ 138 | { 139 | "type": "image", 140 | "path": "http://localhost:8081/image.png", 141 | "start": 0, 142 | "end": 2 143 | }, 144 | { 145 | "type": "video", 146 | "path": "http://localhost:8081/video.mp4", 147 | "start": 2, 148 | "end": 10 149 | } 150 | ] 151 | } 152 | ``` 153 | This request will make a video with 17 seconds length (2'' image + 8'' video + 7'' splash [step 2]), Then overlay audio file with 70% sound level on it and place it inside of the frame. This could be a long process so it will call your call_back url , when it's done. 154 | 155 |
156 | 157 | You can use `localhost:8080/generator/:code` to check process percentage and current status. 158 | 159 | 160 | 161 | ## Contributing 162 | 163 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**. 164 | 165 | 1. Fork the Project 166 | 2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) 167 | 3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) 168 | 4. Push to the Branch (`git push origin feature/AmazingFeature`) 169 | 5. Open a Pull Request 170 | 171 | 172 | 173 | 174 | ## License 175 | 176 | Distributed under the MIT License. See `LICENSE` for more information. 177 | 178 | 179 | 180 | 181 | ## Contact 182 | 183 | Amir Hossein Mokaramifar - [@amupxm](https://twitter.com/amupxm) - amupxm@gmail.com 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=for-the-badge&logo=linkedin&colorB=555 193 | [linkedin-url]: https://www.linkedin.com/in/mokaramifar/ 194 | [product-screenshot]: images/screenshot.png 195 | -------------------------------------------------------------------------------- /wait-for-it/wait-for-it.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Use this script to test if a given TCP host/port are available 3 | 4 | WAITFORIT_cmdname=${0##*/} 5 | 6 | echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } 7 | 8 | usage() 9 | { 10 | cat << USAGE >&2 11 | Usage: 12 | $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] 13 | -h HOST | --host=HOST Host or IP under test 14 | -p PORT | --port=PORT TCP port under test 15 | Alternatively, you specify the host and port as host:port 16 | -s | --strict Only execute subcommand if the test succeeds 17 | -q | --quiet Don't output any status messages 18 | -t TIMEOUT | --timeout=TIMEOUT 19 | Timeout in seconds, zero for no timeout 20 | -- COMMAND ARGS Execute command with args after the test finishes 21 | USAGE 22 | exit 1 23 | } 24 | 25 | wait_for() 26 | { 27 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 28 | echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 29 | else 30 | echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" 31 | fi 32 | WAITFORIT_start_ts=$(date +%s) 33 | while : 34 | do 35 | if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then 36 | nc -z $WAITFORIT_HOST $WAITFORIT_PORT 37 | WAITFORIT_result=$? 38 | else 39 | (echo -n > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 40 | WAITFORIT_result=$? 41 | fi 42 | if [[ $WAITFORIT_result -eq 0 ]]; then 43 | WAITFORIT_end_ts=$(date +%s) 44 | echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" 45 | break 46 | fi 47 | sleep 1 48 | done 49 | return $WAITFORIT_result 50 | } 51 | 52 | wait_for_wrapper() 53 | { 54 | # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 55 | if [[ $WAITFORIT_QUIET -eq 1 ]]; then 56 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 57 | else 58 | timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & 59 | fi 60 | WAITFORIT_PID=$! 61 | trap "kill -INT -$WAITFORIT_PID" INT 62 | wait $WAITFORIT_PID 63 | WAITFORIT_RESULT=$? 64 | if [[ $WAITFORIT_RESULT -ne 0 ]]; then 65 | echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" 66 | fi 67 | return $WAITFORIT_RESULT 68 | } 69 | 70 | # process arguments 71 | while [[ $# -gt 0 ]] 72 | do 73 | case "$1" in 74 | *:* ) 75 | WAITFORIT_hostport=(${1//:/ }) 76 | WAITFORIT_HOST=${WAITFORIT_hostport[0]} 77 | WAITFORIT_PORT=${WAITFORIT_hostport[1]} 78 | shift 1 79 | ;; 80 | --child) 81 | WAITFORIT_CHILD=1 82 | shift 1 83 | ;; 84 | -q | --quiet) 85 | WAITFORIT_QUIET=1 86 | shift 1 87 | ;; 88 | -s | --strict) 89 | WAITFORIT_STRICT=1 90 | shift 1 91 | ;; 92 | -h) 93 | WAITFORIT_HOST="$2" 94 | if [[ $WAITFORIT_HOST == "" ]]; then break; fi 95 | shift 2 96 | ;; 97 | --host=*) 98 | WAITFORIT_HOST="${1#*=}" 99 | shift 1 100 | ;; 101 | -p) 102 | WAITFORIT_PORT="$2" 103 | if [[ $WAITFORIT_PORT == "" ]]; then break; fi 104 | shift 2 105 | ;; 106 | --port=*) 107 | WAITFORIT_PORT="${1#*=}" 108 | shift 1 109 | ;; 110 | -t) 111 | WAITFORIT_TIMEOUT="$2" 112 | if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi 113 | shift 2 114 | ;; 115 | --timeout=*) 116 | WAITFORIT_TIMEOUT="${1#*=}" 117 | shift 1 118 | ;; 119 | --) 120 | shift 121 | WAITFORIT_CLI=("$@") 122 | break 123 | ;; 124 | --help) 125 | usage 126 | ;; 127 | *) 128 | echoerr "Unknown argument: $1" 129 | usage 130 | ;; 131 | esac 132 | done 133 | 134 | if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then 135 | echoerr "Error: you need to provide a host and port to test." 136 | usage 137 | fi 138 | 139 | WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} 140 | WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} 141 | WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} 142 | WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} 143 | 144 | # Check to see if timeout is from busybox? 145 | WAITFORIT_TIMEOUT_PATH=$(type -p timeout) 146 | WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) 147 | 148 | WAITFORIT_BUSYTIMEFLAG="" 149 | if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then 150 | WAITFORIT_ISBUSY=1 151 | # Check if busybox timeout uses -t flag 152 | # (recent Alpine versions don't support -t anymore) 153 | if timeout &>/dev/stdout | grep -q -e '-t '; then 154 | WAITFORIT_BUSYTIMEFLAG="-t" 155 | fi 156 | else 157 | WAITFORIT_ISBUSY=0 158 | fi 159 | 160 | if [[ $WAITFORIT_CHILD -gt 0 ]]; then 161 | wait_for 162 | WAITFORIT_RESULT=$? 163 | exit $WAITFORIT_RESULT 164 | else 165 | if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then 166 | wait_for_wrapper 167 | WAITFORIT_RESULT=$? 168 | else 169 | wait_for 170 | WAITFORIT_RESULT=$? 171 | fi 172 | fi 173 | 174 | if [[ $WAITFORIT_CLI != "" ]]; then 175 | if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then 176 | echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" 177 | exit $WAITFORIT_RESULT 178 | fi 179 | exec "${WAITFORIT_CLI[@]}" 180 | else 181 | exit $WAITFORIT_RESULT 182 | fi 183 | -------------------------------------------------------------------------------- /wait-for-it/test/wait-for-it.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import unittest 4 | import shlex 5 | from subprocess import Popen, PIPE 6 | import os 7 | import sys 8 | import socket 9 | import re 10 | 11 | MISSING_ARGS_TEXT = "Error: you need to provide a host and port to test." 12 | HELP_TEXT = "Usage:" # Start of help text 13 | DIVIDE_LINE = '-'*71 # Output line of dashes 14 | 15 | 16 | class TestWaitForIt(unittest.TestCase): 17 | """ 18 | TestWaitForIt tests the wait-for-it.sh shell script. 19 | The wait-for-it.sh script is assumed to be in the parent directory to 20 | the test script. 21 | """ 22 | 23 | def execute(self, cmd): 24 | """Executes a command and returns exit code, STDOUT, STDERR""" 25 | args = shlex.split(cmd) 26 | proc = Popen(args, stdout=PIPE, stderr=PIPE) 27 | out, err = proc.communicate() 28 | exitcode = proc.returncode 29 | return exitcode, out.decode('utf-8'), err.decode('utf-8') 30 | 31 | def open_local_port(self, timeout=5): 32 | s = socket.socket() 33 | s.bind(('', 0)) 34 | s.listen(timeout) 35 | return s, s.getsockname()[1] 36 | 37 | def check_args(self, args, stdout_regex, stderr_regex, should_succeed): 38 | command = self.wait_script + " " + args 39 | exitcode, out, err = self.execute(command) 40 | 41 | # Check stderr 42 | msg = ("Failed check that STDERR:\n" + 43 | DIVIDE_LINE + "\n" + err + "\n" + DIVIDE_LINE + 44 | "\nmatches:\n" + 45 | DIVIDE_LINE + "\n" + stderr_regex + "\n" + DIVIDE_LINE) 46 | self.assertIsNotNone(re.match(stderr_regex, err, re.DOTALL), msg) 47 | 48 | # Check STDOUT 49 | msg = ("Failed check that STDOUT:\n" + 50 | DIVIDE_LINE + "\n" + out + "\n" + DIVIDE_LINE + 51 | "\nmatches:\n" + 52 | DIVIDE_LINE + "\n" + stdout_regex + "\n" + DIVIDE_LINE) 53 | self.assertIsNotNone(re.match(stdout_regex, out, re.DOTALL), msg) 54 | 55 | # Check exit code 56 | self.assertEqual(should_succeed, exitcode == 0) 57 | 58 | def setUp(self): 59 | script_path = os.path.dirname(sys.argv[0]) 60 | parent_path = os.path.abspath(os.path.join(script_path, os.pardir)) 61 | self.wait_script = os.path.join(parent_path, "wait-for-it.sh") 62 | 63 | def test_no_args(self): 64 | """ 65 | Check that no aruments returns the missing args text and the 66 | correct return code 67 | """ 68 | self.check_args( 69 | "", 70 | "^$", 71 | MISSING_ARGS_TEXT, 72 | False 73 | ) 74 | # Return code should be 1 when called with no args 75 | exitcode, out, err = self.execute(self.wait_script) 76 | self.assertEqual(exitcode, 1) 77 | 78 | def test_help(self): 79 | """ Check that help text is printed with --help argument """ 80 | self.check_args( 81 | "--help", 82 | "", 83 | HELP_TEXT, 84 | False 85 | ) 86 | 87 | def test_no_port(self): 88 | """ Check with missing port argument """ 89 | self.check_args( 90 | "--host=localhost", 91 | "", 92 | MISSING_ARGS_TEXT, 93 | False 94 | ) 95 | 96 | def test_no_host(self): 97 | """ Check with missing hostname argument """ 98 | self.check_args( 99 | "--port=80", 100 | "", 101 | MISSING_ARGS_TEXT, 102 | False 103 | ) 104 | 105 | def test_host_port(self): 106 | """ Check that --host and --port args work correctly """ 107 | soc, port = self.open_local_port() 108 | self.check_args( 109 | "--host=localhost --port={0} --timeout=1".format(port), 110 | "", 111 | "wait-for-it.sh: waiting 1 seconds for localhost:{0}".format(port), 112 | True 113 | ) 114 | soc.close() 115 | 116 | def test_combined_host_port(self): 117 | """ 118 | Tests that wait-for-it.sh returns correctly after establishing a 119 | connectionm using combined host and ports 120 | """ 121 | soc, port = self.open_local_port() 122 | self.check_args( 123 | "localhost:{0} --timeout=1".format(port), 124 | "", 125 | "wait-for-it.sh: waiting 1 seconds for localhost:{0}".format(port), 126 | True 127 | ) 128 | soc.close() 129 | 130 | 131 | def test_port_failure_with_timeout(self): 132 | """ 133 | Note exit status of 124 is exected, passed from the timeout command 134 | """ 135 | self.check_args( 136 | "localhost:8929 --timeout=1", 137 | "", 138 | ".*timeout occurred after waiting 1 seconds for localhost:8929", 139 | False 140 | ) 141 | 142 | def test_command_execution(self): 143 | """ 144 | Checks that a command executes correctly after a port test passes 145 | """ 146 | soc, port = self.open_local_port() 147 | self.check_args( 148 | "localhost:{0} -- echo \"CMD OUTPUT\"".format(port), 149 | "CMD OUTPUT", 150 | ".*wait-for-it.sh: localhost:{0} is available after 0 seconds".format(port), 151 | True 152 | ) 153 | soc.close() 154 | 155 | def test_failed_command_execution(self): 156 | """ 157 | Check command failure. The command in question outputs STDERR and 158 | an exit code of 2 159 | """ 160 | soc, port = self.open_local_port() 161 | self.check_args( 162 | "localhost:{0} -- ls not_real_file".format(port), 163 | "", 164 | ".*No such file or directory\n", 165 | False 166 | ) 167 | soc.close() 168 | 169 | def test_command_after_connection_failure(self): 170 | """ 171 | Test that a command still runs even if a connection times out 172 | and that the return code is correct for the comand being run 173 | """ 174 | self.check_args( 175 | "localhost:8929 --timeout=1 -- echo \"CMD OUTPUT\"", 176 | "CMD OUTPUT", 177 | ".*timeout occurred after waiting 1 seconds for localhost:8929", 178 | True 179 | ) 180 | 181 | if __name__ == '__main__': 182 | unittest.main() 183 | -------------------------------------------------------------------------------- /packages/ffmpeg/tools.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "log" 9 | "net/http" 10 | "sync" 11 | 12 | filemanager "github.com/amupxm/go-video-concat/interfaces/file-manager" 13 | "github.com/amupxm/go-video-concat/interfaces/frame" 14 | "github.com/amupxm/go-video-concat/interfaces/splash" 15 | "github.com/amupxm/go-video-concat/packages/cache" 16 | 17 | mediainfo "github.com/amupxm/go-video-concat/packages/mediaInfo" 18 | "github.com/amupxm/go-video-concat/packages/s3" 19 | "github.com/gofrs/uuid" 20 | "github.com/minio/minio-go/v7" 21 | ) 22 | 23 | func (f *FFmpeg_Generator) GenerateChunks() { 24 | var wg sync.WaitGroup 25 | wg.Add(len(f.Recipe.Chunks)) 26 | for chunkIndex := range f.Recipe.Chunks { 27 | switch f.Recipe.Chunks[chunkIndex].Type { 28 | case "image": 29 | go f.img2vid(chunkIndex, &wg) 30 | case "video": 31 | go f.trimVid(chunkIndex, &wg) 32 | } 33 | } 34 | wg.Wait() 35 | } 36 | 37 | // add color back ground as same size as frame requirements for Splash and place it in center. 38 | func (f *FFmpeg_Generator) FitSplash() { 39 | var splashMediaInfo mediainfo.MediaInfoStruct 40 | err := splashMediaInfo.GetVideoInfo(f.Dir + "splash-file") 41 | if err != nil { 42 | f.Error.Status = err == nil 43 | f.Error.Message = "mediaInfo splash error" 44 | return 45 | } 46 | splashCorrectionOperation := []string{ 47 | "-i", 48 | f.Dir + "splash-file", 49 | "-f", 50 | "lavfi", 51 | "-i", 52 | fmt.Sprintf("color=red:s=%dx%d", MakeEvenNumber(f.Frame.Width), MakeEvenNumber(f.Frame.Height)), 53 | "-filter_complex", 54 | fmt.Sprintf("[1]scale=%d:-1[inner];[inner][0]overlay=%d:0:shortest=1[out]", MakeEvenNumber(f.Frame.Width), MakeEvenNumber((f.Frame.Width-int(splashMediaInfo.Width))/2)), 55 | "-map", 56 | "[out]:v", 57 | "-map", 58 | "0:a", 59 | f.Dir + "splash.final.mp4", 60 | } 61 | err = Execute(splashCorrectionOperation...) 62 | if err != nil { 63 | f.Error.Status = err == nil 64 | f.Error.Message = "mediaInfo splash error" 65 | } 66 | } 67 | 68 | // get splash file using splash from s3 and write to tmp dir 69 | func (f *FFmpeg_Generator) GetSplashFile(wg *sync.WaitGroup) { 70 | var objectStorage = &s3.ObjectStorage 71 | err := objectStorage.Client.FGetObject( 72 | context.Background(), 73 | "splash", 74 | f.Recipe.Splash, 75 | f.Dir+"splash-file", 76 | minio.GetObjectOptions{}) 77 | if err != nil { 78 | f.Error.Status = err == nil 79 | f.Error.Message = "object splash error" 80 | } 81 | wg.Done() 82 | } 83 | 84 | // download all files (chunk + external audio id exists) and write to tmp dir. 85 | func (f *FFmpeg_Generator) DownloadFiles() { 86 | var wg sync.WaitGroup 87 | 88 | // add extra to wait group if has external audio (because it will download audio as external file from url) 89 | if f.Recipe.ExternalAudio { 90 | wg.Add(len(f.Recipe.Chunks) + 1) 91 | 92 | } else { 93 | wg.Add(len(f.Recipe.Chunks)) 94 | } 95 | 96 | innerResponseChannel := make(chan *FFmpeg_Message) 97 | 98 | for chunkIndex := range f.Recipe.Chunks { 99 | go func(chunkIndex int) { 100 | // set a uuid as name for chunk (audio or video file) 101 | if newChunkName, err := uuid.NewV4(); err == nil { 102 | f.Recipe.Chunks[chunkIndex].Name = newChunkName.String() 103 | } else { 104 | innerResponseChannel <- &FFmpeg_Message{ 105 | Status: false, 106 | Message: "internal error", 107 | } 108 | } 109 | 110 | status, statusCode := filemanager.DownloadFile( 111 | &wg, 112 | f.Recipe.Chunks[chunkIndex].Url, 113 | f.Dir+f.Recipe.Chunks[chunkIndex].Name) 114 | innerResponseChannel <- &FFmpeg_Message{ 115 | Status: status, 116 | Message: "url responded withCode" + statusCode, 117 | } 118 | }(chunkIndex) 119 | 120 | } 121 | // download audio if exists 122 | if f.Recipe.ExternalAudio { 123 | go func() { 124 | audioName, _ := uuid.NewV4() 125 | filemanager.DownloadFile( 126 | &wg, 127 | f.Recipe.Audio, 128 | f.Dir+audioName.String()) 129 | f.Recipe.Audio = audioName.String() 130 | }() 131 | 132 | } 133 | wg.Wait() 134 | 135 | isAllDownloaded := true 136 | count := 0 137 | for responseFromInnerChannel := range innerResponseChannel { 138 | isAllDownloaded = isAllDownloaded && responseFromInnerChannel.Status 139 | log.Println("Downloaded to : " + f.Dir + f.Recipe.Chunks[count].Name) 140 | count++ 141 | if count == len(f.Recipe.Chunks) { 142 | close(innerResponseChannel) 143 | } 144 | } 145 | 146 | if !isAllDownloaded { 147 | f.Error.Status = false 148 | f.Error.Message = "error while downloading files" 149 | } 150 | } 151 | 152 | // get frame object from s3 and write to temp dir 153 | func (f *FFmpeg_Generator) GetFrameFile(wg *sync.WaitGroup) { 154 | var objectStorage = &s3.ObjectStorage 155 | err := objectStorage.Client.FGetObject( 156 | context.Background(), 157 | "frame", 158 | f.Frame.FileCode, 159 | f.Dir+f.Recipe.FrameCode, 160 | minio.GetObjectOptions{}) 161 | if err != nil { 162 | f.Error.Status = false 163 | f.Error.Message = "s3 error" 164 | } 165 | wg.Done() 166 | 167 | } 168 | 169 | // send FFMPEG_MESSAGE to response channel. use to send errors to api 170 | func (f *FFmpeg_Generator) ResponseError() { 171 | cache.UpdateStatus(f.UUID, f.Error.Message, false) 172 | } 173 | 174 | // get Splash details from database and validate it. 175 | // set error on invalid frame. 176 | func (f *FFmpeg_Generator) ValidateSplash() { 177 | var splashFinder splash.Splash 178 | tmpSplash, status := splashFinder.FindSplash(f.Recipe.Splash) 179 | f.Error.Status = !status 180 | f.Error.Message = "splash file error" 181 | f.Splash = &tmpSplash 182 | } 183 | 184 | // get frame details from database and validate it. 185 | // set error on invalid frame. 186 | func (f *FFmpeg_Generator) ValidateFrame() { 187 | var frameFinder frame.Frame 188 | tmpFrame, status := frameFinder.FindFrame(f.Recipe.FrameCode) 189 | f.Error.Status = !status 190 | f.Error.Message = "template file error" 191 | f.Frame = &tmpFrame 192 | } 193 | 194 | // modify Temp directory 195 | // with operation (init) 196 | func (f *FFmpeg_Generator) TmpDir(operation string) { 197 | fm := filemanager.Temperory{DirName: f.UUID} 198 | if operation == "init" { 199 | f.Dir = "/tmp/" + f.UUID + "/" 200 | err := fm.CreateTempDir() 201 | if err != nil { 202 | f.Error.Status = false 203 | f.Error.Message = "file operation error" 204 | } 205 | } 206 | } 207 | 208 | func (f *FFmpeg_Generator) GetFromS3(code string) (*minio.Object, string, error) { 209 | 210 | var s3 = &s3.ObjectStorage 211 | var tmp *minio.Object 212 | 213 | reader, err := s3.Client.GetObject(context.Background(), "amupxm", code, minio.GetObjectOptions{}) 214 | if err != nil { 215 | return tmp, "", err 216 | } 217 | 218 | if err != nil { 219 | return tmp, "", err 220 | } 221 | return reader, "", nil 222 | } 223 | 224 | //write results to minio 225 | func (f *FFmpeg_Generator) WriteTos3() error { 226 | var s3 = &s3.ObjectStorage 227 | _, err := s3.Client.FPutObject( 228 | context.Background(), 229 | "amupxm", 230 | f.UUID, 231 | f.Dir+"x.mp4", 232 | minio.PutObjectOptions{}, 233 | ) 234 | if err != nil { 235 | log.Println(111, err) 236 | return err 237 | } 238 | log.Println(111) 239 | return nil 240 | } 241 | 242 | // send callback status 243 | func (f *FFmpeg_Generator) SendResponseCallback() error { 244 | var callBack = []byte(`{"status":true , "url" : "url" }`) 245 | req, err := http.NewRequest( 246 | "POST", 247 | f.Recipe.CallBackAddress, 248 | bytes.NewBuffer(callBack), 249 | ) 250 | if err != nil { 251 | return errors.New("invalid request generation") 252 | } 253 | req.Header.Set("Content-Type", "application/json") 254 | cli := &http.Client{} 255 | resp, err := cli.Do(req) 256 | if err != nil { 257 | return errors.New("call back failed ") 258 | } 259 | if resp.Status != "200 OK" { 260 | return errors.New("callback responded with not 200 code") 261 | } 262 | defer resp.Body.Close() 263 | return nil 264 | } 265 | -------------------------------------------------------------------------------- /packages/ffmpeg/ffmpeg.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "sync" 8 | 9 | filemanager "github.com/amupxm/go-video-concat/interfaces/file-manager" 10 | "github.com/amupxm/go-video-concat/interfaces/splash" 11 | "github.com/amupxm/go-video-concat/packages/cache" 12 | "github.com/amupxm/go-video-concat/packages/s3" 13 | "github.com/minio/minio-go/v7" 14 | ) 15 | 16 | // create video from images with sertain duration ( end - start seconds ) 17 | func (f *FFmpeg_Generator) img2vid(index int, wg *sync.WaitGroup) { 18 | 19 | img2vidArgs := []string{ 20 | "-r", 21 | "30", 22 | "-framerate", 23 | fmt.Sprintf("1/%d", f.Recipe.Chunks[index].End-f.Recipe.Chunks[index].Start), 24 | "-i", 25 | f.Dir + f.Recipe.Chunks[index].Name, 26 | "-f", "lavfi", "-i", "anullsrc", 27 | "-filter_complex", 28 | fmt.Sprintf("[0:v]split=2[blur][vid];[blur]scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d,boxblur=luma_radius=min(h\\,w)/20:luma_power=1:chroma_radius=min(cw\\,ch)/20:chroma_power=1[bg];[vid]scale=%d:%d:force_original_aspect_ratio=decrease[ov];[bg][ov]overlay=(W-w)/2:(H-h)/2[out]", 29 | f.Frame.Width, f.Frame.Height, 30 | f.Frame.Width, f.Frame.Height, 31 | f.Frame.Width, f.Frame.Height, 32 | ), 33 | "-vcodec", "libx264", 34 | "-pix_fmt", "yuv420p", 35 | "-map", 36 | "[out]:v", "-map", "1:a", "-shortest", 37 | fmt.Sprintf("%s/%d.final.mp4", f.Dir, index), 38 | } 39 | err := Execute(img2vidArgs...) 40 | 41 | if err != nil { 42 | f.Error.Status = false 43 | f.Error.Message = "img 2 vid failure" 44 | } 45 | wg.Done() 46 | } 47 | 48 | // cut video with sertain duration ( end - start seconds ) 49 | func (f *FFmpeg_Generator) trimVid(index int, wg *sync.WaitGroup) { 50 | vidTrimArgs := []string{ 51 | "-i", 52 | f.Dir + f.Recipe.Chunks[index].Name, 53 | "-ss", 54 | "0", 55 | "-t", 56 | fmt.Sprintf("%d", f.Recipe.Chunks[index].End-f.Recipe.Chunks[index].Start), 57 | "-async", 58 | "1", 59 | "-filter_complex", 60 | fmt.Sprintf("[0:v]split=2[blur][vid];[blur]scale=%d:%d:force_original_aspect_ratio=increase,crop=%d:%d,boxblur=luma_radius=min(h\\,w)/20:luma_power=1:chroma_radius=min(cw\\,ch)/20:chroma_power=1[bg];[vid]scale=%d:%d:force_original_aspect_ratio=decrease[ov];[bg][ov]overlay=(W-w)/2:(H-h)/2", 61 | f.Frame.Width, f.Frame.Height, 62 | f.Frame.Width, f.Frame.Height, 63 | f.Frame.Width, f.Frame.Height), 64 | "-c:a", 65 | "copy", 66 | "-avoid_negative_ts", "make_zero", "-fflags", "+genpts", 67 | fmt.Sprintf("%s/%d.final.mp4", f.Dir, index), 68 | } 69 | err := Execute(vidTrimArgs...) 70 | if err != nil { 71 | f.Error.Status = false 72 | f.Error.Message = "trim video failure" 73 | } 74 | wg.Done() 75 | } 76 | 77 | // connect chunk sections together to create video ( include splash ) 78 | func (f *FFmpeg_Generator) Concat() bool { 79 | count := len(f.Recipe.Chunks) 80 | inputsStr := []string{} 81 | 82 | inputsStr = append(inputsStr, "-i", f.Dir+"splash.final.mp4") 83 | filterStr := "[0:v:0] [0:a:0] " 84 | 85 | for i := 0; i < count; i++ { 86 | inputsStr = append(inputsStr, "-i", fmt.Sprintf("%s%d.final.mp4", f.Dir, i)) 87 | filterStr = filterStr + fmt.Sprintf("[%d:v:0] [%d:a:0] ", i+1, i+1) 88 | } 89 | concatArgs := fmt.Sprintf("concat=n=%d:v=1:a=1:unsafe=1 [v] [a]", count+1) 90 | mapArgs := []string{ 91 | "-map", "[v]", "-map", "[a]", 92 | } 93 | args := []string{} 94 | args = append(args, inputsStr...) 95 | args = append(args, "-filter_complex") 96 | args = append(args, fmt.Sprintf("%s%s", filterStr, concatArgs)) 97 | args = append(args, mapArgs...) 98 | args = append(args, f.Dir+"final.output.mp4") 99 | 100 | err := Execute(args...) 101 | 102 | return err == nil 103 | } 104 | 105 | // Overlay Concatted video and add audio file to it and set sound level 106 | func (f *FFmpeg_Generator) OverLay() { 107 | var args []string 108 | if f.Recipe.ExternalAudio { 109 | args = []string{"-loop", "1", "-i", f.Dir + f.Frame.FileCode, "-i", f.Dir + "final.output.mp4", 110 | "-i", f.Dir + f.Recipe.Audio, 111 | "-filter_complex", 112 | fmt.Sprintf("[1]scale=%d:%d[inner];[0][inner]overlay=0:%d:shortest=1[out];[1:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,volume=1[a1];[2:a]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo,volume=0.%d[a2];[a1][a2]amerge=inputs=2,pan=stereo:c0 trim audio :: 196 | _, err = storage.Client.StatObject(context.Background(), "splash-audio", splash.BaseAudio, minio.StatObjectOptions{}) 197 | if err != nil { 198 | return &FFmpeg_Message{Message: "Object error", Status: false} 199 | } 200 | var audio = FFmpeg_Audio{Input: "/tmp/" + splash.FileCode + "/" + splash.BaseAudio, Output: "/tmp/" + splash.FileCode + "/" + "output.mp3", Duration: int(splash.MaxLength)} 201 | TrimAudio(audio) 202 | 203 | var video = FFmpeg_Splash{Audio: "/tmp/" + splash.FileCode + "/" + "output.mp3", Video: "/tmp/" + splash.FileCode + "/" + splash.BaseFile, Output: "/tmp/" + splash.FileCode + "/out.mp4"} 204 | CreateSplash(video) 205 | 206 | xx, err := storage.Client.FPutObject(context.Background(), "splash", splash.FileCode, video.Output, minio.PutObjectOptions{}) 207 | fmt.Print("\n\n\n\n\n\n\n\n", xx) 208 | if err != nil { 209 | return &FFmpeg_Message{Message: "Object error", Status: false} 210 | } 211 | // ffmpeg.New().Input() 212 | return &FFmpeg_Message{Message: "done", Status: true} 213 | 214 | } 215 | 216 | /* 217 | return i+1 if i%2 == 0 || i 218 | */ 219 | func MakeEvenNumber(i int) int { 220 | if i%2 == 0 { 221 | return i 222 | } 223 | return i + 1 224 | } 225 | -------------------------------------------------------------------------------- /transcoder/ffmpeg/metadata.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import "github.com/amupxm/go-video-concat/transcoder" 4 | 5 | // Metadata ... 6 | type Metadata struct { 7 | Format Format `json:"format"` 8 | Streams []Streams `json:"streams"` 9 | } 10 | 11 | // Format ... 12 | type Format struct { 13 | Filename string 14 | NbStreams int `json:"nb_streams"` 15 | NbPrograms int `json:"nb_programs"` 16 | FormatName string `json:"format_name"` 17 | FormatLongName string `json:"format_long_name"` 18 | Duration string `json:"duration"` 19 | Size string `json:"size"` 20 | BitRate string `json:"bit_rate"` 21 | ProbeScore int `json:"probe_score"` 22 | Tags Tags `json:"tags"` 23 | } 24 | 25 | // Streams ... 26 | type Streams struct { 27 | Index int 28 | ID string `json:"id"` 29 | CodecName string `json:"codec_name"` 30 | CodecLongName string `json:"codec_long_name"` 31 | Profile string `json:"profile"` 32 | CodecType string `json:"codec_type"` 33 | CodecTimeBase string `json:"codec_time_base"` 34 | CodecTagString string `json:"codec_tag_string"` 35 | CodecTag string `json:"codec_tag"` 36 | Width int `json:"width"` 37 | Height int `json:"height"` 38 | CodedWidth int `json:"coded_width"` 39 | CodedHeight int `json:"coded_height"` 40 | HasBFrames int `json:"has_b_frames"` 41 | SampleAspectRatio string `json:"sample_aspect_ratio"` 42 | DisplayAspectRatio string `json:"display_aspect_ratio"` 43 | PixFmt string `json:"pix_fmt"` 44 | Level int `json:"level"` 45 | ChromaLocation string `json:"chroma_location"` 46 | Refs int `json:"refs"` 47 | QuarterSample string `json:"quarter_sample"` 48 | DivxPacked string `json:"divx_packed"` 49 | RFrameRrate string `json:"r_frame_rate"` 50 | AvgFrameRate string `json:"avg_frame_rate"` 51 | TimeBase string `json:"time_base"` 52 | DurationTs int `json:"duration_ts"` 53 | Duration string `json:"duration"` 54 | Disposition Disposition `json:"disposition"` 55 | BitRate string `json:"bit_rate"` 56 | } 57 | 58 | // Tags ... 59 | type Tags struct { 60 | Encoder string `json:"ENCODER"` 61 | } 62 | 63 | // Disposition ... 64 | type Disposition struct { 65 | Default int `json:"default"` 66 | Dub int `json:"dub"` 67 | Original int `json:"original"` 68 | Comment int `json:"comment"` 69 | Lyrics int `json:"lyrics"` 70 | Karaoke int `json:"karaoke"` 71 | Forced int `json:"forced"` 72 | HearingImpaired int `json:"hearing_impaired"` 73 | VisualImpaired int `json:"visual_impaired"` 74 | CleanEffects int `json:"clean_effects"` 75 | } 76 | 77 | // GetFormat ... 78 | func (m Metadata) GetFormat() transcoder.Format { 79 | return m.Format 80 | } 81 | 82 | // GetStreams ... 83 | func (m Metadata) GetStreams() (streams []transcoder.Streams) { 84 | for _, element := range m.Streams { 85 | streams = append(streams, element) 86 | } 87 | return streams 88 | } 89 | 90 | // GetFilename ... 91 | func (f Format) GetFilename() string { 92 | return f.Filename 93 | } 94 | 95 | // GetNbStreams ... 96 | func (f Format) GetNbStreams() int { 97 | return f.NbStreams 98 | } 99 | 100 | // GetNbPrograms ... 101 | func (f Format) GetNbPrograms() int { 102 | return f.NbPrograms 103 | } 104 | 105 | // GetFormatName ... 106 | func (f Format) GetFormatName() string { 107 | return f.FormatName 108 | } 109 | 110 | // GetFormatLongName ... 111 | func (f Format) GetFormatLongName() string { 112 | return f.FormatLongName 113 | } 114 | 115 | // GetDuration ... 116 | func (f Format) GetDuration() string { 117 | return f.Duration 118 | } 119 | 120 | // GetSize ... 121 | func (f Format) GetSize() string { 122 | return f.Size 123 | } 124 | 125 | // GetBitRate ... 126 | func (f Format) GetBitRate() string { 127 | return f.BitRate 128 | } 129 | 130 | // GetProbeScore ... 131 | func (f Format) GetProbeScore() int { 132 | return f.ProbeScore 133 | } 134 | 135 | // GetTags ... 136 | func (f Format) GetTags() transcoder.Tags { 137 | return f.Tags 138 | } 139 | 140 | // GetEncoder ... 141 | func (t Tags) GetEncoder() string { 142 | return t.Encoder 143 | } 144 | 145 | //GetIndex ... 146 | func (s Streams) GetIndex() int { 147 | return s.Index 148 | } 149 | 150 | //GetID ... 151 | func (s Streams) GetID() string { 152 | return s.ID 153 | } 154 | 155 | //GetCodecName ... 156 | func (s Streams) GetCodecName() string { 157 | return s.CodecName 158 | } 159 | 160 | //GetCodecLongName ... 161 | func (s Streams) GetCodecLongName() string { 162 | return s.CodecLongName 163 | } 164 | 165 | //GetProfile ... 166 | func (s Streams) GetProfile() string { 167 | return s.Profile 168 | } 169 | 170 | //GetCodecType ... 171 | func (s Streams) GetCodecType() string { 172 | return s.CodecType 173 | } 174 | 175 | //GetCodecTimeBase ... 176 | func (s Streams) GetCodecTimeBase() string { 177 | return s.CodecTimeBase 178 | } 179 | 180 | //GetCodecTagString ... 181 | func (s Streams) GetCodecTagString() string { 182 | return s.CodecTagString 183 | } 184 | 185 | //GetCodecTag ... 186 | func (s Streams) GetCodecTag() string { 187 | return s.CodecTag 188 | } 189 | 190 | //GetWidth ... 191 | func (s Streams) GetWidth() int { 192 | return s.Width 193 | } 194 | 195 | //GetHeight ... 196 | func (s Streams) GetHeight() int { 197 | return s.Height 198 | } 199 | 200 | //GetCodedWidth ... 201 | func (s Streams) GetCodedWidth() int { 202 | return s.CodedWidth 203 | } 204 | 205 | //GetCodedHeight ... 206 | func (s Streams) GetCodedHeight() int { 207 | return s.CodedHeight 208 | } 209 | 210 | //GetHasBFrames ... 211 | func (s Streams) GetHasBFrames() int { 212 | return s.HasBFrames 213 | } 214 | 215 | //GetSampleAspectRatio ... 216 | func (s Streams) GetSampleAspectRatio() string { 217 | return s.SampleAspectRatio 218 | } 219 | 220 | //GetDisplayAspectRatio ... 221 | func (s Streams) GetDisplayAspectRatio() string { 222 | return s.DisplayAspectRatio 223 | } 224 | 225 | //GetPixFmt ... 226 | func (s Streams) GetPixFmt() string { 227 | return s.PixFmt 228 | } 229 | 230 | //GetLevel ... 231 | func (s Streams) GetLevel() int { 232 | return s.Level 233 | } 234 | 235 | //GetChromaLocation ... 236 | func (s Streams) GetChromaLocation() string { 237 | return s.ChromaLocation 238 | } 239 | 240 | //GetRefs ... 241 | func (s Streams) GetRefs() int { 242 | return s.Refs 243 | } 244 | 245 | //GetQuarterSample ... 246 | func (s Streams) GetQuarterSample() string { 247 | return s.QuarterSample 248 | } 249 | 250 | //GetDivxPacked ... 251 | func (s Streams) GetDivxPacked() string { 252 | return s.DivxPacked 253 | } 254 | 255 | //GetRFrameRrate ... 256 | func (s Streams) GetRFrameRrate() string { 257 | return s.RFrameRrate 258 | } 259 | 260 | //GetAvgFrameRate ... 261 | func (s Streams) GetAvgFrameRate() string { 262 | return s.AvgFrameRate 263 | } 264 | 265 | //GetTimeBase ... 266 | func (s Streams) GetTimeBase() string { 267 | return s.TimeBase 268 | } 269 | 270 | //GetDurationTs ... 271 | func (s Streams) GetDurationTs() int { 272 | return s.DurationTs 273 | } 274 | 275 | //GetDuration ... 276 | func (s Streams) GetDuration() string { 277 | return s.Duration 278 | } 279 | 280 | //GetDisposition ... 281 | func (s Streams) GetDisposition() transcoder.Disposition { 282 | return s.Disposition 283 | } 284 | 285 | //GetBitRate ... 286 | func (s Streams) GetBitRate() string { 287 | return s.BitRate 288 | } 289 | 290 | //GetDefault ... 291 | func (d Disposition) GetDefault() int { 292 | return d.Default 293 | } 294 | 295 | //GetDub ... 296 | func (d Disposition) GetDub() int { 297 | return d.Dub 298 | } 299 | 300 | //GetOriginal ... 301 | func (d Disposition) GetOriginal() int { 302 | return d.Original 303 | } 304 | 305 | //GetComment ... 306 | func (d Disposition) GetComment() int { 307 | return d.Comment 308 | } 309 | 310 | //GetLyrics ... 311 | func (d Disposition) GetLyrics() int { 312 | return d.Lyrics 313 | } 314 | 315 | //GetKaraoke ... 316 | func (d Disposition) GetKaraoke() int { 317 | return d.Karaoke 318 | } 319 | 320 | //GetForced ... 321 | func (d Disposition) GetForced() int { 322 | return d.Forced 323 | } 324 | 325 | //GetHearingImpaired ... 326 | func (d Disposition) GetHearingImpaired() int { 327 | return d.HearingImpaired 328 | } 329 | 330 | //GetVisualImpaired ... 331 | func (d Disposition) GetVisualImpaired() int { 332 | return d.VisualImpaired 333 | } 334 | 335 | //GetCleanEffects ... 336 | func (d Disposition) GetCleanEffects() int { 337 | return d.CleanEffects 338 | } 339 | -------------------------------------------------------------------------------- /transcoder/ffmpeg/ffmpeg.go: -------------------------------------------------------------------------------- 1 | package ffmpeg 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | "os/exec" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/amupxm/go-video-concat/transcoder" 17 | "github.com/floostack/transcoder/utils" 18 | ) 19 | 20 | // Transcoder ... 21 | type Transcoder struct { 22 | config *Config 23 | input string 24 | output []string 25 | options [][]string 26 | metadata transcoder.Metadata 27 | inputPipeReader *io.ReadCloser 28 | outputPipeReader *io.ReadCloser 29 | inputPipeWriter *io.WriteCloser 30 | outputPipeWriter *io.WriteCloser 31 | } 32 | 33 | // New ... 34 | func New(cfg *Config) transcoder.Transcoder { 35 | return &Transcoder{config: cfg} 36 | } 37 | 38 | // Start ... 39 | func (t *Transcoder) Start(opts transcoder.Options) (<-chan transcoder.Progress, error) { 40 | 41 | var stderrIn io.ReadCloser 42 | 43 | out := make(chan transcoder.Progress) 44 | 45 | defer t.closePipes() 46 | 47 | // Validates config 48 | if err := t.validate(); err != nil { 49 | return nil, err 50 | } 51 | 52 | // Get file metadata 53 | _, err := t.GetMetadata() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // Append input file and standard options 59 | args := append([]string{"-i", t.input}, opts.GetStrArguments()...) 60 | outputLength := len(t.output) 61 | optionsLength := len(t.options) 62 | 63 | if outputLength == 1 && optionsLength == 0 { 64 | // Just append the 1 output file we've got 65 | args = append(args, t.output[0]) 66 | } else { 67 | for index, out := range t.output { 68 | // Get executable flags 69 | // If we are at the last output file but still have several options, append them all at once 70 | if index == outputLength-1 && outputLength < optionsLength { 71 | for i := index; i < len(t.options); i++ { 72 | args = append(args, t.options[i]...) 73 | } 74 | // Otherwise just append the current options 75 | } else { 76 | args = append(args, t.options[index]...) 77 | } 78 | 79 | // Append output flag 80 | args = append(args, out) 81 | } 82 | } 83 | 84 | // Initialize command 85 | cmd := exec.Command(t.config.FfmpegBinPath, args...) 86 | 87 | // If progress enabled, get stderr pipe and start progress process 88 | if t.config.ProgressEnabled && !t.config.Verbose { 89 | stderrIn, err = cmd.StderrPipe() 90 | if err != nil { 91 | return nil, fmt.Errorf("failed getting transcoding progress (%s) with args (%s) with error %s", t.config.FfmpegBinPath, args, err) 92 | } 93 | } 94 | 95 | if t.config.Verbose { 96 | cmd.Stderr = os.Stdout 97 | } 98 | 99 | // Start process 100 | err = cmd.Start() 101 | if err != nil { 102 | return nil, fmt.Errorf("failed starting transcoding (%s) with args (%s) with error %s", t.config.FfmpegBinPath, args, err) 103 | } 104 | 105 | if t.config.ProgressEnabled && !t.config.Verbose { 106 | go func() { 107 | t.progress(stderrIn, out) 108 | }() 109 | 110 | go func() { 111 | defer close(out) 112 | err = cmd.Wait() 113 | }() 114 | } else { 115 | err = cmd.Wait() 116 | } 117 | 118 | return out, nil 119 | } 120 | 121 | // Input ... 122 | func (t *Transcoder) Input(arg string) transcoder.Transcoder { 123 | t.input = arg 124 | return t 125 | } 126 | 127 | // Output ... 128 | func (t *Transcoder) Output(arg string) transcoder.Transcoder { 129 | t.output = append(t.output, arg) 130 | return t 131 | } 132 | 133 | // InputPipe ... 134 | func (t *Transcoder) InputPipe(w *io.WriteCloser, r *io.ReadCloser) transcoder.Transcoder { 135 | if &t.input == nil { 136 | t.inputPipeWriter = w 137 | t.inputPipeReader = r 138 | } 139 | return t 140 | } 141 | 142 | // OutputPipe ... 143 | func (t *Transcoder) OutputPipe(w *io.WriteCloser, r *io.ReadCloser) transcoder.Transcoder { 144 | if &t.output == nil { 145 | t.outputPipeWriter = w 146 | t.outputPipeReader = r 147 | } 148 | return t 149 | } 150 | 151 | // WithOptions Sets the options object 152 | func (t *Transcoder) WithOptions(opts transcoder.Options) transcoder.Transcoder { 153 | t.options = [][]string{opts.GetStrArguments()} 154 | return t 155 | } 156 | 157 | // WithAdditionalOptions Appends an additional options object 158 | func (t *Transcoder) WithAdditionalOptions(opts transcoder.Options) transcoder.Transcoder { 159 | t.options = append(t.options, opts.GetStrArguments()) 160 | return t 161 | } 162 | 163 | // validate ... 164 | func (t *Transcoder) validate() error { 165 | if t.config.FfmpegBinPath == "" { 166 | return errors.New("ffmpeg binary path not found") 167 | } 168 | 169 | if t.input == "" { 170 | return errors.New("missing input option") 171 | } 172 | 173 | outputLength := len(t.output) 174 | 175 | if outputLength == 0 { 176 | return errors.New("missing output option") 177 | } 178 | 179 | // length of output files being greater than length of options would produce an invalid ffmpeg command 180 | // unless there is only 1 output file, which obviously wouldn't be a problem 181 | if outputLength > len(t.options) && outputLength != 1 { 182 | return errors.New("number of options and output files does not match") 183 | } 184 | 185 | for index, output := range t.output { 186 | if output == "" { 187 | return fmt.Errorf("output at index %d is an empty string", index) 188 | } 189 | } 190 | 191 | return nil 192 | } 193 | 194 | // GetMetadata Returns metadata for the specified input file 195 | func (t *Transcoder) GetMetadata() (transcoder.Metadata, error) { 196 | 197 | if t.config.FfprobeBinPath != "" { 198 | var outb, errb bytes.Buffer 199 | 200 | input := t.input 201 | 202 | if t.inputPipeReader != nil { 203 | input = "pipe:" 204 | } 205 | 206 | args := []string{"-i", input, "-print_format", "json", "-show_format", "-show_streams", "-show_error"} 207 | 208 | cmd := exec.Command(t.config.FfprobeBinPath, args...) 209 | cmd.Stdout = &outb 210 | cmd.Stderr = &errb 211 | 212 | err := cmd.Run() 213 | if err != nil { 214 | return nil, fmt.Errorf("error executing (%s) with args (%s) | error: %s | message: %s %s", t.config.FfprobeBinPath, args, err, outb.String(), errb.String()) 215 | } 216 | 217 | var metadata Metadata 218 | 219 | if err = json.Unmarshal([]byte(outb.String()), &metadata); err != nil { 220 | return nil, err 221 | } 222 | 223 | t.metadata = metadata 224 | 225 | return metadata, nil 226 | } 227 | 228 | return nil, errors.New("ffprobe binary not found") 229 | } 230 | 231 | // progress sends through given channel the transcoding status 232 | func (t *Transcoder) progress(stream io.ReadCloser, out chan transcoder.Progress) { 233 | 234 | defer stream.Close() 235 | 236 | split := func(data []byte, atEOF bool) (advance int, token []byte, spliterror error) { 237 | if atEOF && len(data) == 0 { 238 | return 0, nil, nil 239 | } 240 | if i := bytes.IndexByte(data, '\n'); i >= 0 { 241 | // We have a full newline-terminated line. 242 | return i + 1, data[0:i], nil 243 | } 244 | if i := bytes.IndexByte(data, '\r'); i >= 0 { 245 | // We have a cr terminated line 246 | return i + 1, data[0:i], nil 247 | } 248 | if atEOF { 249 | return len(data), data, nil 250 | } 251 | 252 | return 0, nil, nil 253 | } 254 | 255 | scanner := bufio.NewScanner(stream) 256 | scanner.Split(split) 257 | 258 | buf := make([]byte, 2) 259 | scanner.Buffer(buf, bufio.MaxScanTokenSize) 260 | 261 | for scanner.Scan() { 262 | Progress := new(Progress) 263 | line := scanner.Text() 264 | 265 | if strings.Contains(line, "frame=") && strings.Contains(line, "time=") && strings.Contains(line, "bitrate=") { 266 | var re = regexp.MustCompile(`=\s+`) 267 | st := re.ReplaceAllString(line, `=`) 268 | 269 | f := strings.Fields(st) 270 | 271 | var framesProcessed string 272 | var currentTime string 273 | var currentBitrate string 274 | var currentSpeed string 275 | 276 | for j := 0; j < len(f); j++ { 277 | field := f[j] 278 | fieldSplit := strings.Split(field, "=") 279 | 280 | if len(fieldSplit) > 1 { 281 | fieldname := strings.Split(field, "=")[0] 282 | fieldvalue := strings.Split(field, "=")[1] 283 | 284 | if fieldname == "frame" { 285 | framesProcessed = fieldvalue 286 | } 287 | 288 | if fieldname == "time" { 289 | currentTime = fieldvalue 290 | } 291 | 292 | if fieldname == "bitrate" { 293 | currentBitrate = fieldvalue 294 | } 295 | if fieldname == "speed" { 296 | currentSpeed = fieldvalue 297 | } 298 | } 299 | } 300 | 301 | timesec := utils.DurToSec(currentTime) 302 | dursec, _ := strconv.ParseFloat(t.metadata.GetFormat().GetDuration(), 64) 303 | 304 | progress := (timesec * 100) / dursec 305 | Progress.Progress = progress 306 | 307 | Progress.CurrentBitrate = currentBitrate 308 | Progress.FramesProcessed = framesProcessed 309 | Progress.CurrentTime = currentTime 310 | Progress.Speed = currentSpeed 311 | 312 | out <- *Progress 313 | } 314 | } 315 | } 316 | 317 | // closePipes Closes pipes if opened 318 | func (t *Transcoder) closePipes() { 319 | if t.inputPipeReader != nil { 320 | ipr := *t.inputPipeReader 321 | ipr.Close() 322 | } 323 | 324 | if t.outputPipeWriter != nil { 325 | opr := *t.outputPipeWriter 326 | opr.Close() 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 6 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 7 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 8 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 9 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 10 | github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= 11 | github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= 12 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 13 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 14 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 15 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 16 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 17 | github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 18 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 19 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 20 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 21 | github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= 22 | github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= 23 | github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= 24 | github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= 25 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 26 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 27 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 28 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 29 | github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= 30 | github.com/cavaliercoder/grab v1.0.0 h1:H6VQ1NiLO7AvXM6ZyaInnoZrRLeo2FoUTQEcXln4bvQ= 31 | github.com/cavaliercoder/grab v2.0.0+incompatible h1:wZHbBQx56+Yxjx2TCGDcenhh3cJn7cCLMfkEPmySTSE= 32 | github.com/cavaliercoder/grab v2.0.0+incompatible/go.mod h1:tTBkfNqSBfuMmMBFaO2phgyhdYhiZQ/+iXCZDzcDsMI= 33 | github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= 34 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 35 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 36 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 37 | github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= 38 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 39 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 40 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 41 | github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= 42 | github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= 43 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 44 | github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 45 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 46 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 47 | github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 48 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 49 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 50 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 51 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 52 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 53 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 54 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 55 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 56 | github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 57 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 58 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 59 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 60 | github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= 61 | github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 62 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 63 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 64 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 65 | github.com/floostack/transcoder v1.1.1 h1:ApvdTZGt4r1N8dcT1PEl4jxX5OBHaOMY42kmCKNbYCE= 66 | github.com/floostack/transcoder v1.1.1/go.mod h1:lT+f8NEGaHP6AVeYLicas0EI0+TforD+DRuLziJg6bY= 67 | github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= 68 | github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= 69 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 70 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 71 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 72 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 73 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 74 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 75 | github.com/gin-gonic/gin v1.7.1 h1:qC89GU3p8TvKWMAVhEpmpB2CIb1hnqt2UdKZaP93mS8= 76 | github.com/gin-gonic/gin v1.7.1/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= 77 | github.com/go-ini/ini v1.62.0 h1:7VJT/ZXjzqSrvtraFp4ONq80hTcRQth1c9ZnQ3uNQvU= 78 | github.com/go-ini/ini v1.62.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 79 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 80 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 81 | github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= 82 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 83 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 84 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 85 | github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= 86 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 87 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 88 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 89 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 90 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 91 | github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= 92 | github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= 93 | github.com/go-playground/validator/v10 v10.5.0 h1:X9rflw/KmpACwT8zdrm1upefpvdy6ur8d1kWyq6sg3E= 94 | github.com/go-playground/validator/v10 v10.5.0/go.mod h1:xm76BBt941f7yWdGnI2DVPFFg1UK3YY04qifoXU3lOk= 95 | github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= 96 | github.com/go-redis/redis/v8 v8.8.2 h1:O/NcHqobw7SEptA0yA6up6spZVFtwE06SXM8rgLtsP8= 97 | github.com/go-redis/redis/v8 v8.8.2/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y= 98 | github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= 99 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 100 | github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= 101 | github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 102 | github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= 103 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 104 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 105 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 106 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 107 | github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 108 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 109 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 110 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 111 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 112 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 113 | github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= 114 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 115 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 116 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 117 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 118 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 119 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 120 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 121 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 122 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 123 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 124 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 125 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 126 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 127 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 128 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 129 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 130 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 131 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 132 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 133 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 134 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 135 | github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 136 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 137 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 138 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 139 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 140 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 141 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 142 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 143 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 144 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 145 | github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 146 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 147 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 148 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 149 | github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= 150 | github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 151 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 152 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 153 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 154 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 155 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 156 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 157 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 158 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 159 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 160 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 161 | github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 162 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 163 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 164 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 165 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 166 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 167 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 168 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 169 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 170 | github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= 171 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 172 | github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= 173 | github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0= 174 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 175 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 176 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 177 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 178 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 179 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 180 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 181 | github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= 182 | github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 183 | github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= 184 | github.com/jackc/pgconn v1.8.1 h1:ySBX7Q87vOMqKU2bbmKbUvtYhauDFclYbNDYIE1/h6s= 185 | github.com/jackc/pgconn v1.8.1/go.mod h1:JV6m6b6jhjdmzchES0drzCcYcAHS1OPD5xu3OZ/lE2g= 186 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 187 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 188 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA= 189 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 190 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 191 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 192 | github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A= 193 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 194 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 195 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 196 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 197 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 198 | github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 199 | github.com/jackc/pgproto3/v2 v2.0.6 h1:b1105ZGEMFe7aCvrT1Cca3VoVb4ZFMaFJLJcg/3zD+8= 200 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 201 | github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 202 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 203 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 204 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 205 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 206 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 207 | github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= 208 | github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= 209 | github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= 210 | github.com/jackc/pgtype v1.7.0 h1:6f4kVsW01QftE38ufBYxKciO6gyioXSC0ABIRLcZrGs= 211 | github.com/jackc/pgtype v1.7.0/go.mod h1:ZnHF+rMePVqDKaOfJVI4Q8IVvAQMryDlDkZnKOI75BE= 212 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 213 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 214 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 215 | github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= 216 | github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= 217 | github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= 218 | github.com/jackc/pgx/v4 v4.11.0 h1:J86tSWd3Y7nKjwT/43xZBvpi04keQWx8gNC2YkdJhZI= 219 | github.com/jackc/pgx/v4 v4.11.0/go.mod h1:i62xJgdrtVDsnL3U8ekyrQXEwGNTRoG7/8r+CIdYfcc= 220 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 221 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 222 | github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 223 | github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 224 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 225 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 226 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 227 | github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= 228 | github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 229 | github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= 230 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= 231 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= 232 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 233 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 234 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 235 | github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 236 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 237 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 238 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 239 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 240 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 241 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 242 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 243 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 244 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 245 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 246 | github.com/klauspost/cpuid v1.2.3/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= 247 | github.com/klauspost/cpuid v1.3.1 h1:5JNjFYYQrZeKRJ0734q51WCEEn2huer72Dc7K+R/b6s= 248 | github.com/klauspost/cpuid v1.3.1/go.mod h1:bYW4mA6ZgKPob1/Dlai2LviZJO7KGI3uoWLd42rAQw4= 249 | github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 250 | github.com/klauspost/cpuid/v2 v2.0.4 h1:g0I61F2K2DjRHz1cnxlkNSBIaePVoJIjjnHui8QHbiw= 251 | github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 252 | github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= 253 | github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 254 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 255 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 256 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 257 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 258 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 259 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 260 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 261 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 262 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 263 | github.com/lbryio/go_mediainfo v0.0.0-20200109212001-4c7318fd92ad h1:sfsYsbQXjA1uE51yFNtv4k79yMBzeGgAusK6KrLrcYs= 264 | github.com/lbryio/go_mediainfo v0.0.0-20200109212001-4c7318fd92ad/go.mod h1:xJtOuuGAo32jLOmrdBWRCURfahqz6OvK/hdSLhmYA38= 265 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 266 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 267 | github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= 268 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 269 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 270 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 271 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 272 | github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= 273 | github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 274 | github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= 275 | github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= 276 | github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= 277 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 278 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 279 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 280 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 281 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 282 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 283 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 284 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 285 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 286 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 287 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 288 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 289 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 290 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 291 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 292 | github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= 293 | github.com/minio/md5-simd v1.1.0/go.mod h1:XpBqgZULrMYD3R+M28PcmP0CkI7PEMzB3U77ZrKZ0Gw= 294 | github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= 295 | github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= 296 | github.com/minio/minio-go v1.0.0 h1:ooSujki+Z1PRGZsYffJw5jnF5eMBvzMVV86TLAlM0UM= 297 | github.com/minio/minio-go v6.0.14+incompatible h1:fnV+GD28LeqdN6vT2XdGKW8Qe/IfjJDswNVuni6km9o= 298 | github.com/minio/minio-go v6.0.14+incompatible/go.mod h1:7guKYtitv8dktvNUGrhzmNlA5wrAABTQXCoesZdFQO8= 299 | github.com/minio/minio-go/v7 v7.0.10 h1:1oUKe4EOPUEhw2qnPQaPsJ0lmVTYLFu03SiItauXs94= 300 | github.com/minio/minio-go/v7 v7.0.10/go.mod h1:td4gW1ldOsj1PbSNS+WYK43j+P1XVhX/8W8awaYlBFo= 301 | github.com/minio/sha256-simd v0.1.1 h1:5QHSlgo3nt5yKOJrC7W8w7X+NFl8cMPZm96iu8kKUJU= 302 | github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= 303 | github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= 304 | github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= 305 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 306 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 307 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 308 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 309 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 310 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 311 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 312 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 313 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 314 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 315 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 316 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 317 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 318 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 319 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 320 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 321 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 322 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 323 | github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= 324 | github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= 325 | github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= 326 | github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= 327 | github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 328 | github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= 329 | github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 330 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 331 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 332 | github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= 333 | github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= 334 | github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= 335 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 336 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 337 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 338 | github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= 339 | github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= 340 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 341 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 342 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 343 | github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= 344 | github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= 345 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= 346 | github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= 347 | github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= 348 | github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 349 | github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= 350 | github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= 351 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 352 | github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 353 | github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 354 | github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= 355 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 356 | github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= 357 | github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= 358 | github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= 359 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 360 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 361 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 362 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 363 | github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= 364 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 365 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 366 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 367 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 368 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 369 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 370 | github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= 371 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 372 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 373 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 374 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 375 | github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 376 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 377 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 378 | github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= 379 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 380 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 381 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 382 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 383 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 384 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 385 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 386 | github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= 387 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 388 | github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= 389 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 390 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 391 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 392 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 393 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 394 | github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= 395 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 396 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 397 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 398 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 399 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= 400 | github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 401 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 402 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 403 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 404 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 405 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 406 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 407 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 408 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 409 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 410 | github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= 411 | github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= 412 | github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 413 | github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 414 | github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= 415 | github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 416 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 417 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 418 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 419 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 420 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 421 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 422 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 423 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 424 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 425 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 426 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 427 | github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 428 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 429 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 430 | github.com/ugorji/go v1.2.5 h1:NozRHfUeEta89taVkyfsDVSy2f7v89Frft4pjnWuGuc= 431 | github.com/ugorji/go v1.2.5/go.mod h1:gat2tIT8KJG8TVI8yv77nEO/KYT6dV7JE1gfUa8Xuls= 432 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 433 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 434 | github.com/ugorji/go/codec v1.2.5 h1:8WobZKAk18Msm2CothY2jnztY56YVY8kF1oQrj21iis= 435 | github.com/ugorji/go/codec v1.2.5/go.mod h1:QPxoTbPKSEAlAHPYt02++xp/en9B/wUdwFCz+hj5caA= 436 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 437 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 438 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 439 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 440 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 441 | go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 442 | go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= 443 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 444 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 445 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 446 | go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng= 447 | go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= 448 | go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg= 449 | go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= 450 | go.opentelemetry.io/otel/oteltest v0.19.0 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1rU+ip2v9Q= 451 | go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= 452 | go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= 453 | go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= 454 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 455 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 456 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 457 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 458 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 459 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 460 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 461 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 462 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 463 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 464 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 465 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 466 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 467 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 468 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 469 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 470 | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 471 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 472 | golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 473 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 474 | golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 475 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= 476 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 477 | golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 478 | golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 479 | golang.org/x/crypto v0.0.0-20210505212654-3497b51f5e64 h1:QuAh/1Gwc0d+u9walMU1NqzhRemNegsv5esp2ALQIY4= 480 | golang.org/x/crypto v0.0.0-20210505212654-3497b51f5e64/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 481 | golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf h1:B2n+Zi5QeYRDAEodEu72OS36gmTWjgpXr2+cWcBW90o= 482 | golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 483 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 484 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 485 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 486 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 487 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 488 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 489 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 490 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 491 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 492 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 493 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 494 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 495 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 496 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 497 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 498 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 499 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 500 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 501 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 502 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 503 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 504 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 505 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 506 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 507 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 508 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 509 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 510 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 511 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 512 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= 513 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 514 | golang.org/x/net v0.0.0-20210508051633-16afe75a6701 h1:lQVgcB3+FoAXOb20Dp6zTzAIrpj1k/yOOBN7s+Zv1rA= 515 | golang.org/x/net v0.0.0-20210508051633-16afe75a6701/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 516 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 517 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 518 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 519 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 520 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 521 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 522 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 523 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 524 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 525 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 526 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 527 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 528 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 529 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 530 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 531 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 532 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 533 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 534 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 535 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 536 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 537 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 538 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 539 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 540 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 541 | golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 542 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 543 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 544 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 545 | golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 546 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 547 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 548 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 549 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 550 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 551 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 552 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 553 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 554 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 555 | golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c= 556 | golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 557 | golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096 h1:5PbJGn5Sp3GEUjJ61aYbUP6RIo3Z3r2E4Tv9y2z8UHo= 558 | golang.org/x/sys v0.0.0-20210507161434-a76c4d0a0096/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 559 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 560 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 561 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 562 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 563 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 564 | golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= 565 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 566 | golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 567 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 568 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 569 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 570 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 571 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 572 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 573 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 574 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 575 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 576 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 577 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 578 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 579 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 580 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 581 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 582 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 583 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114 h1:DnSr2mCsxyCE6ZgIkmcWUQY2R5cH/6wL7eIxEmQOMSE= 584 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 585 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 586 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 587 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 588 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 589 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 590 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 591 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 592 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 593 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 594 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 595 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 596 | google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 597 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 598 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 599 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 600 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 601 | google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= 602 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55 h1:gSJIx1SDwno+2ElGhA4+qG2zF97qiUzTM+rQ0klBOcE= 603 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 604 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 605 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 606 | google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= 607 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 608 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 609 | google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 610 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 611 | google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 612 | google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= 613 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 614 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 615 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 616 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 617 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 618 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 619 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 620 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 621 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 622 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 623 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 624 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 625 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 626 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 627 | gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= 628 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 629 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 630 | gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= 631 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 632 | gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= 633 | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 634 | gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU= 635 | gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 636 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 637 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 638 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 639 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 640 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 641 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 642 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 643 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 644 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 645 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 646 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 647 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 648 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 649 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 650 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 651 | gorm.io/driver/postgres v1.1.0 h1:afBljg7PtJ5lA6YUWluV2+xovIPhS+YiInuL3kUjrbk= 652 | gorm.io/driver/postgres v1.1.0/go.mod h1:hXQIwafeRjJvUm+OMxcFWyswJ/vevcpPLlGocwAwuqw= 653 | gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E= 654 | gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= 655 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 656 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 657 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 658 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 659 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 660 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 661 | sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= 662 | --------------------------------------------------------------------------------