git.fiddlerwoaroof.com
Raw Blame History
(declaim (optimize (speed 0) (safety 3) (debug 3)))
(in-package :alimenta.pull-feed)

(defmacro setup-libraries-for-feeds (&body body)
  `(let ((plump:*tag-dispatchers* plump:*xml-tags*)
         (drakma:*drakma-default-external-format* :utf-8)
         (drakma:*text-content-types*
           (pairlis '("application" "application" "application" "application")
                    '("atom+xml"    "rss+xml"     "xml"         "rdf+xml")
                    drakma:*text-content-types*)))
     ,@body))

(defvar *user-agent* "alimenta/0.0")

(defun call-with-user-agent (user-agent cb &rest args)
  (let ((*user-agent* user-agent))
    (apply cb args)))

(defun let-bind-special-var-macro-body (var value body)
  `(let ((,var ,value))
     ,@body))

(defmacro with-user-agent ((user-agent) &body body)
  (let-bind-special-var-macro-body '*user-agent* user-agent body))

(defun fetch-doc-from-url (url)
  (setup-libraries-for-feeds
    (let ((data (drakma:http-request url :user-agent *user-agent* :decode-content t)))
      (when (and (not (stringp data))
                 (vectorp data))
        (setf data (babel:octets-to-string data)))
      (plump:parse data))))

(define-condition fetch-error (error) ())
(define-condition feed-ambiguous (fetch-error) ((choices :initarg :choices :initform nil)))
(define-condition no-feed (fetch-error) ((url :initarg :url :initform nil)))

(defun skip-feed (&optional condition)
  (when-let ((restart (find-restart 'skip-feed condition)))
    (invoke-restart restart)))

(defun feed-ambiguous (choices)
  (error 'feed-ambiguous
         :choices choices))

(defun no-feed (url)
  (cerror "Skip this feed" 'no-feed :url url))

(defun fetch-feed-from-url (url &key type)
  (setup-libraries-for-feeds
    (let* ((feeds (alimenta.discover:discover-feed
                   (drakma:http-request url
                                        :user-agent *user-agent*
                                        :decode-content t)))
           (feeds (if type (remove-if-not (lambda (x) (eql type (car x)))
                                          feeds)
                      feeds)))
      (if (not feeds)
          (no-feed url)
          (fetch-doc-from-url
           (cdar
            (restart-case
                (if (cdr feeds) (feed-ambiguous feeds) feeds)
              (take-first-feed nil
                :report (lambda (s) (format s "Take the first feed"))
                feeds)
              (take-nth-feed (n)
                :report (lambda (s) (format s "Take the nth feed"))
                (list (elt feeds n)))
              (select-feed (selector)
                :report (lambda (s) (format s "Provide a function to select the right feed"))
                (find-if selector feeds)))))))))

(defmacro with-retry ((retry-message &rest args) action &body other-restarts)
  `(loop (restart-case (return ,action)
           (retry ()
             :report (lambda (s) (format s ,retry-message ,@args)))
           ,@other-restarts)))

(defun pull-feed (url &key detect type)
  (with-retry ("Retry fetching feed ~a" url)
    (to-feed
     (if detect
         (fetch-feed-from-url url)
         (fetch-doc-from-url url))
     :type type)
    (skip-feed () (return)
               :report (lambda (s) (format s "Skip fetching feed ~a" url)))))