2023-12-04 01:24:00 +01:00
|
|
|
package main
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
/*
|
|
|
|
* This app is basically a frontend using MPlayer to stream, so we don't have to deal with
|
|
|
|
* complicated streaming stuff when there's usually a perfectly working program that will
|
|
|
|
* do this better than we can ever do.
|
|
|
|
*
|
|
|
|
* We keep control of MPlayer through the MPlayer object and pipes to send it commands to
|
|
|
|
* play, stop and which is the URL we want to stream (usually, RadioSpiral's).
|
|
|
|
*
|
|
|
|
* There is also a goroutine that checks the broadcast information every minute, updates
|
|
|
|
* the GUI with the currently playing information and also the next show coming up.
|
|
|
|
*/
|
|
|
|
|
2023-12-04 01:24:00 +01:00
|
|
|
import (
|
|
|
|
"bufio"
|
2023-12-04 11:15:05 +01:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
2023-12-04 01:24:00 +01:00
|
|
|
"io"
|
2023-12-04 11:15:05 +01:00
|
|
|
"net/http"
|
2023-12-04 01:24:00 +01:00
|
|
|
"os/exec"
|
|
|
|
"strings"
|
2023-12-04 11:15:05 +01:00
|
|
|
"time"
|
2023-12-04 01:24:00 +01:00
|
|
|
|
2023-12-04 11:15:05 +01:00
|
|
|
"fyne.io/fyne/v2"
|
2023-12-04 01:24:00 +01:00
|
|
|
"fyne.io/fyne/v2/app"
|
|
|
|
"fyne.io/fyne/v2/container"
|
2023-12-04 11:15:05 +01:00
|
|
|
"fyne.io/fyne/v2/layout"
|
2023-12-04 01:56:39 +01:00
|
|
|
"fyne.io/fyne/v2/theme"
|
2023-12-04 01:24:00 +01:00
|
|
|
"fyne.io/fyne/v2/widget"
|
|
|
|
)
|
|
|
|
|
|
|
|
// helper
|
|
|
|
func check(err error) {
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-04 11:15:05 +01:00
|
|
|
// JSON data we receive from the wp-json/radio/broadcast endpoint
|
|
|
|
type BroadcastResponse struct {
|
|
|
|
Broadcast BroadcastInfo `json:"broadcast"`
|
|
|
|
Updated int `json:"updated"`
|
|
|
|
Success bool `json:"success"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type BroadcastInfo struct {
|
|
|
|
NowPlaying NowPlayingInfo `json:"now_playing"`
|
|
|
|
NextShow NextShowInfo `json:"next_show"`
|
|
|
|
CurrentShow bool `json:"current_show"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type NowPlayingInfo struct {
|
|
|
|
Text string `json:"text"`
|
|
|
|
Artist string `json:"artist"`
|
|
|
|
Title string `json:"title"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type NextShowInfo struct {
|
|
|
|
Day string `json:"day"`
|
|
|
|
Date string `json:"date"`
|
|
|
|
Start string `json:"start"`
|
|
|
|
End string `json:"end"`
|
|
|
|
Show ShowData `json:"show"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type ShowData struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
AvatarUrl string `json:"avatar_url"`
|
|
|
|
ImageUrl string `json:"image_url"`
|
|
|
|
Hosts []HostsData `json:"hosts"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type HostsData struct {
|
|
|
|
Name string `json:"name"`
|
|
|
|
}
|
|
|
|
|
2023-12-04 01:24:00 +01:00
|
|
|
// Radio player interface
|
|
|
|
type RadioPlayer interface {
|
|
|
|
Play(stream_url string)
|
|
|
|
Mute()
|
|
|
|
Pause()
|
|
|
|
IncVolume()
|
|
|
|
DecVolume()
|
|
|
|
Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
// MPlayer
|
|
|
|
type MPlayer struct {
|
|
|
|
player_name string
|
|
|
|
is_playing bool
|
|
|
|
stream_url string
|
|
|
|
command *exec.Cmd
|
|
|
|
in io.WriteCloser
|
|
|
|
out io.ReadCloser
|
|
|
|
pipe_chan chan io.ReadCloser
|
|
|
|
}
|
|
|
|
|
|
|
|
func (player *MPlayer) Play(stream_url string) {
|
|
|
|
if !player.is_playing {
|
|
|
|
var err error
|
|
|
|
is_playlist := strings.HasSuffix(stream_url, ".m3u") || strings.HasSuffix(stream_url, ".pls")
|
|
|
|
if is_playlist {
|
2023-12-04 11:15:05 +01:00
|
|
|
// player.command = exec.Command(player.player_name, "-quiet", "-playlist", stream_url)
|
|
|
|
player.command = exec.Command(player.player_name, "-playlist", stream_url)
|
2023-12-04 01:24:00 +01:00
|
|
|
} else {
|
2023-12-04 11:15:05 +01:00
|
|
|
player.command = exec.Command(player.player_name, stream_url)
|
2023-12-04 01:24:00 +01:00
|
|
|
}
|
|
|
|
player.in, err = player.command.StdinPipe()
|
|
|
|
check(err)
|
|
|
|
player.out, err = player.command.StdoutPipe()
|
|
|
|
check(err)
|
|
|
|
|
|
|
|
err = player.command.Start()
|
|
|
|
check(err)
|
|
|
|
|
|
|
|
player.is_playing = true
|
|
|
|
player.stream_url = stream_url
|
|
|
|
go func() {
|
|
|
|
player.pipe_chan <- player.out
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (player *MPlayer) Close() {
|
|
|
|
if player.is_playing {
|
|
|
|
player.is_playing = false
|
|
|
|
|
|
|
|
player.in.Write([]byte("q"))
|
|
|
|
player.in.Close()
|
|
|
|
player.out.Close()
|
|
|
|
player.command = nil
|
|
|
|
|
|
|
|
player.stream_url = ""
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (player *MPlayer) Mute() {
|
|
|
|
if player.is_playing {
|
|
|
|
player.in.Write([]byte("m"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (player *MPlayer) Pause() {
|
|
|
|
if player.is_playing {
|
|
|
|
player.in.Write([]byte("p"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (player *MPlayer) IncVolume() {
|
|
|
|
if player.is_playing {
|
|
|
|
player.in.Write([]byte("*"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (player *MPlayer) DecVolume() {
|
|
|
|
if player.is_playing {
|
|
|
|
player.in.Write([]byte("/"))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
2023-12-04 11:15:05 +01:00
|
|
|
RADIOSPIRAL_JSON_ENDPOINT := "https://radiospiral.net/wp-json/radio/broadcast"
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Create the status channel, to read from MPlayer and the pipe to send commands to it
|
2023-12-04 01:24:00 +01:00
|
|
|
status_chan := make(chan string)
|
|
|
|
pipe_chan := make(chan io.ReadCloser)
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Create our MPlayer instance
|
2023-12-04 01:24:00 +01:00
|
|
|
mplayer := MPlayer{player_name: "mplayer", is_playing: false, pipe_chan: pipe_chan}
|
|
|
|
|
2023-12-04 11:15:05 +01:00
|
|
|
// Process the output of Mplayer here
|
2023-12-04 01:24:00 +01:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
out_pipe := <-pipe_chan
|
|
|
|
reader := bufio.NewReader(out_pipe)
|
|
|
|
for {
|
|
|
|
data, err := reader.ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
status_chan <- "Playing stopped"
|
|
|
|
break
|
|
|
|
} else {
|
|
|
|
status_chan <- data
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Create our app and window
|
2023-12-04 01:24:00 +01:00
|
|
|
app := app.New()
|
|
|
|
window := app.NewWindow("RadioSpiral")
|
|
|
|
|
2023-12-04 11:15:05 +01:00
|
|
|
window.Resize(fyne.NewSize(400, 600))
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Keep the status of the player
|
2023-12-04 01:56:39 +01:00
|
|
|
play_status := false
|
|
|
|
|
2023-12-04 01:24:00 +01:00
|
|
|
radiospiral_label := widget.NewLabel("RadioSpiral")
|
2023-12-04 11:15:05 +01:00
|
|
|
nowplaying_label := widget.NewLabel("")
|
|
|
|
|
|
|
|
radiospiral_label.Alignment = fyne.TextAlignCenter
|
|
|
|
nowplaying_label.Alignment = fyne.TextAlignCenter
|
|
|
|
|
2023-12-04 01:56:39 +01:00
|
|
|
var play_button *widget.Button
|
|
|
|
play_button = widget.NewButtonWithIcon("", theme.MediaStopIcon(), func() {
|
2023-12-04 11:34:14 +01:00
|
|
|
// Here we control each time the button is pressed and update its
|
|
|
|
// appearance anytime it is clicked. We make the player start playing
|
|
|
|
// or pause.
|
2023-12-04 01:56:39 +01:00
|
|
|
if !mplayer.is_playing {
|
|
|
|
play_button.SetIcon(theme.MediaPlayIcon())
|
2023-12-04 11:15:05 +01:00
|
|
|
mplayer.Play("https://radiospiral.radio/stream.mp3")
|
2023-12-04 01:56:39 +01:00
|
|
|
play_status = true
|
|
|
|
} else {
|
|
|
|
if play_status {
|
|
|
|
play_status = false
|
|
|
|
play_button.SetIcon(theme.MediaPauseIcon())
|
|
|
|
} else {
|
|
|
|
play_status = true
|
|
|
|
play_button.SetIcon(theme.MediaPlayIcon())
|
|
|
|
}
|
|
|
|
mplayer.Pause()
|
|
|
|
}
|
|
|
|
})
|
2023-12-04 01:24:00 +01:00
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// TODO: Check how can we make this area RadioSpiral blue
|
|
|
|
// TODO: Add RadioSpiral icon
|
|
|
|
// TODO: Add RadioSpiral's font
|
2023-12-04 11:26:17 +01:00
|
|
|
header := container.NewCenter(radiospiral_label)
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Layout the whole thing
|
2023-12-04 01:24:00 +01:00
|
|
|
window.SetContent(container.NewVBox(
|
2023-12-04 11:26:17 +01:00
|
|
|
header,
|
2023-12-04 11:15:05 +01:00
|
|
|
layout.NewSpacer(),
|
2023-12-04 01:24:00 +01:00
|
|
|
nowplaying_label,
|
2023-12-04 01:56:39 +01:00
|
|
|
play_button,
|
2023-12-04 01:24:00 +01:00
|
|
|
))
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Now that everything is laid out, we can start this
|
|
|
|
// small goroutine every minute, retrieve the stream data
|
|
|
|
// and the shows data, update the GUI accordingly
|
2023-12-04 11:15:05 +01:00
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
fmt.Println("Retrieving broadcast data")
|
|
|
|
resp, err := http.Get(RADIOSPIRAL_JSON_ENDPOINT)
|
|
|
|
check(err)
|
|
|
|
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
|
|
check(err)
|
|
|
|
|
|
|
|
var broadcastResponse BroadcastResponse
|
|
|
|
json.Unmarshal(body, &broadcastResponse)
|
|
|
|
nowplaying_label.SetText("Now playing: " + broadcastResponse.Broadcast.NowPlaying.Text)
|
|
|
|
time.Sleep(60 * time.Second)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2023-12-04 11:34:14 +01:00
|
|
|
// Showtime!
|
2023-12-04 01:24:00 +01:00
|
|
|
window.ShowAndRun()
|
2023-12-04 11:34:14 +01:00
|
|
|
|
|
|
|
// Window has been closed, make sure that MPlayer closes too
|
2023-12-04 01:28:03 +01:00
|
|
|
mplayer.Close()
|
2023-12-04 01:24:00 +01:00
|
|
|
}
|