commit 8e119819873032058e1b9fd5f358f7f0d7c64ca4 Author: Bram Schoenmakers Date: Mon Jul 1 22:27:12 2024 +0200 Initial commit of elfeed-prune diff --git a/elfeed-prune.el b/elfeed-prune.el new file mode 100644 index 0000000..5044424 --- /dev/null +++ b/elfeed-prune.el @@ -0,0 +1,165 @@ +;;; elfeed-prune.el --- Elfeed database pruning -*- lexical-binding: t; -*- + +;; Copyright (C) 2024 Bram Schoenmakers + +;; Author: Bram Schoenmakers +;; Maintainer: Bram Schoenmakers +;; Created: 1 July 2024 +;; Package-Version: 0.1 +;; Package-Requires: ((emacs "29.1")) +;; Keywords: +;; URL: + +;; This file is not part of GNU Emacs. + +;; MIT License + +;; Copyright (c) 2024 Bram Schoenmakers + +;; Permission is hereby granted, free of charge, to any person obtaining a copy +;; of this software and associated documentation files (the "Software"), to deal +;; in the Software without restriction, including without limitation the rights +;; to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +;; copies of the Software, and to permit persons to whom the Software is +;; furnished to do so, subject to the following conditions: + +;; The above copyright notice and this permission notice shall be included in all +;; copies or substantial portions of the Software. + +;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +;; OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +;; SOFTWARE. + +;;; Commentary: + +;;; Code: + +(require 'elfeed) + +(defcustom elfeed-prune-enabled nil + "Set to t to perform actual pruning on the database. + +This boolean serves as a safety pin, because the elfeed database +was not designed to have items removed. By setting this boolean +to t you are aware of the risks." + :group 'elfeed-prune + :type 'boolean) + +(defcustom elfeed-prune-days-read 90 + "Read items older than this amount of days will be pruned." + :group 'elfeed-prune + :type 'natnum) + +(defcustom elfeed-prune-days-unread 30 + "Unread items older than this amount of days will be pruned." + :group 'elfeed-prune + :type 'natnum) + +(defcustom elfeed-prune-gc t + "Run `elfeed-db-gc' to remove stale content from disk." + :group 'elfeed-prune + :type 'boolean) + +(defcustom elfeed-prune-predicate #'elfeed-prune--elfeed-prune-entry-p + "Function that determines which entries should be pruned. + +The function accepts two parameters: the entry and the feed it +belongs to. These are the data structures defined in the +elfeed-db.el package. + +Look inside `elfeed-prune--elfeed-prune-entry-p' for examples of +using these data structures. Otherwise consult the elfeed-db.el +sources." + :group 'elfeed-prune + :type 'function) + +(defun elfeed-prune--entry-too-old-p (entry) + "Return t if the given ENTRY is considered too old. + +The thresholds are configured through the variables +`elfeed-prune-days-read' and `elfeed-prune-days-unread'." + (let* ((current-time (float-time)) + (entry-time (elfeed-entry-date entry)) + (unread-p (seq-contains-p (elfeed-entry-tags entry) 'unread)) + (threshold-days (if unread-p + elfeed-prune-days-unread + elfeed-prune-days-read)) + (threshold-seconds (* 60 60 24 threshold-days)) + (entry-age (- current-time entry-time))) + (> entry-age threshold-seconds))) + +(defun elfeed-prune--elfeed-prune-entry-p (entry feed) + "Return t if the ENTRY should be removed from the database. + +FEED can be used to remove entries for a whole feed. + +Conditions, any of: +- non-existing feed (not in `elfeed-feeds') +- has tag `rm' +- older than 90 days + +Unless: +- score > 2 +- has star tag" + (let ((tags (elfeed-entry-tags entry)) + (score (elfeed-score-scoring-get-score-from-entry entry))) + (and (or (seq-contains-p tags 'rm) + (elfeed-prune--entry-too-old-p entry) + (not (seq-contains-p (mapcar #'car elfeed-feeds) + (elfeed-feed-url feed) + #'string=)) + (<= score -5)) + (not (> score 2)) + (not (seq-contains-p tags 'star))))) + +(defun elfeed-prune (&optional dry-run) + "Prune the database entries. + +Entries for which `elfeed-prune--elfeed-prune-entry-p' returns t are +removed from the database. + +When DRY-RUN in non-nil, no actual pruning will be done, but the +number of items that would be removed will be shown in the echo +area." + (interactive "P") + (elfeed-db-ensure) + (let ((total-entries (hash-table-count elfeed-db-entries)) + (removed-entries 0) + ;; Make a copy so we don't iterate over a changing data structure + ;; Also useful to gather stats in a dry run. + (elfeed-db-index-copy (avl-tree-copy elfeed-db-index)) + (elfeed-db-entries-copy (copy-hash-table elfeed-db-entries))) + ;; Remove entries + (with-elfeed-db-visit (entry feed) + ;; The `with-elfeed-db-visit' declares the variable `id' that + ;; contains the entry ID. + (when (funcall elfeed-prune-predicate entry feed) + (setq removed-entries (1+ removed-entries)) + (avl-tree-delete elfeed-db-index-copy id) + (remhash id elfeed-db-entries-copy))) + (when (and + elfeed-prune-enabled + (not dry-run) + (< 0 removed-entries)) + (setf (plist-get elfeed-db :index) elfeed-db-index-copy) + (setf elfeed-db-index (plist-get elfeed-db :index)) + (setf (plist-get elfeed-db :entries) elfeed-db-entries-copy) + (setf elfeed-db-entries (plist-get elfeed-db :entries)) + (elfeed-db-set-update-time) + (elfeed-db-save-safe) + (when elfeed-prune-gc + (elfeed-db-gc-safe))) + (if (or dry-run (not elfeed-prune-enabled)) + (message "Would remove %d/%d elfeed entries [dry run]" + removed-entries + total-entries) + (message "Removed %d/%d elfeed entries" + removed-entries + total-entries)))) + +(provide 'elfeed-prune) +;;; elfeed-prune.el ends here