From 318fd2fda65e6a0a329a5fad5be3f81d54ccacff Mon Sep 17 00:00:00 2001 From: Florian Fischer Date: Thu, 21 Jul 2022 00:44:31 +0200 Subject: initial WIP commit --- .editorconfig | 16 +++++ .gitignore | 1 + go.mod | 20 ++++++ go.sum | 70 +++++++++++++++++++ main.go | 162 +++++++++++++++++++++++++++++++++++++++++++ player.go | 35 ++++++++++ playlist.go | 48 +++++++++++++ spotify.go | 77 +++++++++++++++++++++ web/client.html | 211 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wrms.go | 172 +++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 812 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 player.go create mode 100644 playlist.go create mode 100644 spotify.go create mode 100644 web/client.html create mode 100644 wrms.go diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..87ba987 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +trim_trailing_whitespace = true +end_of_line = lf +insert_final_newline = true + +[*.go] +indent_style = tab +indent_size = 2 + +[*.{html,cs,js}] +indent_style = space +indent_size = 2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9490ce8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +wrms diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1e70a32 --- /dev/null +++ b/go.mod @@ -0,0 +1,20 @@ +module muhq.space/wrms + +go 1.18 + +require ( + github.com/badfortrains/mdns v0.0.0-20160325001438-447166384f51 // indirect + github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/klauspost/compress v1.10.3 // indirect + github.com/librespot-org/librespot-golang v0.0.0-20220325184705-31669e5a889f // indirect + github.com/miekg/dns v1.1.8 // indirect + github.com/xlab/portaudio-go v0.0.0-20170905165025-132d041879db // indirect + github.com/xlab/vorbis-go v0.0.0-20210911202351-b5b85f1ec645 // indirect + golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 // indirect + golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 // indirect + golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect + google.golang.org/protobuf v1.27.1 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fdd34ea --- /dev/null +++ b/go.sum @@ -0,0 +1,70 @@ +github.com/badfortrains/mdns v0.0.0-20160325001438-447166384f51 h1:b6+GdpGhuzxPETrVLwY77iq0L/BqLpcPRmaLNW+SoRY= +github.com/badfortrains/mdns v0.0.0-20160325001438-447166384f51/go.mod h1:qHRkxMBkkpAWD2poeBYQt6O5IS4dE6w1Cr4Z8Q3feXI= +github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/librespot-org/librespot-golang v0.0.0-20220325184705-31669e5a889f h1:tTMPsyVClxxV+CnlLqzgxTTTFM6J7i//rSejN1Md0b0= +github.com/librespot-org/librespot-golang v0.0.0-20220325184705-31669e5a889f/go.mod h1:LeHPXRci2Bg6RBmw8BDEFH5nfb8oGU6mll+qTH8UIzw= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= +github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xlab/portaudio-go v0.0.0-20170905165025-132d041879db h1:sSIQlvfIWUHLDhEWUL2K2CeYv9CDksC00VxuxPUe4lw= +github.com/xlab/portaudio-go v0.0.0-20170905165025-132d041879db/go.mod h1:r57mRacDQMS6Fz8ubv1nE8zZ0DbQ/sY0NkCmdxeUXmY= +github.com/xlab/vorbis-go v0.0.0-20190125051917-087364aef51d/go.mod h1:AMqfx3jFwPqem3u8mF2lsRodZs30jG/Mag5HZ3mB3sA= +github.com/xlab/vorbis-go v0.0.0-20210911202351-b5b85f1ec645 h1:lYg/+vV/Fd5WM1+Ptg54Am3y4mDXaMSrT+mKUHV5uVc= +github.com/xlab/vorbis-go v0.0.0-20210911202351-b5b85f1ec645/go.mod h1:AMqfx3jFwPqem3u8mF2lsRodZs30jG/Mag5HZ3mB3sA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480 h1:O5YqonU5IWby+w98jVUG9h7zlCWCcH4RHyPVReBmhzk= +golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/net v0.0.0-20190420063019-afa5a82059c6 h1:HdqqaWmYAUI7/dmByKKEw+yxDksGSo+9GjkUc9Zp34E= +golang.org/x/net v0.0.0-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..e90dcaf --- /dev/null +++ b/main.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "html/template" + "log" + "net/http" + + "github.com/google/uuid" + + "nhooyr.io/websocket" +) + +var wrms = NewWrms() + +func landingPage(w http.ResponseWriter, r *http.Request) { + if _, err := r.Cookie("UUID"); err != nil { + http.SetCookie(w, &http.Cookie{Name: "UUID", Value: uuid.NewString()}) + } + + t, err := template.ParseFiles("web/client.html") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + t.Execute(w, "") +} + +func searchHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Searching for") +} + +func genericVoteHandler(w http.ResponseWriter, r *http.Request, vote string) { + uuidCookie, err := r.Cookie("UUID") + if err != nil { + http.Error(w, "No connection ID cookie set", http.StatusUnauthorized) + return + } + connId := uuidCookie.Value + + songId := r.URL.Query().Get("song") + log.Println("%s song %s via url %s", vote, songId, r.URL) + wrms.AdjustSongWeight(connId, songId, vote) +} + +func upHandler(w http.ResponseWriter, r *http.Request) { + genericVoteHandler(w, r, "up") +} + +func downHandler(w http.ResponseWriter, r *http.Request) { + genericVoteHandler(w, r, "down") +} + +func unvoteHandler(w http.ResponseWriter, r *http.Request) { + genericVoteHandler(w, r, "unvote") +} + +func addHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Added") +} + +func playPauseHandler(w http.ResponseWriter, r *http.Request) { + wrms.PlayPause() +} + +func wsEndpoint(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, nil) + if err != nil { + log.Println("%v", err) + return + } + + ctx, cancel := context.WithCancel(r.Context()) + + ctx = c.CloseRead(ctx) + + uuidCookie, err := r.Cookie("UUID") + if err != nil { + log.Fatal("websocket connection has no set UUID cookie") + } + + id, err := uuid.Parse(uuidCookie.Value) + if err != nil { + log.Fatal("Invalid UUID set in cookie") + } + + defer func() { + log.Println("cancel context of connection", id) + cancel() + }() + + conn := Connection{id, make(chan Event), c} + wrms.Connections = append(wrms.Connections, conn) + + log.Println("New websocket connection with id", id) + + initialCmds := [][]byte{} + if wrms.Playing { + data, err := json.Marshal(Event{"play", []Song{*wrms.CurrentSong}}) + if err != nil { + log.Println(err) + return + } + initialCmds = append(initialCmds, data) + } + + data, err := json.Marshal(Event{"add", wrms.Songs}) + if err != nil { + log.Println(err) + return + } + initialCmds = append(initialCmds, data) + + for _, data := range initialCmds { + log.Println("Sending initial cmd", string(data)) + err = c.Write(ctx, websocket.MessageText, data) + if err != nil { + log.Println(err) + return + } + } + + for ev := range conn.Events { + data, err := json.Marshal(ev) + if err != nil { + log.Println(err) + return + } + + log.Println(fmt.Sprintf("Sending ev %s to %s", string(data), id)) + err = c.Write(ctx, websocket.MessageText, data) + if err != nil { + log.Println(err) + return + } + } +} + +func setupRoutes() { + http.HandleFunc("/", landingPage) + http.HandleFunc("/search", searchHandler) + http.HandleFunc("/up", upHandler) + http.HandleFunc("/down", downHandler) + http.HandleFunc("/unvote", unvoteHandler) + http.HandleFunc("/add", addHandler) + http.HandleFunc("/playpause", playPauseHandler) + http.HandleFunc("/ws", wsEndpoint) +} + +func main() { + setupRoutes() + + wrms.AddSong(NewSong("Lala", "SNFMT", "local")) + wrms.AddSong(NewSong("Hobelbank", "MC Wankwichtel", "local")) + + log.Println(wrms) + defer wrms.Close() + log.Fatal(http.ListenAndServe(":8080", nil)) +} diff --git a/player.go b/player.go new file mode 100644 index 0000000..73b5441 --- /dev/null +++ b/player.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" +) + +type Backend interface { + Play(song Song) + Pause() + Search(keyword string) []Song +} + +type Player struct { + Backends map[string]Backend +} + +func NewPlayer() Player { + available_backends := map[string]Backend{} + + spotify, err := NewSpotify() + if err != nil { + // log.Fatalln("Error during initialization of the spotify backend:", err.Error()) + log.Println("Error during initialization of the spotify backend:", err.Error()) + } + available_backends["spotify"] = spotify + + return Player{available_backends} +} + +func (player *Player) Play(song Song) { + player.Backends[song.Source].Play(song) +} + +func (player *Player) Pause() { +} diff --git a/playlist.go b/playlist.go new file mode 100644 index 0000000..b37ab3d --- /dev/null +++ b/playlist.go @@ -0,0 +1,48 @@ +package main + +import ( + "container/heap" +) + +type Playlist []*Song + +func (pl Playlist) Len() int { return len(pl) } + +func (pl Playlist) Less(i, j int) bool { + return pl[i].Weight > pl[j].Weight +} + +func (pl Playlist) Swap(i, j int) { + pl[i], pl[j] = pl[j], pl[i] + pl[i].index = i + pl[j].index = j +} + +func (pl *Playlist) Push(x any) { + n := len(*pl) + song := x.(*Song) + song.index = n + *pl = append(*pl, song) +} + +func (pl *Playlist) Pop() any { + old := *pl + n := len(old) + s := old[n-1] + old[n-1] = nil // avoid memory leak + s.index = -1 // for safety + *pl = old[0 : n-1] + return s +} + +func (pl *Playlist) PopSong() *Song { + return heap.Pop(pl).(*Song) +} + +func (pl *Playlist) Add(s *Song) { + heap.Push(pl, s) +} + +func (pl *Playlist) Adjust(s *Song) { + heap.Fix(pl, s.index) +} diff --git a/spotify.go b/spotify.go new file mode 100644 index 0000000..a813254 --- /dev/null +++ b/spotify.go @@ -0,0 +1,77 @@ +package main + +// The spotify backend is based on the librespot-golang code released under MIT +// license. +// Authors of the librespot-golang code are: +// Copyright (c) 2015 Paul Lietar +// Copyright (c) 2015 badfortrains +// Copyright (c) 2018 Guillaume "xplodwild" Lesniak + +import ( + "errors" + "os" + + // "github.com/librespot-org/librespot-golang/Spotify" + "github.com/librespot-org/librespot-golang/librespot" + "github.com/librespot-org/librespot-golang/librespot/core" + // "github.com/librespot-org/librespot-golang/librespot/utils" + "github.com/xlab/portaudio-go/portaudio" + // "github.com/xlab/vorbis-go/decoder" +) + +const ( + deviceName = "wrms" + // The number of samples per channel in the decoded audio + samplesPerChannel = 2048 + // The samples bit depth + bitDepth = 16 + // The samples format + sampleFormat = portaudio.PaFloat32 +) + +type SpotifyBackend struct { + session *core.Session +} + +func NewSpotify() (*SpotifyBackend, error) { + spotify := SpotifyBackend{} + + if err := portaudio.Initialize(); paError(err) { + return nil, errors.New(paErrorText(err)) + } + + var err error + + username := os.Getenv("WRMS_SPOTIFY_USER") + password := os.Getenv("WRMS_SPOTIFY_PASSWORD") + if username == "" || password == "" { + return nil, errors.New("User and password for spotify must be provided") + } + + spotify.session, err = librespot.Login(username, password, deviceName) + + if err != nil { + return nil, err + } + + return &spotify, nil +} + +func (spotify *SpotifyBackend) Play(song Song) { +} + +func (spotify *SpotifyBackend) Pause() { +} + +func (spotify *SpotifyBackend) Search(keyword string) []Song { + return []Song{} +} + +// PortAudio helpers +func paError(err portaudio.Error) bool { + return portaudio.ErrorCode(err) != portaudio.PaNoError +} + +func paErrorText(err portaudio.Error) string { + return "PortAudio error: " + portaudio.GetErrorText(err) +} diff --git a/web/client.html b/web/client.html new file mode 100644 index 0000000..d7f8db2 --- /dev/null +++ b/web/client.html @@ -0,0 +1,211 @@ + + + + + + WRMS + + + + + + + + + +

Playing

+

+
+ +
+ +

Playlist

+ + + diff --git a/wrms.go b/wrms.go new file mode 100644 index 0000000..b648744 --- /dev/null +++ b/wrms.go @@ -0,0 +1,172 @@ +package main + +import ( + "crypto/sha1" + "encoding/base64" + "fmt" + "log" + + "github.com/google/uuid" + + "nhooyr.io/websocket" +) + +type Song struct { + Title string `json:"title"` + Artist string `json:"artist"` + Source string `json:"source"` + Id string `json:"id"` + Weight int `json:"weight"` + index int `json:"-"` // used by heap.Interface + Upvotes map[string]struct{} `json:"-"` + Downvotes map[string]struct{} `json:"-"` +} + +func NewSong(title string, artist string, source string) Song { + s := Song{title, artist, source, "", 0, 0, map[string]struct{}{}, map[string]struct{}{}} + h := sha1.New() + h.Write([]byte(title)) + h.Write([]byte(artist)) + h.Write([]byte(source)) + s.Id = base64.URLEncoding.EncodeToString(h.Sum(nil)) + return s +} + +type Event struct { + Event string `json:"cmd"` + Songs []Song `json:"songs"` +} + +type Connection struct { + Id uuid.UUID + Events chan Event + C *websocket.Conn +} + +func (c *Connection) Close() { + log.Println("Closing connection %s", c.Id) + close(c.Events) + c.C.Close(websocket.StatusNormalClosure, "") +} + +type Wrms struct { + Connections []Connection + Songs []Song + Queue Playlist + CurrentSong *Song + Player Player + Playing bool +} + +func NewWrms() Wrms { + wrms := Wrms{} + wrms.Player = NewPlayer() + return wrms +} + +func (wrms *Wrms) Close() { + log.Println("Closing WRMS") + for _, conn := range wrms.Connections { + conn.Close() + } +} + +func (wrms *Wrms) Broadcast(cmd Event) { + for i := 0; i < len(wrms.Connections); i++ { + wrms.Connections[i].Events <- cmd + } +} + +func (wrms *Wrms) AddSong(song Song) { + wrms.Songs = append(wrms.Songs, song) + wrms.Queue.Add(&wrms.Songs[len(wrms.Songs) - 1]) +} + +func (wrms *Wrms) Next() { + wrms.CurrentSong = wrms.Queue.PopSong() + for i, s := range wrms.Songs { + if s.Id == wrms.CurrentSong.Id { + wrms.Songs[i] = wrms.Songs[len(wrms.Songs) - 1] + wrms.Songs = wrms.Songs[:len(wrms.Songs) - 1] + break + } + } +} + +func (wrms *Wrms) PlayPause() { + var ev Event + if !wrms.Playing { + if wrms.CurrentSong == nil { + wrms.Next() + } + ev = Event{"play", []Song{*wrms.CurrentSong}} + } else { + ev = Event{"pause", []Song{}} + } + + wrms.Playing = !wrms.Playing + + wrms.Broadcast(ev) +} + +func (wrms *Wrms) AdjustSongWeight(connId string, songId string, vote string) { + for i := 0; i < len(wrms.Songs); i++ { + s := &wrms.Songs[i] + if s.Id != songId { + continue + } + + switch vote { + case "up": + if _, ok := s.Upvotes[connId]; ok { + log.Println(fmt.Sprintf("Double upvote of song %s by connections %s", songId, connId)) + return + } + + if _, ok := s.Downvotes[connId]; ok { + delete(s.Downvotes, connId) + s.Weight += 2 + } else { + s.Weight += 1 + } + + s.Upvotes[connId] = struct{}{} + + case "down": + if _, ok := s.Downvotes[connId]; ok { + log.Println(fmt.Sprintf("Double downvote of song %s by connections %s", songId, connId)) + return + } + + if _, ok := s.Upvotes[connId]; ok { + delete(s.Upvotes, connId) + s.Weight -= 2 + } else { + s.Weight -= 1 + } + + s.Downvotes[connId] = struct{}{} + + case "unvote": + if _, ok := s.Downvotes[connId]; ok { + delete(s.Downvotes, connId) + s.Weight += 1 + } + + if _, ok := s.Upvotes[connId]; ok { + delete(s.Upvotes, connId) + s.Weight -= 1 + } else { + log.Println(fmt.Sprintf("Double downvote of song %s by connections %s", songId, connId)) + return + } + + default: + log.Fatal("invalid vote") + } + + wrms.Queue.Adjust(s) + wrms.Broadcast(Event{"update", []Song{*s}}) + break + } +} -- cgit v1.2.3