Files
fuzique/fuzique
2025-07-17 14:44:18 +02:00

283 lines
8.9 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
set -eu
APP_NAME="fuzique"
if [ "${1:-}" = "--prompt" ]; then
shift
file="$1"
shift
mode="$1"
case "$mode" in
"$MODE_SEARCH")
printf "search> "
;;
"$MODE_BROWSE")
rel=${file#"$ROOT/"}
artist=$(echo "$rel" | cut -d "/" -f 1)
release=$(echo "$rel" | cut -d "/" -f 2)
d=$(echo "$rel" | awk -F'/' '{ print NF }')
[ "$d" -eq 2 ] && printf " $AFMT" "$artist"
[ "$d" -eq 3 ] && printf " $AFMT$RFMT" "$artist" "$release"
;;
"$MODE_PLAYLIST")
printf ""
;;
esac
exit 0
fi
if [ "${1:-}" = "--preview" ]; then
shift
file="$1"
[ ! -f "$file" ] && exit 0
rel=${file#"$ROOT/"}
artist=$(echo "$rel" | cut -d "/" -f 1)
release=$(echo "$rel" | cut -d "/" -f 2)
track=$(echo "$rel" | cut -d "/" -f 3)
if [ "$track" ]; then
echo "Tracka info..."
echo "$track"
elif [ "$release" ]; then
echo "Release info..."
echo "$release"
elif [ "$artist" ]; then
echo "Artist info..."
fi
tags=$(ffprobe -v quiet -show_entries format -of json "$file" | jq)
{
echo "## Lyrics"
echo ""
printf "%s" "$tags" | jq -C -r '.format.tags.LYRICS'
} | $CAT
exit 0
fi
mpv_text() {
echo "{ \"command\": [\"expand-text\",\"$1\"] }" | socat - "$MPV_SOCKET" | jq -r '.data'
}
tags_from_file() {
ffprobe \
-v quiet \
-show_entries format \
-of json \
"$1" |
jq -r '.format.tags | [ [.artist, .ARTIST], [.album, .ALBUM], [.title, .TITLE] ] | map(map(select(. != null))) | map(.[0]) | join("|")'
}
if [ "${1:-}" = "--show-playlist" ]; then
count=$(mpv_text '${playlist/count}')
if [ "$count" -eq 0 ]; then
printf "(empty playlist)\t\t%s\n" "$MODE_PLAYLIST"
exit 0
fi
pos=$(mpv_text '${playlist-pos}')
cmd=""
for i in $(seq 0 $((count - 1))); do
cmd="$cmd\${playlist/$i/filename}|"
done
fn=$(mpv_text "$cmd")
for i in $(seq 0 $((count - 1))); do
f=$(echo "$fn" | cut -d "|" -f "$((i + 1))")
[ ! "$f" ] && continue
if [ -f "$f" ]; then
tags=$(tags_from_file "$f")
artist=$(echo "$tags" | cut -d "|" -f 1)
release=$(echo "$tags" | cut -d "|" -f 2)
track=$(echo "$tags" | cut -d "|" -f 3)
fi
rel=${f#"$ROOT/"}
[ "${artist:-}" ] || artist=$(echo "$rel" | cut -d "/" -f 1)
[ "${release:-}" ] || release=$(echo "$rel" | cut -d "/" -f 2)
[ "${track:-}" ] || track=$(echo "$rel" | cut -d "/" -f 3 | sed 's/\..*$//')
artist=$(printf "$AFMT" "$artist")
release=$(printf "$RFMT" "$release")
track=$(printf "$TFMT" "$track")
[ "$i" -eq "$pos" ] && pnt="$PLAYLIST_POINTER" || pnt=" "
printf "%s|%s|%s|%s\t%s\t%s\n" "$pnt" "$track" "$release" "$artist" "$f" "$MODE_PLAYLIST"
done |
grep '.' |
column -t -s '|'
exit 0
fi
# Tools
command -v "fdfind" >/dev/null && FD="fdfind" || (err "Did not find \`fdfind\`." && exit 1)
command -v "fzf" >/dev/null && FZF="fzf" || (err "Did not find \`fzf\`." && exit 1)
command -v "mpv" >/dev/null && MPV="mpv" || (err "Did not find \`mpv\`." && exit 1)
if command -v "bat" >/dev/null; then
CAT="bat"
elif command -v "batcat" >/dev/null; then
CAT="batcat"
fi
CAT=${CAT:+$CAT --color=always --style=plain --language=md}
CAT=${CAT:-cat}
export CAT
if [ ! "${1:-}" ] || [ "${1:-}" = "--help" ]; then
$CAT <<EOF
Usage: \`$0 [ --help | [ MUSIC_DIRECTORY [ --reset-cache ] ] ]\`
The \`MUSIC_DIRECTORY\` is the path to a folder with the following tree
structure: \`./<artist>/<album>/<track>\`. For instance:
\`\`\`sh
$ tree /media/Musik
/media/Musik/Mos Def
└── Black on Both Sides
   ├── 01 Fear Not of Man.m4a
   ├── 02 Hip Hop.m4a
   ├── 03 Love.m4a
   ├── 04 Ms. Fat Booty.m4a
   ├── 05 Speed Law.m4a
   ├── 06 Do It Now.m4a
   ├── 07 Got.m4a
   ├── 08 Umi Says.m4a
   ├── 09 New World Water.m4a
   ├── 10 Rock n Roll.m4a
   ├── 11 Know That.m4a
   ├── 12 Climb.m4a
   ├── 13 Brooklyn.m4a
   ├── 14 Habitat.m4a
   ├── 15 Mr. Nigga.m4a
   ├── 16 Mathematics.m4a
   ├── 17 MayDecember.m4a
   └── cover.jpg
\`\`\`
[OPTIONS]
\`--help\`: Show this help and exit
\`--reset-cache\`: Reset cache for specified \`MUSIC_DIRECTORY\`
EOF
exit 0
fi
# Load configuration
[ "${1:?"You did not specify the music directory. Run $0 --help."}" ] && ROOT="$(realpath "$1")" && shift
ROOT=${ROOT%%/}
[ ! -d "${ROOT:-}" ] && echo "Faild to recognize music directory. Run $0 --help." && exit 1
DEPTH=$(echo "$ROOT" | awk -F'/' '{ print NF }')
# Cache support
ROOTHASH=$(echo "$ROOT" | sha1sum | cut -d ' ' -f 1)
CACHE_DIR="$HOME/.cache/$APP_NAME"
CACHE_FILE="$CACHE_DIR/$ROOTHASH"
# Theme
ARTIST_COLOR=${ARTIST_COLOR:-'\033[38;5;202m'}
RELEASE_COLOR=${RELEASE_COLOR:-'\033[38;5;208m'}
TRACK_COLOR=${TRACK_COLOR:-'\033[38;5;215m'}
OFF="\033[m"
AFMT="${AFTM:-"🎤 ${ARTIST_COLOR}%s${OFF}"}"
RFMT="${RFMT:-"💽 ${RELEASE_COLOR}%s${OFF}"}"
TFMT="${TFMT:-"🎵 ${TRACK_COLOR}%s${OFF}"}"
PLAYLIST_POINTER="${PLAYLIST_POINTER:-}"
#PLAYLIST_POINTER="${PLAYLIST_POINTER:-👉}"
# Modes
MODE_BROWSE="B"
MODE_SEARCH="S"
MODE_PLAYLIST="P"
# Make socket
MPV_SOCKET="$(mktemp --suffix=.sock)"
# Export
export ROOT MPV_SOCKET AFMT RFMT TFMT PLAYLIST_POINTER MODE_SEARCH MODE_BROWSE MODE_PLAYLIST
# Load
if [ "${1:-}" = "--reset-cache" ] || [ ! -f "$CACHE_FILE" ]; then
rm -f "$CACHE_FILE"
[ ! -d "$CACHE_DIR" ] && mkdir -p "$CACHE_DIR"
{
$FD --max-depth 2 --type d . "$ROOT"
$FD --exact-depth 3 --type f -i -e "mp3" -e "mp4" -e "m4a" -e "ogg" -e "flac" -e "wav" . "$ROOT"
} |
sort >"$CACHE_FILE"
fi
# Parse
artists_file_browse=$(mktemp)
artists_file_search=$(mktemp)
release_file_browse=$(mktemp)
release_file_search=$(mktemp)
tracks_file_browse=$(mktemp)
tracks_file_search=$(mktemp)
awk \
-F'/' \
-v afmt="$AFMT" \
-v rfmt="$RFMT" \
-v tfmt="$TFMT" \
-v depth="$DEPTH" \
-v artists_file_browse="$artists_file_browse" \
-v release_file_browse="$release_file_browse" \
-v tracks_file_browse="$tracks_file_browse" \
-v artists_file_search="$artists_file_search" \
-v release_file_search="$release_file_search" \
-v tracks_file_search="$tracks_file_search" \
-v mode_browse="$MODE_BROWSE" \
-v mode_search="$MODE_SEARCH" \
'BEGIN {
OFS="\t"
iartist = depth + 1
irelease = depth + 2
itrack = depth + 3
}
NF >= depth + 1 { ar = sprintf(afmt, $iartist) }
NF >= depth + 2 { rl = sprintf(rfmt, $irelease) }
NF >= depth + 3 { tr = $itrack; gsub(/\..*$/, "", tr); tr = sprintf(tfmt, tr) }
NF == depth + 1 { print ar, $0, mode_browse >> artists_file_browse }
NF == depth + 2 { print rl, $0, mode_browse >> release_file_browse }
NF == depth + 3 { print tr, $0, mode_browse >> tracks_file_browse }
NF == depth + 1 { print ar, $0, mode_search >> artists_file_search }
NF == depth + 2 { print rl "|" ar, $0, mode_search >> release_file_search }
NF == depth + 3 { print tr "|" rl "|" ar, $0, mode_search >> tracks_file_search }
' <"$CACHE_FILE"
$MPV --no-config --no-terminal --input-ipc-server="$MPV_SOCKET" --idle &
$FZF \
--reverse \
--ansi \
--no-sort \
--delimiter="\t" \
--with-nth="{1}" \
--cycle \
--bind="ctrl-d:half-page-down,ctrl-u:half-page-up" \
--bind="enter:execute:printf '{ \"command\": [\"loadfile\", \"%s\"] }\n' {2} | socat - \"$MPV_SOCKET\"" \
--bind="alt-enter:execute:printf '{ \"command\": [\"loadfile\", \"%s\", \"append-play\"] }\n' {2} | socat - \"$MPV_SOCKET\"" \
--preview="$0 --preview {2}" \
--bind="alt-1:reload:column -t -s '|' -E 0 \"$artists_file_search\"" \
--bind="alt-2:reload:column -t -s '|' -E 0 \"$release_file_search\"" \
--bind="alt-3:reload:column -t -s '|' -E 0 \"$tracks_file_search\"" \
--bind="ctrl-p:reload:$0 --show-playlist" \
--bind="load:transform:
[ {3} = \"$MODE_SEARCH\" ] && printf \"+hide-preview\" || printf \"+show-preview\"
[ {3} = \"$MODE_PLAYLIST\" ] && printf \"+hide-input\" || printf \"+show-input\"
printf \"+transform-prompt:$0 --prompt {2} {3}\"
" \
--bind="ctrl-l:transform:
printf \"clear-query+pos(1)\"
d=\$(echo {2} | awk -F'/' '{ print NF }')
[ \"\$d\" -eq \"$((DEPTH + 1))\" ] && echo \"+reload:grep -F {2}/ \\\"$release_file_browse\\\" | column -t -s '|' || true\"
[ \"\$d\" -eq \"$((DEPTH + 2))\" ] && echo \"+reload:grep -F {2}/ \\\"$tracks_file_browse\\\" | column -t -s '|' || true\"
" \
--bind="ctrl-h:transform:
printf \"clear-query+pos(1)\"
d=\$(echo {2} | awk -F'/' '{ print NF }')
p=\$(echo {2} | rev | cut -d '/' -f 3- | rev)
[ \"\$d\" -eq \"$((DEPTH + 3))\" ] && echo \"+reload:grep -F \\\"\$p/\\\" \\\"$release_file_browse\\\" | column -t -s '|' || true\"
[ \"\$d\" -eq \"$((DEPTH + 2))\" ] && echo \"+reload:grep -F \\\"\$p/\\\" \\\"$artists_file_browse\\\" | column -t -s '|' || true\"
" \
<"$artists_file_search" || true
printf '{ "command": ["quit"] }\n' | socat - "$MPV_SOCKET"
rm -f "$MPV_SOCKET" \
"$artists_file_browse" \
"$artists_file_search" \
"$release_file_browse" \
"$release_file_search" \
"$tracks_file_browse" \
"$tracks_file_search"