226 lines
7.6 KiB
Bash
Executable File
226 lines
7.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Internet radio (radiorecord.ru) and MPRIS "now playing" for the media panel.
|
|
#
|
|
# radio status loop (deflisten)
|
|
# radio start ID tune to station ID
|
|
# radio toggle play / stop the last station
|
|
# radio stop
|
|
# radio vol PERCENT volume of the radio stream, else of the external player
|
|
# radio mute mute toggle, same target as vol
|
|
#
|
|
# mpv runs as the transient user unit eww-radio: independent of the eww
|
|
# daemon, and `systemctl --user stop eww-radio` always works.
|
|
|
|
api="https://www.radiorecord.ru/api"
|
|
unit=eww-radio
|
|
state_dir=${XDG_STATE_HOME:-$HOME/.local/state}/eww
|
|
cache_dir=${XDG_CACHE_HOME:-$HOME/.cache}/eww
|
|
id_file=$state_dir/radio-station
|
|
mkdir -p "$state_dir" "$cache_dir"
|
|
|
|
station_ids='[507,522,523,536,537,42532,42602]'
|
|
# Extra stations: {id (unique integer >= 1000000), title, stream_hls, icon_fill_white ("" for none)}
|
|
custom_stations='[]'
|
|
|
|
default_song='{"artist":"","song":"","image600":""}'
|
|
default_media='{"player":"","status":"Stopped","artist":"","title":"","art":""}'
|
|
stations_cache="$cache_dir/radio-stations-$(cksum <<<"$station_ids$custom_stations" | cut -d' ' -f1).json"
|
|
|
|
# --- stations --------------------------------------------------------------
|
|
|
|
# Station list, cached for a day (it only changes with station_ids).
|
|
stations() {
|
|
if [[ -s $stations_cache && -n $(find "$stations_cache" -mmin -1440 2>/dev/null) ]]; then
|
|
cat "$stations_cache"
|
|
return
|
|
fi
|
|
local rr out
|
|
rr=$(curl -sf --compressed --max-time 8 "$api/stations/" 2>/dev/null \
|
|
| jq -c --argjson ids "$station_ids" \
|
|
'.result.stations | map(select(.id | IN($ids[]))) | map({id, title, stream_hls, icon_fill_white, radiorecord: true})' 2>/dev/null)
|
|
if [[ -z $rr || $rr == null ]]; then
|
|
[[ -s $stations_cache ]] && { cat "$stations_cache"; return; }
|
|
rr='[]'
|
|
fi
|
|
out=$(jq -cn --argjson rr "$rr" --argjson custom "$custom_stations" '$rr + ($custom | map(. + {radiorecord: false}))')
|
|
[[ $rr != '[]' ]] && printf '%s\n' "$out" >"$stations_cache"
|
|
printf '%s\n' "$out"
|
|
}
|
|
|
|
station() { # ID FIELD
|
|
jq -r --argjson id "${1:-0}" --arg f "$2" 'first(.[] | select(.id == $id) | .[$f]) // empty' <<<"$STATIONS"
|
|
}
|
|
|
|
current_id() {
|
|
local id
|
|
id=$(cat "$id_file" 2>/dev/null)
|
|
[[ $id =~ ^[0-9]+$ ]] && echo "$id" || echo 0
|
|
}
|
|
|
|
playing() { systemctl --user is-active --quiet "$unit"; }
|
|
|
|
# --- playback --------------------------------------------------------------
|
|
|
|
start() {
|
|
local id=${1:-0} url
|
|
((id > 0)) || return 1
|
|
STATIONS=$(stations)
|
|
url=$(station "$id" stream_hls)
|
|
[[ -n $url && $url != null ]] || return 1
|
|
systemctl --user stop "$unit" 2>/dev/null
|
|
detach -u "$unit" mpv --no-video --really-quiet --audio-client-name="$unit" "$url" || return 1
|
|
printf '%s\n' "$id" >"$id_file"
|
|
}
|
|
|
|
stop() { systemctl --user stop "$unit" 2>/dev/null; }
|
|
|
|
# --- volume ----------------------------------------------------------------
|
|
|
|
# PipeWire node id of the first stream whose line matches $1 (case-insensitive).
|
|
stream_node() {
|
|
wpctl status 2>/dev/null | awk -v pat="$1" '
|
|
/Streams:/ { s = 1; next }
|
|
/Sinks:|Sources:|Filters:|Clients:/ { s = 0 }
|
|
s && tolower($0) ~ tolower(pat) && match($0, /[0-9]+\./) { print substr($0, RSTART, RLENGTH - 1); exit }'
|
|
}
|
|
|
|
external_player() { playerctl -l 2>/dev/null | grep -v '^mpv' | head -n1; }
|
|
|
|
# "pw NODE" while the radio plays, else "mpris PLAYER" for an external player.
|
|
volume_target() {
|
|
local n p
|
|
if playing; then
|
|
n=$(stream_node "$unit")
|
|
[[ -n $n ]] && { echo "pw $n"; return; }
|
|
fi
|
|
p=$(external_player)
|
|
[[ -n $p ]] && echo "mpris $p"
|
|
}
|
|
|
|
get_volume() {
|
|
local kind target
|
|
read -r kind target < <(volume_target)
|
|
case $kind in
|
|
pw) wpctl get-volume "$target" 2>/dev/null | awk '{ printf "%d", $2 * 100 + 0.5 }' ;;
|
|
mpris) playerctl -p "$target" volume 2>/dev/null | awk '{ printf "%d", $1 * 100 + 0.5 }' ;;
|
|
esac
|
|
}
|
|
|
|
set_volume() {
|
|
local vol=${1%.*} kind target
|
|
read -r kind target < <(volume_target)
|
|
case $kind in
|
|
pw) wpctl set-volume -l 1.0 "$target" "${vol}%" ;;
|
|
mpris) playerctl -p "$target" volume "$(awk -v v="$vol" 'BEGIN { printf "%.2f", v / 100 }')" 2>/dev/null ;;
|
|
esac
|
|
eww update radio-vol="$vol" 2>/dev/null
|
|
}
|
|
|
|
toggle_mute() {
|
|
local kind target n=""
|
|
read -r kind target < <(volume_target)
|
|
case $kind in
|
|
pw) n=$target ;;
|
|
mpris) n=$(stream_node "${target%%.*}") ;;
|
|
esac
|
|
[[ -n $n ]] || return
|
|
wpctl set-mute "$n" toggle
|
|
if wpctl get-volume "$n" 2>/dev/null | grep -q MUTED; then muted=true; else muted=false; fi
|
|
eww update radio-muted="$muted" 2>/dev/null
|
|
}
|
|
|
|
# --- status loop -----------------------------------------------------------
|
|
|
|
media_json() { # PLAYER STATUS ARTIST TITLE ART
|
|
jq -cn --arg player "$1" --arg status "${2:-Stopped}" --arg artist "$3" --arg title "$4" --arg art "$5" \
|
|
'{player: $player, status: $status, artist: $artist, title: $title, art: $art}'
|
|
}
|
|
|
|
song_playing() { # ID → SONG from the station history (radiorecord) or the title
|
|
local id=$1 fetched
|
|
if [[ $(station "$id" radiorecord) == true ]]; then
|
|
fetched=$(curl -sf --compressed --max-time 8 "$api/station/history/?id=$id" 2>/dev/null \
|
|
| jq -c '.result.history[0] | {artist: (.artist // ""), song: (.song // ""), image600: (.image600 // "")}' 2>/dev/null)
|
|
[[ -n $fetched && $fetched != null ]] && SONG=$fetched
|
|
else
|
|
SONG=$(jq -cn --arg t "$(station "$id" title)" '{artist: "", song: $t, image600: ""}')
|
|
fi
|
|
}
|
|
|
|
song_idle() { # ID → placeholder with the station's name and logo
|
|
SONG=$(jq -cn --arg t "$(station "$1" title)" --arg i "$(station "$1" icon_fill_white)" \
|
|
'{artist: "", song: $t, image600: $i}')
|
|
}
|
|
|
|
# playerctl --follow line: media\tPLAYER\tSTATUS\tARTIST\tTITLE\tART
|
|
media_event() {
|
|
local player status artist title art
|
|
IFS=$'\t' read -r player status artist title art <<<"$1"
|
|
if [[ -z $player ]]; then
|
|
MEDIA=$default_media MEDIA_PLAYER=""
|
|
elif [[ $player != mpv* ]]; then # the radio and jellyfin are mpv, not "external"
|
|
MEDIA=$(media_json "$player" "$status" "$artist" "$title" "$art")
|
|
MEDIA_PLAYER=$player
|
|
fi
|
|
}
|
|
|
|
_media_events() {
|
|
exec playerctl -a --follow metadata \
|
|
--format $'media\t{{playerName}}\t{{status}}\t{{artist}}\t{{title}}\t{{mpris:artUrl}}'
|
|
}
|
|
|
|
refresh() {
|
|
local line id now
|
|
for line in "$@"; do
|
|
[[ $line == media$'\t'* ]] && media_event "${line#media$'\t'}"
|
|
done
|
|
# a player that quit without a final metadata line
|
|
if [[ -n $MEDIA_PLAYER ]] && ! playerctl -l 2>/dev/null | grep -qx "$MEDIA_PLAYER"; then
|
|
MEDIA=$default_media MEDIA_PLAYER=""
|
|
fi
|
|
|
|
id=$(current_id)
|
|
now=$SECONDS
|
|
if playing; then
|
|
if [[ $PLAYING != true ]] || ((now - LAST_SONG >= 15)); then
|
|
PLAYING=true
|
|
song_playing "$id"
|
|
LAST_SONG=$now
|
|
fi
|
|
elif [[ $PLAYING != false ]]; then
|
|
PLAYING=false
|
|
song_idle "$id"
|
|
fi
|
|
|
|
emit "$(jq -cn --argjson stations "$STATIONS" --argjson radio "$id" \
|
|
--argjson paused "$([[ $PLAYING == true ]] && echo 0 || echo 1)" \
|
|
--argjson song "$SONG" --argjson media "$MEDIA" \
|
|
'{is_paused: $paused, song: $song, radio: $radio, stations: $stations, media: $media}')"
|
|
|
|
local v
|
|
v=$(get_volume)
|
|
if [[ -n $v && $v != "$LAST_VOL" ]]; then
|
|
LAST_VOL=$v
|
|
eww update radio-vol="$v" 2>/dev/null
|
|
fi
|
|
}
|
|
|
|
status_loop() {
|
|
STATIONS=$(stations)
|
|
SONG=$default_song MEDIA=$default_media MEDIA_PLAYER="" PLAYING="" LAST_SONG=0 LAST_VOL=""
|
|
refresh
|
|
mux_init
|
|
mux_add _media_events
|
|
mux_tick 2
|
|
mux_loop refresh
|
|
}
|
|
|
|
case ${1:-} in
|
|
start) detach_self "$@"; start "$2" ;;
|
|
stop) detach_self "$@"; stop ;;
|
|
toggle) detach_self "$@"; if playing; then stop; else start "$(current_id)"; fi ;;
|
|
vol) detach_self "$@"; set_volume "$2" ;;
|
|
mute) detach_self "$@"; toggle_mute ;;
|
|
*) status_loop ;;
|
|
esac
|