calibre.el/calibre-edit.el

262 lines
11 KiB
EmacsLisp

;;; calibre-edit.el --- Edit Book metadata -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Free Software Foundation, Inc.
;; This file is part of calibre.el.
;; Author: Kjartan Oli Agustsson <kjartanoli@disroot.org>
;; calibre.el 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.
;; calibre.el 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 calibre.el. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This file contains the code required to edit the metadata of books
;; in a calibre library.
;;; Code:
(require 'compat) ; for defvar-keymap
(require 'wid-edit)
(require 'calibre-widgets)
(require 'calibre-core)
(require 'calibre-util)
(require 'calibre-exec)
(defvar calibre-edit--edited-books nil
"A list containing the original copies of edited books.")
;; Declare these variables to prevent free variable warnings
(defvar-local calibre-edit--book nil
"The book being edited in the current buffer.")
(defvar-local calibre-edit--title nil
"The title widget in the current buffer.")
(defvar-local calibre-edit--authors nil
"The authors widget in the current buffer.")
(defvar-local calibre-edit--publisher nil
"The publisher widget in the current buffer.")
(defvar-local calibre-edit--series nil
"The series widget in the current buffer.")
(defvar-local calibre-edit--tags nil
"The tags widget in the current buffer.")
(defun calibre-edit-apply (&rest _)
"Apply any edits to the book in the current buffer."
(interactive)
(setf (calibre-book-title calibre-edit--book) (widget-value calibre-edit--title)
(calibre-book-authors calibre-edit--book) (widget-value calibre-edit--authors)
(calibre-book-publisher calibre-edit--book) (widget-value calibre-edit--publisher)
(calibre-book-series calibre-edit--book) (if (consp (widget-value calibre-edit--series))
(car (widget-value calibre-edit--series))
nil)
(calibre-book-series-index calibre-edit--book) (if (consp (widget-value calibre-edit--series))
(cdr (widget-value calibre-edit--series))
1)
(calibre-book-tags calibre-edit--book) (widget-value calibre-edit--tags))
(calibre-library--refresh)
(let ((book calibre-edit--book))
(with-current-buffer (get-buffer calibre-library-buffer)
(calibre-library--find-book book)
(tabulated-list-put-tag (char-to-string calibre-mod-marker)))))
(defun calibre-edit-abort (&rest _)
"Abort any changes made in the current buffer."
(interactive)
(let ((book calibre-edit--book))
(unless (calibre-edit--different-fields book (calibre-edit--find-original book))
(setf calibre-edit--edited-books (seq-remove (lambda (b)
(= (calibre-book-id b)
(calibre-book-id book)))
calibre-edit--edited-books))))
(quit-window t))
(defun calibre-edit-reset (&rest _)
"Undo any changes made during this editing session."
(interactive)
(calibre-edit-book calibre-edit--book))
(defun calibre-edit-confirm (&rest _)
"Apply any changes and exit."
(interactive)
(calibre-edit-apply)
(quit-window t))
(defvar-keymap calibre-edit-mode-map
:doc "Keymap for `calibre-edit-mode'."
:parent widget-keymap
"C-c C-c" #'calibre-edit-confirm
"C-c C-a" #'calibre-edit-apply
"C-c C-r" #'calibre-edit-reset
"C-c C-k" #'calibre-edit-abort)
(defvar-keymap calibre-edit-field-keymap
:doc "Keymap used inside editable fields in calibre edit buffers."
:parent widget-field-keymap
"C-c C-c" #'calibre-edit-confirm
"C-c C-a" #'calibre-edit-apply
"C-c C-r" #'calibre-edit-reset
"C-c C-k" #'calibre-edit-abort)
(define-derived-mode calibre-edit-mode nil "Calibre Edit"
:group 'calibre
(widget-put (get 'editable-field 'widget-type) :keymap calibre-edit-field-keymap))
(defun calibre-edit-book (book)
"Edit the metadata of BOOK."
(interactive (list (tabulated-list-get-id)) calibre-library-mode)
(unless (calibre-util-find-book book calibre-edit--edited-books)
(push (copy-calibre-book book) calibre-edit--edited-books))
(let ((buffer (calibre-edit--create-buffer book)))
(pop-to-buffer buffer)))
(defun calibre-edit--create-buffer (book)
"Create a buffer for editing BOOK.
Returns a buffer for editing BOOK, creating it if necessary."
(let ((buffer (get-buffer-create (format "*%s - Metadata*" (calibre-book-title book)))))
(with-current-buffer buffer
(kill-all-local-variables)
(let ((inhibit-read-only t))
(erase-buffer))
(remove-overlays)
(calibre-edit-mode)
(setf calibre-edit--book book)
(setq-local
header-line-format
(substitute-command-keys
"\\<calibre-edit-mode-map>Finish `\\[calibre-edit-confirm]', Apply `\\[calibre-edit-apply]', Reset `\\[calibre-edit-reset]', Cancel `\\[calibre-edit-abort]'"))
(setf calibre-edit--title
(widget-create 'string
:value (calibre-book-title book)
:tag "Title"
:doc "The title of the book."
:format "%t: %v%d"))
(widget-insert "\n")
(setf calibre-edit--authors
(widget-create 'repeat
:value (calibre-book-authors book)
:tag "Authors"
:doc "The authors of the book."
:format "%t:\n%v%i\n%d"
'calibre-author))
(widget-insert "\n")
(setf calibre-edit--publisher
(widget-create 'choice
:value (calibre-book-publisher book)
:tag "Publisher"
:doc "The publisher of the book."
:format "%[%t%]%v%d"
'(const :tag ": None" :menu-tag "None" nil)
'(calibre-publisher :tag "" :menu-tag "Publisher")))
(widget-insert "\n")
(setf calibre-edit--series
(widget-create 'choice
:value (if (calibre-book-series book)
(cons (calibre-book-series book)
(calibre-book-series-index book))
nil)
:tag "Series"
:format "%[%t%]%v%d"
'(const :tag ": None" :menu-tag "None" nil)
`(cons :tag ""
:menu-tag "Series"
(calibre-series :tag "Name"
:doc "The series the book is part of."
:format "%t: %v%d")
(number :tag "Index"
:doc "The book's position within its series"
:format "%t: %v%d"
:value ,(calibre-book-series-index book)))))
(widget-insert "\n")
(setf calibre-edit--tags
(widget-create 'repeat
:value (calibre-book-tags book)
:tag "Tags"
:doc "Tags associated with the book."
:format "%t:\n%v%i\n%d"
'calibre-tag))
(widget-insert "\n")
(widget-create 'push-button
:tag "Confirm"
:help-echo "Apply any changes and exit."
:notify (lambda (&rest _)
(calibre-edit-confirm)
(kill-buffer)))
(widget-insert " ")
(widget-create 'push-button
:tag "Apply"
:help-echo "Apply any changes."
:notify #'calibre-edit-apply)
(widget-insert " ")
(widget-create 'push-button
:tag "Reset"
:help-echo "Undo any changes."
:notify #'calibre-edit-reset)
(widget-insert " ")
(widget-create 'push-button
:tag "Cancel"
:help-echo "Abort all changes."
:notify #'calibre-edit-abort)
(widget-setup)
(goto-char (point-min)))
buffer))
(defun calibre-edit-revert (book)
"Undo any edits performed to BOOK in this session."
(let ((compare-ids (lambda (b)
(= (calibre-book-id book)
(calibre-book-id b)))))
(setf calibre--books
(cl-substitute-if (calibre-util-find-book book calibre-edit--edited-books)
compare-ids
calibre--books))
(calibre-library--refresh)))
(defun calibre-edit--find-original (book)
"Return BOOK absent any modifications performed in this session."
(calibre-util-find-book book calibre-edit--edited-books))
(defun calibre-edit--different-fields (a b)
"Return a list of slot names whose values differ in A and B."
(let (diff)
(dolist (descriptor (cdr (cl-struct-slot-info 'calibre-book)))
(let* ((slot-name (car descriptor))
(a (cl-struct-slot-value 'calibre-book slot-name a))
(b (cl-struct-slot-value 'calibre-book slot-name b)))
(unless (equal a b)
(push slot-name diff))))
diff))
(defun calibre-util-uglify-field-name (field)
"Return FIELD replacing - with -."
(string-join (split-string (symbol-name field) "-") "_"))
(defun calibre-edit--command (book)
"Return the command to commit edits to BOOK to disk."
`("set_metadata"
,@(flatten-list (mapcar (lambda (field)
`("-f" ,(format "%s:%s" (calibre-util-uglify-field-name field)
(cl-case field
(authors (string-join (calibre-book-authors book) " & "))
(title (calibre-book-title book))
(publisher (calibre-book-publisher book))
(series (calibre-book-series book))
(series-index (calibre-book-series-index book))
(tags (string-join (calibre-book-tags book) ","))))))
(calibre-edit--different-fields book (calibre-edit--find-original book))))
,(int-to-string (calibre-book-id book))))
(defun calibre-edit-commit-edits (books)
"Commit edits to BOOKS to disk."
(calibre-exec--queue-commands (mapcar #'calibre-edit--command books)))
(provide 'calibre-edit)
;;; calibre-edit.el ends here