From da629cf1a025eee42a65c0a3df050a4f2d499d5d Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 28 Feb 2021 18:04:41 +0100 Subject: [PATCH] Initial commit. --- .gitignore | 3 + README.md | 3 + go.mod | 3 + icedreammusic/.dockerignore | 8 + icedreammusic/.gitignore | 1 + icedreammusic/README.md | 18 + icedreammusic/docker-compose.yml | 76 ++ icedreammusic/foobar2000/.gitignore | 1 + icedreammusic/foobar2000/filesystem.go | 74 ++ icedreammusic/foobar2000/go.mod | 12 + icedreammusic/foobar2000/go.sum | 51 ++ icedreammusic/foobar2000/main.go | 165 ++++ icedreammusic/foobar2000/memfs.go | 581 ++++++++++++++ icedreammusic/foobar2000/tuna_output.go | 41 + icedreammusic/liquidsoap/.dockerignore | 6 + icedreammusic/liquidsoap/Dockerfile | 49 ++ icedreammusic/liquidsoap/metadata_api.liq | 69 ++ icedreammusic/liquidsoap/outputs.liq | 132 ++++ icedreammusic/liquidsoap/settings.liq | 11 + icedreammusic/liquidsoap/silent_fallback.liq | 9 + icedreammusic/liquidsoap/stream.liq | 30 + icedreammusic/metacollector/.dockerignore | 6 + icedreammusic/metacollector/.gitignore | 6 + icedreammusic/metacollector/Dockerfile | 26 + icedreammusic/metacollector/README.md | 6 + icedreammusic/metacollector/client.go | 61 ++ .../metacollector/cmd/metacollectord/main.go | 423 ++++++++++ icedreammusic/metacollector/go.mod | 15 + icedreammusic/metacollector/go.sum | 359 +++++++++ icedreammusic/ndi-to-srt/Dockerfile | 28 + icedreammusic/ndi-to-srt/ndi-to-srt.sh | 59 ++ icedreammusic/nowplaying_overlay.html | 726 ++++++++++++++++++ icedreammusic/prime4/.gitignore | 1 + icedreammusic/prime4/go.mod | 14 + icedreammusic/prime4/go.sum | 318 ++++++++ icedreammusic/prime4/main.go | 380 +++++++++ icedreammusic/tuna/go.mod | 3 + icedreammusic/tuna/tuna_output.go | 52 ++ icedreammusic/tunaposter.Dockerfile | 16 + icedreammusic/tunaposter/.dockerignore | 2 + icedreammusic/tunaposter/go.mod | 16 + icedreammusic/tunaposter/go.sum | 27 + icedreammusic/tunaposter/main.go | 88 +++ 43 files changed, 3975 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 icedreammusic/.dockerignore create mode 100644 icedreammusic/.gitignore create mode 100644 icedreammusic/README.md create mode 100644 icedreammusic/docker-compose.yml create mode 100644 icedreammusic/foobar2000/.gitignore create mode 100644 icedreammusic/foobar2000/filesystem.go create mode 100644 icedreammusic/foobar2000/go.mod create mode 100644 icedreammusic/foobar2000/go.sum create mode 100644 icedreammusic/foobar2000/main.go create mode 100644 icedreammusic/foobar2000/memfs.go create mode 100644 icedreammusic/foobar2000/tuna_output.go create mode 100644 icedreammusic/liquidsoap/.dockerignore create mode 100644 icedreammusic/liquidsoap/Dockerfile create mode 100644 icedreammusic/liquidsoap/metadata_api.liq create mode 100644 icedreammusic/liquidsoap/outputs.liq create mode 100644 icedreammusic/liquidsoap/settings.liq create mode 100644 icedreammusic/liquidsoap/silent_fallback.liq create mode 100644 icedreammusic/liquidsoap/stream.liq create mode 100644 icedreammusic/metacollector/.dockerignore create mode 100644 icedreammusic/metacollector/.gitignore create mode 100644 icedreammusic/metacollector/Dockerfile create mode 100644 icedreammusic/metacollector/README.md create mode 100644 icedreammusic/metacollector/client.go create mode 100644 icedreammusic/metacollector/cmd/metacollectord/main.go create mode 100644 icedreammusic/metacollector/go.mod create mode 100644 icedreammusic/metacollector/go.sum create mode 100644 icedreammusic/ndi-to-srt/Dockerfile create mode 100644 icedreammusic/ndi-to-srt/ndi-to-srt.sh create mode 100644 icedreammusic/nowplaying_overlay.html create mode 100644 icedreammusic/prime4/.gitignore create mode 100644 icedreammusic/prime4/go.mod create mode 100644 icedreammusic/prime4/go.sum create mode 100644 icedreammusic/prime4/main.go create mode 100644 icedreammusic/tuna/go.mod create mode 100644 icedreammusic/tuna/tuna_output.go create mode 100644 icedreammusic/tunaposter.Dockerfile create mode 100644 icedreammusic/tunaposter/.dockerignore create mode 100644 icedreammusic/tunaposter/go.mod create mode 100644 icedreammusic/tunaposter/go.sum create mode 100644 icedreammusic/tunaposter/main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d76fa8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.exe +*.dll +*.tar* diff --git a/README.md b/README.md new file mode 100644 index 0000000..ed515b9 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Icedream's Livestream Tools + +This repository contains the collection of tools I use to run my livestreams on Twitch, along with some explanation for how the tools work in the whole system. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..651cb47 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/icedream/livestream-tools + +go 1.16 diff --git a/icedreammusic/.dockerignore b/icedreammusic/.dockerignore new file mode 100644 index 0000000..efa5f70 --- /dev/null +++ b/icedreammusic/.dockerignore @@ -0,0 +1,8 @@ +.env + +### + +# Blacklisting all files except Golang-relevant ones +* +!tuna/ +!tunaposter/ diff --git a/icedreammusic/.gitignore b/icedreammusic/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/icedreammusic/.gitignore @@ -0,0 +1 @@ +.env diff --git a/icedreammusic/README.md b/icedreammusic/README.md new file mode 100644 index 0000000..ead5409 --- /dev/null +++ b/icedreammusic/README.md @@ -0,0 +1,18 @@ +# Icedream's Music Channel + +This directory is specifically for https://twitch.tv/icedreammusic. + +## DJ streams + +I have a specific configuration for DJ streams, where I not only stream to Twitch but also stream audio-only to some Icecast servers like those at Mixcloud. + +The system is supposed to do the following things: + +- Capture the audio as directly as possible and turn it into a processable digital audio stream +- Post-process the audio to be more "radio-ready," so multiband compression, limiting, etc. go here +- Apply live metadata and write it down into a file for tracklisting on uploads later +- Stream the end result with as little latency in between as possible via Icecast and via OBS + +Below I'm documenting what each component exactly does and how it acts as part of the system. + +### [Liquidsoap component](liquidsoap/) diff --git a/icedreammusic/docker-compose.yml b/icedreammusic/docker-compose.yml new file mode 100644 index 0000000..101f8b8 --- /dev/null +++ b/icedreammusic/docker-compose.yml @@ -0,0 +1,76 @@ +version: "3.8" +volumes: + metadatabase: + library: + driver_opts: + type: ${LIBRARY_VOLUME_TYPE} + o: ${LIBRARY_VOLUME_OPTIONS} + device: ${LIBRAY_VOLUME_DEVICE} +services: + liquidsoap: + image: icedream/liquidsoap + build: liquidsoap/ + # HACK - haven't quite figured out the ports SRT/NDI use + network_mode: host + # ports: + # - "8050:8050" + # - "8051:8051" + # - "9000:9000" + # - "9000:9000/udp" + stop_signal: SIGTERM + restart: always + devices: + - /dev/dri:/dev/dri + deploy: + resources: + limits: + cpus: "2" + memory: 768M + ndi-to-srt: + image: icedream/ndi-to-srt + restart: always + build: ndi-to-srt + # HACK - haven't quite figured out the ports SRT/NDI use + network_mode: host + deploy: + resources: + limits: + cpus: "1" + memory: 64M + tunaposter: + image: icedream/tunaposter + restart: always + build: + context: . + dockerfile: tunaposter.Dockerfile + # HACK - haven't quite figured out the ports SRT/NDI use + network_mode: host + command: + - /usr/local/bin/tunaposter + # tuna source URL + - ${TUNA_SOURCE_URL} + # liquidsoap metadata API URL + - http://127.0.0.1:21338 + deploy: + resources: + limits: + cpus: "1" + memory: 32M + metacollector: + image: icedream/metacollector + restart: always + build: metacollector/ + ports: + - "8080:8080" + environment: + METACOLLECTOR_DATABASE_URL: /database/app.db + METACOLLECTOR_LIBRARY_PATHS: /library + METACOLLECTOR_SERVER_ADDRESS: ":8080" + volumes: + - library:/library + - metadatabase:/database + deploy: + resources: + limits: + cpus: "2" + memory: 128M diff --git a/icedreammusic/foobar2000/.gitignore b/icedreammusic/foobar2000/.gitignore new file mode 100644 index 0000000..c0ce450 --- /dev/null +++ b/icedreammusic/foobar2000/.gitignore @@ -0,0 +1 @@ +foobar2000 diff --git a/icedreammusic/foobar2000/filesystem.go b/icedreammusic/foobar2000/filesystem.go new file mode 100644 index 0000000..4b16a36 --- /dev/null +++ b/icedreammusic/foobar2000/filesystem.go @@ -0,0 +1,74 @@ +package main + +import ( + "bytes" + "encoding/json" + "path/filepath" + + fuse "github.com/billziss-gh/cgofuse/fuse" +) + +type NowPlayingMetadata struct { + IsPlaying bool + PlaybackTime float64 + PlaybackTimeRemaining float64 + Length float64 + Path string + Samplerate int + LengthSamples int64 + Title string + Artist string + Album string + Publisher string +} + +type NowPlayingFilesystem struct { + *Memfs + + lastMetadata NowPlayingMetadata + metadataC chan NowPlayingMetadata +} + +func NewNowPlayingFilesystem() (c <-chan NowPlayingMetadata, i fuse.FileSystemInterface) { + ch := make(chan NowPlayingMetadata) + c = ch + npfs := &NowPlayingFilesystem{ + Memfs: NewMemfs(), + metadataC: ch, + } + npfs.Memfs.Mkdir("nowplaying", 0777) + // err, fh := npfs.Open("/nowplaying.json", 0) + // if err != 0 { + // panic(fmt.Errorf("Failed to create nowplaying.json in memory, error code %d", err)) + // } + // npfs.Write("/nowplaying.json", []byte("{}"), 0, fh) + // npfs.Release("/nowplaying.json", fh) + + i = npfs + return +} + +func (self *NowPlayingFilesystem) Release(path string, fh uint64) (retval int) { + retval = self.Memfs.Release(path, fh) + if retval != 0 { + return + } + + if filepath.Base(path) != "nowplaying.json" { + return + } + + errC, fh := self.Memfs.Open(path, 0) + if errC != 0 { + retval = errC + return + } + defer self.Memfs.Release(path, fh) + buff := make([]byte, 1024000) + self.Memfs.Read(path, buff, 0, fh) + metadata := new(NowPlayingMetadata) + if err := json.NewDecoder(bytes.NewReader(buff)).Decode(metadata); err == nil { + self.metadataC <- *metadata + } + return +} diff --git a/icedreammusic/foobar2000/go.mod b/icedreammusic/foobar2000/go.mod new file mode 100644 index 0000000..6304570 --- /dev/null +++ b/icedreammusic/foobar2000/go.mod @@ -0,0 +1,12 @@ +module github.com/icedream/livestream-tools/icedreammusic/foobar2000 + +go 1.16 + +replace github.com/icedream/livestream-tools/icedreammusic/tuna => ../tuna + +require ( + github.com/billziss-gh/cgofuse v1.4.0 + github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 + github.com/gin-gonic/gin v1.6.3 + github.com/icedream/livestream-tools/icedreammusic/tuna v0.0.0-00010101000000-000000000000 +) diff --git a/icedreammusic/foobar2000/go.sum b/icedreammusic/foobar2000/go.sum new file mode 100644 index 0000000..786305e --- /dev/null +++ b/icedreammusic/foobar2000/go.sum @@ -0,0 +1,51 @@ +github.com/billziss-gh/cgofuse v1.4.0 h1:kju2jDmdNuDDCrxPob2ggmZr5Mj/odCjU1Y8kx0Th9E= +github.com/billziss-gh/cgofuse v1.4.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 h1:/u5RVRk3Nh7Zw1QQnPtUH5kzcc8JmSSRpHSlGU/zGTE= +github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/icedreammusic/foobar2000/main.go b/icedreammusic/foobar2000/main.go new file mode 100644 index 0000000..2e70870 --- /dev/null +++ b/icedreammusic/foobar2000/main.go @@ -0,0 +1,165 @@ +package main + +import ( + "encoding/base64" + "fmt" + "log" + "net" + "net/http" + "net/url" + "os" + "time" + + "github.com/billziss-gh/cgofuse/fuse" + "github.com/dhowden/tag" + "github.com/gin-gonic/gin" + + "github.com/icedream/livestream-tools/icedreammusic/metacollector" + "github.com/icedream/livestream-tools/icedreammusic/tuna" +) + +func main() { + c, fs := NewNowPlayingFilesystem() + host := fuse.NewFileSystemHost(fs) + host.SetCapReaddirPlus(true) + + r := gin.Default() + r.GET("/cover/:base64Path", func(c *gin.Context) { + path := c.Params.ByName("base64Path") + pathBytes, err := base64.URLEncoding.DecodeString(path) + if err != nil { + c.JSON(500, map[string]string{"error": err.Error()}) + return + } + path = string(pathBytes) + + f, err := os.Open(path) + if err != nil { + c.JSON(500, map[string]string{"error": err.Error()}) + return + } + defer f.Close() + + // get cover if possible + fileMetadata, err := tag.ReadFrom(f) + if err != nil { + c.JSON(500, map[string]string{"error": err.Error()}) + return + } + + picture := fileMetadata.Picture() + if picture == nil { + c.JSON(404, map[string]string{"error": "this file has no picture"}) + return + } + c.Header("Content-type", picture.MIMEType) + c.Writer.Write(picture.Data) + return + }) + + listener, err := net.Listen("tcp", ":0") + if err != nil { + panic(err) + } + apiAddr := &url.URL{ + Scheme: "http", + Host: fmt.Sprintf("%s:%d", "127.0.0.1", listener.Addr().(*net.TCPAddr).Port), + Path: "/", + } + go http.Serve(listener, r) + + go func() { + tunaOutput := tuna.NewTunaOutput() + + lastCoverCheckPath := "" + lastCoverCheckResult := false + lastCoverCheckTime := time.Now() + + metacollectorClient := metacollector.NewMetaCollectorClient(&url.URL{ + Scheme: "http", + Host: "192.168.188.69:8080", // TODO - make configurable + Path: "/", + }) + + for metadata := range c { + // log.Printf("New metadata: %+v", metadata) + + status := "stopped" + if metadata.IsPlaying { + status = "playing" + } + + tunaMetadata := &tuna.TunaData{ + Title: metadata.Title, + Artists: []string{metadata.Artist}, + Label: metadata.Publisher, + Status: status, + Duration: uint64(metadata.Length * 1000), + Progress: uint64(metadata.PlaybackTime * 1000), + } + + if metadata.IsPlaying { + hasChanged := lastCoverCheckPath != metadata.Path + fi, err := os.Stat(metadata.Path) + if err == nil { + if !hasChanged { + hasChanged = fi.ModTime().Sub(lastCoverCheckTime) > 0 + } + lastCoverCheckTime = fi.ModTime() + + if hasChanged { + lastCoverCheckResult = false + lastCoverCheckPath = metadata.Path + f, err := os.Open(metadata.Path) + if err == nil { + // get cover if possible + fileMetadata, err := tag.ReadFrom(f) + if err == nil { + if fileMetadata.Picture() != nil { + lastCoverCheckResult = true + } + } else { + log.Printf("Warning while reading tags for %s: %s", metadata.Path, err) + } + f.Close() + } else { + log.Printf("Warning while opening file %s: %s", metadata.Path, err) + } + } + } else { + log.Printf("Warning while stat'ing file %s: %s", metadata.Path, err) + } + + if lastCoverCheckResult { + tunaMetadata.CoverURL = apiAddr.ResolveReference(&url.URL{ + Path: fmt.Sprintf("cover/%s", base64.URLEncoding.EncodeToString([]byte(metadata.Path))), + }).String() + } + } + + go func() { + // enrich metadata with metacollector + resp, err := metacollectorClient.GetTrack(metacollector.MetaCollectorRequest{ + Artist: tunaMetadata.Artist, + Title: tunaMetadata.Title, + }) + if err == nil { + if resp.CoverURL != nil { + tunaMetadata.CoverURL = *resp.CoverURL + } + tunaMetadata.Label = resp.Publisher + } + + err = tunaOutput.Post(tunaMetadata) + if err != nil { + log.Println(err) + } /*else { + log.Printf("Tuna has received the metadata: %+v", tunaMetadata) + }*/ + }() + } + }() + + host.Mount("", os.Args[1:]) + +} diff --git a/icedreammusic/foobar2000/memfs.go b/icedreammusic/foobar2000/memfs.go new file mode 100644 index 0000000..5fa9b19 --- /dev/null +++ b/icedreammusic/foobar2000/memfs.go @@ -0,0 +1,581 @@ +/* + * memfs.go + * + * Copyright 2017-2020 Bill Zissimopoulos + */ +/* + * This file is part of Cgofuse. + * + * It is licensed under the MIT license. The full license text can be found + * in the License.txt file at the root of this project. + */ + +package main + +import ( + "fmt" + "strings" + "sync" + + "github.com/billziss-gh/cgofuse/examples/shared" + "github.com/billziss-gh/cgofuse/fuse" +) + +func trace(vals ...interface{}) func(vals ...interface{}) { + uid, gid, _ := fuse.Getcontext() + return shared.Trace(1, fmt.Sprintf("[uid=%v,gid=%v]", uid, gid), vals...) +} + +func split(path string) []string { + return strings.Split(path, "/") +} + +func resize(slice []byte, size int64, zeroinit bool) []byte { + const allocunit = 64 * 1024 + allocsize := (size + allocunit - 1) / allocunit * allocunit + if cap(slice) != int(allocsize) { + var newslice []byte + { + defer func() { + if r := recover(); nil != r { + panic(fuse.Error(-fuse.ENOSPC)) + } + }() + newslice = make([]byte, size, allocsize) + } + copy(newslice, slice) + slice = newslice + } else if zeroinit { + i := len(slice) + slice = slice[:size] + for ; len(slice) > i; i++ { + slice[i] = 0 + } + } + return slice +} + +type node_t struct { + stat fuse.Stat_t + xatr map[string][]byte + chld map[string]*node_t + data []byte + opencnt int +} + +func newNode(dev uint64, ino uint64, mode uint32, uid uint32, gid uint32) *node_t { + tmsp := fuse.Now() + self := node_t{ + fuse.Stat_t{ + Dev: dev, + Ino: ino, + Mode: mode, + Nlink: 1, + Uid: uid, + Gid: gid, + Atim: tmsp, + Mtim: tmsp, + Ctim: tmsp, + Birthtim: tmsp, + Flags: 0, + }, + nil, + nil, + nil, + 0} + if fuse.S_IFDIR == self.stat.Mode&fuse.S_IFMT { + self.chld = map[string]*node_t{} + } + return &self +} + +type Memfs struct { + fuse.FileSystemBase + lock sync.Mutex + ino uint64 + root *node_t + openmap map[uint64]*node_t +} + +func (self *Memfs) Mknod(path string, mode uint32, dev uint64) (errc int) { + defer trace(path, mode, dev)(&errc) + defer self.synchronize()() + return self.makeNode(path, mode, dev, nil) +} + +func (self *Memfs) Mkdir(path string, mode uint32) (errc int) { + defer trace(path, mode)(&errc) + defer self.synchronize()() + return self.makeNode(path, fuse.S_IFDIR|(mode&07777), 0, nil) +} + +func (self *Memfs) Unlink(path string) (errc int) { + defer trace(path)(&errc) + defer self.synchronize()() + return self.removeNode(path, false) +} + +func (self *Memfs) Rmdir(path string) (errc int) { + defer trace(path)(&errc) + defer self.synchronize()() + return self.removeNode(path, true) +} + +func (self *Memfs) Link(oldpath string, newpath string) (errc int) { + defer trace(oldpath, newpath)(&errc) + defer self.synchronize()() + _, _, oldnode := self.lookupNode(oldpath, nil) + if nil == oldnode { + return -fuse.ENOENT + } + newprnt, newname, newnode := self.lookupNode(newpath, nil) + if nil == newprnt { + return -fuse.ENOENT + } + if nil != newnode { + return -fuse.EEXIST + } + oldnode.stat.Nlink++ + newprnt.chld[newname] = oldnode + tmsp := fuse.Now() + oldnode.stat.Ctim = tmsp + newprnt.stat.Ctim = tmsp + newprnt.stat.Mtim = tmsp + return 0 +} + +func (self *Memfs) Symlink(target string, newpath string) (errc int) { + defer trace(target, newpath)(&errc) + defer self.synchronize()() + return self.makeNode(newpath, fuse.S_IFLNK|00777, 0, []byte(target)) +} + +func (self *Memfs) Readlink(path string) (errc int, target string) { + defer trace(path)(&errc, &target) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT, "" + } + if fuse.S_IFLNK != node.stat.Mode&fuse.S_IFMT { + return -fuse.EINVAL, "" + } + return 0, string(node.data) +} + +func (self *Memfs) Rename(oldpath string, newpath string) (errc int) { + defer trace(oldpath, newpath)(&errc) + defer self.synchronize()() + oldprnt, oldname, oldnode := self.lookupNode(oldpath, nil) + if nil == oldnode { + return -fuse.ENOENT + } + newprnt, newname, newnode := self.lookupNode(newpath, oldnode) + if nil == newprnt { + return -fuse.ENOENT + } + if "" == newname { + // guard against directory loop creation + return -fuse.EINVAL + } + if oldprnt == newprnt && oldname == newname { + return 0 + } + if nil != newnode { + errc = self.removeNode(newpath, fuse.S_IFDIR == oldnode.stat.Mode&fuse.S_IFMT) + if 0 != errc { + return errc + } + } + delete(oldprnt.chld, oldname) + newprnt.chld[newname] = oldnode + return 0 +} + +func (self *Memfs) Chmod(path string, mode uint32) (errc int) { + defer trace(path, mode)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + node.stat.Mode = (node.stat.Mode & fuse.S_IFMT) | mode&07777 + node.stat.Ctim = fuse.Now() + return 0 +} + +func (self *Memfs) Chown(path string, uid uint32, gid uint32) (errc int) { + defer trace(path, uid, gid)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + if ^uint32(0) != uid { + node.stat.Uid = uid + } + if ^uint32(0) != gid { + node.stat.Gid = gid + } + node.stat.Ctim = fuse.Now() + return 0 +} + +func (self *Memfs) Utimens(path string, tmsp []fuse.Timespec) (errc int) { + defer trace(path, tmsp)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + node.stat.Ctim = fuse.Now() + if nil == tmsp { + tmsp0 := node.stat.Ctim + tmsa := [2]fuse.Timespec{tmsp0, tmsp0} + tmsp = tmsa[:] + } + node.stat.Atim = tmsp[0] + node.stat.Mtim = tmsp[1] + return 0 +} + +func (self *Memfs) Open(path string, flags int) (errc int, fh uint64) { + defer trace(path, flags)(&errc, &fh) + defer self.synchronize()() + return self.openNode(path, false) +} + +func (self *Memfs) Getattr(path string, stat *fuse.Stat_t, fh uint64) (errc int) { + defer trace(path, fh)(&errc, stat) + defer self.synchronize()() + node := self.getNode(path, fh) + if nil == node { + return -fuse.ENOENT + } + *stat = node.stat + return 0 +} + +func (self *Memfs) Truncate(path string, size int64, fh uint64) (errc int) { + defer trace(path, size, fh)(&errc) + defer self.synchronize()() + node := self.getNode(path, fh) + if nil == node { + return -fuse.ENOENT + } + node.data = resize(node.data, size, true) + node.stat.Size = size + tmsp := fuse.Now() + node.stat.Ctim = tmsp + node.stat.Mtim = tmsp + return 0 +} + +func (self *Memfs) Read(path string, buff []byte, ofst int64, fh uint64) (n int) { + defer trace(path, buff, ofst, fh)(&n) + defer self.synchronize()() + node := self.getNode(path, fh) + if nil == node { + return -fuse.ENOENT + } + endofst := ofst + int64(len(buff)) + if endofst > node.stat.Size { + endofst = node.stat.Size + } + if endofst < ofst { + return 0 + } + n = copy(buff, node.data[ofst:endofst]) + node.stat.Atim = fuse.Now() + return +} + +func (self *Memfs) Write(path string, buff []byte, ofst int64, fh uint64) (n int) { + defer trace(path, buff, ofst, fh)(&n) + defer self.synchronize()() + node := self.getNode(path, fh) + if nil == node { + return -fuse.ENOENT + } + endofst := ofst + int64(len(buff)) + if endofst > node.stat.Size { + node.data = resize(node.data, endofst, true) + node.stat.Size = endofst + } + n = copy(node.data[ofst:endofst], buff) + tmsp := fuse.Now() + node.stat.Ctim = tmsp + node.stat.Mtim = tmsp + return +} + +func (self *Memfs) Release(path string, fh uint64) (errc int) { + defer trace(path, fh)(&errc) + defer self.synchronize()() + return self.closeNode(fh) +} + +func (self *Memfs) Opendir(path string) (errc int, fh uint64) { + defer trace(path)(&errc, &fh) + defer self.synchronize()() + return self.openNode(path, true) +} + +func (self *Memfs) Readdir(path string, + fill func(name string, stat *fuse.Stat_t, ofst int64) bool, + ofst int64, + fh uint64) (errc int) { + defer trace(path, fill, ofst, fh)(&errc) + defer self.synchronize()() + node := self.openmap[fh] + fill(".", &node.stat, 0) + fill("..", nil, 0) + for name, chld := range node.chld { + if !fill(name, &chld.stat, 0) { + break + } + } + return 0 +} + +func (self *Memfs) Releasedir(path string, fh uint64) (errc int) { + defer trace(path, fh)(&errc) + defer self.synchronize()() + return self.closeNode(fh) +} + +func (self *Memfs) Setxattr(path string, name string, value []byte, flags int) (errc int) { + defer trace(path, name, value, flags)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + if "com.apple.ResourceFork" == name { + return -fuse.ENOTSUP + } + if fuse.XATTR_CREATE == flags { + if _, ok := node.xatr[name]; ok { + return -fuse.EEXIST + } + } else if fuse.XATTR_REPLACE == flags { + if _, ok := node.xatr[name]; !ok { + return -fuse.ENOATTR + } + } + xatr := make([]byte, len(value)) + copy(xatr, value) + if nil == node.xatr { + node.xatr = map[string][]byte{} + } + node.xatr[name] = xatr + return 0 +} + +func (self *Memfs) Getxattr(path string, name string) (errc int, xatr []byte) { + defer trace(path, name)(&errc, &xatr) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT, nil + } + if "com.apple.ResourceFork" == name { + return -fuse.ENOTSUP, nil + } + xatr, ok := node.xatr[name] + if !ok { + return -fuse.ENOATTR, nil + } + return 0, xatr +} + +func (self *Memfs) Removexattr(path string, name string) (errc int) { + defer trace(path, name)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + if "com.apple.ResourceFork" == name { + return -fuse.ENOTSUP + } + if _, ok := node.xatr[name]; !ok { + return -fuse.ENOATTR + } + delete(node.xatr, name) + return 0 +} + +func (self *Memfs) Listxattr(path string, fill func(name string) bool) (errc int) { + defer trace(path, fill)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + for name := range node.xatr { + if !fill(name) { + return -fuse.ERANGE + } + } + return 0 +} + +func (self *Memfs) Chflags(path string, flags uint32) (errc int) { + defer trace(path, flags)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + node.stat.Flags = flags + node.stat.Ctim = fuse.Now() + return 0 +} + +func (self *Memfs) Setcrtime(path string, tmsp fuse.Timespec) (errc int) { + defer trace(path, tmsp)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + node.stat.Birthtim = tmsp + node.stat.Ctim = fuse.Now() + return 0 +} + +func (self *Memfs) Setchgtime(path string, tmsp fuse.Timespec) (errc int) { + defer trace(path, tmsp)(&errc) + defer self.synchronize()() + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + node.stat.Ctim = tmsp + return 0 +} + +func (self *Memfs) lookupNode(path string, ancestor *node_t) (prnt *node_t, name string, node *node_t) { + prnt = self.root + name = "" + node = self.root + for _, c := range split(path) { + if "" != c { + if 255 < len(c) { + panic(fuse.Error(-fuse.ENAMETOOLONG)) + } + prnt, name = node, c + if node == nil { + return + } + node = node.chld[c] + if nil != ancestor && node == ancestor { + name = "" // special case loop condition + return + } + } + } + return +} + +func (self *Memfs) makeNode(path string, mode uint32, dev uint64, data []byte) int { + prnt, name, node := self.lookupNode(path, nil) + if nil == prnt { + return -fuse.ENOENT + } + if nil != node { + return -fuse.EEXIST + } + self.ino++ + uid, gid, _ := fuse.Getcontext() + node = newNode(dev, self.ino, mode, uid, gid) + if nil != data { + node.data = make([]byte, len(data)) + node.stat.Size = int64(len(data)) + copy(node.data, data) + } + prnt.chld[name] = node + prnt.stat.Ctim = node.stat.Ctim + prnt.stat.Mtim = node.stat.Ctim + return 0 +} + +func (self *Memfs) removeNode(path string, dir bool) int { + prnt, name, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT + } + if !dir && fuse.S_IFDIR == node.stat.Mode&fuse.S_IFMT { + return -fuse.EISDIR + } + if dir && fuse.S_IFDIR != node.stat.Mode&fuse.S_IFMT { + return -fuse.ENOTDIR + } + if 0 < len(node.chld) { + return -fuse.ENOTEMPTY + } + node.stat.Nlink-- + delete(prnt.chld, name) + tmsp := fuse.Now() + node.stat.Ctim = tmsp + prnt.stat.Ctim = tmsp + prnt.stat.Mtim = tmsp + return 0 +} + +func (self *Memfs) openNode(path string, dir bool) (int, uint64) { + _, _, node := self.lookupNode(path, nil) + if nil == node { + return -fuse.ENOENT, ^uint64(0) + } + if !dir && fuse.S_IFDIR == node.stat.Mode&fuse.S_IFMT { + return -fuse.EISDIR, ^uint64(0) + } + if dir && fuse.S_IFDIR != node.stat.Mode&fuse.S_IFMT { + return -fuse.ENOTDIR, ^uint64(0) + } + node.opencnt++ + if 1 == node.opencnt { + self.openmap[node.stat.Ino] = node + } + return 0, node.stat.Ino +} + +func (self *Memfs) closeNode(fh uint64) int { + node := self.openmap[fh] + node.opencnt-- + if 0 == node.opencnt { + delete(self.openmap, node.stat.Ino) + } + return 0 +} + +func (self *Memfs) getNode(path string, fh uint64) *node_t { + if ^uint64(0) == fh { + _, _, node := self.lookupNode(path, nil) + return node + } else { + return self.openmap[fh] + } +} + +func (self *Memfs) synchronize() func() { + self.lock.Lock() + return func() { + self.lock.Unlock() + } +} + +func NewMemfs() *Memfs { + self := Memfs{} + defer self.synchronize()() + self.ino++ + self.root = newNode(0, self.ino, fuse.S_IFDIR|00777, 0, 0) + self.openmap = map[uint64]*node_t{} + return &self +} + +var _ fuse.FileSystemChflags = (*Memfs)(nil) +var _ fuse.FileSystemSetcrtime = (*Memfs)(nil) +var _ fuse.FileSystemSetchgtime = (*Memfs)(nil) diff --git a/icedreammusic/foobar2000/tuna_output.go b/icedreammusic/foobar2000/tuna_output.go new file mode 100644 index 0000000..d0a9aeb --- /dev/null +++ b/icedreammusic/foobar2000/tuna_output.go @@ -0,0 +1,41 @@ +package main + +import ( + "bytes" + "encoding/json" + "net/http" + "time" +) + +type TunaOutput struct { + client *http.Client +} + +type tunaData struct { + CoverURL string `json:"cover_url"` + Title string `json:"title"` + Artists []string `json:"artists"` + Status string `json:"status"` + Progress float64 `json:"progress"` + Duration float64 `json:"duration"` +} + +func NewTunaOutput() *TunaOutput { + return &TunaOutput{ + client: http.DefaultClient, + } +} + +func (output *TunaOutput) Post(data *tunaData) (err error) { + body := new(bytes.Buffer) + json.NewEncoder(body).Encode(&struct { + Data *tunaData `json:"data"` + Hostname string `json:"hostname,omitempty"` + Date string `json:"date"` + }{ + Data: data, + Date: time.Now().Format(time.RFC3339), + }) + _, err = output.client.Post("http://localhost:1608", "application/json", body) + return +} diff --git a/icedreammusic/liquidsoap/.dockerignore b/icedreammusic/liquidsoap/.dockerignore new file mode 100644 index 0000000..d4898e2 --- /dev/null +++ b/icedreammusic/liquidsoap/.dockerignore @@ -0,0 +1,6 @@ +Dockerfile +.docker* + +*.tar* +*.md +*.cmd diff --git a/icedreammusic/liquidsoap/Dockerfile b/icedreammusic/liquidsoap/Dockerfile new file mode 100644 index 0000000..873948a --- /dev/null +++ b/icedreammusic/liquidsoap/Dockerfile @@ -0,0 +1,49 @@ +ARG IMAGE=savonet/liquidsoap:v1.4.3 +# ARG IMAGE=savonet/liquidsoap:master + +# FROM $IMAGE + +# USER root + +# ENV DEBIAN_FRONTEND=noninteractive +# RUN sed -e 's,^deb\s\+,deb-src ,g' /etc/apt/sources.list > /etc/apt/sources.list.d/sources.list +# RUN apt-get update +# RUN sed -i -e '/crontab/d' -e '/Debian-exim/d' /var/lib/dpkg/statoverride +# RUN apt-get build-dep -y ffmpeg +# RUN apt-get install -y libfdk-aac-dev devscripts +# RUN apt-get source -y ffmpeg +# RUN sed -i -e 's,--enable-gnutls,--enable-gnutls \\\n\t--enable-libfdk-aac --enable-nonfree,g' ffmpeg-*/debian/rules +# RUN cd ffmpeg-*; dch --local '-icedream' "Build with --enable-libfdk-aac and --enable-nonfree." && cat debian/changelog +# RUN cd ffmpeg-*; dpkg-buildpackage -b -tc -j$(nproc) +# RUN mkdir -p /packages/ && mv *_*.deb /packages/ + +### + +FROM $IMAGE + +# COPY --from=0 /packages/ /packages/ +# USER root +# RUN DEBIAN_FRONTEND=noninteractive apt install -y \ +# /packages/ffmpeg_*.deb \ +# /packages/libavcodec-dev*_*.deb \ +# /packages/libavcodec-extra*_*.deb \ +# /packages/libavdevice*_*.deb \ +# /packages/libavfilter-dev*_*.deb \ +# /packages/libavfilter-extra*_*.deb \ +# /packages/libavformat*_*.deb \ +# /packages/libavresample*_*.deb \ +# /packages/libavutil*_*.deb \ +# /packages/libpostproc*_*.deb \ +# /packages/libswresample*_*.deb \ +# /packages/libswscale*_*.deb \ +# && rm -r /packages/ +# USER liquidsoap + +WORKDIR /liq/ +COPY . . +RUN liquidsoap -c stream.liq + +EXPOSE 8050 8051 9000 9000/udp +STOPSIGNAL SIGTERM +ENTRYPOINT [ "liquidsoap" ] +CMD ["./stream.liq"] diff --git a/icedreammusic/liquidsoap/metadata_api.liq b/icedreammusic/liquidsoap/metadata_api.liq new file mode 100644 index 0000000..231b129 --- /dev/null +++ b/icedreammusic/liquidsoap/metadata_api.liq @@ -0,0 +1,69 @@ +# This file listens on port 21338 for POST JSON data to apply new metadata to the stream live. +# Our Denon StagelinQ receiver will send the metadata to this interface. +# For fun, I tested this code with the port set to 1608 to emulate the OBS Tuna plugin's HTTP interface, and that works well! + +#metadata_api_port = 1608 # Emulate Tuna API + +def setup_harbor_metadata_api(~metadata_api_port=21338, s) = + # [insert_metadata_func, source_with_new_metadata] + result = insert_metadata(s) + insert_metadata_func = fst(result) + s = snd(result) + + # Only Liquidsoap 2.x+ + # s = insert_metadata(s) + + # Handler for receiving metadata` + def on_http_metadata(~protocol, ~data, ~headers, uri) = + data = of_json(default=[ + ("data",[("key","value")]) + ], data) + + m = list.assoc(default=[], "data", data) + insert_metadata_func(m) + http_response(protocol=protocol, code=200, headers=[ + ("allow","POST"), + ("access-control-allow-origin","*"), + ("access-control-allow-credentials","true"), + ("access-control-allow-methods","POST"), + ("access-control-allow-headers","Origin,X-Requested-With,Content-Type,Accept,Authorization,access-control-allow-headers,access-control-allow-origin"), + ("content-type","application/json"), + ], data=json_of(data)) + # Only Liquidsoap 2.x+ + #s.insert_metadata(m) + # http.response(protocol=protocol, code=200, headers=[ + # ("allow","POST"), + # ("access-control-allow-origin","*"), + # ("access-control-allow-credentials","true"), + # ("access-control-allow-methods","POST"), + # ("access-control-allow-headers","Origin,X-Requested-With,Content-Type,Accept,Authorization,access-control-allow-headers,access-control-allow-origin"), + # ("content-type","application/json"), + # ], data=json_of(data)) + end + + # Just in case we use a browser to send data to this (for example while emulating Tuna) + def on_http_metadata_cors(~protocol, ~data, ~headers, uri) = + http_response(protocol=protocol, code=200, headers=[ + ("allow","POST"), + ("access-control-allow-origin","*"), + ("access-control-allow-credentials","true"), + ("access-control-allow-methods","POST"), + ("access-control-allow-headers","Origin,X-Requested-With,Content-Type,Accept,Authorization,access-control-allow-headers,access-control-allow-origin"), + ("content-type","text/html; charset=utf-8"), + ], data="POST") + # Only Liquidsoap 2.x+ + # http.response(protocol=protocol, code=200, headers=[ + # ("allow","POST"), + # ("access-control-allow-origin","*"), + # ("access-control-allow-credentials","true"), + # ("access-control-allow-methods","POST"), + # ("access-control-allow-headers","Origin,X-Requested-With,Content-Type,Accept,Authorization,access-control-allow-headers,access-control-allow-origin"), + # ("content-type","text/html; charset=utf-8"), + # ], data="POST") + end + + harbor.http.register(port=metadata_api_port, method="POST", "/", on_http_metadata) + harbor.http.register(port=metadata_api_port, method="OPTIONS", "/", on_http_metadata_cors) + + s +end \ No newline at end of file diff --git a/icedreammusic/liquidsoap/outputs.liq b/icedreammusic/liquidsoap/outputs.liq new file mode 100644 index 0000000..8de5485 --- /dev/null +++ b/icedreammusic/liquidsoap/outputs.liq @@ -0,0 +1,132 @@ +# Avoid liquidsoap hiccups due to outputs being offline and the audio just queueing up +# output.dummy(fallible=true, s) + +output.harbor( + id="out_a_harbor", + %ogg(%flac), + # fallible=true, + port=8050, + mount="/outa/flac", + a) +output.harbor( + id="out_a_harbor", + %mp3(bitrate=320), + # fallible=true, + port=8050, + mount="/outa/mp3/320", + a) +output.harbor( + id="out_a_harbor", + %fdkaac(channels=2, samplerate=44100, bitrate=128), + # fallible=true, + port=8050, + mount="/outa/aac/128", + a) +output.icecast( + id="out_a_ifl", + %ogg(%flac), + fallible=true, + mount="ifl/live/1", + port=61120, + host="publish.streaminginter.net", + user="source", + password="ghs[73uqGab8_q", + start=false, + a) +# output.icecast( +# codec, +# id="local", +# fallible=false, +# mount="liquidsoap_out.ogg", +# port=18001, +# host="localhost", +# user="source", +# password="ghs[73uqGab8_q", +# start=false, +# a) +output.icecast( + id="out_a_rekt", + %ogg(%flac), + fallible=true, + mount="rekt", + port=60000, + #host="stream.rekt.network", + host="stream.rekt.fm", + user="icedream", + name=stream_name, + password="ghs[73uqGab8_q", + start=false, + a) +output.icecast( + id="hearthis", + %mp3(bitrate=320), + fallible=false, + mount="21287.ogg", + port=8080, + host="streamlive1.hearthis.at", + user="21287", + password="d48484l5j48424d484e443k4h5p574r2t5p4r4d4k4q5l4y5l5u5v2u5a4y5", + start=false, + a) + +# va = drop_midi(s) +# output.dummy(fallible=true, va) + +# # TODO - once input is actually lossless, make this reencode the audio! +# # flv = %ffmpeg(format="flv", audio_codec="copy", video_codec="copy") +# # ts_aac = %ffmpeg(format="mpegts", codec="aac", ar=44100, b="320k") +# # h264_6000k = %ffmpeg( +# # format="mpegts", +# # %video.copy) +# # aac_320k = %fdkaac(channels=2, samplerate=44100, bitrate=320, afterburner=true, aot="mpeg4_aac_lc", transmux="adts", sbr_mode=true) +# ts = %ffmpeg(format="mpegts", +# %audio( +# codec="libfdk_aac", +# channels=2, +# ar=44100, +# b="320k"), +# # %video( +# # codec="libx264", +# # b="6000k", +# # "x264-params"="scenecut=0:open_gop=0:min-keyint=60:keyint=60", +# # g=60, +# # r=30, +# # threads=4, +# # preset="superfast"), +# %video.copy, +# ) +# streams = [ +# ("video", ts), +# ] +# # fps = video.frame.rate() +# # samplerate = audio.samplerate() +# # output.external( +# # id="out_va_mixcloud", +# # reopen_delay=5., +# # self_sync=false, +# # fallible=true, +# # start=false, +# # ts, +# # "ffmpeg -re -hide_banner -loglevel warning -f mpegts -i pipe:0 -c copy -f flv -bufsize 512k -rtmp_live 1 rtmp://rtmp.mixcloud.com/broadcast/e10868158398484b8589ee04b723f575", +# # va) +# # output.external( +# # id="out_va_hearthis", +# # reopen_delay=5., +# # self_sync=false, +# # fallible=true, +# # start=false, +# # ts, +# # "ffmpeg -hide_banner -loglevel warning -f mpegts -i pipe:0 -c copy -f flv -bufsize 512k -rtmp_live 1 \"rtmp://video.hearthis.at/live/21287?secret=d48484l5j484\"", +# # va) +# output.harbor.hls( +# fallible=true, +# # port=8050, +# # mount="/outv.ts", +# # format="video/mp2t", +# port=8051, +# # segments=5, +# # segment_duration=2., +# path="/outv", +# streams, +# # ts, +# s) diff --git a/icedreammusic/liquidsoap/settings.liq b/icedreammusic/liquidsoap/settings.liq new file mode 100644 index 0000000..e85b3d7 --- /dev/null +++ b/icedreammusic/liquidsoap/settings.liq @@ -0,0 +1,11 @@ +#stream_name="The Escape Plan 003" +#stream_name="Icedream's Secret Mix 08" +#stream_name="TGIF mix" +#stream_name="Icedream pres. Imaginary Frequencies 057" +stream_name="Spontaneously live in the mix" +#stream_name="Sunday Straight Shredding, I guess" +#stream_name="Icedream's New Year's celebration stream" + +# codec = %ogg(%vorbis.abr(samplerate=44100, bitrate=500, min_bitrate=32, max_bitrate=512)) +# codec = %mp3(bitrate=320) +# codec = %ogg(%opus(bitrate=192, complexity=10, max_bandwidth="full_band", application="audio", signal="music")) diff --git a/icedreammusic/liquidsoap/silent_fallback.liq b/icedreammusic/liquidsoap/silent_fallback.liq new file mode 100644 index 0000000..f66f349 --- /dev/null +++ b/icedreammusic/liquidsoap/silent_fallback.liq @@ -0,0 +1,9 @@ +# Silent fallback stream that is still loud enough that it forces Vorbis/OPUS codecs to continue broadcasting data. +# Some Icecast-compatible software on the server-side tends to freak out over us not sending data for extended amount of times, for example during technical difficulties. +def mksafe_soft(s) = + blank_s = blank() + blank_v = drop_audio(blank_s) + silent_a = amplify(0.00005, noise()) + silent_s = mux_video(video=blank_v, silent_a) + mksafe(fallback(track_sensitive=false, [s, silent_s])) +end diff --git a/icedreammusic/liquidsoap/stream.liq b/icedreammusic/liquidsoap/stream.liq new file mode 100644 index 0000000..1a55550 --- /dev/null +++ b/icedreammusic/liquidsoap/stream.liq @@ -0,0 +1,30 @@ +set("log.stdout", true) +set("log.file", false) + +set("log.level", 4) + +set("server.telnet", true) +set("server.telnet.bind_addr", "127.0.0.1") +set("server.telnet.port", 21337) + +set("init.allow_root",true) +set("frame.video.width", 1920) +set("frame.video.height", 1080) + +%include "settings.liq" +%include "metadata_api.liq" +%include "silent_fallback.liq" + +s = input.srt(id="input_srt_main", port=9000) + +# Split audio off to be handled specially +a = drop_video(s) +a = mksafe_soft(a) +a = setup_harbor_metadata_api(a) +output.harbor( + id="out_a_harbor", + %ogg(%flac), + # fallible=true, + port=8050, + mount="/outa", + a) diff --git a/icedreammusic/metacollector/.dockerignore b/icedreammusic/metacollector/.dockerignore new file mode 100644 index 0000000..73557f2 --- /dev/null +++ b/icedreammusic/metacollector/.dockerignore @@ -0,0 +1,6 @@ +*.toml +*.yaml +*.yml +*.json +*.db +/metacollectord diff --git a/icedreammusic/metacollector/.gitignore b/icedreammusic/metacollector/.gitignore new file mode 100644 index 0000000..af017cd --- /dev/null +++ b/icedreammusic/metacollector/.gitignore @@ -0,0 +1,6 @@ +/metacollectord +*.db +*.toml +*.yaml +*.yml +*.json diff --git a/icedreammusic/metacollector/Dockerfile b/icedreammusic/metacollector/Dockerfile new file mode 100644 index 0000000..2e08bdf --- /dev/null +++ b/icedreammusic/metacollector/Dockerfile @@ -0,0 +1,26 @@ +FROM golang:1.16-alpine + +WORKDIR /usr/src/icedreammusic/ +COPY . . + +RUN apk add alpine-sdk sqlite-dev + +RUN go build -v ./cmd/metacollectord/ +RUN install -v -m0755 -d /target/usr/local/bin/ +RUN install -v -m0755 metacollectord /target/usr/local/bin/metacollectord + +### + +FROM alpine:3.13 + +RUN apk add sqlite-libs + +COPY --from=0 /target/ / + +WORKDIR /library + +VOLUME ["/library"] +RUN addgroup -S -g 950 app +RUN adduser -S -k /dev/empty -g "App user" -h /library -u 950 -G app app +USER 950 +CMD ["metacollectord"] diff --git a/icedreammusic/metacollector/README.md b/icedreammusic/metacollector/README.md new file mode 100644 index 0000000..6186fe3 --- /dev/null +++ b/icedreammusic/metacollector/README.md @@ -0,0 +1,6 @@ +# Metadata collector + +This is a duct tape program that will scan given directories for metadata in audio files and store that data in its database. +It will then allow other applications to access that information with merely artist name and title given. + +This is used by the PRIME4 and foobar2000 metadata receivers to enrich the metadata. diff --git a/icedreammusic/metacollector/client.go b/icedreammusic/metacollector/client.go new file mode 100644 index 0000000..e883d6e --- /dev/null +++ b/icedreammusic/metacollector/client.go @@ -0,0 +1,61 @@ +package metacollector + +import ( + "bytes" + "encoding/json" + "net/http" + "net/url" + "strings" + "time" +) + +type MetaCollectorClient struct { + client *http.Client + apiURL *url.URL +} + +type MetaCollectorResponse struct { + Artist, Title, Publisher string + CoverURL *string +} + +type MetaCollectorRequest struct { + Artist, Title string +} + +func NewMetaCollectorClient(apiURL *url.URL) *MetaCollectorClient { + return &MetaCollectorClient{ + client: &http.Client{ + Timeout: 5 * time.Second, + }, + apiURL: apiURL, + } +} + +func (mcc *MetaCollectorClient) json(path string, data interface{}, responseData interface{}) (err error) { + u := mcc.apiURL.ResolveReference(&url.URL{ + Path: path, + }) + buf := new(bytes.Buffer) + if err = json.NewEncoder(buf).Encode(data); err != nil { + return + } + resp, err := mcc.client.Post(u.String(), "application/json", buf) + if err != nil { + return + } + err = json.NewDecoder(resp.Body).Decode(responseData) + return +} + +func (mcc *MetaCollectorClient) path(parts ...string) string { + for i, part := range parts { + parts[i] = url.PathEscape(part) + } + return strings.Join(parts, "/") +} + +func (mcc *MetaCollectorClient) GetTrack(req MetaCollectorRequest) (resp *MetaCollectorResponse, err error) { + err = mcc.json("track/find", req, resp) + return +} diff --git a/icedreammusic/metacollector/cmd/metacollectord/main.go b/icedreammusic/metacollector/cmd/metacollectord/main.go new file mode 100644 index 0000000..3a89178 --- /dev/null +++ b/icedreammusic/metacollector/cmd/metacollectord/main.go @@ -0,0 +1,423 @@ +package main + +import ( + "context" + "database/sql" + "errors" + "fmt" + "io/fs" + "log" + "net/http" + "os" + "os/signal" + "path/filepath" + "strconv" + "strings" + "sync" + "syscall" + + "gorm.io/driver/mysql" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + + "github.com/dhowden/tag" + "github.com/fsnotify/fsnotify" + "github.com/gin-gonic/gin" + "github.com/spf13/viper" + + "github.com/icedream/livestream-tools/icedreammusic/metacollector" +) + +const ( + appID = "metacollector" + appName = "Metadata collector" + appEnvPrefix = appID + + configDatabase = "Database" + configDatabaseType = configDatabase + ".Type" + configDatabaseURL = configDatabase + ".URL" + + configLibrary = "Library" + configLibraryPaths = configLibrary + ".Paths" + + configServer = "Server" + configServerAddress = configServer + ".Address" +) + +type track struct { + gorm.Model + Artist string `gorm:"index:idx_artist_title"` + Title string `gorm:"index:idx_artist_title"` + Publisher string + CoverFile *file `gorm:"ForeignKey:CoverFileID;"` + CoverFileID *uint +} + +type file struct { + gorm.Model + Data []byte + ContentType string +} + +type manager struct { + database *gorm.DB +} + +func newManager(appDatabase *gorm.DB) *manager { + m := &manager{ + database: appDatabase.Debug(), + } + + return m +} + +func (m *manager) Migrate() error { + if err := m.database.AutoMigrate(&file{}); err != nil { + return err + } + if err := m.database.AutoMigrate(&track{}); err != nil { + return err + } + return nil +} + +func (m *manager) GetFile(id uint) (result *file, err error) { + result = new(file) + err = m.database.Find(result, id).Error + return +} + +func (m *manager) UploadFile(file *file) (err error) { + err = m.database.Save(file).Error + return +} + +func (m *manager) DeleteFile(file *file) (err error) { + err = m.database.Delete(file).Error + return +} + +func getPublisherFromTags(tags tag.Metadata) string { + for _, tagName := range []string{ + // ID3v2 + "PUBLISHER", + "GROUPING", + "TPUB", + // VORBIS + "organization", + "publisher", + "grouping", + } { + if value, ok := tags.Raw()[tagName]; ok { + if valueString, ok := value.(string); ok && len(strings.TrimSpace(valueString)) > 0 { + return valueString + } + } + } + return "" +} + +func (m *manager) UpdateFileFromFilesystem(f *os.File) (err error) { + tags, err := tag.ReadFrom(f) + if err != nil { + return + } + + // Try to find old information we can reuse + trackObj, err := m.GetTrackByArtistAndTitle(tags.Artist(), tags.Title(), false) + if errors.Is(err, gorm.ErrRecordNotFound) { + trackObj = new(track) + } else if err != nil { + return // something went terribly wrong + } + + trackObj.Artist = tags.Artist() + trackObj.Title = tags.Title() + + // Cover + if tags.Picture() != nil { + trackObj.CoverFile = new(file) + if trackObj.CoverFileID != nil { + trackObj.CoverFile.ID = *trackObj.CoverFileID + } + trackObj.CoverFile.ContentType = tags.Picture().MIMEType + trackObj.CoverFile.Data = tags.Picture().Data + } + + // Publisher + publisher := getPublisherFromTags(tags) + if len(publisher) > 0 { + trackObj.Publisher = publisher + } + + m.database.Save(trackObj) + + return +} + +func (m *manager) WriteTrack(track *track) (err error) { + // If we got no ID, try to find out first whether we already have an entry with same title and artist + result, err := m.GetTrackByArtistAndTitle(track.Artist, track.Title, false) + if err == nil { + // Found one! + track.ID = result.ID + } else if !errors.Is(err, gorm.ErrRecordNotFound) { + // Something went really wrong hereā€¦ + return + } else { + err = nil + } + + err = m.database.Save(track).Error + return +} + +func (m *manager) GetTrackByArtistAndTitle(artist, title string, withCoverFile bool) (result *track, err error) { + result = new(track) + db := m.database + if withCoverFile { + db = db.Preload("CoverFile") + } + err = db. + Where("artist = @artist AND title = @title", + sql.Named("artist", artist), + sql.Named("title", title)). + Find(result).Error + return +} + +func main() { + var err error + defer func() { + if err != nil { + log.Panic(err) + } + }() + + viper.AddConfigPath("/etc/" + appID + "/") + viper.AddConfigPath("$HOME/.config/" + appID + "/") + viper.AddConfigPath("$HOME/." + appID + "/") + viper.AddConfigPath(".") + viper.AutomaticEnv() + viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) + viper.SetEnvPrefix(appEnvPrefix) + err = viper.ReadInConfig() + if err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Config file not found; ignore error if desired + err = nil + log.Println("No configuration file found, ignoring") + } else { + // Config file was found but another error was produced + log.Panicf("Failed to read configuration: %s", err.Error()) + } + } + + viper.SetDefault(configDatabaseType, "sqlite") + viper.SetDefault(configDatabaseURL, "app.db") + viper.SetDefault(configLibraryPaths, []string{}) + + viper.Debug() + + var dialector gorm.Dialector + switch viper.GetString(configDatabaseType) { + case "sqlite": + dialector = sqlite.Open(viper.GetString(configDatabaseURL)) + case "mysql": + dialector = mysql.New( + mysql.Config{ + DSN: viper.GetString(configDatabaseURL), + }) + default: + err = fmt.Errorf("Unsupported config database type: %s", viper.GetString(configDatabaseType)) + return + } + gormDatabase, err := gorm.Open(dialector, &gorm.Config{}) + if err != nil { + return + } + + m := newManager(gormDatabase) + if err = m.Migrate(); err != nil { + return + } + + // Handle shutdown gracefully + ctx := context.Background() // TODO - Timeouts and fancy stuff + signalChannel := make(chan os.Signal, 1) + cond := sync.NewCond(&sync.Mutex{}) + go signal.Notify(signalChannel, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signalChannel + cond.L.Lock() + cond.Broadcast() + cond.L.Unlock() + }() + + wg := new(sync.WaitGroup) + + // Set up HTTP server + r := gin.Default() + r.GET("/file/:fileID", func(c *gin.Context) { + fileID, err := strconv.ParseUint(c.Param("fileID"), 10, 32) + if err != nil { + c.JSON(http.StatusBadRequest, "invalid file ID") + return + } + file, err := m.GetFile(uint(fileID)) + if err != nil { + c.JSON(http.StatusInternalServerError, err.Error()) + return + } + c.Header("Content-type", file.ContentType) + c.Writer.Write(file.Data) + }) + r.POST("/track/find", func(c *gin.Context) { + var form struct { + Artist string + Title string + } + if err := c.ShouldBind(&form); err != nil { + c.JSON(http.StatusBadRequest, "missing POST data") + return + } + if len(form.Artist) <= 0 { + c.JSON(http.StatusBadRequest, "artist must not be empty") + return + } + if len(form.Title) <= 0 { + c.JSON(http.StatusBadRequest, "title must not be empty") + return + } + track, err := m.GetTrackByArtistAndTitle(form.Artist, form.Title, false) + if err != nil { + c.JSON(http.StatusInternalServerError, err.Error()) + return + } + response := metacollector.MetaCollectorResponse{ + Artist: track.Artist, + Title: track.Title, + Publisher: track.Publisher, + } + if track.CoverFileID != nil { + coverURL := fmt.Sprintf("/file/%d", *track.CoverFileID) + response.CoverURL = &coverURL + } + c.JSON(http.StatusOK, response) + }) + server := new(http.Server) + server.Addr = viper.GetString(configServerAddress) + server.Handler = r + wg.Add(1) + go func() { + defer wg.Done() + if err := server.ListenAndServe(); err != nil { + log.Fatal(err) + } + }() + go func() { + cond.L.Lock() + cond.Wait() + cond.L.Unlock() + if err := server.Shutdown(ctx); err != nil { + log.Print("During server shutdown:", err) + } + }() + + // Watch library paths + watcher, err := fsnotify.NewWatcher() + if err != nil { + log.Fatal(err) + } + defer watcher.Close() + wg.Add(1) + go func() { + defer wg.Done() + quitSignalChannel := make(chan interface{}, 1) + go func() { + cond.L.Lock() + cond.Wait() + cond.L.Unlock() + quitSignalChannel <- nil + }() + for { + select { + case <-quitSignalChannel: + watcher.Close() + return + case event, ok := <-watcher.Events: + if !ok { + return + } + // log.Println("event:", event) + if event.Op&fsnotify.Write == fsnotify.Write { + log.Println("modified file:", event.Name) + f, err := os.Open(event.Name) + if err == nil { + err = m.UpdateFileFromFilesystem(f) + } + if err != nil { + log.Printf("Failed to update tags from file system for %s: %s", event.Name, err) + } + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + log.Println("error:", err) + } + } + }() + + firstScanQuitSignalChannel := make(chan interface{}, 1) + go func() { + cond.L.Lock() + cond.Wait() + cond.L.Unlock() + firstScanQuitSignalChannel <- nil + }() + for _, watchPath := range viper.GetStringSlice(configLibraryPaths) { + if len(strings.TrimSpace(watchPath)) <= 0 { + continue // ignore empty path entries + } + + log.Println("Adding path to watcher:", watchPath) + err = watcher.Add(watchPath) + if err != nil { + return + } + + // Force first scan + filepath.WalkDir(watchPath, func(filePath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + select { + case <-firstScanQuitSignalChannel: + return nil // just quit out asap + default: + } + + // skip directory entries + if d.IsDir() { + return nil + } + + switch strings.ToLower(filepath.Ext(filePath)) { + case ".ogg", ".mp3", ".m4a", ".aac", ".flac", ".wav", ".wma", ".wv": + log.Println("scanning:", filePath) + f, err := os.Open(filePath) + if err == nil { + err = m.UpdateFileFromFilesystem(f) + } + if err != nil { + log.Printf("Failed to update tags from file system for %s: %s", filePath, err) + } + } + return nil + }) + } + + wg.Wait() +} diff --git a/icedreammusic/metacollector/go.mod b/icedreammusic/metacollector/go.mod new file mode 100644 index 0000000..4f0cd51 --- /dev/null +++ b/icedreammusic/metacollector/go.mod @@ -0,0 +1,15 @@ +module github.com/icedream/livestream-tools/icedreammusic/metacollector + +go 1.16 + +require ( + github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 + github.com/fsnotify/fsnotify v1.4.9 + github.com/gin-gonic/gin v1.6.3 + github.com/nicksnyder/go-i18n v1.10.1 // indirect + github.com/spf13/viper v1.7.1 + gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 + gorm.io/driver/mysql v1.0.4 + gorm.io/driver/sqlite v1.1.4 + gorm.io/gorm v1.20.12 +) diff --git a/icedreammusic/metacollector/go.sum b/icedreammusic/metacollector/go.sum new file mode 100644 index 0000000..75b6669 --- /dev/null +++ b/icedreammusic/metacollector/go.sum @@ -0,0 +1,359 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63 h1:/u5RVRk3Nh7Zw1QQnPtUH5kzcc8JmSSRpHSlGU/zGTE= +github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= +github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9 h1:L2auWcuQIvxz9xSEqzESnV/QN/gNRXNApHi3fYwl2w0= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 h1:CEBpW6C191eozfEuWdUmIAHn7lwlLxJ7HVdr2e2Tsrw= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gorm.io/driver/mysql v1.0.4 h1:TATTzt+kR+IV0+h3iUB3dHUe8omCvQ0rOkmfCsUBohk= +gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs= +gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.12 h1:ebZ5KrSHzet+sqOCVdH9mTjW91L298nX3v5lVxAzSUY= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/icedreammusic/ndi-to-srt/Dockerfile b/icedreammusic/ndi-to-srt/Dockerfile new file mode 100644 index 0000000..39d48c4 --- /dev/null +++ b/icedreammusic/ndi-to-srt/Dockerfile @@ -0,0 +1,28 @@ +FROM busybox + +WORKDIR /target/usr/local/bin/ +COPY . . +RUN chmod -v +x *.sh + +### + +# yay build + +FROM archlinux + +WORKDIR /usr/src/ndi-to-srt/ +RUN pacman --noconfirm -Sy git sudo make binutils fakeroot base-devel +RUN echo "" && echo "%wheel ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers +RUN useradd -UMr -d /usr/src/ndi-to-srt/ -G wheel app +RUN chown -R app . + +USER app +RUN git clone --recursive https://aur.archlinux.org/yay.git yay/ +RUN cd yay && makepkg --noconfirm -si && cd .. && rm -r yay +RUN yay --noconfirm -S pod2man && sudo rm -r ~/.cache /var/cache/pacman/* +RUN yay --noconfirm -S ffmpeg-ndi && sudo rm -r ~/.cache /var/cache/pacman/* + +COPY --from=0 /target/ / +CMD ["ndi-to-srt.sh"] + +STOPSIGNAL SIGTERM diff --git a/icedreammusic/ndi-to-srt/ndi-to-srt.sh b/icedreammusic/ndi-to-srt/ndi-to-srt.sh new file mode 100644 index 0000000..c99b1d7 --- /dev/null +++ b/icedreammusic/ndi-to-srt/ndi-to-srt.sh @@ -0,0 +1,59 @@ +#!/bin/bash -ex + +target_url="${1:-srt://127.0.0.1:9000}" +ffmpeg_pid= + +call_ffmpeg() { + command ffmpeg -hide_banner "$@" +} + +daemon_ffmpeg() { + call_ffmpeg "$@" & + ffmpeg_pid=$! +} + +shutdown_ffmpeg() { + if is_ffmpeg_running + then + kill "$ffmpeg_pid" + wait "$ffmpeg_pid" + fi + ffmpeg_pid= +} + +is_ffmpeg_running() { + [ -n "$ffmpeg_pid" ] && kill -0 "$ffmpeg_pid" +} + +on_exit() { + shutdown_ffmpeg +} +trap on_exit EXIT + +while true +do + found_audio_source="" + + while read -r line + do + declare -a "found_source=($(sed -e 's/"/\\"/g' -e "s/'/\"/g" -e 's/[][`~!@#$%^&*():;<>.,?/\|{}=+-]/\\&/g' <<< "$line"))" + found_source[0]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<< "${found_source[0]}") + found_source[1]=$(sed -e 's/\\\([`~!@#$%^&*():;<>.,?/\|{}=+-]\)/\1/g' <<< "${found_source[1]}") + case "${found_source[0]}" in + *\(IDHPC\ Main\ Audio\)) + found_audio_source="${found_source[0]}" + ;; + esac + done < <(call_ffmpeg -loglevel info -extra_ips 192.168.188.21 -find_sources true -f libndi_newtek -i "dummy" 2>&1 | grep -Po "'(.+)'\s+'(.+)" | tee) + + if ! is_ffmpeg_running && [ -n "$found_audio_source" ] + then + echo "starting ffmpeg with audio source: $found_audio_source" >&2 + # HACK - can't use the standard mpegts here, but liquidsoap will happily accept anything ffmpeg can parse (by default)ā€¦ so let's just use nut here even though it feels super duper wrong + daemon_ffmpeg -loglevel warning -extra_ips 192.168.188.21 -f libndi_newtek -i "$found_audio_source" -c copy -f nut -write_index false "${target_url}" + elif is_ffmpeg_running && [ -z "$found_audio_source" ] + then + echo "shutting down ffmpeg since no source has been found" >&2 + shutdown_ffmpeg # it won't shut down by itself unfortunately + fi +done diff --git a/icedreammusic/nowplaying_overlay.html b/icedreammusic/nowplaying_overlay.html new file mode 100644 index 0000000..162afd7 --- /dev/null +++ b/icedreammusic/nowplaying_overlay.html @@ -0,0 +1,726 @@ + + + + + + + + + + + + + + + + +
+
+ + Now playing + Now playing + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0:00 + / + 0:00 +
+
+
+ 0:00 + / + 0:00 +
+
+
+
+
+
+   + https://soundcloud.com/icedream +
+
+   + https://twitter.com/icedream2k9 +
+
+   + https://facebook.com/icedreammusic +
+
+   + https://twitch.tv/icedreammusic +
+
+
+
+
+
+ +
+
+
+ +
+
+
+ + diff --git a/icedreammusic/prime4/.gitignore b/icedreammusic/prime4/.gitignore new file mode 100644 index 0000000..c00d3e3 --- /dev/null +++ b/icedreammusic/prime4/.gitignore @@ -0,0 +1 @@ +/prime4 diff --git a/icedreammusic/prime4/go.mod b/icedreammusic/prime4/go.mod new file mode 100644 index 0000000..863f262 --- /dev/null +++ b/icedreammusic/prime4/go.mod @@ -0,0 +1,14 @@ +module github.com/icedream/livestream-tools/icedreammusic/prime4 + +go 1.16 + +replace ( + github.com/icedream/livestream-tools/icedreammusic/metacollector => ../metacollector + github.com/icedream/livestream-tools/icedreammusic/tuna => ../tuna +) + +require ( + github.com/icedream/go-stagelinq v0.0.0-20210112075500-34c1de688760 + github.com/icedream/livestream-tools/icedreammusic/metacollector v0.0.0-00010101000000-000000000000 + github.com/icedream/livestream-tools/icedreammusic/tuna v0.0.0-00010101000000-000000000000 +) diff --git a/icedreammusic/prime4/go.sum b/icedreammusic/prime4/go.sum new file mode 100644 index 0000000..b9fcd5a --- /dev/null +++ b/icedreammusic/prime4/go.sum @@ -0,0 +1,318 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dhowden/tag v0.0.0-20201120070457-d52dcb253c63/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/icedream/go-stagelinq v0.0.0-20210112075500-34c1de688760 h1:r1nhLACWpncQml0/qeo599ye0NZYeiaiuddPvly9rDM= +github.com/icedream/go-stagelinq v0.0.0-20210112075500-34c1de688760/go.mod h1:jLI5jgxAgnuD/h7Pt3Ao8X+GpUVGR+Bt245/ae9kB8k= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/mysql v1.0.4/go.mod h1:MEgp8tk2n60cSBCq5iTcPDw3ns8Gs+zOva9EUhkknTs= +gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/icedreammusic/prime4/main.go b/icedreammusic/prime4/main.go new file mode 100644 index 0000000..aac3003 --- /dev/null +++ b/icedreammusic/prime4/main.go @@ -0,0 +1,380 @@ +package main + +import ( + "log" + "net" + "net/url" + "sync" + "time" + + "github.com/icedream/go-stagelinq" + "github.com/icedream/livestream-tools/icedreammusic/metacollector" + "github.com/icedream/livestream-tools/icedreammusic/tuna" +) + +type ReceivedMetadata struct { + Device *stagelinq.Device + State *stagelinq.State +} + +type MultiMetadataTracker struct { + lock sync.Mutex + token stagelinq.Token + metadataChannel chan *ReceivedMetadata + connectedDevices map[*stagelinq.Device]DeviceConnections + waitGroup sync.WaitGroup +} + +type DeviceConnections struct { + MainConn *stagelinq.MainConnection + StateMapConn net.Conn +} + +func newMultiMetadataTracker(token stagelinq.Token) *MultiMetadataTracker { + c := make(chan *ReceivedMetadata) + return &MultiMetadataTracker{ + token: token, + metadataChannel: c, + connectedDevices: map[*stagelinq.Device]DeviceConnections{}, + } +} + +func (mmt *MultiMetadataTracker) Stop(dev *stagelinq.Device) { + defer mmt.synchronize()() + for registeredDevice, conns := range mmt.connectedDevices { + if registeredDevice.IsEqual(dev) { + conns.StateMapConn.Close() + conns.MainConn.Close() + return + } + } +} + +func (mmt *MultiMetadataTracker) Close() { + for _, conn := range mmt.connectedDevices { + conn.StateMapConn.Close() + conn.MainConn.Close() + } + mmt.waitGroup.Wait() + close(mmt.metadataChannel) +} + +func (mmt *MultiMetadataTracker) synchronize() func() { + mmt.lock.Lock() + return func() { + mmt.lock.Unlock() + } +} + +func (mmt *MultiMetadataTracker) Start(dev *stagelinq.Device) { + mmt.registerDevice(dev) +} + +func (mmt *MultiMetadataTracker) registerDevice(dev *stagelinq.Device) { + defer mmt.synchronize()() + + // check if device was already added + for registeredDevice := range mmt.connectedDevices { + if registeredDevice.IsEqual(dev) { + return + } + } + + log.Printf("Found %s %s (%s)", dev.SoftwareName, dev.SoftwareVersion, dev.Name) + + // try and connect to device + devConn, err := dev.Connect(mmt.token, []*stagelinq.Service{}) + if err != nil { + log.Printf("WARNING: Could not connect to %s: %s", dev.IP, err.Error()) + return + } + services, err := devConn.RequestServices() + if err != nil { + log.Printf("WARNING: Failed to retrieve services of %s: %s", dev.IP, err.Error()) + devConn.Close() + return + } + for _, service := range services { + if service.Port == 0 { + continue + } + switch service.Name { + case "StateMap": + log.Printf("Connecting to %s:%d for %s...", dev.IP, service.Port, service.Name) + rawConn, err := dev.Dial(service.Port) + if err != nil { + log.Printf("WARNING: Failed to connect to state map service at %s: %s", dev.IP, err.Error()) + return + } + log.Printf("Handshaking with %s:%d for %s...", dev.IP, service.Port, service.Name) + stateMapConn, err := stagelinq.NewStateMapConnection(rawConn, mmt.token) + if err != nil { + log.Printf("WARNING: Failed to handshake state map connection at %s: %s", dev.IP, err.Error()) + rawConn.Close() + devConn.Close() + return + } + mmt.connectedDevices[dev] = DeviceConnections{ + MainConn: devConn, + StateMapConn: rawConn, + } + for _, key := range []string{ + stagelinq.EngineDeck1Play, + stagelinq.EngineDeck1TrackArtistName, + stagelinq.EngineDeck1TrackSongName, + stagelinq.EngineDeck2Play, + stagelinq.EngineDeck2TrackArtistName, + stagelinq.EngineDeck2TrackSongName, + stagelinq.EngineDeck3Play, + stagelinq.EngineDeck3TrackArtistName, + stagelinq.EngineDeck3TrackSongName, + stagelinq.EngineDeck4Play, + stagelinq.EngineDeck4TrackArtistName, + stagelinq.EngineDeck4TrackSongName, + stagelinq.MixerCH1faderPosition, + stagelinq.MixerCH2faderPosition, + stagelinq.MixerCH3faderPosition, + stagelinq.MixerCH4faderPosition, + } { + stateMapConn.Subscribe(key) + } + mmt.trackStateMap(dev, stateMapConn) + } + } +} + +func (mmt *MultiMetadataTracker) unregisterDevice(dev *stagelinq.Device) { + log.Printf("About to unregister %s...", dev.IP) + defer mmt.synchronize()() + for registeredDevice, conns := range mmt.connectedDevices { + if registeredDevice.IsEqual(dev) { + conns.StateMapConn.Close() + conns.MainConn.Close() + delete(mmt.connectedDevices, registeredDevice) + return + } + } +} + +func (mmt *MultiMetadataTracker) trackStateMap(dev *stagelinq.Device, conn *stagelinq.StateMapConnection) { + log.Printf("Tracking %s...", dev.IP) + mmt.waitGroup.Add(1) + go func() { + defer mmt.waitGroup.Done() + defer mmt.unregisterDevice(dev) + for { + select { + case err := <-conn.ErrorC(): + log.Printf("WARNING: Disconnected from state map at %s: %s", dev.IP, err.Error()) + return + case state := <-conn.StateC(): + mmt.metadataChannel <- &ReceivedMetadata{ + Device: dev, + State: state, + } + } + } + }() +} + +func (mmt *MultiMetadataTracker) C() <-chan *ReceivedMetadata { + return mmt.metadataChannel +} + +func main() { + listener, err := stagelinq.ListenWithConfiguration(&stagelinq.ListenerConfiguration{ + Name: "icedreamnowplaying", + SoftwareName: "Icedream's Now Playing", + SoftwareVersion: "0.0.0", + }) + if err != nil { + panic(err) + } + defer listener.Close() + + listener.AnnounceEvery(time.Second) + + tracker := newMultiMetadataTracker(listener.Token()) + defer tracker.Close() + + // Device tracking + go func() { + for { + device, deviceState, err := listener.Discover(0) + if device.SoftwareName == "Icedream's Now Playing" { + continue // found our own software + } + if err != nil { + log.Printf("WARNING: During discovery an error occured: %s", err.Error()) + continue + } + switch deviceState { + case stagelinq.DeviceLeaving: + tracker.Stop(device) + case stagelinq.DevicePresent: + tracker.Start(device) + } + } + }() + + // Actual metadata collection and analysis + var currentMetadata *DeviceMeta + var lastDetectedMeta *DeviceMeta + metadata := map[*stagelinq.Device]map[int]*DeviceMeta{} + + output := tuna.NewTunaOutput() + metacollectorClient := metacollector.NewMetaCollectorClient(&url.URL{ + Scheme: "http", + Host: "192.168.188.69:8080", // TODO - make configurable + Path: "/", + }) + + sendMetadata := func() { + tunaData := &tuna.TunaData{ + Status: "stopped", + } + if currentMetadata != nil { + tunaData.Status = "playing" + tunaData.Artists = []string{currentMetadata.Artist} + tunaData.Title = currentMetadata.Title + } + // enrich metadata with metacollector + resp, err := metacollectorClient.GetTrack(metacollector.MetaCollectorRequest{ + Artist: currentMetadata.Artist, + Title: currentMetadata.Title, + }) + if err == nil { + if resp.CoverURL != nil { + tunaData.CoverURL = *resp.CoverURL + } + tunaData.Label = resp.Publisher + } + if err := output.Post(tunaData); err != nil { + log.Printf("WARNING: Failed to send new metadata to tuna: %s", err.Error()) + } + } + conflictDetect := 0 + sameMeta := 0 + detectNewMetadata := func() { + tracksRunning := 0 + var newFaderValue float64 + var maxVolumeDifference float64 = 2 + var newMeta *DeviceMeta + for _, meta := range metadata { + for _, deck := range meta { + if !deck.Playing { + continue + } + tracksRunning++ + if newMeta != nil { + volumeDifference := newFaderValue - deck.Fader + if volumeDifference < 0 { + volumeDifference *= -1 + } + if volumeDifference < maxVolumeDifference { + maxVolumeDifference = volumeDifference + } + if deck.Fader < newFaderValue { + continue + } + newFaderValue = deck.Fader + newMeta = deck + } else { + newFaderValue = deck.Fader + newMeta = deck + } + } + } + lastDetectedMeta = newMeta + if maxVolumeDifference < 0.4 && tracksRunning > 1 { + conflictDetect++ + if conflictDetect > 15 { + currentMetadata = nil + } + sameMeta = 0 + } else { + isSameMeta := (newMeta == nil && lastDetectedMeta == nil) || ((newMeta != nil && lastDetectedMeta != nil) && *newMeta == *lastDetectedMeta) + if isSameMeta { + sameMeta++ + } else { + sameMeta = 0 + } + if sameMeta > 10 { + currentMetadata = newMeta + } + conflictDetect = 0 + } + log.Printf("Metadata now is %+v (%f volume diff, %d conflicts, %d same, actual: %+v)", currentMetadata, maxVolumeDifference, conflictDetect, sameMeta, lastDetectedMeta) + } + getDevice := func(dev *stagelinq.Device) (devMeta map[int]*DeviceMeta) { + devMeta, ok := metadata[dev] + if !ok { + devMeta = map[int]*DeviceMeta{} + metadata[dev] = devMeta + } + return + } + getDeck := func(dev *stagelinq.Device, deckNum int) (deck *DeviceMeta) { + device := getDevice(dev) + deck, ok := device[deckNum] + if !ok { + deck = new(DeviceMeta) + device[deckNum] = deck + } + return + } + + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + detectNewMetadata() + go sendMetadata() + case state := <-tracker.C(): + log.Printf("%s %s %+v", state.Device.Name, state.State.Name, state.State.Value) + switch state.State.Name { + case stagelinq.EngineDeck1TrackArtistName: + getDeck(state.Device, 0).Artist = state.State.Value["string"].(string) + case stagelinq.EngineDeck1TrackSongName: + getDeck(state.Device, 0).Title = state.State.Value["string"].(string) + case stagelinq.MixerCH1faderPosition: + getDeck(state.Device, 0).Fader = state.State.Value["value"].(float64) + case stagelinq.EngineDeck1Play: + getDeck(state.Device, 0).Playing = state.State.Value["state"].(bool) + case stagelinq.EngineDeck2TrackArtistName: + getDeck(state.Device, 1).Artist = state.State.Value["string"].(string) + case stagelinq.EngineDeck2TrackSongName: + getDeck(state.Device, 1).Title = state.State.Value["string"].(string) + case stagelinq.MixerCH2faderPosition: + getDeck(state.Device, 1).Fader = state.State.Value["value"].(float64) + case stagelinq.EngineDeck2Play: + getDeck(state.Device, 1).Playing = state.State.Value["state"].(bool) + case stagelinq.EngineDeck3TrackArtistName: + getDeck(state.Device, 2).Artist = state.State.Value["string"].(string) + case stagelinq.EngineDeck3TrackSongName: + getDeck(state.Device, 2).Title = state.State.Value["string"].(string) + case stagelinq.MixerCH3faderPosition: + getDeck(state.Device, 2).Fader = state.State.Value["value"].(float64) + case stagelinq.EngineDeck3Play: + getDeck(state.Device, 2).Playing = state.State.Value["state"].(bool) + case stagelinq.EngineDeck4TrackArtistName: + getDeck(state.Device, 3).Artist = state.State.Value["string"].(string) + case stagelinq.EngineDeck4TrackSongName: + getDeck(state.Device, 3).Title = state.State.Value["string"].(string) + case stagelinq.MixerCH4faderPosition: + getDeck(state.Device, 3).Fader = state.State.Value["value"].(float64) + case stagelinq.EngineDeck4Play: + getDeck(state.Device, 3).Playing = state.State.Value["state"].(bool) + } + } + } +} + +type DeviceMeta struct { + Playing bool + Artist string + Title string + Fader float64 +} diff --git a/icedreammusic/tuna/go.mod b/icedreammusic/tuna/go.mod new file mode 100644 index 0000000..8fc61b2 --- /dev/null +++ b/icedreammusic/tuna/go.mod @@ -0,0 +1,3 @@ +module github.com/icedream/livestream-tools/icedreammusic/tuna + +go 1.16 diff --git a/icedreammusic/tuna/tuna_output.go b/icedreammusic/tuna/tuna_output.go new file mode 100644 index 0000000..a993878 --- /dev/null +++ b/icedreammusic/tuna/tuna_output.go @@ -0,0 +1,52 @@ +package tuna + +import ( + "bytes" + "encoding/json" + "fmt" + "log" + "net/http" + "time" +) + +type TunaOutput struct { + client *http.Client +} + +type TunaData struct { + CoverURL string `json:"cover_url"` + Title string `json:"title"` + Artists []string `json:"artists"` + Label string `json:"label"` + Status string `json:"status"` + Progress uint64 `json:"progress"` + Duration uint64 `json:"duration"` +} + +func (d *TunaData) Equal(other *TunaData) bool { + result := fmt.Sprintf("%+v", d) == fmt.Sprintf("%+v", other) + log.Printf("%+v == %+v => %v", d, other, result) + return result +} + +func NewTunaOutput() *TunaOutput { + return &TunaOutput{ + client: &http.Client{ + Timeout: time.Second * 2, + }, + } +} + +func (output *TunaOutput) Post(data *TunaData) (err error) { + body := new(bytes.Buffer) + json.NewEncoder(body).Encode(&struct { + Data *TunaData `json:"data"` + Hostname string `json:"hostname,omitempty"` + Date string `json:"date"` + }{ + Data: data, + Date: time.Now().Format(time.RFC3339), + }) + _, err = output.client.Post("http://localhost:1608", "application/json", body) + return +} diff --git a/icedreammusic/tunaposter.Dockerfile b/icedreammusic/tunaposter.Dockerfile new file mode 100644 index 0000000..0b64c15 --- /dev/null +++ b/icedreammusic/tunaposter.Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.16-alpine + +WORKDIR /usr/src/icedreammusic/ +COPY . . + +RUN cd tunaposter && go build -v . +RUN install -v -m0755 -d /target/usr/local/bin/ +RUN install -v -m0755 tunaposter/tunaposter /target/usr/local/bin/tunaposter + +### + +FROM alpine:3.13 + +COPY --from=0 /target/ / + +CMD ["tunaposter"] diff --git a/icedreammusic/tunaposter/.dockerignore b/icedreammusic/tunaposter/.dockerignore new file mode 100644 index 0000000..18749f7 --- /dev/null +++ b/icedreammusic/tunaposter/.dockerignore @@ -0,0 +1,2 @@ +.docker* +Dockerfile diff --git a/icedreammusic/tunaposter/go.mod b/icedreammusic/tunaposter/go.mod new file mode 100644 index 0000000..74ca2d8 --- /dev/null +++ b/icedreammusic/tunaposter/go.mod @@ -0,0 +1,16 @@ +module github.com/icedream/livestream-tools/icedreammusic/tunaposter + +go 1.16 + +require ( + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/icedream/livestream-tools/icedreammusic/tuna v0.0.0-00010101000000-000000000000 + github.com/nicksnyder/go-i18n v1.10.1 // indirect + github.com/stretchr/testify v1.6.1 // indirect + gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 + gopkg.in/yaml.v2 v2.2.8 // indirect +) + +replace github.com/icedream/livestream-tools/icedreammusic/tuna => ../tuna diff --git a/icedreammusic/tunaposter/go.sum b/icedreammusic/tunaposter/go.sum new file mode 100644 index 0000000..8266dd3 --- /dev/null +++ b/icedreammusic/tunaposter/go.sum @@ -0,0 +1,27 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4= +github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= +github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780 h1:CEBpW6C191eozfEuWdUmIAHn7lwlLxJ7HVdr2e2Tsrw= +gopkg.in/alecthomas/kingpin.v3-unstable v3.0.0-20191105091915-95d230a53780/go.mod h1:3HH7i1SgMqlzxCcBmUHW657sD4Kvv9sC3HpL3YukzwA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/icedreammusic/tunaposter/main.go b/icedreammusic/tunaposter/main.go new file mode 100644 index 0000000..9832946 --- /dev/null +++ b/icedreammusic/tunaposter/main.go @@ -0,0 +1,88 @@ +package main + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "os" + "strings" + "time" + + "github.com/icedream/livestream-tools/icedreammusic/tuna" + "gopkg.in/alecthomas/kingpin.v3-unstable" +) + +var ( + cli = kingpin.New("tunaposter", "Retrieve and copy Tuna now playing information to a Liquidsoap metadata Harbor endpoint.") + + argTunaWebServerURL = cli.Arg("tuna-webserver-url", "Tuna webserver URL").Required().URL() + argLiquidsoapMetaEndpointURL = cli.Arg("liquidsoap-meta-endpoint-url", "Liquidsoap metadata harbor endpoint URL").Required().URL() +) + +type liquidsoapMetadataRequest struct { + Data liquidsoapMetadata `json:"data"` +} + +type liquidsoapMetadata struct { + Artist string `json:"artist"` + Title string `json:"title"` +} + +func init() { + kingpin.MustParse(cli.Parse(os.Args[1:])) +} + +func main() { + client := &http.Client{ + Timeout: 2 * time.Second, + } + + // TODO - shutdown signal handling + var oldTunaData *tuna.TunaData + + for { + // client.Get(url string) + resp, err := client.Get((*argTunaWebServerURL).String()) + if err == nil { + tunaData := new(tuna.TunaData) + if err = json.NewDecoder(resp.Body).Decode(tunaData); err == nil { + // skip empty or same metadata + differentDataReceived := oldTunaData == nil || + oldTunaData.Title != tunaData.Title || + len(oldTunaData.Artists) != len(tunaData.Artists) + if !differentDataReceived { + for i, artist := range oldTunaData.Artists { + differentDataReceived = differentDataReceived || artist != tunaData.Artists[i] + } + } + if differentDataReceived && tunaData.Artists != nil && len(tunaData.Artists) > 0 && len(tunaData.Title) > 0 { + liquidsoapData := &liquidsoapMetadataRequest{ + Data: liquidsoapMetadata{ + Artist: strings.Join(tunaData.Artists, ", "), + Title: tunaData.Title, + }, + } + postBuf := new(bytes.Buffer) + if err = json.NewEncoder(postBuf).Encode(liquidsoapData); err == nil { + postBufCopy := postBuf.Bytes() + log.Println("Will send new metadata:", string(postBufCopy)) + if _, err = client.Post((*argLiquidsoapMetaEndpointURL).String(), "application/json", bytes.NewReader(postBufCopy)); err == nil { + oldTunaData = tunaData + } else { + log.Printf("WARNING: Failed to post metadata to Liquidsoap harbor endpoint: %s", err.Error()) + } + } else { + log.Printf("WARNING: Failed to encode metadata for Liquidsoap harbor endpoint: %s", err.Error()) + } + } + } else { + log.Printf("WARNING: Failed to decode metadata from Tuna webserver: %s", err.Error()) + } + } else { + log.Printf("WARNING: Failed to retrieve metadata from Tuna webserver, resetting old data: %s", err.Error()) + oldTunaData = nil + } + time.Sleep(time.Second) + } +}