Files
fuzique/fuzzique
2025-07-17 11:59:42 +02:00

250 lines
8.2 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
echo "call to --preview" >>/tmp/foo
shift
file="$1"
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
if [ "${1:-}" = "--show-playlist" ]; then
[ ! -S "${MPV_SOCKET:-}" ] && exit 1
count=$(echo "{ \"command\": [\"expand-text\",\"\${playlist/count}\"] }" | socat - "$MPV_SOCKET" | jq -r '.data')
if [ "$count" -eq 0 ]; then
printf "(empty playlist)\t\t%s\n" "$MODE_PLAYLIST"
exit 0
fi
for i in $(seq 0 $((count - 1))); do
file=$(echo "{ \"command\": [\"expand-text\",\"\${playlist/$i/filename}\"] }" | socat - "$MPV_SOCKET" | jq -r '.data')
curr=$(echo "{ \"command\": [\"expand-text\",\"\${playlist/$i/current}\"] }" | socat - "$MPV_SOCKET" | jq -r '.data')
pnt=" "
[ "$curr" = "yes" ] && pnt="$PLAYLIST_POINTER"
artist=$(echo "$file" | rev | cut -d "/" -f 3 | rev)
release=$(echo "$file" | rev | cut -d "/" -f 2 | rev)
title=$(echo "$file" | rev | cut -d "/" -f 1 | rev | sed 's/\..*$//')
artist=$(printf "$AFMT" "$artist")
release=$(printf "$RFMT" "$release")
title=$(printf "$TFMT" "$title")
printf "%s|%s|%s|%s\t%s\t%s\n" "$pnt" "$title" "$release" "$artist" "$file" "$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:-}" = "--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:-}" ] && 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:
echo \"l2:{}\" >> /tmp/foo
[ {3} = \"$MODE_SEARCH\" ] && printf \"+hide-preview\" || printf \"+show-preview\"
[ {3} = \"$MODE_PLAYLIST\" ] && printf \"+hide-input\" || printf \"+show-input\"
echo \"l3:{}\" >> /tmp/foo
printf \"+transform-prompt:$0 --prompt {2} {3}\"
echo \"l4:{}\" >> /tmp/foo
echo \"load ended..\" >> /tmp/foo
" \
--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"