fordon_greeman / voidfiles

Branch: main
Switch branches/tags
main
theidioticdev add emacs 2079ec5 1d ago
269 lines · 10.1 KB Raw Blame History
1 ;;; emms-idapi-musicbrainz.el --- EMMS MusicBrainz API support -*- lexical-binding: t; truncate-lines: t; -*-
2 ;;
3
4 ;; Copyright (C) 2024 Free Software Foundation, Inc.
5
6 ;; Author: Yoni Rabkin <yrk@gnu.org>
7
8 ;; This file is part of EMMS.
9
10 ;; EMMS is free software; you can redistribute it and/or modify it
11 ;; under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 3, or (at your option)
13 ;; any later version.
14
15 ;; EMMS is distributed in the hope that it will be useful, but WITHOUT
16 ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
17 ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
18 ;; License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with EMMS; see the file COPYING. If not, write to the Free
22 ;; Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
23 ;; MA 02110-1301, USA.
24
25
26 ;;; Commentary:
27 ;;
28
29 ;;; Code:
30 (require 'mm-bodies)
31 (require 'url-vars)
32 (require 'json)
33 (require 'emms)
34
35
36 (defvar emms-idapi-musicbrainz-url-timeout-seconds 7
37 "Number of seconds to wait before an API call times out.")
38
39 (defvar emms-idapi-musicbrainz-url-buffer nil
40 "Buffer to store `url' response.")
41
42 (defvar emms-idapi-musicbrainz-response-limit 100
43 "Maximum number of responses to ask for. Maximum is 100.")
44
45 (defconst emms-idapi-musicbrainz-root-url "https://musicbrainz.org/ws/2/"
46 "The API root URL for the MusicBrainz service.")
47
48 (defvar emms-idapi-query-local nil
49 "Cross-call storage for search query.")
50 (make-variable-buffer-local 'emms-idapi-query-local)
51
52 (defvar emms-idapi-musicbrainz-debug-buffer-name
53 " *Emms MusicBrainz Debug Buffer*"
54 "Name of debug buffer for MusicBrainz url responses.")
55
56 (defvar emms-idapi-musicbrainz-debug-buffer nil
57 "Debug buffer for MusicBrainz url responses.")
58
59
60 (defconst emms-idapi-musicbrainz-search-string-map
61 '((info-artist . "artist")
62 (info-albumartist . "artist")
63 (info-title . "recording")
64 (info-album . "release"))
65 "Mapping between MusicBrainz API TYPES and Emms fields.")
66
67
68 ;;; ------------------------------------------------------------------
69 ;;; Response
70 ;;; ------------------------------------------------------------------
71 (defun emms-idapi-musicbrainz-decode (response)
72 "Decode the strings in response."
73 (mapc
74 (lambda (elt)
75 (when (stringp (cdr elt))
76 (setf (cdr elt) (mm-decode-string (cdr elt) 'utf-8))))
77 response))
78
79 (defun emms-idapi-musicbrainz-read-artist (artist)
80 "Return a track from the MusicBrainz ARTIST."
81 (when (not (alist-get 'id artist))
82 (error "could not parse from: %s" artist))
83 `(*track* (search-backend . musicbrainz)
84 (type . idapi-artist)
85 (name . nil)
86 (idapi-artist-id . ,(list (cons 'musicbrainz (alist-get 'id artist))))
87 (info-artist . ,(alist-get 'name artist))
88 (info-gender . ,(alist-get 'gender artist))
89 (info-type . ,(alist-get 'type artist))
90 (info-country . ,(alist-get 'country artist))
91 (info-area-type . ,(alist-get 'type (cddr (assoc 'area artist))))
92 (info-area-country . ,(alist-get 'name (cddr (assoc 'area artist))))
93 (info-aliases . ,(list
94 (seq-map
95 (lambda (elt)
96 (mm-decode-string
97 (alist-get 'sort-name elt) 'utf-8))
98 (alist-get 'aliases artist))))
99 (info-tags . ,(list
100 (seq-map
101 (lambda (elt)
102 (mm-decode-string
103 (alist-get 'name elt) 'utf-8))
104 (alist-get 'tags artist))))
105 (info-time . ,(alist-get 'life-span artist))))
106
107 (defun emms-idapi-musicbrainz-read-release (release)
108 "Return a track from the MusicBrainz RELEASE."
109 (when (not (alist-get 'id release))
110 (error "could not parse from: %s" release))
111 `(*track* (search-backend . musicbrainz)
112 (type . idapi-release)
113 (name . nil)
114 (idapi-release-id . ,(list (cons 'musicbrainz (alist-get 'id release))))
115 (info-artist . ,(alist-get 'name (elt (alist-get 'artist-credit release) 0)))
116 (info-album . ,(alist-get 'title release))
117 (info-status . ,(alist-get 'status release))
118 (info-disambiguation . ,(alist-get 'disambiguation release))
119 (info-packaging . ,(alist-get 'packaging release))
120 (info-date . ,(alist-get 'date release))
121 (info-country . ,(alist-get 'country release))
122 (info-track-count . ,(alist-get 'track-count release))))
123
124 (defun emms-idapi-musicbrainz-read-recording (recording)
125 "Return a track from the MusicBrainz RECORDING."
126 (when (not (alist-get 'id recording))
127 (error "could not parse from: %s" recording))
128 (let ((length-ms (or (alist-get 'length recording) 0)))
129 `(*track* (search-backend . musicbrainz)
130 (type . idapi-recording)
131 (name . ,(alist-get 'title recording))
132 (info-playing-time . ,(floor (/ length-ms 1000)))
133 (info-playing-time-min . ,(floor (/ (/ length-ms 1000) 60)))
134 (info-playing-time-sec . ,(% (floor (/ length-ms 1000)) 60))
135 (info-recording-id . ,(alist-get 'id recording))
136 (idapi-releases . ,(seq-map
137 (lambda (elt)
138 (emms-idapi-musicbrainz-read-release elt))
139 (alist-get 'releases recording)))
140 (info-length-ms . ,length-ms))))
141
142 (defun emms-idapi-musicbrainz-process-type-dispatch (response)
143 "Call the appropriate processing function for RESPONSE."
144 (let ((process-f (cond ((alist-get 'artists response) #'emms-idapi-musicbrainz-read-artist)
145 ((alist-get 'releases response) #'emms-idapi-musicbrainz-read-release)
146 ((alist-get 'recordings response) #'emms-idapi-musicbrainz-read-recording)
147 (t (error "unhandled response type %s" response))))
148 ;; the actual items without header data
149 (elements (cdr (nth 3 response)))
150 (debug-buffer (get-buffer-create emms-idapi-musicbrainz-debug-buffer-name)))
151 (setq emms-idapi-musicbrainz-debug-buffer debug-buffer)
152 (with-current-buffer debug-buffer
153 (erase-buffer)
154 (insert (format "%s" response)))
155 (append (alist-get 'query response)
156 (mapcar
157 #'(lambda (e)
158 (emms-idapi-musicbrainz-decode
159 (cdr (funcall process-f e))))
160 elements))))
161
162 (defun emms-idapi-musicbrainz-process-json (buffer)
163 "Return the sexp form of the json in BUFFER."
164 (with-current-buffer buffer
165 (goto-char (point-min))
166 (let ((header-max (if (not (re-search-forward "\n\n" (point-max)))
167 (error "cannot find header")
168 (point))))
169 (append
170 (json-read-from-string
171 (mm-decode-string
172 (buffer-substring header-max (point-max))
173 'utf-8))
174 `((query . ,(list emms-idapi-query-local)))))))
175
176 (defun emms-idapi-musicbrainz-process (buffer)
177 "Process response stored in BUFFER. Return BUFFER."
178 (when (or (not buffer)
179 (not (bufferp buffer)))
180 (error "cannot access response buffer"))
181 (with-current-buffer buffer
182 (goto-char (point-min))
183 (when (not (re-search-forward "HTTP/1.1 200 OK" (line-end-position) t))
184 (error "not a valid HTTP response from server"))
185 buffer))
186
187
188 ;;; ------------------------------------------------------------------
189 ;;; Call
190 ;;; ------------------------------------------------------------------
191 (defun emms-idapi-musicbrainz-call (search-term-alist)
192 "Make a call into search service based on SEARCH-TERM-ALIST."
193 (let (buffer
194 ;; Robert Kaye from MusicBrainz said on the 21st of February,
195 ;; 2024, that this format of user agent string is good.
196 (url-user-agent (concat "Emacs_Emms/"
197 emms-version
198 " (https://www.gnu.org/software/emms/)"))
199 (url-string (emms-idapi-musicbrainz-make-search-string
200 search-term-alist)))
201 (setq buffer (url-retrieve-synchronously
202 url-string
203 t t
204 emms-idapi-musicbrainz-url-timeout-seconds))
205 (if (not buffer)
206 (error "call to musicbrainz API timeout or returned nothing")
207 (with-current-buffer buffer
208 (setq emms-idapi-query-local search-term-alist))
209 (setq emms-idapi-musicbrainz-url-buffer buffer))))
210
211 (defun emms-idapi-musicbrainz-get-search-string (field)
212 "Return the search string associated with FIELD."
213 (let ((string (alist-get field emms-idapi-musicbrainz-search-string-map)))
214 (when (not string)
215 (error "no associated string for \"%s\" field" field))
216 string))
217
218 (defun emms-idapi-musicbrainz-make-search-string (term-alist)
219 (let ((artist (or (alist-get 'info-artist term-alist)
220 (alist-get 'info-albumartist term-alist)))
221 (release (alist-get 'info-album term-alist))
222 (title (alist-get 'info-title term-alist))
223 ;; (reid (alist-get 'reid term-alist))
224 ;; (arid (alist-get 'arid term-alist))
225 )
226 (concat emms-idapi-musicbrainz-root-url
227
228 (cond ((and title
229 artist)
230 (format "recording/?query=recording:%s%sartist:%s"
231 (url-encode-url (concat "\"" title "\""))
232 (url-encode-url " AND ")
233 (url-encode-url (concat "\"" artist "\""))))
234
235 ((and artist
236 (not release)
237 (not title))
238 (format "artist/?query=%s" (url-encode-url (concat "\"" artist "\""))))
239
240 ;; Will only work if the browser supplies a meaningful musicbrainz ARID
241 ;;
242 ;; (release
243 ;; (format "release/?query=release:%s%s%s"
244 ;; (url-encode-url (concat "\"" release "\""))
245 ;; (if artist (url-encode-url (concat " AND artist:\"" artist "\"")) "")
246 ;; (if arid (concat (url-encode-url " AND ") "arid:" arid) "")))
247
248 (t (error "unhandled field %s" term-alist)))
249 (format "&limit=%d&fmt=json" emms-idapi-musicbrainz-response-limit))))
250
251
252 ;;; ------------------------------------------------------------------
253 ;;; Interface
254 ;;; ------------------------------------------------------------------
255 (defun emms-idapi-musicbrainz-search (search-term-alist)
256 "IDAPI interface function for searching MusicBrainz service.
257
258 ARID limits the search to a MusicBrainz artist id."
259 (when (not (listp search-term-alist))
260 (error "%s is not a list" search-term-alist))
261 (emms-idapi-musicbrainz-process-type-dispatch
262 (emms-idapi-musicbrainz-process-json
263 (emms-idapi-musicbrainz-process
264 (emms-idapi-musicbrainz-call search-term-alist)))))
265
266
267 (provide 'emms-idapi-musicbrainz)
268
269 ;;; emms-idapi-musicbrainz.el ends here