calibre.el/calibre-core.el

298 lines
13 KiB
EmacsLisp

;;; calibre-core.el --- Abstract interface for the Calibre Library -*- lexical-binding: t; -*-
;; Copyright (C) 2023,2024 Free Software Foundation, Inc.
;; This file is part of calibre.el.
;; 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 abstract interface for accessing the calibre
;; library. This abstract layer absolves the upper layers of caring
;; which interface is used.
;;; Code:
(require 'calibre-db)
(require 'calibre-cli)
(defconst calibre-library-buffer "*Library*")
(defmacro calibre-core--interface (function &rest args)
"Determine which interface to call FUNCTION from.
ARGS are arguments to pass to FUNCTION. The functions
calibre-db--FUNCTION and calibre-cli--FUNCTION must exist."
(let ((interface (if (and (fboundp 'sqlite-available-p) (sqlite-available-p))
'calibre-db
'calibre-cli)))
`(,(intern (format "%s--%s" interface function)) ,@args)))
(defvar calibre--books nil)
(defun calibre--books (&optional force)
"Return the in memory list of books.
If FORCE is non-nil the list is refreshed from the database."
(when (or force (not calibre--books))
(setf calibre--books (calibre-core--interface get-books)))
calibre--books)
(defvar calibre-library--filters nil)
(defun calibre-library-clear-filters ()
"Clear all active filters."
(interactive)
(setf calibre-library--filters nil)
(calibre-library--refresh))
(defvar calibre--library nil
"The active library.")
(defun calibre--library ()
"Return the active library.
If no library is active, prompt the user to select one."
(unless calibre--library
(calibre-select-library))
(alist-get calibre--library calibre-libraries nil nil #'string=))
(defun calibre-select-library (&optional library)
"Prompt the user to select a library from `calibre-libraries'.
If LIBRARY is non-nil, select that instead."
(interactive)
(unless calibre-libraries
(error "No Libraries defined"))
(setf calibre--library (if library
library
(let ((names (calibre--library-names)))
(if (not (length> names 1))
(car names)
(completing-read "Library: " names nil t))))
calibre--db nil
calibre--books nil)
(calibre-library--refresh t))
(defun calibre-library--refresh (&optional force)
"Refresh the contents of the library buffer.
If FORCE is non-nil fetch book data from the database."
(let ((buffer (get-buffer calibre-library-buffer)))
(when buffer
(with-current-buffer buffer
(let ((book (tabulated-list-get-id)))
(calibre-with-preserved-marks (not force)
(setf tabulated-list-entries
(mapcar #'calibre-book--print-info
(calibre-library--filter calibre-library--filters
(calibre--books force))))
(tabulated-list-print)
(if book
(calibre-library--find-book book)
(goto-char (point-max)))))))))
(defun calibre-library--set-header ()
"Set the header of the Library buffer."
(let ((buffer (get-buffer calibre-library-buffer)))
(when buffer
(with-current-buffer buffer
(setf tabulated-list-format (calibre-library--header-format))
(tabulated-list-init-header)))))
(defcustom calibre-library-time-format "%x"
"String specifying format for displaying time related metadata.
See `format-time-string' for an explanation of how to write this
string."
:type 'string
:set (lambda (symbol value)
(set-default symbol value)
(calibre-library--refresh))
:group 'calibre
:package-version '("calibre" . "1.1.0"))
(defcustom calibre-library-columns '((id . 4)
(title . 35)
(authors . 20)
(publisher . 10)
(series . 15)
(series-index . 3)
(tags . 10)
(formats . 10))
"The metadata fields to display in the library buffer.
Each entry is a key identifying a metadata field and the width that
column should have."
:type '(repeat (cons
:tag "Column"
(choice
:tag "Attribute"
(const :tag "ID" id)
(const :tag "Title" title)
(const :tag "Author(s)" authors)
(const :tag "Publisher(s)" publisher)
(const :tag "Series" series)
(const :tag "Series Index" series-index)
(const :tag "Tags" tags)
(const :tag "Formats" formats)
(const :tag "Publication date" pubdate))
(integer :tag "Width")))
:set (lambda (symbol value)
(set-default symbol value)
(calibre-library--set-header)
(calibre-library--refresh))
:package-version '("calibre" . "1.1.0")
:group 'calibre)
(defun calibre-library--header-format ()
"Create the header for the Library buffer.
Return a vector suitable as the value of `tabulated-list-format'
with values determined by `calibre-library-columns'."
(vconcat
(mapcar (lambda (x)
(let ((column (car x))
(width (cdr x)))
(cl-case column
(id `("ID" ,width (lambda (a b)
(< (calibre-book-id (car a))
(calibre-book-id (car b))))
:right-align t))
(title `("Title" ,width t))
(authors `("Author(s)" ,width t))
(publisher `("Publisher" ,width t))
(series `("Series" ,width (lambda (a b)
(calibre-book-sort-by-series (car a) (car b)))))
(series-index `("#" ,width (lambda (a b)
(calibre-book-sort-by-series (car a) (car b)))
:right-align t))
(tags `("Tags" ,width))
(formats `("Formats" ,width))
(pubdate `("Publication Date" ,width (lambda (a b)
(time-less-p (calibre-book-pubdate (car a))
(calibre-book-pubdate (car b)))))))))
calibre-library-columns)))
(defun calibre-book--print-info (book)
"Return list suitable as a value of `tabulated-list-entries'.
BOOK is a `calibre-book'."
(list book
(vconcat (mapcar (lambda (x)
(let ((column (car x)))
(cl-case column
(id (int-to-string (calibre-book-id book)))
(title (calibre-book-title book))
(authors (string-join (calibre-book-authors book) ", "))
(publisher (let ((publisher (calibre-book-publisher book)))
(if (not publisher) "" publisher)))
(series (let ((series (calibre-book-series book))) (if (not series) "" series)))
(series-index (if (calibre-book-series book) (format "%.1f" (calibre-book-series-index book)) ""))
(tags (string-join (calibre-book-tags book) ", "))
(formats (string-join (mapcar (lambda (f) (upcase (symbol-name f))) (calibre-book-formats book)) ", "))
(pubdate (if (calibre-book-pubdate book)
(format-time-string calibre-library-time-format (calibre-book-pubdate book))
"Invalid")))))
calibre-library-columns))))
(defun calibre-book--file (book format)
"Return the path to BOOK in FORMAT."
(let ((path (calibre-book-path book))
(file-name (calibre-book-file-name book)))
(expand-file-name (file-name-concat (calibre--library)
path
(format "%s.%s" file-name format)))))
(defun calibre-composite-filter-p (object)
"Return t if OBJECT is a composite filter."
(and (vectorp object) (length= object 2) (listp (elt object 1))))
(defun calibre--get-filter-items (filter)
"Return the id's of books matching FILTER."
(if (calibre-composite-filter-p filter)
(seq-let (op filters) filter
(seq-reduce (if (eq op '+)
#'seq-union
#'seq-intersection)
(mapcar (lambda (f)
(calibre--get-filter-items (vconcat `[,op] f)))
filters)
(if (eq op '+)
'()
(calibre--books))))
(seq-let (_ field value &rest params) filter
(let ((fuzzy-match (seq-contains-p params '~)))
(cl-case field
(title (calibre-core--interface get-title-books value fuzzy-match))
(author (calibre-core--interface get-author-books value fuzzy-match))
(tag (calibre-core--interface get-tag-books value fuzzy-match))
(publisher (calibre-core--interface get-publisher-books value fuzzy-match))
(series (calibre-core--interface get-series-books value fuzzy-match))
(format (calibre-core--interface get-format-books value fuzzy-match)))))))
(defun calibre-library--filter (filters books)
"Return those books in BOOKS that match FILTERS.
FILTERS should be a list of vectors, for the exact contents see
`calibre-virtual-libraries'."
(let* ((include (seq-filter (lambda (f) (eq (elt f 0) '+)) filters))
(exclude (seq-filter (lambda (f) (eq (elt f 0) '-)) filters))
(include-ids (when include
(seq-reduce #'seq-intersection
(mapcar #'calibre--get-filter-items include)
(mapcar #'calibre-book-id calibre--books))))
(exclude-ids (when exclude
(seq-reduce #'seq-union
(mapcar #'calibre--get-filter-items exclude)
'()))))
(seq-filter (lambda (b)
(not (seq-find (lambda (id)
(= id (calibre-book-id b)))
exclude-ids)))
(if include-ids
(seq-filter (lambda (b)
(seq-find (lambda (id)
(= id (calibre-book-id b)))
include-ids))
books)
(if include
nil
books)))))
;; The ignored optional argument makes these functions valid arguments
;; to completion-table-dynamic.
(defun calibre-core--get-titles (&optional _)
"Return a list of the titles in the active library."
(calibre-core--interface get-titles))
(defun calibre-core--get-authors (&optional _)
"Return a list of the authors in the active library."
(calibre-core--interface get-authors))
(defun calibre-core--get-tags (&optional _)
"Return a list of the tags in the active library."
(calibre-core--interface get-tags))
(defun calibre-core--get-formats (&optional _)
"Return a list of the file formats stored in the active library."
(calibre-core--interface get-formats))
(defun calibre-core--get-series (&optional _)
"Return a list of the series in the active library."
(calibre-core--interface get-series))
(defun calibre-core--get-publishers (&optional _)
"Return a list of the publishers in the active library."
(calibre-core--interface get-publishers))
;; Completion tables
(defvar calibre-authors-completion-table
(completion-table-dynamic #'calibre-core--get-authors))
(defvar calibre-publishers-completion-table
(completion-table-dynamic #'calibre-core--get-publishers))
(defvar calibre-series-completion-table
(completion-table-dynamic #'calibre-core--get-series))
(defvar calibre-tags-completion-table
(completion-table-dynamic #'calibre-core--get-tags))
(provide 'calibre-core)
;;; calibre-core.el ends here