summaryrefslogtreecommitdiff
path: root/durand-bongo.el
diff options
context:
space:
mode:
Diffstat (limited to 'durand-bongo.el')
-rw-r--r--durand-bongo.el1202
1 files changed, 1202 insertions, 0 deletions
diff --git a/durand-bongo.el b/durand-bongo.el
new file mode 100644
index 0000000..a93053a
--- /dev/null
+++ b/durand-bongo.el
@@ -0,0 +1,1202 @@
+;;; durand-bongo.el --- My configurations of Bongo -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022 Jean Sévère Durand
+
+;; Author: Jean Sévère Durand <durand@jsdurand.xyz>
+;; Keywords: files, games, hardware, hypermedia, multimedia
+
+;; This program is free software; you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This is my configuration file for Bongo.
+
+;;; Code:
+
+;;; First require bongo
+
+(use-package "bongo" 'bongo)
+
+;;; Basic customizations
+
+;;;; No electric modes
+
+;; I just don't like those.
+
+(setq bongo-seek-electric-mode nil)
+(setq volume-electric-mode nil)
+
+;;;; No icons
+
+(setq bongo-track-mark-icon-file-name nil)
+(setq bongo-action-track-icon nil)
+(setq bongo-display-track-icons nil)
+(setq bongo-display-header-icons nil)
+
+;;;; No special handling of tracks
+
+;; I do not distinguish tracks especially.
+;;
+;; Maybe this will change in the future.
+
+(setq bongo-display-track-lengths nil)
+
+;;;; Default directory to find music
+
+(setq bongo-default-directory
+ (expand-file-name "~/Desktop/Centre/Musique"))
+
+;;;; Prefer playlist buffer
+
+(setq bongo-prefer-library-buffers nil)
+
+;;;; Insert whose directory tree
+
+(setq bongo-insert-whole-directory-trees t)
+
+;;;; No logo
+
+(setq bongo-logo nil)
+
+;;;; No inline playback indication
+
+;; But no inline indication
+(setq bongo-display-inline-playback-progress nil)
+
+;;;; Do not mark played tracks
+
+(setq bongo-mark-played-tracks nil)
+
+;;;; Display song name in the header line
+
+(setq bongo-header-line-mode t)
+(setq bongo-header-line-function #'bongo-default-header-line-function)
+
+;;;; Disable mode line indication by default
+
+(setq bongo-mode-line-indicator-mode nil)
+
+(setq bongo-display-playback-mode-indicator t)
+
+;;;; Backend
+
+(setq bongo-enabled-backends (list 'mpv))
+
+(setq bongo-custom-backend-matchers
+ '((mpv local-file "webm" "m4a")
+ ;; NOTE
+ ;;
+ ;; For a regular expression as a matcher, it is supposed to be
+ ;; a string, instead of a list of strings.
+ (mpv "https:" . "youtube")))
+
+;;; Circular playing
+
+(setq-default bongo-next-action #'durand-bongo-play-next-or-first)
+
+(defun durand-bongo-play-next-or-first (&optional n)
+ "Start playing the next track in the nearest Bongo playlist buffer.
+If there is no next track to play, signal an error.
+With numerical prefix argument N, skip that many tracks.
+With \\[universal-argument] as prefix argument, just switch to \
+progressive playback mode.
+With \\[universal-argument] \\[universal-argument] as prefix argument, \
+insert an action track at point."
+ (interactive "P")
+ (cond
+ ((equal n '(16))
+ (bongo-insert-line 'bongo-action '(bongo-progressive-playback-mode)))
+ ((equal n '(4))
+ (bongo-progressive-playback-mode))
+ ((consp n)
+ (user-error "This prefix argument %S is not supported."
+ n))
+ ((< (prefix-numeric-value n) 0)
+ (durand-bongo-play-previous-or-last (- (prefix-numeric-value n))))
+ (t
+ (with-imminent-bongo-player-start
+ (bongo-stop)
+ (when bongo-mark-played-tracks
+ (bongo-mark-current-track-line-as-played))
+ (durand-bongo-next-or-first n)
+ (bongo-start)))))
+
+(defun durand-bongo-play-previous-or-last (&optional n)
+ "Start playing the previous track in the nearest playlist buffer.
+If there is no previous track to play, play the last track.
+
+With numerical prefix argument N, skip that many tracks.
+
+With \\[universal-argument] as prefix argument, just switch to
+regressive playback mode.
+
+With \\[universal-argument] \\[universal-argument] as prefix
+argument, insert an action track at point."
+ (interactive "P")
+ (cond
+ ((equal n '(16))
+ (bongo-insert-line 'bongo-action '(bongo-regressive-playback-mode)))
+ ((equal n '(4))
+ (bongo-regressive-playback-mode))
+ ((consp n)
+ (user-error "This prefix argument %S is not supported."
+ n))
+ ((< (prefix-numeric-value n) 0)
+ (durand-bongo-play-next-or-first (- (prefix-numeric-value n))))
+ (t
+ (with-imminent-bongo-player-start
+ (bongo-stop)
+ (when bongo-mark-played-tracks
+ (bongo-mark-current-track-line-as-played))
+ (durand-bongo-previous-or-last n)
+ (bongo-start)))))
+
+(defun durand-bongo-play-first ()
+ "Play the first song."
+ (interactive)
+ (with-bongo-playlist-buffer
+ (with-imminent-bongo-player-start
+ (bongo-stop)
+ (when bongo-mark-played-tracks
+ (bongo-mark-current-track-line-as-played))
+ (goto-char (or (bongo-point-at-first-track-line)
+ (prog1 (point)
+ (message "No track in the playlist buffer."))))
+ (bongo-set-current-track-position)
+ (bongo-start))))
+
+(defun durand-bongo-play-last ()
+ "Play the last song."
+ (interactive)
+ (with-bongo-playlist-buffer
+ (with-imminent-bongo-player-start
+ (bongo-stop)
+ (when bongo-mark-played-tracks
+ (bongo-mark-current-track-line-as-played))
+ (goto-char (or (bongo-point-at-last-track-line)
+ (prog1 (point)
+ (message "No track in the playlist buffer."))))
+ (bongo-set-current-track-position)
+ (bongo-start))))
+
+;;; Bongo info path
+
+;; I place bongo in a custom path, so I need to provide the path to
+;; its info files.
+
+(eval-after-load 'info
+ '(cond
+ ((null Info-directory-list)
+ (setq Info-directory-list
+ (append Info-default-directory-list
+ (list (expand-file-name "bongo/" package-dir)))))
+ ((add-to-list 'Info-directory-list
+ (expand-file-name "bongo/" package-dir)))))
+
+;;; Custom music directory
+
+;; This is not needed anymore, but is still preserved, in case of
+;; future needs.
+
+(defvar durand-bongo-music-dir nil
+ "Directories to store my songs.
+This is used since my music directories used to contain symbolic
+links.")
+
+(setq durand-bongo-music-dir
+ (cons bongo-default-directory
+ (delq
+ nil
+ (mapcar
+ (lambda (file)
+ (cond
+ ((let ((attr (car (file-attributes file))))
+ (and (stringp attr)
+ (file-directory-p attr)))
+ file)))
+ (directory-files-recursively
+ bongo-default-directory
+ ".*" t nil t)))))
+
+;;; Key bindings
+
+(define-key global-map (vector 3 ?b) #'bongo)
+
+;; to avoid repetitions
+(let ((map bongo-playlist-mode-map))
+ (define-key map [?n] #'bongo-next-object-line)
+ (define-key map [?p] #'bongo-previous-object-line)
+ (define-key map [?j] #'durand-bongo-save-playlist)
+ (define-key map [tab] #'bongo-show)
+ (define-key map [?\C-c ?n] #'durand-bongo-play-next-or-first)
+ (define-key map [?\C-c ?p] #'durand-bongo-play-previous-or-last)
+ (define-key map [?\C-d] #'prot/bongo-clear-playlist-and-stop)
+ (define-key map [?x] #'durand-bongo-stop-and-exit)
+ (define-key map [?I] #'durand-bongo-insert-delete-playlist))
+
+(let ((map bongo-dired-library-mode-map))
+ (define-key map [?\C-c ?n] #'durand-bongo-play-next-or-first)
+ (define-key map [?\C-c ?p] #'durand-bongo-play-previous-or-last)
+ (define-key map [C-return]
+ #'prot/bongo-library-insert-and-play-random))
+
+(define-key bongo-seek-mode-map [?t] 'bongo-seek-to)
+
+;;; dired library mode
+
+(defun durand-bongo-dired-library ()
+ "Set `bongo-dired-library-mode' when accessing \
+`bongo-default-directory'.
+This is meant to be hooked to `dired-mode'.
+
+Upon activation, the directory and all its sub-directories become
+a valid library buffer for Bongo, from where we can, among
+others, add tracks to playlists.
+
+The added benefit is that Dired will continue to behave as
+normal, making this a superior alternative to a purpose-specific
+library buffer.
+
+Adapted from Protesilaos' dotemacs."
+ (cond
+ ((let ((temp durand-bongo-music-dir) dir found)
+ (while (and (consp temp) (not found))
+ (setq dir (car temp)
+ temp (cdr temp))
+ (cond
+ ((file-in-directory-p default-directory dir)
+ (setq found t))))
+ found)
+ (bongo-dired-library-mode 1))))
+
+(add-hook 'dired-mode-hook #'durand-bongo-dired-library)
+
+(defun contrib/bongo-add-dired-files ()
+ "Add marked files inside of a Dired buffer to the Bongo library."
+ (interactive)
+ (require 'dired)
+ (let (file-point file files)
+ (dired-map-over-marks
+ (setq file-point (dired-move-to-filename)
+ file (dired-get-filename)
+ files (append files (list file)))
+ nil t)
+ (ignore file-point)
+ (with-current-buffer (bongo-playlist-buffer)
+ (let ((beg (point)))
+ (mapc 'bongo-insert-file files)
+ (bongo-maybe-join-inserted-tracks beg (point))))))
+
+;;; Clear playlist and stop
+
+(defun prot/bongo-clear-playlist-and-stop ()
+ "Stop playback and clear the entire `bongo' playlist buffer.
+Contrary to the standard `bongo-erase-buffer', this also removes
+the currently-playing track.
+
+Modified by Durand so that this also runs while not in a bongo
+buffer."
+ (interactive)
+ (with-bongo-playlist-buffer (bongo-stop) (bongo-erase-buffer)))
+
+;;; Random playing
+
+(defun prot/bongo-play-random ()
+ "Play a random track with `bongo'."
+ (interactive)
+ (when (or (bongo-playlist-buffer-p)
+ (bongo-library-buffer-p))
+ (bongo-play-random)
+ (setf bongo-next-action #'durand-bongo-play-next-or-first)))
+
+(defun prot/bongo-library-insert-and-play-random ()
+ "Add directory tree or marked items to the `bongo' playlist.
+Create the playlist buffer if necessary.
+
+This is meant to work while inside a `dired' buffer that doubles
+as a library buffer (see `prot/bongo-dired-library')."
+ (interactive)
+ (when (bongo-library-buffer-p)
+ (unless (bongo-playlist-buffer-p)
+ (bongo-playlist-buffer))
+ (contrib/bongo-add-dired-files)))
+
+;;; Playlist
+
+;;;; Insert or delete a playlist
+
+(defun durand-bongo-insert-delete-playlist ()
+ "Insert or delete a `bongo' playlist.
+The files are stored in a predetermined path inside the Music
+directory. Upon insertion, playback starts immediately, in
+accordance with `prot/bongo-play-random'.
+
+Adapted from Protesilaos' dotemacs by Durand."
+ (interactive)
+ (let* ((path (file-name-as-directory
+ (expand-file-name
+ "playlists"
+ bongo-default-directory)))
+ (dotless directory-files-no-dot-files-regexp)
+ (playlist (directory-files path t dotless)))
+ (with-bongo-playlist-buffer
+ (bongo-insert-playlist-contents
+ (completing-read "Select playlist: " playlist nil t path)))))
+
+;;;; Save playlist
+
+(defvar durand-bongo-save-playlist-hist nil
+ "A variable that holds the history values for saving playlist \
+names.")
+
+(defun durand-bongo-save-playlist ()
+ "Save the current playlist into a file."
+ (interactive)
+ (let ((playlist-name
+ (expand-file-name
+ (read-string
+ "Playlist name:" nil 'durand-bongo-save-playlist-hist)
+ (expand-file-name "playlists"
+ bongo-default-directory)))
+ (append-p (y-or-n-p "Append? "))
+ files end)
+ (with-current-buffer bongo-default-playlist-buffer-name
+ (save-excursion
+ (goto-char (or (bongo-point-at-first-track-line)
+ (user-error
+ "No tracks in the playlist!")))
+ (push (bongo-line-file-name) files)
+ (while (not end)
+ (if-let ((next-pos (bongo-point-at-next-track-line)))
+ (progn
+ (goto-char next-pos)
+ (push (bongo-line-file-name) files))
+ (setf end t)))))
+ (with-temp-buffer
+ (cl-loop for file in files
+ do (progn
+ (insert file)
+ (newline)))
+ (write-region nil nil playlist-name append-p))))
+
+;;; Fix remote syntax
+
+;; REVIEW:
+;;
+;; This is oft not overriding the original function, to the effect
+;; that the first time bongo starts, this function does not work, and
+;; I have to define this function again, in order to play the
+;; music. So I decided to make this into an overriding advice.
+
+(defun durand-bongo-compose-remote-option (socket-file)
+ "Get the command line argument for starting mpv's remote interface \
+at SOCKET-FILE.
+
+This has to be fixed for mpv to work, since its argument parsing
+convention is changed."
+ (when (equal bongo-mpv-remote-option 'unknown)
+ (setq bongo-mpv-remote-option (bongo--mpv-get-remote-option)))
+ (list (concat bongo-mpv-remote-option "=" socket-file)))
+
+(advice-add #'bongo-compose-remote-option :override 'durand-bongo-compose-remote-option)
+
+;;; Custom seek function
+
+;; NOTE: Fix a bug: there is no face called `modeline'; it should be
+;; `mode-line'.
+(defun durand-bongo-seek ()
+ "Interactively seek in the current Bongo track."
+ ;; :override 'bongo-seek
+ (interactive)
+ (setq bongo-seek-buffer (get-buffer-create "*Bongo Seek*"))
+ (if bongo-seek-electric-mode
+ (unwind-protect
+ (save-window-excursion
+ (require 'electric)
+ (message nil)
+ (let ((garbage-collection-messages nil)
+ (bongo-seeking-electrically t))
+ (ignore bongo-seeking-electrically)
+ (set-window-buffer
+ (minibuffer-window) bongo-seek-buffer)
+ (select-window (minibuffer-window))
+ (let ((old-local-map (current-local-map))
+ (old-global-map (current-global-map)))
+ (use-local-map nil)
+ (use-global-map bongo-seek-mode-map)
+ (setq major-mode 'bongo-seek-mode)
+ (unwind-protect
+ (progn
+ (bongo-seek-redisplay)
+ (run-hooks 'bongo-seek-mode-hook)
+ (catch 'bongo-seek-done
+ (Electric-command-loop
+ 'bongo-seek-done
+ ;; Avoid `noprompt' due to
+ ;; a bug in electric.el.
+ '(lambda () 'noprompt)
+ nil
+ (lambda (_x _y) (bongo-seek-redisplay)))))
+ (use-local-map old-local-map)
+ (use-global-map old-global-map)))))
+ (when bongo-seek-buffer
+ (kill-buffer bongo-seek-buffer)
+ (setq bongo-seek-buffer nil)))
+ (cond
+ ((null (get-buffer-window bongo-seek-buffer))
+ (let ((window-min-height 2)
+ (split-window-keep-point nil))
+ (select-window
+ (split-window-vertically
+ (if (and (fboundp 'face-attr-construct)
+ ;; NOTE: bug occurs here.
+ (plist-get (face-attr-construct 'mode-line) :box))
+ -3 -2)))
+ (switch-to-buffer bongo-seek-buffer)))
+ ((not (eq (current-buffer) bongo-seek-buffer))
+ (select-window (get-buffer-window bongo-seek-buffer))))
+ (bongo-seek-mode)
+ (setq buffer-read-only t)
+ (setq cursor-type nil)
+ (bongo-seek-redisplay)
+ (durand-bongo-seek-start-timer-maybe)))
+
+(advice-add #'bongo-seek :override #'durand-bongo-seek)
+
+;;; Go to the bongo-default-directory when no track is under point
+
+(defun durand-bongo-dired-line (&optional point)
+ "Open a Dired buffer containing the track at POINT.
+Modified by Durand."
+ (interactive)
+ ;;; NOTE: The body-form inside condition-case is the original
+ ;;; function body.
+ (condition-case nil
+ (save-excursion
+ (bongo-goto-point point)
+ (bongo-snap-to-object-line)
+ (dired (file-name-directory
+ (save-excursion
+ (while (bongo-header-line-p)
+ (bongo-down-section))
+ (if (bongo-local-file-track-line-p)
+ (bongo-line-file-name)
+ (error "No local file track here")))))
+ (bongo-dired-library-mode 1))
+ ('error (dired bongo-default-directory))))
+
+(advice-add 'bongo-dired-line :override #'durand-bongo-dired-line)
+
+;;; Jump to playlist buffer immediately
+
+(defun durand-bongo-buffer ()
+ "Return the buffer (bongo-playlist-buffer)."
+ (interactive)
+ (bongo-playlist-buffer))
+
+(advice-add #'bongo-buffer :override #'durand-bongo-buffer)
+
+;;; Some navigation functions
+
+;; These are mostly deprecated, but still kept here.
+
+(defun durand-bongo-next-song (&optional n)
+ "Play the next N song."
+ (interactive "p")
+ (with-bongo-playlist-buffer
+ (durand-bongo-play-next-or-first n)))
+
+(defun durand-bongo-previous-song (&optional n)
+ "Play the previous N song."
+ (interactive "p")
+ (with-bongo-playlist-buffer
+ (durand-bongo-play-previous-or-last n)))
+
+(defun durand-bongo-seek-anywhere ()
+ "A wrapper around `durand-bongo-seek'."
+ (interactive)
+ (with-bongo-playlist-buffer
+ (durand-bongo-seek)
+ (setf durand-bongo-hydra-volume-return-p t)))
+
+(defun bongo-seek-forward-5 (&optional n)
+ "Seek 5 N seconds forward in the currently playing track."
+ (interactive "p")
+ (bongo-seek-forward (* 5 (or n 1))))
+
+(defun bongo-seek-backward-5 (&optional n)
+ "Seek 5 N seconds backward in the currently playing track."
+ (interactive "p")
+ (bongo-seek-backward (* 5 (or n 1))))
+
+(defun durand-bongo-kill-line ()
+ "Kill the currently playing bongo line."
+ (interactive)
+ (with-bongo-playlist-buffer
+ (bongo-recenter)
+ (bongo-kill-line)))
+
+;;; Fix volume redisplay
+
+(defun durand-bongo-refresh-after-set-a (&rest _args)
+ "Refresh the display after we set the volume."
+ (volume-redisplay))
+
+(advice-add #'volume-set :after #'durand-bongo-refresh-after-set-a)
+
+;;; Custom default playlist buffer function
+
+(defun durand-bongo-default-playlist-buffer-a ()
+ "Don't insert text in the playlist buffer."
+ (or (get-buffer bongo-default-playlist-buffer-name)
+ (let ((buffer
+ (get-buffer-create bongo-default-playlist-buffer-name)))
+ (prog1 buffer
+ (with-current-buffer buffer
+ (bongo-playlist-mode))))))
+
+(advice-add #'bongo-default-playlist-buffer
+ :override #'durand-bongo-default-playlist-buffer-a)
+
+;;; Stop playing and exit and kill the buffer
+
+(defun durand-bongo-stop-and-exit ()
+ "Stop playing and exit and kill the buffer."
+ (interactive)
+ (bongo-stop)
+ (bongo-erase-buffer)
+ (cond
+ (bongo-player
+ (bongo-player-stop bongo-player)
+ (setq bongo-player nil)))
+ (kill-buffer))
+
+;;; Improved bongo-mpv backend
+
+;;;; Disable the tick timer as soon as possible
+
+(defun durand-bongo-mpv-player-tick (player)
+ "Fetch metadata and length of track of PLAYER \
+ if not fetched already.
+Also fetch the time-pos initially.
+
+Afterwards just stop the annoying timer."
+ (condition-case nil
+ (progn
+ (bongo--run-mpv-command player "time-pos" "get_property" "time-pos")
+ (let ((timer (bongo-player-get player 'timer)))
+ (cond
+ ;; ((or (not (bongo-player-running-p player))
+ ;; (and (bongo-player-get player 'socket)
+ ;; (not (equal (process-status (bongo-player-get player 'socket))
+ ;; 'open))))
+ ;; (bongo-mpv-player-stop-timer player))
+ ((null (bongo-player-total-time player))
+ (bongo--run-mpv-command player "duration" "get_property" "duration"))
+ ;; ((null (bongo-player-get player 'metadata-fetched))
+ ;; (bongo--run-mpv-command player "metadata" "get_property" "metadata"))
+ (t (bongo-mpv-player-stop-timer player)))))
+ (error (bongo-mpv-player-stop-timer player))))
+
+;;;###autoload
+(advice-add #'bongo-mpv-player-tick :override #'durand-bongo-mpv-player-tick)
+
+;;;; get-elapsed-time
+
+;; Since the tick is stopped, we are not constantly and furiously
+;; updating the elapsed time of mpv now. This is exactly what we
+;; want. But there is a problem: when seeking we don't know the
+;; time-pos anymore. So we fix this by manually requesting the
+;; time-pos in seeking.
+
+(defun durand-bongo-seek-stop-timer (player)
+ "Stop seek timer for the PLAYER."
+ (let ((timer (bongo-player-get player 'seek-timer)))
+ (cond
+ (timer
+ (cancel-timer timer)
+ (bongo-player-put player 'seek-timer nil)))))
+
+(defun durand-bongo-seek-tick (player)
+ "Tick when seeking."
+ (if (or (null bongo-seek-buffer)
+ (not (bongo-player-running-p player))
+ (and (bongo-player-get player 'socket)
+ (not (equal (process-status (bongo-player-get player 'socket))
+ 'open))))
+ (durand-bongo-seek-stop-timer player)
+ (bongo--run-mpv-command player "time-pos" "get_property" "time-pos")
+ (when (null (bongo-player-total-time player))
+ (bongo--run-mpv-command player "duration" "get_property" "duration"))
+ (unless (bongo-player-get player 'metadata-fetched)
+ (bongo--run-mpv-command player "metadata" "get_property" "metadata"))
+ (bongo-seek-redisplay)))
+
+(defun durand-bongo-seek-start-timer (player)
+ "Start ticking when seeking."
+ (durand-bongo-seek-stop-timer player)
+ (let ((timer (run-with-timer bongo-mpv-initialization-period
+ (bongo-player-get
+ player 'time-update-delay-after-seek)
+ 'durand-bongo-seek-tick
+ player)))
+ (bongo-player-put player 'seek-timer timer)))
+
+(defun durand-bongo-seek-start-timer-maybe (&rest _args)
+ "Start the seek timer for mpv backends."
+ (with-bongo-playlist-buffer
+ (cond
+ ((and bongo-player
+ (eq (car bongo-player)
+ 'mpv))
+ (durand-bongo-seek-start-timer bongo-player)))))
+
+;;; replace names in playlist files
+
+(defun durand-bongo-replace-name-in-playlist-files (old new)
+ "Replace names in the playlist files containing OLD by the name in \
+NEW subdirectory.
+If one moves songs containing the string NAME to the subdirectory
+NAME in its original directory, where NAME is the name of the
+singer, then running this function with parameters both equal to NAME
+will do the right renaming."
+ (interactive "sOld name: \nsNew name: ")
+ (let ((files (directory-files
+ (expand-file-name
+ "playlists" bongo-default-directory)
+ nil "m3u$" t)))
+ (mapc
+ (lambda (file)
+ (with-temp-buffer
+ (insert-file-contents file)
+ (goto-char (point-min))
+ (cond
+ ((save-excursion (re-search-forward old nil t))
+ (while (re-search-forward old nil t)
+ (let* ((file-name
+ (buffer-substring-no-properties
+ (line-beginning-position) (line-end-position)))
+ (expanded
+ (expand-file-name
+ (file-name-nondirectory file-name)
+ (expand-file-name
+ new (file-name-directory file-name))))
+ (po (line-beginning-position)))
+ (delete-region (line-beginning-position) (line-end-position))
+ (goto-char po)
+ (insert expanded)
+ (goto-char (+ po (length expanded)))))
+ (write-region nil nil file)))))
+ files)))
+
+;;; Play subtitles synchronously
+
+;; Sometimes I want to play the subtitles as the song plays.
+
+;;;; Subtitle buffer
+
+(defvar durand-bongo-sub-buffer-name "*Subtitles*"
+ "The name of the buffer for playing subtitles.")
+
+(defvar durand-bongo-sub-buffer nil
+ "The buffer for playing subtitles.")
+
+;;;; Start timer
+
+(defun durand-bongo-sub-start-timer (player)
+ "Start ticking when playing subtitles."
+ (durand-bongo-sub-stop-timer player)
+ (let ((timer (run-with-timer bongo-mpv-initialization-period
+ (bongo-player-get
+ player 'time-update-delay-after-seek)
+ 'durand-bongo-sub-tick
+ player)))
+ (bongo-player-put player 'sub-timer timer)))
+
+;;;; Start the timer when appropriate
+
+(defun durand-bongo-sub-start-timer-maybe (&rest _args)
+ "Start the subtitle timer for the mpv backend."
+ (with-bongo-playlist-buffer
+ (cond
+ ((and bongo-player (eq (car bongo-player) 'mpv))
+ (durand-bongo-sub-start-timer bongo-player)))))
+
+;;;; Stop timer
+
+(defun durand-bongo-sub-stop-timer (player)
+ "Stop subtitle timer for the PLAYER."
+ (let ((timer (bongo-player-get player 'sub-timer)))
+ (cond
+ (timer
+ (cancel-timer timer)
+ (bongo-player-put player 'sub-timer nil)))))
+
+;;;; Tick timer
+
+(defun durand-bongo-sub-tick (player)
+ "Tick when playing subtitles."
+ (if (or (null durand-bongo-sub-buffer)
+ (not (bongo-player-running-p player))
+ (and (bongo-player-get player 'socket)
+ (not (equal
+ (process-status
+ (bongo-player-get player 'socket))
+ 'open))))
+ (durand-bongo-sub-stop-timer player)
+ (bongo--run-mpv-command
+ player "sub-pos" "get_property" "time-pos")
+ (bongo-sub-redisplay)))
+
+;;;; Mode for displaying subtitles
+
+(define-derived-mode durand-bongo-sub-mode special-mode "Bongo Sub"
+ "Major mode for displaying subtitles in accordance with Bongo."
+ (set 'buffer-undo-list t)
+ (set 'cursor-type nil))
+
+(define-key durand-bongo-sub-mode-map (vector ?s)
+ #'durand-bongo-sub-stop)
+
+;;;; Clean up after exiting
+
+(defun durand-bongo-sub-stop ()
+ "Stop everything related to displaying subtitles."
+ (interactive)
+ (durand-bongo-sub-stop-timer bongo-player)
+ (with-bongo-playlist-buffer
+ (bongo-player-put bongo-player 'sub-time nil)
+ (cond
+ ((buffer-live-p
+ (bongo-player-get bongo-player 'sub-file-buffer))
+ (kill-buffer
+ (bongo-player-get bongo-player 'sub-file-buffer))
+ (bongo-player-put bongo-player 'sub-file-buffer nil)))
+ (cond
+ ((buffer-live-p durand-bongo-sub-buffer)
+ (kill-buffer durand-bongo-sub-buffer)
+ (setq durand-bongo-sub-buffer nil)))))
+
+;;;; Find the subtitles file and load into a buffer
+
+(require 'rx)
+
+(defun durand-bongo-sub-prepare-file-buffer ()
+ "Prepare the subtitles file buffer for the currently playing song."
+ (let* ((file-name (or (bongo-player-get bongo-player 'file-name)
+ (user-error "No file found")))
+ (file-name-se (file-name-sans-extension file-name))
+ (sub-file-name (format "%s.srt" file-name-se))
+ (sub-exists-p (file-exists-p sub-file-name))
+ (sub-file-buffer
+ (bongo-player-get bongo-player 'sub-file-buffer)))
+ (cond
+ ((not sub-exists-p)
+ (user-error "No subtitles file found for the current song.")))
+ (cond
+ ((not (buffer-live-p sub-file-buffer))
+ (setq sub-file-buffer (get-buffer-create " * Bongo Sub File*"))
+ (bongo-player-put bongo-player
+ 'sub-file-buffer sub-file-buffer)))
+ (with-current-buffer sub-file-buffer
+ ;; The first line is the current song's file name.
+ (goto-char (point-min))
+ (cond
+ ((not (looking-at-p
+ (rx-to-string (list 'seq file-name 'eol) t)))
+ (erase-buffer)
+ (insert file-name ?\n)
+ (insert-file-contents sub-file-name))))
+ sub-file-buffer))
+
+;;;; My custom filter
+
+;; To handle subtitle events
+
+(defun durand-bongo--mpv-socket-filter (process output)
+ "Filter for socket connection with mpv.
+
+PROCESS is the socket which returned the OUTPUT."
+ (let ((player (process-get process 'bongo-player)))
+ (dolist (parsed-response (mapcar #'json-read-from-string
+ (split-string output "\n" t)))
+ ;; Events are treated differently from normal responses
+ (if (assoc 'event parsed-response)
+ (pcase (bongo-alist-get parsed-response 'event)
+ (`"pause" (progn
+ (bongo-player-put player 'paused t)
+ (bongo-player-paused/resumed player)))
+ (`"unpause" (progn
+ (bongo-player-put player 'paused nil)
+ (bongo-player-paused/resumed player))))
+ ;; Use request-id to identify the type of response
+ (pcase (bongo-alist-get parsed-response 'request_id)
+ (`"sub-pos" (progn
+ ;; (message "data = %S" (bongo-alist-get parsed-response 'data))
+ (durand-bongo-update-sub-time
+ player
+ (bongo-alist-get parsed-response 'data))
+ ))
+ (`"time-pos" (progn
+ (bongo-player-update-elapsed-time player
+ (bongo-alist-get parsed-response
+ 'data))
+ (bongo-player-times-changed player)))
+ (`"duration" (progn
+ (bongo-player-update-total-time player
+ (bongo-alist-get parsed-response
+ 'data))
+ (bongo-player-times-changed player)))
+ (`"metadata" (let* ((data (bongo-alist-get parsed-response 'data))
+ (album (bongo-alist-get data 'album))
+ (title (bongo-alist-get data 'title))
+ (genre (bongo-alist-get data 'genre)))
+ (bongo-player-put player 'metadata-fetched t)
+ (when album
+ (bongo-player-put player 'stream-name album))
+ (when title
+ (bongo-player-put player 'stream-part-title title))
+ (when genre
+ (bongo-player-put player 'stream-genre genre))
+ (when (or album title genre)
+ (bongo-player-metadata-changed player)))))))))
+
+(advice-add #'bongo--mpv-socket-filter :override
+ #'durand-bongo--mpv-socket-filter)
+
+;;;; Update subtime
+
+(defun durand-bongo-update-sub-time (player elapsed-time)
+ "Set PLAYER's `sub-time' property to ELAPSED-TIME,
+unless PLAYER's last seek happened less than N seconds ago, where N
+is the value of PLAYER's `time-update-delay-after-seek' property."
+ (let ((delay (bongo-player-get player 'time-update-delay-after-seek)))
+ (when (or (null delay) (zerop delay)
+ (let ((time (bongo-player-get player 'last-sub-time)))
+ (or (null time)
+ (time-less-p (seconds-to-time delay)
+ (subtract-time (current-time) time)))))
+ (bongo-player-put player 'sub-time elapsed-time))))
+
+;;;; Redisplay
+
+(defvar bongo-sub-redisplaying nil
+ "Non-nil in the dynamic scope of `bongo-sub-redisplay'.")
+
+(defun bongo-sub-redisplay ()
+ "Update the Bongo Subtitle buffer to reflect the current time."
+ (interactive)
+ (unless bongo-sub-redisplaying
+ (let ((bongo-sub-redisplaying t)
+ (sub (or (bongo-sub-status-string (window-width)) "")))
+ (let ((inhibit-read-only t))
+ (set-buffer durand-bongo-sub-buffer)
+ (delete-region (point-min) (point-max))
+ (insert sub)))))
+
+;;;; Display string
+
+(defun durand-convert-sub-second (time sub)
+ "Convert TIME to add sub-seconds time given by SUB.
+TIME must be a list of two to four elements."
+ (cond
+ ((and (listp time)
+ (<= (length time) 4)
+ (<= 2 (length time))))
+ ((error "Wrong time: %S" time)))
+ ;; high
+ (let ((th (car time))
+ (tl (cadr time))
+ (tu (cond
+ ((>= (length time) 3) (nth 3 time))
+ (0)))
+ (tp (cond
+ ((>= (length time) 4) (nth 4 time))
+ (0)))
+ (sub-digit-n
+ (let ((n sub)
+ (count 0))
+ (while (> n 0)
+ (setq count (1+ count))
+ (setq n (/ n 10)))
+ count)))
+ (cond
+ ((<= sub-digit-n 6)
+ (setq tu (+ tu sub)))
+ ((<= sub-digit-n 12)
+ (setq tu (+ tu (/ sub (expt 10 6))))
+ (setq tp (+ tp (% sub (expt 10 6)))))
+ ((error "Wrong sub: %S" sub)))
+ (append
+ (list th tl)
+ (delq
+ nil
+ (mapcar
+ (lambda (ele)
+ (cond ((/= ele 0) ele)))
+ (list tu tp))))))
+
+(defun bongo-sub-status-string (width)
+ "Return the subtitle filled to WIDTH."
+ (cond
+ ((and (bongo-playing-p)
+ (bongo-player-get bongo-player 'sub-time)
+ (bongo-player-get bongo-player 'sub-file-buffer))
+ (let ((fb (bongo-player-get bongo-player 'sub-file-buffer))
+ (time (bongo-player-get bongo-player 'sub-time))
+ found start start-sub end end-sub)
+ (setq
+ time
+ (let ((fake-time
+ (parse-time-string
+ (format-seconds "%.2h:%.2m:%.2s" time))))
+ (encode-time
+ (append
+ (mapcar (lambda (ele) (or ele 0))
+ (durand-take 6 fake-time))
+ (nthcdr 6 fake-time)))))
+ (with-current-buffer fb
+ (goto-char (point-min))
+ (save-match-data
+ (while (and
+ (not found)
+ (re-search-forward
+ (rx bol
+ (group
+ (1+ digit) ":"
+ (1+ digit) ":" (1+ digit)
+ (zero-or-one
+ "," (1+ digit)))
+ (zero-or-more " ")
+ "-->"
+ (zero-or-more " ")
+ (group
+ (1+ digit) ":"
+ (1+ digit) ":" (1+ digit)
+ (zero-or-one
+ "," (1+ digit)))
+ eol)
+ nil t))
+ (setq start (match-string 1))
+ (setq end (match-string 2))
+ (cond
+ ((string-match ",\\([[:digit:]]+\\)$" start)
+ (setq start-sub (match-string 1 start))
+ (setq
+ start
+ (durand-convert-sub-second
+ (let ((fake-time (parse-time-string
+ (replace-regexp-in-string
+ ",.*$" "" start))))
+ (encode-time
+ (append
+ (mapcar (lambda (ele) (or ele 0))
+ (durand-take 6 fake-time))
+ (nthcdr 6 fake-time))))
+ (string-to-number start-sub))))
+ ((setq start (parse-time-string start))))
+ (cond
+ ((string-match ",\\([[:digit:]]+\\)$" end)
+ (setq end-sub (match-string 1 end))
+ (setq
+ end
+ (durand-convert-sub-second
+ (let ((fake-time (parse-time-string
+ (replace-regexp-in-string
+ ",.*$" "" end))))
+ (encode-time
+ (append
+ (mapcar (lambda (ele) (or ele 0))
+ (durand-take 6 fake-time))
+ (nthcdr 6 fake-time))))
+ (string-to-number end-sub))))
+ ((setq end (parse-time-string end))))
+ (cond
+ ((and (time-less-p start time)
+ (time-less-p time end))
+ (setq found t))))
+ (cond
+ (found
+ (save-match-data
+ (save-excursion
+ (setq
+ end
+ (cond
+ ((re-search-forward (rx bol (zero-or-more space) eol) nil t)
+ (point))
+ ((point-max)))))
+ (buffer-substring-no-properties
+ (1+ (point)) end))))))))))
+
+;; Normally we should do this, but I override the function by my
+;; custom function, so I can just call this function there.
+
+;; (advice-add #'bongo-seek :after #'durand-bongo-seek-start-timer-maybe)
+
+;;;; Subtitle command
+
+(defun durand-bongo-sub ()
+ "Display subtitles in a separate buffer."
+ (setq durand-bongo-sub-buffer
+ (get-buffer-create durand-bongo-sub-buffer-name))
+ (cond
+ ((null (get-buffer-window durand-bongo-sub-buffer))
+ (let ((durand-window-max-height 0.01))
+ (display-buffer
+ durand-bongo-sub-buffer
+ (list (list #'display-buffer-reuse-window
+ #'display-buffer-in-side-window)
+ (cons 'side 'bottom)
+ (cons 'window-height
+ #'durand-fit-window-to-buffer-with-max))))))
+ (with-bongo-playlist-buffer (durand-bongo-sub-prepare-file-buffer))
+ (set-buffer durand-bongo-sub-buffer)
+ (durand-bongo-sub-mode)
+ (bongo-sub-redisplay)
+ (durand-bongo-sub-start-timer-maybe))
+
+;;; Edit subtitles
+
+;; It would be too cumbersome if I cannot edit subtitles with Bongo.
+
+;; On a second thought, I think the library 'subed' is good enough.
+
+;;;; Major mode
+
+(define-derived-mode durand-bongo-sub-edit-mode text-mode "BOSE"
+ "BOngo Subtitles Edit mode
+
+The subtitles are assumed to be for the song that Bongo is
+currently playing. To change the audio or the video, change it
+in Bongo.
+
+Moreover, the audio and video playing is completely handled by
+Bongo, and this mode only acts as a frontend of some sort.
+
+\\{durand-bongo-sub-edit-mode-map}"
+ )
+
+;;;; Regular expression for time specification
+
+(defvar durand-bongo-sub-time-regex
+ (rx (group
+ (= 2 digit) ?:
+ (= 2 digit) ?:
+ (= 2 digit) ?,
+ (= 3 digit))
+ (zero-or-more space)
+ "-->"
+ (zero-or-more space)
+ (group
+ (= 2 digit) ?:
+ (= 2 digit) ?:
+ (= 2 digit) ?,
+ (= 3 digit))
+ (zero-or-more space))
+ "A regular expression that matches the time specification in a
+subtitle file, in the SubRip format.")
+
+;;;; Regular expression for file extensions
+
+(defvar durand-bongo-sub-exts
+ (rx-to-string
+ (cons
+ 'or
+ (append
+ bongo-audio-file-name-extensions
+ bongo-video-file-name-extensions)))
+ "A regular expression that matches the file extensions recognized
+by Bongo.")
+
+;;;; Key bindings
+
+;; (let ((map durand-bongo-sub-edit-mode-map))
+;; (define-key map (vector 3 )))
+
+;;;; Insert subtitle
+
+(defun durand-bongo-sub-insert ()
+ "Insert a section of subtitles."
+ (interactive)
+ (let ((end (point))
+ (num 0))
+ (search-forward "\n\n" nil 'go)
+ (save-excursion
+ (goto-char (point-min))
+ (while (re-search-forward
+ (concat
+ (rx-to-string
+ '(seq bol
+ (one-or-more digit) ?\n)
+ t)
+ durand-bongo-sub-time-regex)
+ end t)
+ (setq num (1+ num))))
+ (newline)
+ (newline)
+ (insert (format "%d" (1+ num))
+ ?\n)))
+
+;;; Volume package
+
+(use-package "volume" 'volume)
+
+(define-key volume-mode-map [?s] #'volume-set)
+
+(defvar durand-view-map)
+
+(define-key durand-view-map (vector ?V) #'volume)
+
+;;; Bonmark
+
+(use-package "bonmark" 'bonmark)
+
+;;; search and insert
+
+(defun durand-bongo-search-insert ()
+ "Search a song and insert it in the playlist buffer."
+ (interactive)
+ (let* ((songs
+ (mapcar
+ (lambda (file)
+ (cons
+ (file-name-nondirectory file)
+ (file-name-directory file)))
+ (flatten-tree
+ (mapcar
+ (lambda (dir)
+ (directory-files-recursively
+ dir
+ (rx-to-string
+ '(seq "." (or "mp3" "mkv" "webm" "m4a") eos)
+ t)
+ nil nil t))
+ durand-bongo-music-dir))))
+ (choice
+ (completing-read
+ "Search and insert: " songs
+ nil t))
+ (choice
+ (assoc choice songs #'equal))
+ (choice
+ (expand-file-name
+ (car choice)
+ (cdr choice))))
+ (bongo-insert-file choice)))
+
+(define-key bongo-playlist-mode-map (vector ?/)
+ #'durand-bongo-search-insert)
+
+(provide 'durand-bongo)
+;;; durand-bongo.el ends here