This commit is contained in:
gauthiier 2019-01-02 16:11:22 +01:00
commit 052616c318
10 changed files with 868 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.DS_Store
serve
main

124
archive/archive.go Normal file
View File

@ -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()
}

71
audio/audio.go Normal file
View File

@ -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)
}
}

88
config/config.go Normal file
View File

@ -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
}

205
icecast/icecast.go Normal file
View File

@ -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
}

181
playlist/playlist.go Normal file
View File

@ -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
}

20
serve.go Normal file
View File

@ -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))
}

26
socket/socket.go Normal file
View File

@ -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
}

69
www/tmpl/playlist.html Normal file
View File

@ -0,0 +1,69 @@
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css">
<style type="text/css">
#sortable { list-style-type: none; margin: 10; padding: 10; width: 60%; }
#sortable li { margin: 0 3px 3px 3px; padding: 0.4em; padding-left: 1.5em; height: 18px; }
#sortable li span { position: absolute; margin-left: -1.3em; }
</style>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>
<script>
$( function() {
$( "#sortable" ).sortable({
update: function (e, u) {
console.log("update")
list = Array.from(document.querySelectorAll('#sortable>li'));
var i = list.indexOf(u.item[0])
console.log(u.item.attr("id") + " is now " + i)
on_update_operation(u.item.attr("id"), i)
}
});
$( "#sortable" ).disableSelection();
// connect to ppop
sock = new WebSocket("ws://localhost:8718/ppop");
sock.onopen = function() {
console.log("ppop open")
}
sock.onclose = function() {
console.log("ppop closed")
sock = new WebSocket("ws://localhost:8718/ppop");
}
sock.onmessage = function(msg) {
console.log(msg.data)
}
// setInterval(function() {
// sock.send(JSON.stringify({"op": "tick", "id": "0", "index": 0}));
// },3000);
} );
var sock = null;
function on_update_operation(id, index) {
if(sock) {
console.log("sending")
sock.send(JSON.stringify({"op": "update", "id": id, "index": index}));
}
}
</script>
<title>Radiodiodio</title>
</head>
<body>
<h1>Playlist: {{.NAME}}</h1>
<h2>Current Track: {{.CTRACK.NAME}} </h2>
<h2>Current Album : {{.CALBUM.NAME}} - {{.CALBUM.MAKER}}</h2>
<ul id="sortable">
{{range $.LIST}}
<li class="ui-state-default" id="{{.ID}}"><span>{{.MAKER}} - {{.ALBUM}} - {{.NAME}}</span></li>
{{end}}
</ul>
</body>
</html>

81
www/www.go Normal file
View File

@ -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)
// }