1
0
dwelling-radio/internal/radio/icecast.go

208 lines
4.7 KiB
Go
Raw Normal View History

2022-03-08 01:17:24 +04:00
package radio
import (
"bytes"
"dwelling-radio/pkg/watcher"
2022-03-08 01:17:24 +04:00
"encoding/json"
"io"
2022-03-08 01:17:24 +04:00
"net/http"
"os"
2022-03-08 01:17:24 +04:00
"strings"
2022-04-01 20:44:32 +04:00
"sync"
"syscall"
2022-03-08 01:17:24 +04:00
"time"
"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"
bufferSize = 8192
2022-04-02 04:29:23 +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"`
}
func (is *IcecastStatusDTO) Song() string {
return is.Icestats.Source.Artist + " - " + is.Icestats.Source.Title
}
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 {
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
}
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,
}, nil
2022-03-08 01:17:24 +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
}
var ret []Song = make([]Song, lastNSongs)
copy(ret[:], lastPlayedCache[lpcLen-lastNSongs:])
return ret, nil
}
}
return make([]Song, 0), nil
}
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
}
}
song, err := lastPlayedSong(playlistPath)
if err != nil {
return Song{}, err
}
return *song, nil
}
func lastPlayedSong(playlistPath string) (*Song, error) {
buf := make([]byte, bufferSize)
var last_song_line string
2022-03-08 01:17:24 +04:00
playlist, err := os.Open(playlistPath)
if err != nil {
return nil, err
}
2022-03-08 01:17:24 +04:00
playlist_stat, _ := playlist.Stat()
if playlist_stat.Size() == 0 {
return nil, nil
2022-03-08 01:17:24 +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
}
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
}
var playlistWatcher watcher.InotifyWatcher
var playlistFired chan uint32 = make(chan uint32)
func IcecastWatchPlaylist(playlistPath string, lastNSongs int) error {
playlistWatcher, err := watcher.NewInotifyWatcher()
if err != nil {
return errors.Wrap(err, "cannot instantiate inotify watcher")
}
err = playlistWatcher.AddWatch(playlistPath, watcher.ModIgnMask)
if err != nil {
return errors.Wrap(err, "cannot set a playlist to watch")
}
playlistWatcher.WatchForMask(playlistFired, watcher.ModIgnMask)
go func() {
for {
select {
case mask := <-playlistFired:
if mask&syscall.IN_MODIFY > 0 {
lastPlayedCacheMutex.Lock()
if song, err := lastPlayedSong(playlistPath); err == nil {
lastPlayedCache = append(lastPlayedCache, *song)
if len(lastPlayedCache) > lastNSongs {
lastPlayedCache = lastPlayedCache[1:]
}
}
lastPlayedCacheMutex.Unlock()
} else if mask&syscall.IN_IGNORED > 0 {
playlistWatcher.Close()
IcecastWatchPlaylist(playlistPath, lastNSongs)
return
}
}
}
}()
return nil
}
func IcecastWatchClose() {
playlistWatcher.Close()
}