From 0d8032da46b962b2bde5752dc552a1cb33f7e432 Mon Sep 17 00:00:00 2001 From: "Alexander \"Arav\" Andreev" Date: Sun, 8 Oct 2023 00:52:40 +0400 Subject: [PATCH] MostListenedSong was rewritten. --- cmd/dwelling-radio/main.go | 9 +-- internal/http/dj_handlers.go | 15 +++-- internal/http/handlers.go | 8 ++- internal/radio/mostlistened.go | 119 ++++++++++++++++----------------- web/templates/index.pug | 7 +- 5 files changed, 80 insertions(+), 78 deletions(-) diff --git a/cmd/dwelling-radio/main.go b/cmd/dwelling-radio/main.go index 15f9161..6abe4a7 100644 --- a/cmd/dwelling-radio/main.go +++ b/cmd/dwelling-radio/main.go @@ -37,8 +37,9 @@ func main() { return } + var mostListenedSong radio.MostListenedSong if data, err := os.ReadFile(*mostListenedSongPath); err == nil { - if err := radio.LoadMostListenedSong(data); err != nil { + if err := mostListenedSong.Load(data); err != nil { log.Fatalln(err) } } @@ -50,7 +51,7 @@ func main() { log.Fatalln(err) } - hand := ihttp.NewHandlers(*filelistPath, songList, lstnrs) + hand := ihttp.NewHandlers(*filelistPath, songList, lstnrs, &mostListenedSong) r := httpr.New() r.Handler(http.MethodGet, "/", hand.Index) @@ -64,7 +65,7 @@ func main() { r.ServeStatic("/assets/*filepath", web.Assets()) - djh := ihttp.NewDJHandlers(lstnrs, plylst, songList, *songListLen) + djh := ihttp.NewDJHandlers(lstnrs, plylst, songList, &mostListenedSong) s := r.Sub("/api/listener") s.Handler(http.MethodGet, "/", djh.ListenersGet) @@ -86,7 +87,7 @@ func main() { } defer func() { - fileData := radio.StoreMostListenedSong() + fileData := mostListenedSong.Store() if fileData != nil { err := os.WriteFile(*mostListenedSongPath, fileData, fs.ModePerm) if err != nil { diff --git a/internal/http/dj_handlers.go b/internal/http/dj_handlers.go index 891f5c5..1f79778 100644 --- a/internal/http/dj_handlers.go +++ b/internal/http/dj_handlers.go @@ -14,10 +14,11 @@ type DJHandlers struct { listeners *radio.ListenerCounter playlist *radio.Playlist songList *radio.SongList + mostLSong *radio.MostListenedSong } -func NewDJHandlers(l *radio.ListenerCounter, p *radio.Playlist, sl *radio.SongList, slLen int) *DJHandlers { - return &DJHandlers{listeners: l, playlist: p, songList: sl} +func NewDJHandlers(l *radio.ListenerCounter, p *radio.Playlist, sl *radio.SongList, mls *radio.MostListenedSong) *DJHandlers { + return &DJHandlers{listeners: l, playlist: p, songList: sl, mostLSong: mls} } func (dj *DJHandlers) ListenersGet(w http.ResponseWriter, _ *http.Request) { @@ -65,8 +66,12 @@ func (dj *DJHandlers) PlaylistNext(w http.ResponseWriter, _ *http.Request) { Duration: oggf.GetDuration(), MaxListeners: dj.listeners.Current(), StartAt: time.Now()} + + if dj.songList.Current() != nil { + dj.mostLSong.Update(*dj.songList.Current()) + } + dj.songList.Add(song) - radio.CheckAndUpdateMostListenedSong(*dj.songList.Current()) }() } fmt.Fprintln(w, nxt) @@ -106,7 +111,7 @@ func (dj *DJHandlers) Status(w http.ResponseWriter, r *http.Request) { Current: curSong, Listeners: dj.listeners, List: dj.songList.List(), - Mls: radio.MostListened()}) + Mls: dj.mostLSong.Get()}) if err != nil { log.Println("DJHandlers.Status:", err) http.Error(w, "status parsing failed", http.StatusInternalServerError) @@ -115,7 +120,7 @@ func (dj *DJHandlers) Status(w http.ResponseWriter, r *http.Request) { func (dj *DJHandlers) MostListenedSong(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") - mls := radio.MostListened() + mls := dj.mostLSong.Get() if mls == nil { w.WriteHeader(http.StatusNotFound) return diff --git a/internal/http/handlers.go b/internal/http/handlers.go index bff198c..d5fd817 100644 --- a/internal/http/handlers.go +++ b/internal/http/handlers.go @@ -11,18 +11,20 @@ import ( type Handlers struct { songList *radio.SongList listeners *radio.ListenerCounter + mostLSong *radio.MostListenedSong filelistPath string } -func NewHandlers(filelistPath string, songList *radio.SongList, listeners *radio.ListenerCounter) *Handlers { +func NewHandlers(filelistPath string, songList *radio.SongList, listeners *radio.ListenerCounter, mls *radio.MostListenedSong) *Handlers { return &Handlers{ songList: songList, filelistPath: filelistPath, - listeners: listeners} + listeners: listeners, + mostLSong: mls} } func (h *Handlers) Index(w http.ResponseWriter, r *http.Request) { - web.Index(utils.MainSite(r.Host), h.songList, h.listeners, r, w) + web.Index(utils.MainSite(r.Host), h.songList, h.listeners, h.mostLSong.Get(), r, w) } func (h *Handlers) Playlist(w http.ResponseWriter, _ *http.Request) { diff --git a/internal/radio/mostlistened.go b/internal/radio/mostlistened.go index 602e320..47ba655 100644 --- a/internal/radio/mostlistened.go +++ b/internal/radio/mostlistened.go @@ -2,101 +2,96 @@ package radio import ( "bytes" - "encoding/json" - "errors" "strconv" + "sync" "time" + + "github.com/pkg/errors" ) -const MostListenedDateFormat = "02 January 2006" - -var mlsChanged = false +const MostListenedDateFormat string = "02 January 2006" +// MostListenedSong holds a metadata for a most listened song. type MostListenedSong struct { - Listeners int - Date time.Time - Song string + sync.RWMutex + Date time.Time `json:"date"` + Listeners int `json:"listeners"` + Song string `json:"song"` + changed bool } -func (mls *MostListenedSong) DateString() string { - return mls.Date.Format(MostListenedDateFormat) -} - -func (mls *MostListenedSong) MarshalJSON() ([]byte, error) { - return json.Marshal(&struct { - Song string `json:"song"` - Listeners int `json:"listeners"` - Date string `json:"date"` - }{ - Song: mls.Song, - Listeners: mls.Listeners, - Date: mls.Date.UTC().Format(time.RFC3339)}) -} - -var mostListened MostListenedSong - -// CheckAndUpdateMostListenedSong compares current most played song with -// provided `cur`rent song's listeners, and if it is larger, then it takes -// `prev`ious song's name. -// -// Why we take a previous song's name? Experimentally I noticed that Icecast -// writes amount of listeners that was by the very start of a next song. So -// it means that it was actually amount of listeners by the end of -// the previous song. -// -// So it would be fairer to give these listeners back to a song they was -// listening to. -func CheckAndUpdateMostListenedSong(cur Song) { - if cur.Artist == "" { +func (mls *MostListenedSong) Update(song Song) { + mls.Lock() + defer mls.Unlock() + if song.Artist == "" { return } - if cur.MaxListeners > mostListened.Listeners { - mostListened = MostListenedSong{ - Listeners: cur.MaxListeners, - Date: cur.StartAt, - Song: cur.Artist + " - " + cur.Title} + if song.MaxListeners > mls.Listeners { + mls.Listeners = song.MaxListeners + mls.Date = song.StartAt + mls.Song = song.Artist + " - " + song.Title + mls.changed = true } - mlsChanged = true } -// MostListened returns song that currently is the song with most simultaneous -// listeners. -func MostListened() *MostListenedSong { - if mostListened.Date.Year() == 1 { +func (mls *MostListenedSong) Get() *MostListenedSong { + mls.RLock() + defer mls.RUnlock() + + if mls.Date.Year() == 1 { return nil } - return &mostListened + + return &MostListenedSong{ + Date: mls.Date, + Listeners: mls.Listeners, + Song: mls.Song} } -func LoadMostListenedSong(data []byte) (err error) { +// Load parses given data and fill a MostListenedSong. +func (mls *MostListenedSong) Load(data []byte) (err error) { + mls.Lock() + defer mls.Unlock() + lines := bytes.Split(data, []byte{'\n'}) if len(lines) != 3 { return errors.New("lines count mismatch, should be 3") } - mostListened = MostListenedSong{} - if mostListened.Date, err = time.Parse(time.RFC3339, string(lines[0])); err != nil { - return err + + var date time.Time + if date, err = time.Parse(time.RFC3339, string(lines[0])); err != nil { + return errors.Wrap(err, "wrong date/time format") } - if mostListened.Listeners, err = strconv.Atoi(string(lines[1])); err != nil { - return err + + var listeners int + if listeners, err = strconv.Atoi(string(lines[1])); err != nil { + return errors.Wrap(err, "a listeners number failed to parse") } - mostListened.Song = string(lines[2]) + + if len(lines[2]) == 0 { + return errors.New("a song is empty") + } + + mls.Date = date + mls.Listeners = listeners + mls.Song = string(lines[2]) return nil } -func StoreMostListenedSong() []byte { - if !mlsChanged { +// Store returns a byte slice of a marshalled to text MostListenedSong. +func (mls *MostListenedSong) Store() []byte { + if !mls.changed { return nil } - buf := make([]byte, 0, 30+len(mostListened.Song)) + buf := make([]byte, 0, 30+len(mls.Song)) b := bytes.NewBuffer(buf) - b.WriteString(mostListened.Date.Format(time.RFC3339)) + b.WriteString(mls.Date.Format(time.RFC3339)) b.WriteByte('\n') - b.WriteString(strconv.Itoa(mostListened.Listeners)) + b.WriteString(strconv.Itoa(mls.Listeners)) b.WriteByte('\n') - b.WriteString(mostListened.Song) + b.WriteString(mls.Song) return b.Bytes() } diff --git a/web/templates/index.pug b/web/templates/index.pug index 449436f..59dd545 100644 --- a/web/templates/index.pug +++ b/web/templates/index.pug @@ -1,4 +1,4 @@ -:go:func Index(mainSite string, songList *radio.SongList, listeners *radio.ListenerCounter, r *http.Request) +:go:func Index(mainSite string, songList *radio.SongList, listeners *radio.ListenerCounter, mls *radio.MostListenedSong, r *http.Request) :go:import "dwelling-radio/internal/radio" :go:import "dwelling-radio/pkg/utils" @@ -65,8 +65,7 @@ html(lang='en') else td td #{song.Artist} - #{song.Title} - - ml := radio.MostListened() - if ml.Song != "" - p.right Most listened song was "#{ml.Song}" on #{utils.ToClientTimezone(ml.Date, r).Format(radio.MostListenedDateFormat)} with #[b #{ml.Listeners}] listeners. + if mls != nil + p.right Most listened song was "#{mls.Song}" on #{utils.ToClientTimezone(mls.Date, r).Format(radio.MostListenedDateFormat)} with #[b #{mls.Listeners}] listeners. footer | 2017—2023 Alexander "Arav" Andreev <#[a(href='mailto:me@arav.su') me@arav.su]> #[a(href=mainSite+'/privacy') Privacy statements]