branch: externals/calibre
commit 5d4952641441ad91e3d260f2aebaaa482c0695a4
Author: Kjartan Óli Ágústsson <[email protected]>
Commit: Kjartan Óli Ágústsson <[email protected]>

    Add support for search and virtual libraries
    
    * README: Add mention of search and virtual libraries
    * calibre-db.el (calibre-db--get-authors, calibre-db--get-series,
    calibre-db--get-tags, calibre-db--get-publishers,
    calibre-db--get-author-books, calibre-db--get-tag-books,
    calibre-db--get-publisher-books, calibre-db--get-series-books,
    calibre-db--get-format-books, calibre--get-filter-items,
    calibre-library--filter, calibre-library-clear-filters): Add new
    functions to facilitate search.
    (calibre-library--filters): Add new variable to store the currently
    active search filters.
    (calibre-library--refresh): Apply the active search filters before
    displaying library contents.
    * calibre-library.el: Require the new modules calibre-search and
    calibre-virtual-library.
    (calibre-library-open-book-other-window): Add function to open a book
    in another window, for symmetry with Dired.
    (calibre-library-mode-map): Add bindings for the new functions
    calibre-library-open-book-other-window,
    calibre-select-virtual-library, and calibre-search.
    * calibre-search.el:
    (calibre-chose-author, calibre-chose-publisher, calibre-chose-tag,
    calibre-search--operation, calibre-library-search-author,
    calibre-library-search-publisher, calibre-library-search-tag,
    calibre-library-clear-last-search): Add new functions to facilitate
    search.
    (calibre-search): Add a transient to provide UI for search.
    * calibre-virtual-library.el:
    (calibre-virtual-libraries): Add new defcustom to store virtual
    libraries.
    (calibre-select-virtual-library): Add new function to select virtual
    libraries.
    * calibre.el: Bump package version.
---
 README                     |   2 +
 calibre-db.el              | 104 ++++++++++++++++++++++++++++++++++++++++++++-
 calibre-library.el         |  12 +++++-
 calibre-search.el          |  95 +++++++++++++++++++++++++++++++++++++++++
 calibre-virtual-library.el |  67 +++++++++++++++++++++++++++++
 calibre.el                 |   2 +-
 6 files changed, 279 insertions(+), 3 deletions(-)

diff --git a/README b/README
index e1c6597d65..3ca2423cdb 100644
--- a/README
+++ b/README
@@ -6,6 +6,8 @@ Calibre.el is a package for interacting with Calibre
 include:
 - Easily switch between multiple libraries
 - List the contents of your library
+- Filter the contents of your library based on various metadata
+- Create Virtual Libraries by giving names to sets of filters
 - Add and remove books from your library
 - Read your books in Emacs
 
