diff options
Diffstat (limited to 'elfeed-custom-feeds-github.el')
| -rw-r--r-- | elfeed-custom-feeds-github.el | 161 |
1 files changed, 95 insertions, 66 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 |
