mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2025-12-08 10:48:01 -08:00
146 lines
4.1 KiB
Bash
Executable File
146 lines
4.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
# Toggle desktop audio recording and attach the result to the newest Anki note
|
|
# (as tagged by Yomichan). Run once to start recording, run again to stop.
|
|
# Dependencies: jq, curl, ffmpeg/ffprobe, pulseaudio (parec+pactl), bc, notify-send
|
|
|
|
set -euo pipefail
|
|
|
|
ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
|
|
AUDIO_FIELD_NAME="${AUDIO_FIELD_NAME:-SentenceAudio}"
|
|
FORMAT="${FORMAT:-mp3}" # mp3 or ogg
|
|
CUT_DURATION="${CUT_DURATION:-0.1}"
|
|
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/record-audio"
|
|
RECORD_TIMEOUT="${RECORD_TIMEOUT:-60}"
|
|
ANKI_URL="http://localhost:${ANKI_CONNECT_PORT}"
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || {
|
|
echo "Missing dependency: $1" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
notify() {
|
|
# Best-effort notification; keep script running if notify-send is missing.
|
|
if command -v notify-send >/dev/null 2>&1; then
|
|
notify-send -t 1000 "$@"
|
|
fi
|
|
}
|
|
|
|
get_active_sink() {
|
|
pactl list sinks short 2>/dev/null | awk '$6=="RUNNING"{print $1; exit 0}'
|
|
}
|
|
|
|
get_newest_note_id() {
|
|
local response
|
|
response=$(curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' \
|
|
-d '{"action":"findNotes","version":6,"params":{"query":"is:new"}}')
|
|
jq -r '.result[-1] // empty' <<<"$response"
|
|
}
|
|
|
|
update_anki_note() {
|
|
local note_id="$1" audio_path="$2" filename="$3"
|
|
|
|
local payload
|
|
payload=$(jq -n --argjson noteId "$note_id" --arg field "$AUDIO_FIELD_NAME" \
|
|
--arg path "$audio_path" --arg filename "$filename" '
|
|
{action:"updateNoteFields",version:6,
|
|
params:{note:{id:$noteId,fields:{($field):""},
|
|
audio:[{path:$path,filename:$filename,fields:[$field]}]}}}')
|
|
|
|
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
|
|
}
|
|
|
|
open_note_in_browser() {
|
|
local note_id="$1"
|
|
local payload
|
|
payload=$(jq -n --argjson noteId "$note_id" '
|
|
{action:"guiBrowse",version:6,params:{query:("nid:" + ($noteId|tostring))}}')
|
|
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
|
|
}
|
|
|
|
record_audio() {
|
|
local note_id="$1"
|
|
local sink
|
|
sink=$(get_active_sink) || true
|
|
|
|
if [[ -z "$sink" ]]; then
|
|
notify "Record Error" "No running PulseAudio sink found"
|
|
exit 1
|
|
fi
|
|
|
|
mkdir -p "$CACHE_DIR"
|
|
|
|
local timestamp wav_file out_file
|
|
timestamp=$(date +%s)
|
|
wav_file="$CACHE_DIR/$timestamp.wav"
|
|
out_file="$CACHE_DIR/$timestamp.$FORMAT"
|
|
|
|
notify "Audio recording started"
|
|
|
|
if ! timeout "$RECORD_TIMEOUT" parec -d"$sink" --file-format=wav "$wav_file"; then
|
|
notify "Record Error" "No audio captured (timeout or sink issue)"
|
|
rm -f "$wav_file"
|
|
exit 1
|
|
fi
|
|
|
|
local input_duration output_duration
|
|
input_duration=$(ffprobe -v error -select_streams a:0 \
|
|
-show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$wav_file")
|
|
output_duration=$(echo "$input_duration - $CUT_DURATION" | bc -l)
|
|
|
|
# Guard against negative durations
|
|
if [[ $(echo "$output_duration < 0" | bc -l) -eq 1 ]]; then
|
|
output_duration="0"
|
|
fi
|
|
|
|
case "$FORMAT" in
|
|
ogg)
|
|
ffmpeg -nostdin -y -i "$wav_file" -vn -codec:a libvorbis -b:a 64k \
|
|
-t "$output_duration" "$out_file"
|
|
;;
|
|
mp3)
|
|
ffmpeg -nostdin -y -i "$wav_file" -vn -codec:a libmp3lame -qscale:a 1 \
|
|
-t "$output_duration" "$out_file"
|
|
;;
|
|
*)
|
|
notify "Record Error" "Unknown format: $FORMAT"
|
|
rm -f "$wav_file"
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
rm -f "$wav_file"
|
|
|
|
update_anki_note "$note_id" "$out_file" "$timestamp.$FORMAT"
|
|
open_note_in_browser "$note_id"
|
|
|
|
notify "Audio recording copied"
|
|
rm -f "$out_file"
|
|
}
|
|
|
|
main() {
|
|
for cmd in curl jq ffmpeg ffprobe parec pactl bc; do
|
|
require_cmd "$cmd"
|
|
done
|
|
|
|
if pgrep -x parec >/dev/null 2>&1; then
|
|
pkill -x parec
|
|
notify "Audio recording stopped"
|
|
exit 0
|
|
fi
|
|
|
|
local newest_note
|
|
newest_note=$(get_newest_note_id)
|
|
|
|
if [[ -z "$newest_note" ]]; then
|
|
notify "Anki Connect" "No new notes found or AnkiConnect unavailable"
|
|
exit 1
|
|
fi
|
|
|
|
record_audio "$newest_note"
|
|
}
|
|
|
|
main "$@"
|