Once more the fortress of pure numbers

This commit is contained in:
nik gaffney 2023-05-19 16:07:54 +02:00
parent 1d2c153d5e
commit 91a3524869
2 changed files with 346 additions and 76 deletions

View file

@ -3,32 +3,73 @@
#+author:
#+title: MusicBrainz & ListenBrainz & other
* MusicBrainz
[[file:img/musicbrainz-logo.svg]]
MusicBrainz is a community-maintained open source encyclopedia of [[https://musicbrainz.org/doc/About][music information]]. The REST-based [[https://musicbrainz.org/doc/MusicBrainz_API][webservice API]] can be used for direct access to MusicBrainz data with output in XML and JSON.
This code provides a simple, incomplete yet possibly useful interface to some of the MusicBrainz and ListenBrainz APIs from emacs for exploratory use in =org-mode= or behind the scenes sending listening metadata.
* MusicBrainz API
- Recording documentation: https://musicbrainz.org/doc/Recording
- Release documentation: https://musicbrainz.org/doc/Release
- Artist documentation: https://musicbrainz.org/doc/Artist
** searching & browsing
Search supports the full [[https://lucene.apache.org/core/7_7_2/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description][Lucene search syntax]].
** some examples
#+BEGIN_SRC emacs-lisp
(musicbrainz-search "recording" "taema")
#+END_SRC
Autechre albums & eps
docs provide the example URL https://musicbrainz.org/ws/2/release-group?artist=410c9baf-5469-44f6-9852-826524b80c61&type=album|ep
The API docs provide an example search for “Autechre albums & eps” using the URL https://musicbrainz.org/ws/2/release-group?artist=410c9baf-5469-44f6-9852-826524b80c61&type=album|ep
The equivalent function (which returns a raw response as an alist) would be…
#+BEGIN_SRC emacs-lisp
(musicbrainz-browse "release-group" "artist" "410c9baf-5469-44f6-9852-826524b80c61" "album|ep")
#+END_SRC
For slightly more legible output wrap with =musicbrainz-format=
#+BEGIN_SRC emacs-lisp
(musicbrainz-search-artist "Autechre")
(musicbrainz-format (musicbrainz-browse "release-group" "artist" "410c9baf-5469-44f6-9852-826524b80c61" "album|ep"))
#+END_SRC
#+RESULTS:
| Autechre | 410c9baf-5469-44f6-9852-826524b80c61 |
A more interactive approach could start with =(musicbrainz-search "artist" "Autechre")= or =(musicbrainz-search-artist "Autechre")= Which returns the MBID required for the lookup → =410c9baf-5469-44f6-9852-826524b80c61=
The MBID can be checked if needed, with =(musicbrainz-mbid-p "410c9baf-5469-44f6-9852-826524b80c61")=
The MBID can then be used for specific lookups…
#+BEGIN_SRC emacs-lisp
(musicbrainz-lookup "artist" "410c9baf-5469-44f6-9852-826524b80c61" "releases")
#+END_SRC
#+BEGIN_SRC emacs-lisp
(musicbrainz-lookup-artist "410c9baf-5469-44f6-9852-826524b80c61")
#+END_SRC
| Autechre | electronic music duo | Group | [[https://musicbrainz.org/artist/410c9baf-5469-44f6-9852-826524b80c61][410c9baf-5469-44f6-9852-826524b80c61]] |
#+BEGIN_SRC emacs-lisp
(musicbrainz-lookup-artist-releases "410c9baf-5469-44f6-9852-826524b80c61")
#+END_SRC
#+BEGIN_SRC emacs-lisp
(musicbrainz-lookup-artist-recordings "410c9baf-5469-44f6-9852-826524b80c61")
#+END_SRC
#+BEGIN_SRC emacs-lisp
(musicbrainz-lookup-release "ec1ecfcc-f529-43d1-8aa6-2c7051ede00c")
#+END_SRC
| 1990 | Autechre / Saw You | Cassette Case | [[https://musicbrainz.org/release/ec1ecfcc-f529-43d1-8aa6-2c7051ede00c][ec1ecfcc-f529-43d1-8aa6-2c7051ede00c]] |
#+BEGIN_SRC emacs-lisp
(musicbrainz-lookup-recording "83730176-89ec-41a5-a4b6-476998f6291c")
#+END_SRC
| [untitled] | [[https://musicbrainz.org/recording/83730176-89ec-41a5-a4b6-476998f6291c][83730176-89ec-41a5-a4b6-476998f6291c]] |
#+BEGIN_SRC emacs-lisp
@ -85,7 +126,7 @@ John Williams, the classical guitar player, has an artist MBID of 8b8a38a9-a290-
=7feb02f2-51fa-422d-838e-2c14ecb4c7b8= → Tomorrows Bad Seeds
#+BEGIN_SRC emacs-lisp
(musicbrainz-disambiguate-artist "Bad Seeds")
(musicbrainz-disambiguate-artist "Bad Seeds" 7)
#+END_SRC
#+RESULTS:
@ -96,11 +137,15 @@ John Williams, the classical guitar player, has an artist MBID of 8b8a38a9-a290-
| 98 | The Bad Seeds, backing band for Nick Cave | [[https://musicbrainz.org/artist/eb2a8edc-5670-4896-82be-87db38de9583][eb2a8edc-5670-4896-82be-87db38de9583]] |
| 86 | Nick Cave & the Bad Seeds, nil | [[https://musicbrainz.org/artist/172e1f1a-504d-4488-b053-6344ba63e6d0][172e1f1a-504d-4488-b053-6344ba63e6d0]] |
| 50 | The Lightning Seeds, nil | [[https://musicbrainz.org/artist/1ba601a0-3401-4b28-8ddd-9af8203661e8][1ba601a0-3401-4b28-8ddd-9af8203661e8]] |
| 49 | Seeds, UK dancehall | [[https://musicbrainz.org/artist/a03cf587-a3d3-4847-ac41-e488f779a313][a03cf587-a3d3-4847-ac41-e488f779a313]] |
* ListenBrainz
[[file:img/listenbrainz-logo.svg]]
* listening
- https://listenbrainz.org
@ -125,11 +170,11 @@ John Williams, the classical guitar player, has an artist MBID of 8b8a38a9-a290-
#+END_SRC
#+BEGIN_SRC emacs-lisp
(listenbrainz-submit-single-listen "farmersmanual" "808808008088 (11)")
(listenbrainz-submit-single-listen "Matthew Thomas" "Taema" "Architecture")
#+END_SRC
#+BEGIN_SRC emacs-lisp
(listenbrainz-submit-single-listen "Matthew Thomas" "Taema" "Architecture")
(listenbrainz-submit-single-listen "farmersmanual" "808808008088 (11)")
#+END_SRC
#+BEGIN_SRC emacs-lisp
@ -141,7 +186,7 @@ John Williams, the classical guitar player, has an artist MBID of 8b8a38a9-a290-
#+END_SRC
#+BEGIN_SRC emacs-lisp
(listenbrainz-stats-artists "zzzkt") ;; defaults to all time
(listenbrainz-stats-artists "zzzkt")
#+END_SRC
#+BEGIN_SRC emacs-lisp
@ -246,6 +291,11 @@ https://listenbrainz.readthedocs.io/en/production/dev/api/#pinned-recording-api-
| GET /1/(user_name)/pins | - |
* otherBrainz
- [[https://critiquebrainz.org/][CritiqueBrainz]]
- [[https://bookbrainz.org/][BookBrainz]] → https://api.test.bookbrainz.org/1/docs/
- [[https://listenbrainz.org/messybrainz/][MessyBrainz]]
- [[https://coverartarchive.org/][Cover art archive]]
* further
- https://listenbrainz.org/user/troi-bot/playlists/

View file

@ -5,7 +5,7 @@
;; Author: nik gaffney <nik@fo.am>
;; Created: 2023-05-05
;; Version: 0.1
;; Package-Requires: ((emacs "27.1") (request "0.3"))
;; Package-Requires: ((emacs "28.1") (request "0.3"))
;; Keywords: music, scrobbling, multimedia
;; URL: https://github.com/zzkt/metabrainz
@ -97,6 +97,31 @@ Documentation available at https://musicbrainz.org/doc/MusicBrainz_API"
"recording" "release" "release-group" "series" "work" "url")
"API resources for linked entites in the MusicBrainz database.")
(defconst musicbrainz-search-types
(list "annotation" "area" "artist" "cdstub" "event" "instrument"
"label" "place" "recording" "release" "release-group"
"series" "tag" "work" "url")
"Valid TYPE parameters for MusicBrainz searches.")
;; entity checks
(defun musicbrainz-core-entity-p (entity)
"Check if ENTITY is a core entity."
(if (member entity musicbrainz-entities-core) t nil))
(defun musicbrainz-non-core-entity-p (entity)
"Check if ENTITY is a non-core entity."
(if (member entity musicbrainz-entities-non-core) t nil))
(defun musicbrainz-uid-entity-p (entity)
"Check if ENTITY is a unique identifier entity."
(if (member entity musicbrainz-entities-uids) t nil))
(defun musicbrainz-search-type-p (type)
"Check if TYPE is a valid search type."
(if (member type musicbrainz-search-types) t nil))
;; Linked entities
@ -129,7 +154,8 @@ The following list shows which linked entities you can use in a browse request:
(defun musicbrainz-mbid-p (mbid)
"Check (permissive) if MBID is valid and/or well formatted.
An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz.org/doc/MusicBrainz_Identifier for details."
(if (and (length= mbid 36)
(if (and (length= mbid 36) ;; length= requires emacs > 28.1
(string-match-p
(rx (repeat 8 hex) ;; [A-F0-9]{8}
"-" (repeat 4 hex) ;; -[A-F0-9]{4}
@ -140,43 +166,99 @@ An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz
t nil))
(defun musicbrainz-format (response)
"Format a generic RESPONSE."
(format "%s" (pp response)))
;;; ;; ;; ; ; ; ; ; ;
;;
;; Search API
;; https://musicbrainz.org/doc/MusicBrainz_API/Search
;;
;; The MusicBrainz API search requests provide a way to search for MusicBrainz
;; entities based on different sorts of queries and are provided by a search
;; server built using Lucene technology.
;;
;; Parameters common to all resources
;;
;; type Selects the entity index to be searched: annotation, area, artist,
;; cdstub, event, instrument, label, place, recording, release,
;; release-group, series, tag, work, url
;;
;; query Lucene search query. This is mandatory
;;
;; limit An integer value defining how many entries should be returned.
;; Only values between 1 and 100 (both inclusive) are allowed.
;; If not given, this defaults to 25.
;;
;; offset Return search results starting at a given offset.
;; Used for paging through more than one page of results.
;;
;; dismax If set to "true", switches the Solr query parser from edismax to dismax,
;; which will escape certain special query syntax characters by default
;; for ease of use. This is equivalent to switching from the "Indexed search
;; with advanced query syntax" method to the plain "Indexed search" method
;; on the website. Defaults to "false".
;;
;; ;; ; ; ;
;;;###autoload
(defun musicbrainz-search (entity query &optional limit)
"Search the MusicBrainz database for ENTITY matching QUERY.
Optionally return only LIMIT number of results.
(defun musicbrainz-search (type query &optional limit offset)
"Search the MusicBrainz database for TYPE matching QUERY.
Optionally return only LIMIT number of results from OFFSET.
The QUERY field supports the full Lucene Search syntax, some details
can be found near https://musicbrainz.org/doc/MusicBrainz_API/Search
or in the Lucene docs."
(message "musicbrainz: searching %s=%s" entity query)
(message "musicbrainz: searching %s=%s" type query)
(let* ((max (if limit limit 1))
(from (if offset offset ""))
(response
(request-response-data
(request
(url-encode-url
(format "%s/%s?query=%s&fmt=json&limit=%s"
musicbrainz-api-url entity query max))
:type "GET"
:parser 'json-read
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
(if (eq t (assoc-default 'valid data))
(message "Token is valid for user: %s"
(assoc-default 'user_name data))
(message "Not a valid user token"))))))))
(request-response-data
(request
(url-encode-url
(format "%s/%s?query=%s&fmt=json&limit=%s&offset=%s"
musicbrainz-api-url type query max from))
:type "GET"
:parser 'json-read
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "ok")))))))
response))
;;;###autoload
(defun musicbrainz-find (query &rest extras)
"Search the MusicBrainz database for QUERY or recommend a more specific search.
MusicBrainz makes a distinction between `search' and `browse' this a more general
entry point to searching/browsing the database.
Heuristics.
- if QUERY is an MBID, check artist, recording, etc
- if QUERY is text, search for artists or recordings, etc"
(message "musicbrainz: finding: %s" query)
(if (musicbrainz-mbid-p query)
;; search (lookup) for things that could have an mbid
(let ((mbid query))
(message "searching mbid: %s" mbid))
;; search (query) for other things
(progn
(message "searching other: %s" mbid)
;; (message "searching artist: %s" query)
;; (musicbrainz-format (musicbrainz-search "artist" query))
;; (message "searching label: %s" query)
;; (musicbrainz-format (musicbrainz-search "label" query))
;; (message "searching release: %s" query)
;; (musicbrainz-format (musicbrainz-search "release" query))
)))
;; various specific searches
;;;###autoload
@ -185,15 +267,15 @@ or in the Lucene docs."
Optionally return LIMIT number of results."
(let ((data (musicbrainz-search "artist" artist limit)))
(let-alist
data
(seq-map
(lambda (i)
(let-alist i
(if (not limit)
(format "%s | %s |\n" .name .id)
(format "%s | %s | %s |\n"
.score .name .id))))
.artists))))
data
(seq-map
(lambda (i)
(let-alist i
(if (not limit)
(format "%s | %s |\n" .name .id)
(format "%s | %s | %s |\n"
.score .name .id))))
.artists))))
;;;###autoload
@ -203,26 +285,27 @@ See `musicbrainz-disambiguate-artist' if there are multiple matches."
(let ((data (musicbrainz-search "artist" artist)))
(let-alist data
(car (remove nil (seq-map
(lambda (i)
(let-alist i
(when (= 100 .score)
(format "%s" .id))))
.artists))))))
(lambda (i)
(let-alist i
(when (= 100 .score)
(format "%s" .id))))
.artists))))))
;;;###autoload
(defun musicbrainz-disambiguate-artist (artist &optional limit)
"More ARTIST data. less ambiguity (with optional LIMIT).
Outputs an `org-mode' table with descriptions and MBID link to artists pages."
(let ((data (musicbrainz-search "artist" artist limit)))
(let* ((max (if limit limit 11))
(data (musicbrainz-search "artist" artist max)))
(let-alist data
(cons (format "| Artist: %s| MBID |\n" artist)
(seq-map
(lambda (i)
(let-alist i
(format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
.score .name .disambiguation .id .id)))
.artists)))))
(seq-map
(lambda (i)
(let-alist i
(format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
.score .name .disambiguation .id .id)))
.artists)))))
;;;###autoload
@ -231,22 +314,159 @@ Outputs an `org-mode' table with descriptions and MBID link to artists pages."
Optionally return LIMIT number of results."
(let ((data (musicbrainz-search "label" label limit)))
(let-alist
data
(seq-map
(lambda (i)
(let-alist i
(if (not limit)
(format "%s | %s |\n" .name .id)
(format "%s | %s | %s (%s%s) | %s |\n"
.score .name
(if .disambiguation .disambiguation "")
(if .life-span.begin
(format "%s " .life-span.begin) "")
(if .life-span.end
(format "—%s" .life-span.end)
"ongoing")
.id))))
.labels))))
data
(seq-map
(lambda (i)
(let-alist i
(if (not limit)
(format "%s | %s |\n" .name .id)
(format "%s | %s | %s (%s%s) | %s |\n"
.score .name
(if .disambiguation .disambiguation "")
(if .life-span.begin
(format "%s " .life-span.begin) "")
(if .life-span.end
(format "—%s" .life-span.end)
"ongoing")
.id))))
.labels))))
;;;###autoload
(defun musicbrainz-search-recording (query &optional limit)
"Search for a recording using QUERY and show matches.
Optionally return LIMIT number of results."
(let ((data (musicbrainz-search "recording" query limit)))
(let-alist
data
(seq-map
(lambda (i)
(let-alist i
(format "%s | %s, %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
.score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)))
.recordings))))
(defun musicbrainz--unwrap-0 (entity)
"Unwrap (fragile) .artist-credit ENTITY -> .name more or less."
(format "%s" (cdar (aref entity 0))))
;;;###autoload
(defun musicbrainz-search-release (query &optional limit)
"Search for a release using QUERY and show matches.
Optionally return LIMIT number of results."
(let ((data (musicbrainz-search "release" query limit)))
(let-alist
data
(seq-map
(lambda (i)
(let-alist i
(format "%s | %s, %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
.score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)))
.releases))))
;;; ;; ;; ; ; ; ; ; ;
;;
;; Lookups
;; https://musicbrainz.org/doc/MusicBrainz_API#Lookups
;;
;;; ;; ;; ; ;
;;;###autoload
(defun musicbrainz-lookup (entity mbid &optional inc)
"Search (lookup not browse) the MusicBrainz database for ENTITY with MBID.
Optionally add an INC list.
Subqueries
/ws/2/area
/ws/2/artist recordings, releases, release-groups, works
/ws/2/collection user-collections (includes private collections, requires authentication)
/ws/2/event
/ws/2/genre
/ws/2/instrument
/ws/2/label releases
/ws/2/place
/ws/2/recording artists, releases, isrcs, url-rels
/ws/2/release artists, collections, labels, recordings, release-groups
/ws/2/release-group artists, releases
/ws/2/series
/ws/2/work
/ws/2/url"
(message "musicbrainz: lookup: %s/%s" entity mbid)
(if (and (musicbrainz-core-entity-p entity)
(musicbrainz-mbid-p mbid))
(let* ((add (if inc inc ""))
(response
(request-response-data
(request
(url-encode-url
(format "%s/%s/%s?inc=%s&fmt=json"
musicbrainz-api-url entity mbid add))
:type "GET"
:parser 'json-read
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
(message "%s data: %s" entity mbid)))))))
response)
(error "MusicBrainz: search requires a valid MBID and entity (i.e. one of %s)"
musicbrainz-entities-core)))
;; specific MBID subrequests (limited to 25 results?)
(defun musicbrainz-lookup-artist (mbid)
"MusicBrainz lookup for artist with MBID."
(let ((response
(musicbrainz-lookup "artist" mbid)))
(let-alist response
(format "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
.name .disambiguation .type .id .id))))
(defun musicbrainz-lookup-release (mbid)
"MusicBrainz lookup for release with MBID."
(let ((response
(musicbrainz-lookup "release" mbid)))
(let-alist response
(format "| %s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
.date .title .packaging .id .id))))
(defun musicbrainz-lookup-recording (mbid)
"MusicBrainz lookup for recording with MBID."
(let ((response
(musicbrainz-lookup "recording" mbid)))
(let-alist response
(format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
.title .id .id))))
(defun musicbrainz-lookup-artist-releases (mbid)
"MusicBrainz lookup for releases from artist with MBID."
(let ((response
(musicbrainz-lookup "artist" mbid "releases")))
(let-alist response
(seq-map
(lambda (i)
(let-alist i
(format "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
.date .title .packaging .id .id)))
.releases))))
(defun musicbrainz-lookup-artist-recordings (mbid)
"MusicBrainz lookup for recordings from artist with MBID."
(let ((response
(musicbrainz-lookup "artist" mbid "recordings")))
(let-alist response
(seq-map
(lambda (i)
(let-alist i
(format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
.title .id .id)))
.recordings))))
@ -278,16 +498,16 @@ Optionally return LIMIT number of results."
;;;###autoload
(defun musicbrainz-browse (entity link lookup &optional type)
"Search the MusicBrainz database for ENTITY with LINK matching LOOKUP.
(defun musicbrainz-browse (entity link query &optional type)
"Search the MusicBrainz database for ENTITY with LINK matching QUERY.
Optionally limit the search to TYPE results for ENTITY."
(message "musicbrainz: browsing %s linked to %s" entity link)
(message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link lookup type)
(message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type)
(let ((response
(request-response-data
(request
(url-encode-url
(format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link lookup type))
(format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type))
:type "GET"
:parser 'json-read
:sync t