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"
|
2022-03-08 01:17:24 +04:00
|
|
|
"net/http"
|
2022-08-29 08:58:35 +04:00
|
|
|
"os"
|
2022-03-08 01:17:24 +04:00
|
|
|
"strings"
|
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
|
|
|
|
|
|
|
bufferSize = 8192
|
2022-04-02 04:29:23 +04:00
|
|
|
)
|
|
|
|
|
2022-08-29 09:08:08 +04:00
|
|
|
var (
|
|
|
|
lastPlayedCache []Song = make([]Song, 10)
|
|
|
|
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-03-30 18:54:50 +04:00
|
|
|
func (is *IcecastStatusDTO) Song() 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 {
|
|
|
|
ServerStartISO8601 string `json:"server_start_iso8601"`
|
2022-03-31 02:16:15 +04:00
|
|
|
ServerStartDate string `json:"server_start_date"`
|
2022-03-08 01:17:24 +04:00
|
|
|
SongName string `json:"song"`
|
|
|
|
ListenerPeak int `json:"listener_peak"`
|
|
|
|
Listeners int `json:"listeners"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Song struct {
|
2022-03-31 20:01:13 +04:00
|
|
|
Time string `json:"time"`
|
|
|
|
Listeners string `json:"listeners"`
|
|
|
|
Song 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 {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
iceStatDTO := &IcecastStatusDTO{}
|
|
|
|
|
|
|
|
if err := json.NewDecoder(resp.Body).Decode(iceStatDTO); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2022-03-31 18:11:04 +04:00
|
|
|
return &IcecastStatus{
|
2022-03-08 01:17:24 +04:00
|
|
|
ServerStartISO8601: iceStatDTO.Icestats.ServerStartISO8601,
|
|
|
|
ServerStartDate: iceStatDTO.Icestats.ServerStartDate,
|
|
|
|
SongName: iceStatDTO.Song(),
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-03-30 20:21:18 +04:00
|
|
|
func IcecastLastPlayedSongs(lastNSongs int, playlistPath string) ([]Song, error) {
|
2022-04-01 20:44:32 +04:00
|
|
|
{
|
|
|
|
lastPlayedCacheMutex.Lock()
|
|
|
|
defer lastPlayedCacheMutex.Unlock()
|
|
|
|
if lpcLen := len(lastPlayedCache); lpcLen > 0 {
|
|
|
|
if lastNSongs > lpcLen {
|
|
|
|
lastNSongs = lpcLen
|
|
|
|
}
|
2022-04-02 02:00:28 +04:00
|
|
|
var ret []Song = make([]Song, lastNSongs)
|
|
|
|
copy(ret[:], lastPlayedCache[lpcLen-lastNSongs:])
|
|
|
|
return ret, nil
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
return make([]Song, 0), nil
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
func IcecastLastSong(playlistPath string) (Song, error) {
|
2022-04-01 20:44:32 +04:00
|
|
|
{
|
|
|
|
lastPlayedCacheMutex.Lock()
|
|
|
|
defer lastPlayedCacheMutex.Unlock()
|
|
|
|
if lpcLen := len(lastPlayedCache); lpcLen > 0 {
|
|
|
|
return lastPlayedCache[lpcLen-1], nil
|
|
|
|
}
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
song, err := lastPlayedSong(playlistPath)
|
|
|
|
if err != nil {
|
|
|
|
return Song{}, err
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
return *song, nil
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
func lastPlayedSong(playlistPath string) (*Song, error) {
|
|
|
|
buf := make([]byte, bufferSize)
|
|
|
|
var last_song_line string
|
2022-03-08 01:17:24 +04:00
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
playlist, 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
|
|
|
}
|
2022-03-08 01:17:24 +04:00
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
playlist_stat, _ := playlist.Stat()
|
|
|
|
|
|
|
|
if playlist_stat.Size() == 0 {
|
|
|
|
return nil, nil
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
playlist.Seek(-bufferSize, os.SEEK_END)
|
|
|
|
|
|
|
|
_, err = playlist.Read(buf)
|
|
|
|
if err != nil && err != io.EOF {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
newline_end_pos := bytes.LastIndexByte(buf, '\n')
|
|
|
|
|
|
|
|
if newline_end_pos == -1 && !bytes.ContainsRune(buf, '|') {
|
|
|
|
return nil, nil
|
|
|
|
} else if newline_end_pos == -1 {
|
|
|
|
newline_end_pos = len(buf)
|
|
|
|
}
|
|
|
|
|
|
|
|
newline_start_pos := bytes.LastIndexByte(buf[:newline_end_pos-1], '\n')
|
|
|
|
|
|
|
|
last_song_line = string(buf[newline_start_pos+1 : newline_end_pos-1])
|
|
|
|
|
|
|
|
if strings.Count(last_song_line, "|") != 3 {
|
|
|
|
return nil, nil
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
|
|
|
|
2022-08-29 08:58:35 +04:00
|
|
|
fields := strings.Split(last_song_line, "|")
|
|
|
|
|
|
|
|
tim, _ := time.Parse(IcecastPlaylistDateFormat, fields[0])
|
|
|
|
|
|
|
|
return &Song{
|
|
|
|
Time: tim.Format(SongTimeFormat),
|
|
|
|
Listeners: fields[2],
|
|
|
|
Song: fields[3]}, nil
|
2022-03-08 01:17:24 +04:00
|
|
|
}
|
2022-03-31 15:35:04 +04:00
|
|
|
|
|
|
|
var playlistWatcher watcher.InotifyWatcher
|
2022-04-01 22:35:30 +04:00
|
|
|
var playlistFired chan uint32 = make(chan uint32)
|
2022-03-31 15:35:04 +04:00
|
|
|
|
|
|
|
func IcecastWatchPlaylist(playlistPath string, lastNSongs int) error {
|
|
|
|
playlistWatcher, err := watcher.NewInotifyWatcher()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot instantiate inotify watcher")
|
|
|
|
}
|
|
|
|
|
2022-05-24 23:20:19 +04:00
|
|
|
err = playlistWatcher.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-05-24 23:20:19 +04:00
|
|
|
playlistWatcher.WatchForMask(playlistFired, watcher.ModIgnMask)
|
2022-03-31 15:35:04 +04:00
|
|
|
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
select {
|
2022-05-24 23:20:19 +04:00
|
|
|
case mask := <-playlistFired:
|
|
|
|
if mask&syscall.IN_MODIFY > 0 {
|
|
|
|
lastPlayedCacheMutex.Lock()
|
2022-08-29 08:58:35 +04:00
|
|
|
if song, err := lastPlayedSong(playlistPath); err == nil {
|
|
|
|
lastPlayedCache = append(lastPlayedCache, *song)
|
|
|
|
if len(lastPlayedCache) > lastNSongs {
|
|
|
|
lastPlayedCache = lastPlayedCache[1:]
|
|
|
|
}
|
2022-05-24 23:20:19 +04:00
|
|
|
}
|
|
|
|
lastPlayedCacheMutex.Unlock()
|
|
|
|
} else if mask&syscall.IN_IGNORED > 0 {
|
|
|
|
playlistWatcher.Close()
|
|
|
|
IcecastWatchPlaylist(playlistPath, lastNSongs)
|
|
|
|
return
|
2022-03-31 15:35:04 +04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func IcecastWatchClose() {
|
|
|
|
playlistWatcher.Close()
|
|
|
|
}
|