commit 816c2b593ce1c2707cd03d3063b5633e139e4056 Author: gauthiier Date: Wed Jan 2 16:11:22 2019 +0100 haha diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16026b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +serve +main diff --git a/archive/archive.go b/archive/archive.go new file mode 100644 index 0000000..88a4625 --- /dev/null +++ b/archive/archive.go @@ -0,0 +1,124 @@ +package archive + +import ( + "fmt" + "strings" + "io/ioutil" + "path/filepath" + "hash/fnv" + "../config" +) + +var pln = fmt.Println + +var Archive_map map[uint32]config.Track_t +var Archive_map_keys []uint32 + +var built = false + +func Build() error { + + if built { + return nil + } + + Archive_map = make(map[uint32]config.Track_t) + + album_dirs, err := ioutil.ReadDir(config.Xcfg.Archive.PATH) + if err != nil { + return err + } + + for _, dir := range album_dirs { + + album := new(config.Album_t) + album.TRACKS = make(map[uint32]config.Track_t) + album.PATH = filepath.Join(config.Xcfg.Archive.PATH, dir.Name()) + album.ID = hash(album.PATH) + + album.MAKER, album.NAME = parse_album_name(dir.Name()) + + track_files, _ := filepath.Glob(album.PATH + "/*.mp3") + for _, file := range track_files { + track := new(config.Track_t) + track.NAME = parse_track_name(filepath.Base(file), album.MAKER, album.NAME) + track.MAKER = album.MAKER + track.ALBUM = album.NAME + track.PATH = file + track.ID = hash(track.PATH) + track.AID = album.ID + + album.TRACKS[track.ID] = *track + + Archive_map[track.ID] = *track + Archive_map_keys = append(Archive_map_keys, track.ID) + + } + + cover, _ := filepath.Glob(album.PATH + "/*.jpg") + if len(cover) > 0 { + album.COVER = filepath.Join(album.PATH, cover[0]) + } + + // print_album(album) + + config.Xcfg.Archive.ALBUMS[album.ID] = *album + } + + built = true + return nil +} + +func parse_album_name(name string) (string, string) { + chunks := strings.Split(name, " - ") + if len(chunks) >= 2 { + return chunks[0], chunks[1] + } + return chunks[0], "n/a" +} + +func print_album(a *config.Album_t) { + pln(a.MAKER) + pln(a.NAME) + pln(a.PATH) + pln(a.COVER) + pln(a.ID) + pln(len(a.TRACKS)) + pln("-----") + for _, t := range a.TRACKS { + print_track(t) + } +} + +// ex.: Electronic Works 1958-1995-002-Else Marie Pade-Faust Suite Faust & Mefisto.mp3 +func parse_track_name(name string, maker string, album string) string { + + chunks := []string{} + if strings.HasPrefix(name, album) { // bleep + chunks = strings.Split(name, "-" + maker + "-") + } else if strings.HasPrefix(name, maker) { // bandcamp + chunks = strings.Split(name, " - " + album + " - ") + } + + if len(chunks) >= 2 { + return strings.Split(chunks[1], ".")[0] + } + return "n/a" +} + +func print_track(t config.Track_t) { + + pln(" " + t.NAME) + pln(" " + t.PATH) + pln(t.ID) + pln(t.AID) + pln("////////") + +} + + +func hash(s string) uint32 { + h := fnv.New32a() + h.Write([]byte(s)) + return h.Sum32() +} diff --git a/audio/audio.go b/audio/audio.go new file mode 100644 index 0000000..7803d5e --- /dev/null +++ b/audio/audio.go @@ -0,0 +1,71 @@ +package audio + +import ( + "fmt" + "os" + "os/signal" + "time" + "../icecast" +) + +// type stream_t struct { +// filepath string +// bitrate int +// channels int +// encoding_src string +// encoding_dst string +// } + +func Play(filename string) { + + sig := make(chan os.Signal, 1) + signal.Notify(sig, os.Interrupt, os.Kill) + + samplerate := 44100 + bytes_per_sample := 1 + channels := 1 + bytes_per_sec := int(samplerate) * channels * bytes_per_sample + + r, e := os.Open(filename) + chk(e) + defer r.Close() + + _, er := icecast.Connect() + chk(er) + + audio := make([]byte, 2 * 1024) + + dt := time.Second * time.Duration(len(audio)) / time.Duration(bytes_per_sec) + + now := time.Now() + + for { + + n, err := r.Read(audio) + if n == 0 { + break + } + chk(err) + + icecast.Send(audio) + + select { + case <-sig: + return + default: + } + + time.Sleep(dt) + + } + + fmt.Println(time.Duration(time.Now().Sub(now))) + +} + + +func chk(err error) { + if err != nil { + panic(err) + } +} diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..9f5c621 --- /dev/null +++ b/config.ini @@ -0,0 +1,22 @@ +[server] + +name = iceice-server +addr = 192.168.1.80 +port = 8088 +mount = wwww +usr = source +pwd = R1t4R1t4 + +[ice] + +name = noise +desc = description of noize +genre = lmnop +url = https://subject041293.xyz +irc = nope +aim = nope +pub = 0 + +[archive] + +path = /Volumes/QQQ/wellwellwell/mp3 \ No newline at end of file diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..4edf03a --- /dev/null +++ b/config/config.go @@ -0,0 +1,88 @@ +package config + +import ( + "github.com/go-ini/ini" +) + +type server_t struct { + NAME string + ADDR string + PORT int + MOUNT string + USR string + PWD string + STYPE int +} + +type ice_t struct { + NAME string + DESC string + GENRE string + URL string + IRC string + AIM string + PUB string +} + +type Track_t struct { + ID uint32 + AID uint32 + NAME string + MAKER string + ALBUM string + PATH string +} + +type Album_t struct { + ID uint32 + NAME string + MAKER string + PATH string + COVER string + TRACKS map[uint32]Track_t +} + +type archive_t struct { + PATH string + ALBUMS map[uint32]Album_t +} + +type config_t struct { + Track Track_t + Server server_t + Ice ice_t + Archive archive_t +} + +var Xcfg config_t + +func Loadconfig(filename string) error { + + ini, err := ini.Load(filename) + if err != nil { + return err + } + + Xcfg.Server.NAME = ini.Section("server").Key("name").Value() + Xcfg.Server.ADDR = ini.Section("server").Key("addr").Value() + Xcfg.Server.PORT, _ = ini.Section("server").Key("port").Int() + Xcfg.Server.MOUNT = ini.Section("server").Key("mount").Value() + Xcfg.Server.USR = ini.Section("server").Key("usr").Value() + Xcfg.Server.PWD = ini.Section("server").Key("pwd").Value() + + Xcfg.Ice.NAME = ini.Section("ice").Key("name").Value() + Xcfg.Ice.DESC = ini.Section("ice").Key("desc").Value() + Xcfg.Ice.GENRE = ini.Section("ice").Key("genre").Value() + Xcfg.Ice.URL = ini.Section("ice").Key("url").Value() + Xcfg.Ice.IRC = ini.Section("ice").Key("irc").Value() + Xcfg.Ice.AIM = ini.Section("ice").Key("aim").Value() + Xcfg.Ice.PUB = ini.Section("ice").Key("pub").Value() + + Xcfg.Archive.PATH = ini.Section("archive").Key("path").Value() + + + Xcfg.Archive.ALBUMS = make(map[uint32]Album_t) + + return nil + +} \ No newline at end of file diff --git a/icecast/icecast.go b/icecast/icecast.go new file mode 100644 index 0000000..4fe55a3 --- /dev/null +++ b/icecast/icecast.go @@ -0,0 +1,205 @@ +package icecast + +import ( + "fmt" + "encoding/base64" + "net" + "net/http" + "net/url" + "bufio" + "strings" + "time" + "errors" + "strconv" + "io/ioutil" + "../config" + "../socket" +) + +var connected bool = false +var stream_socket net.Conn +var status int = 0 +var server_msg string + +var pln = fmt.Println +var spf = fmt.Sprintf + + +func Connect() (net.Conn, error){ + + if connected { + return stream_socket, nil + } + + var sock net.Conn + host := config.Xcfg.Server.ADDR + ":" + strconv.Itoa(config.Xcfg.Server.PORT) + sock, err := net.Dial("tcp", host) + + if err != nil { + connected = false + return sock, err + } + + time.Sleep(time.Second) + + // doing connection BUTT style (i.e. multiple sends) + // icecast only for now + // try PUT method + + mount := config.Xcfg.Server.MOUNT + s := spf("PUT %s HTTP/1.1\r\n", mount) + if mount[0] != '/' { + s = spf("PUT /%s HTTP/1.1\r\n", mount) + } + err = socket.Send(sock, []byte(s)) + + s = spf("%s:%s", config.Xcfg.Server.USR, config.Xcfg.Server.PWD) + sb64 := "Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(s)) + "\r\n" + err = socket.Send(sock, []byte(sb64)) + + s = "User-Agent: radiodiodio/v0.0\r\n" + err = socket.Send(sock, []byte(s)) + + s = "Content-Type: audio/mp3\r\n" + err = socket.Send(sock, []byte(s)) + + s = spf("ice-name: %s\r\n", config.Xcfg.Ice.NAME) + err = socket.Send(sock, []byte(s)) + + s = spf("ice-public: %s\r\n", config.Xcfg.Ice.PUB) + err = socket.Send(sock, []byte(s)) + + s = spf("ice-url: %s\r\n", config.Xcfg.Ice.URL) + err = socket.Send(sock, []byte(s)) + + s = spf("ice-genre: %s\r\n", config.Xcfg.Ice.GENRE) + err = socket.Send(sock, []byte(s)) + + s = spf("ice-description: %s\r\n", config.Xcfg.Ice.DESC) + err = socket.Send(sock, []byte(s)) + + s = "ice-audio-info: ice-bitrate=192000; ice-channels=2; ice-samplerate=44100\r\n" + err = socket.Send(sock, []byte(s)) + + s = "\r\n" + err = socket.Send(sock, []byte(s)) + + if err != nil { + pln("error: sending PUT to the icecast server") + connected = false + return sock, err + } + + time.Sleep(time.Second) + + resp, err := socket.Recv(sock) + if err != nil { + pln("error: receiving response from the icecast server") + connected = false + return sock, err + } + + status, server_msg, err := read_http_response(resp) + + if status != 200 { + connected = false + return sock, errors.New("Icecast connection failed: " + strconv.Itoa(status) + " - " + server_msg) + } + + pln("YAYAY!!") + + connected = true + stream_socket = sock + + return sock, nil + +} + +func Disconnect() { + stream_socket.Close() + connected = false +} + +func Send(buff []byte) error { + if !connected { + return errors.New("Not connected to Icecast server") + } + err := socket.Send(stream_socket, buff) + return err +} + +func Update() error { + + host := config.Xcfg.Server.ADDR + ":" + strconv.Itoa(config.Xcfg.Server.PORT) + track := url.QueryEscape(config.Xcfg.Track.NAME) + mount := config.Xcfg.Server.MOUNT + if mount[0] != '/' { + mount = "/" + mount + } + + s := spf("%s:%s", config.Xcfg.Server.USR, config.Xcfg.Server.PWD) + sb64 := "Basic " + base64.StdEncoding.EncodeToString([]byte(s)) + + header := "GET /admin/metadata?mode=updinfo&mount=" + mount + "&song=" + track + " HTTP/1.0\r\n" + + "User-Agent: radiodiodio/v0.0\r\n" + + "Authorization: " + sb64 + "\r\n" + + "\r\n" + + pln(host) + pln(header) + + var sock net.Conn + sock, err := net.Dial("tcp", host) + if err != nil { + return err + } + + time.Sleep(time.Second) + + err = socket.Send(sock, []byte(header)) + + time.Sleep(time.Second) + + defer sock.Close() + + resp, err := socket.Recv(sock) + if err != nil { + pln("error: receiving response from the icecast server") + connected = false + return err + } + + status, server_msg, err := read_http_response(resp) + + if status != 200 { + connected = false + return errors.New("Icecast connection failed: " + strconv.Itoa(status) + " - " + server_msg) + } else { + pln(server_msg) + } + + return err + +} + + +func read_http_response(raw_resp []byte) (int, string, error) { + + reader := bufio.NewReader(strings.NewReader(string(raw_resp))) + resp, err := http.ReadResponse(reader, nil) + if err != nil { + return 0, "", err + } + + status := resp.StatusCode + + defer resp.Body.Close() + body, er := ioutil.ReadAll(resp.Body) + if er != nil { + return status, "", er + } + + return status, string(body), nil + +} + diff --git a/playlist/playlist.go b/playlist/playlist.go new file mode 100644 index 0000000..8fa386f --- /dev/null +++ b/playlist/playlist.go @@ -0,0 +1,181 @@ +package playlist + +import ( + "fmt" + "time" + "errors" + "strconv" + "math/rand" + "encoding/json" + "../archive" + "../config" +) + +var pln = fmt.Println + +type Playlist struct { + NAME string + CTRACK config.Track_t + CALBUM config.Album_t + LIST []uint32 + MAX int +} + +type PrettyPlaylist struct { + NAME string + CTRACK config.Track_t + CALBUM config.Album_t + LIST []config.Track_t + MAX int +} + + +func MakeRandom(name string, max int) (*Playlist, error) { + err := archive.Build() + if err != nil { + return nil, err + } + + p := &Playlist{NAME: name, MAX: max} + p.LIST = make([]uint32, max) + + for i := 0; i < max; i++ { + r, _ := Random() + p.LIST[i] = r // might duplicate + } + + return p, nil; +} + +func (p *Playlist) Encode() string { + + pp := p.Pretty() + res, _ := json.Marshal(pp) + return string(res) + +} + +func Decode(jsonstr string) (*Playlist, error) { + + pp := &PrettyPlaylist{} + + if err := json.Unmarshal([]byte(jsonstr), pp); err != nil { + return nil, err + } + + p := pp.Unpretty() + return p, nil +} + +func (p *Playlist) Pretty() *PrettyPlaylist { + pp := &PrettyPlaylist{NAME: p.NAME, CTRACK: p.CTRACK, CALBUM: p.CALBUM, MAX: p.MAX} + pp.LIST = make([]config.Track_t, len(p.LIST)) + for i := 0; i < len(p.LIST); i++ { + pp.LIST[i] = archive.Archive_map[p.LIST[i]] + } + return pp +} + +func (pp *PrettyPlaylist) Unpretty() *Playlist { + p := &Playlist{NAME: pp.NAME, CTRACK: pp.CTRACK, CALBUM: pp.CALBUM, MAX: pp.MAX} + p.LIST = make([]uint32, len(pp.LIST)) + for i := 0; i < len(pp.LIST); i++ { + p.LIST = append(p.LIST, pp.LIST[i].ID) + } + return p +} + +// type PrettyPlaylist struct { +// NAME string +// CTRACK config.Track_t +// CALBUM config.Album_t +// LIST []config.Track_t +// MAX int +// } + + +func (pp *PrettyPlaylist) Print() { + + pln("Name: " + pp.NAME) + pln("CTRACK: " + pp.CTRACK.NAME) + pln("CALBUM: " + pp.CALBUM.NAME) + pln("Next:") + for i, r := range pp.LIST { + pln(" " + strconv.Itoa(i) + " - " + r.NAME) + } +} + +// https://stackoverflow.com/questions/33834742/remove-and-adding-elements-to-array-in-go-lang + +func (p *Playlist) Pop() error { + if len(p.LIST) == 0 { + return errors.New("Playlist is empty") + } + + t := p.LIST[0] + if len(p.LIST) < 2 { + p.LIST = make([]uint32, 0) + } else { + p.LIST = p.LIST[1:] + } + + p.CTRACK = archive.Archive_map[t] + p.CALBUM = config.Xcfg.Archive.ALBUMS[p.CTRACK.AID] + + return nil +} + +func (p *Playlist) Push(track_id uint32) error { + if len(p.LIST) == p.MAX { + return errors.New("Playlist is full") + } + p.LIST = append(p.LIST, track_id) + return nil +} + +func (p *Playlist) PushFront(track_id uint32) error { + if len(p.LIST) == p.MAX { + return errors.New("Playlist is full") + } + p.LIST = append([]uint32{ track_id }, p.LIST...) // don't forget '...'' + return nil +} + +func (p *Playlist) Insert(track_id uint32, at_index int) error { + if at_index > p.MAX { + return errors.New("Invalid insert index") + } + + p.LIST = append(p.LIST, 0) + copy(p.LIST[at_index+1:], p.LIST[at_index:]) + p.LIST[at_index] = track_id + + if len(p.LIST) > p.MAX { + p.LIST = p.LIST[:p.MAX] + } + + return nil +} + +func (p *Playlist) Delete(track_id uint32) error { + + k := -1 + for i := 0; i <= len(p.LIST); i++ { + if p.LIST[i] == track_id { k = i } + } + + if k < 0 { + return errors.New("Invalid track ID") + } + + p.LIST = append(p.LIST[:k-1], p.LIST[k+1:]...) + return nil + +} + + +func Random() (uint32, string) { + rand.Seed(time.Now().UTC().UnixNano()) + index := archive.Archive_map_keys[rand.Intn(len(archive.Archive_map_keys) - 1)] + return archive.Archive_map[index].ID, archive.Archive_map[index].NAME +} \ No newline at end of file diff --git a/serve.go b/serve.go new file mode 100644 index 0000000..94bcb64 --- /dev/null +++ b/serve.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "log" + "net/http" + "./config" + "./playlist" + "./www" +) + +var pln = fmt.Println + +func main() { + config.Loadconfig("config.ini") + p, _ := playlist.MakeRandom("YOYO", 10) + p.Pop() + www.Init(p) + log.Fatal(http.ListenAndServe(":8718", nil)) +} \ No newline at end of file diff --git a/socket/socket.go b/socket/socket.go new file mode 100644 index 0000000..e98bef2 --- /dev/null +++ b/socket/socket.go @@ -0,0 +1,26 @@ +package socket + +import ( + "net" + "errors" +) + +func Send(sock net.Conn, buf []byte) error { + n, err := sock.Write(buf) + if err != nil { + return err + } + if n != len(buf) { + return errors.New("error: socket send") + } + return nil +} + +func Recv(sock net.Conn) ([]byte, error) { + var buf []byte = make([]byte, 1024) + n, err := sock.Read(buf) + if err != nil { + return nil, err + } + return buf[0:n], err +} diff --git a/www/tmpl/playlist.html b/www/tmpl/playlist.html new file mode 100644 index 0000000..6918922 --- /dev/null +++ b/www/tmpl/playlist.html @@ -0,0 +1,69 @@ + + + + + + + + + + + + Radiodiodio + + +

Playlist: {{.NAME}}

+ +

Current Track: {{.CTRACK.NAME}}

+

Current Album : {{.CALBUM.NAME}} - {{.CALBUM.MAKER}}

+ + + + diff --git a/www/www.go b/www/www.go new file mode 100644 index 0000000..5d89dc0 --- /dev/null +++ b/www/www.go @@ -0,0 +1,81 @@ +package www + +import ( + "log" + "html/template" + "net/http" + // "golang.org/x/net/websocket" + "github.com/gorilla/websocket" + "../playlist" +) + +// todo: hub - https://stackoverflow.com/questions/31532652/go-websocket-send-all-clients-a-message + +var pp *playlist.PrettyPlaylist + +var pln = log.Println + +type op_t struct { + OP string `json:"op"` + ID string `json:"id"` + INDEX int `json:"index"` +} + +var upgrader = websocket.Upgrader{ + ReadBufferSize: 512, + WriteBufferSize: 512, +} + +func Init(p *playlist.Playlist) { + pp = p.Pretty() + http.HandleFunc("/pp", pp_handler) + http.HandleFunc("/ppop", pp_operations) +} + +func pp_handler(w http.ResponseWriter, r *http.Request) { + t, err := template.ParseFiles("./www/tmpl/playlist.html") + if err != nil { + pln(err) + return + } + pp.Print() + t.Execute(w, pp) +} + +func pp_operations(w http.ResponseWriter, r *http.Request) { + pln("x") + c, err := upgrader.Upgrade(w, r, nil) + if err != nil { + pln(err) + return + } + go readop(c) +} + +func readop(c *websocket.Conn) { + for { + opdata := &op_t{} + if err := c.ReadJSON(&opdata); err != nil { + pln(err) + return //connection lost? + } + pln(opdata.OP) + pln(opdata.ID) + pln(opdata.INDEX) + if err := c.WriteJSON(opdata); err != nil { + pln(err) + return //connection lost? + } + } +} + +// func pp_operations(ws *websocket.Conn) { +// opdata := &op_t{} +// if err := websocket.JSON.Receive(ws, &opdata); err != nil { +// pln(err) +// } +// // websocket.JSON.Send(ws, "ok") +// pln(opdata.OP) +// pln(opdata.ID) +// pln(opdata.INDEX) +// } \ No newline at end of file