~vladh/radio-api

f39dc7c01ddc2972c718fd0541e48d4e299bdce7 — Vlad-Stefan Harbuz 2 years ago
initial commit
5 files changed, 395 insertions(+), 0 deletions(-)

A README.md
A config.toml
A go.mod
A go.sum
A main.go
A  => README.md +3 -0
@@ 1,3 @@
# Radio API

API for radio.vladh.net.

A  => config.toml +192 -0
@@ 1,192 @@
MusicRoot = "/mnt/balcora/bentus/music"

[[Stations]]
Id = "classical-post-1945"
Name = "Classical Post 1945"
Paths = [
    "Arnold Schoenberg",
    "Arvo Part",
    "Balanescu Quartet",
    "Ben Frost",
    "Brian Eno",
    "Colin Stetson & Sarah Neufeld",
    "David Lang",
    "Elaine Funaro",
    "Ellen Reid",
    "Floating Points",
    "Hans Zimmer",
    "Hilary Hahn",
    "Jessica Curry",
    "Jonny Greenwood",
    "Kammen and Swan",
    "Kjartan Sveinsson",
    "Kronos Quartet",
    "Leonard Bernstein",
    "Max Richter",
    "Michael Torke",
    "Pat Martino",
    "Philip Glass",
    "Rachel Grimes",
    "Rachel's",
    "Richard Reed Parry",
    "Ryuichi Sakamoto",
    "Steve Reich",
    "Taylor Deupree",
    "Trent Reznor & Atticus Ross",
    "William Basinski",
    "h hunt",
    "Óskar Guðjónsson & Skúli Sverrisson",
]

[[Stations]]
Id = "classical-pre-1945"
Name = "Classical Pre 1945"
Paths = [
    "Antonin Dvorak",
    "Bela Bartok",
    "Claude Debussy",
    "Edvard Grieg",
    "Edward Elgar",
    "Erik Satie",
    "Franz Schubert",
    "Frederic Chopin",
    "George Gershwin",
    "Gustav Mahler",
    "Johann Sebastian Bach",
    "Johannes Brahms",
    "John Dowland",
    "John Williams",
    "Ludwig van Beethoven",
    "Maurice Ravel",
    "Richard Strauss",
    "Sergei Rachmaninoff",
    "Shostakovich",
    "Tchaikovsky",
    "The Orthodox Singers",
    "Ukrainian Choir Dzvinochok",
    "Wolfgang Amadeus Mozart",
    "Yo-Yo Ma",
    "Yo-Yo Ma, Wynton Marsalis, Cho-Liang Lin",
]

[[Stations]]
Id = "hyperpop"
Name = "Hyperpop"
Paths = [
    "100 gecs",
    "Anamanaguchi",
    "Caroline Polachek",
    "Charli XCX",
    "Clarence Clarity",
    "Iglooghost",
    "Kai Whiston",
    "MEGAREX",
    "aldn",
    "gupi",
    "quinn",
]

[[Stations]]
Id = "japanese-and-korean"
Name = "Japanese and Korean"
Paths = [
    "Aimer",
    "BLACKPINK",
    "Back Number",
    "CAPSULE",
    "DADARAY",
    "Frederic",
    "GFRIEND",
    "GReeeeN",
    "Gesu no Kiwami Otome",
    "Haru Nemuri",
    "Haruka Nakamura",
    "Hyde",
    "Hyuna",
    "Ichiko Aoba",
    "Kitri",
    "Kyary Pamyu Pamyu",
    "L'Arc-en-Ciel",
    "Lamp",
    "MEGAREX",
    "MIYAVI",
    "MONDO GROSSO",
    "MONGOL800",
    "Masaki Suda",
    "Mito Tsukino",
    "Mono",
    "Official HIGE DANdism",
    "Perfume",
    "Polkadot Stingray",
    "RADWIMPS",
    "Rina Sawayama",
    "Ryuichi Sakamoto",
    "Shiina Ringo",
    "THREE1989",
    "Toe",
    "Tokyo Jihen",
    "Utada Hikaru",
    "Wednesday Campanella",
    "Yakushimaru Etsuko",
    "aimyon",
    "supercell",
]

[[Stations]]
Id = "pop"
Name = "Pop"
Paths = [
    "Allie X",
    "Ariana Grande",
    "Beyonce",
    "Carly Rae Jepsen",
    "Dua Lipa",
    "Rina Sawayama",
    "Taylor Swift",
    "The Format",
]

[[Stations]]
Id = "quiet-indie"
Name = "Quiet Indie"
Paths = [
    "Agnes Obel",
    "Aidan Knight",
    "Bon Iver",
    "Haruka Nakamura",
    "Ichiko Aoba",
    "Justin Vernon",
    "Kings of Convenience",
    "Right Away, Great Captain!",
    "Sufjan Stevens",
    "Yvette Young",
]

