fetching the album artwork

 17th February 2024 at 6:13pm

This is needed so that every tiddler becomes much prettier. 🖌️


Roadmap

Since the album artwork URI is tied to every groupId, there's a pending change in the record-info struct.

change record-info struct

; creates a record-info struct
(defstruct record-info artist title year groupid image-link)

Add retrieving of image on groupid-response -> record-info function

; retrieves a list of information in the record from the intarweb#response
; groupid-route-response -> record-info
(define (make-record-info-obj-from-groupid-response json-response)
	(make-record-info 
		[...]
		image-link: (cdr (assoc 'wikiImage (cdr (cadadr json-response))))))

Which is tested with an assertion:

(test "creating record-info struct from groupid-route-response"
	  #t
	  (let ((test-record-info (make-record-info-obj-from-groupid-response grouprecord-1234-response)))
		(and (record-info? test-record-info)
			 (string=? (record-info-artist test-record-info) "Kraftwerk")
			 [...]
			 (string=? (record-info-image-uri test-record-info) EXPECTED-IMAGE-URI-WITH-ID-1234))))

Download the image to a file

Breaking the task into its parts, there's the first HTTP request to the image file link.


; NOTE could this exception handler be refactored?
; sends the request and saves the image file
; URIString filename -> boolean intarweb#uri intarweb#response
(define (request-and-save-image-file endpoint filename)
 	(log-message INFO (string-append "Sending image GET request to " endpoint))
	 (handle-exceptions exn
				   (begin
					 (let ((exn-message ((condition-property-accessor 'exn 'message) exn)))
						 (log-message ERROR exn-message))
					 #f)
				   (call-with-input-request* 
					(make-request-to-image-uri (absolute-uri endpoint))
					#f
; NOTE `response` is not being used; it's one of the advantages of using 
; call-with-input-request*, and it can later be taken advantage of to check for 
; the status code, response-type, etc.
					(lambda (from resp) (saves-image from filename)))))


; routine to save an album-art file 
; record-info -> boolean
(define (produce-record-info-album-art record-info)
  (let*-values (((image-uri) (record-info-image-uri record-info))
				(filename (get-album-art-filename record-info))
				((bool-result uri-obj image-response)
				 (request-and-save-image-file image-uri filename)))
    bool-result))

And here are some preliminary tests...

(define capri-ri (make-record-info title: "CAPRISONGS"
								   artist: "FKA Twigs"
								   year: 2022
								   groupid: 1678784
								   image-uri: TEST-IMAGE-URI))

(test "creates the album art filename for a record-info object"
	  #t
	  (string=? (get-album-art-filename capri-ri) "caprisongs-fka-twigs.jpg"))


(let ((test-path (string-append "./"
								(get-album-art-filename capri-ri) ".")))
	(test "can save image file from the request"
		  test-path
		  (begin (request-and-save-image-file (record-info-image-uri capri-ri) test-path)
				 (and (and (file-exists? test-path) #t)
					  (delete-file* test-path)))))

now, let me be upfront: this was much harder than anticipated. And not necessarily because of code or conceptual complexity. It was a mixture of being tired, too many hours at the computer, and last but not least — trying to code without proper testing.

Many issues arose, some pertaining to Chicken Scheme syntax (like call-with-input-request* returning three values, which was not clear how to handle), but some others were just compounding mistakes which could have definitely been avoided with some proper testing.

The most baffling bug: an earlier version of the saves-image function was fed raw-image-data from a response handler that used write-string to pass data; of course, this is crazy, because there's bound to be issues with writing data as if it were a string. So that entailed a complete rework of the saves-image file, with tricks that would be way beyond my reach.

; saves the image file from the image response
; port filename -> boolean
(define (saves-image from filename)
  (begin (log-message DEBUG (string-append "saving album-art image to " filename))
		 (call-with-output-file filename (lambda (to) (copy-port from to)))
		 #t))

which requires a port — a Scheme abstraction, higher-level than file descriptors in a language like C. From what I understood, a port can, in essence, be anything. From the official Scheme documentation,

To Scheme, an input port is a Scheme object that can deliver data upon command, while an output port is a Scheme object that can accept data. Whether the input and output port types are disjoint is implementation-dependent. (In MIT/GNU Scheme, there are input ports, output ports, and input/output ports.)

In any case, I cannot thank sjamaan and my scheme friend enough for the kindness and availability in solving these apparently minor mistakes. This particular feature took me much longer than anticipated, and set me back a bit in my completely imaginary deadlines.