You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

fzmp 7.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  1. #!/usr/bin/env bash
  2. declare -r esc=$'\033'
  3. declare -r c_reset="${esc}[0m"
  4. declare -r c_red="${esc}[31m"
  5. declare -r config_file="${XDG_CONFIG_DIR:-$HOME/.config}/fzmp/conf"
  6. declare verbose
  7. declare default_filter='filter_all_songs'
  8. declare track_format='[[[%artist% / ][[(%date%) ]%album% / ][[%track% - ][%title%]]]|%file%]'
  9. declare -a config_err
  10. usage() {
  11. LESS=-FEXR less <<'HELP'
  12. fzmp [OPTIONS]
  13. OPTIONS:
  14. -A --all search all songs in the library (or F1 when running)
  15. -a --artist search artist then filter by album (or F2 when running)
  16. -p --playlist search the current playlist (or F3 when running)
  17. playlist view has the following keybinds:
  18. > go to the next song in the playlist
  19. < go to the previous song in the playlist
  20. C-d delete the selected songs from the playlist
  21. -v --verbose print errors when parsing the config file
  22. -h --help print this help
  23. CONFIGURATION:
  24. A configuration file can be defined at $XDG_CONFIG_DIR (defaults to ~/.config)
  25. If a line begins with '#' it is treated as a comment and ignored
  26. The configuration file reads the following options:
  27. default_view must be 'artists' 'songs' or 'playlist'
  28. full_song_format a format string to be passed directly to `mpc format -f` in 'playlist' and 'all' views
  29. defaults to '[[[%artist% / ][[(%date%) ]%album% / ][[%track% - ][%title%]]]|%file%]'
  30. for colorized output try: '[[[\e\[32m%artist%\e\[0m / ][\e\[31m[(%date%) ]%album%\e\[0m / ][\e\[34m[%track% - ][%title%]\e\[0m]]|%file%]'
  31. fzf_options command line options to be passed directly to fzf
  32. changing this will override the default options: '+s -e -i --reverse --cycle'
  33. to use the jump feature of fzf you can try '+s -e -i --reverse --cycle --bind=`:jump'
  34. this can also be overridden with the environment variable FZMP_FZF_OPTIONS
  35. HELP
  36. }
  37. is_running() {
  38. pgrep "$1" &> /dev/null
  39. }
  40. err() {
  41. printf "${c_red}%s${c_reset}\n" "$*" >&2
  42. }
  43. die() {
  44. [[ -n "$1" ]] && err "$*"
  45. exit 1
  46. }
  47. has() {
  48. local verbose=0
  49. if [[ $1 == '-v' ]]; then
  50. verbose=1
  51. shift
  52. fi
  53. for c; do c="${c%% *}"
  54. if ! command -v "$c" &> /dev/null; then
  55. (( verbose > 0 )) && err "$c not found"
  56. return 1
  57. fi
  58. done
  59. }
  60. fzf() {
  61. command fzf ${FZMP_FZF_OPTIONS:-+s -e -i --reverse --cycle} --inline-info --ansi "$@"
  62. }
  63. parse_config_file() {
  64. local line key val nr=0
  65. while IFS= read -r line; do
  66. (( ++nr ))
  67. [[ -z "$line" || "$line" = '#'* ]] && continue
  68. read -r key <<< "${line%% *}"
  69. read -r val <<< "${line#* }"
  70. if [[ -z "$val" ]]; then
  71. config_err+=( "missing value for \"$key\" in config file on line $nr" )
  72. continue
  73. fi
  74. case "$key" in
  75. full_song_format) track_format="$val" ;;
  76. fzf_options) [[ ! -v FZMP_FZF_OPTIONS ]] && FZMP_FZF_OPTIONS="$val" ;;
  77. default_view)
  78. if [[ "$val" =~ playlist|songs|artists ]]; then
  79. case "$val" in
  80. playlist) default_filter='filter_by_playlist' ;;
  81. songs) default_filter='filter_all_songs' ;;
  82. artists) default_filter='filter_by_artist' ;;
  83. esac
  84. else
  85. config_err+=( "unknown format \"$val\" in config file on line $nr" )
  86. fi
  87. ;;
  88. *) config_err+=( "unknown key \"$key\" in config file on line $nr" )
  89. esac
  90. done
  91. }
  92. filter_all_songs() {
  93. local choice
  94. mapfile -t choice < <( mpc search -f "%file%\t$track_format" filename '' |
  95. fzf --multi \
  96. --with-nth='2..' \
  97. --delimiter='\t' \
  98. --expect='f2,f3,enter' |
  99. cut -f1)
  100. case "${choice[0]}" in
  101. 'f2') filter_by_artist ;;
  102. 'f3') filter_by_playlist ;;
  103. 'enter') printf '%s\n' "${choice[@]:1}" | play_songs ;;
  104. esac
  105. }
  106. filter_by_artist() {
  107. local choice
  108. mapfile -t choice < <(mpc list albumartist |
  109. sort -h |
  110. fzf \
  111. --preview='mpc list album artist {}' \
  112. --expect='f1,f3,enter,ctrl-x')
  113. (( ${#choice[@]} > 0 )) || die
  114. case "${choice[0]}" in
  115. f1) filter_all_songs ;;
  116. f3) filter_by_playlist ;;
  117. enter) filter_by_album_from_artist "${choice[1]}" ;;
  118. ctrl-x) play_artist "${choice[1]}" ;;
  119. esac
  120. }
  121. filter_by_album_from_artist() {
  122. local album artist choice
  123. [[ -z "$1" ]] && filter_by_artist
  124. artist="$1"
  125. mapfile -t choice < <(mpc search -f '[[%album%]' albumartist "$artist" |
  126. sort -h | uniq |
  127. fzf --prompt="$artist > " \
  128. --preview="album=\$(cut -f2 <<< {}); mpc search -f '[[[%track% - ][%title%]]|%file%]' artist \"$artist\" album \"\$album\"" \
  129. --expect='f1,f3,enter,ctrl-x' \
  130. --bind='Ctrl-A:select-all' |
  131. cut -f2)
  132. case "${choice[0]}" in
  133. 'f1') filter_all_songs ;;
  134. 'f3') filter_by_playlist ;;
  135. 'enter') filter_songs_from_album "$artist" "${choice[1]}" ;;
  136. 'ctrl-x') play_album "${choice[1]}" ;;
  137. *) filter_by_artist ;;
  138. esac
  139. }
  140. filter_songs_from_album() {
  141. local album artist choice
  142. [[ -z "$1" || -z "$2" ]] && die
  143. artist="$1"
  144. album="$2"
  145. mapfile -t choice < <(mpc search -f '%file%\t[[[%track% - ][%title%]]|%file%]' artist "${artist}" album "${album}" |
  146. fzf --prompt="$artist - $album > " \
  147. --multi \
  148. --with-nth='2..' \
  149. --delimiter='\t' \
  150. --expect='f1,f3,enter' \
  151. --bind='Ctrl-A:select-all' |
  152. cut -f1)
  153. case "${choice[0]}" in
  154. 'f1') filter_all_songs ;;
  155. 'f3') filter_by_playlist ;;
  156. 'enter') printf '%s\n' "${choice[@]:1}" | play_songs ;;
  157. *) filter_by_album_from_artist "$artist" ;;
  158. esac
  159. }
  160. filter_by_playlist() {
  161. local choice
  162. current_song=$(mpc current -f "$track_format")
  163. mapfile -t choice < <(mpc playlist -f "%position%\t$track_format" |
  164. fzf --prompt='playlist > ' \
  165. --multi \
  166. ${current_song:+--header="now playing: ${current_song}"} \
  167. --delimiter='\t' \
  168. --with-nth='2..' \
  169. --expect='f1,f2,>,<,ctrl-d,enter,del' |
  170. cut -f1) || die
  171. case "${choice[0]}" in
  172. 'f1') filter_all_songs ;;
  173. 'f2') filter_by_artist ;;
  174. '>') mpc -q next; filter_by_playlist ;;
  175. '<') mpc -q prev; filter_by_playlist ;;
  176. 'ctrl-d') [[ -n "${choice[1]}" ]] && mpc -q del "${choice[@]:1}"; filter_by_playlist ;;
  177. 'enter') [[ -n "${choice[1]}" ]] && mpc -q play "${choice[@]:1}"; filter_by_playlist ;;
  178. 'del') [[ -n "${choice[1]}" ]] && mpc -q del "${choice[@]:1}"; filter_by_playlist ;;
  179. esac
  180. }
  181. play_songs() {
  182. mapfile -t songs
  183. (( "${#songs[@]}" > 0 )) || die
  184. printf '%s\n' "${songs[@]}" | mpc -q add
  185. index=$(mpc playlist | wc -l)
  186. (( ${#songs[@]} > 1 )) &&
  187. index=$(( index - ${#songs[@]} + 1))
  188. mpc -q play "$index"
  189. filter_by_playlist
  190. }
  191. play_artist() {
  192. mpc findadd albumartist "$@"
  193. filter_by_playlist
  194. }
  195. play_album() {
  196. mpc findadd album "$@"
  197. filter_by_playlist
  198. }
  199. [[ -s "$config_file" ]] && parse_config_file < "$config_file"
  200. if (( verbose > 0 && ${#config_err[@]} > 0 )); then
  201. err 'there were errors parsing config file:'
  202. for e in "${config_err[@]}"; do
  203. err "$e"
  204. done
  205. fi
  206. while :; do
  207. case "$1" in
  208. -A|--all) default_filter='filter_all_songs'; shift ;;
  209. -a|--artist) default_filter='filter_by_artist'; shift ;;
  210. -p|--playlist) default_filter='filter_by_playlist'; shift ;;
  211. -h|--help) usage; exit ;;
  212. -v|--verbose) (( verbose++ )); shift ;;
  213. *) break
  214. esac
  215. done
  216. has -v fzf mpc || die
  217. is_running mpd || [[ -v MPD_HOST ]] || die "can't connect to mpd"
  218. $default_filter