From 6db95d3feb1c51ffcb5d97fd0894ea1631c2def3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84min=20Baumeler?= Date: Tue, 14 Oct 2025 22:33:36 +0200 Subject: [PATCH] feat: lyrics support --- src/main.sh | 32 +++++++++++++ src/sh/keys.sh | 19 ++++++++ src/sh/lyrics.sh | 114 +++++++++++++++++++++++++++++++++++++++++++++++ src/sh/tools.sh | 20 +++++++++ 4 files changed, 185 insertions(+) create mode 100644 src/sh/lyrics.sh diff --git a/src/main.sh b/src/main.sh index 16b4346..38e3cc0 100755 --- a/src/main.sh +++ b/src/main.sh @@ -103,6 +103,9 @@ MODE_INSERT="show" # Load filters . "sh/filter.sh" +# Load lyrics support +. "sh/lyrics.sh" + # Command-line options that may only be used internally. # --lines # --playback @@ -115,6 +118,8 @@ MODE_INSERT="show" # --preview # --show-keybindings # --remove-from-cache +# --edit-lyrics +# --lyrics-custom case "${1:-}" in "--lines") # Print lines that are fed into fzf. @@ -369,6 +374,20 @@ case "${1:-}" in esac exit 0 ;; +"--edit-lyrics") + # Edit lyrics file in an external window + info "Call to $*" + mbid="${2:-}" + file="$(lyrics_file "$mbid")" + [ -f "$file" ] || echo "No lyrics" | store_lyrics "$mbid" + [ "$EXTERNALEDIT" ] && $EXTERNALEDIT "$file" || err "Failed to externally edit the file $file" + exit 0 + ;; +"--lyrics-custom") + # Use custom command to (re-)fetch the lyrics + store_lyrics_custom "$2" "$3" + exit 0 + ;; esac # Non-interactive user commands intended to the user. These commands do not @@ -432,6 +451,11 @@ case "${1:-}" in cut -d "$(printf '\t')" -f 1 exit 0 ;; +"--lyrics") + shift + lyrics "$@" + exit 0 + ;; "--help") # Print help string cat < Load specified playlist --print-playlist Print specified playlist and exit + --lyrics Show lyrics specified track and exit MANAGE LOCAL MUSIC: --decorate Decorate directory containing a tagged release @@ -571,6 +596,7 @@ fi IN_NORMAL_MODE="[ \$FZF_INPUT_STATE = hidden ]" IN_VIEW_PATTERN="[ \$FZF_LIST_LABEL = %s ]" IN_LIST_ARTISTS_VIEW="$(printf "$IN_VIEW_PATTERN" "$VIEW_LIST_ARTISTS")" +IN_RELEASE_VIEW="$(printf "$IN_VIEW_PATTERN" "$VIEW_RELEASE")" FZF_CURRENT_MODE="\$FZF_INPUT_STATE" FZF_CURRENT_VIEW="\$FZF_LIST_LABEL" FZF_CURRENT_MBID="\$FZF_BORDER_LABEL" @@ -735,6 +761,9 @@ while true; do --bind="$KEYS_PLAYLIST_GOTO_RELEASE:print($VIEW_RELEASE)+accept" \ --bind="$KEYS_PLAYLIST_STORE:print($VIEW_PLAYLIST_STORE)+print("")+print($LASTVIEW)+print($LASTARG)+accept" \ --bind="$KEYS_PLAYLIST_OPEN_STORE:print($VIEW_PLAYLIST_PLAYLISTSTORE)+print("")+print($LASTVIEW)+print($LASTARG)+accept" \ + --bind="$KEYS_N_LYRICS:show-preview+preview:$0 --lyrics {3} {4}" \ + --bind="$KEYS_LYRICS_EDIT:execute-silent:$0 --edit-lyrics {4}" \ + --bind="$KEYS_N_LYRICS_FETCH_CUSTOM:execute-silent($0 --lyrics-custom {3} {4})+show-preview+preview:$0 --lyrics {3} {4}" \ --preview-window="hidden" \ --wrap-sign="" \ --delimiter="\t" \ @@ -830,6 +859,9 @@ open \"\$(dirname {5})\"" \ --bind="$KEYS_PREVIEW_CLOSE:hide-preview" \ --bind="$KEYS_PLAYBACK:transform:$0 --playback $FZF_CURRENT_VIEW \"$FZF_CURRENT_MBID\" {4} {5}" \ --bind="$KEYS_N_PLAYBACK:transform:$IN_NORMAL_MODE && $0 --playback $FZF_CURRENT_VIEW \"$FZF_CURRENT_MBID\" {4} {5} || $PUT_FZF_KEY_LOGIC" \ + --bind="$KEYS_N_LYRICS:transform:$IN_NORMAL_MODE && $IN_RELEASE_VIEW && printf \"show-preview+preview:%s --lyrics %s %s\" \"$0\" {3} {4}" \ + --bind="$KEYS_LYRICS_EDIT:transform:$IN_RELEASE_VIEW && printf \"execute-silent:%s --edit-lyrics %s\" \"$0\" {4}" \ + --bind="$KEYS_N_LYRICS_FETCH_CUSTOM:transform:$IN_NORMAL_MODE && $IN_RELEASE_VIEW && {$0 --lyrics-custom {3} {4};printf \"show-preview+preview:%s --lyrics %s %s\" \"$0\" {3} {4}}" \ --bind="change:execute-silent($0 --mbsearch $FZF_CURRENT_VIEW &)+reload:$0 --lines $FZF_CURRENT_VIEW" \ --preview-window="$FZF_DEFAULT_PREVIEW_WINDOW" \ --wrap-sign="" \ diff --git a/src/sh/keys.sh b/src/sh/keys.sh index 872a115..ee95dda 100644 --- a/src/sh/keys.sh +++ b/src/sh/keys.sh @@ -63,6 +63,11 @@ # - KEYS_PREVIEW_TOGGLE_SIZE: Toggle size (small, large) of preview window # - KEYS_REFRESH: Refresh current entry # +# Lyrics: +# - KEYS_N_LYRICS: Show lyrics of selected track +# - KEYS_LYRICS_EDIT: Edit lyrics of selected track +# - KEYS_N_LYRICS_FETCH_CUSTOM: Fetch lyrics using custom command +# # Playback: # - KEYS_PLAY: Play selected release or selected track # - KEYS_QUEUE: Queue selected release or selected track @@ -165,6 +170,12 @@ if [ ! "${KEYS_LOADED:-}" ]; then KEYS_PREVIEW_OPEN KEYS_PREVIEW_TOGGLE_WRAP KEYS_PREVIEW_TOGGLE_SIZE \ KEYS_REFRESH + # Lyrics: + KEYS_N_LYRICS="${KEYS_N_LYRICS:-"L"}" + KEYS_LYRICS_EDIT="${KEYS_LYRICS_EDIT:-"alt-L"}" + KEYS_N_LYRICS_FETCH_CUSTOM="${KEYS_N_LYRICS_FETCH_CUSTOM:-"Y"}" + export KEYS_N_LYRICS KEYS_LYRICS_EDIT KEYS_N_LYRICS_FETCH_CUSTOM + # Playback: KEYS_PLAY="${KEYS_PLAY:-"enter"}" KEYS_QUEUE="${KEYS_QUEUE:-"ctrl-alt-m"}" # That's actually alt-enter @@ -317,6 +328,10 @@ print_keybindings() { "$KEYS_PLAY_PREV,$KEYS_N_PLAY_PREV" "Play previous track" \ "$KEYS_SEEK_FORWARD,$KEYS_N_SEEK_FORWARD" "Seek forward" \ "$KEYS_SEEK_BACKWARD,$KEYS_N_SEEK_BACKWARD" "Seek backward" + __keybindinggroup_from_args "Lyrics" \ + "$KEYS_N_LYRICS" "Show lyrics" \ + "$KEYS_LYRICS_EDIT" "Edit lyrics" \ + "$KEYS_N_LYRICS_FETCH_CUSTOM" "Fetch lyrics using custom command" __keybindinggroup_from_args "Special operations" \ "$KEYS_BROWSE" "Open selected item in browser" \ "$KEYS_OPEN" "Open selected item in file manager" \ @@ -405,6 +420,10 @@ print_keybindings() { "$KEYS_N_PLAY_PREV" "Play previous track" \ "$KEYS_N_SEEK_FORWARD" "Seek forward" \ "$KEYS_N_SEEK_BACKWARD" "Seek backward" + __keybindinggroup_from_args "Lyrics" \ + "$KEYS_N_LYRICS" "Show lyrics (normal mode)" \ + "$KEYS_LYRICS_EDIT" "Edit lyrics" \ + "$KEYS_N_LYRICS_FETCH_CUSTOM" "Fetch lyrics using custom command (normal)" __keybindinggroup_from_args "Special operations" \ "$KEYS_SHOW_PLAYLIST" "Show playlist" \ "$KEYS_BROWSE" "Open selected item in browser" \ diff --git a/src/sh/lyrics.sh b/src/sh/lyrics.sh new file mode 100644 index 0000000..ec74154 --- /dev/null +++ b/src/sh/lyrics.sh @@ -0,0 +1,114 @@ +# Methods and constants for lyrics handling +# +# Lyrics are retrieved as following: +# 1. Check if the lyrics are already stored in this store +# 2. If the track is playable, check if an accompanying `.lrc` file is present. +# 3. If the track is playable, read lyrics from the tags +# 4. Call custom fetch command +# +# The path to the lyrics is `__radix(mbid)/mbid.lrc` where `mbid` is the +# MusicBrainz ID of the track. + +if [ ! "${LYRICS_LOADED:-}" ]; then + # Folder to store lyrics + LYRICS_DIRECTORY="${LYRICS_DIRECTORY:-"$LOCALDATADIR/lyrics"}" + [ -d "$LYRICS_DIRECTORY" ] || mkdir -p "$LYRICS_DIRECTORY" + export LYRICS_DIRECTORY + + # Custom command to fetch lyrics + # + # This command reads from stdin the json object of the track and prints the + # lyrics. + LYRICS_FETCH_CUSTOM="${LYRICS_FETCH_CUSTOM:-"$JQ '.trackid as \$tid | .release.media[].tracks[] | select(.id == \$tid) | .title'"}" + #LYRICS_FETCH_CUSTOM="${LYRICS_FETCH_CUSTOM:-"$JQ '.trackid as \$trid | \"Lyrics for \" + .media[].tracks[] | select(.id == \$trid) | .title'"}" + export LYRICS_FETCH_CUSTOM + + export LYRICS_LOADED=1 +fi + +# File path for lyrics file +# +# @argument $1: MusicBrainz track ID +lyrics_file() { + mbid="${1:-}" + echo "$LYRICS_DIRECTORY/$(__radix "$mbid").lrc" +} + +# Store lyrics +# +# @argument $1: MusicBrainz track ID +# +# This methods reads from stdin and stores it. +store_lyrics() { + mbid="${1:-}" + file="$(lyrics_file "$mbid")" + dir="$(dirname "$file")" + [ -d "$dir" ] || mkdir -p "$dir" + cat >"$file" +} + +# Fetch lyrics using custom command and store them +# +# @argument $1: MusicBrainz release ID +# @argument $2: MusicBrainz track ID +store_lyrics_custom() { + rlid="${1:-}" + mbid="${2:-}" + mb_release "$rlid" | + $JQ --arg mbid "$mbid" '{release: ., trackid: $mbid}' | + sh -c "$LYRICS_FETCH_CUSTOM" | + store_lyrics "$mbid" +} + +# Print lyrics +# +# @argument $1: MusicBrainz release ID +# @argument $2: MusicBrainz track ID +lyrics() { + rlid="${1:-}" + mbid="${2:-}" + # 1. Check if lyrics has already been stored + file="$(lyrics_file "$mbid")" + if [ -f "$file" ]; then + cat "$file" + return + fi + # 2. & 3.: For playable tracks only + decoration="$(grep "^$rlid" "$LOCALDATA_RELEASES" | cut -d "$(printf '\t')" -f 2)" + if [ "$decoration" ] && [ -f "$decoration" ]; then + afname="$($JQ --arg mbid "$mbid" '.tracks | to_entries[] | select(.key == $mbid) | .value' "$decoration")" + af="$(dirname "$decoration")/$afname" + # Check if `.lrc` file exists + lf="$(echo "$af" | rev | cut -d "." -f 2- | rev).lrc" + if [ -f "$lf" ]; then + store_lyrics "$mbid" <"$lf" + cat "$file" + return + fi + # Read lyrics from tag + if [ "$FFPROBE" ]; then + lyrics="$($FFPROBE -v error -show_entries format_tags -print_format json "$af" | + $JQ '.format.tags | ."USLT:description" // ."LYRICS" // ."Lyrics" // ."©lyr" // ."WM/Lyrics" // ""')" + if [ "$lyrics" ]; then + echo "$lyrics" | store_lyrics "$mbid" + cat "$file" + return + fi + fi + fi + # Make call to external command + store_lyrics_custom "$rlid" "$mbid" + cat "$file" +} + +# Reload lyrics file +# +# @argument $1: MusicBrainz release ID +# @argument $2: MusicBrainz track ID +reload_lyrics() { + rlid="${1:-}" + mbid="${2:-}" + file="$(lyrics_file "$mbid")" + rm -f "$file" + lyrics "$rlid" "$mbid" +} diff --git a/src/sh/tools.sh b/src/sh/tools.sh index 9cca575..93c05e7 100644 --- a/src/sh/tools.sh +++ b/src/sh/tools.sh @@ -55,5 +55,25 @@ if [ ! "${TOOLS_LOADED:-}" ]; then command -v "xsel" >/dev/null && CLIP="xsel" || CLIP="true" export CLIP + # Detect external editor + editor="${EDITOR:-vi}" + if command -v "$editor" >/dev/null; then + if command -v "kitty" >/dev/null; then + extedit=$(printf "kitty %s" "$editor") + elif command -v "x-terminal-emulator" >/dev/null; then + extedit=$(printf "x-terminal-emulator -e %s" "$editor") + elif command -v "gnome-terminal" >/dev/null; then + extedit=$(printf "gnome-terminal -- %s" "$editor") + elif command -v "xterm" >/dev/null; then + extedit=$(printf "xterm -e %s" "$editor") + else + extedit="" + fi + else + extedit="" + fi + EXTERNALEDIT="${EXTERNALEDIT:-"$extedit"}" + export EXTERNALEDIT + export TOOLS_LOADED=1 fi