Browse Source

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.
Carsten Dominik 16 năm trước cách đây
mục cha
commit
6ccda9d79c
1 tập tin đã thay đổi với 146 bổ sung69 xóa
  1. 146 69
      lisp/org-feed.el

+ 146 - 69
lisp/org-feed.el

@@ -25,8 +25,11 @@
 ;;
 ;;; Commentary:
 
-;;  This library allows to create entries in an Org-mode file from
-;;  RSS feeds.
+;;  This module allows to create and change entries in an Org-mode
+;;  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
 ;;  -----------------------------------
@@ -42,11 +45,14 @@
 ;;  With this setup, the command `M-x org-feed-update-all' will
 ;;  collect new entries in the feed at the given URL and create
 ;;  entries as subheadings under the "ReQall Entries" heading in the
-;;  file "~/org.feeds.org". 
-;;  In addition to these standard arguments, additional keyword-value
-;;  pairs are possible.  For example, here we deselect entries with
-;;  a description containing "Reqall is typing" using the `:filter'
-;;  argument:
+;;  file "~/org.feeds.org".  You can then change and even move these
+;;  entries, and they will not be added again (see below).
+
+;;  In addition to these standard elements, additional keyword-value
+;;  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
 ;;          '(("ReQall"
@@ -55,14 +61,12 @@
 ;;             :filter my-reqall-filter)))
 ;;
 ;;     (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
 ;;         e)
 ;;
-;;  A `:template' entry in the alist would override the template
-;;  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.
+;;  See the docstring for `org-feed-alist' for more details.
 ;;
 ;;  Keeping track of previously added entries
 ;;  -----------------------------------------
@@ -77,7 +81,7 @@
 ;;
 ;;    #+DRAWERS: PROPERTIES LOGBOOK FEEDSTATUS
 ;;
-;;  Acknowledgements
+;;  Acknowledgments
 ;;  ----------------
 ;;
 ;;  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
 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
+argument.  An entry is a property list that describes a feed item.  The
+property list has properties for each field in the item, for example `:title'
+for the `<title>' field and `:pubDate' for the publication date.  In addition,
+it contains the following properties:
+
+`:item-full-text'   the full text in the <item> tag
+`:guid-permalink'   t when the guid property is a permalink
 
 :filter filter-function
-             A function to filter entries before Org nodes are
-             created from them.
+     A function to select interesting entries in the feed.  It gets a single
+     entry as parameter.  It should return the entry if it is relevant, or
+     nil if it is not.
 
 :template template-string
-             The template to create an Org node from a feed item.
-             For more control, use the `:formatter'.
+     The default action on new items in the feed is to add them as children
+     under the headline for the feed.  The template describes how the entry
+     should be formatted.  If not given, it defaults to
+     `org-feed-default-template'. 
 
 :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:
-
-`:item-full-text'   the full text in the <item> tag
-`:guid-permalink'   t when the guid property is a permalink
-
-The filter function should do only one thing:  decide whether this entry
-is worth being added now to the Org file.  The filter does not need to worry
-if the entry was added in the past, just decide if this is a junk entry,
-or something useful.  Entries with a given GUID will be added only once,
-the first time they pass the filter.
-
-Entries will be turned onto Org nodes acccording to a template.  If no
-template is given here, `org-feed-default-template' is used.  See the
-docstring of that variable for information on the template syntax.  If
-creating the node requires more logic than a template can provide, define a
-:formatter function that will take an entry and return the formatted Org
-node as a string."
+     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
   :type '(repeat
 	  (list :value ("" "http://" "" "")
@@ -152,12 +164,21 @@ node as a string."
 	   (string :tag "Headline for inbox")
 	   (repeat :inline t
 		   (choice
-		    (list :inline t :tag "Template"
-			  (const :template) (string :tag "Template"))
 		    (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"
-			  (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"
@@ -224,7 +245,7 @@ have been saved."
 	     (if (= nfeeds 1) "feed" "feeds"))))
 
 ;;;###autoload
-(defun org-feed-update (feed)
+(defun org-feed-update (feed &optional retrieve-only)
   "Get inbox items from FEED.
 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'."
@@ -239,56 +260,97 @@ it can be a list structured like an entry in `org-feed-alist'."
 	  (headline (nth 3 feed))
 	  (filter (nth 1 (memq :filter 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))
 			org-feed-default-template))
 	  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))
       (unless (and feed-buffer (bufferp feed-buffer))
 	(error "Cannot get feed %s" name))
+      (when retrieve-only
+	(throw 'exit feed-buffer))
       (setq entries (org-feed-parse-feed feed-buffer))
       (ignore-errors (kill-buffer feed-buffer))
       (save-excursion
 	(save-window-excursion
 	  (setq inbox-pos (org-feed-goto-inbox-internal file headline))
 	  (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 e (plist-put e :added
+				  (setq e (plist-put e :handled
 						     (nth 1 (assoc 
 							     (plist-get e :guid)
 							     old-status)))))
 				entries))
-	  ;; Find out which entries are new
-	  (setq new (delq nil (mapcar (lambda (e)
-					(if (plist-get e :added) nil e))
-				      entries)))
-	  ;; Parse the entries fully
-	  (setq new (mapcar 'org-feed-parse-entry new))
+	  ;; Find out which entries are new and which are changed
+	  (dolist (e entries)
+	    (if (not (plist-get e :handled))
+		(push e new)
+	      (setq olds (nth 2 (assoc (plist-get e :guid) old-status)))
+	      (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
 	  (when filter
-	    (setq new (delq nil (mapcar filter new))))
-	  (when (not new)
+	    (setq new     (delq nil (mapcar filter new))
+		  changed (delq nil (mapcar filter new))))
+
+	  (when (not (or new changed))
 	    (message "No new items in feed %s" name)
 	    (throw 'exit 0))
-	  ;; Format the new entries into an alist with GUIDs in the car
-	  (setq new (mapcar
-		     (lambda (e)
-		       (list (plist-get e :guid)
-			     (org-feed-format-entry e template formatter)))
-		     new))
-		
+
+	  ;; Get alist based on guid, to look up entries
+	  (setq guid-alist
+		(append
+		 (mapcar (lambda (e) (list (plist-get e :guid) e)) new)
+		 (mapcar (lambda (e) (list (plist-get e :guid) e)) changed)))
+
 	  ;; Construct the new status
 	  (setq status
 		(mapcar
 		 (lambda (e)
 		   (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))
-	  ;; Insert the new items
-	  (org-feed-add-items inbox-pos new)
+
+	  ;; 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
+	    (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
+	  ;; 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)
 	  
 	  ;; 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)
 	  (show-children)
 	  (org-cycle-hide-drawers 'children)
+
+	  ;; Hooks and messages
 	  (when org-feed-save-after-adding (save-buffer))
 	  (message "Added %d new item%s from feed %s to file %s, heading %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"))
   (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)
   "Find or create HEADING in FILE.
 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)
       (setq pos (point))
       (while (setq entry (pop entries))
-	(insert "\n")
-	(org-paste-subtree level (nth 1 entry)))
+	(org-paste-subtree level entry 'yank))
       (org-mark-ring-push pos))))
 
 (defun org-feed-format-entry (entry template formatter)