Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 artist |
  109. sort -h |
  110. fzf \
  111. --preview='mpc list album artist {}' \
  112. --expect='f1,f3,enter')
  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. esac
  119. }
  120. filter_by_album_from_artist() {
  121. local album artist choice
  122. [[ -z "$1" ]] && filter_by_artist
  123. artist="$1"
  124. mapfile -t choice < <(mpc search -f '[(%date%)\t][%album%]' artist "$artist" |
  125. sort -h | uniq |
  126. fzf --prompt="$artist > " \
  127. --preview="album=\$(cut -f2 <<< {}); mpc search -f '[[[%track% - ][%title%]]|%file%]' artist \"$artist\" album \"\$album\"" \
  128. --expect='f1,f3,enter' \
  129. --bind='Ctrl-A:select-all' |
  130. cut -f2)
  131. case "${choice[0]}" in
  132. 'f1') filter_all_songs ;;
  133. 'f3') filter_by_playlist ;;
  134. 'enter') filter_songs_from_album "$artist" "${choice[1]}" ;;
  135. *) filter_by_artist ;;
  136. esac
  137. }
  138. filter_songs_from_album() {
  139. local album artist choice
  140. [[ -z "$1" || -z "$2" ]] && die
  141. artist="$1"
  142. album="$2"
  143. mapfile -t choice < <(mpc search -f '%file%\t[[[%track% - ][%title%]]|%file%]' artist "${artist}" album "${album}" |
  144. fzf --prompt="$artist - $album > " \
  145. --multi \
  146. --with-nth='2..' \
  147. --delimiter='\t' \
  148. --expect='f1,f3,enter' \
  149. --bind='Ctrl-A:select-all' |
  150. cut -f1)
  151. case "${choice[0]}" in
  152. 'f1') filter_all_songs ;;
  153. 'f3') filter_by_playlist ;;
  154. 'enter') printf '%s\n' "${choice[@]:1}" | play_songs ;;
  155. *) filter_by_album_from_artist "$artist" ;;
  156. esac
  157. }
  158. filter_by_playlist() {
  159. local choice
  160. current_song=$(mpc current -f "$track_format")
  161. mapfile -t choice < <(mpc playlist -f "%position%\t$track_format" |
  162. fzf --prompt='playlist > ' \
  163. --multi \
  164. ${current_song:+--header="now playing: ${current_song}"} \
  165. --delimiter='\t' \
  166. --with-nth='2..' \
  167. --expect='f1,f2,>,<,ctrl-d,enter' |
  168. cut -f1) || die
  169. case "${choice[0]}" in
  170. 'f1') filter_all_songs ;;
  171. 'f2') filter_by_artist ;;
  172. '>') mpc -q next; filter_by_playlist ;;
  173. '<') mpc -q prev; filter_by_playlist ;;
  174. 'ctrl-d') [[ -n "${choice[1]}" ]] && mpc -q del "${choice[@]:1}"; filter_by_playlist ;;
  175. 'enter') [[ -n "${choice[1]}" ]] && mpc -q play "${choice[@]:1}"; filter_by_playlist ;;
  176. esac
  177. }
  178. play_songs() {
  179. mapfile -t songs
  180. (( "${#songs[@]}" > 0 )) || die
  181. printf '%s\n' "${songs[@]}" | mpc -q add
  182. index=$(mpc playlist | wc -l)
  183. (( ${#songs[@]} > 1 )) &&
  184. index=$(( index - ${#songs[@]} + 1))
  185. mpc -q play "$index"
  186. filter_by_playlist
  187. }
  188. [[ -s "$config_file" ]] && parse_config_file < "$config_file"
  189. if (( verbose > 0 && ${#config_err[@]} > 0 )); then
  190. err 'there were errors parsing config file:'
  191. for e in "${config_err[@]}"; do
  192. err "$e"
  193. done
  194. fi
  195. while :; do
  196. case "$1" in
  197. -A|--all) default_filter='filter_all_songs'; shift ;;
  198. -a|--artist) default_filter='filter_by_artist'; shift ;;
  199. -p|--playlist) default_filter='filter_by_playlist'; shift ;;
  200. -h|--help) usage; exit ;;
  201. -v|--verbose) (( verbose++ )); shift ;;
  202. *) break
  203. esac
  204. done
  205. has -v fzf mpc || die
  206. is_running mpd || [[ -v MPD_HOST ]] || die "can't connect to mpd"
  207. $default_filter