0
0
Fork 1
mirror of https://git.savannah.gnu.org/git/emacs/org-mode.git synced 2024-09-30 02:30:03 +00:00

org-feed.el: More improvements.

This now keep a memory of what the items in the feed looked like using
a sha1 hash.  Therefore we now have the capability to trigger on item
*change* rather than addition.
This commit is contained in:
Carsten Dominik 2009-03-26 18:21:57 +01:00
parent f8ae635ba3
commit 6ccda9d79c

View file

@ -25,8 +25,11 @@
;; ;;
;;; Commentary: ;;; Commentary:
;; This library allows to create entries in an Org-mode file from ;; This module allows to create and change entries in an Org-mode
;; RSS feeds. ;; file triggered by items in an RSS feed. The basic functionality is
;; geared toward simply adding items found in a feed as outline nodes
;; in an Org file, but using hooks, other actions can be performed
;; including changing entries based on changes of items in the feed.
;; ;;
;; Selecting feeds and target locations ;; Selecting feeds and target locations
;; ----------------------------------- ;; -----------------------------------
@ -42,11 +45,14 @@
;; With this setup, the command `M-x org-feed-update-all' will ;; With this setup, the command `M-x org-feed-update-all' will
;; collect new entries in the feed at the given URL and create ;; collect new entries in the feed at the given URL and create
;; entries as subheadings under the "ReQall Entries" heading in the ;; entries as subheadings under the "ReQall Entries" heading in the
;; file "~/org.feeds.org". ;; file "~/org.feeds.org". You can then change and even move these
;; In addition to these standard arguments, additional keyword-value ;; entries, and they will not be added again (see below).
;; pairs are possible. For example, here we deselect entries with
;; a description containing "Reqall is typing" using the `:filter' ;; In addition to these standard elements, additional keyword-value
;; argument: ;; pairs are possible as part of a feed setting. For example, here
;; we de-select entries with a title containing
;; "reQall is typing what you said"
;; using the `:filter' argument:
;; ;;
;; (setq org-feed-alist ;; (setq org-feed-alist
;; '(("ReQall" ;; '(("ReQall"
@ -55,14 +61,12 @@
;; :filter my-reqall-filter))) ;; :filter my-reqall-filter)))
;; ;;
;; (defun my-reqall-filter (e) ;; (defun my-reqall-filter (e)
;; (if (string-match "Reqall is typing" (plist-get e :description)) ;; (if (string-match "reQall is typing what you said"
;; (plist-get e :title))
;; nil ;; nil
;; e) ;; e)
;; ;;
;; A `:template' entry in the alist would override the template ;; See the docstring for `org-feed-alist' for more details.
;; in `org-feed-default-template' for the construction of the outline
;; node to be inserted. You may also write your own function to format
;; the entry and specify it using the `:formatter' keyword.
;; ;;
;; Keeping track of previously added entries ;; Keeping track of previously added entries
;; ----------------------------------------- ;; -----------------------------------------
@ -77,7 +81,7 @@
;; ;;
;; #+DRAWERS: PROPERTIES LOGBOOK FEEDSTATUS ;; #+DRAWERS: PROPERTIES LOGBOOK FEEDSTATUS
;; ;;
;; Acknowledgements ;; Acknowledgments
;; ---------------- ;; ----------------
;; ;;
;; org-feed.el is based on ideas by Brad Bozarth who implemented a ;; org-feed.el is based on ideas by Brad Bozarth who implemented a
@ -109,40 +113,48 @@ URL the Feed URL
file the target Org file where entries should be listed file the target Org file where entries should be listed
headline the headline under which entries should be listed headline the headline under which entries should be listed
Additional argumetns can be given using keyword-value pairs: Additional arguments can be given using keyword-value pairs. Many of these
specify functions that receive one or a list of \"entries\" as their single
:filter filter-function argument. An entry is a property list that describes a feed item. The
A function to filter entries before Org nodes are property list has properties for each field in the item, for example `:title'
created from them. for the `<title>' field and `:pubDate' for the publication date. In addition,
:template template-string
The template to create an Org node from a feed item.
For more control, use the `:formatter'.
:formatter formatter-function
A function to filter entries before Org nodes are
created from them.
The filter function gets as a argument a property list describing the item.
That list has a property for each field, for example `:title' for the
`<title>' field and `:pubDate' for the publication date. In addition,
it contains the following properties: it contains the following properties:
`:item-full-text' the full text in the <item> tag `:item-full-text' the full text in the <item> tag
`:guid-permalink' t when the guid property is a permalink `:guid-permalink' t when the guid property is a permalink
The filter function should do only one thing: decide whether this entry :filter filter-function
is worth being added now to the Org file. The filter does not need to worry A function to select interesting entries in the feed. It gets a single
if the entry was added in the past, just decide if this is a junk entry, entry as parameter. It should return the entry if it is relevant, or
or something useful. Entries with a given GUID will be added only once, nil if it is not.
the first time they pass the filter.
Entries will be turned onto Org nodes acccording to a template. If no :template template-string
template is given here, `org-feed-default-template' is used. See the The default action on new items in the feed is to add them as children
docstring of that variable for information on the template syntax. If under the headline for the feed. The template describes how the entry
creating the node requires more logic than a template can provide, define a should be formatted. If not given, it defaults to
:formatter function that will take an entry and return the formatted Org `org-feed-default-template'.
node as a string."
:formatter formatter-function
Instead of relying on a template, you may specify a function to format
the outline node to be inserted as a child. This function gets passed
a property list describing a single feed item, and it should return a
string that is a properly formatted Org outline node of level 1.
:new-handler function
If adding new items as children to the outline is not what you want
to do with new items, define a handler function that is called with
a list of all new items in the feed, each one represented as a property
list. The handler should do what needs to be done, and org-feed will
mark all items given to this handler as \"handled\", i.e. they will not
be passed to this handler again in future readings of the feed.
When the handler is called, point will be at the feed headline.
:changed-handler function
This function gets passed a list of all entries that have been
handled before, but are now still in the feed and have *changed*
since last handled (as evidenced by a different sha1 hash).
When the handler is called, point will be at the feed headline.
"
:group 'org-feed :group 'org-feed
:type '(repeat :type '(repeat
(list :value ("" "http://" "" "") (list :value ("" "http://" "" "")
@ -152,12 +164,21 @@ node as a string."
(string :tag "Headline for inbox") (string :tag "Headline for inbox")
(repeat :inline t (repeat :inline t
(choice (choice
(list :inline t :tag "Template"
(const :template) (string :tag "Template"))
(list :inline t :tag "Filter" (list :inline t :tag "Filter"
(const :filter) (symbol :tag "Filter Function")) (const :filter)
(symbol :tag "Filter Function"))
(list :inline t :tag "Template"
(const :template)
(string :tag "Template"))
(list :inline t :tag "Formatter" (list :inline t :tag "Formatter"
(const :filter) (symbol :tag "Formatter Function")) (const :formatter)
(symbol :tag "Formatter Function"))
(list :inline t :tag "New items handler"
(const :new-handler)
(symbol :tag "Handler Function"))
(list :inline t :tag "Changed items"
(const :changed-handler)
(symbol :tag "Handler Function"))
))))) )))))
(defcustom org-feed-default-template "\n* %h\n %U\n %description\n %a\n" (defcustom org-feed-default-template "\n* %h\n %U\n %description\n %a\n"
@ -224,7 +245,7 @@ have been saved."
(if (= nfeeds 1) "feed" "feeds")))) (if (= nfeeds 1) "feed" "feeds"))))
;;;###autoload ;;;###autoload
(defun org-feed-update (feed) (defun org-feed-update (feed &optional retrieve-only)
"Get inbox items from FEED. "Get inbox items from FEED.
FEED can be a string with an association in `org-feed-alist', or FEED can be a string with an association in `org-feed-alist', or
it can be a list structured like an entry in `org-feed-alist'." it can be a list structured like an entry in `org-feed-alist'."
@ -239,56 +260,97 @@ it can be a list structured like an entry in `org-feed-alist'."
(headline (nth 3 feed)) (headline (nth 3 feed))
(filter (nth 1 (memq :filter feed))) (filter (nth 1 (memq :filter feed)))
(formatter (nth 1 (memq :formatter feed))) (formatter (nth 1 (memq :formatter feed)))
(new-handler (nth 1 (memq :new-handler feed)))
(changed-handler (nth 1 (memq :changed-handler feed)))
(template (or (nth 1 (memq :template feed)) (template (or (nth 1 (memq :template feed))
org-feed-default-template)) org-feed-default-template))
feed-buffer inbox-pos feed-buffer inbox-pos
entries old-status status new e guid) entries old-status status new changed guid-alist e guid olds)
(setq feed-buffer (org-feed-get-feed url)) (setq feed-buffer (org-feed-get-feed url))
(unless (and feed-buffer (bufferp feed-buffer)) (unless (and feed-buffer (bufferp feed-buffer))
(error "Cannot get feed %s" name)) (error "Cannot get feed %s" name))
(when retrieve-only
(throw 'exit feed-buffer))
(setq entries (org-feed-parse-feed feed-buffer)) (setq entries (org-feed-parse-feed feed-buffer))
(ignore-errors (kill-buffer feed-buffer)) (ignore-errors (kill-buffer feed-buffer))
(save-excursion (save-excursion
(save-window-excursion (save-window-excursion
(setq inbox-pos (org-feed-goto-inbox-internal file headline)) (setq inbox-pos (org-feed-goto-inbox-internal file headline))
(setq old-status (org-feed-read-previous-status inbox-pos)) (setq old-status (org-feed-read-previous-status inbox-pos))
;; Add the "added" status to the appropriate entries ;; Add the "handled" status to the appropriate entries
(setq entries (mapcar (lambda (e) (setq entries (mapcar (lambda (e)
(setq e (plist-put e :added (setq e (plist-put e :handled
(nth 1 (assoc (nth 1 (assoc
(plist-get e :guid) (plist-get e :guid)
old-status))))) old-status)))))
entries)) entries))
;; Find out which entries are new ;; Find out which entries are new and which are changed
(setq new (delq nil (mapcar (lambda (e) (dolist (e entries)
(if (plist-get e :added) nil e)) (if (not (plist-get e :handled))
entries))) (push e new)
;; Parse the entries fully (setq olds (nth 2 (assoc (plist-get e :guid) old-status)))
(setq new (mapcar 'org-feed-parse-entry new)) (if (and olds
(not (string= (sha1-string (plist-get e :item-full-text))
olds)))
(push e changed))))
;; Parse the relevant entries fully
(setq new (mapcar 'org-feed-parse-entry new)
changed (mapcar 'org-feed-parse-entry changed))
;; Run the filter ;; Run the filter
(when filter (when filter
(setq new (delq nil (mapcar filter new)))) (setq new (delq nil (mapcar filter new))
(when (not new) changed (delq nil (mapcar filter new))))
(when (not (or new changed))
(message "No new items in feed %s" name) (message "No new items in feed %s" name)
(throw 'exit 0)) (throw 'exit 0))
;; Format the new entries into an alist with GUIDs in the car
(setq new (mapcar ;; Get alist based on guid, to look up entries
(lambda (e) (setq guid-alist
(list (plist-get e :guid) (append
(org-feed-format-entry e template formatter))) (mapcar (lambda (e) (list (plist-get e :guid) e)) new)
new)) (mapcar (lambda (e) (list (plist-get e :guid) e)) changed)))
;; Construct the new status ;; Construct the new status
(setq status (setq status
(mapcar (mapcar
(lambda (e) (lambda (e)
(setq guid (plist-get e :guid)) (setq guid (plist-get e :guid))
(list guid (if (assoc guid new) t (plist-get e :added)))) (list guid
;; things count as handled if we handle them now,
;; or if they were handled previously
(if (assoc guid guid-alist) t (plist-get e :handled))
;; A hash, to detect changes
(sha1-string (plist-get e :item-full-text))))
entries)) entries))
;; Handle new items in the feed
(when new
(if new-handler
(progn
(goto-char inbox-pos)
(funcall new-handler new))
;; No custom handler, do the default adding
;; Format the new entries into an alist with GUIDs in the car
(setq new-formatted
(mapcar
(lambda (e) (org-feed-format-entry e template formatter))
new)))
;; Insert the new items ;; Insert the new items
(org-feed-add-items inbox-pos new) (org-feed-add-items inbox-pos new-formatted))
;; Handle changed items in the feed
(when (and changed-handler changed)
(goto-char inbox-pos)
(funcall changed-handler changed))
;; Write the new status ;; Write the new status
;; We do this only now, in case something goes wrong above, so
;; that would would end up with a status that does not reflect
;; which items truely have been handled
(org-feed-write-status inbox-pos status) (org-feed-write-status inbox-pos status)
;; Normalize the visibility of the inbox tree ;; Normalize the visibility of the inbox tree
@ -296,6 +358,8 @@ it can be a list structured like an entry in `org-feed-alist'."
(hide-subtree) (hide-subtree)
(show-children) (show-children)
(org-cycle-hide-drawers 'children) (org-cycle-hide-drawers 'children)
;; Hooks and messages
(when org-feed-save-after-adding (save-buffer)) (when org-feed-save-after-adding (save-buffer))
(message "Added %d new item%s from feed %s to file %s, heading %s" (message "Added %d new item%s from feed %s to file %s, heading %s"
(length new) (if (> (length new) 1) "s" "") (length new) (if (> (length new) 1) "s" "")
@ -316,6 +380,20 @@ it can be a list structured like an entry in `org-feed-alist'."
(error "No such feed in `org-feed-alist")) (error "No such feed in `org-feed-alist"))
(org-feed-goto-inbox-internal (nth 2 feed) (nth 3 feed))) (org-feed-goto-inbox-internal (nth 2 feed) (nth 3 feed)))
;;;###autoload
(defun org-feed-show-raw-feed (feed)
"Show the raw feed buffer of a feed."
(interactive
(list (if (= (length org-feed-alist) 1)
(car org-feed-alist)
(org-completing-read "Feed name: " org-feed-alist))))
(if (stringp feed) (setq feed (assoc feed org-feed-alist)))
(unless feed
(error "No such feed in `org-feed-alist"))
(switch-to-buffer
(org-feed-update feed 'retrieve-only))
(goto-char (point-min)))
(defun org-feed-goto-inbox-internal (file heading) (defun org-feed-goto-inbox-internal (file heading)
"Find or create HEADING in FILE. "Find or create HEADING in FILE.
Switch to that buffer, and return the position of that headline." Switch to that buffer, and return the position of that headline."
@ -374,8 +452,7 @@ This will find the FEEDSTATUS drawer and extract the alist."
(beginning-of-line 2) (beginning-of-line 2)
(setq pos (point)) (setq pos (point))
(while (setq entry (pop entries)) (while (setq entry (pop entries))
(insert "\n") (org-paste-subtree level entry 'yank))
(org-paste-subtree level (nth 1 entry)))
(org-mark-ring-push pos)))) (org-mark-ring-push pos))))
(defun org-feed-format-entry (entry template formatter) (defun org-feed-format-entry (entry template formatter)