Re: [PATCH] Add offline scrobble queueing to listenbrainz scrobbler

"Fran Burstall (Gmail)" <[email protected]> Fri, 22 May 2026 09:36:12 +0100
Newsgroups gmane.emacs.emms.user
Message-ID <CADxS8igSy7APE94PftzYmEOP02RG8MounZ7boBQKE5bfHYEa8Q@mail.gmail.com>
--0000000000008836dd065263ecab
Content-Type: text/plain; charset="UTF-8"

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"
>

--0000000000008836dd065263ecab
Content-Type: text/html; charset="UTF-8"
Content-Transfer-Encoding: quoted-printable

<div dir=3D"ltr"><div class=3D"gmail_default" style=3D"font-family:arial,he=
lvetica,sans-serif">I will look at this but it may be a while due to family=
 issues.</div><div class=3D"gmail_default" style=3D"font-family:arial,helve=
tica,sans-serif"><br></div><div class=3D"gmail_default" style=3D"font-famil=
y:arial,helvetica,sans-serif">---Fran</div></div><br><div class=3D"gmail_qu=
ote gmail_quote_container"><div dir=3D"ltr" class=3D"gmail_attr">On Thu, 21=
 May 2026 at 20:39, Yoni Rabkin &lt;<a href=3D"mailto:[email protected]">yon=
