]> git.vanrenterghem.biz Git - musicbrainz.git/blob - musicbrainz.el
e5f41a91b4f4c8cf17d7f9adf6407c7220a1b845
[musicbrainz.git] / musicbrainz.el
1 ;;; musicbrainz.el --- MusicBrainz API interface -*- coding: utf-8; lexical-binding: t -*-
3 ;; Copyright 2023 FoAM
4 ;;
5 ;; Author: nik gaffney <nik@fo.am>
6 ;; Created: 2023-05-05
7 ;; Version: 0.1
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/>.
28 ;;; Commentary:
30 ;; - basic MusicBrainz interface
31 ;; - partial & incomplete
32 ;; - no error checks
33 ;; - sync -> async
36 ;;; Code:
38 (require 'request)
39 (require 'json)
42 ;;; ;; ;; ;  ; ;   ;  ;      ;
43 ;;
44 ;; API config
45 ;;
46 ;;; ; ;; ;;
48 (defcustom musicbrainz-api-url "https://musicbrainz.org/ws/2"
49   "URL for musicbrainz API.
50 Documentation available at https://musicbrainz.org/doc/MusicBrainz_API"
51   :type 'string
52   :group 'musicbrainz)
54 (defcustom musicbrainz-api-token ""
55   "An auth token is required for some functions."
56   :type 'string
57   :group 'musicbrainz)
59 ;;; ; ; ;;;   ;  ;
60 ;;
61 ;; API entities
62 ;;  https://musicbrainz.org/doc/MusicBrainz_API#Browse
63 ;;
64 ;; On each entity resource, you can perform three different GET requests:
65 ;;  lookup:   /<ENTITY_TYPE>/<MBID>?inc=<INC>
66 ;;  browse:   /<RESULT_ENTITY_TYPE>?<BROWSING_ENTITY_TYPE>=<MBID>&limit=<LIMIT>&offset=<OFFSET>&inc=<INC>
67 ;;  search:   /<ENTITY_TYPE>?query=<QUERY>&limit=<LIMIT>&offset=<OFFSET>
68 ;;
69 ;; Note: Keep in mind only the search request is available without an MBID
70 ;; (or, in specific cases, a disc ID, ISRC or ISWC). If all you have is the
71 ;; name of an artist or album, for example, you'll need to make a search and
72 ;; pick the right result to get its MBID; only then will you able to use it
73 ;; in a lookup or browse request.
74 ;;
75 ;; On the genre resource, we support an "all" sub-resource to fetch all genres,
76 ;; paginated, in alphabetical order:
77 ;;
78 ;;  all:      /genre/all?limit=<LIMIT>&offset=<OFFSET>
79 ;;
80 ;; ; ;;; ;
82 (defconst musicbrainz-entities-core
83   (list "area" "artist" "event" "genre" "instrument" "label" "place"
84         "recording" "release" "release-group" "series" "work" "url")
85   "API resources which represent core entities in the MusicBrainz database.")
87 (defconst musicbrainz-entities-non-core
88   (list "rating" "tag" "collection")
89   "API resources which represent non-core entities in the MusicBrainz database.")
91 (defconst musicbrainz-entities-uids
92   (list "discid" "isrc" "iswc")
93   "API resources based on other unique identifiers in the MusicBrainz database.")
95 (defconst musicbrainz-entities-linked
96   (list "area" "artist" "collection" "event" "instrument" "label" "place"
97         "recording" "release" "release-group" "series" "work" "url")
98   "API resources for linked entites in the MusicBrainz database.")
100 (defconst musicbrainz-search-types
101   (list "annotation" "area" "artist" "cdstub" "event" "instrument"
102         "label" "place" "recording" "release" "release-group"
103         "series" "tag" "work" "url")
104   "Valid TYPE parameters for MusicBrainz searches.")
107 ;; entity checks
109 (defun musicbrainz-core-entity-p (entity)
110   "Check if ENTITY is a core entity."
111   (if (member entity musicbrainz-entities-core) t nil))
113 (defun musicbrainz-non-core-entity-p (entity)
114   "Check if ENTITY is a non-core entity."
115   (if (member entity musicbrainz-entities-non-core) t nil))
117 (defun musicbrainz-uid-entity-p (entity)
118   "Check if ENTITY is a unique identifier entity."
119   (if (member entity musicbrainz-entities-uids) t nil))
121 (defun musicbrainz-search-type-p (type)
122   "Check if TYPE is a valid search type."
123   (if (member type musicbrainz-search-types) t nil))
126 ;; Linked entities
128 (defun musicbrainz-linked-entity-p (entity)
129   "Check if ENTITY can be used in a browse request (incomplete).
131 The following list shows which linked entities you can use in a browse request:
133  /ws/2/area            collection
134  /ws/2/artist          area, collection, recording, release, release-group, work
135  /ws/2/collection      area, artist, editor, event, label, place, recording,
136                        release, release-group, work
137  /ws/2/event           area, artist, collection, place
138  /ws/2/instrument      collection
139  /ws/2/label           area, collection, release
140  /ws/2/place           area, collection
141  /ws/2/recording       artist, collection, release, work
142  /ws/2/release         area, artist, collection, label, track, track_artist,
143                        recording, release-group
144  /ws/2/release-group   artist, collection, release
145  /ws/2/series          collection
146  /ws/2/work            artist, collection
147  /ws/2/url             resource"
149   (if (member entity musicbrainz-entities-linked) t nil))
152 ;; utils & aux
154 (defun musicbrainz-mbid-p (mbid)
155   "Check (permissive) if MBID is valid and/or well formatted.
156 An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz.org/doc/MusicBrainz_Identifier for details."
158   (if (and (length= mbid 36) ;; length= requires emacs > 28.1
159            (string-match-p
160             (rx (repeat 8 hex)        ;;  [A-F0-9]{8}
161                 "-" (repeat 4 hex)    ;; -[A-F0-9]{4}
162                 "-4" (repeat 3 hex)   ;; -4[A-F0-9]{3}
163                 "-" (repeat 4 hex)    ;; -[89AB][A-F0-9]{3}
164                 "-" (repeat 12 hex))  ;; -[A-F0-9]{12}
165             mbid))
166       t nil))
169 (defun musicbrainz-format (response)
170   "Format a generic RESPONSE."
171   (format "%s" (pp response)))
174 ;;; ;; ;; ;  ; ;   ;  ;      ;
175 ;;
176 ;; Search API
177 ;;  https://musicbrainz.org/doc/MusicBrainz_API/Search
178 ;;
179 ;; The MusicBrainz API search requests provide a way to search for MusicBrainz
180 ;; entities based on different sorts of queries and are provided by a search
181 ;; server built using Lucene technology.
182 ;;
183 ;; Parameters common to all resources
184 ;;
185 ;;  type    Selects the entity index to be searched: annotation, area, artist,
186 ;;          cdstub, event, instrument, label, place, recording, release,
187 ;;          release-group, series, tag, work, url
188 ;;
189 ;;  query   Lucene search query. This is mandatory
190 ;;
191 ;;  limit   An integer value defining how many entries should be returned.
192 ;;          Only values between 1 and 100 (both inclusive) are allowed.
193 ;;          If not given, this defaults to 25.
194 ;;
195 ;;  offset  Return search results starting at a given offset.
196 ;;          Used for paging through more than one page of results.
197 ;;
198 ;;  dismax  If set to "true", switches the Solr query parser from edismax to dismax,
199 ;;          which will escape certain special query syntax characters by default
200 ;;          for ease of use. This is equivalent to switching from the "Indexed search
201 ;;          with advanced query syntax" method to the plain "Indexed search" method
202 ;;          on the website. Defaults to "false".
203 ;;
204 ;; ;; ; ;  ;
206 ;;;###autoload
207 (defun musicbrainz-search (type query &optional limit offset)
208   "Search the MusicBrainz database for TYPE matching QUERY.
209 Optionally return only LIMIT number of results from OFFSET.
211 The QUERY field supports the full Lucene Search syntax, some details
212 can be found near https://musicbrainz.org/doc/MusicBrainz_API/Search
213 or in the Lucene docs."
215   (message "musicbrainz: searching %s=%s" type query)
216   (let* ((max (if limit limit 1))
217          (from (if offset offset ""))
218          (response
219            (request-response-data
220             (request
221              (url-encode-url
222               (format "%s/%s?query=%s&fmt=json&limit=%s&offset=%s"
223                       musicbrainz-api-url type query max from))
224              :type "GET"
225              :parser 'json-read
226              :sync t
227              :success (cl-function
228                        (lambda (&key data &allow-other-keys)
229                          (message "ok")))))))
230     response))
234 ;;;###autoload
235 (defun musicbrainz-find (query &rest extras)
236   "Search the MusicBrainz database for QUERY or recommend a more specific search.
237 MusicBrainz makes a distinction between `search' and `browse' this a more general
238 entry point to searching/browsing the database.
240 Heuristics.
241 - if QUERY is an MBID, check artist, recording, etc
242 - if QUERY is text, search for artists or recordings, etc"
244   (message "musicbrainz: finding: %s" query)
245   (if (musicbrainz-mbid-p query)
246       ;; search (lookup) for things that could have an mbid
247       (let ((mbid query))
248         (message "searching mbid: %s" mbid))
249       ;; search (query) for other things
250       (progn
251         (message "searching other: %s" mbid)
252         ;; (message "searching artist: %s" query)
253         ;; (musicbrainz-format (musicbrainz-search "artist"  query))
254         ;; (message "searching label: %s" query)
255         ;; (musicbrainz-format (musicbrainz-search "label"  query))
256         ;; (message "searching release: %s" query)
257         ;; (musicbrainz-format (musicbrainz-search "release"  query))
258         )))
262 ;; various specific searches
264 ;;;###autoload
265 (defun musicbrainz-search-artist (artist &optional limit)
266   "Search for an ARTIST and show matches.
267 Optionally return LIMIT number of results."
268   (let ((data (musicbrainz-search "artist" artist limit)))
269     (let-alist
270      data
271      (seq-map
272       (lambda (i)
273         (let-alist i
274                    (if (not limit)
275                        (format "%s | %s |\n" .name .id)
276                        (format "%s | %s | %s |\n"
277                                .score .name .id))))
278       .artists))))
281 ;;;###autoload
282 (defun musicbrainz-artist-to-mbid (artist)
283   "Find an MBID for ARTIST (with 100% match).
284 See `musicbrainz-disambiguate-artist' if there are multiple matches."
285   (let ((data (musicbrainz-search "artist" artist)))
286     (let-alist data
287                (car (remove nil (seq-map
288                                  (lambda (i)
289                                    (let-alist i
290                                               (when (= 100 .score)
291                                                 (format "%s" .id))))
292                                  .artists))))))
295 ;;;###autoload
296 (defun musicbrainz-disambiguate-artist (artist &optional limit)
297   "More ARTIST data. less ambiguity (with optional LIMIT).
298 Outputs an `org-mode' table with descriptions and MBID link to artists pages."
299   (let* ((max (if limit limit 11))
300          (data (musicbrainz-search "artist" artist max)))
301     (let-alist data
302                (cons (format "| Artist: %s| MBID |\n" artist)
303                      (seq-map
304                       (lambda (i)
305                         (let-alist i
306                                    (format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
307                                            .score .name .disambiguation .id .id)))
308                       .artists)))))
311 ;;;###autoload
312 (defun musicbrainz-search-label (label &optional limit)
313   "Search for a LABEL and show matches.
314 Optionally return LIMIT number of results."
315   (let ((data (musicbrainz-search "label" label limit)))
316     (let-alist
317      data
318      (seq-map
319       (lambda (i)
320         (let-alist i
321                    (if (not limit)
322                        (format "%s | %s |\n" .name .id)
323                        (format "%s | %s | %s (%s%s) | %s |\n"
324                                .score .name
325                                (if .disambiguation .disambiguation "")
326                                (if .life-span.begin
327                                    (format "%s " .life-span.begin) "")
328                                (if .life-span.end
329                                    (format "—%s" .life-span.end)
330                                    "ongoing")
331                                .id))))
332       .labels))))
335 ;;;###autoload
336 (defun musicbrainz-search-recording (query &optional limit)
337   "Search for a recording using QUERY and show matches.
338 Optionally return LIMIT number of results."
339   (let ((data (musicbrainz-search "recording" query limit)))
340     (let-alist
341      data
342      (seq-map
343       (lambda (i)
344         (let-alist i
345                    (format "%s | %s, %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
346                            .score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)))
347       .recordings))))
350 (defun musicbrainz--unwrap-0 (entity)
351   "Unwrap (fragile) .artist-credit ENTITY -> .name more or less."
352   (format "%s" (cdar (aref entity 0))))
354 ;;;###autoload
355 (defun musicbrainz-search-release (query &optional limit)
356   "Search for a release using QUERY and show matches.
357 Optionally return LIMIT number of results."
358   (let ((data (musicbrainz-search "release" query limit)))
359     (let-alist
360      data
361      (seq-map
362       (lambda (i)
363         (let-alist i
364                    (format "%s | %s, %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
365                            .score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)))
366       .releases))))
369 ;;; ;; ;; ;  ; ;   ;  ;      ;
370 ;;
371 ;; Lookups
372 ;;  https://musicbrainz.org/doc/MusicBrainz_API#Lookups
373 ;;
374 ;;; ;; ;;   ; ;
376 ;;;###autoload
377 (defun musicbrainz-lookup (entity mbid &optional inc)
378   "Search (lookup not browse) the MusicBrainz database for ENTITY with MBID.
379 Optionally add an INC list.
381 Subqueries
382  /ws/2/area
383  /ws/2/artist            recordings, releases, release-groups, works
384  /ws/2/collection        user-collections (includes private collections, requires authentication)
385  /ws/2/event
386  /ws/2/genre
387  /ws/2/instrument
388  /ws/2/label             releases
389  /ws/2/place
390  /ws/2/recording         artists, releases, isrcs, url-rels
391  /ws/2/release           artists, collections, labels, recordings, release-groups
392  /ws/2/release-group     artists, releases
393  /ws/2/series
394  /ws/2/work
395  /ws/2/url"
397   (message "musicbrainz: lookup: %s/%s" entity mbid)
398   (if (and (musicbrainz-core-entity-p entity)
399            (musicbrainz-mbid-p mbid))
400       (let* ((add (if inc inc ""))
401              (response
402                (request-response-data
403                 (request
404                  (url-encode-url
405                   (format "%s/%s/%s?inc=%s&fmt=json"
406                           musicbrainz-api-url entity mbid add))
407                  :type "GET"
408                  :parser 'json-read
409                  :sync t
410                  :success (cl-function
411                            (lambda (&key data &allow-other-keys)
412                              (message "%s data: %s" entity mbid)))))))
413         response)
414       (error "MusicBrainz: search requires a valid MBID and entity (i.e. one of %s)"
415              musicbrainz-entities-core)))
418 ;; specific MBID subrequests (limited to 25 results?)
420 (defun musicbrainz-lookup-artist (mbid)
421   "MusicBrainz lookup for artist with MBID."
422   (let ((response
423           (musicbrainz-lookup "artist" mbid)))
424     (let-alist response
425                (format "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
426                        .name .disambiguation .type .id .id))))
429 (defun musicbrainz-lookup-release (mbid)
430   "MusicBrainz lookup for release with MBID."
431   (let ((response
432           (musicbrainz-lookup "release" mbid)))
433     (let-alist response
434                (format "| %s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
435                        .date .title .packaging .id .id))))
437 (defun musicbrainz-lookup-recording (mbid)
438   "MusicBrainz lookup for recording with MBID."
439   (let ((response
440           (musicbrainz-lookup "recording" mbid)))
441     (let-alist response
442                (format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
443                        .title .id .id))))
446 (defun musicbrainz-lookup-artist-releases (mbid)
447   "MusicBrainz lookup for releases from artist with MBID."
448   (let ((response
449           (musicbrainz-lookup "artist" mbid "releases")))
450     (let-alist response
451                (seq-map
452                 (lambda (i)
453                   (let-alist i
454                              (format "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
455                                      .date .title .packaging .id .id)))
456                 .releases))))
459 (defun musicbrainz-lookup-artist-recordings (mbid)
460   "MusicBrainz lookup for recordings from artist with MBID."
461   (let ((response
462           (musicbrainz-lookup "artist" mbid "recordings")))
463     (let-alist response
464                (seq-map
465                 (lambda (i)
466                   (let-alist i
467                              (format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
468                                      .title .id .id)))
469                 .recordings))))
473 ;;;;;; ; ; ;; ;   ;     ;  ; ; ;;   ;
474 ;;
475 ;; Browse API
476 ;;  https://musicbrainz.org/doc/MusicBrainz_API#Browse
477 ;;
478 ;;;; ; ; ; ; ;
480 ;; Browse requests are a direct lookup of all the entities directly linked
481 ;; to another entity ("directly linked" here meaning it does not include
482 ;; entities linked by a relationship). For example, you may want to see all
483 ;; releases on the label ubiktune:
485 ;; /ws/2/release?label=47e718e1-7ee4-460c-b1cc-1192a841c6e5
487 ;; Note that browse requests are not searches: in order to browse all the releases
488 ;; on the ubiktune label you will need to know the MBID of ubiktune.
490 ;; The order of the results depends on what linked entity you are browsing
491 ;; by (however it will always be consistent). If you need to sort the entities,
492 ;; you will have to fetch all entities and sort them yourself.
494 ;; Keep in mind only the search request is available without an MBID (or, in
495 ;; specific cases, a disc ID, ISRC or ISWC). If all you have is the name of an
496 ;; artist or album, for example, you'll need to make a search and pick the right
497 ;; result to get its MBID to use it in a lookup or browse request.
500 ;;;###autoload
501 (defun musicbrainz-browse (entity link query &optional type)
502   "Search the MusicBrainz database for ENTITY with LINK matching QUERY.
503 Optionally limit the search to TYPE results for ENTITY."
504   (message "musicbrainz: browsing %s linked to %s" entity link)
505   (message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type)
506   (let ((response
507           (request-response-data
508            (request
509             (url-encode-url
510              (format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type))
511             :type "GET"
512             :parser 'json-read
513             :sync t
514             :success (cl-function
515                       (lambda (&key data &allow-other-keys)
516                         (message "ok")))))))
517     response))
520 ;;;
522 (provide 'musicbrainz)
524 ;;; musicbrainz.el ends here