bjarneo--cliamp
156 行
3.4 KiB
Go
156 行
3.4 KiB
Go
package radio
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/bjarneo/cliamp/internal/appdir"
|
|
"github.com/bjarneo/cliamp/internal/tomlutil"
|
|
)
|
|
|
|
const favoritesFile = "radio_favorites.toml"
|
|
|
|
// Favorites manages a persistent set of favorite radio stations.
|
|
type Favorites struct {
|
|
stations []CatalogStation
|
|
byURL map[string]struct{}
|
|
path string
|
|
}
|
|
|
|
// LoadFavorites reads favorites from ~/.config/cliamp/radio_favorites.toml.
|
|
func LoadFavorites() *Favorites {
|
|
f := &Favorites{byURL: make(map[string]struct{})}
|
|
dir, err := appdir.Dir()
|
|
if err != nil {
|
|
return f
|
|
}
|
|
f.path = filepath.Join(dir, favoritesFile)
|
|
stations, err := loadFavoriteStations(f.path)
|
|
if err != nil {
|
|
return f
|
|
}
|
|
f.stations = stations
|
|
for _, s := range stations {
|
|
f.byURL[s.URL] = struct{}{}
|
|
}
|
|
return f
|
|
}
|
|
|
|
// Stations returns all favorite stations.
|
|
func (f *Favorites) Stations() []CatalogStation {
|
|
return f.stations
|
|
}
|
|
|
|
// Contains returns true if the station URL is in favorites.
|
|
func (f *Favorites) Contains(url string) bool {
|
|
_, ok := f.byURL[url]
|
|
return ok
|
|
}
|
|
|
|
// Add adds a station to favorites and saves to disk.
|
|
func (f *Favorites) Add(s CatalogStation) error {
|
|
if f.Contains(s.URL) {
|
|
return nil
|
|
}
|
|
f.stations = append(f.stations, s)
|
|
f.byURL[s.URL] = struct{}{}
|
|
return f.save()
|
|
}
|
|
|
|
// Remove removes a station by URL from favorites and saves to disk.
|
|
func (f *Favorites) Remove(url string) error {
|
|
if !f.Contains(url) {
|
|
return nil
|
|
}
|
|
for i, s := range f.stations {
|
|
if s.URL == url {
|
|
f.stations = append(f.stations[:i], f.stations[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
delete(f.byURL, url)
|
|
return f.save()
|
|
}
|
|
|
|
func (f *Favorites) save() error {
|
|
if f.path == "" {
|
|
dir, err := appdir.Dir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.path = filepath.Join(dir, favoritesFile)
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(f.path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Build the full content in memory (writes to a Builder can't fail), then
|
|
// write a temp file and rename so a partial/failed write can never truncate
|
|
// or corrupt the existing favorites file.
|
|
var b strings.Builder
|
|
for i, s := range f.stations {
|
|
if i > 0 {
|
|
fmt.Fprintln(&b)
|
|
}
|
|
fmt.Fprintln(&b, "[[station]]")
|
|
fmt.Fprintf(&b, "name = %q\n", s.Name)
|
|
fmt.Fprintf(&b, "url = %q\n", s.URL)
|
|
if s.Country != "" {
|
|
fmt.Fprintf(&b, "country = %q\n", s.Country)
|
|
}
|
|
if s.Bitrate > 0 {
|
|
fmt.Fprintf(&b, "bitrate = %d\n", s.Bitrate)
|
|
}
|
|
if s.Codec != "" {
|
|
fmt.Fprintf(&b, "codec = %q\n", s.Codec)
|
|
}
|
|
if s.Tags != "" {
|
|
fmt.Fprintf(&b, "tags = %q\n", s.Tags)
|
|
}
|
|
if s.Homepage != "" {
|
|
fmt.Fprintf(&b, "homepage = %q\n", s.Homepage)
|
|
}
|
|
}
|
|
|
|
tmp := f.path + ".tmp"
|
|
if err := os.WriteFile(tmp, []byte(b.String()), 0o644); err != nil {
|
|
return err
|
|
}
|
|
return os.Rename(tmp, f.path)
|
|
}
|
|
|
|
// loadFavoriteStations parses the favorites TOML file.
|
|
func loadFavoriteStations(path string) ([]CatalogStation, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
var stations []CatalogStation
|
|
tomlutil.ParseSections(data, "station", func(f map[string]string) {
|
|
s := CatalogStation{
|
|
Name: f["name"],
|
|
URL: f["url"],
|
|
Country: f["country"],
|
|
Codec: f["codec"],
|
|
Tags: f["tags"],
|
|
Homepage: f["homepage"],
|
|
}
|
|
if n, err := strconv.Atoi(f["bitrate"]); err == nil {
|
|
s.Bitrate = n
|
|
}
|
|
if s.Name != "" && s.URL != "" {
|
|
stations = append(stations, s)
|
|
}
|
|
})
|
|
return stations, nil
|
|
}
|