Re: [PATCH] Add offline scrobble queueing to listenbrainz scrobbler
Yoni Rabkin <[email protected]> Fri, 22 May 2026 07:51:11 -0400
| Newsgroups | gmane.emacs.emms.user |
|---|---|
| Message-ID | <[email protected]> |
I'm sorry to hear that, and I hope for the best for you all. Please take your time. "Fran Burstall (Gmail)" <[email protected]> writes: > I will look at this but it may be a while due to family issues. > > ---Fran > > On Thu, 21 May 2026 at 20:39, Yoni Rabkin <[email protected]> wrote: > >> >> Fran, can you please have a look at this patch to your work? >> >> >> coopi <[email protected]> writes: >> >> >>From 3381b0af16d02814ce1e254dcf49553ad73ad2f1 Mon Sep 17 00:00:00 2001 >> > Message-ID: <[email protected]> >> > MIME-Version: 1.0 >> > Content-Type: text/plain >> > >> > >> > Queue failed listen submissions to a local file and flush them to >> ListenBrainz >> > when connectivity is restored. Failed HTTPS connections hang silently >> with no >> > callback invocation; handle this by starting a per-request connection >> timeout >> > that queues the listen and kills the hung connection if no response >> arrives. >> > >> > * emms-listenbrainz-scrobbler.el >> (emms-listenbrainz-scrobbler-queue-file): New >> > defcustom. >> > (emms-listenbrainz-scrobbler-flush-interval): New defcustom. >> > (emms-listenbrainz-scrobbler-max-batch-size): New defcustom. >> > (emms-listenbrainz-scrobbler-queue-timer): New variable. >> > (emms-listenbrainz-scrobbler-connection-timeout): New variable. >> > (emms-listenbrainz-scrobbler-flushing): New variable. >> > (emms-listenbrainz-scrobbler--completed): New defvar-local. >> > (emms-listenbrainz-scrobbler--queue-listen): New function. >> > (emms-listenbrainz-scrobbler--read-queue): New function. >> > (emms-listenbrainz-scrobbler--write-queue): New function. >> > (emms-listenbrainz-scrobbler--make-import-payload): New function. >> > (emms-listenbrainz-scrobbler--handle-submission-timeout): New function. >> > (emms-listenbrainz-scrobbler--handle-flush-timeout): New function. >> > (emms-listenbrainz-scrobbler--flush-callback): New function. >> > (emms-listenbrainz-scrobbler--flush-batch): New function. >> > (emms-listenbrainz-scrobbler-flush-queue): New function. >> > (emms-listenbrainz-scrobbler-queue-count): New function. >> > (emms-listenbrainz-scrobbler-clear-queue): New function. >> > (emms-listenbrainz-scrobbler-make-query): Fix `duration' only being >> added to >> > extra-data when `track-number' was set. >> > (emms-listenbrainz-scrobbler-make-async-submission-call): Wrap >> `url-retrieve' in >> > `condition-case' to catch synchronous network errors. Extract listen >> data from >> > the payload and pass it to the callback for potential offline queueing. >> Start a >> > connection timeout timer; on expiry, queue the listen and kill the hung >> > connection. >> > (emms-listenbrainz-scrobbler-submission-callback): Change argument list >> > from (STATUS &optional CBARGS) to (STATUS &optional TRACK PLAYING-NOW >> > LISTEN-DATA). Check `emms-listenbrainz-scrobbler--completed' to avoid >> > double-queueing when a timeout kills the connection before the callback >> fires. >> > Check `plist-get STATUS :error' for network failures and queue failed >> listens >> > for later submission. >> > (emms-listenbrainz-scrobbler-enable): Start periodic queue flush timer. >> Flush >> > any queued listens from previous sessions. >> > (emms-listenbrainz-scrobbler-disable): Cancel periodic flush timer. >> > --- >> > emms-listenbrainz-scrobbler.el | 375 ++++++++++++++++++++++++++++++--- >> > 1 file changed, 344 insertions(+), 31 deletions(-) >> > >> > diff --git a/emms-listenbrainz-scrobbler.el >> b/emms-listenbrainz-scrobbler.el >> > index 2262d4e..0d30f5f 100644 >> > --- a/emms-listenbrainz-scrobbler.el >> > +++ b/emms-listenbrainz-scrobbler.el >> > @@ -67,6 +67,47 @@ Note that the preferred way of authenticating is >> using authinfo.") >> > nil >> > "Non-nil if emms-listenbrainz-scrobbler is active.") >> > >> > +(defcustom emms-listenbrainz-scrobbler-queue-file >> > + (expand-file-name "listenbrainz-queue" emms-directory) >> > + "File for storing offline listen queue." >> > + :type 'file >> > + :group 'emms) >> > + >> > +(defcustom emms-listenbrainz-scrobbler-flush-interval >> > + (* 10 60) ; 10 minutes >> > + "Interval in seconds between automatic queue flush attempts. >> > +Set to nil to disable periodic flushing." >> > + :type '(choice (integer :tag "Seconds") >> > + (const :tag "Disable periodic flush" nil)) >> > + :group 'emms) >> > + >> > +(defcustom emms-listenbrainz-scrobbler-max-batch-size >> > + 100 >> > + "Maximum number of listens to submit in a single import request. >> > +ListenBrainz accepts up to 1000, but smaller batches are gentler >> > +on the server." >> > + :type 'integer >> > + :group 'emms) >> > + >> > +(defvar emms-listenbrainz-scrobbler-queue-timer >> > + nil >> > + "Timer for periodic queue flushing.") >> > + >> > +(defvar emms-listenbrainz-scrobbler-connection-timeout >> > + 10 >> > + "Seconds to wait for a connection before treating it as failed.") >> > + >> > +(defvar emms-listenbrainz-scrobbler-flushing >> > + nil >> > + "Non-nil if a queue flush is currently in progress. >> > +Prevents overlapping flush operations.") >> > + >> > +(defvar-local emms-listenbrainz-scrobbler--completed >> > + nil >> > + "Non-nil when the HTTP callback has already handled this request. >> > +Set to t by the callback, or 'timeout by the timeout handler. Used to >> > +prevent double-queueing when a timeout kills a hung connection.") >> > + >> > >> > ;;* User token >> > >> > @@ -110,7 +151,7 @@ Note that the preferred way of authenticating is >> using authinfo.") >> > ;; additional data >> > (when track-number (push (cons "tracknumber" (string-to-number >> track-number)) >> > extra-data)) >> > - (when track-number (push (cons "duration" track-length) extra-data)) >> > + (when track-length (push (cons "duration" track-length) extra-data)) >> > (when extra-data (push (cons "additional_info" extra-data) >> metadata)) >> > ;; payload >> > (push (cons "track_metadata" metadata) payload) >> > @@ -156,8 +197,12 @@ Note that the preferred way of authenticating is >> using authinfo.") >> > ;; data but headers! >> > >> > (defun emms-listenbrainz-scrobbler-make-async-submission-call (track >> &optional playing-now) >> > - "Submit listen, or playing-now if PLAYING-NOW non-nil, of TRACK to >> listenbrainz." >> > + "Submit listen, or playing-now if PLAYING-NOW non-nil, of TRACK to >> ListenBrainz. >> > +On network failure, the listen is queued to the offline queue (unless >> > +PLAYING-NOW is non-nil, since playing-now notifications are ephemeral)." >> > (let* ((payload (emms-listenbrainz-scrobbler-make-query track >> playing-now)) >> > + ;; Extract the inner listen data (for offline queueing on >> failure) >> > + (listen-data (aref (cdr (assoc "payload" payload)) 0)) >> > (token (emms-listenbrainz-scrobbler-get-token)) >> > (token-string (encode-coding-string (concat "Token " token) >> 'utf-8)) >> > (url-request-method "POST") >> > @@ -167,35 +212,287 @@ Note that the preferred way of authenticating is >> using authinfo.") >> > (ignore url-request-method >> > url-request-data >> > url-request-extra-headers) >> > - (url-retrieve >> > - emms-listenbrainz-scrobbler-submission-url >> > - #'emms-listenbrainz-scrobbler-submission-callback >> > - (list (cons track playing-now))))) >> > + (condition-case _ >> > + (let ((buffer (url-retrieve >> > + emms-listenbrainz-scrobbler-submission-url >> > + #'emms-listenbrainz-scrobbler-submission-callback >> > + (list track playing-now listen-data)))) >> > + (when (buffer-live-p buffer) >> > + (with-current-buffer buffer >> > + (setq emms-listenbrainz-scrobbler--completed nil)) >> > + (run-at-time emms-listenbrainz-scrobbler-connection-timeout nil >> > + >> #'emms-listenbrainz-scrobbler--handle-submission-timeout >> > + buffer track playing-now listen-data))) >> > + (error >> > + ;; Synchronous error (e.g. DNS resolution failure on some >> platforms). >> > + ;; `url-retrieve' signaled an error before the callback was >> invoked. >> > + (unless playing-now >> > + (emms-listenbrainz-scrobbler--queue-listen listen-data) >> > + (message "Listenbrainz: connection error, queued for later: %s" >> > + (emms-track-get track 'info-title))))))) >> > + >> > +(defun emms-listenbrainz-scrobbler--handle-submission-timeout >> > + (buffer track playing-now listen-data) >> > + "Handle a submission connection timeout for BUFFER. >> > +If the callback has not yet handled this request, queue the listen and >> > +kill the hung connection." >> > + (when (and (buffer-live-p buffer) >> > + (not (buffer-local-value >> > + 'emms-listenbrainz-scrobbler--completed buffer))) >> > + (with-current-buffer buffer >> > + (setq emms-listenbrainz-scrobbler--completed 'timeout)) >> > + (kill-buffer buffer) >> > + (unless playing-now >> > + (emms-listenbrainz-scrobbler--queue-listen listen-data) >> > + (message "Listenbrainz: connection timed out, queued: %s" >> > + (emms-track-get track 'info-title))))) >> > >> > -(defun emms-listenbrainz-scrobbler-submission-callback (status >> &optional cbargs) >> > +(defun emms-listenbrainz-scrobbler-submission-callback >> > + (status &optional track playing-now listen-data) >> > "Callback to handle response from listenbrainz server. >> > +STATUS is the `url-retrieve' status plist. TRACK is the EMMS track. >> > +PLAYING-NOW is non-nil if this was a playing_now submission. >> > +LISTEN-DATA is the serialized listen object for offline queueing." >> > + (if (eq emms-listenbrainz-scrobbler--completed 'timeout) >> > + ;; Already handled by the timeout handler; buffer is being killed. >> > + nil >> > + (setq emms-listenbrainz-scrobbler--completed t) >> > + (if (plist-get status :error) >> > + ;; Network error: queue for later and bail out >> > + (unless playing-now >> > + (emms-listenbrainz-scrobbler--queue-listen listen-data) >> > + (message "Listenbrainz: network error, queued for later: %s" >> > + (emms-track-get track 'info-title))) >> > + (when (< (point-max) 1) >> > + (error "No response from submission server")) >> > + (goto-char (point-min)) >> > + (let* ((response (ignore-errors >> > + (re-search-forward "\n\n") >> > + (json-read))) >> > + (title (emms-track-get track 'info-title))) >> > + (cond ((string= "ok" (alist-get 'status response)) >> > + (when (and (not playing-now) >> > + emms-listenbrainz-scrobbler-display-submissions) >> > + (message "Listenbrainz: submitted %s." title)) >> > + (kill-buffer)) >> > + ((assoc 'error response) >> > + (unless playing-now >> > + (emms-listenbrainz-scrobbler--queue-listen listen-data)) >> > + (message "Listenbrainz error: %s while submitting %s" >> > + (alist-get 'error response) >> > + title)) >> > + (t >> > + (unless playing-now >> > + (emms-listenbrainz-scrobbler--queue-listen listen-data)) >> > + (error "Listenbrainz: unhandled error while submitting %s" >> title))))))) >> > + >> > + >> > +;;* Offline Queue >> > + >> > +;;; Storage >> > + >> > +(defun emms-listenbrainz-scrobbler--queue-listen (listen-data) >> > + "Append LISTEN-DATA to the offline queue file. >> > +LISTEN-DATA should be an alist suitable for ListenBrainz submission, >> > +i.e. it must have \"listened_at\" and \"track_metadata\" keys." >> > + (when listen-data >> > + (with-temp-buffer >> > + (prin1 listen-data (current-buffer)) >> > + (insert "\n") >> > + (condition-case err >> > + (append-to-file (point-min) (point-max) >> > + emms-listenbrainz-scrobbler-queue-file) >> > + (error >> > + (message "Listenbrainz: could not write to queue file: %s" >> > + (error-message-string err))))))) >> > + >> > +(defun emms-listenbrainz-scrobbler--read-queue () >> > + "Read all listens from the queue file. >> > +Return a list of listen alists. Each alist has \"listened_at\" and >> > +\"track_metadata\" keys, suitable for ListenBrainz import." >> > + (let ((file emms-listenbrainz-scrobbler-queue-file)) >> > + (when (file-exists-p file) >> > + (with-temp-buffer >> > + (insert-file-contents file) >> > + (goto-char (point-min)) >> > + (let (listens) >> > + (while (not (eobp)) >> > + (let ((start-pos (point)) >> > + (line (buffer-substring-no-properties >> > + (line-beginning-position) (line-end-position)))) >> > + (if (string-empty-p (string-trim line)) >> > + (forward-line) >> > + (condition-case err >> > + (progn >> > + (push (read line) listens) >> > + (forward-line)) >> > + (error >> > + (message "Listenbrainz: skipping malformed queue entry: >> %s" >> > + (error-message-string err)) >> > + (forward-line)))) >> > + ;; Safety: ensure we always advance past the current line >> > + (when (and (not (eobp)) >> > + (= (point) start-pos)) >> > + (forward-line)))) >> > + (nreverse listens)))))) >> > + >> > +(defun emms-listenbrainz-scrobbler--write-queue (listens) >> > + "Write LISTENS to the queue file, replacing any existing content. >> > +If LISTENS is nil, the queue file is deleted." >> > + (if (null listens) >> > + (when (file-exists-p emms-listenbrainz-scrobbler-queue-file) >> > + (delete-file emms-listenbrainz-scrobbler-queue-file)) >> > + (with-temp-buffer >> > + (dolist (listen listens) >> > + (prin1 listen (current-buffer)) >> > + (insert "\n")) >> > + (write-region (point-min) (point-max) >> > + emms-listenbrainz-scrobbler-queue-file nil 'silent)))) >> > >> > -Ignore STATUS argument and store submission data in CBARGS." >> > - (ignore status) >> > - (when (< (point-max) 1) >> > - (error "No response from submission server")) >> > - (goto-char (point-min )) >> > - (let* ((response (ignore-errors >> > - (re-search-forward "\n\n") >> > - (json-read))) >> > - (track (car cbargs)) >> > - (playing-now (cdr cbargs)) >> > - (title (emms-track-get track 'info-title))) >> > - (cond ((string= "ok" (alist-get 'status response)) >> > - (when (and (not playing-now) >> emms-listenbrainz-scrobbler-display-submissions) >> > - (message "Listenbrainz: submitted %s." title)) >> > - ;; tidy up >> > - (kill-buffer)) >> > - ((assoc 'error response) >> > - (message "Listenbrainz error: %s while submitting %s" >> > - (alist-get 'error response) >> > - title)) >> > - (t (error "Listenbrainz: unhandled error while submitting %s" >> title))))) >> > + >> > +;;; Flushing >> > + >> > +(defun emms-listenbrainz-scrobbler--make-import-payload (listens) >> > + "Create a ListenBrainz import payload from a list of LISTEN alists. >> > +Returns an alist with \"listen_type\" set to \"import\" and a >> > +\"payload\" vector containing the listens." >> > + `(("listen_type" . "import") >> > + ("payload" . ,(apply #'vector listens)))) >> > + >> > +(defun emms-listenbrainz-scrobbler-flush-queue () >> > + "Submit all queued (offline) listens to ListenBrainz. >> > +Uses the \"import\" listen type to submit listens in batches. This is >> > +safe to call at any time; it does nothing if the queue is empty or if a >> > +flush is already in progress." >> > + (interactive) >> > + (unless emms-listenbrainz-scrobbler-flushing >> > + (let ((listens (emms-listenbrainz-scrobbler--read-queue))) >> > + (when listens >> > + (setq emms-listenbrainz-scrobbler-flushing t) >> > + (message "Listenbrainz: flushing %d queued listens..." >> > + (length listens)) >> > + (emms-listenbrainz-scrobbler--flush-batch listens 0))))) >> > + >> > +(defun emms-listenbrainz-scrobbler--handle-flush-timeout >> > + (buffer remaining-listens) >> > + "Handle a flush connection timeout for BUFFER. >> > +REMAINING-LISTENS is all unsubmitted listens (including the timed-out >> > +batch). Write them back to the queue and reset the flushing flag." >> > + (when (and (buffer-live-p buffer) >> > + (not (buffer-local-value >> > + 'emms-listenbrainz-scrobbler--completed buffer))) >> > + (with-current-buffer buffer >> > + (setq emms-listenbrainz-scrobbler--completed 'timeout)) >> > + (kill-buffer buffer) >> > + (message "Listenbrainz: connection timed out during flush, %d >> listens remain queued." >> > + (length remaining-listens)) >> > + (emms-listenbrainz-scrobbler--write-queue remaining-listens) >> > + (setq emms-listenbrainz-scrobbler-flushing nil))) >> > + >> > +(defun emms-listenbrainz-scrobbler--flush-callback (status batch rest >> submitted-count) >> > + "Handle flush batch response from ListenBrainz server. >> > +STATUS is the `url-retrieve' status plist. BATCH is the list of listens >> > +that were just submitted. REST is the remaining unsubmitted listens. >> > +SUBMITTED-COUNT is the number of listens already submitted in this flush >> > +session." >> > + (if (eq emms-listenbrainz-scrobbler--completed 'timeout) >> > + ;; Already handled by the timeout handler. >> > + nil >> > + (setq emms-listenbrainz-scrobbler--completed t) >> > + (let ((err (plist-get status :error))) >> > + (if err >> > + ;; Network error: write everything remaining back to queue >> > + (progn >> > + (message >> > + "Listenbrainz: network error during flush, %d listens remain >> queued." >> > + (+ (length batch) (length rest))) >> > + (emms-listenbrainz-scrobbler--write-queue >> > + (append batch rest)) >> > + (setq emms-listenbrainz-scrobbler-flushing nil) >> > + (condition-case nil (kill-buffer) (error nil))) >> > + ;; Connection succeeded: check HTTP response >> > + (goto-char (point-min)) >> > + (let ((response (ignore-errors >> > + (re-search-forward "\n\n") >> > + (json-read)))) >> > + (condition-case nil (kill-buffer) (error nil)) >> > + (if (string= "ok" (alist-get 'status response)) >> > + ;; Batch accepted: continue with rest >> > + (emms-listenbrainz-scrobbler--flush-batch >> > + rest >> > + (+ submitted-count (length batch))) >> > + ;; API error: write everything remaining back to queue >> > + (message >> > + "Listenbrainz: API error during flush, %d listens remain >> queued." >> > + (+ (length batch) (length rest))) >> > + (emms-listenbrainz-scrobbler--write-queue >> > + (append batch rest)) >> > + (setq emms-listenbrainz-scrobbler-flushing nil))))))) >> > + >> > +(defun emms-listenbrainz-scrobbler--flush-batch (remaining-listens >> submitted-count) >> > + "Submit the next batch of REMAINING-LISTENS to ListenBrainz. >> > +SUBMITTED-COUNT is the number of listens already submitted in this flush >> > +session. On success, continues with the rest of the queue. On failure, >> > +writes remaining listens back to the queue file." >> > + (if (null remaining-listens) >> > + ;; All done >> > + (progn >> > + (emms-listenbrainz-scrobbler--write-queue nil) >> > + (setq emms-listenbrainz-scrobbler-flushing nil) >> > + (when (> submitted-count 0) >> > + (message "Listenbrainz: flushed %d queued listens." >> submitted-count))) >> > + ;; Submit next batch >> > + (let* ((batch-size (min (length remaining-listens) >> > + emms-listenbrainz-scrobbler-max-batch-size)) >> > + (batch (seq-take remaining-listens batch-size)) >> > + (rest (seq-drop remaining-listens batch-size)) >> > + (payload (emms-listenbrainz-scrobbler--make-import-payload >> batch)) >> > + (token (emms-listenbrainz-scrobbler-get-token)) >> > + (token-string (encode-coding-string (concat "Token " token) >> 'utf-8)) >> > + (url-request-method "POST") >> > + (url-request-data (encode-coding-string (json-encode payload) >> 'utf-8)) >> > + (url-request-extra-headers >> > + `(("Content-type" . "application/json; charset=utf-8") >> > + ("Authorization" . ,token-string)))) >> > + (ignore url-request-method >> > + url-request-data >> > + url-request-extra-headers) >> > + (condition-case _ >> > + (let ((buffer (url-retrieve >> > + emms-listenbrainz-scrobbler-submission-url >> > + #'emms-listenbrainz-scrobbler--flush-callback >> > + (list batch rest submitted-count)))) >> > + (when (buffer-live-p buffer) >> > + (with-current-buffer buffer >> > + (setq-local emms-listenbrainz-scrobbler--completed nil)) >> > + (run-at-time emms-listenbrainz-scrobbler-connection-timeout >> nil >> > + >> #'emms-listenbrainz-scrobbler--handle-flush-timeout >> > + buffer remaining-listens))) >> > + (error >> > + ;; Synchronous error (e.g. DNS failure) >> > + (message "Listenbrainz: connection error during flush, %d listens >> remain queued." >> > + (length remaining-listens)) >> > + (emms-listenbrainz-scrobbler--write-queue remaining-listens) >> > + (setq emms-listenbrainz-scrobbler-flushing nil)))))) >> > + >> > + >> > +;;; Interactive Commands >> > + >> > +(defun emms-listenbrainz-scrobbler-queue-count () >> > + "Display and return the number of listens in the offline queue." >> > + (interactive) >> > + (let ((count (length (emms-listenbrainz-scrobbler--read-queue)))) >> > + (message "Listenbrainz: %d listens queued for submission." count) >> > + count)) >> > + >> > +(defun emms-listenbrainz-scrobbler-clear-queue () >> > + "Clear all listens from the offline queue. >> > +This discards listens that have not yet been submitted. This action >> > +cannot be undone." >> > + (interactive) >> > + (when (yes-or-no-p "Discard all queued offline listens? ") >> > + (emms-listenbrainz-scrobbler--write-queue nil) >> > + (message "Listenbrainz: offline queue cleared."))) >> > >> > >> > ;;* Hooks >> > @@ -230,11 +527,15 @@ That is, if it has been played for 240 seconds or >> half the length of the track." >> > >> > >> > ;;* Entry points >> > + >> > (defun emms-listenbrainz-scrobbler-enable () >> > - "Enable the scrobbler and submit played tracks." >> > + "Enable the scrobbler and submit played tracks. >> > +Also starts a periodic timer to flush any queued offline listens, and >> > +performs an initial flush attempt." >> > (interactive) >> > ;; We rely on emms-playing-time so check for it! >> > - (unless emms-playing-time-mode (error "Listenbrainz scrobbler: please >> activate emms-playing-time-mode")) >> > + (unless emms-playing-time-mode >> > + (error "Listenbrainz scrobbler: please activate >> emms-playing-time-mode")) >> > ;; check we have credentials >> > (if (emms-listenbrainz-scrobbler-get-token) >> > (unless emms-listenbrainz-scrobbler-running >> > @@ -244,7 +545,15 @@ That is, if it has been played for 240 seconds or >> half the length of the track." >> > 'emms-listenbrainz-scrobbler-stop-hook) >> > (add-hook 'emms-player-finished-hook >> > 'emms-listenbrainz-scrobbler-stop-hook) >> > - (setq emms-listenbrainz-scrobbler-running t)) >> > + ;; Start periodic queue flushing >> > + (when emms-listenbrainz-scrobbler-flush-interval >> > + (setq emms-listenbrainz-scrobbler-queue-timer >> > + (run-at-time t >> > + emms-listenbrainz-scrobbler-flush-interval >> > + #'emms-listenbrainz-scrobbler-flush-queue))) >> > + (setq emms-listenbrainz-scrobbler-running t) >> > + ;; Flush any listens queued from previous sessions >> > + (emms-listenbrainz-scrobbler-flush-queue)) >> > (error "Listenbrainz scrobbler: no user token. Please supply and >> try again"))) >> > >> > (defun emms-listenbrainz-scrobbler-disable () >> > @@ -257,6 +566,10 @@ That is, if it has been played for 240 seconds or >> half the length of the track." >> > 'emms-listenbrainz-scrobbler-stop-hook) >> > (remove-hook 'emms-player-finished-hook >> > 'emms-listenbrainz-scrobbler-stop-hook) >> > + ;; Cancel periodic flush timer >> > + (when emms-listenbrainz-scrobbler-queue-timer >> > + (cancel-timer emms-listenbrainz-scrobbler-queue-timer) >> > + (setq emms-listenbrainz-scrobbler-queue-timer nil)) >> > (setq emms-listenbrainz-scrobbler-running nil))) >> >> -- >> "Cut your own wood and it will warm you twice" >> -- "Cut your own wood and it will warm you twice"