Browse Source

it's alive!

sungo 7 months ago
parent
commit
3b92169722
3 changed files with 204 additions and 0 deletions
  1. 17 0
      Makefile
  2. 63 0
      README.md
  3. 124 0
      main.go

+ 17 - 0
Makefile

@@ -0,0 +1,17 @@
+default: check np
+
+.PHONY: clean
+clean: ## Clean up build artifacts
+	rm -f np
+
+np: main.go ## Build the main binary
+	go build -o np main.go
+
+check: ## Using staticcheck, ensure that the code matches best practices
+	staticcheck main.go
+
+
+.PHONY: help
+help: ## Display this help message
+	@echo "make(1) targets:"
+	@grep -E '^[a-zA-Z_.-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

+ 63 - 0
README.md

@@ -0,0 +1,63 @@
+# What?
+
+Grabs icecast metadata from an audio stream and dumps out the stream title.
+
+# Usage
+
+```
+$ ./np -url http://my.station
+My Artist - My Song
+
+$ ./np -url http://my.station -suffix "#nowplaying"
+My Artist - My Song #nowplaying
+```
+
+All errors go to stderr so you can `./np 2>/dev/null` to avoid errors showing
+up wherever.
+
+# Testing
+
+Tested against:
+
+* hackers.town, running icecast 2.6
+* di.fm, running icecast 2.1
+
+# Support / Contributing
+
+This is a personal side project and will get about that much attention,
+maybe less. If you have patches, feel free to contact me (see
+https://sungo.wtf) but I make no promise as to when or if I'll respond.
+But, feel free to fork the code, respecting the license, and have your
+way with it.
+
+# License
+
+Copyright Matt Cashner (sungo)
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+1. Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright
+notice, this list of conditions and the following disclaimer in the
+documentation and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+

+ 124 - 0
main.go

@@ -0,0 +1,124 @@
+// Copyright Matt Cashner (sungo). Licensed under BSD-3-Clause. See the
+// included LICENSE file
+
+package main
+
+//lint:file-ignore ST1005 I like my error strings pretty
+
+// http://www.smackfu.com/stuff/programming/shoutcast.html
+
+import (
+	"bufio"
+	"flag"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+)
+
+func bail(err error) {
+	fmt.Fprintf(os.Stderr, "%s\n", err)
+	os.Exit(1)
+}
+
+func main() {
+	flags := struct {
+		URL    string
+		Suffix string
+	}{}
+
+	flag.StringVar(
+		&flags.URL,
+		"url",
+		"http://hackers.town:8000",
+		"URL for the icecast server",
+	)
+
+	flag.StringVar(
+		&flags.Suffix,
+		"suffix",
+		"",
+		"Content to append after the Now Playing data",
+	)
+
+	flag.Parse()
+
+	client := &http.Client{}
+
+	req, err := http.NewRequest("GET", flags.URL, nil)
+	if err != nil {
+		bail(err)
+	}
+
+	req.Header.Add("Icy-Metadata", "1")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		bail(err)
+	}
+
+	defer resp.Body.Close()
+	if resp.StatusCode != 200 {
+		bail(fmt.Errorf("HTTP %d\n", resp.StatusCode))
+	}
+
+	head, ok := resp.Header["Icy-Metaint"]
+
+	if !ok {
+		bail(fmt.Errorf("No Icy-Metaint header returned"))
+	}
+
+	metaint, err := strconv.Atoi(head[0])
+	if err != nil {
+		bail(err)
+	}
+
+	/* Throw this blob out. It's content not metadata */
+	garbage := make([]byte, metaint)
+	_, err = io.ReadFull(resp.Body, garbage)
+	if err != nil {
+		bail(err)
+	}
+
+	reader := bufio.NewReader(resp.Body)
+	metasizeRaw, err := reader.ReadByte()
+	if err != nil {
+		bail(err)
+	}
+	metasize, err := strconv.Atoi(fmt.Sprintf("%d", metasizeRaw))
+	if err != nil {
+		bail(err)
+	}
+
+	metasize = metasize * 16
+
+	metaBuf := make([]byte, metasize)
+	_, err = io.ReadFull(reader, metaBuf)
+	if err != nil {
+		bail(err)
+	}
+
+	metaRaw := strings.Split(fmt.Sprintf("%s\n", metaBuf), ";")
+
+	meta := make(map[string]string)
+	for _, data := range metaRaw {
+		bits := strings.Split(data, "=")
+		if len(bits) != 2 {
+			continue
+		}
+		meta[bits[0]] = bits[1]
+	}
+
+	if val, ok := meta["StreamTitle"]; ok {
+		valCleaned := strings.TrimLeft(
+			strings.TrimRight(
+				val,
+				"'",
+			),
+			"'",
+		)
+		fmt.Printf("%s %s\n", valCleaned, flags.Suffix)
+	}
+}