elfeed-custom-feeds.el

Custom feed generators (GitHub PR/issues and HuggingFace model releases) for elfeed.
Log | Files | Refs | LICENSE

commit f6e925363cd72b0cdfd3ec5d96ab2faf0e569313
parent dd583bc92c00994ef41b384f40ff8b9da11dec84
Author: Vineet Kumar <git@vineetk.net>
Date:   Sat,  7 Mar 2026 17:52:17 -0500

replace url-retrieve with elfeed-curl-enqueue to eliminate blocking

url-retrieve blocks emacs during dns resolution and tls handshake.
elfeed-curl-enqueue runs curl as a subprocess, making fetches fully
non-blocking and mirrors regular elfeed operation.
Also adds Accept header for GitHub API requests.

Diffstat:
Melfeed-custom-feeds-github.el | 124+++++++++++++++++++++++++++++++++++++------------------------------------------
Melfeed-custom-feeds-huggingface.el | 114++++++++++++++++++++++++++++++++++++-------------------------------------------
2 files changed, 110 insertions(+), 128 deletions(-)

diff --git a/elfeed-custom-feeds-github.el b/elfeed-custom-feeds-github.el @@ -11,7 +11,7 @@ ;;; Code: (require 'elfeed) -(require 'url) +(require 'elfeed-curl) (require 'json) (require 'cl-lib) @@ -44,29 +44,27 @@ (dn display-name)) (run-at-time delay nil (lambda () - (url-retrieve url - (lambda (status) - (if (plist-get status :error) - (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error)) - (goto-char (point-min)) - (re-search-forward "\n\n") - ;; Capture JSON text for async parsing - (let ((json-text (buffer-substring-no-properties (point) (point-max)))) - ;; Parse JSON in idle timer to avoid blocking - (run-with-idle-timer 0 nil - (lambda () - (condition-case err - (with-temp-buffer - (insert json-text) - (goto-char (point-min)) - (let* ((json-object-type 'alist) - (json-array-type 'list) - (response (json-read)) - (items (alist-get 'items response))) - (funcall cb r dn items))) - (error - (message "Error parsing JSON for GitHub %s [%s]: %s" r dn err))))))) - nil t))))) + (elfeed-curl-enqueue url + (lambda (status) + (if (not status) + (message "Error fetching GitHub PRs %s [%s]: %s" r dn elfeed-curl-error-message) + ;; Capture JSON text for async parsing + (let ((json-text (buffer-substring-no-properties (point-min) (point-max)))) + ;; Parse JSON in idle timer to avoid blocking + (run-with-idle-timer 0 nil + (lambda () + (condition-case err + (with-temp-buffer + (insert json-text) + (goto-char (point-min)) + (let* ((json-object-type 'alist) + (json-array-type 'list) + (response (json-read)) + (items (alist-get 'items response))) + (funcall cb r dn items))) + (error + (message "Error parsing JSON for GitHub %s [%s]: %s" r dn err)))))))) + :headers '(("Accept" . "application/vnd.github+json"))))))) (defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs completion-callback) "Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME. @@ -126,7 +124,7 @@ Call COMPLETION-CALLBACK with (repo display-name new-entries) when done." (run-with-idle-timer 0 nil #'process-next-chunk) ;; All done - call completion callback (funcall completion-callback repo display-name (nreverse new-entries))))))) - (process-next-chunk))))) + (process-next-chunk)))) ;;;###autoload (defun elfeed-custom-feeds-github-update () @@ -138,48 +136,42 @@ Batches all entries and updates UI once at the end to avoid blocking." (all-new-entries '()) (results '())) - (when (zerop total) - (message "No GitHub PR queries configured") - (cl-return-from elfeed-custom-feeds-github-update nil)) - - ;; Completion callback that batches everything - (let ((completion-callback - (lambda (repo display-name new-entries) - (run-with-idle-timer 0 nil - (lambda () - (setq processed (1+ processed)) - (when new-entries - (setq all-new-entries (append all-new-entries new-entries))) - (push (list repo display-name (length new-entries)) results) - - ;; When all queries are done, batch add to DB and update UI once - (when (= processed total) - (let ((total-count (length all-new-entries))) - (when (> total-count 0) - (run-with-idle-timer 0 nil + (if (zerop total) + (message "No GitHub PR queries configured") + (let ((completion-callback + (lambda (repo display-name new-entries) + (run-with-idle-timer 0 nil + (lambda () + (setq processed (1+ processed)) + (when new-entries + (setq all-new-entries (append all-new-entries new-entries))) + (push (list repo display-name (length new-entries)) results) + ;; When all queries are done, batch add to DB and update UI once + (when (= processed total) + (let ((total-count (length all-new-entries))) + (when (> total-count 0) + (run-with-idle-timer 0 nil + (lambda () + (elfeed-db-add all-new-entries)))) + ;; Report results for each feed + (dolist (result (nreverse results)) + (message "GitHub feed %s [%s]: %d new entries" + (nth 0 result) (nth 1 result) (nth 2 result))) + ;; Defer UI update to avoid blocking + (run-with-idle-timer 0.2 nil (lambda () - (elfeed-db-add all-new-entries)))) - ;; Report results for each feed - (dolist (result (nreverse results)) - (message "GitHub feed %s [%s]: %d new entries" - (nth 0 result) (nth 1 result) (nth 2 result))) - ;; Defer UI update to avoid blocking - (run-with-idle-timer 0.2 nil - (lambda () - (when (get-buffer "*elfeed-search*") - (with-current-buffer "*elfeed-search*" - (elfeed-search-update--force))))))))))))) - - ;; Start all async fetches - (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries - for i from 0 - for delay = (* i elfeed-custom-github-request-delay) - do (elfeed-custom-feeds-github--fetch-prs - repo query display-name - (lambda (r dn items) - (elfeed-custom-feeds-github--prs-to-elfeed r dn items completion-callback)) - delay))) - nil) + (when (get-buffer "*elfeed-search*") + (with-current-buffer "*elfeed-search*" + (elfeed-search-update--force)))))))))))) + ;; Start all async fetches + (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries + for i from 0 + for delay = (* i elfeed-custom-github-request-delay) + do (elfeed-custom-feeds-github--fetch-prs + repo query display-name + (lambda (r dn items) + (elfeed-custom-feeds-github--prs-to-elfeed r dn items completion-callback)) + delay)))))) (provide 'elfeed-custom-feeds-github) diff --git a/elfeed-custom-feeds-huggingface.el b/elfeed-custom-feeds-huggingface.el @@ -11,7 +11,7 @@ ;;; Code: (require 'elfeed) -(require 'url) +(require 'elfeed-curl) (require 'json) (require 'cl-lib) (require 'dom) @@ -44,37 +44,33 @@ (mid model-id)) (run-at-time delay nil (lambda () - (url-retrieve url - (lambda (status) - (if (plist-get status :error) - (run-with-idle-timer 0 nil cb nil) - (goto-char (point-min)) - (if (re-search-forward "\n\n" nil t) - ;; Capture raw HTML for async processing - (let ((html-text (buffer-substring-no-properties (point) (point-max)))) - ;; Process HTML parsing in idle timer to avoid blocking - (run-with-idle-timer 0 nil - (lambda () - (condition-case err - (with-temp-buffer - (insert html-text) - (let* ((html (libxml-parse-html-region (point-min) (point-max))) - (content-div (or (dom-by-class html "model-card-content") - (dom-by-class html "prose") - (dom-by-tag html 'article)))) - (if content-div - (with-temp-buffer - (dom-print (if (listp content-div) - (car content-div) - content-div)) - (let ((html-string (buffer-string))) - (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string)))) - (funcall cb nil)))) - (error - (message "Error parsing HTML for %s: %s" mid err) - (funcall cb nil)))))) - (run-with-idle-timer 0 nil cb nil)))) - nil t))))) + (elfeed-curl-enqueue url + (lambda (status) + (if (not status) + (run-with-idle-timer 0 nil cb nil) + ;; Capture raw HTML for async processing + (let ((html-text (buffer-substring-no-properties (point-min) (point-max)))) + ;; Process HTML parsing in idle timer to avoid blocking + (run-with-idle-timer 0 nil + (lambda () + (condition-case err + (with-temp-buffer + (insert html-text) + (let* ((html (libxml-parse-html-region (point-min) (point-max))) + (content-div (or (dom-by-class html "model-card-content") + (dom-by-class html "prose") + (dom-by-tag html 'article)))) + (if content-div + (with-temp-buffer + (dom-print (if (listp content-div) + (car content-div) + content-div)) + (let ((html-string (buffer-string))) + (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string)))) + (funcall cb nil)))) + (error + (message "Error parsing HTML for %s: %s" mid err) + (funcall cb nil))))))))))))) (defun elfeed-custom-feeds-huggingface--fetch-models (org-name callback) "Fetch models for ORG-NAME from HuggingFace API and call CALLBACK with results." @@ -82,28 +78,25 @@ (url-hexify-string org-name))) (cb callback) (org org-name)) - (url-retrieve url - (lambda (status) - (if (plist-get status :error) - (message "Error fetching HF org %s: %s" org (plist-get status :error)) - (goto-char (point-min)) - (re-search-forward "\n\n") - ;; Capture JSON text for async parsing - (let ((json-text (buffer-substring-no-properties (point) (point-max)))) - ;; Parse JSON in idle timer to avoid blocking - (run-with-idle-timer 0 nil - (lambda () - (condition-case err - (with-temp-buffer - (insert json-text) - (goto-char (point-min)) - (let* ((json-object-type 'alist) - (json-array-type 'list) - (models (json-read))) - (funcall cb org models))) - (error - (message "Error parsing JSON for HF org %s: %s" org err)))))))) - nil t))) + (elfeed-curl-enqueue url + (lambda (status) + (if (not status) + (message "Error fetching HF org %s: %s" org elfeed-curl-error-message) + ;; Capture JSON text for async parsing + (let ((json-text (buffer-substring-no-properties (point-min) (point-max)))) + ;; Parse JSON in idle timer to avoid blocking + (run-with-idle-timer 0 nil + (lambda () + (condition-case err + (with-temp-buffer + (insert json-text) + (goto-char (point-min)) + (let* ((json-object-type 'alist) + (json-array-type 'list) + (models (json-read))) + (funcall cb org models))) + (error + (message "Error parsing JSON for HF org %s: %s" org err))))))))))) (defun elfeed-custom-feeds-huggingface--create-entry (org-name model model-card-html) "Create an elfeed entry for MODEL with model card HTML content. @@ -154,13 +147,10 @@ Batches all entries and updates UI once at the end to avoid blocking." (setf (elfeed-feed-url feed) feed-url (elfeed-feed-title feed) (format "HuggingFace: %s" org-name))))) - ;; Return immediately if no models - (when (zerop total) - (message "HuggingFace feed %s: no models to process" org-name) - (cl-return-from elfeed-custom-feeds-huggingface--models-to-elfeed nil)) - - ;; Fetch model cards asynchronously and batch entries - (cl-loop for model in models + (if (zerop total) + (message "HuggingFace feed %s: no models to process" org-name) + ;; Fetch model cards asynchronously and batch entries + (cl-loop for model in models for i from 0 for model-id = (alist-get 'id model) for delay = (* i elfeed-custom-huggingface-request-delay) @@ -190,7 +180,7 @@ Batches all entries and updates UI once at the end to avoid blocking." (with-current-buffer "*elfeed-search*" (elfeed-search-update--force))))))))))) delay)) - nil)) + nil))) ;;;###autoload (defun elfeed-custom-feeds-huggingface-update ()