2022-03-08 01:17:24 +04:00
|
|
|
package radio
|
|
|
|
|
|
|
|
import (
|
2022-08-29 08:58:35 +04:00
|
|
|
"bytes"
|
2022-03-31 15:35:04 +04:00
|
|
|
"dwelling-radio/pkg/watcher"
|
2022-03-08 01:17:24 +04:00
|
|
|
"encoding/json"
|
2022-08-29 08:58:35 +04:00
|
|
|
"io"
|
2023-08-20 18:03:51 +04:00
|
|
|
"log"
|
2022-03-08 01:17:24 +04:00
|
|
|
"net/http"
|
2022-08-29 08:58:35 +04:00
|
|
|
"os"
|
2022-04-01 20:44:32 +04:00
|
|
|
"sync"
|
2022-05-24 23:20:19 +04:00
|
|
|
"syscall"
|
2022-03-08 01:17:24 +04:00
|
|
|
"time"
|
2022-03-31 15:35:04 +04:00
|
|
|
|
|
|
|
"github.com/pkg/errors"
|
2022-03-08 01:17:24 +04:00
|
|
|
)
|
|
|
|
|
2022-04-02 04:29:23 +04:00
|
|
|
const (
|
|
|
|
IcecastPlaylistDateFormat = "02/Jan/2006:15:04:05 -0700"
|
|
|
|
SongTimeFormat = "2006 15:04-0700"
|
2022-08-29 08:58:35 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
bufferSizePerLine = 512
|
2023-09-17 19:18:18 +04:00
|
|
|
// Positions of first and second "|" separator on playlist.log line.
|
|
|
|
// The third one may float if amount of listeners will be >= 10.
|
|
|
|
separatorOne = 26
|
|
|
|
// +1 added to a second one just because it is used only at a start pos of
|
|
|
|
// a listeners' field, so there's no need to increment it every time.
|
|
|
|
separatorTwo = 38 + 1
|
2022-04-02 04:29:23 +04:00
|
|
|
)
|
|
|
|
|
2022-08-29 09:08:08 +04:00
|
|
|
var (
|
2023-03-12 22:25:58 +04:00
|
|
|
currentlyPlaying Song
|
2023-03-12 02:19:07 +04:00
|
|
|
lastPlayedCache []Song
|
2022-08-29 09:08:08 +04:00
|
|
|
lastPlayedCacheMutex sync.Mutex
|
|
|
|
)
|
|
|
|
|
2022-03-08 01:17:24 +04:00
|
|
|
type IcecastStatusDTO struct {
|
|
|
|
Icestats struct {
|
|
|
|
ServerStartISO8601 string `json:"server_start_iso8601"`
|
|
|
|
ServerStartDate string `json:"server_start"`
|
|
|
|
Source struct {
|
|
|
|
Artist string `json:"artist"`
|
|
|
|
Title string `json:"title"`
|
|
|
|
ListenerPeak int `json:"listener_peak"`
|
|
|
|
Listeners int `json:"listeners"`
|
|
|
|
} `json:"source"`
|
|
|
|
} `json:"icestats"`
|
|
|
|
}
|
|
|
|
|
2022-08-31 00:52:50 +04:00
|
|
|
func (is *IcecastStatusDTO) SongName() string {
|
2022-03-31 18:05:47 +04:00
|
|
|
return is.Icestats.Source.Artist + " - " + is.Icestats.Source.Title
|
2022-03-30 18:54:50 +04:00
|
|
|
}
|
|
|
|
|
2022-03-08 01:17:24 +04:00
|
|
|
type IcecastStatus struct {
|
2023-08-21 18:30:21 +04:00
|
|
|
ListenerPeak int `json:"listener_peak"`
|
|
|
|
Listeners int `json:"listeners"`
|
2023-09-15 03:20:12 +04:00
|
|
|
SongName string `json:"song"`
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
func IcecastGetStatus(icecastURL string) (*IcecastStatus, error) {
|
|
|
|
resp, err := http.Get(icecastURL)
|
|
|
|
if err != nil {
|
2023-08-21 18:31:06 +04:00
|
|
|
return &IcecastStatus{SongName: "Offline"}, err
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
iceStatDTO := &IcecastStatusDTO{}
|
|
|
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(iceStatDTO); err != nil {
|
2023-07-22 23:08:18 +04:00
|
|
|
return &IcecastStatus{}, err
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
2022-03-31 18:11:04 +04:00
|
|
|
return &IcecastStatus{
|
2023-08-21 18:30:21 +04:00
|
|
|
SongName: iceStatDTO.SongName(),
|
|
|
|
ListenerPeak: iceStatDTO.Icestats.Source.ListenerPeak,
|
|
|
|
Listeners: iceStatDTO.Icestats.Source.Listeners,
|
2022-03-31 18:11:04 +04:00
|
|
|
}, nil
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
2023-09-15 03:20:12 +04:00
|
|
|
type Song struct {
|
|
|
|
Time string `json:"time"`
|
|
|
|
Listeners string `json:"listeners"`
|
|
|
|
Song string `json:"song"`
|
|
|
|
}
|
|
|
|
|
2023-09-14 17:59:14 +04:00
|
|
|
func IcecastLastSongs(playlistPath string) []Song {
|
2023-03-12 04:01:32 +04:00
|
|
|
lastPlayedCacheMutex.Lock()
|
|
|
|
defer lastPlayedCacheMutex.Unlock()
|
|
|
|
if lpcLen := len(lastPlayedCache); lpcLen > 0 {
|
2023-09-14 18:20:47 +04:00
|
|
|
ret := make([]Song, lpcLen)
|
|
|
|
copy(ret, lastPlayedCache)
|
2023-09-14 17:59:14 +04:00
|
|
|
return ret
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
2023-09-14 17:59:14 +04:00
|
|
|
return nil
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
|
2023-09-14 17:59:14 +04:00
|
|
|
func IcecastLastSong(playlistPath string) *Song {
|
2023-03-12 04:01:32 +04:00
|
|
|
lastPlayedCacheMutex.Lock()
|
|
|
|
defer lastPlayedCacheMutex.Unlock()
|
|
|
|
if lpcLen := len(lastPlayedCache); lpcLen > 0 {
|
2023-09-14 17:59:14 +04:00
|
|
|
return &lastPlayedCache[lpcLen-1]
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
2023-09-14 17:59:14 +04:00
|
|
|
return nil
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
func icecastCurrentSong(playlistPath string) (*Song, error) {
|
|
|
|
fd, err := os.Open(playlistPath)
|
2022-03-30 20:21:18 +04:00
|
|
|
if err != nil {
|
2022-08-29 08:58:35 +04:00
|
|
|
return nil, err
|
2022-03-30 20:21:18 +04:00
|
|
|
}
|
2023-09-15 04:19:28 +04:00
|
|
|
defer fd.Close()
|
2022-08-29 08:58:35 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
fdSize, _ := fd.Seek(0, io.SeekEnd)
|
|
|
|
if fdSize == 0 {
|
2022-08-29 08:58:35 +04:00
|
|
|
return nil, nil
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
var bufSize int64 = bufferSizePerLine
|
|
|
|
if fdSize < bufferSizePerLine {
|
|
|
|
bufSize = fdSize
|
2022-09-21 04:56:06 +04:00
|
|
|
}
|
2022-08-29 08:58:35 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
_, err = fd.Seek(-bufSize, io.SeekEnd)
|
|
|
|
if err != nil {
|
2022-08-29 08:58:35 +04:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
buf := make([]byte, bufSize)
|
2023-03-13 05:10:17 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
_, err = fd.Read(buf)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
2022-09-21 04:56:06 +04:00
|
|
|
}
|
2022-08-30 02:29:11 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
curSongEnd := bytes.LastIndexByte(buf, '\n')
|
|
|
|
line := buf[bytes.LastIndexByte(buf[:curSongEnd], '\n')+1 : curSongEnd]
|
2022-08-29 08:58:35 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
songTime, _ := time.Parse(IcecastPlaylistDateFormat, string(line[:separatorOne]))
|
2022-08-30 02:29:11 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
separatorThree := bytes.LastIndexByte(line, '|')
|
2022-08-29 08:58:35 +04:00
|
|
|
|
2023-09-15 04:19:28 +04:00
|
|
|
return &Song{
|
|
|
|
Time: songTime.Format(SongTimeFormat),
|
|
|
|
Listeners: string(line[separatorTwo:separatorThree]),
|
|
|
|
Song: string(line[separatorThree+1:])}, nil
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
2022-03-31 15:35:04 +04:00
|
|
|
|
2022-09-19 01:55:09 +04:00
|
|
|
type PlaylistLogWatcher struct {
|
|
|
|
watcher *watcher.InotifyWatcher
|
|
|
|
changed chan uint32
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewPlaylistLogWatcher() *PlaylistLogWatcher {
|
|
|
|
return &PlaylistLogWatcher{changed: make(chan uint32)}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pw *PlaylistLogWatcher) Watch(playlistPath string, n int) (err error) {
|
|
|
|
if pw.watcher != nil {
|
|
|
|
pw.watcher.Close()
|
|
|
|
}
|
2022-03-31 15:35:04 +04:00
|
|
|
|
2022-09-19 01:55:09 +04:00
|
|
|
pw.watcher, err = watcher.NewInotifyWatcher()
|
2022-03-31 15:35:04 +04:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot instantiate inotify watcher")
|
|
|
|
}
|
|
|
|
|
2022-09-19 01:55:09 +04:00
|
|
|
err = pw.watcher.AddWatch(playlistPath, watcher.ModIgnMask)
|
2022-03-31 15:35:04 +04:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot set a playlist to watch")
|
|
|
|
}
|
|
|
|
|
2022-09-19 01:55:09 +04:00
|
|
|
pw.watcher.WatchForMask(pw.changed, watcher.ModIgnMask)
|
2022-03-31 15:35:04 +04:00
|
|
|
|
2023-08-20 03:20:49 +04:00
|
|
|
if lastPlayedCache == nil {
|
2023-09-15 04:19:28 +04:00
|
|
|
lastPlayedCache = make([]Song, 0, n)
|
|
|
|
}
|
|
|
|
|
|
|
|
cur, err := icecastCurrentSong(playlistPath)
|
|
|
|
if err != nil {
|
|
|
|
log.Println("failed to fetch current song:", err)
|
|
|
|
} else if cur != nil && currentlyPlaying.Time != "" {
|
|
|
|
currentlyPlaying = *cur
|
2023-08-20 03:20:49 +04:00
|
|
|
}
|
2023-03-12 21:31:18 +04:00
|
|
|
|
2022-03-31 15:35:04 +04:00
|
|
|
go func() {
|
|
|
|
for {
|
2023-08-21 06:08:21 +04:00
|
|
|
mask := <-pw.changed
|
|
|
|
|
|
|
|
if mask&syscall.IN_MODIFY > 0 {
|
|
|
|
lastPlayedCacheMutex.Lock()
|
2023-09-15 04:19:28 +04:00
|
|
|
song, err := icecastCurrentSong(playlistPath)
|
|
|
|
if err == nil && song != nil {
|
2023-09-15 03:17:44 +04:00
|
|
|
CheckAndUpdateMostListenedSong(song, ¤tlyPlaying)
|
2023-08-21 06:08:21 +04:00
|
|
|
if currentlyPlaying.Time == "" {
|
2023-09-15 04:19:28 +04:00
|
|
|
currentlyPlaying = *song
|
2023-08-21 06:08:21 +04:00
|
|
|
} else {
|
2023-09-15 04:19:28 +04:00
|
|
|
currentlyPlaying.Listeners = song.Listeners
|
2023-08-21 06:08:21 +04:00
|
|
|
if len(lastPlayedCache) == n {
|
|
|
|
lastPlayedCache = append(lastPlayedCache[1:], currentlyPlaying)
|
2023-03-12 21:48:01 +04:00
|
|
|
} else {
|
2023-08-21 06:08:21 +04:00
|
|
|
lastPlayedCache = append(lastPlayedCache, currentlyPlaying)
|
2023-03-12 21:48:01 +04:00
|
|
|
}
|
2023-09-15 04:19:28 +04:00
|
|
|
currentlyPlaying = *song
|
2022-05-24 23:20:19 +04:00
|
|
|
}
|
2023-08-21 06:08:21 +04:00
|
|
|
} else if err != nil {
|
|
|
|
log.Println("failed to retrieve last songs:", err)
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
2023-08-21 06:08:21 +04:00
|
|
|
lastPlayedCacheMutex.Unlock()
|
|
|
|
} else if mask&syscall.IN_IGNORED > 0 {
|
|
|
|
pw.Close()
|
|
|
|
pw.Watch(playlistPath, n)
|
|
|
|
return
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2022-09-19 01:55:09 +04:00
|
|
|
func (pw *PlaylistLogWatcher) Close() {
|
|
|
|
pw.watcher.Close()
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|