[email protected]</a>&gt; wrote:<br></div><blockquote class=3D"gmail_quote" sty=
le=3D"margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);paddi=
ng-left:1ex"><br>
Fran, can you please have a look at this patch to your work?<br>
<br>
<br>
coopi &lt;<a href=3D"mailto:[email protected]" target=3D"_bl=
ank">[email protected]</a>&gt; writes:<br>
<br>
&gt;&gt;From 3381b0af16d02814ce1e254dcf49553ad73ad2f1 Mon Sep 17 00:00:00 2=
001<br>
&gt; Message-ID: &lt;<a href=3D"mailto:[email protected]" target=
=3D"_blank">[email protected]</a>&gt;<br>
&gt; MIME-Version: 1.0<br>
&gt; Content-Type: text/plain<br>
&gt;<br>
&gt;<br>
&gt; Queue failed listen submissions to a local file and flush them to List=
enBrainz<br>
&gt; when connectivity is restored.=C2=A0 Failed HTTPS connections hang sil=
ently with no<br>
&gt; callback invocation; handle this by starting a per-request connection =
timeout<br>
&gt; that queues the listen and kills the hung connection if no response ar=
rives.<br>
&gt;<br>
&gt; * emms-listenbrainz-scrobbler.el (emms-listenbrainz-scrobbler-queue-fi=
le): New<br>
&gt; defcustom.<br>
&gt; (emms-listenbrainz-scrobbler-flush-interval): New defcustom.<br>
&gt; (emms-listenbrainz-scrobbler-max-batch-size): New defcustom.<br>
&gt; (emms-listenbrainz-scrobbler-queue-timer): New variable.<br>
&gt; (emms-listenbrainz-scrobbler-connection-timeout): New variable.<br>
&gt; (emms-listenbrainz-scrobbler-flushing): New variable.<br>
&gt; (emms-listenbrainz-scrobbler--completed): New defvar-local.<br>
&gt; (emms-listenbrainz-scrobbler--queue-listen): New function.<br>
&gt; (emms-listenbrainz-scrobbler--read-queue): New function.<br>
&gt; (emms-listenbrainz-scrobbler--write-queue): New function.<br>
&gt; (emms-listenbrainz-scrobbler--make-import-payload): New function.<br>
&gt; (emms-listenbrainz-scrobbler--handle-submission-timeout): New function=
.<br>
&gt; (emms-listenbrainz-scrobbler--handle-flush-timeout): New function.<br>
&gt; (emms-listenbrainz-scrobbler--flush-callback): New function.<br>
&gt; (emms-listenbrainz-scrobbler--flush-batch): New function.<br>
&gt; (emms-listenbrainz-scrobbler-flush-queue): New function.<br>
&gt; (emms-listenbrainz-scrobbler-queue-count): New function.<br>
&gt; (emms-listenbrainz-scrobbler-clear-queue): New function.<br>
&gt; (emms-listenbrainz-scrobbler-make-query): Fix `duration&#39; only bein=
g added to<br>
&gt; extra-data when `track-number&#39; was set.<br>
&gt; (emms-listenbrainz-scrobbler-make-async-submission-call): Wrap `url-re=
trieve&#39; in<br>
&gt; `condition-case&#39; to catch synchronous network errors.=C2=A0 Extrac=
t listen data from<br>
&gt; the payload and pass it to the callback for potential offline queueing=
.=C2=A0 Start a<br>
&gt; connection timeout timer; on expiry, queue the listen and kill the hun=
g<br>
&gt; connection.<br>
&gt; (emms-listenbrainz-scrobbler-submission-callback): Change argument lis=
t<br>
&gt; from (STATUS &amp;optional CBARGS) to (STATUS &amp;optional TRACK PLAY=
ING-NOW<br>
&gt; LISTEN-DATA).=C2=A0 Check `emms-listenbrainz-scrobbler--completed&#39;=
 to avoid<br>
&gt; double-queueing when a timeout kills the connection before the callbac=
k fires.<br>
&gt; Check `plist-get STATUS :error&#39; for network failures and queue fai=
led listens<br>
&gt; for later submission.<br>
&gt; (emms-listenbrainz-scrobbler-enable): Start periodic queue flush timer=
.=C2=A0 Flush<br>
&gt; any queued listens from previous sessions.<br>
&gt; (emms-listenbrainz-scrobbler-disable): Cancel periodic flush timer.<br=
>
&gt; ---<br>
&gt;=C2=A0 emms-listenbrainz-scrobbler.el | 375 +++++++++++++++++++++++++++=
+++---<br>
&gt;=C2=A0 1 file changed, 344 insertions(+), 31 deletions(-)<br>
&gt;<br>
&gt; diff --git a/emms-listenbrainz-scrobbler.el b/emms-listenbrainz-scrobb=
ler.el<br>
&gt; index 2262d4e..0d30f5f 100644<br>
&gt; --- a/emms-listenbrainz-scrobbler.el<br>
&gt; +++ b/emms-listenbrainz-scrobbler.el<br>
&gt; @@ -67,6 +67,47 @@ Note that the preferred way of authenticating is us=
ing authinfo.&quot;)<br>
&gt;=C2=A0 =C2=A0 nil<br>
&gt;=C2=A0 =C2=A0 &quot;Non-nil if emms-listenbrainz-scrobbler is active.&q=
uot;)<br>
&gt;=C2=A0 <br>
&gt; +(defcustom emms-listenbrainz-scrobbler-queue-file<br>
&gt; +=C2=A0 (expand-file-name &quot;listenbrainz-queue&quot; emms-director=
y)<br>
&gt; +=C2=A0 &quot;File for storing offline listen queue.&quot;<br>
&gt; +=C2=A0 :type &#39;file<br>
&gt; +=C2=A0 :group &#39;emms)<br>
&gt; +<br>
&gt; +(defcustom emms-listenbrainz-scrobbler-flush-interval<br>
&gt; +=C2=A0 (* 10 60)=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=
=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0; 10 minutes<br>
&gt; +=C2=A0 &quot;Interval in seconds between automatic queue flush attemp=
ts.<br>
&gt; +Set to nil to disable periodic flushing.&quot;<br>
&gt; +=C2=A0 :type &#39;(choice (integer :tag &quot;Seconds&quot;)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(const =
:tag &quot;Disable periodic flush&quot; nil))<br>
&gt; +=C2=A0 :group &#39;emms)<br>
&gt; +<br>
&gt; +(defcustom emms-listenbrainz-scrobbler-max-batch-size<br>
&gt; +=C2=A0 100<br>
&gt; +=C2=A0 &quot;Maximum number of listens to submit in a single import r=
equest.<br>
&gt; +ListenBrainz accepts up to 1000, but smaller batches are gentler<br>
&gt; +on the server.&quot;<br>
&gt; +=C2=A0 :type &#39;integer<br>
&gt; +=C2=A0 :group &#39;emms)<br>
&gt; +<br>
&gt; +(defvar emms-listenbrainz-scrobbler-queue-timer<br>
&gt; +=C2=A0 nil<br>
&gt; +=C2=A0 &quot;Timer for periodic queue flushing.&quot;)<br>
&gt; +<br>
&gt; +(defvar emms-listenbrainz-scrobbler-connection-timeout<br>
&gt; +=C2=A0 10<br>
&gt; +=C2=A0 &quot;Seconds to wait for a connection before treating it as f=
ailed.&quot;)<br>
&gt; +<br>
&gt; +(defvar emms-listenbrainz-scrobbler-flushing<br>
&gt; +=C2=A0 nil<br>
&gt; +=C2=A0 &quot;Non-nil if a queue flush is currently in progress.<br>
&gt; +Prevents overlapping flush operations.&quot;)<br>
&gt; +<br>
&gt; +(defvar-local emms-listenbrainz-scrobbler--completed<br>
&gt; +=C2=A0 nil<br>
&gt; +=C2=A0 &quot;Non-nil when the HTTP callback has already handled this =
request.<br>
&gt; +Set to t by the callback, or &#39;timeout by the timeout handler.=C2=
=A0 Used to<br>
&gt; +prevent double-queueing when a timeout kills a hung connection.&quot;=
)<br>
&gt; +<br>
&gt;=C2=A0 <br>
&gt;=C2=A0 ;;* User token<br>
&gt;=C2=A0 <br>
&gt; @@ -110,7 +151,7 @@ Note that the preferred way of authenticating is u=
sing authinfo.&quot;)<br>
&gt;=C2=A0 =C2=A0 =C2=A0 ;; additional data<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (when track-number (push (cons &quot;tracknumber&q=
uot; (string-to-number track-number))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =
=C2=A0 =C2=A0 =C2=A0 =C2=A0 extra-data))<br>
&gt; -=C2=A0 =C2=A0 (when track-number (push (cons &quot;duration&quot; tra=
ck-length) extra-data))<br>
&gt; +=C2=A0 =C2=A0 (when track-length (push (cons &quot;duration&quot; tra=
ck-length) extra-data))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (when extra-data (push (cons &quot;additional_info=
&quot; extra-data) metadata))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 ;; payload<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (push (cons &quot;track_metadata&quot; metadata) p=
ayload)<br>
&gt; @@ -156,8 +197,12 @@ Note that the preferred way of authenticating is =
using authinfo.&quot;)<br>
&gt;=C2=A0 ;; data but headers!<br>
&gt;=C2=A0 <br>
&gt;=C2=A0 (defun emms-listenbrainz-scrobbler-make-async-submission-call (t=
rack &amp;optional playing-now)<br>
&gt; -=C2=A0 &quot;Submit listen, or playing-now if PLAYING-NOW non-nil, of=
 TRACK to listenbrainz.&quot;<br>
&gt; +=C2=A0 &quot;Submit listen, or playing-now if PLAYING-NOW non-nil, of=
 TRACK to ListenBrainz.<br>
&gt; +On network failure, the listen is queued to the offline queue (unless=
<br>
&gt; +PLAYING-NOW is non-nil, since playing-now notifications are ephemeral=
).&quot;<br>
&gt;=C2=A0 =C2=A0 (let* ((payload (emms-listenbrainz-scrobbler-make-query t=
rack playing-now))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0;; Extract the inner listen data (f=
or offline queueing on failure)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (listen-data (aref (cdr (assoc &quot;payload&quo=
t; payload)) 0))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 (token (emms-listenbrainz-scrobbler-get-tok=
en))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 (token-string (encode-coding-string (concat=
 &quot;Token &quot; token) &#39;utf-8))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 (url-request-method &quot;POST&quot;)<br>
&gt; @@ -167,35 +212,287 @@ Note that the preferred way of authenticating i=
s using authinfo.&quot;)<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (ignore url-request-method<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0url-request-data<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0url-request-extra-headers)<br>
&gt; -=C2=A0 =C2=A0 (url-retrieve<br>
&gt; -=C2=A0 =C2=A0 =C2=A0emms-listenbrainz-scrobbler-submission-url<br>
&gt; -=C2=A0 =C2=A0 =C2=A0#&#39;emms-listenbrainz-scrobbler-submission-call=
back<br>
&gt; -=C2=A0 =C2=A0 =C2=A0(list (cons track playing-now)))))<br>
&gt; +=C2=A0 =C2=A0 (condition-case _<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(let ((buffer (url-retrieve<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 emms-listenbrainz-scrobbler-submission-url<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 #&#39;emms-listenbrainz-scrobbler-submission-callback<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 (list track playing-now listen-data))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(when (buffer-live-p buffer)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(with-current-buffer buffer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scro=
bbler--completed nil))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(run-at-time emms-listenbrainz-scro=
bbler-connection-timeout nil<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 #&#39;emms-listenbrainz-scrobbler--handle-submission-timeout<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 buffer track playing-now listen-data)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (error<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0;; Synchronous error (e.g. DNS resolution =
failure on some platforms).<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0;; `url-retrieve&#39; signaled an error be=
fore the callback was invoked.<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(unless playing-now<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (emms-listenbrainz-scrobbler--queue-listen liste=
n-data)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz: connection error, q=
ueued for later: %s&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(emms-track-ge=
t track &#39;info-title)))))))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--handle-submission-timeout<br>
&gt; +=C2=A0 =C2=A0 (buffer track playing-now listen-data)<br>
&gt; +=C2=A0 &quot;Handle a submission connection timeout for BUFFER.<br>
&gt; +If the callback has not yet handled this request, queue the listen an=
d<br>
&gt; +kill the hung connection.&quot;<br>
&gt; +=C2=A0 (when (and (buffer-live-p buffer)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (not (buffer-local-value<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &#39;emms-lis=
tenbrainz-scrobbler--completed buffer)))<br>
&gt; +=C2=A0 =C2=A0 (with-current-buffer buffer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler--completed &#3=
9;timeout))<br>
&gt; +=C2=A0 =C2=A0 (kill-buffer buffer)<br>
&gt; +=C2=A0 =C2=A0 (unless playing-now<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (emms-listenbrainz-scrobbler--queue-listen liste=
n-data)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz: connection timed ou=
t, queued: %s&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (emms-track-get track &#39;=
info-title)))))<br>
&gt;=C2=A0 <br>
&gt; -(defun emms-listenbrainz-scrobbler-submission-callback (status &amp;o=
ptional cbargs)<br>
&gt; +(defun emms-listenbrainz-scrobbler-submission-callback<br>
&gt; +=C2=A0 =C2=A0 (status &amp;optional track playing-now listen-data)<br=
>
&gt;=C2=A0 =C2=A0 &quot;Callback to handle response from listenbrainz serve=
r.<br>
&gt; +STATUS is the `url-retrieve&#39; status plist.=C2=A0 TRACK is the EMM=
S track.<br>
&gt; +PLAYING-NOW is non-nil if this was a playing_now submission.<br>
&gt; +LISTEN-DATA is the serialized listen object for offline queueing.&quo=
t;<br>
&gt; +=C2=A0 (if (eq emms-listenbrainz-scrobbler--completed &#39;timeout)<b=
r>
&gt; +=C2=A0 =C2=A0 =C2=A0 ;; Already handled by the timeout handler; buffe=
r is being killed.<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 nil<br>
&gt; +=C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler--completed t)<br>
&gt; +=C2=A0 =C2=A0 (if (plist-get status :error)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0;; Network error: queue for later and bail out<br=
>
&gt; +=C2=A0 =C2=A0 =C2=A0(unless playing-now<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler--queue-listen=
 listen-data)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(message &quot;Listenbrainz: network error=
, queued for later: %s&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (emms-track-g=
et track &#39;info-title)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (when (&lt; (point-max) 1)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(error &quot;No response from submission server&q=
uot;))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (goto-char (point-min))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (let* ((response (ignore-errors<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 (re-search-forward &quot;\n\n&quot;)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 (json-read)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (title (emms-track-get track &#39;=
info-title)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(cond ((string=3D &quot;ok&quot; (alist-get &#39;=
status response))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (when (and (not playing-now=
)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0emms-listenbrainz-scrobbler-display-submissions)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (message &quot;Liste=
nbrainz: submitted %s.&quot; title))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (kill-buffer))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0((assoc &#39;error response)=
<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (unless playing-now<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (emms-listenbrainz-s=
crobbler--queue-listen listen-data))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz=
 error: %s while submitting %s&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0(alist-get &#39;error response)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0title))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(t<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (unless playing-now<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (emms-listenbrainz-s=
crobbler--queue-listen listen-data))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (error &quot;Listenbrainz: =
unhandled error while submitting %s&quot; title)))))))<br>
&gt; +<br>
&gt; +=0C<br>
&gt; +;;* Offline Queue<br>
&gt; +<br>
&gt; +;;; Storage<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--queue-listen (listen-data)<br>
&gt; +=C2=A0 &quot;Append LISTEN-DATA to the offline queue file.<br>
&gt; +LISTEN-DATA should be an alist suitable for ListenBrainz submission,<=
br>
&gt; +i.e. it must have \&quot;listened_at\&quot; and \&quot;track_metadata=
\&quot; keys.&quot;<br>
&gt; +=C2=A0 (when listen-data<br>
&gt; +=C2=A0 =C2=A0 (with-temp-buffer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (prin1 listen-data (current-buffer))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (insert &quot;\n&quot;)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (condition-case err<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(append-to-file (point-min) (point-max)<br=
>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0emms-listenbrainz-scrobbler-queue-file)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(error<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz: could not write to =
queue file: %s&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(error-message=
-string err)))))))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--read-queue ()<br>
&gt; +=C2=A0 &quot;Read all listens from the queue file.<br>
&gt; +Return a list of listen alists.=C2=A0 Each alist has \&quot;listened_=
at\&quot; and<br>
&gt; +\&quot;track_metadata\&quot; keys, suitable for ListenBrainz import.&=
quot;<br>
&gt; +=C2=A0 (let ((file emms-listenbrainz-scrobbler-queue-file))<br>
&gt; +=C2=A0 =C2=A0 (when (file-exists-p file)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (with-temp-buffer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(insert-file-contents file)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(goto-char (point-min))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(let (listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(while (not (eobp))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(let ((start-pos (point))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(line (buffer-=
substring-no-properties<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 (line-beginning-position) (line-end-position))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(if (string-empty-p (string-=
trim line))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(forward-line)=
<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(condition-case err<b=
r>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(progn<=
br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
(push (read line) listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
(forward-line))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(error<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (message &quo=
t;Listenbrainz: skipping malformed queue entry: %s&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 =C2=A0(error-message-string err))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (forward-line=
))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0;; Safety: ensure we always =
advance past the current line<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(when (and (not (eobp))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 (=3D (point) start-pos))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(forward-line))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(nreverse listens))))))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--write-queue (listens)<br>
&gt; +=C2=A0 &quot;Write LISTENS to the queue file, replacing any existing =
content.<br>
&gt; +If LISTENS is nil, the queue file is deleted.&quot;<br>
&gt; +=C2=A0 (if (null listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (when (file-exists-p emms-listenbrainz-scrobbler=
-queue-file)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(delete-file emms-listenbrainz-scrobbler-queue-fi=
le))<br>
&gt; +=C2=A0 =C2=A0 (with-temp-buffer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (dolist (listen listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (prin1 listen (current-buffer))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(insert &quot;\n&quot;))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (write-region (point-min) (point-max)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0emms-li=
stenbrainz-scrobbler-queue-file nil &#39;silent))))<br>
&gt;=C2=A0 <br>
&gt; -Ignore STATUS argument and store submission data in CBARGS.&quot;<br>
&gt; -=C2=A0 (ignore status)<br>
&gt; -=C2=A0 (when (&lt; (point-max) 1)<br>
&gt; -=C2=A0 =C2=A0 (error &quot;No response from submission server&quot;))=
<br>
&gt; -=C2=A0 (goto-char (point-min ))<br>
&gt; -=C2=A0 (let* ((response (ignore-errors<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (re-se=
arch-forward &quot;\n\n&quot;)<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (json-=
read)))<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 (track (car cbargs))<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 (playing-now (cdr cbargs))<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 (title (emms-track-get track &#39;info-title)))<=
br>
&gt; -=C2=A0 =C2=A0 (cond ((string=3D &quot;ok&quot; (alist-get &#39;status=
 response))<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 (when (and (not playing-now) emms-listenb=
rainz-scrobbler-display-submissions)<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz: submi=
tted %s.&quot; title))<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 ;; tidy up<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 (kill-buffer))<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0((assoc &#39;error response)<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz error: %s whi=
le submitting %s&quot;<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(alist-=
get &#39;error response)<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0title))=
<br>
&gt; -=C2=A0 =C2=A0 =C2=A0 =C2=A0(t (error &quot;Listenbrainz: unhandled er=
ror while submitting %s&quot; title)))))<br>
&gt; +=0C<br>
&gt; +;;; Flushing<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--make-import-payload (listens)<br>
&gt; +=C2=A0 &quot;Create a ListenBrainz import payload from a list of LIST=
EN alists.<br>
&gt; +Returns an alist with \&quot;listen_type\&quot; set to \&quot;import\=
&quot; and a<br>
&gt; +\&quot;payload\&quot; vector containing the listens.&quot;<br>
&gt; +=C2=A0 `((&quot;listen_type&quot; . &quot;import&quot;)<br>
&gt; +=C2=A0 =C2=A0 (&quot;payload&quot; . ,(apply #&#39;vector listens))))=
<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler-flush-queue ()<br>
&gt; +=C2=A0 &quot;Submit all queued (offline) listens to ListenBrainz.<br>
&gt; +Uses the \&quot;import\&quot; listen type to submit listens in batche=
s.=C2=A0 This is<br>
&gt; +safe to call at any time; it does nothing if the queue is empty or if=
 a<br>
&gt; +flush is already in progress.&quot;<br>
&gt; +=C2=A0 (interactive)<br>
&gt; +=C2=A0 (unless emms-listenbrainz-scrobbler-flushing<br>
&gt; +=C2=A0 =C2=A0 (let ((listens (emms-listenbrainz-scrobbler--read-queue=
)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (when listens<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-flushing t)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(message &quot;Listenbrainz: flushing %d queued l=
istens...&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (length listens))<br=
>
&gt; +=C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler--flush-batch listens=
 0)))))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--handle-flush-timeout<br>
&gt; +=C2=A0 =C2=A0 (buffer remaining-listens)<br>
&gt; +=C2=A0 &quot;Handle a flush connection timeout for BUFFER.<br>
&gt; +REMAINING-LISTENS is all unsubmitted listens (including the timed-out=
<br>
&gt; +batch).=C2=A0 Write them back to the queue and reset the flushing fla=
g.&quot;<br>
&gt; +=C2=A0 (when (and (buffer-live-p buffer)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (not (buffer-local-value<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &#39;emms-lis=
tenbrainz-scrobbler--completed buffer)))<br>
&gt; +=C2=A0 =C2=A0 (with-current-buffer buffer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler--completed &#3=
9;timeout))<br>
&gt; +=C2=A0 =C2=A0 (kill-buffer buffer)<br>
&gt; +=C2=A0 =C2=A0 (message &quot;Listenbrainz: connection timed out durin=
g flush, %d listens remain queued.&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (length remaining-listens))<br>
&gt; +=C2=A0 =C2=A0 (emms-listenbrainz-scrobbler--write-queue remaining-lis=
tens)<br>
&gt; +=C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler-flushing nil)))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--flush-callback (status batch rest=
 submitted-count)<br>
&gt; +=C2=A0 &quot;Handle flush batch response from ListenBrainz server.<br=
>
&gt; +STATUS is the `url-retrieve&#39; status plist.=C2=A0 BATCH is the lis=
t of listens<br>
&gt; +that were just submitted.=C2=A0 REST is the remaining unsubmitted lis=
tens.<br>
&gt; +SUBMITTED-COUNT is the number of listens already submitted in this fl=
ush<br>
&gt; +session.&quot;<br>
&gt; +=C2=A0 (if (eq emms-listenbrainz-scrobbler--completed &#39;timeout)<b=
r>
&gt; +=C2=A0 =C2=A0 =C2=A0 ;; Already handled by the timeout handler.<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 nil<br>
&gt; +=C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler--completed t)<br>
&gt; +=C2=A0 =C2=A0 (let ((err (plist-get status :error)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (if err<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0;; Network error: write everything remaini=
ng back to queue<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(progn<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(message<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;Listenbrainz: network error =
during flush, %d listens remain queued.&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (+ (length batch) (length rest)))<=
br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler--write=
-queue<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (append batch rest))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-f=
lushing nil)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(condition-case nil (kill-buffer) (=
error nil)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0;; Connection succeeded: check HTTP response<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(goto-char (point-min))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(let ((response (ignore-errors<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0(re-search-forward &quot;\n\n&quot;)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0(json-read))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(condition-case nil (kill-buffer) (error n=
il))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(if (string=3D &quot;ok&quot; (alist-get &=
#39;status response))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0;; Batch accepted: continue =
with rest<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler=
--flush-batch<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 rest<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (+ submitted-count (length =
batch)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0;; API error: write everything rema=
ining back to queue<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(message<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &quot;Listenbrainz: API error duri=
ng flush, %d listens remain queued.&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (+ (length batch) (length rest)))<=
br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler--write=
-queue<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 (append batch rest))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-f=
lushing nil)))))))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler--flush-batch (remaining-listens su=
bmitted-count)<br>
&gt; +=C2=A0 &quot;Submit the next batch of REMAINING-LISTENS to ListenBrai=
nz.<br>
&gt; +SUBMITTED-COUNT is the number of listens already submitted in this fl=
ush<br>
&gt; +session.=C2=A0 On success, continues with the rest of the queue.=C2=
=A0 On failure,<br>
&gt; +writes remaining listens back to the queue file.&quot;<br>
&gt; +=C2=A0 (if (null remaining-listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 ;; All done<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (progn<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler--write-queue nil)<br=
>
&gt; +=C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-flushing nil)<b=
r>
&gt; +=C2=A0 =C2=A0 =C2=A0(when (&gt; submitted-count 0)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(message &quot;Listenbrainz: flushed %d qu=
eued listens.&quot; submitted-count)))<br>
&gt; +=C2=A0 =C2=A0 ;; Submit next batch<br>
&gt; +=C2=A0 =C2=A0 (let* ((batch-size (min (length remaining-listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 =C2=A0emms-listenbrainz-scrobbler-max-batch-size))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (batch (seq-take remaining-listens batch-=
size))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (rest (seq-drop remaining-listens batch-s=
ize))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (payload (emms-listenbrainz-scrobbler--ma=
ke-import-payload batch))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (token (emms-listenbrainz-scrobbler-get-t=
oken))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (token-string (encode-coding-string (conc=
at &quot;Token &quot; token) &#39;utf-8))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (url-request-method &quot;POST&quot;)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (url-request-data (encode-coding-string (=
json-encode payload) &#39;utf-8))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 (url-request-extra-headers<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0`((&quot;Content-type&quot; . &quot=
;application/json; charset=3Dutf-8&quot;)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(&quot;Authorization&quot; .=
 ,token-string))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (ignore url-request-method<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0url-request-data<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0url-request-extra-headers)<b=
r>
&gt; +=C2=A0 =C2=A0 =C2=A0 (condition-case _<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(let ((buffer (url-retrieve<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 emms-listenbrainz-scrobbler-submission-url<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 #&#39;emms-listenbrainz-scrobbler--flush-callback<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 (list batch rest submitted-count))))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(when (buffer-live-p buffer)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(with-current-buffer buffer<=
br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(setq-local emms-list=
enbrainz-scrobbler--completed nil))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(run-at-time emms-listenbrai=
nz-scrobbler-connection-timeout nil<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 #&#39;emms-listenbrainz-scrobbler--handle-flush-timeout<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 buffer remaining-listens)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(error<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 ;; Synchronous error (e.g. DNS failure)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (message &quot;Listenbrainz: connection error du=
ring flush, %d listens remain queued.&quot;<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(length remain=
ing-listens))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (emms-listenbrainz-scrobbler--write-queue remain=
ing-listens)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler-flushing nil))=
))))<br>
&gt; +<br>
&gt; +=0C<br>
&gt; +;;; Interactive Commands<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler-queue-count ()<br>
&gt; +=C2=A0 &quot;Display and return the number of listens in the offline =
queue.&quot;<br>
&gt; +=C2=A0 (interactive)<br>
&gt; +=C2=A0 (let ((count (length (emms-listenbrainz-scrobbler--read-queue)=
)))<br>
&gt; +=C2=A0 =C2=A0 (message &quot;Listenbrainz: %d listens queued for subm=
ission.&quot; count)<br>
&gt; +=C2=A0 =C2=A0 count))<br>
&gt; +<br>
&gt; +(defun emms-listenbrainz-scrobbler-clear-queue ()<br>
&gt; +=C2=A0 &quot;Clear all listens from the offline queue.<br>
&gt; +This discards listens that have not yet been submitted.=C2=A0 This ac=
tion<br>
&gt; +cannot be undone.&quot;<br>
&gt; +=C2=A0 (interactive)<br>
&gt; +=C2=A0 (when (yes-or-no-p &quot;Discard all queued offline listens? &=
quot;)<br>
&gt; +=C2=A0 =C2=A0 (emms-listenbrainz-scrobbler--write-queue nil)<br>
&gt; +=C2=A0 =C2=A0 (message &quot;Listenbrainz: offline queue cleared.&quo=
t;)))<br>
&gt;=C2=A0 <br>
&gt;=C2=A0 <br>
&gt;=C2=A0 ;;* Hooks<br>
&gt; @@ -230,11 +527,15 @@ That is, if it has been played for 240 seconds o=
r half the length of the track.&quot;<br>
&gt;=C2=A0 <br>
&gt;=C2=A0 <br>
&gt;=C2=A0 ;;* Entry points<br>
&gt; +<br>
&gt;=C2=A0 (defun emms-listenbrainz-scrobbler-enable ()<br>
&gt; -=C2=A0 &quot;Enable the scrobbler and submit played tracks.&quot;<br>
&gt; +=C2=A0 &quot;Enable the scrobbler and submit played tracks.<br>
&gt; +Also starts a periodic timer to flush any queued offline listens, and=
<br>
&gt; +performs an initial flush attempt.&quot;<br>
&gt;=C2=A0 =C2=A0 (interactive)<br>
&gt;=C2=A0 =C2=A0 ;; We rely on emms-playing-time so check for it!<br>
&gt; -=C2=A0 (unless emms-playing-time-mode (error &quot;Listenbrainz scrob=
bler: please activate emms-playing-time-mode&quot;))<br>
&gt; +=C2=A0 (unless emms-playing-time-mode<br>
&gt; +=C2=A0 =C2=A0 (error &quot;Listenbrainz scrobbler: please activate em=
ms-playing-time-mode&quot;))<br>
&gt;=C2=A0 =C2=A0 ;; check we have credentials<br>
&gt;=C2=A0 =C2=A0 (if (emms-listenbrainz-scrobbler-get-token)<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 (unless emms-listenbrainz-scrobbler-running=
<br>
&gt; @@ -244,7 +545,15 @@ That is, if it has been played for 240 seconds or=
 half the length of the track.&quot;<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&#39;emms=
-listenbrainz-scrobbler-stop-hook)<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0(add-hook &#39;emms-player-finished-hook<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0&#39;emms=
-listenbrainz-scrobbler-stop-hook)<br>
&gt; -=C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-running t))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0;; Start periodic queue flushing<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(when emms-listenbrainz-scrobbler-flush-interval<=
br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-queue-ti=
mer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0(run-at-time t<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 =C2=A0 emms-listenbrainz-scrobbler-flush-interval<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0=
 =C2=A0 =C2=A0 =C2=A0 #&#39;emms-listenbrainz-scrobbler-flush-queue)))<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(setq emms-listenbrainz-scrobbler-running t)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0;; Flush any listens queued from previous session=
s<br>
&gt; +=C2=A0 =C2=A0 =C2=A0(emms-listenbrainz-scrobbler-flush-queue))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (error &quot;Listenbrainz scrobbler: no user token=
.=C2=A0 Please supply and try again&quot;)))<br>
&gt;=C2=A0 <br>
&gt;=C2=A0 (defun emms-listenbrainz-scrobbler-disable ()<br>
&gt; @@ -257,6 +566,10 @@ That is, if it has been played for 240 seconds or=
 half the length of the track.&quot;<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &#39;emms-liste=
nbrainz-scrobbler-stop-hook)<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (remove-hook &#39;emms-player-finished-hook<br>
&gt;=C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 =C2=A0 &#39;emms-liste=
nbrainz-scrobbler-stop-hook)<br>
&gt; +=C2=A0 =C2=A0 ;; Cancel periodic flush timer<br>
&gt; +=C2=A0 =C2=A0 (when emms-listenbrainz-scrobbler-queue-timer<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (cancel-timer emms-listenbrainz-scrobbler-queue-=
timer)<br>
&gt; +=C2=A0 =C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler-queue-timer ni=
l))<br>
&gt;=C2=A0 =C2=A0 =C2=A0 (setq emms-listenbrainz-scrobbler-running nil)))<b=
r>
<br>
-- <br>
=C2=A0 =C2=A0&quot;Cut your own wood and it will warm you twice&quot;<br>
</blockquote></div>

--0000000000008836dd065263ecab--