226 lines
6.5 KiB
Go
226 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"image/jpeg"
|
|
_ "image/jpeg"
|
|
_ "image/png"
|
|
|
|
"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.")
|
|
|
|
argLiquidsoapMetaEndpointURL = cli.Arg("liquidsoap-meta-endpoint-url", "Liquidsoap metadata harbor endpoint URL").Required().URL()
|
|
argTunaWebServerURL = cli.Arg("tuna-webserver-url", "Tuna webserver URL").Default("http://localhost:1608").URL()
|
|
)
|
|
|
|
type liquidsoapMetadataRequest struct {
|
|
Data liquidsoapMetadata `json:"data"`
|
|
}
|
|
|
|
type liquidsoapMetadata struct {
|
|
CoverURL string `json:"cover_url,omitempty"`
|
|
MetadataBlockPicture string `json:"metadata_block_picture,omitempty"`
|
|
Artist string `json:"artist,omitempty"`
|
|
Title string `json:"title"`
|
|
Publisher string `json:"publisher,omitempty"`
|
|
Year string `json:"year,omitempty"`
|
|
}
|
|
|
|
func (lm *liquidsoapMetadata) SetCover(r io.Reader, compressToJPEG bool) (err error) {
|
|
description := ""
|
|
|
|
// prepare data for reuse
|
|
imageBuffer := new(bytes.Buffer)
|
|
if _, err = io.Copy(imageBuffer, r); err != nil {
|
|
return
|
|
}
|
|
imageBytes := imageBuffer.Bytes()
|
|
|
|
// parse image metadata
|
|
decodedImage, imageFormatName, err := image.Decode(bytes.NewReader(imageBytes))
|
|
if err != nil {
|
|
return
|
|
}
|
|
mime := ""
|
|
switch imageFormatName {
|
|
case "jpeg":
|
|
mime = "image/jpeg"
|
|
case "png":
|
|
mime = "image/png"
|
|
default:
|
|
err = image.ErrFormat
|
|
}
|
|
|
|
// compress image if wanted
|
|
if compressToJPEG && imageFormatName != "jpeg" {
|
|
imageBuffer = new(bytes.Buffer)
|
|
if err = jpeg.Encode(imageBuffer, decodedImage, &jpeg.Options{
|
|
Quality: 75,
|
|
}); err != nil {
|
|
return
|
|
}
|
|
mime = "image/jpeg"
|
|
}
|
|
|
|
// Build METADATA_BLOCK_PICTURE
|
|
// https://xiph.org/flac/format.html#metadata_block_picture
|
|
|
|
w := new(strings.Builder)
|
|
wb64 := base64.NewEncoder(base64.StdEncoding, w)
|
|
binary.Write(wb64, binary.BigEndian, uint32(3)) // type: cover (front)
|
|
binary.Write(wb64, binary.BigEndian, uint32(len(mime))) // mime length
|
|
wb64.Write([]byte(mime)) // mime
|
|
binary.Write(wb64, binary.BigEndian, uint32(len(description))) // description length
|
|
wb64.Write([]byte(description)) // description
|
|
binary.Write(wb64, binary.BigEndian, uint32(decodedImage.Bounds().Dx())) // pixel width
|
|
binary.Write(wb64, binary.BigEndian, uint32(decodedImage.Bounds().Dy())) // pixel height
|
|
|
|
// color depth and paletted color count
|
|
var bpp uint32
|
|
var colorsUsed uint32
|
|
switch v := decodedImage.(type) {
|
|
case *image.Gray:
|
|
bpp = 8
|
|
case *image.Paletted:
|
|
bpp = 8
|
|
colorsUsed = uint32(len(v.Palette))
|
|
case *image.RGBA:
|
|
if v.Opaque() {
|
|
bpp = 24
|
|
} else {
|
|
bpp = 32
|
|
}
|
|
case *image.NRGBA:
|
|
if v.Opaque() {
|
|
bpp = 24
|
|
} else {
|
|
bpp = 32
|
|
}
|
|
default:
|
|
bpp = 24
|
|
}
|
|
binary.Write(wb64, binary.BigEndian, bpp)
|
|
binary.Write(wb64, binary.BigEndian, colorsUsed)
|
|
|
|
binary.Write(wb64, binary.BigEndian, uint32(len(imageBytes))) // raw image size
|
|
wb64.Write(imageBytes)
|
|
|
|
wb64.Close()
|
|
lm.MetadataBlockPicture = w.String()
|
|
return
|
|
}
|
|
|
|
func init() {
|
|
kingpin.MustParse(cli.Parse(os.Args[1:]))
|
|
}
|
|
|
|
func main() {
|
|
client := &http.Client{
|
|
Timeout: 10 * 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 {
|
|
liquidsoapMetadata := &liquidsoapMetadata{
|
|
Artist: strings.Join(tunaData.Artists, ", "),
|
|
CoverURL: tunaData.CoverURL,
|
|
Publisher: tunaData.Label,
|
|
Title: tunaData.Title,
|
|
}
|
|
|
|
if tunaData.Year > 0 {
|
|
liquidsoapMetadata.Year = fmt.Sprintf("%d", tunaData.Year)
|
|
}
|
|
|
|
// transfer cover to liquidsoap metadata
|
|
if coverURL, err := url.Parse(tunaData.CoverURL); err == nil {
|
|
if strings.EqualFold(coverURL.Scheme, "http") ||
|
|
strings.EqualFold(coverURL.Scheme, "https") {
|
|
log.Println("Downloading cover:", tunaData.CoverURL)
|
|
resp, err := http.Get(tunaData.CoverURL)
|
|
if err == nil {
|
|
err = liquidsoapMetadata.SetCover(resp.Body, true)
|
|
resp.Body.Close()
|
|
if err != nil {
|
|
log.Println("WARNING: Failed to transfer cover to liquidsoap metadata, skipping:", err.Error())
|
|
}
|
|
}
|
|
|
|
// remove reference to localhost/127.*.*.*
|
|
localhost := coverURL.Host == "localhost" || strings.HasSuffix(coverURL.Host, ".localhost")
|
|
if !localhost {
|
|
if ip := net.ParseIP(coverURL.Host); ip != nil {
|
|
localhost = ip[0] == 127
|
|
}
|
|
}
|
|
if localhost {
|
|
liquidsoapMetadata.CoverURL = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
liquidsoapData := &liquidsoapMetadataRequest{
|
|
Data: *liquidsoapMetadata,
|
|
}
|
|
|
|
postBuf := new(bytes.Buffer)
|
|
jsonEncoder := json.NewEncoder(postBuf)
|
|
jsonEncoder.SetEscapeHTML(false)
|
|
if err = jsonEncoder.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)
|
|
}
|
|
}
|