diff --git a/calibre-db.el b/calibre-db.el
index 5ced64be90..9bbb623f7a 100644
--- a/calibre-db.el
+++ b/calibre-db.el
@@ -87,6 +87,26 @@ WHERE books.id = ?"
                                "SELECT format FROM data WHERE book = ?"
                                `[,id]))))
 
+(defun calibre-db--get-authors ()
+  "Return a list of the authors in the active library."
+  (sqlite-select (calibre--db)
+                               "SELECT name FROM authors;"))
+
+(defun calibre-db--get-series ()
+  "Return a list of the series in the active library."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT name FROM series;")))
+
+(defun calibre-db--get-tags ()
+  "Return a list of the tags in the active library."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT name FROM tags;")))
+
+(defun calibre-db--get-publishers ()
+  "Return a list of the publishers in the active library."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT name FROM publishers;")))
+
 (defvar calibre--db nil)
 (defun calibre--db ()
   "Return the metadata database."
@@ -112,6 +132,79 @@ FROM books
 LEFT JOIN books_series_link sl ON books.id = sl.book
 LEFT JOIN series ON sl.series = series.id;"))))
 
+(defun calibre-db--get-author-books (author)
+  "Return the id's of books written by AUTHOR."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT book
+FROM books_authors_link al
+LEFT JOIN authors a ON al.author = a.id
+WHERE a.name = ?" `[,author])))
+
+(defun calibre-db--get-tag-books (tag)
+  "Return the id's of books tagged with TAG."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT book
+FROM books_tags_link tl
+LEFT JOIN tags t ON tl.tag = t.id
+WHERE t.name = ?" `[,tag])))
+
+(defun calibre-db--get-publisher-books (publisher)
+  "Return the id's of books published by PUBLISHER."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT book
+FROM books_publishers_link pl
+LEFT JOIN publishers p ON pl.publisher = p.id
+WHERE p.name = ?" `[,publisher])))
+
+(defun calibre-db--get-series-books (series)
+  "Return the id's of books that are part of SERIES."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT book
+FROM books_series_link sl
+LEFT JOIN series s ON sl.publisher = s.id
+WHERE s.name = ?" `[,series])))
+
+(defun calibre-db--get-format-books (format)
+  "Return the id's of books available in FORMAT."
+  (flatten-list (sqlite-select (calibre--db)
+                               "SELECT book
+FROM data
+WHERE format = ?" `[,format])))
+
+(defun calibre--get-filter-items (filter)
+  "Return the id's of books matching FILTER."
+  (seq-let (_ field value) filter
+    (cl-case field
+      (author (calibre-db--get-author-books value))
+      (tag (calibre-db--get-tag-books value))
+      (publisher (calibre-db--get-publisher-books value))
+      (series (calibre-db--get-series-books value))
+      (format (calibre-db--get-format-books (upcase (symbol-name value)))))))
+
+(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 (cl-remove-if-not (lambda (f) (eq (elt f 0) '+)) filters))
+         (exclude (cl-remove-if-not (lambda (f) (eq (elt f 0) '-)) filters))
+         (include-ids (when include
+                        (cl-reduce #'cl-intersection
+                                   (mapcar #'calibre--get-filter-items 
include))))
+         (exclude-ids (when exclude
+                        (cl-reduce #'cl-union
+                                   (mapcar #'calibre--get-filter-items 
exclude)))))
+    (cl-remove-if (lambda (b)
+                    (seq-find (lambda (id)
+                                (= id (calibre-book-id b)))
+                              exclude-ids))
+                  (if include-ids
+                      (cl-remove-if-not (lambda (b)
+                                      (seq-find (lambda (id)
+                                                  (= id (calibre-book-id b)))
+                                                include-ids))
+                                        books)
+                    books))))
+
 (defvar calibre--books nil)
 (defun calibre--books (&optional force)
   "Return the in memory list of books.
@@ -120,6 +213,14 @@ If FORCE is non-nil the list is refreshed from the 
database."
     (setf calibre--books (calibre-db--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.")
 
@@ -150,7 +251,8 @@ If FORCE is non-nil fetch book data from the database."
       (with-current-buffer buffer
         (setf tabulated-list-entries
               (mapcar #'calibre-book--print-info
-                      (calibre--books force)))
+                      (calibre-library--filter calibre-library--filters
+                                               (calibre--books force))))
         (tabulated-list-print)))))
 
 (defun calibre-library--set-header ()
diff --git a/calibre-library.el b/calibre-library.el
index 59a0fb09b0..9c2c70660a 100644
--- a/calibre-library.el
+++ b/calibre-library.el
@@ -26,6 +26,8 @@
 (require 'dired)
 (require 'calibre-db)
 (require 'calibre-book)
+(require 'calibre-search)
+(require 'calibre-virtual-library)
 
 ;;;###autoload
 (defun calibre-library-add-book (file)
@@ -114,6 +116,11 @@ ARGS should be a list of strings.  SENTINEL is a process 
sentinel to install."
   (interactive (list (tabulated-list-get-id)) calibre-library-mode)
   (find-file (calibre-book--file book (calibre-book--pick-format book))))
 
+(defun calibre-library-open-book-other-window (book)
+  "Open BOOK in its preferred format."
+  (interactive (list (tabulated-list-get-id)) calibre-library-mode)
+  (find-file-other-window (calibre-book--file book (calibre-book--pick-format 
book))))
+
 (defvar-keymap calibre-library-mode-map
   :doc "Local keymap for Calibre Library buffers."
   :parent tabulated-list-mode-map
@@ -121,7 +128,10 @@ ARGS should be a list of strings.  SENTINEL is a process 
sentinel to install."
   "u" #'calibre-library-mark-unmark
   "x" #'calibre-library-execute
   "a" #'calibre-library-add-book
-  "RET" #'calibre-library-open-book)
+  "v" #'calibre-select-virtual-library
+  "s" #'calibre-search
+  "RET" #'calibre-library-open-book
+  "o" #'calibre-library-open-book-other-window)
 
 (define-derived-mode calibre-library-mode tabulated-list-mode
   (setf tabulated-list-padding 2)
diff --git a/calibre-search.el b/calibre-search.el
new file mode 100644
index 0000000000..4c30203106
--- /dev/null
+++ b/calibre-search.el
@@ -0,0 +1,95 @@
+;;; calibre-search.el --- Filter books based on search criteria.  -*- 
lexical-binding: t; -*-
+;; Copyright (C) 2023  Kjartan Óli Águstsson
+
+;; 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 provides an interface for filtering the current library
+;; on criteria such as Author, Publisher, Tags, etc.
+
+;;; Code:
+(require 'transient)
+(require 'calibre-db)
+
+(defun calibre-chose-author ()
+  "Prompt the user to select an author."
+  (interactive)
+  (completing-read "Author: " (calibre-db--get-authors)))
+
+(defun calibre-chose-publisher ()
+  "Prompt the user to select a publisher."
+  (interactive)
+  (completing-read "Author: " (calibre-db--get-publishers)))
+
+(defun calibre-chose-tag ()
+  "Prompt the user to select a tag."
+  (interactive)
+  (completing-read "Tag: " (calibre-db--get-tags)))
+
+(defun calibre-search--operation (args)
+  "Return the appropriate symbol for a filter operation.
+ARGS is the argument list of a transient command."
+  (if (cl-find "--exclude" args :test #'string=) '- '+))
+
+(defun calibre-library-search-author (author &optional args)
+  "Add a filter for AUTHOR.
+ARGS determines whether the created filter is inclusive or exclusive."
+  (interactive (list (calibre-chose-author)
+                     (transient-args 'calibre-search)))
+  (setf calibre-library--filters (cons `[,(calibre-search--operation args) 
author ,author]
+                                       calibre-library--filters))
+  (calibre-library--refresh))
+
+(defun calibre-library-search-publisher (publisher &optional args)
+    "Add a filter for PUBLISHER.
+ARGS determines whether the created filter is inclusive or exclusive."
+  (interactive (list (calibre-chose-publisher)
+                     (transient-args 'calibre-search)))
+  (setf calibre-library--filters (cons `[,(calibre-search--operation args) 
publisher ,publisher]
+                                       calibre-library--filters))
+  (calibre-library--refresh))
+
+(defun calibre-library-search-tag (tag &optional args)
+    "Add a filter for TAG.
+ARGS determines whether the created filter is inclusive or exclusive."
+  (interactive (list (calibre-chose-tag)
+                     (transient-args 'calibre-search)))
+  (setf calibre-library--filters (cons `[,(calibre-search--operation args) tag 
,tag]
+                                       calibre-library--filters))
+  (calibre-library--refresh))
+
+(defun calibre-library-clear-last-search ()
+  "Clear the last applied search filter."
+  (interactive)
+  (when calibre-library--filters
+    (setf calibre-library--filters (cdr calibre-library--filters)))
+  (calibre-library--refresh))
+
+(transient-define-prefix calibre-search ()
+  ""
+  :transient-suffix 'transient--do-call
+  ["Arguments"
+   ("-e" "Exclude" "--exclude")]
+  ["Search"
+   ("a" "Author" calibre-library-search-author)
+   ("p" "Publisher" calibre-library-search-publisher)
+   ("t" "Tag" calibre-library-search-tag)]
+  ["Misc"
+   ("u" "Undo" calibre-library-clear-last-search)
+   ("c" "Clear" calibre-library-clear-filters)
+   ("q" "Exit" transient-quit-one)])
+
+(provide 'calibre-search)
diff --git a/calibre-virtual-library.el b/calibre-virtual-library.el
new file mode 100644
index 0000000000..70f55357f4
--- /dev/null
+++ b/calibre-virtual-library.el
@@ -0,0 +1,67 @@
+;;; calibre-virtual-library.el --- Virtual library support -*- 
lexical-binding: t; -*-
+
+;; Copyright (C) 2023  Kjartan Óli Águstsson
+
+;; 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 provides virtual library support for calibre.el.  A
+;; virtual library is a named set of predefined search filters.
+
+;;; Code:
+(require 'calibre-db)
+
+(defcustom calibre-virtual-libraries nil
+  "A list of predefined filters, i.e. Virtual Libraries.
+Each element is a cons cell (name . filters) where name is a
+string, and filters is a list of vectors [op field value]"
+  :type '(repeat :tag "Virtual Libraries"
+                 (cons :tag "Virtual Library"
+                       (string :tag "Name")
+                       (repeat :tag "Filters"
+                               (vector :tag "Filter"
+                                       (choice :tag "Operation"
+                                               (const :tag "Include" +)
+                                               (const  :tag "Exclude" -))
+                                       (choice :tag "Field"
+                                               (const :tag "Author" author)
+                                               (const :tag "Publisher" 
publisher)
+                                               (const :tag "Series" series)
+                                               (const :tag "Tag" tag)
+                                               (const :tag "Format" format))
+                                       (string :tag "Value")))))
+  :group 'calibre
+  :package-version '("calibre" . "1.1.0"))
+
+(defun calibre-select-virtual-library (arg &optional virtual-library)
+  "Prompt the user to select a virtual library.
+If called with a prefix argument clear the active virtual library."
+  (interactive "P")
+  (if arg
+      (calibre-library-clear-filters)
+    (setf calibre-library--filters
+          (alist-get
+           (if virtual-library
+               virtual-library
+             (completing-read "Virtual Library: "
+                              (mapcar #'car calibre-virtual-libraries)
+                              nil
+                              t))
+           calibre-virtual-libraries nil nil #'string=)))
+  (calibre-library--refresh t))
+
+(provide 'calibre-virtual-library)
+;;; calibre-virtual-library.el ends here
diff --git a/calibre.el b/calibre.el
index c74062282f..3112bb0f73 100644
--- a/calibre.el
+++ b/calibre.el
@@ -4,7 +4,7 @@
 
 ;; Author: Kjartan Oli Agustsson <[email protected]>
 ;; Maintainer: Kjartan Oli Agustsson <[email protected]>
-;; Version: 1.0.5
+;; Version: 1.1.0
 ;; Package-Requires: ((emacs "29.1"))
 ;; URL: https://git.disroot.org/kjartanoli/calibre.el
 

Reply via email to