@@ 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",
+]
@@ 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))
+}