improve eww
This commit is contained in:
@@ -1,229 +1,225 @@
|
||||
#!/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.
|
||||
|
||||
URL_BASE="https://www.radiorecord.ru/api"
|
||||
MPV_PID_FILE="/tmp/mpv_radio_pid"
|
||||
RADIO_ID_FILE="/tmp/radio_id"
|
||||
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]'
|
||||
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='[]'
|
||||
|
||||
# Custom (non-radiorecord) stations
|
||||
# id must be a unique integer >= 1000000 to avoid collision with radiorecord IDs.
|
||||
# icon_fill_white: URL to station icon image, or "" 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="[]"
|
||||
DEFAULT_INFO='{"artist":"","song":"","image600":""}'
|
||||
DEFAULT_MEDIA='{"player":"","status":"Stopped","artist":"","title":"","art":""}'
|
||||
# --- stations --------------------------------------------------------------
|
||||
|
||||
PID=$([ -e "$MPV_PID_FILE" ] && cat "$MPV_PID_FILE" || echo 0)
|
||||
RADIO_ID=$([ -e "$RADIO_ID_FILE" ] && cat "$RADIO_ID_FILE" || echo 0)
|
||||
PAUSED=$(( PID == 0 ? 1 : 0 ))
|
||||
INFO="$DEFAULT_INFO"
|
||||
MEDIA="$DEFAULT_MEDIA"
|
||||
STATUS="{}"
|
||||
|
||||
get_stations() {
|
||||
local rr
|
||||
rr=$(curl -s --compressed "$URL_BASE/stations/" 2>/dev/null \
|
||||
| jq --argjson ids "$STATION_IDS" \
|
||||
'.result.stations | map(select(.id | IN($ids[]))) | map({id, title, stream_hls, icon_fill_white, "radiorecord": true})' 2>/dev/null)
|
||||
jq -n \
|
||||
--argjson rr "${rr:-[]}" \
|
||||
--argjson custom "$CUSTOM_STATIONS" \
|
||||
'$rr + ($custom | map(. + {"radiorecord": false}))'
|
||||
}
|
||||
|
||||
get_song() {
|
||||
curl -s --compressed "$URL_BASE/station/history/?id=$RADIO_ID" \
|
||||
| jq '.result.history[0] | {artist, song, image600}'
|
||||
}
|
||||
|
||||
get_stream_url() {
|
||||
echo "$STATIONS" | jq -r --argjson id "$RADIO_ID" 'map(select(.id == $id))[0].stream_hls // empty'
|
||||
}
|
||||
|
||||
get_player_info() {
|
||||
local player status artist title art
|
||||
# exclude mpv (radio) so it never appears as "external player"
|
||||
player=$(playerctl -l 2>/dev/null | grep -Ev '^mpv' | head -1)
|
||||
if [ -z "$player" ]; then
|
||||
echo '{"player":"","status":"Stopped","artist":"","title":"","art":""}'
|
||||
# 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
|
||||
status=$(playerctl -p "$player" status 2>/dev/null || echo "Stopped")
|
||||
artist=$(playerctl -p "$player" metadata artist 2>/dev/null || echo "")
|
||||
title=$(playerctl -p "$player" metadata title 2>/dev/null || echo "")
|
||||
art=$(playerctl -p "$player" metadata mpris:artUrl 2>/dev/null || echo "")
|
||||
jq -cnr --arg player "$player" --arg status "$status" \
|
||||
--arg artist "$artist" --arg title "$title" --arg art "$art" \
|
||||
'{player:$player, status:$status, artist:$artist, title:$title, art:$art}'
|
||||
}
|
||||
|
||||
update_state() {
|
||||
PID=$([ -e "$MPV_PID_FILE" ] && cat "$MPV_PID_FILE" || echo 0)
|
||||
RADIO_ID=$([ -e "$RADIO_ID_FILE" ] && cat "$RADIO_ID_FILE" || echo 0)
|
||||
if [ "$PID" -gt 0 ] && ! kill -0 "$PID" 2>/dev/null; then
|
||||
PID=0
|
||||
rm -f "$MPV_PID_FILE"
|
||||
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
|
||||
PAUSED=$(( PID == 0 ? 1 : 0 ))
|
||||
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"
|
||||
}
|
||||
|
||||
emit_status() {
|
||||
jq -cnr \
|
||||
--argjson stations "${STATIONS:-[]}" \
|
||||
--argjson radio_id "${RADIO_ID:-0}" \
|
||||
--argjson is_paused "${PAUSED:-1}" \
|
||||
--argjson info "${INFO:-$DEFAULT_INFO}" \
|
||||
--argjson media "${MEDIA:-$DEFAULT_MEDIA}" \
|
||||
'{"is_paused": $is_paused, "song": $info, "radio": $radio_id, "stations": $stations, "media": $media}'
|
||||
station() { # ID FIELD
|
||||
jq -r --argjson id "${1:-0}" --arg f "$2" 'first(.[] | select(.id == $id) | .[$f]) // empty' <<<"$STATIONS"
|
||||
}
|
||||
|
||||
get_stream_node() {
|
||||
local name="$1"
|
||||
wpctl status 2>/dev/null | awk -v pat="$name" '
|
||||
/Streams:/ { in_s = 1 }
|
||||
/Sinks:|Sources:|Clients:/ { in_s = 0 }
|
||||
in_s && /[0-9]+\./ && tolower($0) ~ tolower(pat) {
|
||||
match($0, /[0-9]+/); print substr($0, RSTART, RLENGTH); exit
|
||||
}
|
||||
'
|
||||
current_id() {
|
||||
local id
|
||||
id=$(cat "$id_file" 2>/dev/null)
|
||||
[[ $id =~ ^[0-9]+$ ]] && echo "$id" || echo 0
|
||||
}
|
||||
|
||||
get_mpv_node() {
|
||||
get_stream_node "mpv"
|
||||
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() {
|
||||
if [ "$PID" -gt 0 ] && kill -0 "$PID" 2>/dev/null; then
|
||||
local node_id; node_id=$(get_mpv_node)
|
||||
[ -n "$node_id" ] && wpctl get-volume "$node_id" 2>/dev/null \
|
||||
| awk '{printf "%d", $2 * 100}'
|
||||
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
|
||||
local player; player=$(playerctl -l 2>/dev/null | grep -Ev '^mpv' | head -1)
|
||||
[ -n "$player" ] && playerctl -p "$player" volume 2>/dev/null \
|
||||
| awk '{printf "%d", $1 * 100}'
|
||||
SONG=$(jq -cn --arg t "$(station "$id" title)" '{artist: "", song: $t, image600: ""}')
|
||||
fi
|
||||
}
|
||||
|
||||
do_vol() {
|
||||
local vol="${1%.*}"
|
||||
if [ "$PID" -gt 0 ] && kill -0 "$PID" 2>/dev/null; then
|
||||
local node_id; node_id=$(get_mpv_node)
|
||||
[ -n "$node_id" ] && wpctl set-volume "$node_id" "${vol}%"
|
||||
else
|
||||
playerctl volume "$(awk -v v="$vol" 'BEGIN{printf "%.2f", v/100}')" 2>/dev/null
|
||||
fi
|
||||
eww update radio-vol="$vol"
|
||||
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}')
|
||||
}
|
||||
|
||||
do_mute() {
|
||||
local node_id
|
||||
if [ "$PID" -gt 0 ] && kill -0 "$PID" 2>/dev/null; then
|
||||
node_id=$(get_mpv_node)
|
||||
else
|
||||
local player; player=$(playerctl -l 2>/dev/null | grep -Ev '^mpv' | head -1)
|
||||
[ -n "$player" ] && node_id=$(get_stream_node "${player%%.*}")
|
||||
fi
|
||||
[ -z "$node_id" ] && return
|
||||
wpctl set-mute "$node_id" toggle
|
||||
if wpctl get-volume "$node_id" 2>/dev/null | grep -q MUTED; then
|
||||
eww update radio-muted=true
|
||||
else
|
||||
eww update radio-muted=false
|
||||
# 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
|
||||
}
|
||||
|
||||
do_start() {
|
||||
[ "$RADIO_ID" -le 0 ] && return
|
||||
STATIONS=$(get_stations)
|
||||
RADIO_URL=$(get_stream_url)
|
||||
[ -z "$RADIO_URL" ] || [ "$RADIO_URL" = "null" ] && return
|
||||
[ "$PID" -gt 0 ] && kill "$PID" 2>/dev/null
|
||||
nohup mpv --no-video --quiet "$RADIO_URL" >/dev/null 2>&1 &
|
||||
echo $! > "$MPV_PID_FILE"
|
||||
echo "$RADIO_ID" > "$RADIO_ID_FILE"
|
||||
_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=$(get_stations)
|
||||
echo "$(emit_status)"
|
||||
|
||||
last_pid_check=0
|
||||
last_song_fetch=0
|
||||
last_media_fetch=0
|
||||
|
||||
while true; do
|
||||
now=$(date -u +%s%3N)
|
||||
|
||||
if (( now - last_pid_check > 1000 )); then
|
||||
update_state
|
||||
NEW_STATUS=$(emit_status)
|
||||
if [ "$NEW_STATUS" != "$STATUS" ]; then STATUS=$NEW_STATUS; echo "$STATUS"; fi
|
||||
last_pid_check=$now
|
||||
fi
|
||||
|
||||
if (( now - last_song_fetch > 15000 )); then
|
||||
if [ "$PAUSED" = 0 ]; then
|
||||
IS_RR=$(echo "$STATIONS" | jq --argjson id "$RADIO_ID" 'map(select(.id == $id))[0].radiorecord // true')
|
||||
if [ "$IS_RR" = "true" ]; then
|
||||
FETCHED=$(get_song)
|
||||
INFO=$(echo "$INFO" "$FETCHED" | jq -s '
|
||||
reduce .[] as $x ({}; . + ($x | with_entries(select(.value != null))))')
|
||||
else
|
||||
STATION_TITLE=$(echo "$STATIONS" | jq -r --argjson id "$RADIO_ID" 'map(select(.id == $id))[0].title // ""')
|
||||
INFO=$(jq -cnr --arg title "$STATION_TITLE" '{artist:"",song:$title,image600:""}')
|
||||
fi
|
||||
else
|
||||
STATION_IMG=$(echo "$STATIONS" \
|
||||
| jq -r --argjson id "$RADIO_ID" 'map(select(.id == $id))[0].icon_fill_white // ""')
|
||||
STATION_TITLE=$(echo "$STATIONS" \
|
||||
| jq -r --argjson id "$RADIO_ID" 'map(select(.id == $id))[0].title // ""')
|
||||
INFO=$(jq -cnr --arg img "$STATION_IMG" --arg title "$STATION_TITLE" \
|
||||
'{artist:"",song:$title,image600:$img}')
|
||||
fi
|
||||
NEW_STATUS=$(emit_status)
|
||||
if [ "$NEW_STATUS" != "$STATUS" ]; then STATUS=$NEW_STATUS; echo "$STATUS"; fi
|
||||
last_song_fetch=$now
|
||||
fi
|
||||
|
||||
if (( now - last_media_fetch > 3000 )); then
|
||||
MEDIA=$(get_player_info)
|
||||
NEW_STATUS=$(emit_status)
|
||||
if [ "$NEW_STATUS" != "$STATUS" ]; then STATUS=$NEW_STATUS; echo "$STATUS"; fi
|
||||
VOL=$(get_volume)
|
||||
[ -n "$VOL" ] && eww update radio-vol="$VOL" 2>/dev/null
|
||||
last_media_fetch=$now
|
||||
fi
|
||||
|
||||
sleep 0.5
|
||||
done
|
||||
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)
|
||||
RADIO_ID="$2"
|
||||
echo "$RADIO_ID" > "$RADIO_ID_FILE"
|
||||
do_start
|
||||
;;
|
||||
vol)
|
||||
do_vol "$2"
|
||||
;;
|
||||
mute)
|
||||
update_state
|
||||
do_mute
|
||||
;;
|
||||
toggle)
|
||||
update_state
|
||||
if [ "$PAUSED" = 1 ]; then
|
||||
[ "$RADIO_ID" -gt 0 ] && do_start
|
||||
else
|
||||
[ "$PID" -gt 0 ] && kill "$PID" 2>/dev/null
|
||||
rm -f "$MPV_PID_FILE"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
status_loop
|
||||
;;
|
||||
case ${1:-} in
|
||||
start) start "$2" ;;
|
||||
stop) stop ;;
|
||||
toggle) if playing; then stop; else start "$(current_id)"; fi ;;
|
||||
vol) set_volume "$2" ;;
|
||||
mute) toggle_mute ;;
|
||||
*) status_loop ;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user