diff --git a/README.org b/README.org
new file mode 100644
index 0000000..930fad2
--- /dev/null
+++ b/README.org
@@ -0,0 +1,87 @@
+#+TITLE: elfeed-sync
+
+The package syncs the read and marked status of entries between [[https://github.com/skeeto/elfeed][elfeed]] and [[https://tt-rss.org/][tt-rss]].
+
+Supports [[https://github.com/SqrtMinusOne/elfeed-summary][elfeed-summary]].
+
+* Installation
+The project consists of the tt-rss plugin and the Emacs package.
+
+If you are using the [[https://git.tt-rss.org/fox/ttrss-docker-compose.git/tree/README.md][tt-rss docker]] setup, the steps are as follows. Change them accordingly if you are not.
+1. Mount the =/var/www/html= directory from the container somewhere to the filesystem as described [[https://git.tt-rss.org/fox/ttrss-docker-compose.wiki.git/tree/Home.md#how-do-i-use-dynamic-image-for-development][here]].
+2. Put the repository to the =tt-rss/plugins.local/elfeed_sync= folder:
+ #+begin_src bash
+ cd ./html/tt-rss/plugins.local/
+ git clone https://github.com/SqrtMinusOne/elfeed-sync.git elfeed_sync
+ #+end_src
+3. Add =elfeed_sync= to the =TTRSS_PLUGINS= environment variable.
+ #+begin_src dotenv
+ TTRSS_PLUGINS=auth_internal, auth_remote, nginx_xaccel, elfeed_sync
+ #+end_src
+4. Allow larger request body sizes in nginx. Add the following to the =server= directive:
+ #+begin_src conf-space
+ client_max_body_size 10M;
+ #+end_src
+
+ For me, the sync payload is around 3M.
+
+5. Increase the read timeout in nginx. Add the following to the php location directive:
+ #+begin_src conf-space
+ fastcgi_read_timeout 600;
+ #+end_src
+
+ Syncing the entries is usually pretty fast, but the first feed sync takes a while.
+6. Then restart tt-rss. Check if the plugin appears in the Preferences > Plugins section.
+7. Enable "Allows accessing this account through the API" in the Preferences > Preferences. You also may want to disable "Purge unread articles", because elfeed doesn't do that.
+
+Install the Emacs package however you normally install packages, I prefer use-package and straight.el. Make sure to enable =elfeed-sync-mode=.
+#+begin_src emacs-lisp
+(use-package elfeed-sync
+ :straight (:host github :repo "SqrtMinusOne/elfeed-sync")
+ :after elfeed
+ :config
+ (elfeed-sync-mode))
+#+end_src
+
+Then set up the following variables:
+- =elfeed-sync-tt-rss-instance= - point that to your tt-rss instance, e.g.
+ #+begin_example
+ https://example.com/tt-rss
+ #+end_example
+ (No trailing slash)
+- =elfeed-sync-tt-rss-login=
+- =elfeed-sync-tt-rss-password=
+- =elfeed-sync-unread-tag= - elfeed tag to map the read status in tt-rss. =unread= by default.
+- =elfeed-sync-marked-tag= - elfeed tag to map to the marked status in tt-rss. =later= by default.
+
+* Usage
+** Syncing the feed list
+The first thing you probably want to do is to sync the feed list.
+
+It makes little sense to sync tt-rss feeds to elfeed, because people often use projects like [[https://github.com/remyhonig/elfeed-org][elfeed-org]]. But it's possible to sync elfeed feeds to tt-rss.
+
+The function =M-x elfeed-sync-feeds= does exactly that. If you have [[https://github.com/SqrtMinusOne/elfeed-summary][elfeed-summary]] installed and tt-rss categories enabled, the function will recreate the elfeed-summary tree in tt-rss.
+
+The first run of the function takes a while because tt-rss has to fetch the feed at the moment of the first subscription. Which is why increasing the timeout may be necessary.
+
+However, running the function multiple times until it succeeds should also work.
+** Syncing the entry list
+To sync the entry list, run =M-x elfeed-sync=. The sync usually takes a couple of seconds.
+
+The sync finishes at the "Sync complete!" message. Check the =*elfeed-log*= buffer for statistics.
+
+Occasionally, some entries do not match. Here are the possible cases:
+- Entry exists in the elfeed database, but not in tt-rss.
+ Run =M-x elfeed-sync-search-missing= to display such entries.
+- Entry exists in the tt-rss database, but not in elfeed:
+ - Entry appeared in the feed after the last =elfeed-update=.
+ Run =M-x elfeed-update= and then =M-x elfeed-sync=.
+ - Entry appeared and disappeared in the feed after the last =elfeed-update=.
+ Such an entry will never get to the elfeed database. If you want to, run =M-x elfeed-sync= and then =M-x elfeed-sync-read-ttrss-missing= to mark all such entries as read.
+- Entry appeared in the feed before =elfeed-sync-look-back=.
+ Such an entry will never be matched. This is an inconvenience if you have just set up tt-rss, it fetched old entries from the feeds and such entries remain permanently unread because they are untouched by the =M-x elfeed-sync=.
+ To mark such entries as read, run =M-x elfeed-sync-read-ttrss-old=.
+* Implementation details
+The heavy-lifting is done on the elisp side because I ran into strange performance issues with associative arrays in PHP.
+
+Check the =elfeed-sync--do-sync= function for the description of the synchronization algorithm. The tl;dr is to download all entries from tt-rss and match each entry against the elfeed database. In the case of discrepancy update whichever entry has the lower priority.
diff --git a/elfeed-sync.el b/elfeed-sync.el
index 3c5232b..1618272 100644
--- a/elfeed-sync.el
+++ b/elfeed-sync.el
@@ -1,4 +1,4 @@
-;;; elfeed-sync.el --- TODO -*- lexical-binding: t -*-
+;;; elfeed-sync.el --- Sync elfeed with tt-rss -*- lexical-binding: t -*-
;; Copyright (C) 2022 Korytov Pavel
@@ -24,7 +24,33 @@
;; along with this program. If not, see .
;;; Commentary:
-;; TODO
+;; Sync the read and marked status of entries between elfeed and
+;; tt-rss. Supports elfeed-summary.
+;;
+;; The package consists of the tt-rss plugin and the Emacs
+;; package. Check the package README at
+;; for the tt-rss
+;; installation details.
+;;
+;; As for the Emacs part, you have to set the following variables:
+;; - `elfeed-sync-tt-rss-instance' - point that to your tt-rss
+;; instance.
+;; - `elfeed-sync-tt-rss-login'
+;; - `elfeed-sync-tt-rss-password'
+;; - `elfeed-sync-tt-rss-unread-tag'
+;; - `elfeed-sync-tt-rss-marked-tag'
+;;
+;; Make sure to enable `elfeed-sync-mode'.
+;;
+;; The sync period is limited by the `elfeed-sync-look-back' variable.
+;;
+;; To add the elfeed feeds to tt-rss, run `elfeed-sync-feeds'. If you
+;; have `elfeed-summary' installed, and tt-rss categories enabled, the
+;; function will recreate the `elfeed-summary' tree in tt-rss. The
+;; inverse operation is not supported.
+;;
+;; To sync the entries, run `elfeed-sync'. Check the function
+;; docstring on what to do next.
;;; Code:
(require 'elfeed)
@@ -37,7 +63,9 @@
:group 'elfeed)
(defcustom elfeed-sync-tt-rss-instance "http://localhost:8280/tt-rss"
- "URL of the tt-rss instance."
+ "URL of the tt-rss instance.
+
+Do not add the trailing slash."
:group 'elfeed-sync
:type 'string)
@@ -57,12 +85,12 @@
:type 'number)
(defcustom elfeed-sync-unread-tag 'unread
- "Unread tag for sync."
+ "Unread elfeed tag for sync."
:group 'elfeed-sync
:type 'symbol)
(defcustom elfeed-sync-marked-tag 'later
- "Marked tag for sync."
+ "Marked elfeed tag for sync."
:group 'elfeed-sync
:type 'symbol)
@@ -70,7 +98,16 @@
"Session ID.")
(defvar elfeed-sync--state nil
- "State of the tt-rss sync.")
+ "State of the tt-rss sync.
+
+This is an alist with the following keys:
+- `:last-sync-time' - time of the last successful sync.
+- `:ids-missing-tt-rss' - tt-rss entries that are missing in elfeed.
+ This is a hash table, where the key is the tt-rss id and the value
+ is the cons cell:
+ - The car is the time on which the entry was last updated.
+ - The cdr is the time of the sync when the entry was put into the
+ hash table.")
(defvar elfeed-sync--start-time nil
"Start time of the tt-rss sync.")
@@ -81,19 +118,17 @@
(defvar elfeed-sync--tt-rss-missed nil
"List of tt-rss entries missed in elfeed.")
-(cl-defstruct (elfeed-sync-datum (:constructor elfeed-sync-datum--create))
- id tags)
-
(defun elfeed-sync--state-empty ()
- "Create an empty elfeed-sync state."
+ "Create an empty `elfeed-sync' state."
`((:last-sync-time . nil)
(:ids-missing-tt-rss . ,(make-hash-table :test #'equal))))
(defun elfeed-sync--state-file ()
+ "Get the location of the state file."
(concat elfeed-db-directory "/sync-state"))
(defun elfeed-sync--state-load ()
- "Load elfeed-sync state from the filesystem."
+ "Load `elfeed-sync' state from the filesystem."
(if (not (file-exists-p (elfeed-sync--state-file)))
(setf elfeed-sync--state (elfeed-sync--state-empty))
(with-temp-buffer
@@ -111,7 +146,7 @@
(when (null elfeed-sync--state) (elfeed-sync--state-load)))
(defun elfeed-sync-reset ()
- "Reset the elfeed sync state"
+ "Reset the elfeed sync state."
(interactive)
(setf elfeed-sync--state (elfeed-sync--state-empty)))
@@ -138,6 +173,7 @@ with exceptions."
(org-journal-tags-db-save)))
(defun elfeed-sync--json-read-safe ()
+ "Read JSON from the current buffer, ignoring errors."
(condition-case _
(json-read)
(with-current-buffer (get-buffer-create "*elfeed-sync-json-read-error*")
@@ -149,8 +185,8 @@ with exceptions."
"Run CALLBACK with the active tt-rss session.
The `elfeed-sync--tt-rss-sid' variable stores the active session ID.
-If it's non-nil, CALLBACK is called outright, otherwise CALLBACK is
-called only after a succesful login query."
+If it's non-nil, CALLBACK is called outright, otherwise, CALLBACK is
+called only after a successful login query."
(if elfeed-sync--tt-rss-sid
(funcall callback)
(request (concat elfeed-sync-tt-rss-instance "/api/")
@@ -171,13 +207,17 @@ called only after a succesful login query."
(message "Error: %S" error-thrown))))))
(defmacro elfeed-sync--session (&rest body)
- "A wrapper around `elfeed-sync--with-session'."
+ "A wrapper around `elfeed-sync--with-session'.
+
+Pass BODY to the function callback."
`(elfeed-sync--with-session
(lambda ()
,@body)))
(defun elfeed-sync--get-tree-summary (&optional data)
- "Get list of feeds and categories from elfeed-summary."
+ "Get the list of feeds and categories from `elfeed-summary'.
+
+DATA is the output of `elfeed-summary--get-data'."
(unless data
(setq data (elfeed-summary--get-data)))
(cl-loop for datum in data
@@ -201,19 +241,27 @@ called only after a succesful login query."
(alist-get 'feed (cdr datum))))))))))
(defun elfeed-sync--get-tree-plain ()
- "Get list of feeds and categories from plain elfeed."
+ "Get the list of feeds and categories from plain elfeed."
(cl-loop for datum in elfeed-feeds
collect `(("feed" . (("tags" . ,(cdr datum))
("url" . ,(car datum)))))))
(defun elfeed-sync--get-tree ()
- "Get list of feeds and categories."
+ "Get the list of feeds and categories."
(if (fboundp #'elfeed-summary--get-data)
(elfeed-sync--get-tree-summary)
(elfeed-sync--get-tree-plain)))
(defmacro elfeed-sync--handler (&rest body)
- "Default response handler for tt-rss."
+ "Default response handler for tt-rss.
+
+tt-rss often returns a non-zero status key in the response
+object instead of an HTTP code. The handler checks for this.
+
+Also handles the case where the session ID is invalid, and
+resets it.
+
+Execute BODY with the data variable scoped."
`(cl-function
(lambda (&key data &allow-other-keys)
(if (= (alist-get 'status data) 0)
@@ -227,7 +275,18 @@ called only after a succesful login query."
(setq elfeed-sync--tt-rss-sid nil))))))
(defun elfeed-sync-feeds ()
- "Sync feeds with tt-rss."
+ "Sync feeds with tt-rss.
+
+If `elfeed-summary' is available, it serves as a source of the
+feed tree. Otherwise, the `elfeed-feeds' variable is used.
+
+The first run of the function takes a while because tt-rss has to
+fetch the feed at the moment of the first subscription. It is
+recommended to increase the server timeout to at least a couple of
+minutes.
+
+However, running the function multiple times until it succeeds
+should also work."
(interactive)
(elfeed-sync--session
(request (concat elfeed-sync-tt-rss-instance "/api/")
@@ -245,6 +304,9 @@ called only after a succesful login query."
(message "Error: %S" error-thrown))))))
(defun elfeed-sync--get-bad-feeds ()
+ "Get feeds that have URLs repeat for different entries.
+
+Return a hash table with the feed URL as the key."
(let ((feed-hash (make-hash-table :test #'equal))
(bad-feeds-hash (make-hash-table :test #'equal)))
(with-elfeed-db-visit (entry feed)
@@ -262,17 +324,17 @@ called only after a succesful login query."
bad-feeds-hash))
(defun elfeed-sync--entry-unread-p (entry)
- "Return non-nil if ENTRY is unread."
+ "Return t if ENTRY is unread."
(not (null (member elfeed-sync-unread-tag (elfeed-entry-tags entry)))))
(defun elfeed-sync--entry-marked-p (entry)
- "Return non-nil if ENTRY is marked."
+ "Return t if ENTRY is marked."
(not (null (member elfeed-sync-marked-tag (elfeed-entry-tags entry)))))
(defun elfeed-sync--set-entry-unread (entry status)
"Set the unread status of ENTRY to STATUS.
-STATUS is a boolean. If nil, the entry is marked as read. ENTRY
+STATUS is a boolean. If nil, the entry is marked as read. ENTRY
is an instance of `elfeed-entry'."
(let ((is-unread (elfeed-sync--entry-unread-p entry)))
(when (and is-unread (not status))
@@ -292,6 +354,11 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(elfeed-tag entry elfeed-sync-marked-tag))))
(defun elfeed-sync--ttrss-key (bad-feeds ttrss-entry)
+ "Return the key for the TTRSS-ENTRY.
+
+If the feed is in BAD-FEEDS, the key looks like this:
+---. Otherwise, the key is the
+entry URL."
(let ((feed-url (alist-get 'feed_url ttrss-entry)))
(if (gethash feed-url bad-feeds)
(format "%s---%s" (alist-get 'title ttrss-entry)
@@ -299,12 +366,21 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(alist-get 'link ttrss-entry))))
(defun elfeed-sync--elfeed-key (bad-feeds elfeed-entry feed-url)
+ "Return the key for the ELFEED-ENTRY.
+
+If the FEED-URL is in BAD-FEEDS, the key looks like this:
+---. Otherwise, the key is the
+entry URL."
(if (gethash feed-url bad-feeds)
(format "%s---%s" (elfeed-entry-title elfeed-entry)
(floor (elfeed-entry-date elfeed-entry)))
(elfeed-entry-link elfeed-entry)))
(defun elfeed-sync--ttrss-get-updated-time (ttrss-entry)
+ "Return the last updated time of the TTRSS-ENTRY.
+
+It is the last time when the entry was read or marked. Can also
+be nil."
(if (and (alist-get 'last_read ttrss-entry)
(alist-get 'last_marked ttrss-entry))
(max (alist-get 'last_read ttrss-entry)
@@ -313,6 +389,20 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(alist-get 'last_marked ttrss-entry))))
(defun elfeed-sync--ttrss-get-last-sync-time (ttrss-id ttrss-time)
+ "Get the time when the entry with TTRSS-ID was last synced.
+
+TTRSS-TIME is the time when the entry was last updated.
+
+If there is a record in the `:ids-missing-tt-rss' value in the
+`elfeed-sync--state', that means that the entry has already been
+encountered and not found in the elfeed database. In this case,
+return the time of the last sync and the moment when that occurred.
+
+Otherwise (if the entry has not been seen or has been updated),
+return the global last sync time.
+
+The `elfeed-sync--do-sync' has a more detailed description of why
+this is necessary."
(if ttrss-time
(if-let* ((val (gethash
ttrss-id
@@ -324,6 +414,19 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(alist-get :last-sync-time elfeed-sync--state)))
(defun elfeed-sync--update-ttrss-missing (ttrss-entries ttrss-entries-processed)
+ "Update the `:ids-missing-tt-rss' value in the `elfeed-sync--state'.
+
+If an entry from TTRSS-ENTRIES is not in TTRSS-ENTRIES-PROCESSED, it
+means that the entry has not been found in elfeed.
+
+It has to be put to the `:ids-missing-tt-rss' value in the
+`elfeed-sync--state' if:
+- The entry has the update time set (look
+ `elfeed-sync--ttrss-get-updated-time')
+- The entry does not already appear in the `:ids-missing-tt-rss' or
+ has been updated since the time it appeared.
+
+Look at `elfeed-sync--do-sync' for the details."
(let (all-missing)
(maphash (lambda (ttrss-id ttrss-entry)
(unless (gethash ttrss-id ttrss-entries-processed)
@@ -336,13 +439,64 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(is-equal (= (car old-val) ttrss-time)))
t ;; do nothing
(puthash ttrss-id (cons ttrss-time
- elfeed-sync--start-time)
+ (or
+ (alist-get :last-sync-time
+ elfeed-sync--state)
+ elfeed-sync--start-time))
(alist-get :ids-missing-tt-rss
elfeed-sync--state))))))
ttrss-entries)
all-missing))
(defun elfeed-sync--do-sync (entries bad-feeds)
+ "Sync the ENTRIES with the elfeed database.
+
+ENTRIES is a list of entries from tt-rss. BAD-FEEDS is a
+hashtable of the feeds that have repeating URLs for
+entries (`elfeed-sync--get-bad-feeds').
+
+The sync process is as follows. Each entry in the elfeed
+database is matched against the tt-rss entry from ENTRIES. Two
+entries match if their keys (`elfeed-sync--elfeed-key' and
+`elfeed-sync--ttrss-key' respectively) match.
+
+The only things that are synced are the unread status and the
+marked status, tags for which are set in the
+`elfeed-sync-unread-tag' and `elfeed-sync-marked-tag'.
+
+Contrary to elfeed entries, tt-rss entries store the time when
+their status has been changed, which is retrieved by the
+`elfeed-sync--ttrss-get-updated-time' function. If that time is
+larger than the last sync time, it means that the entry has
+been updated in between the last and the current syncs and thus
+the tt-rss entry has the priority.
+
+One caveat with the last sync time is that the tt-rss entry does
+not necessarily exist in elfeed, i.e. the following scenario has
+to be addressed:
+- tt-rss fetch, the entry exists in tt-rss but not in elfeed
+- the entry is read in tt-rss
+- tt-rss and elfeed sync, the entry does not exist in elfeed
+- elfeed fetch, the entry is finally added to elfeed
+- tt-rss and elfeed sync.
+Now the entry is read in tt-rss and unread in elfeed, but technically
+it has been updated before the last sync, i.e. before the step 3 in
+the list above.
+
+So, to resolve that, the `:ids-missing-tt-rss' value in the
+`elfeed-sync--state' stores the cons cell for each such entry,
+where the car is the entry update time and the cdr is the time of
+the last sync for when the entry is entry encountered.
+
+Update of that value is done with the
+`elfeed-sync--update-ttrss-missing' function. That function also
+returns a list of all tt-rss entries that were missing in elfeed,
+updated or not.
+
+The current function returns an alist of the data, which is then
+passed to `elfeed-sync--apply-to-ttrss' to propagate the changes to
+tt-rss and `elfeed-sync--process-sync-data' to populate the elfeed
+log."
(let ((ttrss-entries (make-hash-table :test #'equal))
(ttrss-entries-processed (make-hash-table :test #'equal))
(ttrss-toggle-marked nil)
@@ -404,6 +558,7 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(hash-table-count ttrss-entries-processed))))))
(defun elfeed-sync--process-sync-data (sync-data)
+ "Output SYNC-DATA to the elfeed log."
(elfeed-log 'info "Total entries in %s: %s"
(propertize "elfeed" 'face 'elfeed-log-info-level-face)
(alist-get :elfeed-total-entries sync-data))
@@ -432,6 +587,7 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(message "Sync complete!"))
(defun elfeed-sync--apply-to-ttrss (sync-data)
+ "Propagate changes in SYNC-DATA to tt-rss."
(message "Propagating changes to tt-rss...")
(elfeed-sync--session
(request (concat elfeed-sync-tt-rss-instance "/api/")
@@ -451,7 +607,46 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(message "Error: %S" error-thrown))))))
(defun elfeed-sync ()
+ "Sync elfeed and tt-rss.
+
+The function downloads the tt-rss database and matches it against
+the elfeed database. Then it synchronizes the matching entry and
+records the unmatched ones. Check the `elfeed-sync--do-sync' function
+for details.
+
+The `elfeed-sync-look-back' sets the number of seconds to look
+back in the entries database (6 months by default).
+
+The function synchronizes the unread status (`elfeed-sync-unread-tag')
+and the marked status (`elfeed-sync-marked-tag').
+
+The sync depends on a persistent state, so make sure to enable
+`elfeed-sync-mode':
+\(with-eval-after-load 'elfeed
+ \(elfeed-sync-mode\)\)
+
+The sync finishes at the \"Sync complete!\" message. Check the
+*elfeed-log* buffer for statistics.
+
+Occasionally, some entries do not match. Here are the possible cases:
+- Entry exists in the elfeed database, but not in tt-rss.
+ Run `elfeed-sync-search-missing' to display such entries.
+- Entry exists in the tt-rss database, but not in elfeed:
+ - Entry appeared in the feed after the last `elfeed-update'.
+ Run `elfeed-update' and then `elfeed-sync'.
+ - Entry appeared and disappeared in the feed after the last
+ `elfeed-update'.
+ Such an entry will never get to the elfeed database. If you want
+ to, run `elfeed-sync' and then `elfeed-sync-read-ttrss-missing' to
+ mark all such entries as read.
+- Entry appeared in the feed before `elfeed-sync-look-back'.
+ Such an entry will never be matched. This is an inconvenience if you
+ have just set up tt-rss, it fetched old entries from the feeds and
+ such entries remain permanently unread because they are untouched by
+ the `elfeed-sync'.
+ To mark such entries as read, run `elfeed-sync-read-ttrss-old'."
(interactive)
+ (elfeed-sync--state-ensure)
(elfeed-sync--session
(setq elfeed-sync--start-time (time-convert nil 'integer))
(elfeed-log 'info "Sync start: %s" (format-time-string "%Y-%m-%d %H:%M:%S"))
@@ -478,6 +673,9 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(message "Error: %S" error-thrown)))))))
(defun elfeed-sync-search-missing ()
+ "Display elfeed entries that were missing in tt-rss.
+
+Should be run after `elfeed-sync'."
(interactive)
(switch-to-buffer (elfeed-search-buffer))
(unless (eq major-mode 'elfeed-search-mode)
@@ -500,8 +698,11 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(run-hooks 'elfeed-search-update-hook)))
(defun elfeed-sync-read-ttrss-missing ()
+ "Mark tt-rss entries missing in elfeed as read.
+
+Should be run after `elfeed-sync'."
(interactive)
- (when (y-or-n-p "This will read all the missing tt-rss entries. Are you sure? ")
+ (when (y-or-n-p "This will read all the missing tt-rss entries. Are you sure? ")
(elfeed-sync--session
(request (concat elfeed-sync-tt-rss-instance "/api/")
:type "POST"
@@ -523,8 +724,9 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(message "Error: %S" error-thrown)))))))
(defun elfeed-sync-read-ttrss-old ()
+ "Mark tt-rss entries older than `elfeed-sync-look-back' as read."
(interactive)
- (when (y-or-n-p "This will read all the old tt-rss entries. Are you sure? ")
+ (when (y-or-n-p "This will read all the old tt-rss entries. Are you sure? ")
(elfeed-sync--session
(request (concat elfeed-sync-tt-rss-instance "/api/")
:type "POST"
@@ -541,14 +743,31 @@ unmarked. ENTRY is an instance of `elfeed-entry'."
(cl-function (lambda (&key error-thrown &allow-other-keys)
(message "Error: %S" error-thrown)))))))
+(defun elfeed-sync--header-around (fun &rest args)
+ "Advise `elfeed-search--header' to show the sync time.
+
+FUN and ARGS are passed to `apply'."
+ (elfeed-sync--state-ensure)
+ (let ((header (apply fun args))
+ (last-sync-time (alist-get :last-sync-time elfeed-sync--state)))
+ (if last-sync-time
+ (format "%s, Synced at"
+ header
+ (format-time-string
+ "%Y-%m-%d %H:%M:%S"
+ (time-convert last-sync-time 'integer)))
+ header)))
+
;;;###autoload
- (define-minor-mode elfeed-sync-mode
- "TODO"
- :global t
- (if elfeed-sync-mode
- (progn
- (add-hook 'kill-emacs-hook #'elfeed-sync-state-save-safe))
- (remove-hook 'kill-emacs-hook #'elfeed-sync-state-save-safe)))
+(define-minor-mode elfeed-sync-mode
+ "Persist the sync state."
+ :global t
+ (if elfeed-sync-mode
+ (progn
+ (add-hook 'kill-emacs-hook #'elfeed-sync-state-save-safe)
+ (advice-add #'elfeed-search--header :around #'elfeed-sync--header-around))
+ (remove-hook 'kill-emacs-hook #'elfeed-sync-state-save-safe)
+ (advice-remove #'elfeed-search--header #'elfeed-sync--header-around)))
(provide 'elfeed-sync)