diff options
| -rw-r--r-- | elfeed-custom-feeds-github.el | 161 | ||||
| -rw-r--r-- | elfeed-custom-feeds-huggingface.el | 103 |
2 files changed, 161 insertions, 103 deletions
diff --git a/elfeed-custom-feeds-github.el b/elfeed-custom-feeds-github.el index a857963..5bfdb6c 100644 --- a/elfeed-custom-feeds-github.el +++ b/elfeed-custom-feeds-github.el | |||
| @@ -50,58 +50,83 @@ | |||
| 50 | (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error)) | 50 | (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error)) |
| 51 | (goto-char (point-min)) | 51 | (goto-char (point-min)) |
| 52 | (re-search-forward "\n\n") | 52 | (re-search-forward "\n\n") |
| 53 | (let* ((json-object-type 'alist) | 53 | ;; Capture JSON text for async parsing |
| 54 | (json-array-type 'list) | 54 | (let ((json-text (buffer-substring-no-properties (point) (point-max)))) |
| 55 | (response (json-read)) | 55 | ;; Parse JSON in idle timer to avoid blocking |
| 56 | (items (alist-get 'items response))) | 56 | (run-with-idle-timer 0 nil |
| 57 | (funcall cb r dn items)))) | 57 | (lambda () |
| 58 | (condition-case err | ||
| 59 | (with-temp-buffer | ||
| 60 | (insert json-text) | ||
| 61 | (goto-char (point-min)) | ||
| 62 | (let* ((json-object-type 'alist) | ||
| 63 | (json-array-type 'list) | ||
| 64 | (response (json-read)) | ||
| 65 | (items (alist-get 'items response))) | ||
| 66 | (funcall cb r dn items))) | ||
| 67 | (error | ||
| 68 | (message "Error parsing JSON for GitHub %s [%s]: %s" r dn err))))))) | ||
| 58 | nil t))))) | 69 | nil t))))) |
| 59 | 70 | ||
| 60 | (defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs completion-callback) | 71 | (defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs completion-callback) |
| 61 | "Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME. | 72 | "Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME. |
| 62 | Call COMPLETION-CALLBACK with (repo display-name new-entries) when done." | 73 | Call COMPLETION-CALLBACK with (repo display-name new-entries) when done." |
| 63 | (let ((feed-url (format "https://github.com/%s/pulls" repo)) | 74 | (let ((feed-url (format "https://github.com/%s/pulls" repo)) |
| 64 | (new-entries ())) | 75 | (new-entries ()) |
| 65 | (let ((feed (elfeed-db-get-feed feed-url))) | 76 | (prs-to-process prs)) |
| 66 | (setf (elfeed-feed-url feed) feed-url | 77 | ;; Set up feed metadata asynchronously |
| 67 | (elfeed-feed-title feed) (format "GitHub: %s [%s]" repo display-name))) | 78 | (run-with-idle-timer 0 nil |
| 68 | 79 | (lambda () | |
| 69 | (dolist (pr prs) | 80 | (let ((feed (elfeed-db-get-feed feed-url))) |
| 70 | (let* ((number (alist-get 'number pr)) | 81 | (setf (elfeed-feed-url feed) feed-url |
| 71 | (title-text (alist-get 'title pr)) | 82 | (elfeed-feed-title feed) (format "GitHub: %s [%s]" repo display-name))))) |
| 72 | (html-url (alist-get 'html_url pr)) | 83 | |
| 73 | (state (alist-get 'state pr)) | 84 | ;; Process PRs in chunks to avoid blocking |
| 74 | (updated (alist-get 'updated_at pr)) | 85 | (cl-labels ((process-next-chunk () |
| 75 | (created (alist-get 'created_at pr)) | 86 | (when prs-to-process |
| 76 | (user (alist-get 'login (alist-get 'user pr))) | 87 | ;; Process 5 PRs at a time |
| 77 | (body (or (alist-get 'body pr) "No description provided.")) | 88 | (let ((chunk (cl-subseq prs-to-process 0 (min 5 (length prs-to-process))))) |
| 78 | (labels (mapcar (lambda (label) (alist-get 'name label)) | 89 | (setq prs-to-process (cl-subseq prs-to-process (length chunk))) |
| 79 | (alist-get 'labels pr))) | 90 | ;; Process this chunk |
| 80 | (entry-id html-url) | 91 | (dolist (pr chunk) |
| 81 | (title (format "#%d: %s" number title-text)) | 92 | (let* ((number (alist-get 'number pr)) |
| 82 | (date (float-time (date-to-time updated))) | 93 | (title-text (alist-get 'title pr)) |
| 83 | (content (format "<h2>PR #%d: %s</h2>\n<p><strong>State:</strong> %s</p>\n<p><strong>Author:</strong> %s</p>\n<p><strong>Created:</strong> %s</p>\n<p><strong>Updated:</strong> %s</p>\n%s\n<h3>Description:</h3>\n<pre>%s</pre>" | 94 | (html-url (alist-get 'html_url pr)) |
| 84 | number title-text state user created updated | 95 | (state (alist-get 'state pr)) |
| 85 | (if labels | 96 | (updated (alist-get 'updated_at pr)) |
| 86 | (format "<p><strong>Labels:</strong> %s</p>" | 97 | (created (alist-get 'created_at pr)) |
| 87 | (mapconcat 'identity labels ", ")) | 98 | (user (alist-get 'login (alist-get 'user pr))) |
| 88 | "") | 99 | (body (or (alist-get 'body pr) "No description provided.")) |
| 89 | (replace-regexp-in-string "<" "<" | 100 | (labels (mapcar (lambda (label) (alist-get 'name label)) |
| 90 | (replace-regexp-in-string ">" ">" body))))) | 101 | (alist-get 'labels pr))) |
| 91 | (unless (elfeed-db-get-entry entry-id) | 102 | (entry-id html-url) |
| 92 | (let ((entry (elfeed-entry--create | 103 | (title (format "#%d: %s" number title-text)) |
| 93 | :id entry-id | 104 | (date (float-time (date-to-time updated))) |
| 94 | :title title | 105 | (content (format "<h2>PR #%d: %s</h2>\n<p><strong>State:</strong> %s</p>\n<p><strong>Author:</strong> %s</p>\n<p><strong>Created:</strong> %s</p>\n<p><strong>Updated:</strong> %s</p>\n%s\n<h3>Description:</h3>\n<pre>%s</pre>" |
| 95 | :link html-url | 106 | number title-text state user created updated |
| 96 | :date date | 107 | (if labels |
| 97 | :content content | 108 | (format "<p><strong>Labels:</strong> %s</p>" |
| 98 | :content-type 'html | 109 | (mapconcat 'identity labels ", ")) |
| 99 | :feed-id feed-url | 110 | "") |
| 100 | :tags '(unread github)))) | 111 | (replace-regexp-in-string "<" "<" |
| 101 | (push entry new-entries))))) | 112 | (replace-regexp-in-string ">" ">" body))))) |
| 102 | 113 | (unless (elfeed-db-get-entry entry-id) | |
| 103 | ;; Call completion callback with results | 114 | (let ((entry (elfeed-entry--create |
| 104 | (funcall completion-callback repo display-name (nreverse new-entries)))) | 115 | :id entry-id |
| 116 | :title title | ||
| 117 | :link html-url | ||
| 118 | :date date | ||
| 119 | :content content | ||
| 120 | :content-type 'html | ||
| 121 | :feed-id feed-url | ||
| 122 | :tags '(unread github)))) | ||
| 123 | (push entry new-entries))))) | ||
| 124 | ;; Schedule next chunk or call completion | ||
| 125 | (if prs-to-process | ||
| 126 | (run-with-idle-timer 0 nil #'process-next-chunk) | ||
| 127 | ;; All done - call completion callback | ||
| 128 | (funcall completion-callback repo display-name (nreverse new-entries))))))) | ||
| 129 | (process-next-chunk)))) | ||
| 105 | 130 | ||
| 106 | ;;;###autoload | 131 | ;;;###autoload |
| 107 | (defun elfeed-custom-feeds-github-update () | 132 | (defun elfeed-custom-feeds-github-update () |
| @@ -120,26 +145,30 @@ Batches all entries and updates UI once at the end to avoid blocking." | |||
| 120 | ;; Completion callback that batches everything | 145 | ;; Completion callback that batches everything |
| 121 | (let ((completion-callback | 146 | (let ((completion-callback |
| 122 | (lambda (repo display-name new-entries) | 147 | (lambda (repo display-name new-entries) |
| 123 | (setq processed (1+ processed)) | 148 | (run-with-idle-timer 0 nil |
| 124 | (when new-entries | 149 | (lambda () |
| 125 | (setq all-new-entries (append all-new-entries new-entries))) | 150 | (setq processed (1+ processed)) |
| 126 | (push (list repo display-name (length new-entries)) results) | 151 | (when new-entries |
| 127 | 152 | (setq all-new-entries (append all-new-entries new-entries))) | |
| 128 | ;; When all queries are done, batch add to DB and update UI once | 153 | (push (list repo display-name (length new-entries)) results) |
| 129 | (when (= processed total) | 154 | |
| 130 | (let ((total-count (length all-new-entries))) | 155 | ;; When all queries are done, batch add to DB and update UI once |
| 131 | (when (> total-count 0) | 156 | (when (= processed total) |
| 132 | (elfeed-db-add all-new-entries)) | 157 | (let ((total-count (length all-new-entries))) |
| 133 | ;; Report results for each feed | 158 | (when (> total-count 0) |
| 134 | (dolist (result (nreverse results)) | 159 | (run-with-idle-timer 0 nil |
| 135 | (message "GitHub feed %s [%s]: %d new entries" | 160 | (lambda () |
| 136 | (nth 0 result) (nth 1 result) (nth 2 result))) | 161 | (elfeed-db-add all-new-entries)))) |
| 137 | ;; Defer UI update to avoid blocking | 162 | ;; Report results for each feed |
| 138 | (run-with-idle-timer 0.1 nil | 163 | (dolist (result (nreverse results)) |
| 139 | (lambda () | 164 | (message "GitHub feed %s [%s]: %d new entries" |
| 140 | (when (get-buffer "*elfeed-search*") | 165 | (nth 0 result) (nth 1 result) (nth 2 result))) |
| 141 | (with-current-buffer "*elfeed-search*" | 166 | ;; Defer UI update to avoid blocking |
| 142 | (elfeed-search-update--force)))))))))) | 167 | (run-with-idle-timer 0.2 nil |
| 168 | (lambda () | ||
| 169 | (when (get-buffer "*elfeed-search*") | ||
| 170 | (with-current-buffer "*elfeed-search*" | ||
| 171 | (elfeed-search-update--force))))))))))) | ||
| 143 | 172 | ||
| 144 | ;; Start all async fetches | 173 | ;; Start all async fetches |
| 145 | (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries | 174 | (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries |
diff --git a/elfeed-custom-feeds-huggingface.el b/elfeed-custom-feeds-huggingface.el index 43f64c8..81f4b5b 100644 --- a/elfeed-custom-feeds-huggingface.el +++ b/elfeed-custom-feeds-huggingface.el | |||
| @@ -47,22 +47,33 @@ | |||
| 47 | (url-retrieve url | 47 | (url-retrieve url |
| 48 | (lambda (status) | 48 | (lambda (status) |
| 49 | (if (plist-get status :error) | 49 | (if (plist-get status :error) |
| 50 | (funcall cb nil) | 50 | (run-with-idle-timer 0 nil cb nil) |
| 51 | (goto-char (point-min)) | 51 | (goto-char (point-min)) |
| 52 | (if (re-search-forward "\n\n" nil t) | 52 | (if (re-search-forward "\n\n" nil t) |
| 53 | (let* ((html (libxml-parse-html-region (point) (point-max))) | 53 | ;; Capture raw HTML for async processing |
| 54 | (content-div (or (dom-by-class html "model-card-content") | 54 | (let ((html-text (buffer-substring-no-properties (point) (point-max)))) |
| 55 | (dom-by-class html "prose") | 55 | ;; Process HTML parsing in idle timer to avoid blocking |
| 56 | (dom-by-tag html 'article)))) | 56 | (run-with-idle-timer 0 nil |
| 57 | (if content-div | 57 | (lambda () |
| 58 | (with-temp-buffer | 58 | (condition-case err |
| 59 | (dom-print (if (listp content-div) | 59 | (with-temp-buffer |
| 60 | (car content-div) | 60 | (insert html-text) |
| 61 | content-div)) | 61 | (let* ((html (libxml-parse-html-region (point-min) (point-max))) |
| 62 | (let ((html-string (buffer-string))) | 62 | (content-div (or (dom-by-class html "model-card-content") |
| 63 | (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string)))) | 63 | (dom-by-class html "prose") |
| 64 | (funcall cb nil))) | 64 | (dom-by-tag html 'article)))) |
| 65 | (funcall cb nil)))) | 65 | (if content-div |
| 66 | (with-temp-buffer | ||
| 67 | (dom-print (if (listp content-div) | ||
| 68 | (car content-div) | ||
| 69 | content-div)) | ||
| 70 | (let ((html-string (buffer-string))) | ||
| 71 | (funcall cb (elfeed-custom-feeds-huggingface--strip-svg-tags html-string)))) | ||
| 72 | (funcall cb nil)))) | ||
| 73 | (error | ||
| 74 | (message "Error parsing HTML for %s: %s" mid err) | ||
| 75 | (funcall cb nil)))))) | ||
| 76 | (run-with-idle-timer 0 nil cb nil)))) | ||
| 66 | nil t))))) | 77 | nil t))))) |
| 67 | 78 | ||
| 68 | (defun elfeed-custom-feeds-huggingface--fetch-models (org-name callback) | 79 | (defun elfeed-custom-feeds-huggingface--fetch-models (org-name callback) |
| @@ -77,10 +88,21 @@ | |||
| 77 | (message "Error fetching HF org %s: %s" org (plist-get status :error)) | 88 | (message "Error fetching HF org %s: %s" org (plist-get status :error)) |
| 78 | (goto-char (point-min)) | 89 | (goto-char (point-min)) |
| 79 | (re-search-forward "\n\n") | 90 | (re-search-forward "\n\n") |
| 80 | (let* ((json-object-type 'alist) | 91 | ;; Capture JSON text for async parsing |
| 81 | (json-array-type 'list) | 92 | (let ((json-text (buffer-substring-no-properties (point) (point-max)))) |
| 82 | (models (json-read))) | 93 | ;; Parse JSON in idle timer to avoid blocking |
| 83 | (funcall cb org models)))) | 94 | (run-with-idle-timer 0 nil |
| 95 | (lambda () | ||
| 96 | (condition-case err | ||
| 97 | (with-temp-buffer | ||
| 98 | (insert json-text) | ||
| 99 | (goto-char (point-min)) | ||
| 100 | (let* ((json-object-type 'alist) | ||
| 101 | (json-array-type 'list) | ||
| 102 | (models (json-read))) | ||
| 103 | (funcall cb org models))) | ||
| 104 | (error | ||
| 105 | (message "Error parsing JSON for HF org %s: %s" org err)))))))) | ||
| 84 | nil t))) | 106 | nil t))) |
| 85 | 107 | ||
| 86 | (defun elfeed-custom-feeds-huggingface--create-entry (org-name model model-card-html) | 108 | (defun elfeed-custom-feeds-huggingface--create-entry (org-name model model-card-html) |
| @@ -126,9 +148,11 @@ Batches all entries and updates UI once at the end to avoid blocking." | |||
| 126 | (new-entries '()) | 148 | (new-entries '()) |
| 127 | (processed 0)) | 149 | (processed 0)) |
| 128 | ;; Set up feed metadata | 150 | ;; Set up feed metadata |
| 129 | (let ((feed (elfeed-db-get-feed feed-url))) | 151 | (run-with-idle-timer 0 nil |
| 130 | (setf (elfeed-feed-url feed) feed-url | 152 | (lambda () |
| 131 | (elfeed-feed-title feed) (format "HuggingFace: %s" org-name))) | 153 | (let ((feed (elfeed-db-get-feed feed-url))) |
| 154 | (setf (elfeed-feed-url feed) feed-url | ||
| 155 | (elfeed-feed-title feed) (format "HuggingFace: %s" org-name))))) | ||
| 132 | 156 | ||
| 133 | ;; Return immediately if no models | 157 | ;; Return immediately if no models |
| 134 | (when (zerop total) | 158 | (when (zerop total) |
| @@ -144,22 +168,27 @@ Batches all entries and updates UI once at the end to avoid blocking." | |||
| 144 | model-id | 168 | model-id |
| 145 | (let ((m model)) | 169 | (let ((m model)) |
| 146 | (lambda (html) | 170 | (lambda (html) |
| 147 | (let ((entry (elfeed-custom-feeds-huggingface--create-entry org-name m html))) | 171 | ;; Process entry creation in idle timer to avoid blocking |
| 148 | (when entry | 172 | (run-with-idle-timer 0 nil |
| 149 | (push entry new-entries))) | 173 | (lambda () |
| 150 | (setq processed (1+ processed)) | 174 | (let ((entry (elfeed-custom-feeds-huggingface--create-entry org-name m html))) |
| 151 | ;; When all are processed, batch add to DB and update UI once | 175 | (when entry |
| 152 | (when (= processed total) | 176 | (push entry new-entries))) |
| 153 | (let ((count (length new-entries))) | 177 | (setq processed (1+ processed)) |
| 154 | (when (> count 0) | 178 | ;; When all are processed, batch add to DB and update UI once |
| 155 | (elfeed-db-add (nreverse new-entries))) | 179 | (when (= processed total) |
| 156 | (message "HuggingFace feed %s: %d new entries" org-name count) | 180 | (let ((count (length new-entries))) |
| 157 | ;; Defer UI update to avoid blocking | 181 | (when (> count 0) |
| 158 | (run-with-idle-timer 0.1 nil | 182 | (run-with-idle-timer 0 nil |
| 159 | (lambda () | 183 | (lambda () |
| 160 | (when (get-buffer "*elfeed-search*") | 184 | (elfeed-db-add (nreverse new-entries))))) |
| 161 | (with-current-buffer "*elfeed-search*" | 185 | (message "HuggingFace feed %s: %d new entries" org-name count) |
| 162 | (elfeed-search-update--force))))))))) | 186 | ;; Defer UI update to avoid blocking |
| 187 | (run-with-idle-timer 0.2 nil | ||
| 188 | (lambda () | ||
| 189 | (when (get-buffer "*elfeed-search*") | ||
| 190 | (with-current-buffer "*elfeed-search*" | ||
| 191 | (elfeed-search-update--force))))))))))) | ||
| 163 | delay)) | 192 | delay)) |
| 164 | nil)) | 193 | nil)) |
| 165 | 194 | ||
