1 ;;; musicbrainz.el --- MusicBrainz API interface -*- coding: utf-8; lexical-binding: t -*-
5 ;; Author: nik gaffney <nik@fo.am>
8 ;; Package-Requires: ((emacs "28.1") (request "0.3"))
9 ;; Keywords: music, scrobbling, multimedia
10 ;; URL: https://github.com/zzkt/metabrainz
12 ;; This file is not part of GNU Emacs.
14 ;; This program is free software; you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with this program. If not, see <https://www.gnu.org/licenses/>.
30 ;; - basic MusicBrainz interface
31 ;; - partial & incomplete
41 ;; debug level for http requests
42 (setq request-log-level 'warn
43 request-message-level 'warn)
52 (defcustom musicbrainz-api-url "https://musicbrainz.org/ws/2"
53 "URL for musicbrainz API.
54 Documentation available at https://musicbrainz.org/doc/MusicBrainz_API"
58 (defcustom musicbrainz-api-token ""
59 "An auth token is required for some functions."
63 (defcustom musicbrainz-user-agent "musicbrainz.el/0.1"
64 "A User-Agent header to identify source of API requests.
65 As seen in https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting"
72 ;; https://musicbrainz.org/doc/MusicBrainz_API#Browse
74 ;; On each entity resource, you can perform three different GET requests:
75 ;; lookup: /<ENTITY_TYPE>/<MBID>?inc=<INC>
76 ;; browse: /<RESULT_ENTITY_TYPE>?<BROWSING_ENTITY_TYPE>=<MBID>&limit=<LIMIT>&offset=<OFFSET>&inc=<INC>
77 ;; search: /<ENTITY_TYPE>?query=<QUERY>&limit=<LIMIT>&offset=<OFFSET>
79 ;; Note: Keep in mind only the search request is available without an MBID
80 ;; (or, in specific cases, a disc ID, ISRC or ISWC). If all you have is the
81 ;; name of an artist or album, for example, you'll need to make a search and
82 ;; pick the right result to get its MBID; only then will you able to use it
83 ;; in a lookup or browse request.
85 ;; On the genre resource, we support an "all" sub-resource to fetch all genres,
86 ;; paginated, in alphabetical order:
88 ;; all: /genre/all?limit=<LIMIT>&offset=<OFFSET>
92 (defconst musicbrainz-entities-core
93 (list "area" "artist" "event" "genre" "instrument" "label" "place"
94 "recording" "release" "release-group" "series" "work" "url")
95 "API resources which represent core entities in the MusicBrainz database.")
97 (defconst musicbrainz-entities-non-core
98 (list "rating" "tag" "collection")
99 "API resources which represent non-core entities in the MusicBrainz database.")
101 (defconst musicbrainz-entities-uids
102 (list "discid" "isrc" "iswc")
103 "API resources based on other unique identifiers in the MusicBrainz database.")
105 (defconst musicbrainz-entities-linked
106 (list "area" "artist" "collection" "event" "instrument" "label" "place"
107 "recording" "release" "release-group" "series" "work" "url")
108 "API resources for linked entites in the MusicBrainz database.")
110 (defconst musicbrainz-search-types
111 (list "annotation" "area" "artist" "cdstub" "event" "instrument"
112 "label" "place" "recording" "release" "release-group"
113 "series" "tag" "work" "url")
114 "Valid TYPE parameters for MusicBrainz searches.")
119 (defun musicbrainz-core-entity-p (entity)
120 "Check if ENTITY is a core entity."
121 (if (member entity musicbrainz-entities-core) t nil))
123 (defun musicbrainz-non-core-entity-p (entity)
124 "Check if ENTITY is a non-core entity."
125 (if (member entity musicbrainz-entities-non-core) t nil))
127 (defun musicbrainz-uid-entity-p (entity)
128 "Check if ENTITY is a unique identifier entity."
129 (if (member entity musicbrainz-entities-uids) t nil))
131 (defun musicbrainz-search-type-p (type)
132 "Check if TYPE is a valid search type."
133 (if (member type musicbrainz-search-types) t nil))
138 (defun musicbrainz-linked-entity-p (entity)
139 "Check if ENTITY can be used in a browse request (incomplete).
141 The following list shows which linked entities you can use in a browse request:
143 /ws/2/area collection
144 /ws/2/artist area, collection, recording, release, release-group, work
145 /ws/2/collection area, artist, editor, event, label, place, recording,
146 release, release-group, work
147 /ws/2/event area, artist, collection, place
148 /ws/2/instrument collection
149 /ws/2/label area, collection, release
150 /ws/2/place area, collection
151 /ws/2/recording artist, collection, release, work
152 /ws/2/release area, artist, collection, label, track, track_artist,
153 recording, release-group
154 /ws/2/release-group artist, collection, release
155 /ws/2/series collection
156 /ws/2/work artist, collection
159 (if (member entity musicbrainz-entities-linked) t nil))
164 (defun musicbrainz-mbid-p (mbid)
165 "Check (permissive) if MBID is valid and/or well formatted.
166 An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz.org/doc/MusicBrainz_Identifier for details."
168 (if (and (length= mbid 36) ;; length= requires emacs > 28.1
170 (rx (repeat 8 hex) ;; [A-F0-9]{8}
171 "-" (repeat 4 hex) ;; -[A-F0-9]{4}
172 "-" (repeat 4 hex) ;; -[34][A-F0-9]{3}
173 "-" (repeat 4 hex) ;; -[89AB][A-F0-9]{3}
174 "-" (repeat 12 hex)) ;; -[A-F0-9]{12}
179 (defun musicbrainz-format (response)
180 "Format a generic RESPONSE."
181 (format "%s" (pp response)))
184 (defun musicbrainz--unwrap-0 (entity)
185 "Unwrap (fragile) .artist-credit ENTITY -> .name more or less."
186 (format "%s" (cdar (aref entity 0))))
189 ;;; ;; ;; ; ; ; ; ; ;
192 ;; https://musicbrainz.org/doc/MusicBrainz_API/Search
194 ;; The MusicBrainz API search requests provide a way to search for MusicBrainz
195 ;; entities based on different sorts of queries and are provided by a search
196 ;; server built using Lucene technology.
198 ;; Parameters common to all resources
200 ;; type Selects the entity index to be searched: annotation, area, artist,
201 ;; cdstub, event, instrument, label, place, recording, release,
202 ;; release-group, series, tag, work, url
204 ;; query Lucene search query. This is mandatory
206 ;; limit An integer value defining how many entries should be returned.
207 ;; Only values between 1 and 100 (both inclusive) are allowed.
208 ;; If not given, this defaults to 25.
210 ;; offset Return search results starting at a given offset.
211 ;; Used for paging through more than one page of results.
213 ;; dismax If set to "true", switches the Solr query parser from edismax to dismax,
214 ;; which will escape certain special query syntax characters by default
215 ;; for ease of use. This is equivalent to switching from the "Indexed search
216 ;; with advanced query syntax" method to the plain "Indexed search" method
217 ;; on the website. Defaults to "false".
222 (defun musicbrainz-search (type query &optional limit offset)
223 "Search the MusicBrainz database for TYPE matching QUERY.
224 Optionally return only LIMIT number of results from OFFSET.
226 The QUERY field supports the full Lucene Search syntax, some details
227 can be found near https://musicbrainz.org/doc/MusicBrainz_API/Search
228 or in the Lucene docs."
230 (message "MusicBrainz: searching %s=%s" type query)
231 (let* ((max (if limit limit 1))
232 (from (if offset offset ""))
234 (request-response-data
237 (format "%s/%s?query=%s&fmt=json&limit=%s&offset=%s"
238 musicbrainz-api-url type query max from))
240 :headers (list `("User-Agent" . ,musicbrainz-user-agent))
243 :success (cl-function
244 (lambda (&key data &allow-other-keys)
250 (defun musicbrainz-find (query &rest extras)
251 "Search MusicBrainz for QUERY (and EXTRAS) or recommend a more specific search.
252 MusicBrainz makes a distinction between `search', `lookup' and `browse' this
253 provides a more general entry point to searching/browsing the database.
257 - if QUERY is an MBID, check artist, recording, etc
258 - if QUERY is text, search for artists or recordings, etc"
260 (message "MusicBrainz: query %s" query)
261 (if (musicbrainz-mbid-p query)
262 ;; search (lookup) for things that could have an mbid
264 (message "searching mbid: %s" mbid))
265 ;; search (search/browse/query) for other things
267 (message "searching other: %s" mbid)
271 ;; various specific searches
273 (defun musicbrainz-search-artist (artist &optional limit)
274 "Search for an ARTIST and show matches.
275 Optionally return LIMIT number of results."
276 (let ((data (musicbrainz-search "artist" artist limit)))
283 (format "%s | %s |\n" .name .id)
284 (format "%s | %s | %s |\n"
289 (defun musicbrainz-artist-to-mbid (artist)
290 "Find an MBID for ARTIST (with 100% match).
291 See `musicbrainz-disambiguate-artist' if there are multiple matches."
292 (let ((data (musicbrainz-search "artist" artist)))
294 (car (remove nil (seq-map
302 (defun musicbrainz-disambiguate-artist (artist &optional limit)
303 "More ARTIST data. less ambiguity (with optional LIMIT).
304 Outputs an `org-mode' table with descriptions and MBID link to artists pages."
305 (let* ((max (if limit limit 11))
306 (data (musicbrainz-search "artist" artist max)))
308 (cons (format "| Artist: %s| MBID |\n" artist)
312 (format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
313 .score .name .disambiguation .id .id)))
317 (defun musicbrainz-search-label (label &optional limit)
318 "Search for a LABEL and show matches.
319 Optionally return LIMIT number of results."
320 (let ((data (musicbrainz-search "label" label limit)))
327 (format "%s | [[https://musicbrainz.org/label/%s][%s]] |\n" .name .id .id)
328 (format "%s | %s | %s (%s%s) | [[https://musicbrainz.org/label/%s][%s]] |\n"
330 (if .disambiguation .disambiguation "")
332 (format "%s " .life-span.begin) "")
334 (format "—%s" .life-span.end)
340 (defun musicbrainz-search-recording (query &optional limit)
341 "Search for a recording using QUERY and show matches.
342 Optionally return LIMIT number of results."
343 (let ((data (musicbrainz-search "recording" query limit)))
349 (format "%s | %s, %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
350 .score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)))
354 (defun musicbrainz-search-release (query &optional limit)
355 "Search for a release using QUERY and show matches.
356 Optionally return LIMIT number of results."
357 (let ((data (musicbrainz-search "release" query limit)))
363 (format "%s | %s, %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
364 .score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)))
368 ;;; ;; ;; ; ; ; ; ; ;
371 ;; https://musicbrainz.org/doc/MusicBrainz_API#Lookups
376 (defun musicbrainz-lookup (entity mbid &optional inc)
377 "Search (lookup not browse) MusicBrainz for ENTITY with MBID.
378 Optionally add an INC list.
382 /ws/2/artist recordings, releases, release-groups, works
383 /ws/2/collection user-collections (requires authentication)
389 /ws/2/recording artists, releases, isrcs, url-rels
390 /ws/2/release artists, collections, labels, recordings, release-groups
391 /ws/2/release-group artists, releases
396 (message "MusicBrainz: lookup: %s/%s" entity mbid)
397 (if (and (musicbrainz-core-entity-p entity)
398 (musicbrainz-mbid-p mbid))
399 (let* ((add (if inc inc ""))
401 (request-response-data
404 (format "%s/%s/%s?inc=%s&fmt=json"
405 musicbrainz-api-url entity mbid add))
409 :success (cl-function
410 (lambda (&key data &allow-other-keys)
411 (message "%s data: %s" entity mbid)))))))
413 (error "MusicBrainz: search requires a valid MBID and entity (i.e. one of %s)"
414 musicbrainz-entities-core)))
417 ;; specific MBID lookup requests & subrequests (limited to 25 results?)
419 (defmacro musicbrainz--deflookup-1 (name format-string format-args)
420 "Generate lookup function to format a single item.
421 NAME FORMAT-STRING FORMAT-ARGS
422 See listenbrainz--deformatter for details."
423 (let ((f (intern (concat "musicbrainz-lookup-" name)))
424 (doc "MusicBrainz lookup."))
425 `(defun ,f (mbid) ,doc
427 (musicbrainz-lookup ,name mbid)))
429 (format ,format-string ,@format-args))))))
432 (defmacro musicbrainz--deflookup-2 (query subquery format-string format-args alist)
433 "Generate lookup function to format multiple items.
434 QUERY SUBQUERY FORMAT-STRING FORMAT-ARGS ALIST
435 See listenbrainz--deformatter for details."
436 (let ((f (intern (format "musicbrainz-lookup-%s-%s" query subquery)))
437 (doc "MusicBrainz lookup."))
438 `(defun ,f (mbid) ,doc
440 (musicbrainz-lookup ,query mbid ,subquery)))
445 (format ,format-string ,@format-args)))
449 ;; (defun musicbrainz-lookup-artist (mbid)
450 ;; "MusicBrainz lookup for artist with MBID."
452 ;; (musicbrainz-lookup "artist" mbid)))
453 ;; (let-alist response
454 ;; (format "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
455 ;; .name .disambiguation .type .id .id))))
458 ;; (defun musicbrainz-lookup-artist-recordings (mbid)
459 ;; "MusicBrainz lookup for recordings from artist with MBID."
461 ;; (musicbrainz-lookup "artist" mbid "recordings")))
462 ;; (let-alist response
466 ;; (format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
471 ;; lookup -> musicbrainz-lookup-area
472 (musicbrainz--deflookup-1 "area"
473 "| %s | [[https://musicbrainz.org/area/%s][%s]] |\n"
476 ;; lookup -> musicbrainz-lookup-artist
477 (musicbrainz--deflookup-1 "artist"
478 "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
479 (.name .disambiguation .type .id .id))
481 ;; lookup -> musicbrainz-lookup-artist-recordings
482 (musicbrainz--deflookup-2 "artist" "recordings"
483 "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
487 ;; lookup -> musicbrainz-lookup-artist-releases
488 (musicbrainz--deflookup-2 "artist" "releases"
489 "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
490 (.date .title .packaging .id .id)
493 ;; lookup -> musicbrainz-lookup-artist-release-groups
494 (musicbrainz--deflookup-2 "artist" "release-groups"
495 "%s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
496 (.first-release-date .title .primary-type .id .id)
499 ;; lookup -> musicbrainz-lookup-artist-works
500 (musicbrainz--deflookup-2 "artist" "works"
501 " %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
505 ;; lookup -> musicbrainz-lookup-collection
506 (musicbrainz--deflookup-1 "collection"
507 "| %s | [[https://musicbrainz.org/collection/%s][%s]] |\n"
510 ;; lookup -> musicbrainz-lookup-collection-user-collections (requires authentication)
511 (musicbrainz--deflookup-2 "collection" "user-collections"
512 " %s | [[https://musicbrainz.org/collection/%s][%s]] |\n"
516 ;; lookup -> musicbrainz-lookup-event
517 (musicbrainz--deflookup-1 "event"
518 "| %s | [[https://musicbrainz.org/event/%s][%s]] |\n"
521 ;; lookup -> musicbrainz-lookup-genre
522 (musicbrainz--deflookup-1 "genre"
523 "| %s | [[https://musicbrainz.org/genre/%s][%s]] |\n"
526 ;; lookup -> musicbrainz-lookup-instrument
527 (musicbrainz--deflookup-1 "instrument"
528 "| %s | %s | [[https://musicbrainz.org/instrument/%s][%s]] |\n"
529 (.name .type .id .id))
531 ;; lookup -> musicbrainz-lookup-label
532 (musicbrainz--deflookup-1 "label"
533 "| %s | %s | [[https://musicbrainz.org/label/%s][%s]] |\n"
534 (.name .disambiguation .id .id))
537 ;; lookup -> musicbrainz-lookup-label-releases
538 (musicbrainz--deflookup-2 "label" "releases"
539 "%s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
540 (.date .title .id .id)
543 ;; lookup -> musicbrainz-lookup-place
544 (musicbrainz--deflookup-1 "place"
545 "| %s | [[https://musicbrainz.org/place/%s][%s]] |\n"
548 ;; lookup -> musicbrainz-lookup-recording
549 (musicbrainz--deflookup-1 "recording"
550 "| %s | %s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
551 (.first-release-date .title .id .id))
554 ;; lookup -> musicbrainz-lookup-recording-artists
555 (musicbrainz--deflookup-2 "recording" "artists"
556 "%s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
557 (.artist.name .artist.id .artist.id)
560 ;; lookup -> musicbrainz-lookup-recording-releases
561 (musicbrainz--deflookup-2 "recording" "releases"
562 "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
563 (.date .title .packaging .id .id)
566 ;; lookup -> musicbrainz-lookup-recording-isrcs
567 (musicbrainz--deflookup-2 "recording" "isrcs"
568 "%s | [[https://musicbrainz.org/isrc/%s][%s]] |\n"
572 ;; lookup -> musicbrainz-lookup-recording-url-rels
573 (musicbrainz--deflookup-2 "recording" "url-rels"
574 "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
578 ;; lookup -> musicbrainz-lookup-release
579 (musicbrainz--deflookup-1 "release"
580 "| %s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
581 (.date .title .packaging .id .id))
583 ;; lookup -> musicbrainz-lookup-release-artists
584 (musicbrainz--deflookup-2 "release" "artists"
585 "%s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
586 (.artist.name .artist.id .artist.id)
589 ;; lookup -> musicbrainz-lookup-release-collections
591 ;; lookup -> musicbrainz-lookup-release-labels
593 ;; lookup -> musicbrainz-lookup-release-recordings
595 ;; lookup -> musicbrainz-lookup-release-release-groups
597 ;; lookup -> musicbrainz-lookup-release-group
598 (musicbrainz--deflookup-1 "release-group"
599 "| %s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
600 (.first-release-date .title .primary-type .id .id))
602 ;; lookup -> musicbrainz-lookup-release-group-artists
603 (musicbrainz--deflookup-2 "release-group" "artists"
604 "%s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
605 (.artist.name .artist.id .artist.id)
608 ;; lookup -> musicbrainz-lookup-release-group-releases
610 ;; lookup -> musicbrainz-lookup-series
611 (musicbrainz--deflookup-1 "series"
612 "| %s | [[https://musicbrainz.org/series/%s][%s]] |\n"
615 ;; lookup -> musicbrainz-lookup-work
616 (musicbrainz--deflookup-1 "work"
617 "| %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
620 ;; lookup -> musicbrainz-lookup-url
621 (musicbrainz--deflookup-1 "url"
622 "| %s | [[https://musicbrainz.org/url/%s][%s]] |\n"
627 ;;;;;; ; ; ;; ; ; ; ; ; ;; ;
630 ;; https://musicbrainz.org/doc/MusicBrainz_API#Browse
634 ;; Browse requests are a direct lookup of all the entities directly linked
635 ;; to another entity ("directly linked" here meaning it does not include
636 ;; entities linked by a relationship). For example, you may want to see all
637 ;; releases on the label ubiktune:
639 ;; /ws/2/release?label=47e718e1-7ee4-460c-b1cc-1192a841c6e5
641 ;; Note that browse requests are not searches: in order to browse all the releases
642 ;; on the ubiktune label you will need to know the MBID of ubiktune.
644 ;; The order of the results depends on what linked entity you are browsing
645 ;; by (however it will always be consistent). If you need to sort the entities,
646 ;; you will have to fetch all entities and sort them yourself.
648 ;; Keep in mind only the search request is available without an MBID (or, in
649 ;; specific cases, a disc ID, ISRC or ISWC). If all you have is the name of an
650 ;; artist or album, for example, you'll need to make a search and pick the right
651 ;; result to get its MBID to use it in a lookup or browse request.
655 (defun musicbrainz-browse (entity link query &optional type)
656 "Search the MusicBrainz database for ENTITY with LINK matching QUERY.
657 Optionally limit the search to TYPE results for ENTITY."
658 (message "MusicBrainz: browsing %s linked to %s" entity link)
659 (message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type)
661 (request-response-data
664 (format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type))
666 :header (list `("User-Agent" . ,musicbrainz-user-agent))
669 :success (cl-function
670 (lambda (&key data &allow-other-keys)
677 (provide 'musicbrainz)
679 ;;; musicbrainz.el ends here