;;; emms-init.el --- Initialize emms

;; This file is part of Michael Olson's Emacs settings.

;; The code in this file may be used, distributed, and modified
;; without restriction.

;; I use initsplit.el to separate customize settings on a per-project
;; basis.

;;; Setup

(add-to-list 'load-path "/home/mwolson/proj/emacs/emms/git-emms/lisp")

;; Initialize
(require 'emms)
(require 'emms-browser)
(require 'emms-cache)
(require 'emms-info)
(require 'emms-lastfm)
(require 'emms-player-mpd)
(require 'emms-playing-time)
(require 'emms-playlist-mode)
(require 'emms-streams)
(require 'emms-tag-editor)
(require 'emms-volume)

(unless my-start-gnus
  (emms-cache 1)
  (emms-playing-time 1)
  (emms-lastfm 1)                    ; must come after emms-playing-time
  )

;; Load authentication info from an external source
(load "~/.emacs.d/.emms-auth")

(unless my-start-gnus
  ;; Only show files in emms-browser, not playlists
  (emms-browser-make-filter
   "all-files" (emms-browser-filter-only-type 'file))
  (emms-browser-set-filter (assoc "all-files" emms-browser-filters)))

;;; Track descriptions

(defun my-upcase-initials (string)
  "Do `upcase-initials' on STRING, but do not uppercase letters
that come after quote characters."
  (with-temp-buffer
    (insert (upcase-initials string))
    (goto-char (point-min))
    (while (re-search-forward "['`]\\([[:upper:]]\\)" nil t)
      (downcase-region (match-beginning 1) (match-end 1)))
    (buffer-string)))

(defun my-emms-info-track-description (track)
  "Return a description of the current track."
  (let ((artist (emms-track-get track 'info-artist))
        (title (emms-track-get track 'info-title))
        (name (emms-track-get track 'name))
        (type (emms-track-type track)))
    (cond ((and (stringp artist) (not (string= artist ""))
                (stringp title) (not (string= title "")))
           (let ((desc (format "%s - %s" artist title)))
             (if (memq type '(file url))
                 desc
               (concat (symbol-name type) ": " desc))))
          ((and (stringp title) (not (string= title "")))
           (my-upcase-initials title))
          ((null name)
           "Invalid track!")
          ((memq type '(url streamlist))
           "")
          (t
           (concat (symbol-name type) ": " name)))))

;;; Track info

(defvar my-emms-info-annoying-titles
  '("[[:blank:]]*([^(]*\\(Album\\|LP\\)\\( Version\\)?)\\([[:blank:]]\\|$\\)"
    "[[:blank:]]*\\[Explicit\\]$")
  "List of annoying things in titles to strip out.")

(defun my-emms-info-filter-titles (track)
  "Remove annoying things from titles."
  (let ((title (emms-track-get track 'info-title)))
    (when title
      (save-match-data
        (dolist (regexp my-emms-info-annoying-titles)
          (setq title (replace-regexp-in-string regexp "" title))))
      (emms-track-set track 'info-title title))))

(defun my-emms-info-fromname (track)
  "Add track information to TRACK.
This is a useful element for `emms-info-functions'."
  (when (and (memq (emms-track-type track) '(file playlist))
             (not (or (emms-track-get track 'info-artist)
                      (emms-track-get track 'info-title))))
    (let ((name (emms-track-get track 'name))
          artist title)
      (unless (or (null name) (string-match "\\`http://" name))
        (setq name (file-name-sans-extension (file-name-nondirectory name)))
        (save-match-data
          (when (string-match "\\`\\([^-]+?\\) *- *\\(.*\\)\\'" name)
            (setq artist (match-string 1 name)
                  title (match-string 2 name))
            (setq artist (my-upcase-initials
                          (emms-replace-regexp-in-string "_" " " artist)))
            (setq title (my-upcase-initials
                         (emms-replace-regexp-in-string "_" " " title)))
            (emms-track-set track 'info-artist artist)
            (emms-track-set track 'info-title title)))))))

;;; Utilities

;; Show the currently-playing song
(defun np (&optional arg)
  (interactive "P")
  (emms-player-mpd-show arg))

;; Reset the cached info for all songs in the current buffer
(defun my-emms-reset-cache ()
  (interactive)
  (save-excursion
    (goto-char (point-min))
    (emms-walk-tracks
      (let ((track (emms-playlist-track-at (point))))
        (emms-cache-set (emms-track-get track 'type)
                        (emms-track-get track 'name)
                        nil)))))

;; Open file at point in playlist
(defun my-emms-playlist-open-file ()
  (interactive)
  (unless (emms-playlist-track-at)
    (emms-playlist-next))
  (let ((name (emms-track-get (emms-playlist-track-at) 'name)))
    (when name
      (find-file name))))

;; Switch to the radio buffer
(defun my-emms-streams ()
  (interactive)
  (let ((buf (get-buffer emms-stream-buffer-name)))
    (if buf
        (switch-to-buffer buf)
      (emms-streams))))

;; Switch to either the radio buffer or the current EMMS playlist
(defun my-emms-switch-to-current-playlist ()
  (interactive)
  (if (and (boundp 'emms-stream-playlist-buffer)
           (eq emms-stream-playlist-buffer emms-playlist-buffer))
      (switch-to-buffer emms-stream-buffer-name)
    (if (or (null emms-playlist-buffer)
            (not (buffer-live-p emms-playlist-buffer)))
        (error "No current Emms buffer")
      (switch-to-buffer emms-playlist-buffer))))

;; Bring up the browser interface with my preferences
(defvar my-emms-browser-buffer nil)

(defun my-emms-switch-to-browser ()
  (interactive)
  (if (and my-emms-browser-buffer
           (buffer-live-p my-emms-browser-buffer))
      (switch-to-buffer my-emms-browser-buffer)
    (emms-browse-by-artist)
    (emms-browser-expand-to-level-2)
    (setq my-emms-browser-buffer (current-buffer)))
  (isearch-forward))

;;; Every time the song changes, show me its description

(defvar np-interval 5
  "How often (in seconds) to check whether the current song has changed.")

;; Internal
(defvar np-last nil
  "Description for most recently-played track.")
(defvar np-timer nil
  "Timer used by `np-maybe'.")

(defun np-maybe ()
  (condition-case nil
      (emms-player-mpd-show
       nil
       (lambda (buffer desc)
         (when (and (stringp desc)
                    (not (string= desc "")))
           (unless (and (stringp np-last)
                        (string= np-last desc))
             (setq np-last desc)
             (message "%s" desc)))))
    (error (np-remove))))

(defun np-insinuate ()
  (unless np-timer
    (setq np-timer (run-with-idle-timer np-interval t #'np-maybe))))

(defun np-remove ()
  (interactive)
  (when np-timer
    (emms-cancel-timer np-timer)
    (setq np-timer nil)))

(unless my-start-gnus
  (add-hook 'emms-player-started-hook #'np-insinuate)
  (add-hook 'emms-player-stopped-hook #'np-remove)
  (add-hook 'emms-player-finished-hook #'np-remove))

;;; Fancy playlist tree display

(defun my-emms-iterate-directory (dir regex file-fn dir-fn closure)
  "Call FILE-FN for each file under DIR that matches REGEX, and call
DIR-FN for each directory under DIR that matches REGEX.

The FILE-FN and DIR-FN functions should both accept three
arguments: the name of the file, its depth level in the tree, and
the CLOSURE argument."
  (let ((dirs (list (cons dir 0))))
    (while dirs
      (cond
       ((file-directory-p (caar dirs))
        (if (string-match "/\\.\\.?$" (caar dirs))
            (setq dirs (cdr dirs))
          (let ((depth (cdar dirs)))
            (unless (equal depth 0)
              (funcall dir-fn (caar dirs) (cdar dirs) closure))
            (setq depth (1+ depth))
            (setq dirs
                  (condition-case nil
                      (nconc (mapcar `(lambda (entry)
                                        (cons entry ,depth))
                                     (directory-files (caar dirs) t))
                             (cdr dirs))
                    (error (cdr dirs)))))))
       ((string-match regex (caar dirs))
        (funcall file-fn (caar dirs) (cdar dirs) closure)
        (setq dirs (cdr dirs)))
       (t
        (setq dirs (cdr dirs)))))))

(defun my-fancy-directory-tree-insert-file (file depth type)
  (unless (let ((case-fold-search nil))
            (string-match emms-source-file-exclude-regexp file))
    (with-current-emms-playlist
      (let ((inhibit-read-only t))
        (insert (make-string (* (1- depth) 2) ?\ ))))
    (emms-playlist-insert-track
     (emms-track type (expand-file-name file)))))

(defun my-fancy-directory-tree-insert-dir (dir depth ignored)
  (emms-playlist-ensure-playlist-buffer)
  (let ((inhibit-read-only t))
    (unless (bobp)
      (insert "\n"))
    (insert (make-string (* (1- depth) 2) ?\ )
            "[ " (my-upcase-initials (file-name-nondirectory dir))
            " ]\n")))

;;;###autoload (autoload 'emms-play-fancy-playlist-directory-tree
;;;###autoload           "emms-init" nil t)
;;;###autoload (autoload 'emms-add-fancy-playlist-directory-tree
;;;###autoload           "emms-init" nil t)
(define-emms-source fancy-playlist-directory-tree (dir)
  "An EMMS source for multiple directory trees of playlist files.
If DIR is not specified, it is queried from the user."
  (interactive (list
                (emms-read-directory-name "Play directory tree: "
                                          emms-source-file-default-directory
                                          emms-source-file-default-directory
                                          t)))
  (my-emms-iterate-directory (expand-file-name dir) "^[^.]"
                             #'my-fancy-directory-tree-insert-file
                             #'my-fancy-directory-tree-insert-dir
                             'playlist))

;;;###autoload (autoload 'emms-play-fancy-directory-tree
;;;###autoload           "emms-init" nil t)
;;;###autoload (autoload 'emms-add-fancy-directory-tree
;;;###autoload           "emms-init" nil t)
(define-emms-source fancy-directory-tree (dir)
  "An EMMS source for multiple directory trees of files.
If DIR is not specified, it is queried from the user."
  (interactive (list
                (emms-read-directory-name "Play directory tree: "
                                          emms-source-file-default-directory
                                          emms-source-file-default-directory
                                          t)))
  (my-emms-iterate-directory (expand-file-name dir) "^[^.]"
                             #'my-fancy-directory-tree-insert-file
                             #'my-fancy-directory-tree-insert-dir
                             'file))

(defun my-emms-rebuild-main ()
  (interactive)
  (let ((buffer (get-buffer "*EMMS Playlist*")))
    (unless buffer
      (setq buffer (setq emms-playlist-buffer (emms-playlist-new))))
    (let ((emms-playlist-buffer buffer))
      (with-current-emms-playlist
        (emms-playlist-mode-clear)
        (emms-add-fancy-playlist-directory-tree "/stuff/music/playlists")))))

;;; Key customizations

(define-key emms-playlist-mode-map "o" 'my-emms-playlist-open-file)
(define-key emms-playlist-mode-map "q" 'emms-playlist-mode-current-kill)
(define-key emms-playlist-mode-map (kbd "SPC") 'emms-pause)

(global-set-key "\C-cm" nil)
(global-set-key "\C-cm " 'emms-pause)
(global-set-key "\C-cm0" 'emms-playlist-mode-clear)
(global-set-key "\C-cmb" 'my-emms-switch-to-browser)
(global-set-key "\C-cmc" 'emms-player-mpd-connect)
(global-set-key "\C-cml" nil)
(global-set-key "\C-cmlb" 'emms-lastfm-radio-ban)
(global-set-key "\C-cmll" 'emms-lastfm-radio-love)
(global-set-key "\C-cmls" 'emms-lastfm-radio-skip)
(global-set-key "\C-cmn" 'emms-player-mpd-next)
(global-set-key "\C-cmo" 'my-emms-switch-to-current-playlist)
(global-set-key "\C-cmp" 'emms-player-mpd-previous)
(global-set-key "\C-cmq" 'emms-stop)
(global-set-key "\C-cmQ" 'emms-player-mpd-clear)
(global-set-key "\C-cmr" 'my-emms-streams)
(global-set-key "\C-cmR" 'my-emms-rebuild-main)
(global-set-key "\C-cms" 'emms-seek)
(global-set-key "\C-cm-" 'emms-volume-mode-minus)
(global-set-key "\C-cm+" 'emms-volume-mode-plus)
(global-set-key "\C-cm\C-c" 'my-emms-reset-cache)

;;; Custom variables

(custom-set-variables
 '(emms-cache-file "~/.emacs.d/.emms-cache")
 '(emms-info-asynchronously nil)
 '(emms-info-functions (quote (emms-info-mpd my-emms-info-fromname)))
 '(emms-lastfm-radio-metadata-period nil)
 '(emms-player-list (quote (emms-player-mpd emms-player-lastfm-radio)))
 '(emms-player-mpd-music-directory "/stuff/music")
 '(emms-playlist-buffer-name "*EMMS Playlist*")
 '(emms-playlist-default-major-mode (quote emms-playlist-mode))
 '(emms-playlist-mode-open-playlists t)
 '(emms-show-format "NP: %s")
 '(emms-stream-default-action "play")
 '(emms-track-description-function (quote my-emms-info-track-description))
 '(emms-track-info-filters (quote (my-emms-info-filter-titles)))
 '(emms-track-initialize-functions (quote (emms-info-initialize-track)))
 '(emms-volume-change-amount 3)
 '(emms-volume-change-function (quote emms-volume-mpd-change)))
(custom-set-faces)

;;; Initialization

;(define-emms-combined-source all nil
;  '((emms-source-playlist-directory "/stuff/music/playlists/albums")
;    (emms-source-playlist-directory "/stuff/music/albums/classical")
;    (emms-source-playlist-directory "/stuff/music/albums/candy_mind")
;    (emms-source-playlist-directory "/stuff/music/mixes")
;    (emms-source-playlist-file "~/.emacs.d/emms-playlist")))

;(emms-add-all)

(define-emms-source misc ()
  (emms-add-fancy-playlist-directory-tree "/stuff/music/playlists"))

(my-emms-rebuild-main)

(when my-start-gnus
  (setq emms-player-mpd-check-interval nil))

;;; emms-init.el ends here