Compare commits

...

7 Commits

Author SHA1 Message Date
ed12cef7e4 typos fixed 2025-10-16 21:01:42 +02:00
06485fbce7 typo fixed and feature update 2025-10-15 20:38:00 +02:00
214a1cda4c bugfix: playlist list 2025-10-15 15:22:40 +02:00
d13436b963 feat: lyrics support readme and examples 2025-10-15 09:12:02 +02:00
4343521a46 bugfix: enter past releases 2025-10-14 22:38:36 +02:00
6db95d3feb feat: lyrics support 2025-10-14 22:33:36 +02:00
a0f8e79665 bugfix: xargs delim 2025-10-13 22:30:04 +02:00
8 changed files with 254 additions and 8 deletions

View File

@@ -12,6 +12,7 @@ Control playback from your terminal while browsing the vast world of music relea
- Handle metadata without touching your audio files
- Fast and hackable application with [vim](https://www.vim.org)-like keybindings
- Save and load playlists
- Lyrics support
🎞️ Screenshot
-------------
@@ -19,7 +20,7 @@ Control playback from your terminal while browsing the vast world of music relea
The MusicBrainz database is automatically searched after entering a query.
In the screenshot below, the emoji 🔆 indicates that some tracks of _Mc Solaar_ are available in the local music library.
They are played by hitting `<enter>`.
![MusicBrainz artist search](//screenshots/search.png)
![MusicBrainz artist search](./screenshots/search.png)
### Browse Local Music Library
Here, we fuzzily search an album of _Daft Punk._
@@ -119,7 +120,7 @@ $ fuzic --decorate ~/Music/Daft\ Punk/Random\ Access\ Memories
Info: Decorating /home/amin/Music/Daft Punk/Random Access Memories as release 5000a285-b67e-4cfc-b54b-2b98f1810d2e
$ fuzic --decorate-as ~/Music/Mc\ Solaar/Prose\ Combat 69e5cf67-7cea-4fe8-9129-9779f0a93d69
Info: Decorating /home/amin/Music/Mc Solaar/Prose Combat as the release Prose Combat by MC Solaar
Info: We discovered the following associatoin:
Info: We discovered the following association:
Track 'Aubade' File './01 Aubade.m4a'
Track 'Obsolète' File './02 Obsolète.m4a'
Track 'Nouveau western' File './03 Nouveau western.m4a'
@@ -176,6 +177,28 @@ You may also define the playlist directory using the `PLAYLIST_DIRECTORY` enviro
In `share/playlists` you find example playlists.
Thus, the command `PLAYLIST_DIRECTORY=share/playlists fuzic` launches this application with access to these example playlists.
📜 Lyrics
---------
By using the key `L` (normal mode), the lyrics of the selected track are displayed.
The lyrics are taken from (in that order)
1. the lyrics stored using `fuzic`, or
2. from the `.lrc` file with the same name as the audio file, or
3. from the audio-file tags (requires `ffprobe`), or
4. from a custom script.
To specify the custom script, you can set the `LYRICS_FETCH_CUSTOM` environment variable to point to an executable.
The executable reads a JSON string on the standard input, and is supposed to print the lyrics (see `share/lyrics/example.sh`).
You may also skip the first three sources and directly make a call to your custom script by using the key `Y` (normal mode).
The key `alt-L` opens the lyrics in a new window using the `EDITOR`.
This requires one of the following terminal emulators (`kitty`, `x-terminal-emulator`, `gnome-terminal`, `xterm`).
If you use another terminal emulator, you may specify the environment variable `EXTERNALEDIT` to hold the string such that
```sh
$ExTERNALEDIT "$file"
```
spawns a text editor with `$file` loaded inside a terminal emulator.
🧭 Planned Features
-------------------
The following features are planned:

25
share/lyrics/example.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/sh
# Example script for `fuzic` to display the lyrics of a track. This script
# reads from stdin a JSON string and stores it in the variable `j`. The
# variable `tj` contains the JSON string of the track.
j="$(cat)"
tj="$(echo "$j" | jq -r '.trackid as $tid | .release.media[].tracks[] | select(.id == $tid)')"
# The following four variables are self-explanatory:
track_name="$(echo "$tj" | jq -r '.title')"
artist_name="$(echo "$tj" | jq -r '."artist-credit" | map([.name, .joinphrase] | join("")) | join("")')"
album_name="$(echo "$j" | jq -r '.release.title')"
duration="$(echo "$tj" | jq -r '.length / 1000 | round')"
# Now, you may call an API to fetch the lyrics for this track,
#
# curl \
# --get \
# --silent \
# --data-urlencode "track_name=$track_name" \
# --data-urlencode "artist_name=$artist_name" \
# --data-urlencode "album_name=$album_name" \
# --data-urlencode "duration=$duration" \
# "$URL"
#
# or simply print a template to write the lyrics yourself:
printf "Lyrics '%s' by '%s' (album: %s, duration: %s seconds)\n" "$track_name" "$artist_name" "$album_name" "$duration"

View File

@@ -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.
@@ -286,6 +291,7 @@ case "${1:-}" in
"$VIEW_SEARCH_ARTIST" | "$VIEW_LIST_ARTISTS") view="$VIEW_ARTIST" ;;
"$VIEW_ARTIST" | "$VIEW_SEARCH_ALBUM" | "$VIEW_LIST_ALBUMS") view="$VIEW_RELEASEGROUP" ;;
"$VIEW_RELEASEGROUP") view="$VIEW_RELEASE" ;;
"$VIEW_RELEASE") exit 0 ;;
esac
;;
*) ;;
@@ -369,6 +375,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
@@ -423,7 +443,7 @@ case "${1:-}" in
;;
"--playlists")
# List available playlists
stored_playlists
stored_playlists | cut -d "$(printf '\t')" -f 1
exit 0
;;
"--print-playlist")
@@ -432,6 +452,11 @@ case "${1:-}" in
cut -d "$(printf '\t')" -f 1
exit 0
;;
"--lyrics")
shift
lyrics "$@"
exit 0
;;
"--help")
# Print help string
cat <<EOF
@@ -449,6 +474,7 @@ GENERAL OPTIONS:
--playlists List stored playlists and exit
--load-playlist <playlist> Load specified playlist
--print-playlist <playlist> Print specified playlist and exit
--lyrics <relid> <mbid> Show lyrics of track <mbid> in release <relid> and exit
MANAGE LOCAL MUSIC:
--decorate <path> Decorate directory containing a tagged release
@@ -571,6 +597,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 +762,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 +860,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="" \

