music 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. #!/bin/dash
  2. ###################
  3. ##### Globals #####
  4. ###################
  5. # Requires MPD and MPC
  6. # Path to the configuration file.
  7. config="/home/eddie/.config/mpd-actions/settings.conf"
  8. # Path to the current user's trash folder.
  9. trash="$XDG_DATA_HOME/Trash/files"
  10. # Path to the current user's MPD configuration file.
  11. mpd_conf="/home/eddie/.mpd/mpd.conf"
  12. # File name for our backup playlist.
  13. backup_playlist="zz.`basename "$0"`.saved"
  14. # Dmenu command.
  15. dm="dmenu -i $DMENU_OPTIONS"
  16. # Menu items for the [Current Song] submenu.
  17. menu_current='[Add to Playlist]
  18. [Move to Trash]
  19. [Open Directory]'
  20. # Menu items for the [Search for Song] submenu.
  21. menu_search='Title
  22. Artist
  23. Album
  24. Genre'
  25. #####################
  26. ##### Functions #####
  27. #####################
  28. # Feeding menu items and a prompt to Dmenu the regular way gets messy.
  29. # To save sanity, this function makes it as simple as:
  30. # menu "Your Prompt" "Item A" ["Item B" "Item C" ...]
  31. menu () {
  32. # We grab the prompt message...
  33. prompt="$1"
  34. # ...then shift to the next argument.
  35. shift
  36. # We will now iterate through the rest of the arguments...
  37. until [ -z "$1" ]; do
  38. # ...add the menu item to the list we're going to feed to Dmenu...
  39. items="$items$1\n"
  40. # ...move on to the next argument...
  41. shift
  42. # ...and keep doing this until there are no more arguments.
  43. done
  44. # Now that we're done with that, we can feed the hungry Dmenu.
  45. # We feed the list though `head -c-1` first, to get rid of that
  46. # trailing newline, since Dmenu isn't smart enough to ignore it.
  47. echo "$items" | head -c-1 | $dm -p "$prompt"
  48. }
  49. # We can use menu() function for yes/no prompts.
  50. confirm () {
  51. menu "$*" 'No' 'Yes'
  52. }
  53. # And we can even use it for a simple notice.
  54. alert () {
  55. menu "$*" 'OK'
  56. }
  57. # A function to prompt the user for a playlist. But it does so much
  58. # more than that! It will also let the user create new playlists and
  59. # populate them with the current MPD playlist or his or her entire
  60. # music library. It will also let the user play and delete playlists.
  61. playlist_manager () {
  62. # We'll loop forever (unless something breaks out) so when the user
  63. # wants to return to the playlist selection, he or she does not have
  64. # to restart the whole program.
  65. while :; do
  66. # We ask the user to choose a playlist. We get our playlist list
  67. # from MPC, sort this, and feed the list as arguments to menu().
  68. # We put the answer from the user into $playlist.
  69. playlist=`menu 'Choose a playlist:' "$(mpc lsplaylists | sort)" '[Create New]'`
  70. # If the user chose the create a new playlist...
  71. if [ "$playlist" = '[Create New]' ]; then
  72. # Ask him or her what he or she wishes to name it...
  73. playlist=`menu 'Enter name of new playlist:' $(mpc lsplaylists | sort)`
  74. # ...and if anything was said...
  75. if [ "$playlist" ]; then
  76. # ...we need to make sure that there's not already a file by
  77. # the name he or she gave us.
  78. if [ -f "$playlist_dir/$playlist.m3u" ]; then
  79. # If there is, ask the user if he or she wants to replace it.
  80. if [ "`confirm 'Replace existing playlist?'`" = 'Yes' ]; then
  81. # Get rid of it if they tell us "Yes."
  82. mpc rm "$playlist"
  83. fi
  84. fi
  85. # We check again to see if there's a file by the same name.
  86. # If the file is still there, then the user has already told
  87. # us "no" to replacing it, and we skip this.
  88. if [ ! -f "$playlist_dir/$playlist.m3u" ]; then
  89. # Otherwise, we ask if he or she wants to populate the new
  90. # playlist with something or leave it empty.
  91. action=`menu "Populate \"$playlist\"?" '[Leave Empty]' '[With MPD Playlist]' '[With Library]'`
  92. # If chosen to populate it with the current MPD playlist...
  93. if [ "$action" = '[With MPD Playlist]' ]; then
  94. # ...we make a new file containing the playlist header...
  95. echo "#EXTM3U" > "$playlist_dir/$playlist.m3u"
  96. # ...and get the list of the files MPD has on its playlist
  97. # and tack the path to the user's music library onto the
  98. # beginning of each entry so we have absolute paths. We
  99. # then append this to the playlist file.
  100. mpc -f '%file%' playlist | sed "s:^:$music_dir/:" >> "$playlist_dir/$playlist.m3u"
  101. # If chosen to populate the playlist with the library...
  102. elif [ "$action" = '[With Library]' ]; then
  103. # ...we do the same thing as from the MPD playlist, except
  104. # we grab the list of files in the user's music library.
  105. echo "#EXTM3U" > "$playlist_dir/$playlist.m3u"
  106. mpc -f '%file%' listall | sed "s:^:$music_dir/:" > "$playlist_dir/$playlist.m3u"
  107. # Finally, if they chose to not populate the playlist...
  108. else
  109. # ...we'll just create an empty playlist.
  110. echo "#EXTM3U" > "$playlist_dir/$playlist.m3u"
  111. fi
  112. else
  113. # Forget the user selected anything so it won't get sent
  114. # to the caller by accident.
  115. unset playlist
  116. fi
  117. fi
  118. fi
  119. # If the user chose a playlist and not '[Create New]' ages ago...
  120. if [ "$playlist" ]; then
  121. # Ask him or her what is to be done with the playlist.
  122. action=`menu "$playlist:" '[Select]' '[Go Back]' '[Play]' '[Edit]' '[Delete]'`
  123. case "$action" in
  124. *'Play'*) # If the user wants to play it...
  125. load_playlist "$playlist"
  126. # Forget the user ever selected anything so that whoever
  127. # called this function won't try to do anything to the list
  128. # the user only wanted to play. Actually, I could have just
  129. # said `break` here, and have been done with it.
  130. unset playlist
  131. ;;
  132. *'Delete'*) # If the user chose the delete the playlist...
  133. # Make sure he or she really wants to do this, and if yes,
  134. # kill it.
  135. [ "`confirm "Delete \"$playlist\"?"`" = 'Yes' ] && mpc rm "$playlist"
  136. ;;
  137. *'Edit'*)
  138. if which exo-open; then
  139. exo-open --launch TerminalEmulator vim "$playlist_dir/$playlist.m3u" &
  140. elif which urxvt; then
  141. urxvt -e vim "$playlist_dir/$playlist.m3u" &
  142. else
  143. xterm -e vim "$playlist_dir/$playlist.m3u" &
  144. fi
  145. unset playlist
  146. break
  147. ;;
  148. # If the user chose to do anything else, other than go back to
  149. # the playlist selection prompt, we break out of the `while`
  150. # loop.
  151. *'Back'*)
  152. ;;
  153. *)
  154. [ -z "$action" ] && unset playlist
  155. break
  156. ;;
  157. esac
  158. else
  159. # And if the user didn't choose a playlist, we break out of the
  160. # loop, assuming the user wants to exit.
  161. break
  162. fi
  163. done
  164. # Return whatever playlist (if any) the user selected to the caller.
  165. echo "$playlist"
  166. }
  167. # I could really do without making these next two functions.
  168. # But it'll make the main code look more aeshetically pleasing to me.
  169. check_for_mpc () {
  170. # If MPC does not adknowledge its existance, we'll assume the user
  171. # must not have it installed. Yes, I could have used `which` here,
  172. # but I didn't. So shush.
  173. if ! mpc > /dev/null; then
  174. alert 'MPC must be installed.'
  175. exit 1
  176. fi
  177. }
  178. exit_if_mpd_stopped () {
  179. if ! mpc | grep -qE 'playing|paused'; then
  180. # We obviously can't get any information when nothing is playing!
  181. alert 'You are not playing anything.'
  182. exit 1
  183. fi
  184. }
  185. # Grabs some information about the current song:
  186. get_song_info () {
  187. mpc | grep -q '[stopped]' && song_title=`mpc --format '%title%' | head -1` || song_title='[MPD stopped.]'
  188. song_rpath=`mpc --format '%file%' | head -1` # Relative path
  189. song_apath="$music_dir/$song_rpath" # and absolute path.
  190. }
  191. save_config () {
  192. # Store the settings in a way that we can just source the config
  193. # when we want to recall them.
  194. echo "music_dir=\"$music_dir\"
  195. playlist_dir=\"$playlist_dir\"
  196. last_list_pos=\"$last_list_pos\"
  197. last_time_pos=\"$last_time_pos\"" > "$config"
  198. }
  199. load_config () {
  200. # Create the configuration directory if it does not exist.
  201. mkdir -p "`dirname "$config"`"
  202. # Create the configuration file if it does not exist.
  203. touch "$config" # You've been a naughty file.
  204. # Read in [source] the configuration.
  205. . "$config"
  206. # If we don't have one set yet, ask where the user is keeping his or
  207. # her musical goods at.
  208. if [ -z "$music_dir" ] || [ ! -d "$music_dir" ]; then
  209. music_dir=`menu 'Path to your music library:'`
  210. # If the user didn't enter anything, assume they wanted to exit.
  211. [ -z "$music_dir" ] && exit
  212. # If we don't find the directory the user gave us (or it turns out
  213. # to be something else other than a directory), bitch about this...
  214. if [ ! -d "$music_dir" ]; then
  215. alert 'No such directory.'
  216. # ...and promptly exit.
  217. exit 1
  218. fi
  219. fi
  220. # Save this to our config file.
  221. save_config
  222. # Now, we do the same thing for the playlists directory.
  223. if [ -z "$playlist_dir" ] || [ ! -d "$playlist_dir" ]; then
  224. playlist_dir=`menu 'Path to your playlists:'`
  225. [ -z "$playlist_dir" ] && exit
  226. if [ ! -d "$playlist_dir" ]; then
  227. alert 'No such directory.'
  228. exit 1
  229. fi
  230. fi
  231. save_config
  232. }
  233. backup_playlist () {
  234. # Remember where we left off.
  235. last_list_pos=`mpc | sed -n 2p | cut -d'#' -f2 | cut -d'/' -f1`
  236. last_time_pos=`mpc | sed -n 2p | cut -d' ' -f5 | cut -d'/' -f1`
  237. save_config
  238. # Shift down backups of backups and get rid of the old backup
  239. # playlist, if it exists.
  240. [ -f "$playlist_dir/$backup_playlist.1.m3u" ] && mv "$playlist_dir/$backup_playlist.1.m3u" "$playlist_dir/$backup_playlist.2.m3u"
  241. if [ -f "$playlist_dir/$backup_playlist.m3u" ]; then
  242. mv "$playlist_dir/$backup_playlist.m3u" "$playlist_dir/$backup_playlist.1.m3u"
  243. mpc rm "$backup_playlist"
  244. fi
  245. # ...save the current MPD playlist over it...
  246. mpc save "$backup_playlist"
  247. }
  248. restore_playlist () {
  249. # Stop and clear the current MPD playlist...
  250. mpc stop
  251. mpc clear
  252. mpc load $backup_playlist
  253. # ...recall where we left last left off.
  254. load_config
  255. mpc play $last_list_pos
  256. mpc seek $last_time_pos
  257. }
  258. # Backs up current playlist, then loads and plays argument.
  259. load_playlist () {
  260. backup_playlist
  261. mpc stop
  262. mpc clear
  263. mpc load "$1"
  264. mpc play
  265. }
  266. ################
  267. ##### Main #####
  268. ################
  269. # Check for MPC and load configuration.
  270. check_for_mpc
  271. load_config
  272. while :; do
  273. # Menu items for the main menu.
  274. menu_main="[Current Song]
  275. [Search for Song]
  276. [Playlists]
  277. [Toggle]
  278. [Previous]
  279. [Next]
  280. [Seek -10s]
  281. [Seek +10s]
  282. [Replay]
  283. [Random: `mpc | sed 's/volume://' | sed -n 3p | awk '{print $5}'`]
  284. [Repeat: `mpc | sed 's/volume://' | sed -n 3p | awk '{print $3}'`]
  285. [Single: `mpc | sed 's/volume://' | sed -n 3p | awk '{print $7}'`]
  286. [Consume: `mpc | sed 's/volume://' | sed -n 3p | awk '{print $9}'`]
  287. [Crossfade: `mpc crossfade | awk '{print $2}'`]
  288. [Restore List]
  289. [Backup List]
  290. [Update Database]
  291. [Restart Server]
  292. [Set Directories]"
  293. get_song_info
  294. action=`menu "$song_title:" "$menu_main"`
  295. case "$action" in
  296. *'Current'*) #[Current Song]
  297. exit_if_mpd_stopped
  298. get_song_info
  299. # Now, we ask the user what they want to do.
  300. action=`menu "$song_title:" "$menu_current"`
  301. case "$action" in
  302. *'Add'*) #[Add to Playlist]
  303. # We ask for a playlist to append to.
  304. # I'm lazy. We'll re-use $action.
  305. action=`playlist_manager`
  306. [ -z "$action" ] && exit
  307. # We append the absolute path to the selected playlist...
  308. echo "$song_apath" >> "$playlist_dir/$action.m3u"
  309. # ...then sort the playlist, removing duplicates.
  310. sort -u "$playlist_dir/$action.m3u" -o "$playlist_dir/$action.m3u"
  311. ;;
  312. *'Trash'*) #[Move to Trash]
  313. # If the user really wants to...
  314. if [ "`confirm 'Delete playing song?'`" = 'Yes' ]; then
  315. # We remove the current song from the MPD playlist...
  316. mpc del 0
  317. # ...create the trash folder if it doesn't exist...
  318. mkdir -p "$trash"
  319. # ...and move the song to said trash folder.
  320. mv "$song_apath" "$trash"
  321. fi
  322. ;;
  323. *'Open'*) #[Open Directory]
  324. if which exo-open; then
  325. exo-open "`dirname "$song_apath"`" &
  326. elif which nautilus; then
  327. nautilus "`dirname "$song_apath"`" &
  328. elif which dolphin; then
  329. dolphin "`dirname "$song_apath"`" &
  330. elif which thunar; then
  331. thunar "`dirname "$song_apath"`" &
  332. else
  333. alert "Couldn't detect file manager."
  334. fi
  335. ;;
  336. esac
  337. break
  338. ;;
  339. *'Search'*)
  340. exact='off' # Exact match flag.
  341. append='off' # Append results flag.
  342. # Loop in case the user wants to do multiple searches.
  343. # Other actions can break out of this loop.
  344. while :; do
  345. # We ask the user what field to search.
  346. # Use a loop so he or she can toggle exact match.
  347. while :; do
  348. action=`menu 'Search:' "[Exact Match: $exact]" "[Append Results: $append]" "$menu_search"`
  349. case "$action" in
  350. *'[Exact'*)
  351. exact=`[ $exact = off ] && echo on || echo off`
  352. ;;
  353. *'[Append'*)
  354. append=`[ $append = off ] && echo on || echo off`
  355. ;;
  356. *)
  357. break
  358. ;;
  359. esac
  360. done
  361. [ -z "$action" ] && exit
  362. # Now, we ask what they want to search for.
  363. query=`menu 'Enter search query:'`
  364. [ -z "$query" ] && exit
  365. # We're going overwrite the results to the MPD playlist.
  366. # We don't want to just kill what the user has going on there,
  367. # so we will back his or her current playlist up and remember
  368. # which song and what time position MPD was at.
  369. backup_playlist
  370. if [ "$append" = 'off' ]; then
  371. last_item=1
  372. mpc stop
  373. mpc clear
  374. else
  375. last_item=$((`mpc | sed -n 2p | cut -d' ' -f2 | cut -d'/' -f2` + 1))
  376. fi
  377. # Search the MPD database for what the user seeks and add the
  378. # results to the MPD playlist.
  379. mpc `[ $exact = off ] && echo search || echo find` "$action" "$query" | mpc add
  380. mpc play $last_item
  381. # Give the user a chance to search again or restore their
  382. # playlist and preview the results.
  383. while :; do
  384. action=`menu "Preview: $(mpc -f '%title%' | head -1)" '[Continue]' '[Search Again]' '[Previous]' '[Next]' '[Skip -10s]' '[Skip +10s]' "[Random: $(mpc | sed -n 3p | awk '{print $6}')]" '[Restore Playlist]'`
  385. case "$action" in
  386. *'Previous'*)
  387. mpc prev
  388. ;;
  389. *'Next'*)
  390. mpc next
  391. ;;
  392. *'+10s'*)
  393. mpc seek +10
  394. ;;
  395. *'-10s'*)
  396. mpc seek -10
  397. ;;
  398. *'Random'*)
  399. mpc random
  400. ;;
  401. *)
  402. break
  403. ;;
  404. esac
  405. done
  406. [ "$action" != '[Continue]' ] && restore_playlist
  407. # If the user doesn't want to search again, break out of the
  408. # `while` loop.
  409. [ "$action" != '[Search Again]' ] && break
  410. done
  411. ;;
  412. *'Playlist'*) #[Playlists]
  413. # Since the playlist manager pretty much covers everything...
  414. action=`playlist_manager`
  415. [ -z "$action" ] && exit
  416. load_playlist "$action"
  417. break
  418. ;;
  419. *'Toggle'*)
  420. mpc toggle
  421. ;;
  422. *'Previous'*)
  423. mpc prev
  424. ;;
  425. *'Next'*)
  426. mpc next
  427. ;;
  428. *'+10s'*)
  429. mpc seek +10
  430. ;;
  431. *'-10s'*)
  432. mpc seek -10
  433. ;;
  434. *'Replay'*)
  435. mpc stop
  436. mpc play
  437. ;;
  438. *'Random'*)
  439. mpc random
  440. ;;
  441. *'Repeat'*)
  442. mpc repeat
  443. ;;
  444. *'Single'*)
  445. mpc single
  446. ;;
  447. *'Consume'*)
  448. mpc consume
  449. ;;
  450. *'Crossfade'*)
  451. action=`menu 'Crossfade time:' 0 1 2 3 4 5 6 7 8 9 10`
  452. [ -z "$action" ] && exit
  453. mpc crossfade "$action"
  454. ;;
  455. *'Restore'*)
  456. [ "`confirm 'Restore last saved playlist?'`" = 'Yes' ] && restore_playlist
  457. ;;
  458. *'Backup'*)
  459. [ "`confirm 'Backup current playlist?'`" = 'Yes' ] && backup_playlist
  460. ;;
  461. *'Update'*) #[Update Database]
  462. mpc update
  463. alert 'Database update started.'
  464. ;;
  465. *'Restart'*) #[Restart Server]
  466. if [ "`confirm 'Are you sure?'`" = 'Yes' ]; then
  467. notify-send -t 5000 'Restarting MPD server, please wait...'
  468. # Ask MPD to politely commit suicide.
  469. mpd --kill "$mpd_conf"
  470. # Wait patiently.
  471. sleep 5
  472. # See if MPD is still alive.
  473. mpd_pid=`pidof mpd`
  474. if [ "$mpd_pid" ]; then
  475. # If it is, commit murder.
  476. kill -9 $mpd_pid
  477. sleep 1
  478. fi
  479. # Resurrect the dead.
  480. mpd "$mpd_conf"
  481. alert 'MPD server restarted.'
  482. fi
  483. ;;
  484. *'Directories'*) #[Set Directories]
  485. if [ "`confirm 'Are you sure?'`" = 'Yes' ]; then
  486. # Forget what we know about where the user's music and playlists
  487. # are and ask again.
  488. rm "$config"
  489. unset music_dir
  490. unset playlist_dir
  491. load_config
  492. fi
  493. ;;
  494. *)
  495. break
  496. ;;
  497. esac
  498. done