feat: remove the dj command
This commit is contained in:
parent
910e0b0cda
commit
5e25c5d785
1 changed files with 340 additions and 12 deletions
352
home.nix
352
home.nix
|
|
@ -107,6 +107,327 @@ let
|
||||||
exec ${pkgs.zellij}/bin/zellij action rename-tab "$next_tab_name"
|
exec ${pkgs.zellij}/bin/zellij action rename-tab "$next_tab_name"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
browserAudio = pkgs.writeShellApplication {
|
||||||
|
name = "browser-audio";
|
||||||
|
runtimeInputs = [
|
||||||
|
pkgs.coreutils
|
||||||
|
pkgs.curl
|
||||||
|
pkgs.ffmpeg-full
|
||||||
|
pkgs.fzf
|
||||||
|
pkgs.gawk
|
||||||
|
pkgs.gnugrep
|
||||||
|
pkgs.jq
|
||||||
|
pkgs.pulseaudio
|
||||||
|
pkgs.qpwgraph
|
||||||
|
pkgs.pavucontrol
|
||||||
|
pkgs.procps
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
sink_name="''${BROWSER_AUDIO_SINK:-browser-radio}"
|
||||||
|
sink_description="''${BROWSER_AUDIO_DESCRIPTION:-Browser Radio}"
|
||||||
|
bitrate="''${BROWSER_AUDIO_BITRATE:-128k}"
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
printf '%s\n' \
|
||||||
|
"Usage: browser-audio <command> [args]" \
|
||||||
|
"" \
|
||||||
|
"Commands:" \
|
||||||
|
" setup Create the dedicated browser sink if needed" \
|
||||||
|
" pick Pick a live playback stream and move it to the sink" \
|
||||||
|
" route <regex> Move matching playback streams to the sink" \
|
||||||
|
" status Show the sink and any streams already routed to it" \
|
||||||
|
" open Open pavucontrol and qpwgraph for manual routing" \
|
||||||
|
" cast <icecast-url> Stream the sink monitor to Icecast/Shoutcast with ffmpeg" \
|
||||||
|
" cast-pick <url> Pick a live stream, route it, then start casting" \
|
||||||
|
" stop Stop prior ffmpeg jobs and remove the sink" \
|
||||||
|
" remove Remove the dedicated sink" \
|
||||||
|
"" \
|
||||||
|
"Notes:" \
|
||||||
|
" - If your browser exposes a tab as its own playback stream, pick can isolate it." \
|
||||||
|
" - Otherwise, launch a dedicated browser instance for music and route that stream."
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_sink() {
|
||||||
|
if ! pactl list short sinks | awk '{print $2}' | grep -Fxq "$sink_name"; then
|
||||||
|
pactl load-module module-null-sink \
|
||||||
|
sink_name="$sink_name" \
|
||||||
|
sink_properties="device.description=$sink_description" >/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
sink_module_id() {
|
||||||
|
pactl -f json list modules | jq -r --arg sink_name "$sink_name" '
|
||||||
|
.[]
|
||||||
|
| select(.name == "module-null-sink")
|
||||||
|
| select((.argument // "") | contains("sink_name=" + $sink_name))
|
||||||
|
| .index
|
||||||
|
' | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_existing_casts() {
|
||||||
|
pkill -f "ffmpeg.*$sink_name\.monitor" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_sink() {
|
||||||
|
local module_id
|
||||||
|
|
||||||
|
module_id="$(sink_module_id || true)"
|
||||||
|
if [ -n "$module_id" ] && [ "$module_id" != "null" ]; then
|
||||||
|
pactl unload-module "$module_id"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_state() {
|
||||||
|
stop_existing_casts
|
||||||
|
remove_sink
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_rows() {
|
||||||
|
pactl -f json list sink-inputs | jq -r '
|
||||||
|
.[]
|
||||||
|
| [
|
||||||
|
(.index | tostring),
|
||||||
|
(.sink | tostring),
|
||||||
|
(.properties."application.name" // "unknown-app"),
|
||||||
|
(.properties."media.name" // .properties."node.description" // "unknown-media"),
|
||||||
|
(.properties."application.process.binary" // "unknown-bin")
|
||||||
|
]
|
||||||
|
| @tsv
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
pick_stream() {
|
||||||
|
local selection
|
||||||
|
|
||||||
|
selection="$(stream_rows | fzf \
|
||||||
|
--delimiter=$'\t' \
|
||||||
|
--with-nth=3,4,5 \
|
||||||
|
--layout=reverse \
|
||||||
|
--border \
|
||||||
|
--prompt='audio> ' \
|
||||||
|
--header=$'Pick a live playback stream to route into browser-radio\napp | media | binary' \
|
||||||
|
--exit-0)"
|
||||||
|
|
||||||
|
if [ -z "$selection" ]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pactl move-sink-input "$(printf '%s\n' "$selection" | cut -f1)" "$sink_name"
|
||||||
|
}
|
||||||
|
|
||||||
|
route_matching() {
|
||||||
|
local pattern="$1"
|
||||||
|
local matches
|
||||||
|
|
||||||
|
matches="$(stream_rows | grep -Ei "$pattern" || true)"
|
||||||
|
if [ -z "$matches" ]; then
|
||||||
|
printf 'No playback streams matched %s\n' "$pattern" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$matches" | while IFS=$'\t' read -r stream_id _rest; do
|
||||||
|
[ -n "$stream_id" ] || continue
|
||||||
|
pactl move-sink-input "$stream_id" "$sink_name"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
printf 'Sink: %s\n' "$sink_name"
|
||||||
|
pactl list short sinks | awk -v sink="$sink_name" '$2 == sink {print}'
|
||||||
|
printf '\nStreams on %s:\n' "$sink_name"
|
||||||
|
pactl -f json list sink-inputs | jq -r --arg sink_name "$sink_name" --argjson sink_index "$(pactl list short sinks | awk -v sink="$sink_name" '$2 == sink {print $1; exit}')" '
|
||||||
|
.[]
|
||||||
|
| select(.sink == $sink_index)
|
||||||
|
| "- #\(.index) \(.properties["application.name"] // "unknown-app") :: \(.properties["media.name"] // .properties["node.description"] // "unknown-media")"
|
||||||
|
'
|
||||||
|
}
|
||||||
|
|
||||||
|
cast_sink() {
|
||||||
|
local url="$1"
|
||||||
|
local rest auth host_path endpoint
|
||||||
|
local tmpdir fifo ffmpeg_pid curl_pid ffmpeg_status curl_status interrupted=0
|
||||||
|
|
||||||
|
case "$url" in
|
||||||
|
icecast://*)
|
||||||
|
rest="''${url#icecast://}"
|
||||||
|
auth="''${rest%%@*}"
|
||||||
|
host_path="''${rest#*@}"
|
||||||
|
endpoint="http://''${host_path}"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'cast requires an icecast:// URL\n' >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
tmpdir="$(mktemp -d)"
|
||||||
|
fifo="$tmpdir/audio.mp3"
|
||||||
|
mkfifo "$fifo"
|
||||||
|
|
||||||
|
cleanup_cast() {
|
||||||
|
local pid
|
||||||
|
local waited
|
||||||
|
|
||||||
|
trap - INT TERM EXIT
|
||||||
|
|
||||||
|
for pid in "''${ffmpeg_pid:-}" "''${curl_pid:-}"; do
|
||||||
|
[ -n "$pid" ] || continue
|
||||||
|
kill "$pid" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
for pid in "''${ffmpeg_pid:-}" "''${curl_pid:-}"; do
|
||||||
|
[ -n "$pid" ] || continue
|
||||||
|
waited=0
|
||||||
|
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||||
|
if [ "$waited" -ge 20 ]; then
|
||||||
|
kill -9 "$pid" >/dev/null 2>&1 || true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.1
|
||||||
|
waited=$((waited + 1))
|
||||||
|
done
|
||||||
|
wait "$pid" >/dev/null 2>&1 || true
|
||||||
|
done
|
||||||
|
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
if [ "$interrupted" -eq 1 ]; then
|
||||||
|
exit 130
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'interrupted=1; cleanup_cast' INT TERM
|
||||||
|
trap cleanup_cast EXIT
|
||||||
|
|
||||||
|
curl \
|
||||||
|
--silent \
|
||||||
|
--show-error \
|
||||||
|
--http1.0 \
|
||||||
|
--user "$auth" \
|
||||||
|
--header 'Content-Type: audio/mpeg' \
|
||||||
|
--request SOURCE \
|
||||||
|
--data-binary @- \
|
||||||
|
"$endpoint" <"$fifo" &
|
||||||
|
curl_pid=$!
|
||||||
|
|
||||||
|
ffmpeg \
|
||||||
|
-y \
|
||||||
|
-hide_banner \
|
||||||
|
-loglevel warning \
|
||||||
|
-f pulse \
|
||||||
|
-channel_layout stereo \
|
||||||
|
-i "$sink_name.monitor" \
|
||||||
|
-ac 2 \
|
||||||
|
-ar 44100 \
|
||||||
|
-acodec libmp3lame \
|
||||||
|
-b:a "$bitrate" \
|
||||||
|
-id3v2_version 0 \
|
||||||
|
-write_xing 0 \
|
||||||
|
-f mp3 \
|
||||||
|
"$fifo" &
|
||||||
|
ffmpeg_pid=$!
|
||||||
|
|
||||||
|
set +e
|
||||||
|
wait "$ffmpeg_pid"
|
||||||
|
ffmpeg_status=$?
|
||||||
|
wait "$curl_pid"
|
||||||
|
curl_status=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
trap - INT TERM EXIT
|
||||||
|
rm -rf "$tmpdir"
|
||||||
|
|
||||||
|
if [ "$ffmpeg_status" -ne 0 ]; then
|
||||||
|
return "$ffmpeg_status"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$curl_status" -ne 0 ]; then
|
||||||
|
return "$curl_status"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
command="''${1:-help}"
|
||||||
|
case "$command" in
|
||||||
|
setup)
|
||||||
|
ensure_sink
|
||||||
|
show_status
|
||||||
|
;;
|
||||||
|
pick)
|
||||||
|
ensure_sink
|
||||||
|
pick_stream
|
||||||
|
show_status
|
||||||
|
;;
|
||||||
|
route)
|
||||||
|
ensure_sink
|
||||||
|
if [ "''${2:-}" = "" ]; then
|
||||||
|
printf 'route requires a regex\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
route_matching "$2"
|
||||||
|
show_status
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
ensure_sink
|
||||||
|
show_status
|
||||||
|
;;
|
||||||
|
open)
|
||||||
|
ensure_sink
|
||||||
|
pavucontrol >/dev/null 2>&1 &
|
||||||
|
qpwgraph >/dev/null 2>&1 &
|
||||||
|
;;
|
||||||
|
cast)
|
||||||
|
reset_state
|
||||||
|
ensure_sink
|
||||||
|
if [ "''${2:-}" = "" ]; then
|
||||||
|
printf 'cast requires an icecast:// URL\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cast_sink "$2"
|
||||||
|
;;
|
||||||
|
cast-pick)
|
||||||
|
reset_state
|
||||||
|
ensure_sink
|
||||||
|
if [ "''${2:-}" = "" ]; then
|
||||||
|
printf 'cast-pick requires an icecast:// URL\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
pick_stream
|
||||||
|
show_status
|
||||||
|
cast_sink "$2"
|
||||||
|
;;
|
||||||
|
stop)
|
||||||
|
reset_state
|
||||||
|
;;
|
||||||
|
remove)
|
||||||
|
remove_sink
|
||||||
|
;;
|
||||||
|
help|-h|--help)
|
||||||
|
usage
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
printf 'Unknown command: %s\n\n' "$command" >&2
|
||||||
|
usage >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
zenRadio = pkgs.writeShellApplication {
|
||||||
|
name = "zen-radio";
|
||||||
|
runtimeInputs = [
|
||||||
|
config.programs.zen-browser.package
|
||||||
|
pkgs.coreutils
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
profile_dir="''${XDG_STATE_HOME:-$HOME/.local/state}/zen-radio-profile"
|
||||||
|
mkdir -p "$profile_dir"
|
||||||
|
|
||||||
|
exec zen --new-instance --profile "$profile_dir" "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
imports = [ inputs.zen-browser.homeModules.default ];
|
imports = [ inputs.zen-browser.homeModules.default ];
|
||||||
|
|
@ -175,6 +496,7 @@ in
|
||||||
|
|
||||||
# CLI
|
# CLI
|
||||||
bat
|
bat
|
||||||
|
browserAudio
|
||||||
ffmpeg-full
|
ffmpeg-full
|
||||||
claude-code
|
claude-code
|
||||||
opencode
|
opencode
|
||||||
|
|
@ -213,11 +535,14 @@ in
|
||||||
libreoffice
|
libreoffice
|
||||||
logseq
|
logseq
|
||||||
obs-studio
|
obs-studio
|
||||||
|
pavucontrol
|
||||||
prismlauncher
|
prismlauncher
|
||||||
|
qpwgraph
|
||||||
qbittorrent-enhanced
|
qbittorrent-enhanced
|
||||||
signal-desktop
|
signal-desktop
|
||||||
vesktop
|
vesktop
|
||||||
vlc
|
vlc
|
||||||
|
zenRadio
|
||||||
zulip
|
zulip
|
||||||
linphone
|
linphone
|
||||||
lmstudio
|
lmstudio
|
||||||
|
|
@ -406,13 +731,8 @@ in
|
||||||
h = "hx";
|
h = "hx";
|
||||||
t = "trash";
|
t = "trash";
|
||||||
vanity = "mkp224o-amd64-64-24k -d noisebridgevanitytor noisebridge{2..7}";
|
vanity = "mkp224o-amd64-64-24k -d noisebridgevanitytor noisebridge{2..7}";
|
||||||
dj = "ffmpeg -f pulse -i alsa_output.pci-0000_c1_00.6.analog-stereo.monitor -ac 2 -ar 44100 -acodec libmp3lame -b:a 128k -content_type audio/mpeg -f mp3 'icecast://nbradio:nbradio@beyla:8005/live'";
|
|
||||||
};
|
};
|
||||||
initExtra = ''
|
initExtra = ''
|
||||||
if [ -z "''${ZELLIJ:-}" ]; then
|
|
||||||
zellij -l zoxide-picker
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Automatically list directory contents when changing directories
|
# Automatically list directory contents when changing directories
|
||||||
auto_l_on_cd() {
|
auto_l_on_cd() {
|
||||||
if [ "$__LAST_PWD" != "$PWD" ]; then
|
if [ "$__LAST_PWD" != "$PWD" ]; then
|
||||||
|
|
@ -421,13 +741,7 @@ in
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
auto_rename_zellij_tab() {
|
export PROMPT_COMMAND="auto_l_on_cd; $PROMPT_COMMAND"
|
||||||
if [ -n "''${ZELLIJ:-}" ]; then
|
|
||||||
zellij-sync-tab-name >/dev/null 2>&1 || true
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
export PROMPT_COMMAND="auto_l_on_cd; auto_rename_zellij_tab; $PROMPT_COMMAND"
|
|
||||||
__LAST_PWD="$PWD"
|
__LAST_PWD="$PWD"
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
@ -562,6 +876,20 @@ in
|
||||||
comment = "Fast, featureful, GPU based terminal emulator";
|
comment = "Fast, featureful, GPU based terminal emulator";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
xdg.desktopEntries.zen-radio = {
|
||||||
|
name = "Zen Radio";
|
||||||
|
genericName = "Dedicated Browser Audio";
|
||||||
|
exec = "zen-radio %U";
|
||||||
|
icon = "zen";
|
||||||
|
type = "Application";
|
||||||
|
categories = [
|
||||||
|
"Network"
|
||||||
|
"WebBrowser"
|
||||||
|
"AudioVideo"
|
||||||
|
];
|
||||||
|
comment = "Dedicated Zen instance for isolating music playback";
|
||||||
|
};
|
||||||
|
|
||||||
# Extract archives on double-click
|
# Extract archives on double-click
|
||||||
xdg.desktopEntries.extract-here = {
|
xdg.desktopEntries.extract-here = {
|
||||||
name = "Extract Here";
|
name = "Extract Here";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue