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