Initial commit.
commit
da629cf1a0
|
@ -0,0 +1,3 @@
|
|||
*.exe
|
||||
*.dll
|
||||
*.tar*
|
|
@ -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.
|
|
@ -0,0 +1,8 @@
|
|||
.env
|
||||
|
||||
###
|
||||
|
||||
# Blacklisting all files except Golang-relevant ones
|
||||
*
|
||||
!tuna/
|
||||
!tunaposter/
|
|
@ -0,0 +1 @@
|
|||
.env
|
|
@ -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/)
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
foobar2000
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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:])
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
Dockerfile
|
||||
.docker*
|
||||
|
||||
*.tar*
|
||||
*.md
|
||||
*.cmd
|
|
@ -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"]
|
|
@ -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
|
|
@ -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)
|
|
@ -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"))
|
|
@ -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
|
|
@ -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)
|
|
@ -0,0 +1,6 @@
|
|||
*.toml
|
||||
*.yaml
|
||||
*.yml
|
||||
*.json
|
||||
*.db
|
||||
/metacollectord
|
|
@ -0,0 +1,6 @@
|
|||
/metacollectord
|
||||
*.db
|
||||
*.toml
|
||||
*.yaml
|
||||
*.yml
|
||||
*.json
|
|
@ -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"]
|
|
@ -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.
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,726 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css"
|
||||
integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w=="
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Orbitron:ital,wght@0,400;0,700;1,500;1,700&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,700;1,500;1,700&display=swap"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://fonts.googleapis.com/css2?family=Oxanium:ital,wght@0,400;0,700;1,500;1,700&display=swap"
|
||||
/>
|
||||
<script
|
||||
src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"
|
||||
integrity="sha512-bZS47S7sPOxkjU/4Bt0zrhEtWx0y0CRkhEp8IckzK+ltifIIE9EMIMTuT/mEzoIMewUINruDBIR/jJnbguonqQ=="
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
<script>
|
||||
/**
|
||||
* @var HTMLElement overlay
|
||||
*/
|
||||
let overlay;
|
||||
let events = [];
|
||||
/**
|
||||
* @var HTMLElement ticker
|
||||
*/
|
||||
let ticker;
|
||||
// TODO - make processEvent react as instantly as possible to hideOverlay/showOverlay by resetting interval timer
|
||||
function processEvent() {
|
||||
let lastChangeHadEffect = false;
|
||||
do {
|
||||
const message = events.shift();
|
||||
console.debug('message:', message);
|
||||
if (!message) return;
|
||||
switch (message.type) {
|
||||
case 'show':
|
||||
lastChangeHadEffect = showOverlay(message.metadata, true);
|
||||
break;
|
||||
case 'hide':
|
||||
lastChangeHadEffect = hideOverlay(true);
|
||||
break;
|
||||
}
|
||||
} while (!lastChangeHadEffect);
|
||||
}
|
||||
function hideOverlay(immediate = false) {
|
||||
if (!immediate) {
|
||||
events.push({ type: 'hide' });
|
||||
return;
|
||||
}
|
||||
if (overlay.classList.contains('active')) {
|
||||
overlay.classList.add('hidden');
|
||||
overlay.classList.remove('active');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
function millisecondsToTimestamp(ms) {
|
||||
const milliseconds = ms % 1000;
|
||||
const totalSeconds = Math.floor(ms / 1000);
|
||||
const seconds = totalSeconds % 60;
|
||||
const totalMinutes = Math.floor(totalSeconds / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
const hours = totalHours;
|
||||
return `${hours > 0 ? `${hours}:` : ''}${
|
||||
hours > 0 ? minutes.toString().padStart(2, '0') : minutes
|
||||
}:${seconds.toString().padStart(2, '0')}`;
|
||||
}
|
||||
function showOverlay(metadata = null, immediate = false) {
|
||||
if (!immediate) {
|
||||
events.push({ type: 'show', metadata });
|
||||
return;
|
||||
}
|
||||
if (metadata) {
|
||||
const {
|
||||
id,
|
||||
image,
|
||||
cover = '',
|
||||
artist = '',
|
||||
extra = '',
|
||||
heading = 'Now playing',
|
||||
label = '',
|
||||
title = '',
|
||||
text = '',
|
||||
duration = 0,
|
||||
progress = 0,
|
||||
} = metadata;
|
||||
switchProgress(id);
|
||||
overlay.querySelector('.nowplaying-heading').innerText = heading;
|
||||
overlay.querySelector(
|
||||
'.nowplaying-heading-layer'
|
||||
).innerText = heading;
|
||||
overlay.querySelector('.nowplaying-title').innerText = title;
|
||||
overlay.querySelector('.nowplaying-artist').innerText = artist;
|
||||
overlay.querySelector('.nowplaying-extra').innerText = extra;
|
||||
overlay.querySelector('.nowplaying-label').innerText = label;
|
||||
overlay.querySelector('.nowplaying-text').innerHTML = text;
|
||||
const coverWrapper = overlay.querySelector(
|
||||
'.nowplaying-cover-wrapper'
|
||||
);
|
||||
let coverImg = coverWrapper.querySelector('img');
|
||||
if (image) {
|
||||
if (coverImg) {
|
||||
coverImg.remove();
|
||||
}
|
||||
coverImg = image;
|
||||
coverImg.classList.add('nowplaying-cover');
|
||||
coverWrapper.appendChild(coverImg);
|
||||
coverWrapper.classList.remove('hidden');
|
||||
} else if (typeof cover === 'string' && cover.length > 0) {
|
||||
const image = new Image();
|
||||
image.src = cover;
|
||||
// image.style.position = 'absolute';
|
||||
// image.style.left = 0;
|
||||
// image.style.top = -1;
|
||||
// image.style.width = 1;
|
||||
// image.style.height = 1;
|
||||
// document.body.appendChild(image);
|
||||
// image.offsetHeight; // force render image
|
||||
// const completed = image.completed;
|
||||
// console.log({ completed });
|
||||
// document.body.removeChild(image);
|
||||
// image.style.opacity = 1;
|
||||
// image.style.position = null;
|
||||
// image.style.left = null;
|
||||
// image.style.top = null;
|
||||
// image.style.width = null;
|
||||
// image.style.height = null;
|
||||
// if (!completed) {
|
||||
const start = Date.now();
|
||||
image.addEventListener('load', () => {
|
||||
let immediate = false;
|
||||
if (Date.now() - start < 500) {
|
||||
immediate = true;
|
||||
}
|
||||
showOverlay(
|
||||
{
|
||||
...metadata,
|
||||
image,
|
||||
},
|
||||
immediate
|
||||
); // requeue to display
|
||||
});
|
||||
image.addEventListener('error', () => {
|
||||
showOverlay(metadata); // requeue to try again
|
||||
});
|
||||
return;
|
||||
// }
|
||||
// showOverlay(
|
||||
// {
|
||||
// ...metadata,
|
||||
// image,
|
||||
// },
|
||||
// true
|
||||
// );
|
||||
} else {
|
||||
if (coverImg) {
|
||||
coverImg.remove();
|
||||
}
|
||||
coverWrapper.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
if (!overlay.classList.contains('active')) {
|
||||
overlay.classList.remove('hidden');
|
||||
overlay.offsetHeight; // flush
|
||||
overlay.classList.add('active');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
let progresses = {};
|
||||
let currentProgressId = null;
|
||||
let progressUpdateTimer = null;
|
||||
function getAlignedTimestamp(ts = Date.now()) {
|
||||
return Math.floor(ts / 1000) * 1000;
|
||||
}
|
||||
function switchProgress(id) {
|
||||
if (id === currentProgressId) {
|
||||
return;
|
||||
}
|
||||
console.log('switching progress to:', id);
|
||||
if (currentProgressId !== null) {
|
||||
delete progresses[currentProgressId];
|
||||
}
|
||||
if (progressUpdateTimer !== null) {
|
||||
clearInterval(progressUpdateTimer);
|
||||
}
|
||||
currentProgressId = id;
|
||||
const lastProgress = progresses[currentProgressId] || {};
|
||||
resetProgressBar();
|
||||
if (
|
||||
typeof lastProgress.progress === 'number' &&
|
||||
typeof lastProgress.duration === 'number'
|
||||
) {
|
||||
updateProgressBar({
|
||||
progress: lastProgress.progress,
|
||||
duration: lastProgress.duration,
|
||||
});
|
||||
}
|
||||
progressUpdateTimer = setInterval(function () {
|
||||
const lastProgress = progresses[id] || {};
|
||||
if (
|
||||
typeof lastProgress.progress !== 'number' ||
|
||||
typeof lastProgress.duration !== 'number'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const newProgress = Date.now() - lastProgress.playbackStartedAt;
|
||||
updateProgressBar({
|
||||
duration: lastProgress.duration,
|
||||
progress: newProgress,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
function resetProgressBar() {
|
||||
const progressWrapper = overlay.querySelector('.nowplaying-progress');
|
||||
const progressInner = progressWrapper.querySelector(
|
||||
'.nowplaying-progress-inner'
|
||||
);
|
||||
const newProgressInner = progressInner.cloneNode(true);
|
||||
newProgressInner.style.width = '0%';
|
||||
progressInner.parentElement.replaceChild(
|
||||
newProgressInner,
|
||||
progressInner
|
||||
);
|
||||
}
|
||||
function showProgress({ id, duration, progress }) {
|
||||
const lastProgress = progresses[id] || {};
|
||||
if (typeof duration !== 'number' || duration <= 0) {
|
||||
// cause progress bar to be hidden by setting zero values
|
||||
lastProgress.duration = 0;
|
||||
lastProgress.progress = 0;
|
||||
lastProgress.playbackStartedAt = 0;
|
||||
} else {
|
||||
const { playbackStartedAt } = lastProgress;
|
||||
lastProgress.playbackStartedAt = Date.now() - progress;
|
||||
if (typeof playbackStartedAt === 'number') {
|
||||
const supposedProgress = Date.now() - playbackStartedAt;
|
||||
const progressDifference = Math.abs(progress - supposedProgress);
|
||||
if (progressDifference < 2000) {
|
||||
// prefer oldest timestamp to make the progress less jumpy
|
||||
lastProgress.playbackStartedAt = Math.min(
|
||||
playbackStartedAt || Infinity,
|
||||
Date.now() - progress
|
||||
);
|
||||
}
|
||||
console.log({
|
||||
progress,
|
||||
supposedProgress,
|
||||
progressDifference,
|
||||
oldPlaybackStartedAt: playbackStartedAt,
|
||||
newPlaybackStartedAt: lastProgress.playbackStartedAt,
|
||||
});
|
||||
} else {
|
||||
console.log('new progress');
|
||||
}
|
||||
|
||||
lastProgress.duration = duration;
|
||||
lastProgress.progress = Date.now() - lastProgress.playbackStartedAt;
|
||||
// updateProgressBar({ duration, progress: lastProgress.progress });
|
||||
|
||||
// progressUpdateTimer = setInterval(function () {
|
||||
// const newProgress = Date.now() - lastProgress.playbackStartedAt;
|
||||
// updateProgressBar({ duration, progress: newProgress });
|
||||
// }, 100);
|
||||
}
|
||||
|
||||
progresses[id] = lastProgress;
|
||||
}
|
||||
function updateProgressBar({ duration, progress } = {}) {
|
||||
const progressWrapper = overlay.querySelector('.nowplaying-progress');
|
||||
if (duration > 0 && progress >= 0) {
|
||||
progressWrapper.classList.remove('hidden');
|
||||
const progressInner = progressWrapper.querySelector(
|
||||
'.nowplaying-progress-inner'
|
||||
);
|
||||
progressInner.style.width = `${((100 * progress) / duration).toFixed(
|
||||
3
|
||||
)}%`;
|
||||
progressWrapper
|
||||
.querySelectorAll('.nowplaying-progress-text-total')
|
||||
.forEach((e) => (e.innerText = millisecondsToTimestamp(duration)));
|
||||
progressWrapper
|
||||
.querySelectorAll('.nowplaying-progress-text-current')
|
||||
.forEach((e) => (e.innerText = millisecondsToTimestamp(progress)));
|
||||
} else {
|
||||
progressWrapper.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
function getTrackIdentifier({ title, artist }) {
|
||||
return `${JSON.stringify({
|
||||
title,
|
||||
artist,
|
||||
})}`;
|
||||
}
|
||||
function tick() {
|
||||
const currentlyActiveElement = ticker.querySelector('.active');
|
||||
let nextElement;
|
||||
if (!currentlyActiveElement) {
|
||||
nextElement = ticker.firstElementChild;
|
||||
} else {
|
||||
nextElement =
|
||||
currentlyActiveElement.nextElementSibling ||
|
||||
ticker.firstElementChild;
|
||||
currentlyActiveElement.classList.remove('active');
|
||||
currentlyActiveElement.classList.add('hidden');
|
||||
}
|
||||
while (nextElement.nodeType === Node.TEXT_NODE /* text node */) {
|
||||
nextElement =
|
||||
nextElement.nextElementSibling || ticker.firstElementChild;
|
||||
}
|
||||
console.debug('next ticker elemnet:', nextElement);
|
||||
nextElement.classList.remove('hidden');
|
||||
nextElement.classList.add('active');
|
||||
}
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
overlay = document.getElementById('left');
|
||||
console.debug('message processing enabled');
|
||||
setInterval(processEvent, 1000);
|
||||
|
||||
ticker = document.getElementById('ticker');
|
||||
setInterval(tick, 5000);
|
||||
});
|
||||
</script>
|
||||
<script>
|
||||
/* Fetch data from Tuna API. */
|
||||
let lastId = null;
|
||||
setInterval(async function () {
|
||||
const { data } = await axios.get('http://localhost:1608');
|
||||
// console.info(data);
|
||||
const almostEnding =
|
||||
data.progress > 0 && data.duration > 0
|
||||
? data.progress > data.duration - 15000
|
||||
: false;
|
||||
if (data.status === 'stopped' || almostEnding) {
|
||||
// stopped
|
||||
hideOverlay();
|
||||
} else {
|
||||
// playing or paused
|
||||
const artistString = data.artists
|
||||
? data.artists.reduce((previous, currentValue, currentIndex) => {
|
||||
if (previous.length <= 0) {
|
||||
return currentValue;
|
||||
}
|
||||
if (currentIndex === data.artists.length - 1) {
|
||||
return `${previous} & ${currentValue}`;
|
||||
}
|
||||
return `${previous}, ${currentValue}`;
|
||||
}, '')
|
||||
: undefined;
|
||||
const id = getTrackIdentifier({
|
||||
title: data.title,
|
||||
artist: artistString,
|
||||
});
|
||||
if (lastId !== null && id !== lastId) {
|
||||
hideOverlay();
|
||||
}
|
||||
lastId = id;
|
||||
showOverlay({
|
||||
id,
|
||||
title: data.title
|
||||
? data.title
|
||||
.replace(
|
||||
/\s+\((original|extended)(\s+(edit|mix|version))?\)/i,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/\((.+) (?:original|extended)(\s+(edit|remix|mix|version))?\)/i,
|
||||
'($1$2)'
|
||||
)
|
||||
: '',
|
||||
artist: artistString,
|
||||
label: data.label
|
||||
? data.label
|
||||
.replace(/\bw?reck?(ord(ing)?)?s?\b/i, '')
|
||||
.replace(/\b(digital|audio|music)(\s+bundles?)?$/i, '')
|
||||
.replace(/\s+\([\s\da-z]+\)$/i, '')
|
||||
.replace(/\b(holland|italy)\b/i, '')
|
||||
.trim()
|
||||
: null,
|
||||
cover: data.cover_url,
|
||||
});
|
||||
showProgress({
|
||||
id,
|
||||
duration: data.duration,
|
||||
progress: data.progress,
|
||||
});
|
||||
}
|
||||
}, 2000);
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,700;1,500;1,700&display=swap'); */
|
||||
/* @import url('https://fonts.googleapis.com/css2?family=Orbitron:ital,wght@0,400;0,700;1,500;1,700&display=swap'); */
|
||||
:root {
|
||||
--background: #249;
|
||||
--color: white;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
background: black;
|
||||
}
|
||||
.nowplaying-heading-row,
|
||||
.logo {
|
||||
font-family: 'Orbitron', sans-serif;
|
||||
}
|
||||
body {
|
||||
font-family: 'Oxanium', sans-serif;
|
||||
/* font-family: 'Bahnschrift', sans-serif; */
|
||||
/* font-family: 'Montserrat', Arial, Helvetica, sans-serif; */
|
||||
font-size: 24px;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 2em;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
margin-right: 1em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
flex-grow: 0;
|
||||
text-align: right;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.nowplaying-flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.nowplaying-top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.nowplaying-cover-wrapper {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
.nowplaying-cover-wrapper.hidden {
|
||||
display: none;
|
||||
}
|
||||
.nowplaying-cover-wrapper img {
|
||||
height: 4em;
|
||||
width: 4em;
|
||||
}
|
||||
.nowplaying-flow {
|
||||
transition: opacity linear 1s;
|
||||
}
|
||||
.nowplaying-heading,
|
||||
.nowplaying-heading-layer,
|
||||
.nowplaying-content-wrapper,
|
||||
.nowplaying-cover-wrapper {
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
.nowplaying-cover-wrapper {
|
||||
margin-right: 0.2em;
|
||||
}
|
||||
.nowplaying-heading,
|
||||
.nowplaying-heading-layer,
|
||||
.nowplaying-content-wrapper {
|
||||
padding-left: 0.2em;
|
||||
padding-right: 0.2em;
|
||||
}
|
||||
.nowplaying-heading-row {
|
||||
text-transform: lowercase;
|
||||
/* text-transform: uppercase; */
|
||||
position: relative;
|
||||
min-height: 1.66em;
|
||||
}
|
||||
.nowplaying-progress {
|
||||
position: relative;
|
||||
height: 1em;
|
||||
min-height: 1em;
|
||||
max-height: 1em;
|
||||
color: white;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: white 1px solid;
|
||||
}
|
||||
.nowplaying-progress.hidden {
|
||||
display: none;
|
||||
}
|
||||
.nowplaying-progress-text {
|
||||
font-size: 0.8em;
|
||||
word-break: keep-all;
|
||||
white-space: nowrap;
|
||||
padding-left: calc(0.2em * (1 / 0.8));
|
||||
padding-right: calc(0.2em * (1 / 0.8));
|
||||
}
|
||||
.nowplaying-progress-text-current {
|
||||
font-weight: bold;
|
||||
}
|
||||
.nowplaying-progress-inner {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: white;
|
||||
color: var(--background);
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
transition: width linear 0.2s;
|
||||
}
|
||||
.nowplaying-heading-row,
|
||||
.nowplaying-content-wrapper,
|
||||
.nowplaying-progress {
|
||||
margin-bottom: 0.33em;
|
||||
}
|
||||
.nowplaying-heading,
|
||||
.nowplaying-heading-layer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
}
|
||||
.nowplaying-heading {
|
||||
background: var(--background);
|
||||
color: var(--color);
|
||||
}
|
||||
.nowplaying-heading-layer {
|
||||
background: var(--color);
|
||||
color: var(--background);
|
||||
}
|
||||
.active .nowplaying-heading,
|
||||
.hidden .nowplaying-heading,
|
||||
.active .nowplaying-heading-layer,
|
||||
.hidden .nowplaying-heading-layer {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
|
||||
.nowplaying-heading-wrapper {
|
||||
opacity: 1;
|
||||
transition: opacity linear 1s;
|
||||
}
|
||||
.active .nowplaying-heading-wrapper {
|
||||
transition-duration: 0.1s;
|
||||
}
|
||||
/* .hidden .nowplaying-heading-wrapper {
|
||||
opacity: 0;
|
||||
} */
|
||||
.active .nowplaying-heading {
|
||||
transition: clip-path ease-out 0.5s 0.5s;
|
||||
/* animation: random forwards infinite 20s 0.5s; */
|
||||
}
|
||||
.active .nowplaying-heading-layer {
|
||||
transition: clip-path ease-in 0.5s;
|
||||
}
|
||||
|
||||
.hidden .nowplaying-heading,
|
||||
.hidden .nowplaying-heading-layer {
|
||||
clip-path: inset(0 0 0 100%);
|
||||
}
|
||||
.hidden .nowplaying-heading-layer {
|
||||
transition: clip-path ease-out 0.5s 0.5s;
|
||||
}
|
||||
.hidden .nowplaying-heading {
|
||||
transition: clip-path ease-in 0.5s;
|
||||
}
|
||||
.nowplaying-flow {
|
||||
margin-bottom: 0.5em;
|
||||
/* text-transform: uppercase; */
|
||||
}
|
||||
.nowplaying-flow,
|
||||
.hidden .nowplaying-flow {
|
||||
opacity: 0;
|
||||
}
|
||||
.active .nowplaying-flow {
|
||||
opacity: 1;
|
||||
}
|
||||
.nowplaying-heading-wrapper {
|
||||
background: white;
|
||||
}
|
||||
.nowplaying-artist {
|
||||
font-weight: bold;
|
||||
}
|
||||
.nowplaying-label,
|
||||
.nowplaying-extra {
|
||||
font-style: italic;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
.nowplaying-label:not(:empty)::before {
|
||||
display: inline;
|
||||
content: '[';
|
||||
}
|
||||
.nowplaying-label:not(:empty)::after {
|
||||
display: inline;
|
||||
content: ']';
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-weight: bold;
|
||||
}
|
||||
.episode {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
@keyframes random {
|
||||
47.5% {
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
50% {
|
||||
clip-path: inset(0 0 0 100%);
|
||||
}
|
||||
50.1% {
|
||||
transition-duration: 0;
|
||||
clip-path: inset(0 100% 0 0);
|
||||
}
|
||||
52.5% {
|
||||
transition-duration: 0.5s;
|
||||
clip-path: inset(0 0 0 0);
|
||||
}
|
||||
}
|
||||
|
||||
.ticker {
|
||||
position: relative;
|
||||
min-height: 1.5em;
|
||||
font-size: 0.7em;
|
||||
}
|
||||
.ticker > * {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
transition: opacity linear 0.5s;
|
||||
}
|
||||
.ticker .active {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="left" id="left">
|
||||
<div class="nowplaying-heading-row">
|
||||
<span class="nowplaying-heading-wrapper">
|
||||
<span class="nowplaying-heading-layer">Now playing</span>
|
||||
<span class="nowplaying-heading">Now playing</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="nowplaying-flow">
|
||||
<div class="nowplaying-top">
|
||||
<div class="nowplaying-cover-wrapper"></div>
|
||||
<div class="nowplaying-content-wrapper">
|
||||
<div class="nowplaying-artist"></div>
|
||||
<div class="nowplaying-title"></div>
|
||||
<div class="nowplaying-extra"></div>
|
||||
<div class="nowplaying-label"></div>
|
||||
<div class="nowplaying-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nowplaying-progress">
|
||||
<div class="nowplaying-progress-text" style="width: 0%">
|
||||
<span class="nowplaying-progress-text-current">0:00</span>
|
||||
/
|
||||
<span class="nowplaying-progress-text-total">0:00</span>
|
||||
</div>
|
||||
<div class="nowplaying-progress-inner" style="width: 0%">
|
||||
<div class="nowplaying-progress-text" style="width: 0%">
|
||||
<span class="nowplaying-progress-text-current">0:00</span>
|
||||
/
|
||||
<span class="nowplaying-progress-text-total">0:00</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ticker" class="ticker">
|
||||
<div>
|
||||
<span class="fab fa-soundcloud"> </span>
|
||||
https://soundcloud.com/icedream
|
||||
</div>
|
||||
<div>
|
||||
<span class="fab fa-twitter"> </span>
|
||||
https://twitter.com/icedream2k9
|
||||
</div>
|
||||
<div>
|
||||
<span class="fab fa-facebook"> </span>
|
||||
https://facebook.com/icedreammusic
|
||||
</div>
|
||||
<div>
|
||||
<span class="fab fa-twitch"> </span>
|
||||
https://twitch.tv/icedreammusic
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<div>
|
||||
<div class="logo"></div>
|
||||
<div class="episode"></div>
|
||||
</div>
|
||||
<div>
|
||||
<img
|
||||
src="file:///D:/Users/Icedream/Pictures/Icedream/artistlogo.png"
|
||||
style="height: 3.7em"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1 @@
|
|||
/prime4
|
|
@ -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
|
||||
)
|
|
@ -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=
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
module github.com/icedream/livestream-tools/icedreammusic/tuna
|
||||
|
||||
go 1.16
|
|
@ -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
|
||||
}
|
|
@ -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"]
|
|
@ -0,0 +1,2 @@
|
|||
.docker*
|
||||
Dockerfile
|
|
@ -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
|
|
@ -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=
|
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue