improve eww

This commit is contained in:
2026-09-10 19:54:50 +02:00
parent e551d36e1c
commit 7bb27f9373
46 changed files with 1296 additions and 1092 deletions
+192 -196
View File
@@ -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