#!/bin/sh set -eu VIEW_ARTIST="artist" VIEW_RELEASEGROUP="rg" VIEW_RELEASE="release" VIEW_SEARCH_ARTIST="search-artist" VIEW_SEARCH_ALBUM="search-album" VIEW_LIST_ARTISTS="list-artists" VIEW_LIST_ALBUMS="list-albums" VIEW_SELECT_ARTIST="select-artist" VIEW_PLAYLIST="playlist" VIEW_QUIT="quit" MODE_NORMAL="hidden" MODE_INSERT="show" # Load helper methods . "sh/info.sh" # Load helper methods . "sh/helper.sh" # Load configuration . "sh/config.sh" # Load theme . "sh/theme.sh" # Load keys . "sh/keys.sh" # Load filters . "sh/filter.sh" # Load AWK scripts . "sh/awk.sh" # Load tools . "sh/tools.sh" # Load MusicBrainz and Discogs methods . "sh/api.sh" # Load mpv methods . "sh/mpv.sh" # Load preview methods . "sh/preview.sh" # Load cache functionality . "sh/cache.sh" # Load MusicBrainz wrappers . "sh/mb.sh" # Load local file handling . "sh/local.sh" # Load list-generating methods . "sh/lists.sh" # State management . "sh/state.sh" # FZF handlers . "sh/fzf.sh" case "${1:-}" in "--lines") view=${2:-} mbid=${3:-} case "$view" in "$VIEW_ARTIST") list_releasegroups "$mbid" ;; "$VIEW_RELEASEGROUP") list_releases "$mbid" ;; "$VIEW_RELEASE") list_recordings "$mbid" ;; "$VIEW_LIST_ARTISTS") list_local_artists ;; "$VIEW_LIST_ALBUMS") list_local_releasegroups ;; esac exit 0 ;; "--filter") mode=$2 view=$3 q="$(default_query "$view" "$FZF_KEY")" printf "show-input+change-query(%s )" "$q" [ "$mode" = "$MODE_NORMAL" ] && printf "+hide-input" exit 0 ;; "--jumpto-artist") mode=$2 view=$3 mbid_cur="${4:-}" mbid="${5:-}" case "$view" in "$VIEW_ARTIST" | "$VIEW_SEARCH_ALBUM" | "$VIEW_LIST_ALBUMS") j="$(mb_releasegroup "$mbid" | $JQ -r --compact-output '."artist-credit"')" ;; "$VIEW_RELEASEGROUP") j="$(mb_release "$mbid" | $JQ -r --compact-output '."artist-credit"')" ;; "$VIEW_RELEASE") j="$(mb_release "$mbid_cur" | $JQ -r --compact-output ".media | map(.tracks) | flatten[] | select(.id == \"$mbid\") | .\"artist-credit\"")" ;; "$VIEW_SEARCH_ARTIST" | "$VIEW_LIST_ARTISTS") aid="$mbid" ;; esac if [ "${j:-}" ]; then cnt=$(echo "$j" | $JQ 'length') [ "$cnt" -eq 1 ] && aid="$(echo "$j" | $JQ -r '.[0].artist.id')" fi [ "${aid:-}" ] && $0 --draw "$mode" "$VIEW_ARTIST" "$aid" || printf "print(%s)+print(%s)+accept" "$VIEW_SELECT_ARTIST" "$j" exit 0 ;; "--draw") debug "call to $*" # Generate fzf command to draw screen. # # @argument $2: mode (default `normal`) # @argument $3: view (default list artists) # @argument $4: MusicBrainz ID (optional) # @argument $5: level (optional) # # The argument `level` specifies the view relative to `view`: If `level` is # set to +1, then the specified MusicBrainz ID is an ID of an object one level # deeper than `view`. Alternatively, the argument `level` may be set to `-1`. # Anything else is interpreted as "on the level of `view`". # # The choice of possible arguments ($5) depends on the view. # These views are independent of the MusicBrainz ID ($4) and of the argument # ($5): # - VIEW_SEARCH_ARTIST: Get ready to query MusicBrainz for artists # - VIEW_SEARCH_ALBUM: Get ready to query MusicBrainz for albums # - VIEW_LIST_ARTISTS: List all locally available artists # - VIEW_LIST_ALBUMS: List al locally available albums # # If no argument ($5) is specified, then the remaining views act as follows: # - VIEW_ARTIST: Display all release groups of that artist # - VIEW_RELEASEGROUP: Display all releases within that release group # - VIEW_RELEASE: Display track list of specified release # # Here, if the argument is set to `-1`, then the parent entry is displayed: # - VIEW_ARTIST: Divert view to VIEW_LIST_ARTISTS # - VIEW_RELEASEGROUP: For single-artist release groups, divert to # VIEW_ARTIST of that artist, else display the artist selection. # - VIEW_RELEASE: Divert view to VIEW_LIST_RELEASEGROUP. # # Alternatively, if the argument is set to `+1`, then the child entry is # displayed: # - VIEW_ARTIST: Divert view to VIEW_LIST_ARTISTS # - VIEW_RELEASEGROUP: For single-artist release groups, divert to # VIEW_ARTIST of that artist, else display the artist selection. # - VIEW_RELEASE: Divert view to VIEW_LIST_RELEASEGROUP. # # Hence, the view is only diverted in this last case. mode="${2:-"$MODE_NORMAL"}" view="${3:-"$VIEW_LIST_ARTISTS"}" mbid="${4:-}" level="${5:-}" # Change state, if we are being diverted. case "$level" in "-1") case "$view" in "$VIEW_ARTIST") view="$VIEW_LIST_ARTISTS" mbid="" ;; "$VIEW_RELEASEGROUP") view="$VIEW_ARTIST" mbid="$(mb_releasegroup "$mbid" | $JQ -r --compact-output '."artist-credit"[0].artist.id')" ;; "$VIEW_RELEASE") view="$VIEW_RELEASEGROUP" mbid="$(mb_release "$mbid" | $JQ -r --compact-output '."release-group".id')" ;; esac ;; "+1") case "$view" 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" ;; esac ;; *) ;; esac # Set initial query printf "show-input+change-query(%s )" "$(default_query "$view")" # Store current state printf "+change-border-label(%s)+change-list-label(%s)" "$view" "$mbid" # Set header fzf_command_set_header "$view" "$mbid" # Load lines printf "+reload($0 --lines %s %s)" "$view" "$mbid" [ "$mode" = "$MODE_NORMAL" ] && printf "+hide-input" exit 0 ;; "--fzf-reload") fzf_handle_reload exit 0 ;; "--fzf-load") fzf_handle_load exit 0 ;; "--fzf-info") fzf_info exit 0 ;; "--fzf-change-reload") fzf_reload_after_change exit 0 ;; "--fzf-change") fzf_handle_change exit 0 ;; "--fzf-key") fzf_handle_key "${2:-}" "${3:-}" "${4:-}" exit 0 ;; "--decorate") [ ! "${2:-}" ] && err "You did not specify a directory." && exit 1 [ ! -d "$2" ] && err "Path $2 does not point to a directory." && exit 1 if ! decorate "$2"; then err "Something went wrong. Are you're files tagged correctly?" exit 1 fi exit 0 ;; "--reload") [ ! "${2:-}" ] && err "Path to decorated music is missing." && exit 1 [ ! -d "$2" ] && err "Path does not point to a directory." && exit 1 info "Reloading information of local music directory $2" load_local "$2" || err "Failed to load local data" info "Done" exit 0 ;; "--internal-preview-artist") __preview_artist "$2" exit 0 ;; "--playlist") list_playlist exit 0 ;; "--help") cat <] Search artist on MusicBrainz --search-album [] Search album on MusicBrainz --artist List release groups of given artist --releasegroup List releases in given release group --release Show release given by --ni-search-artist [] Non-interactive search on MusicBrainz --ni-search-album [] Non-interactive search on MusicBrainz MANAGE LOCAL MUSIC: --decorate Decorate directory containing a tagged release --reload Populate database with decorated local music from EOF exit 0 ;; "--refresh-view") precompute_view exit 0 ;; esac # Set window title printf '\033]0;%s\007' "$WINDOW_TITLE" # Generate filenames for temporary files tmpdir=$(mktemp -d) LOCKFILE="$tmpdir/lock" RESULTS="$tmpdir/results" PIDFILE="$tmpdir/pid" trap 'rm -rf "$tmpdir"' EXIT INT export LOCKFILE RESULTS PIDFILE statedir=$(mktemp -d) STATEFILE="$statedir/state" ARGSFILE="$statedir/state-args" STATEFILE_LAST="$statedir/state2" ARGSFILE_LAST="$statedir/state-args2" touch "$STATEFILE" "$ARGSFILE" "$STATEFILE_LAST" "$ARGSFILE_LAST" export STATEFILE ARGSFILE STATEFILE_LAST ARGSFILE_LAST # Views and modes case "${1:-}" in "--artist") [ ! "${2:-}" ] && err "MusicBrainz Artist ID not specified (see --help)" && exit 1 VIEW="$VIEW_ARTIST" MODE="$MODE_NORMAL" MBID="$2" ;; "--releasegroup") [ ! "${2:-}" ] && err "MusicBrainz Release-Group ID not specified (see --help)" && exit 1 VIEW="$VIEW_RELEASEGROUP" MODE="$MODE_NORMAL" MBID="$2" ;; "--release") [ ! "${2:-}" ] && err "MusicBrainz Release ID not specified (see --help)" && exit 1 VIEW="$VIEW_RELEASE" MODE="$MODE_NORMAL" MBID="$2" ;; "--search-artist") state_init "$VIEW_SEARCH_ARTIST" "$MODE_INSERT" "${2:-}" VIEW="$VIEW_SEARCH_ARTIST" MODE="$MODE_INSERT" MBID="" ;; "--search-album") VIEW="$VIEW_SEARCH_ALBUM" MODE="$MODE_INSERT" MBID="" ;; "--artists" | "") VIEW="$VIEW_LIST_ARTISTS" MODE="$MODE_NORMAL" MBID="" ;; "--albums") VIEW="$VIEW_LIST_ALBUMS" MODE="$MODE_NORMAL" MBID="" ;; *) err "Unknown option $1 (see --help)" exit 1 ;; esac # Start mpv mpv_start # main loop # states are stored in (in)visible labels # # mode: [$MODE_NORMAL, $MODE_INSERT] # The mode is reflected on the input visibility. The variable $FZF_INPUT_STATE # is set to "hidden" if and only if the mode is `normal`. To swtich to `normal` # mode, we call `hide-input`. To switch to insert mode, we call `show-input`. # # view: [$VIEW_*] # The view is stored in $FZF_BORDER_LABEL. To set the view, call # `change-border-label($VIEW)`. # # argument: string # The argument is stored in $FZF_LIST_LABEL. To set the argument, call # `change-list-label($arg)`. IN_NORMAL_MODE="[ \"\$FZF_INPUT_STATE\" = \"hidden\" ]" IN_VIEW_PATTERN="[ \"\$FZF_BORDER_LABEL\" = \"%s\" ]" IN_LIST_ARTISTS_VIEW="$(printf "$IN_VIEW_PATTERN" "$VIEW_LIST_ARTISTS")" IN_LIST_ALBUMS_VIEW="$(printf "$IN_VIEW_PATTERN" "$VIEW_LIST_ALBUMS")" IN_SEARCH_ARTIST_VIEW="$(printf "$IN_VIEW_PATTERN" "$VIEW_SEARCH_ARTIST")" IN_SEARCH_ALBUM_VIEW="$(printf "$IN_VIEW_PATTERN" "$VIEW_SEARCH_ALBUM")" FZF_CURRENT_MODE="\$FZF_INPUT_STATE" FZF_CURRENT_VIEW="\$FZF_BORDER_LABEL" PUT_FZF_KEY_LOGIC="case \"\$FZF_KEY\" in \"space\") echo \"put( )\";; \"backspace\"|\"bspace\"|\"bs\") echo \"backward-delete-char\";; \"delete\"|\"del\") echo \"delete-char\";; *) echo \"put(\$FZF_KEY)\";; esac" while true; do case "$VIEW" in "$VIEW_SELECT_ARTIST") sel=$( echo "$ARGS" | list_artists_from_json | $FZF \ --bind="$KEYS_HALFPAGE_DOWN,$KEYS_HALFPAGE_UP,\ $KEYS_BROWSE,\ $KEYS_OPEN,\ $KEYS_FILTER_LOCAL:transform:$0 --fzf-key {2} {3} {4}" \ -0 -1 \ --border="bold" \ --border-label="Select artist" \ --delimiter="\t" \ --prompt="$SEARCH_PROMPT" \ --margin="5%,20%" \ --bind="$KEYS_FILTER_LOCAL:change-query('$QUERY_LOCAL' )" \ --accept-nth="{3}" \ --with-nth="{1}" || true ) [ "$sel" ] || continue MODE="$MODE_NORMAL" VIEW="$VIEW_ARTIST" MBID="$sel" ;; "$VIEW_PLAYLIST") list_playlist | $FZF \ --reverse \ --no-sort \ --border=double \ --border-label=" Playlist " \ --no-input \ --margin="2%,10%" \ --bind="$KEYS_ALL:transform:$0 --fzf-key {2} {3} {4}" \ --bind="r,ctrl-r:reload:$0 --playlist" \ --delimiter="\t" \ --with-nth="{1}" >/dev/null ;; "$VIEW_QUIT") debug "Quitting..." break ;; *) # Main instance # # KEY-BINDINGS: # Key variables contain comma-delimited sequences of keys. Every key # variable starts with `KEYS_`. Key variables with the prefix `KEYS_I_` are # exclusive to the `insert` mode. Key variables with the prefix `KEYS_N_` # are exclusive to the `normal` mode. All other keys are bound to both # modes. It is important that the keys used in `KEYS_N_` variables are # naturally printable or modifications of the input string. See # `$PUT_FZF_KEY_LOGIC` for details. # # Here is a list of all keys grouped by type (see `src/sh/keys.sh`). #--bind="start:change-border-label($VIEW)+change-list-label($MBID)+$MODE-input+transform:$0 --display" \ sel=$( printf "" | $FZF \ --reverse \ --info="inline-right" \ --header-first \ --header-border="bottom" \ --bind="start:transform:$0 --draw $MODE $VIEW $MBID" \ --bind="$KEYS_I_NORMAL:transform:$IN_NORMAL_MODE || echo \"hide-input\"" \ --bind="$KEYS_N_INSERT:transform:$IN_NORMAL_MODE && echo \"show-input\" || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_DOWN:down" \ --bind="$KEYS_UP:up" \ --bind="$KEYS_HALFPAGE_DOWN:half-page-down" \ --bind="$KEYS_HALFPAGE_UP:half-page-up" \ --bind="$KEYS_N_DOWN:transform:$IN_NORMAL_MODE && echo \"down\" || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_N_UP:transform:$IN_NORMAL_MODE && echo \"up\" || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_N_BOT:transform:$IN_NORMAL_MODE && echo \"last\" || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_N_TOP:transform:$IN_NORMAL_MODE && echo \"first\" || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_IN:transform:[ {3} ] && $0 --draw $FZF_CURRENT_MODE $FZF_CURRENT_VIEW {3} \"+1\"" \ --bind="$KEYS_OUT:transform:[ {2} ] && $0 --draw $FZF_CURRENT_MODE $FZF_CURRENT_VIEW {2} \"-1\"" \ --bind="$KEYS_N_IN:transform:$IN_NORMAL_MODE && ([ {3} ] && $0 --draw $FZF_CURRENT_MODE $FZF_CURRENT_VIEW {3} \"+1\") || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_N_OUT:transform:$IN_NORMAL_MODE && ([ {2} ] && $0 --draw $FZF_CURRENT_MODE $FZF_CURRENT_VIEW {2} \"-1\") || $PUT_FZF_KEY_LOGIC" \ --bind="$KEYS_SELECT_ARTIST:transform:$0 --jumpto-artist $FZF_CURRENT_MODE $FZF_CURRENT_VIEW {2} {3}" \ --bind="$KEYS_LIST_ARTISTS:transform:$0 --draw \$FZF_INPUT_STATE $VIEW_LIST_ARTISTS" \ --bind="$KEYS_LIST_ALBUMS:transform:$0 --draw \$FZF_INPUT_STATE $VIEW_LIST_ALBUMS" \ --bind="$KEYS_SEARCH_ARTIST:transform:$0 --draw $MODE_INSERT $VIEW_SEARCH_ARTIST" \ --bind="$KEYS_SEARCH_ALBUM:transform:$0 --draw $MODE_INSERT $VIEW_SEARCH_ALBUM" \ --bind="$KEYS_SWITCH_ARTIST_ALBUM:transform:case \"$FZF_CURRENT_VIEW\" in \"$VIEW_LIST_ARTISTS\") $0 --draw $FZF_CURRENT_MODE $VIEW_LIST_ALBUMS ;; \"$VIEW_LIST_ALBUMS\") $0 --draw $FZF_CURRENT_MODE $VIEW_LIST_ARTISTS ;; \"$VIEW_SEARCH_ARTIST\") $0 --draw $MODE_INSERT $VIEW_SEARCH_ALBUM ;; \"$VIEW_SEARCH_ALBUM\") $0 --draw $MODE_INSERT $VIEW_SEARCH_ARTIST ;; esac" \ --bind="$KEYS_SWITCH_LOCAL_REMOTE:transform:case \"$FZF_CURRENT_VIEW\" in \"$VIEW_LIST_ARTISTS\") $0 --draw $MODE_INSERT $VIEW_SEARCH_ARTIST ;; \"$VIEW_LIST_ALBUMS\") $0 --draw $MODE_INSERT $VIEW_SEARCH_ALBUM ;; \"$VIEW_SEARCH_ARTIST\") $0 --draw $MODE_NORMAL $VIEW_LIST_ARTISTS ;; \"$VIEW_SEARCH_ALBUM\") $0 --draw $MODE_NORMAL $VIEW_LIST_ALBUMS ;; esac" \ --bind="$KEYS_FILTER:transform:$0 --filter $FZF_CURRENT_MODE $FZF_CURRENT_VIEW" \ --bind="$KEYS_BROWSE:execute-silent: [ {3} ] || exit 0 case \"\$FZF_BORDER_LABEL\" in \"$VIEW_LIST_ARTISTS\" | \"$VIEW_SEARCH_ARTIST\") t=\"artist\" ;; \"$VIEW_ARTIST\" | \"$VIEW_SEARCH_ALBUM\" | \"$VIEW_LIST_ALBUMS\") t=\"release-group\" ;; \"$VIEW_RELEASEGROUP\") t=\"release\" ;; \"$VIEW_RELEASE\") t=\"track\" ;; esac open \"https://musicbrainz.org/\$t/{r3}\"" \ --bind="$KEYS_OPEN:execute-silent: [ {4} ] || exit 0 open \"\$(dirname {4})\"" \ --bind="$KEYS_SELECT_ARTIST:" \ --bind="$KEYS_FILTER_LOCAL:" \ --bind="$KEYS_FILTER_1:" \ --bind="$KEYS_FILTER_2:" \ --bind="$KEYS_FILTER_3:" \ --bind="$KEYS_FILTER_4:" \ --bind="$KEYS_SWITCH_ARTIST_ALBUM:" \ --bind="$KEYS_SWITCH_LOCAL_REMOTE:" \ --bind="$KEYS_PLAY:" \ --bind="$KEYS_QUEUE:" \ --bind="$KEYS_SHOW_PLAYLIST:" \ --bind="$KEYS_QUIT:print($VIEW_QUIT)+accept" \ --delimiter="\t" \ --with-nth="{1}" || true ) VIEW="$(echo "$sel" | head -1)" ARGS="$(echo "$sel" | head -2 | tail -1)" debug "FZF terminated: view=$VIEW, ARGS=$ARGS" ;; esac done #[ \"$MODE\" = \"$MODE_NORMAL\" ] && echo \"+hide-input\" || echo \"+show-input\" # --bind="load:transform:$0 --fzf-load" \ # --bind="change:execute-silent($0 --fzf-change &)+reload:$0 --fzf-change-reload" \ # --bind="$KEYS_ALL:transform:$0 --fzf-key {2} {3} {4}" \ # --preview-window="right,25%,border-left,wrap,<30(hidden)" \ # --preview="$0 --internal-preview-artist {3}" \