]> git.vanrenterghem.biz Git - musicbrainz.git/blob - musicbrainz.el
Eat your greens, especially broccoli
[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 "27.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.")
101 ;; Linked entities
103 (defun musicbrainz-linked-entity-p (entity)
104   "Check if ENTITY can be used in a browse request (incomplete).
106 The following list shows which linked entities you can use in a browse request:
108  /ws/2/area            collection
109  /ws/2/artist          area, collection, recording, release, release-group, work
110  /ws/2/collection      area, artist, editor, event, label, place, recording,
111                        release, release-group, work
112  /ws/2/event           area, artist, collection, place
113  /ws/2/instrument      collection
114  /ws/2/label           area, collection, release
115  /ws/2/place           area, collection
116  /ws/2/recording       artist, collection, release, work
117  /ws/2/release         area, artist, collection, label, track, track_artist,
118                        recording, release-group
119  /ws/2/release-group   artist, collection, release
120  /ws/2/series          collection
121  /ws/2/work            artist, collection
122  /ws/2/url             resource"
124   (if (member entity musicbrainz-entities-linked) t nil))
127 ;; utils & aux
129 (defun musicbrainz-mbid-p (mbid)
130   "Check (permissive) if MBID is valid and/or well formatted.
131 An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz.org/doc/MusicBrainz_Identifier for details."
132   (if (and (length= mbid 36)
133            (string-match-p
134             (rx (repeat 8 hex)        ;;  [A-F0-9]{8}
135                 "-" (repeat 4 hex)    ;; -[A-F0-9]{4}
136                 "-4" (repeat 3 hex)   ;; -4[A-F0-9]{3}
137                 "-" (repeat 4 hex)    ;; -[89AB][A-F0-9]{3}
138                 "-" (repeat 12 hex))  ;; -[A-F0-9]{12}
139             mbid))
140       t nil))
143 ;;; ;; ;; ;  ; ;   ;  ;      ;
144 ;;
145 ;; Search API
146 ;;  https://musicbrainz.org/doc/MusicBrainz_API/Search
147 ;;
148 ;; ;; ; ;  ;
151 ;;;###autoload
152 (defun musicbrainz-search (entity query &optional limit)
153   "Search the MusicBrainz database for ENTITY matching QUERY.
154 Optionally return only LIMIT number of results.
156 The QUERY field supports the full Lucene Search syntax, some details
157 can be found near https://musicbrainz.org/doc/MusicBrainz_API/Search
158 or in the Lucene docs."
160   (message "musicbrainz: searching %s=%s" entity query)
161   (let* ((max (if limit limit 1))
162          (response
163           (request-response-data
164            (request
165             (url-encode-url
166              (format "%s/%s?query=%s&fmt=json&limit=%s"
167                      musicbrainz-api-url entity query max))
168             :type "GET"
169             :parser 'json-read
170             :sync t
171             :success (cl-function
172                       (lambda (&key data &allow-other-keys)
173                         (if (eq t (assoc-default 'valid data))
174                             (message "Token is valid for user: %s"
175                                      (assoc-default 'user_name data))
176                             (message "Not a valid user token"))))))))
177     response))
180 ;; various specific searches
182 ;;;###autoload
183 (defun musicbrainz-search-artist (artist &optional limit)
184   "Search for an ARTIST and show matches.
185 Optionally return LIMIT number of results."
186   (let ((data (musicbrainz-search "artist" artist limit)))
187     (let-alist
188       data
189               (seq-map
190                (lambda (i)
191                  (let-alist i
192                             (if (not limit)
193                                 (format "%s | %s |\n" .name .id)
194                                 (format "%s | %s | %s |\n"
195                                         .score .name .id))))
196                .artists))))
199 ;;;###autoload
200 (defun musicbrainz-artist-to-mbid (artist)
201   "Find an MBID for ARTIST (with 100% match).
202 See `musicbrainz-disambiguate-artist' if there are multiple matches."
203   (let ((data (musicbrainz-search "artist" artist)))
204     (let-alist data
205                (car (remove nil (seq-map
206                             (lambda (i)
207                               (let-alist i
208                                          (when (= 100 .score)
209                                            (format "%s" .id))))
210                             .artists))))))
213 ;;;###autoload
214 (defun musicbrainz-disambiguate-artist (artist &optional limit)
215   "More ARTIST data. less ambiguity (with optional LIMIT).
216 Outputs an `org-mode' table with descriptions and MBID link to artists pages."
217     (let ((data (musicbrainz-search "artist" artist limit)))
218     (let-alist data
219                (cons (format "| Artist: %s| MBID |\n" artist)
220                (seq-map
221                 (lambda (i)
222                   (let-alist i
223                              (format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
224                                      .score .name .disambiguation .id .id)))
225                 .artists)))))
228 ;;;###autoload
229 (defun musicbrainz-search-label (label &optional limit)
230   "Search for a LABEL and show matches.
231 Optionally return LIMIT number of results."
232   (let ((data (musicbrainz-search "label" label limit)))
233     (let-alist
234       data
235       (seq-map
236        (lambda (i)
237          (let-alist i
238                     (if (not limit)
239                         (format "%s | %s |\n" .name .id)
240                         (format "%s | %s | %s (%s%s) | %s |\n"
241                                 .score .name
242                                 (if .disambiguation .disambiguation "")
243                                 (if .life-span.begin
244                                     (format "%s " .life-span.begin) "")
245                                 (if .life-span.end
246                                     (format "—%s" .life-span.end)
247                                     "ongoing")
248                                 .id))))
249        .labels))))
253 ;;;;;; ; ; ;; ;   ;     ;  ; ; ;;   ;
254 ;;
255 ;; Browse API
256 ;;  https://musicbrainz.org/doc/MusicBrainz_API#Browse
257 ;;
258 ;;;; ; ; ; ; ;
260 ;; Browse requests are a direct lookup of all the entities directly linked
261 ;; to another entity ("directly linked" here meaning it does not include
262 ;; entities linked by a relationship). For example, you may want to see all
263 ;; releases on the label ubiktune:
265 ;; /ws/2/release?label=47e718e1-7ee4-460c-b1cc-1192a841c6e5
267 ;; Note that browse requests are not searches: in order to browse all the releases
268 ;; on the ubiktune label you will need to know the MBID of ubiktune.
270 ;; The order of the results depends on what linked entity you are browsing
271 ;; by (however it will always be consistent). If you need to sort the entities,
272 ;; you will have to fetch all entities and sort them yourself.
274 ;; Keep in mind only the search request is available without an MBID (or, in
275 ;; specific cases, a disc ID, ISRC or ISWC). If all you have is the name of an
276 ;; artist or album, for example, you'll need to make a search and pick the right
277 ;; result to get its MBID to use it in a lookup or browse request.
280 ;;;###autoload
281 (defun musicbrainz-browse (entity link lookup &optional type)
282   "Search the MusicBrainz database for ENTITY with LINK matching LOOKUP.
283 Optionally limit the search to TYPE results for ENTITY."
284   (message "musicbrainz: browsing %s linked to %s" entity link)
285   (message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link lookup type)
286   (let ((response
287           (request-response-data
288            (request
289             (url-encode-url
290              (format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link lookup type))
291             :type "GET"
292             :parser 'json-read
293             :sync t
294             :success (cl-function
295                       (lambda (&key data &allow-other-keys)
296                         (message "ok")))))))
297     response))
300 ;;;
302 (provide 'musicbrainz)
304 ;;; musicbrainz.el ends here