docs: a lot of docstrings

This commit is contained in:
Pavel Korytov 2022-05-29 12:24:20 +03:00
parent 941359b39c
commit b4f37a8800
2 changed files with 339 additions and 33 deletions

87
README.org Normal file
View file

@ -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.

View file

@ -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 <https://www.gnu.org/licenses/>.
;;; 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
;; <https://github.con/SqrtMinusOne/elfeed-sync> 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,11 +324,11 @@ 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)
@ -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:
<entry-title>---<entry-updated-at>. 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:
<entry-title>---<entry-updated-at>. 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,6 +698,9 @@ 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? ")
(elfeed-sync--session
@ -523,6 +724,7 @@ 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? ")
(elfeed-sync--session
@ -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"
"Persist the sync state."
: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)))
(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)