package radio import ( "bytes" "dwelling-radio/pkg/watcher" "encoding/json" "io" "net/http" "os" "sync" "syscall" "time" "github.com/pkg/errors" ) const ( IcecastPlaylistDateFormat = "02/Jan/2006:15:04:05 -0700" SongTimeFormat = "2006 15:04-0700" bufferSize = 32768 ) var ( lastPlayedCache []Song = make([]Song, 10) lastPlayedCacheMutex sync.Mutex ) 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) SongName() string { return is.Icestats.Source.Artist + " - " + is.Icestats.Source.Title } type IcecastStatus struct { ServerStartISO8601 string `json:"server_start_iso8601"` ServerStartDate string `json:"server_start_date"` 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"` } 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{ ServerStartISO8601: iceStatDTO.Icestats.ServerStartISO8601, ServerStartDate: iceStatDTO.Icestats.ServerStartDate, SongName: iceStatDTO.SongName(), ListenerPeak: iceStatDTO.Icestats.Source.ListenerPeak, Listeners: iceStatDTO.Icestats.Source.Listeners, }, nil } func IcecastLastPlayedSongs(lastNSongs int, playlistPath string) ([]Song, error) { { 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 } } songs, err := icecastLastPlayedSongs(playlistPath, lastNSongs) if err != nil { return make([]Song, 0), err } return songs, nil } func IcecastLastSong(playlistPath string) (Song, error) { { lastPlayedCacheMutex.Lock() defer lastPlayedCacheMutex.Unlock() if lpcLen := len(lastPlayedCache); lpcLen > 0 { return lastPlayedCache[lpcLen-1], nil } } song, err := icecastLastPlayedSongs(playlistPath, 1) if err != nil { return Song{}, err } return song[0], nil } func icecastLastPlayedSongs(playlistPath string, n int) ([]Song, error) { songs := make([]Song, n) var buf []byte var offset int64 = 0 playlist, err := os.Open(playlistPath) if err != nil { return nil, err } defer playlist.Close() playlist_stat, _ := playlist.Stat() if playlist_stat.Size() == 0 { return nil, nil } if playlist_stat.Size() < bufferSize { buf = make([]byte, playlist_stat.Size()) } else { buf = make([]byte, bufferSize) offset = playlist_stat.Size() - bufferSize } _, err = playlist.ReadAt(buf, offset) if err != nil && err != io.EOF { return nil, err } lines := bytes.Split(buf, []byte("\n")) if len(lines) < 3 { return nil, nil } lines = lines[:len(lines)-2] if len(lines) > n { lines = lines[len(lines)-n:] } for _, line := range lines { fields := bytes.Split(line, []byte("|")) tim, _ := time.Parse(IcecastPlaylistDateFormat, string(fields[0])) songs = append(songs, Song{ Time: tim.Format(SongTimeFormat), Listeners: string(fields[2]), Song: string(fields[3])}) } return songs, nil } 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() } pw.watcher, err = watcher.NewInotifyWatcher() if err != nil { return errors.Wrap(err, "cannot instantiate inotify watcher") } err = pw.watcher.AddWatch(playlistPath, watcher.ModIgnMask) if err != nil { return errors.Wrap(err, "cannot set a playlist to watch") } pw.watcher.WatchForMask(pw.changed, watcher.ModIgnMask) go func() { for { select { case mask := <-pw.changed: if mask&syscall.IN_MODIFY > 0 { lastPlayedCacheMutex.Lock() if songs, err := icecastLastPlayedSongs(playlistPath, n); err == nil { lastPlayedCache = songs } lastPlayedCacheMutex.Unlock() } else if mask&syscall.IN_IGNORED > 0 { pw.Close() pw.Watch(playlistPath, n) return } } } }() return nil } func (pw *PlaylistLogWatcher) Close() { pw.watcher.Close() }