View File

@@ -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" \

View File

@@ -157,7 +157,7 @@ END { for (i in id) print title[i], id[i], fname[i] }
' "$tmpj" "$tmpf" >"$assocfile"
rm -f "$tmpj" "$tmpf"
# Ask user if this is ok
info "We discovered the following associatoin:"
info "We discovered the following association:"
while IFS= read -r line; do
t="$(echo "$line" | cut -d "$(printf '\t')" -f 1)"
f="$(echo "$line" | cut -d "$(printf '\t')" -f 3)"
@@ -191,7 +191,7 @@ END { for (i in id) print title[i], id[i], fname[i] }
# theme-independent fashion. The lists are stored in the files
# `LOCALDATA_ARTISTS_LIST` and `LOCALDATA_RELEASEGROUPS_LIST`.
__precompute_lists() {
cache_get_file_batch "$TYPE_ARTIST" <"$LOCALDATA_ARTISTS" | xargs \
cache_get_file_batch "$TYPE_ARTIST" <"$LOCALDATA_ARTISTS" | xargs -d "\n" \
$JQ '[
.id,
.type,
@@ -201,7 +201,7 @@ __precompute_lists() {
.["life-span"].begin,
.["life-span"].end
] | join("\t")' >"$LOCALDATA_ARTISTS_LIST" &
cache_get_file_batch "$TYPE_RELEASEGROUP" <"$LOCALDATA_RELEASEGROUPS" | xargs \
cache_get_file_batch "$TYPE_RELEASEGROUP" <"$LOCALDATA_RELEASEGROUPS" | xargs -d "\n" \
$JQ '[
.id,
."primary-type",
@@ -244,7 +244,7 @@ reloaddb() {
tmpreleasefiles=$(mktemp)
cache_get_file_batch "$TYPE_RELEASE" <"$tmpreleases" >"$tmpreleasefiles"
(
xargs \
xargs -d "\n" \
$JQ '."release-group".id' \
<"$tmpreleasefiles" >"$LOCALDATA_RELEASEGROUPS"
tf1=$(mktemp)
@@ -252,7 +252,7 @@ reloaddb() {
mv "$tf1" "$LOCALDATA_RELEASEGROUPS"
) &
(
xargs \
xargs -d "\n" \
$JQ '."release-group"."artist-credit" | map(.artist.id) | join("\n")' \
<"$tmpreleasefiles" >"$LOCALDATA_ARTISTS"
tf2=$(mktemp)

121
src/sh/lyrics.sh Normal file
View File

@@ -0,0 +1,121 @@
# 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 release and prints the
# lyrics of the track.
LYRICS_FETCH_CUSTOM="${LYRICS_FETCH_CUSTOM:-""}"
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
#
# The custom script is executed only if the environment variable is set. Else
# the message stored in `$LYRICS_NO_LYRICS` is saved.
store_lyrics_custom() {
rlid="${1:-}"
mbid="${2:-}"
if [ "$LYRICS_FETCH_CUSTOM" ]; then
mb_release "$rlid" |
$JQ --arg mbid "$mbid" '{release: ., trackid: $mbid}' |
sh -c "$LYRICS_FETCH_CUSTOM" |
store_lyrics "$mbid"
else
echo "$LYRICS_NO_LYRICS" |
store_lyrics "$mbid"
fi
}
# 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"
}

View File

@@ -399,5 +399,10 @@ if [ ! "${THEME_LOADED:-}" ]; then
PLYSTORE_PLAYLIST="${PLYSTORE_PLAYLIST:-"🎼 ${CPURPLE}%f${OFF}"}"
export TITLE_PLYLST TITLE_PLYLST_STORE PLYSTORE_PLAYLIST
# Lyrics
# ======
LYRICS_NO_LYRICS="${LYRICS_NO_LYRICS:-"(no lyrics)"}"
export LYRICS_NO_LYRICS
export THEME_LOADED=1
fi

View File

@@ -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