11; ;; stock-tracker.el --- Track stock price -*- lexical-binding : t ; -*-
22
3- ; ; Copyright (C) 2019-2025 Huming Chen
3+ ; ; Copyright (C) 2019-2026 Huming Chen
44
55; ; Author: Huming Chen <chenhuming@gmail.com>
66; ; URL: https://github.com/beacoder/stock-tracker
7- ; ; Version: 0.1.8
7+ ; ; Version: 0.2.0
88; ; Created: 2019-08-18
99; ; Keywords: convenience, stock, finance
1010; ; Package-Requires: ((emacs "27.1") (dash "2.16.0") (async "1.9.5"))
5353; ; Add test for both CHN and US stocks
5454; ; 0.1.8 Disable test for CHN stocks due to API not working
5555; ; 0.1.9 Fix wrong stock order issue for us-stocks during auto-refreshing
56+ ; ; 0.2.0 Add curl support, and default to use curl, fallback to url
5657
5758; ;; Code:
5859
101102 :type 'boolean
102103 :group 'stock-tracker )
103104
105+ (defcustom stock-tracker-fetch-backend 'curl
106+ " Backend used for HTTP requests.
107+ When set to 'url, use `url-retrieve-synchronously' .
108+ When set to 'curl, use the curl command-line tool.
109+ When set to 'auto, use curl if available, otherwise fall back to url."
110+ :type '(choice (const :tag " url-retrieve-synchronously" url)
111+ (const :tag " curl" curl)
112+ (const :tag " auto-detect" auto))
113+ :group 'stock-tracker )
114+
115+ (defcustom stock-tracker-curl-program " curl"
116+ " Name or path of the curl executable."
117+ :type 'string
118+ :group 'stock-tracker )
119+
120+ (defcustom stock-tracker-curl-timeout 10
121+ " Timeout in seconds for curl requests."
122+ :type 'integer
123+ :group 'stock-tracker )
124+
104125; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
105126; ; Interface
106127; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -300,20 +321,67 @@ It defaults to a comma."
300321; ; Core Functions
301322; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
302323
324+ (defun stock-tracker--effective-backend ()
325+ " Return the effective fetch backend, resolving 'auto to curl or url."
326+ (cond
327+ ((eq stock-tracker-fetch-backend 'curl ) 'curl )
328+ ((eq stock-tracker-fetch-backend 'url ) 'url )
329+ (t
330+ (if (executable-find stock-tracker-curl-program) 'curl 'url ))))
331+
332+ (defun stock-tracker--fetch-url (url )
333+ " Fetch URL using `url-retrieve-synchronously' .
334+ Return the response body as a string, or nil on failure."
335+ (ignore-errors
336+ (with-current-buffer
337+ (url-retrieve-synchronously url t nil 5 )
338+ (set-buffer-multibyte t )
339+ (goto-char (point-min ))
340+ (let (body)
341+ (when (string-match " 200 OK" (buffer-string ))
342+ ; ; Skip HTTP headers
343+ (re-search-forward " \n\n " nil t )
344+ (setq body (buffer-substring-no-properties (point ) (point-max ))))
345+ (kill-current-buffer )
346+ body))))
347+
348+ (defun stock-tracker--fetch-curl (url )
349+ " Fetch URL using the curl command-line tool.
350+ Return the response body as a string, or nil on failure."
351+ (ignore-errors
352+ (with-temp-buffer
353+ (let* ((args (list " --silent" " --show-error"
354+ " --max-time" (number-to-string stock-tracker-curl-timeout)
355+ " --location"
356+ " -H" " User-Agent: Mozilla/5.0"
357+ url))
358+ (status (apply #'call-process stock-tracker-curl-program nil t nil args)))
359+ (if (and (numberp status) (zerop status))
360+ (buffer-string )
361+ nil )))))
362+
363+ (defun stock-tracker--fetch (url )
364+ " Fetch URL and return the response body as a string, or nil on failure.
365+ Uses the backend specified by `stock-tracker-fetch-backend' ."
366+ (let ((backend (stock-tracker--effective-backend)))
367+ (if (eq backend 'curl )
368+ (or (stock-tracker--fetch-curl url)
369+ (stock-tracker--fetch-url url))
370+ (stock-tracker--fetch-url url))))
371+
303372(defun stock-tracker--request-synchronously (stock tag )
304373 " Get STOCK data with TAG synchronously, return a list of JSON each as alist."
305374 (let* (jsons response)
306375 (ignore-errors
307- (with-current-buffer
308- (url-retrieve-synchronously
309- (format (stock-tracker--api-url tag) (url-hexify-string stock)) t nil 5 )
310- (set-buffer-multibyte t )
311- (goto-char (point-min ))
312- (when (string-match " 200 OK" (buffer-string ))
313- (re-search-forward (stock-tracker--result-prefix tag) nil 'move )
314- (setq response (buffer-substring-no-properties (point ) (point-max )))
315- (setq jsons (json-read-from-string (replace-regexp-in-string " [\\ [\\ ]]" " " response))))
316- (kill-current-buffer )))
376+ (let ((body (stock-tracker--fetch
377+ (format (stock-tracker--api-url tag) (url-hexify-string stock)))))
378+ (when body
379+ (with-temp-buffer
380+ (insert body)
381+ (goto-char (point-min ))
382+ (re-search-forward (stock-tracker--result-prefix tag) nil 'move )
383+ (setq response (buffer-substring-no-properties (point ) (point-max )))
384+ (setq jsons (json-read-from-string (replace-regexp-in-string " [\\ [\\ ]]" " " response)))))))
317385 jsons))
318386
319387(defun stock-tracker--format-json (json tag )
@@ -465,11 +533,15 @@ It defaults to a comma."
465533 ; ; libraries
466534 (require 'subr-x )
467535 (require 'url )
536+ (require 'json )
468537
469538 ; ; pass params to subprocess, use literal (string, integer, float) here
470539 (setq subprocess-chn-stocks-string ,chn-stocks-string
471540 subprocess-us-stocks-string ,us-stocks-string
472- subprocess-kill-delay ,stock-tracker-subprocess-kill-delay )
541+ subprocess-kill-delay ,stock-tracker-subprocess-kill-delay
542+ subprocess-fetch-backend ',(stock-tracker--effective-backend)
543+ subprocess-curl-program ,stock-tracker-curl-program
544+ subprocess-curl-timeout ,stock-tracker-curl-timeout )
473545
474546 ; ; mininum required functions in subprocess
475547 (defun stock-tracker--subprocess-api-url (string-tag )
@@ -483,20 +555,54 @@ It defaults to a comma."
483555 (if (equal string-tag " chn-stock" )
484556 " _ntes_quote_callback(" " \\ [" ))
485557
558+ (defun stock-tracker--subprocess-fetch-url (url )
559+ " Fetch URL using `url-retrieve-synchronously' ."
560+ (ignore-errors
561+ (with-current-buffer
562+ (url-retrieve-synchronously url t nil 5 )
563+ (set-buffer-multibyte t )
564+ (goto-char (point-min ))
565+ (let (body)
566+ (when (string-match " 200 OK" (buffer-string ))
567+ (re-search-forward " \n\n " nil t )
568+ (setq body (buffer-substring-no-properties (point ) (point-max ))))
569+ (kill-current-buffer )
570+ body))))
571+
572+ (defun stock-tracker--subprocess-fetch-curl (url )
573+ " Fetch URL using curl."
574+ (ignore-errors
575+ (with-temp-buffer
576+ (let* ((args (list " --silent" " --show-error"
577+ " --max-time" (number-to-string subprocess-curl-timeout)
578+ " --location"
579+ " -H" " User-Agent: Mozilla/5.0"
580+ url))
581+ (status (apply #'call-process subprocess-curl-program nil t nil args)))
582+ (if (and (numberp status) (zerop status))
583+ (buffer-string )
584+ nil )))))
585+
586+ (defun stock-tracker--subprocess-fetch (url )
587+ " Fetch URL using the configured backend."
588+ (if (eq subprocess-fetch-backend 'curl )
589+ (or (stock-tracker--subprocess-fetch-curl url)
590+ (stock-tracker--subprocess-fetch-url url))
591+ (stock-tracker--subprocess-fetch-url url)))
592+
486593 (defun stock-tracker--subprocess-request-synchronously (stock string-tag )
487594 " Get stock data synchronously, return a list of JSON each as alist."
488595 (let* (jsons response)
489596 (ignore-errors
490- (with-current-buffer
491- (url-retrieve-synchronously
492- (format (stock-tracker--subprocess-api-url string-tag) (url-hexify-string stock)) t nil 5 )
493- (set-buffer-multibyte t )
494- (goto-char (point-min ))
495- (when (string-match " 200 OK" (buffer-string ))
496- (re-search-forward (stock-tracker--subprocess-result-prefix string-tag) nil 'move )
497- (setq response (buffer-substring-no-properties (point ) (point-max )))
498- (setq jsons (json-read-from-string (replace-regexp-in-string " [\\ [\\ ]]" " " response))))
499- (kill-current-buffer )))
597+ (let ((body (stock-tracker--subprocess-fetch
598+ (format (stock-tracker--subprocess-api-url string-tag) (url-hexify-string stock)))))
599+ (when body
600+ (with-temp-buffer
601+ (insert body)
602+ (goto-char (point-min ))
603+ (re-search-forward (stock-tracker--subprocess-result-prefix string-tag) nil 'move )
604+ (setq response (buffer-substring-no-properties (point ) (point-max )))
605+ (setq jsons (json-read-from-string (replace-regexp-in-string " [\\ [\\ ]]" " " response)))))))
500606 jsons))
501607
502608 ; ; make sure subprocess can exit successfully
0 commit comments