diff options
Diffstat (limited to 'durand-bongo.el')
-rw-r--r-- | durand-bongo.el | 1202 |
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 |