[[Stations]]
Id = "jazz"
Name = "Jazz"
Paths = [
    "Art Blakey & The Jazz Messengers",
    "Art Ensemble of Chicago",
    "Art Tatum",
    "Bill Evans",
    "Cannonball Adderley",
    "Charles Mingus",
    "Charlie Parker",
    "Dave Brubeck",
    "Django Reinhardt",
    "Eric Dolphy",
    "Hank Mobley",
    "Joe Pass",
    "John Coltrane",
    "John Zorn",
    "Kyle Eastwood",
    "Masada",
    "Miles Davis",
    "Milt Jackson & Modern Jazz Quartet",
    "Ornette Coleman",
    "The Dave Brubeck Quartet",
    "Thelonious Monk",
    "Tom Waits",
    "Wayne Shorter",
]

A  => go.mod +10 -0
@@ 1,10 @@
module git.sr.ht/~vladh/radio-api

go 1.19

require (
	github.com/BurntSushi/toml v1.2.1
	github.com/davecgh/go-spew v1.1.1
	github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086
	github.com/julienschmidt/httprouter v1.3.0
)

A  => go.sum +8 -0
@@ 1,8 @@
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
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-20220618230019-adf36e896086 h1:ORubSQoKnncsBnR4zD9CuYFJCPOCuSNEpWEZrDdBXkc=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=

A  => main.go +182 -0
@@ 1,182 @@
package main

import (
	"encoding/json"
	"log"
	"math/rand"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/BurntSushi/toml"
	"github.com/davecgh/go-spew/spew"
	"github.com/dhowden/tag"
	"github.com/julienschmidt/httprouter"
)

type state struct {
	Playing map[string]Song
}

type Song struct {
	Path string `json:"path"`
	Title string `json:"title"`
	Artist string `json:"artist"`
	Album string `json:"album"`
	AlbumArtist string `json:"albumartist"`
	FileType string `json:"filetype"`
}

type Station struct {
	Id string
	Name string
	Paths []string
}

type Config struct {
	MusicRoot string
	Stations []Station
}

type ErrResponse struct {
	Err string
}

type RandomResponse = Song

var gState state


func isSong(path string) bool {
	return strings.HasSuffix(path, ".flac") ||
		strings.HasSuffix(path, ".mp3") ||
		strings.HasSuffix(path, ".m4a")
}


func getConfig() Config {
	var config Config
	_, err := toml.DecodeFile("config.toml", &config)
	if err != nil { log.Fatal(err) }
	return config
}


func getSong(path string) (Song, error) {
	var err error

	f, err := os.Open(path)
	if err != nil { return Song{}, err}

	m, err := tag.ReadFrom(f)
	if err != nil { return Song{}, err }

	defer f.Close()
	return Song{
		Path: path,
		Title: m.Title(),
		Artist: m.Artist(),
		Album: m.Album(),
		AlbumArtist: m.AlbumArtist(),
		FileType: string(m.FileType()),
	}, nil
}


func getSongsInDirs(dirs []string) ([]string, error) {
	var err error
	songPaths := make([]string, 0)
	config := getConfig()

	for _, dir := range dirs {
		fullDir := config.MusicRoot + "/" + dir
		err = filepath.Walk(fullDir, func(path string, fileinfo os.FileInfo, err error) error {
			if err != nil { return err }
			if isSong(path) {
				songPaths = append(songPaths, path)
			}
			return nil
		})
		if err != nil { return nil, err }
	}

	return songPaths, nil
}


func getStation(config Config, id string) Station {
	for _, candidate := range config.Stations {
		if candidate.Id == id {
			return candidate
		}
	}
	return Station{}
}


func handleNp(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	var err error

	config := getConfig()
	station := getStation(config, ps.ByName("stationId"))

	if len(station.Id) == 0 {
		resData := ErrResponse{Err: "Invalid station"}
		err = json.NewEncoder(w).Encode(resData)
		if err != nil { log.Printf("ERROR: %s\n", err) }
		return
	}

	err = json.NewEncoder(w).Encode(gState.Playing[station.Id])
	if err != nil { log.Printf("ERROR: %s\n", err) }
}


func handleRandom(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	var err error

	config := getConfig()
	station := getStation(config, ps.ByName("stationId"))

	if len(station.Id) == 0 {
		resData := ErrResponse{Err: "Invalid station"}
		err = json.NewEncoder(w).Encode(resData)
		if err != nil { log.Printf("ERROR: %s\n", err) }
		return
	}

	songPaths, err := getSongsInDirs(station.Paths)
	if err != nil {
		resData := ErrResponse{Err: "Could not get songs for station"}
		err = json.NewEncoder(w).Encode(resData)
		if err != nil { log.Printf("ERROR: %s\n", err) }
		return
	}

	chosenPath := songPaths[rand.Intn(len(songPaths))]
	song, err := getSong(chosenPath)
	if err != nil { log.Printf("ERROR: %s\n", err) }

	spew.Dump(song)
	gState.Playing[station.Id] = song

	err = json.NewEncoder(w).Encode(song)
	if err != nil { log.Printf("ERROR: %s\n", err) }
}


func main() {
	rand.Seed(time.Now().Unix())
	log.SetFlags(0)

	gState.Playing = make(map[string]Song)

	router := httprouter.New()
	router.GET("/api/:stationId/random", handleRandom)
	router.GET("/api/:stationId/np", handleNp)

	log.Printf("Listening on port 8100\n")
	log.Fatal(http.ListenAndServe(":8100", router))
}