dotemacs

My Emacs configuration
git clone git://git.entf.net/dotemacs
Log | Files | Refs | LICENSE

elfeed.el (28043B)


      1 ;;; elfeed.el --- an Emacs Atom/RSS feed reader -*- lexical-binding: t; -*-
      2 
      3 ;; This is free and unencumbered software released into the public domain.
      4 
      5 ;; Author: Christopher Wellons <wellons@nullprogram.com>
      6 ;; URL: https://github.com/skeeto/elfeed
      7 
      8 ;;; Commentary:
      9 
     10 ;; Elfeed is a web feed client for Emacs, inspired by notmuch. See
     11 ;; the README for full documentation.
     12 
     13 ;;; Code:
     14 
     15 (require 'cl-lib)
     16 (require 'xml)
     17 (require 'xml-query)
     18 (require 'url-parse)
     19 (require 'url-queue)
     20 
     21 (require 'elfeed-db)
     22 (require 'elfeed-lib)
     23 (require 'elfeed-log)
     24 (require 'elfeed-curl)
     25 
     26 ;; Interface to elfeed-search (lazy required)
     27 (declare-function elfeed-search-buffer 'elfeed-search ())
     28 (declare-function elfeed-search-mode   'elfeed-search ())
     29 
     30 (defgroup elfeed ()
     31   "An Emacs web feed reader."
     32   :group 'comm)
     33 
     34 (defconst elfeed-version "3.4.1")
     35 
     36 (defcustom elfeed-feeds ()
     37   "List of all feeds that Elfeed should follow.
     38 You must add your feeds to this list.
     39 
     40 In its simplest form this will be a list of strings of feed URLs.
     41 Items in this list can also be list whose car is the feed URL
     42 and cdr is a list of symbols to be applied to all discovered
     43 entries as tags (\"autotags\"). For example,
     44 
     45   (setq elfeed-feeds '(\"http://foo/\"
     46                        \"http://bar/\"
     47                        (\"http://baz/\" comic)))
     48 
     49 All entries from the \"baz\" feed will be tagged as \"comic\"
     50 when they are first discovered."
     51   :group 'elfeed
     52   :type '(repeat (choice string
     53                          (cons string (repeat symbol)))))
     54 
     55 (defcustom elfeed-feed-functions
     56   '(elfeed-get-link-at-point
     57     elfeed-get-url-at-point
     58     elfeed-clipboard-get)
     59   "List of functions to use to get possible feeds for `elfeed-add-feed'.
     60 Each function should accept no arguments, and return a string or nil."
     61   :group 'elfeed
     62   :type 'hook
     63   :options '(elfeed-get-link-at-point
     64              elfeed-get-url-at-point
     65              elfeed-clipboard-get))
     66 
     67 (defcustom elfeed-use-curl
     68   (not (null (executable-find elfeed-curl-program-name)))
     69   "If non-nil, fetch feeds using curl instead of `url-retrieve'."
     70   :group 'elfeed
     71   :type 'boolean)
     72 
     73 (defcustom elfeed-user-agent (format "Emacs Elfeed %s" elfeed-version)
     74   "User agent string to use for Elfeed (requires `elfeed-use-curl')."
     75   :group 'elfeed
     76   :type 'string)
     77 
     78 (defcustom elfeed-initial-tags '(unread)
     79   "Initial tags for new entries."
     80   :group 'elfeed
     81   :type '(repeat symbol))
     82 
     83 ;; Fetching:
     84 
     85 (defvar elfeed-http-error-hooks ()
     86   "Hooks to run when an http connection error occurs.
     87 It is called with 2 arguments. The first argument is the url of
     88 the failing feed. The second argument is the http status code.")
     89 
     90 (defvar elfeed-parse-error-hooks ()
     91   "Hooks to run when an error occurs during the parsing of a feed.
     92 It is called with 2 arguments. The first argument is the url of
     93 the failing feed. The second argument is the error message .")
     94 
     95 (defvar elfeed-update-hooks ()
     96   "Hooks to run any time a feed update has completed a request.
     97 It is called with 1 argument: the URL of the feed that was just
     98 updated. The hook is called even when no new entries were
     99 found.")
    100 
    101 (defvar elfeed-update-init-hooks ()
    102   "Hooks called when one or more feed updates have begun.
    103 Receivers may want to, say, update a display to indicate that
    104 updates are pending.")
    105 
    106 (defvar elfeed-tag-hooks ()
    107   "Hooks called when one or more entries add tags.
    108 It is called with 2 arguments. The first argument is the entry
    109 list. The second argument is the tag list.")
    110 
    111 (defvar elfeed-untag-hooks ()
    112   "Hooks called when one or more entries remove tags.
    113 It is called with 2 arguments. The first argument is the entry
    114 list. The second argument is the tag list.")
    115 
    116 (defvar elfeed--inhibit-update-init-hooks nil
    117   "When non-nil, don't run `elfeed-update-init-hooks'.")
    118 
    119 (defun elfeed-queue-count-active ()
    120   "Return the number of items in process."
    121   (if elfeed-use-curl
    122       elfeed-curl-queue-active
    123     (cl-count-if #'url-queue-buffer url-queue)))
    124 
    125 (defun elfeed-queue-count-total ()
    126   "Return the number of items in process."
    127   (if elfeed-use-curl
    128       (+ (length elfeed-curl-queue) elfeed-curl-queue-active)
    129     (length url-queue)))
    130 
    131 (defun elfeed-set-max-connections (n)
    132   "Limit the maximum number of concurrent connections to N."
    133   (if elfeed-use-curl
    134       (setf elfeed-curl-max-connections n)
    135     (setf url-queue-parallel-processes n)))
    136 
    137 (defun elfeed-get-max-connections ()
    138   "Get the maximum number of concurrent connections."
    139   (if elfeed-use-curl
    140       elfeed-curl-max-connections
    141     url-queue-parallel-processes))
    142 
    143 (defun elfeed-set-timeout (seconds)
    144   "Limit the time for fetching a feed to SECONDS."
    145   (if elfeed-use-curl
    146       (setf elfeed-curl-timeout seconds)
    147     (setf url-queue-timeout seconds)))
    148 
    149 (defun elfeed-get-timeout ()
    150   "Get the time limit for fetching feeds in SECONDS."
    151   (if elfeed-use-curl
    152       elfeed-curl-timeout
    153     url-queue-timeout))
    154 
    155 (defun elfeed-is-status-error (status use-curl)
    156   "Check if HTTP request returned status means a error."
    157   (or (and use-curl (null status)) ; nil = error
    158       (and (not use-curl) (eq (car status) :error))))
    159 
    160 (defmacro elfeed-with-fetch (url &rest body)
    161   "Asynchronously run BODY in a buffer with the contents from URL.
    162 This macro is anaphoric, with STATUS referring to the status from
    163 `url-retrieve'/cURL and USE-CURL being the original invoked-value
    164 of `elfeed-use-curl'."
    165   (declare (indent defun))
    166   `(let* ((use-curl elfeed-use-curl) ; capture current value in closure
    167           (cb (lambda (status) ,@body)))
    168      (if elfeed-use-curl
    169          (let* ((feed (elfeed-db-get-feed url))
    170                 (last-modified (elfeed-meta feed :last-modified))
    171                 (etag (elfeed-meta feed :etag))
    172                 (headers `(("User-Agent" . ,elfeed-user-agent))))
    173            (when etag
    174              (push `("If-None-Match" . ,etag) headers))
    175            (when last-modified
    176              (push `("If-Modified-Since" . ,last-modified) headers))
    177            (elfeed-curl-enqueue ,url cb :headers headers))
    178        (url-queue-retrieve ,url cb () t t))))
    179 
    180 (defun elfeed-unjam ()
    181   "Manually clear the connection pool when connections fail to timeout.
    182 This is a workaround for issues in `url-queue-retrieve'."
    183   (interactive)
    184   (if elfeed-use-curl
    185       (setf elfeed-curl-queue nil
    186             elfeed-curl-queue-active 0)
    187     (let ((fails (mapcar #'url-queue-url url-queue)))
    188       (when fails
    189         (elfeed-log 'warn "Elfeed aborted feeds: %s"
    190                     (mapconcat #'identity fails " ")))
    191       (setf url-queue nil)))
    192   (run-hooks 'elfeed-update-init-hooks))
    193 
    194 ;; Parsing:
    195 
    196 (defun elfeed-feed-type (content)
    197   "Return the feed type (:atom, :rss, :rss1.0) or nil for unknown."
    198   (let ((top (xml-query-strip-ns (caar content))))
    199     (cadr (assoc top '((feed :atom)
    200                        (rss :rss)
    201                        (RDF :rss1.0))))))
    202 
    203 (defun elfeed-generate-id (&optional content)
    204   "Generate an ID based on CONTENT or from the current time."
    205   (concat "urn:sha1:" (sha1 (format "%s" (or content (float-time))))))
    206 
    207 (defun elfeed--atom-content (entry)
    208   "Get content string from ENTRY."
    209   (let ((content-type (xml-query* (content :type) entry)))
    210     (if (equal content-type "xhtml")
    211         (with-temp-buffer
    212           (let ((xhtml (cddr (xml-query* (content) entry))))
    213             (dolist (element xhtml)
    214               (if (stringp element)
    215                   (insert element)
    216                 (elfeed-xml-unparse element))))
    217           (buffer-string))
    218       (let ((all-content
    219              (or (xml-query-all* (content *) entry)
    220                  (xml-query-all* (summary *) entry))))
    221         (when all-content
    222           (apply #'concat all-content))))))
    223 
    224 (defvar elfeed-new-entry-parse-hook '()
    225   "Hook to be called after parsing a new entry.
    226 
    227 Take three arguments: the feed TYPE, the XML structure for the
    228 entry, and the Elfeed ENTRY object. Return value is ignored, and
    229 is called for side-effects on the ENTRY object.")
    230 
    231 (defsubst elfeed--fixup-protocol (protocol url)
    232   "Prepend PROTOCOL to URL if it is protocol-relative.
    233 If PROTOCOL is nil, returns URL."
    234   (if (and protocol url (string-match-p "^//[^/]" url))
    235       (concat protocol ":" url)
    236     url))
    237 
    238 (defsubst elfeed--atom-authors-to-plist (authors)
    239   "Parse list of author XML tags into list of plists."
    240   (let ((result ()))
    241     (dolist (author authors)
    242       (let ((plist ())
    243             (name (xml-query* (name *) author))
    244             (uri (xml-query* (uri *) author))
    245             (email (xml-query* (email *) author)))
    246         (when email
    247           (setf plist (list :email (elfeed-cleanup email))))
    248         (when uri
    249           (setf plist (nconc (list :uri (elfeed-cleanup uri)) plist)))
    250         (when name
    251           (setf plist (nconc (list :name (elfeed-cleanup name)) plist)))
    252         (push plist result)))
    253     (nreverse result)))
    254 
    255 (defsubst elfeed--creators-to-plist (creators)
    256   "Convert Dublin Core list of creators into an authors plist."
    257   (cl-loop for creator in creators
    258            collect (list :name creator)))
    259 
    260 (defun elfeed-entries-from-atom (url xml)
    261   "Turn parsed Atom content into a list of elfeed-entry structs."
    262   (let* ((feed-id url)
    263          (protocol (url-type (url-generic-parse-url url)))
    264          (namespace (elfeed-url-to-namespace url))
    265          (feed (elfeed-db-get-feed feed-id))
    266          (title (elfeed-cleanup (xml-query* (feed title *) xml)))
    267          (authors (xml-query-all* (feed author) xml))
    268          (xml-base (or (xml-query* (feed :base) xml) url))
    269          (autotags (elfeed-feed-autotags url)))
    270     (setf (elfeed-feed-url feed) url
    271           (elfeed-feed-title feed) title
    272           (elfeed-feed-author feed) (elfeed--atom-authors-to-plist authors))
    273     (cl-loop for entry in (xml-query-all* (feed entry) xml) collect
    274              (let* ((title (or (xml-query* (title *) entry) ""))
    275                     (xml-base (elfeed-update-location
    276                                xml-base (xml-query* (:base) (list entry))))
    277                     (anylink (xml-query* (link :href) entry))
    278                     (altlink (xml-query* (link [rel "alternate"] :href) entry))
    279                     (link (elfeed--fixup-protocol
    280                            protocol
    281                            (elfeed-update-location xml-base
    282                                                    (or altlink anylink))))
    283                     (date (or (xml-query* (published *) entry)
    284                               (xml-query* (updated *) entry)
    285                               (xml-query* (date *) entry)
    286                               (xml-query* (modified *) entry) ; Atom 0.3
    287                               (xml-query* (issued *) entry))) ; Atom 0.3
    288                     (authors (nconc (elfeed--atom-authors-to-plist
    289                                      (xml-query-all* (author) entry))
    290                                     ;; Dublin Core
    291                                     (elfeed--creators-to-plist
    292                                      (xml-query-all* (creator *) entry))))
    293                     (categories (xml-query-all* (category :term) entry))
    294                     (content (elfeed--atom-content entry))
    295                     (id (or (xml-query* (id *) entry) link
    296                             (elfeed-generate-id content)))
    297                     (type (or (xml-query* (content :type) entry)
    298                               (xml-query* (summary :type) entry)
    299                               ""))
    300                     (tags (elfeed-normalize-tags autotags elfeed-initial-tags))
    301                     (content-type (if (string-match-p "html" type) 'html nil))
    302                     (etags (xml-query-all* (link [rel "enclosure"]) entry))
    303                     (enclosures
    304                      (cl-loop for enclosure in etags
    305                               for wrap = (list enclosure)
    306                               for href = (xml-query* (:href) wrap)
    307                               for type = (xml-query* (:type) wrap)
    308                               for length = (xml-query* (:length) wrap)
    309                               collect (list href type length)))
    310                     (db-entry (elfeed-entry--create
    311                                :title (elfeed-cleanup title)
    312                                :feed-id feed-id
    313                                :id (cons namespace (elfeed-cleanup id))
    314                                :link (elfeed-cleanup link)
    315                                :tags tags
    316                                :date (or (elfeed-float-time date) (float-time))
    317                                :content content
    318                                :enclosures enclosures
    319                                :content-type content-type
    320                                :meta `(,@(when authors
    321                                            (list :authors authors))
    322                                        ,@(when categories
    323                                            (list :categories categories))))))
    324                (dolist (hook elfeed-new-entry-parse-hook)
    325                  (funcall hook :atom entry db-entry))
    326                db-entry))))
    327 
    328 (defsubst elfeed--rss-author-to-plist (author)
    329   "Parse an RSS author element into an authors plist."
    330   (when author
    331     (let ((clean (elfeed-cleanup author)))
    332       (if (string-match "^\\(.*\\) (\\([^)]+\\))$" clean)
    333           (list (list :name (match-string 2 clean)
    334                       :email (match-string 1 clean)))
    335         (list (list :email clean))))))
    336 
    337 (defun elfeed-entries-from-rss (url xml)
    338   "Turn parsed RSS content into a list of elfeed-entry structs."
    339   (let* ((feed-id url)
    340          (protocol (url-type (url-generic-parse-url url)))
    341          (namespace (elfeed-url-to-namespace url))
    342          (feed (elfeed-db-get-feed feed-id))
    343          (title (elfeed-cleanup (xml-query* (rss channel title *) xml)))
    344          (autotags (elfeed-feed-autotags url)))
    345     (setf (elfeed-feed-url feed) url
    346           (elfeed-feed-title feed) title)
    347     (cl-loop for item in (xml-query-all* (rss channel item) xml) collect
    348              (let* ((title (or (xml-query* (title *) item) ""))
    349                     (guid (xml-query* (guid *) item))
    350                     (link (elfeed--fixup-protocol
    351                            protocol
    352                            (or (xml-query* (link *) item) guid)))
    353                     (date (or (xml-query* (pubDate *) item)
    354                               (xml-query* (date *) item)))
    355                     (authors (nconc (elfeed--rss-author-to-plist
    356                                      (xml-query* (author *) item))
    357                                     ;; Dublin Core
    358                                     (elfeed--creators-to-plist
    359                                      (xml-query-all* (creator *) item))))
    360                     (categories (xml-query-all* (category *) item))
    361                     (content (or (xml-query-all* (encoded *) item)
    362                                  (xml-query-all* (description *) item)))
    363                     (description (apply #'concat content))
    364                     (id (or guid link (elfeed-generate-id description)))
    365                     (full-id (cons namespace (elfeed-cleanup id)))
    366                     (original (elfeed-db-get-entry full-id))
    367                     (original-date (and original (elfeed-entry-date original)))
    368                     (tags (elfeed-normalize-tags autotags elfeed-initial-tags))
    369                     (etags (xml-query-all* (enclosure) item))
    370                     (enclosures
    371                      (cl-loop for enclosure in etags
    372                               for wrap = (list enclosure)
    373                               for url = (xml-query* (:url) wrap)
    374                               for type = (xml-query* (:type) wrap)
    375                               for length = (xml-query* (:length) wrap)
    376                               collect (list url type length)))
    377                     (db-entry (elfeed-entry--create
    378                                :title (elfeed-cleanup title)
    379                                :id full-id
    380                                :feed-id feed-id
    381                                :link (elfeed-cleanup link)
    382                                :tags tags
    383                                :date (elfeed-new-date-for-entry
    384                                       original-date date)
    385                                :enclosures enclosures
    386                                :content description
    387                                :content-type 'html
    388                                :meta `(,@(when authors
    389                                            (list :authors authors))
    390                                        ,@(when categories
    391                                            (list :categories categories))))))
    392                (dolist (hook elfeed-new-entry-parse-hook)
    393                  (funcall hook :rss item db-entry))
    394                db-entry))))
    395 
    396 (defun elfeed-entries-from-rss1.0 (url xml)
    397   "Turn parsed RSS 1.0 content into a list of elfeed-entry structs."
    398   (let* ((feed-id url)
    399          (namespace (elfeed-url-to-namespace url))
    400          (feed (elfeed-db-get-feed feed-id))
    401          (title (elfeed-cleanup (xml-query* (RDF channel title *) xml)))
    402          (autotags (elfeed-feed-autotags url)))
    403     (setf (elfeed-feed-url feed) url
    404           (elfeed-feed-title feed) title)
    405     (cl-loop for item in (xml-query-all* (RDF item) xml) collect
    406              (let* ((title (or (xml-query* (title *) item) ""))
    407                     (link (xml-query* (link *) item))
    408                     (date (or (xml-query* (pubDate *) item)
    409                               (xml-query* (date *) item)))
    410                     (description
    411                      (apply #'concat (xml-query-all* (description *) item)))
    412                     (id (or link (elfeed-generate-id description)))
    413                     (full-id (cons namespace (elfeed-cleanup id)))
    414                     (original (elfeed-db-get-entry full-id))
    415                     (original-date (and original (elfeed-entry-date original)))
    416                     (tags (elfeed-normalize-tags autotags elfeed-initial-tags))
    417                     (db-entry (elfeed-entry--create
    418                                :title (elfeed-cleanup title)
    419                                :id full-id
    420                                :feed-id feed-id
    421                                :link (elfeed-cleanup link)
    422                                :tags tags
    423                                :date (elfeed-new-date-for-entry
    424                                       original-date date)
    425                                :content description
    426                                :content-type 'html)))
    427                (dolist (hook elfeed-new-entry-parse-hook)
    428                  (funcall hook :rss1.0 item db-entry))
    429                db-entry))))
    430 
    431 (defun elfeed-feed-list ()
    432   "Return a flat list version of `elfeed-feeds'.
    433 Only a list of strings will be returned."
    434   ;; Validate elfeed-feeds and fail early rather than asynchronously later.
    435   (dolist (feed elfeed-feeds)
    436     (unless (cl-typecase feed
    437               (list (and (stringp (car feed))
    438                          (cl-every #'symbolp (cdr feed))))
    439               (string t))
    440       (error "elfeed-feeds malformed, bad entry: %S" feed)))
    441   (cl-loop for feed in elfeed-feeds
    442            when (listp feed) collect (car feed)
    443            else collect feed))
    444 
    445 (defun elfeed-feed-autotags (url-or-feed)
    446   "Return tags to automatically apply to all entries from URL-OR-FEED."
    447   (let ((url (if (elfeed-feed-p url-or-feed)
    448                  (or (elfeed-feed-url url-or-feed)
    449                      (elfeed-feed-id url-or-feed))
    450                url-or-feed)))
    451     (mapcar #'elfeed-keyword->symbol (cdr (assoc url elfeed-feeds)))))
    452 
    453 (defun elfeed-apply-autotags-now ()
    454   "Apply autotags to existing entries according to `elfeed-feeds'."
    455   (interactive)
    456   (with-elfeed-db-visit (entry feed)
    457     (apply #'elfeed-tag entry (elfeed-feed-autotags feed))))
    458 
    459 (defun elfeed-handle-http-error (url status)
    460   "Handle an http error during retrieval of URL with STATUS code."
    461   (cl-incf (elfeed-meta (elfeed-db-get-feed url) :failures 0))
    462   (run-hook-with-args 'elfeed-http-error-hooks url status)
    463   (elfeed-log 'error "%s: %S" url status))
    464 
    465 (defun elfeed-handle-parse-error (url error)
    466   "Handle parse error during parsing of URL with ERROR message."
    467   (cl-incf (elfeed-meta (elfeed-db-get-feed url) :failures 0))
    468   (run-hook-with-args 'elfeed-parse-error-hooks url error)
    469   (elfeed-log 'error "%s: %s" url error))
    470 
    471 (defun elfeed-update-feed (url)
    472   "Update a specific feed."
    473   (interactive (list (completing-read "Feed: " (elfeed-feed-list))))
    474   (unless elfeed--inhibit-update-init-hooks
    475     (run-hooks 'elfeed-update-init-hooks))
    476   (elfeed-with-fetch url
    477     (if (elfeed-is-status-error status use-curl)
    478         (let ((print-escape-newlines t))
    479           (elfeed-handle-http-error
    480            url (if use-curl elfeed-curl-error-message status)))
    481       (condition-case error
    482           (let ((feed (elfeed-db-get-feed url)))
    483             (unless use-curl
    484               (elfeed-move-to-first-empty-line)
    485               (set-buffer-multibyte t))
    486             (unless (eql elfeed-curl-status-code 304)
    487               ;; Update Last-Modified and Etag
    488               (setf (elfeed-meta feed :last-modified)
    489                     (cdr (assoc "last-modified" elfeed-curl-headers))
    490                     (elfeed-meta feed :etag)
    491                     (cdr (assoc "etag" elfeed-curl-headers)))
    492               (if (equal url elfeed-curl-location)
    493                   (setf (elfeed-meta feed :canonical-url) nil)
    494                 (setf (elfeed-meta feed :canonical-url) elfeed-curl-location))
    495               (let* ((xml (elfeed-xml-parse-region (point) (point-max)))
    496                      (entries (cl-case (elfeed-feed-type xml)
    497                                 (:atom (elfeed-entries-from-atom url xml))
    498                                 (:rss (elfeed-entries-from-rss url xml))
    499                                 (:rss1.0 (elfeed-entries-from-rss1.0 url xml))
    500                                 (otherwise
    501                                  (error (elfeed-handle-parse-error
    502                                          url "Unknown feed type."))))))
    503                 (elfeed-db-add entries))))
    504         (error (elfeed-handle-parse-error url error))))
    505     (unless use-curl
    506       (kill-buffer))
    507     (run-hook-with-args 'elfeed-update-hooks url)))
    508 
    509 (defun elfeed-candidate-feeds ()
    510   "Return a list of possible feeds from `elfeed-feed-functions'."
    511   (let (res)
    512     (run-hook-wrapped
    513      'elfeed-feed-functions
    514      (lambda (fun)
    515        (let* ((val (elfeed-cleanup (funcall fun))))
    516          (when (and (not (zerop (length val)))
    517                     (elfeed-looks-like-url-p val))
    518            (cl-pushnew val res :test #'equal)))
    519        nil))
    520     (nreverse res)))
    521 
    522 (cl-defun elfeed-add-feed (url &key save)
    523   "Manually add a feed to the database.
    524 If SAVE is non-nil the new value of ‘elfeed-feeds’ is saved.  When
    525 called interactively, SAVE is set to t."
    526   (interactive
    527    (list
    528     (let* ((feeds (elfeed-candidate-feeds))
    529            (prompt (if feeds (concat "URL (default " (car feeds)  "): ")
    530                      "URL: "))
    531            (input (read-from-minibuffer prompt nil nil nil nil feeds))
    532            (result (elfeed-cleanup input)))
    533       (cond ((not (zerop (length result))) result)
    534             (feeds (car feeds))
    535             ((user-error "No feed to add"))))
    536     :save t))
    537   (cl-pushnew url elfeed-feeds :test #'equal)
    538   (when save
    539     (customize-save-variable 'elfeed-feeds elfeed-feeds))
    540   (elfeed-update-feed url))
    541 
    542 ;;;###autoload
    543 (defun elfeed-update ()
    544   "Update all the feeds in `elfeed-feeds'."
    545   (interactive)
    546   (elfeed-log 'info "Elfeed update: %s"
    547               (format-time-string "%B %e %Y %H:%M:%S %Z"))
    548   (let ((elfeed--inhibit-update-init-hooks t))
    549     (mapc #'elfeed-update-feed (elfeed--shuffle (elfeed-feed-list))))
    550   (run-hooks 'elfeed-update-init-hooks)
    551   (elfeed-db-save))
    552 
    553 ;;;###autoload
    554 (defun elfeed ()
    555   "Enter elfeed."
    556   (interactive)
    557   (switch-to-buffer (elfeed-search-buffer))
    558   (unless (eq major-mode 'elfeed-search-mode)
    559     (elfeed-search-mode)))
    560 
    561 ;; New entry filtering
    562 
    563 (cl-defun elfeed-make-tagger
    564     (&key feed-title feed-url entry-title entry-link after before
    565           add remove callback)
    566   "Create a function that adds or removes tags on matching entries.
    567 
    568 FEED-TITLE, FEED-URL, ENTRY-TITLE, and ENTRY-LINK are regular
    569 expressions or a list (not <regex>), which indicates a negative
    570 match. AFTER and BEFORE are relative times (see
    571 `elfeed-time-duration'). Entries must match all provided
    572 expressions. If an entry matches, add tags ADD and remove tags
    573 REMOVE.
    574 
    575 Examples,
    576 
    577   (elfeed-make-tagger :feed-url \"youtube\\\\.com\"
    578                       :add '(video youtube))
    579 
    580   (elfeed-make-tagger :before \"1 week ago\"
    581                       :remove 'unread)
    582 
    583   (elfeed-make-tagger :feed-url \"example\\\\.com\"
    584                       :entry-title '(not \"something interesting\")
    585                       :add 'junk)
    586 
    587 The returned function should be added to `elfeed-new-entry-hook'."
    588   (let ((after-time  (and after  (elfeed-time-duration after)))
    589         (before-time (and before (elfeed-time-duration before))))
    590     (when (and add (symbolp add)) (setf add (list add)))
    591     (when (and remove (symbolp remove)) (setf remove (list remove)))
    592     (lambda (entry)
    593       (let ((feed (elfeed-entry-feed entry))
    594             (date (elfeed-entry-date entry))
    595             (case-fold-search t))
    596         (cl-flet ((match (r s)
    597                          (or (null r)
    598                              (if (listp r)
    599                                  (not (string-match-p (cl-second r) s))
    600                                (string-match-p r s)))))
    601           (when (and
    602                  (match feed-title  (elfeed-feed-title  feed))
    603                  (match feed-url    (elfeed-feed-url    feed))
    604                  (match entry-title (elfeed-entry-title entry))
    605                  (match entry-link  (elfeed-entry-link  entry))
    606                  (or (not after-time)  (> date (- (float-time) after-time)))
    607                  (or (not before-time) (< date (- (float-time) before-time))))
    608             (when add
    609               (apply #'elfeed-tag entry add))
    610             (when remove
    611               (apply #'elfeed-untag entry remove))
    612             (when callback
    613               (funcall callback entry))
    614             entry))))))
    615 
    616 ;; OPML
    617 
    618 (defun elfeed--parse-opml (xml)
    619   "Parse XML (from `xml-parse-region') into `elfeed-feeds' list."
    620   (cl-loop for (tag attr . content) in (cl-remove-if-not #'listp xml)
    621            count tag into work-around-bug  ; bug#15326
    622            when (assoc 'xmlUrl attr) collect (cdr it)
    623            else append (elfeed--parse-opml content)))
    624 
    625 ;;;###autoload
    626 (defun elfeed-load-opml (file)
    627   "Load feeds from an OPML file into `elfeed-feeds'.
    628 When called interactively, the changes to `elfeed-feeds' are
    629 saved to your customization file."
    630   (interactive "fOPML file: ")
    631   (let* ((xml (xml-parse-file file))
    632          (feeds (elfeed--parse-opml xml))
    633          (full (append feeds elfeed-feeds)))
    634     (prog1 (setf elfeed-feeds (cl-delete-duplicates full :test #'string=))
    635       (when (called-interactively-p 'any)
    636         (customize-save-variable 'elfeed-feeds elfeed-feeds)
    637         (elfeed-log 'notice "%d feeds loaded from %s" (length feeds) file)))))
    638 
    639 ;;;###autoload
    640 (defun elfeed-export-opml (file)
    641   "Export the current feed listing to OPML-formatted FILE."
    642   (interactive "FOutput OPML file: ")
    643   (with-temp-file file
    644     (let ((standard-output (current-buffer)))
    645       (princ "<?xml version=\"1.0\"?>\n")
    646       (xml-print
    647        `((opml ((version . "1.0"))
    648                (head () (title () "Elfeed Export"))
    649                (body ()
    650                      ,@(cl-loop for url in (elfeed-feed-list)
    651                                 for feed = (elfeed-db-get-feed url)
    652                                 for title = (or (elfeed-feed-title feed) "")
    653                                 collect `(outline ((xmlUrl . ,url)
    654                                                    (title . ,title)))))))))))
    655 
    656 (provide 'elfeed)
    657 
    658 (cl-eval-when (load eval)
    659   ;; run-time only, so don't load when compiling other files
    660   (unless byte-compile-root-dir
    661     (require 'elfeed-csv)
    662     (require 'elfeed-show)
    663     (require 'elfeed-search)))
    664 
    665 ;;; elfeed.el ends here