From 91a352486919bcf1b316273a79a5c250e1d25435 Mon Sep 17 00:00:00 2001 From: nik gaffney Date: Fri, 19 May 2023 16:07:54 +0200 Subject: [PATCH] Once more the fortress of pure numbers --- README.org | 78 +++++++++-- musicbrainz.el | 344 ++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 346 insertions(+), 76 deletions(-) diff --git a/README.org b/README.org index 6cefd7c..8250835 100644 --- a/README.org +++ b/README.org @@ -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/ diff --git a/musicbrainz.el b/musicbrainz.el index ce7def4..e5f41a9 100644 --- a/musicbrainz.el +++ b/musicbrainz.el @@ -5,7 +5,7 @@ ;; Author: nik gaffney ;; 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