fetch-lyrics (3966B)
1 #!/usr/bin/env bash 2 # rmpc on_song_change hook: fetch synced lyrics from LRCLIB when missing. 3 # 4 # rmpc provides (among others): $ARTIST $TITLE $ALBUM $DURATION $FILE 5 # $HAS_LRC $LRC_FILE $PID $VERSION 6 # Writes to $LRC_FILE (under lyrics_dir), then hot-indexes it via 7 # `rmpc remote indexlrc`. Never blocks rmpc: all work happens in a 8 # backgrounded subshell; all failures are silent (logged to cache dir). 9 10 CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/rmpc-lyrics" 11 MISS_DIR="$CACHE_DIR/misses" 12 LOG_FILE="$CACHE_DIR/fetch.log" 13 LRCLIB="https://lrclib.net" 14 MISS_TTL_DAYS=14 15 16 main() { 17 mkdir -p "$MISS_DIR" 18 19 # Nothing to do without metadata or if lyrics already resolved. 20 [ "${HAS_LRC:-true}" = "false" ] || exit 0 21 [ -n "${ARTIST:-}" ] && [ -n "${TITLE:-}" ] || exit 0 22 [ -n "${LRC_FILE:-}" ] || exit 0 23 [ -s "$LRC_FILE" ] && exit 0 24 25 # Negative cache: don't hammer LRCLIB for known misses. 26 local key miss 27 key=$(printf '%s\t%s\t%s' "$ARTIST" "$TITLE" "${ALBUM:-}" | sha1sum | cut -d' ' -f1) 28 miss="$MISS_DIR/$key" 29 if [ -e "$miss" ] && [ -z "$(find "$miss" -mtime +"$MISS_TTL_DAYS" 2>/dev/null)" ]; then 30 exit 0 31 fi 32 33 # Serialize concurrent hook invocations (fast track skipping). 34 exec 9>"$CACHE_DIR/lock" 35 flock -n 9 || exit 0 36 37 # DURATION may be seconds ("312.43") or "MM:SS"; normalise to integer seconds. 38 local dur_s="" 39 case "${DURATION:-}" in 40 *:*) dur_s=$(( $(echo "$DURATION" | awk -F: '{print $(NF-1)*60 + int($NF)}') )) ;; 41 *[0-9]*) dur_s=$(printf '%.0f' "$DURATION" 2>/dev/null || true) ;; 42 esac 43 44 # Try /api/get with maximum specificity first, then progressively relax. 45 local resp="" synced="" instrumental="" 46 for attempt in full noalbum bare; do 47 local args=(-sG --max-time 15 -H "Lrclib-Client: rmpc-${VERSION:-unknown}" 48 --data-urlencode "artist_name=$ARTIST" 49 --data-urlencode "track_name=$TITLE") 50 case $attempt in 51 full) 52 [ -n "${ALBUM:-}" ] && args+=(--data-urlencode "album_name=$ALBUM") 53 [ -n "$dur_s" ] && args+=(--data-urlencode "duration=$dur_s") 54 ;; 55 noalbum) 56 [ -n "$dur_s" ] && args+=(--data-urlencode "duration=$dur_s") 57 ;; 58 esac 59 resp=$(curl "${args[@]}" "$LRCLIB/api/get" 2>>"$LOG_FILE") || resp="" 60 synced=$(printf '%s' "$resp" | jq -r '.syncedLyrics // empty' 2>/dev/null) 61 instrumental=$(printf '%s' "$resp" | jq -r '.instrumental // false' 2>/dev/null) 62 [ -n "$synced" ] && break 63 [ "$instrumental" = "true" ] && break 64 sleep 1 # be polite between retries 65 done 66 67 if [ -z "$synced" ]; then 68 # Instrumental or genuinely not found: remember the miss, stay silent. 69 touch "$miss" 70 printf '%s MISS %s - %s\n' "$(date -Iseconds)" "$ARTIST" "$TITLE" >>"$LOG_FILE" 71 exit 0 72 fi 73 74 # Write the .lrc with metadata tags so rmpc's index matches it to the 75 # song regardless of file path. UTF-8 passes through untouched. 76 mkdir -p "$(dirname "$LRC_FILE")" 77 local tmp 78 tmp=$(mktemp "$(dirname "$LRC_FILE")/.fetch.XXXXXX") || exit 0 79 { 80 printf '[ar:%s]\n' "$ARTIST" 81 [ -n "${ALBUM:-}" ] && printf '[al:%s]\n' "$ALBUM" 82 printf '[ti:%s]\n' "$TITLE" 83 printf '%s\n' "$synced" | sed -E '/^\[(ar|al|ti|length):/d' 84 } >"$tmp" 85 mv -f "$tmp" "$LRC_FILE" 86 rm -f "$miss" 87 printf '%s HIT %s - %s -> %s\n' "$(date -Iseconds)" "$ARTIST" "$TITLE" "$LRC_FILE" >>"$LOG_FILE" 88 89 # Tell the running rmpc instance about the new file (hot reload). 90 if [ -n "${PID:-}" ]; then 91 rmpc remote --pid "$PID" indexlrc --path "$LRC_FILE" 2>>"$LOG_FILE" || true 92 rmpc remote --pid "$PID" status "Lyrics downloaded: $ARTIST - $TITLE" --level info 2>>"$LOG_FILE" || true 93 fi 94 } 95 96 # Never block rmpc's hook runner. 97 main >/dev/null 2>&1 & 98 exit 0