;;; Commentary:
-;; - basic MusicBrainz interface
-;; - partial & incomplete
-;; - no error checks
-;; - sync -> async
-
+;; An interface to the MusicBrainz "open music encyclopedia" collection
+;; of music metadata. The main entry points are `musicbrainz-search' for
+;; general searches and `musicbrainz-lookup' for the more specific.
+;; There are also some narrower searches such as `musicbrainz-search-artist'
+;;
+;; Naming follows the MusicBrainz API reasonably closely, so the official API
+;; documentation can provide insight into how searching, browsing and lookups
+;; are structured. MusicBrainz has it's particular taxonomy and quirks, so
+;; some familiarity may be required to get useful results in some cases.
+;;
+;; https://musicbrainz.org/doc/MusicBrainz_API
;;; Code:
(require 'request)
(require 'json)
-
-;; debug level for http requests
-(setq request-log-level 'warn
- request-message-level 'warn)
-
+(require 'pp)
;;; ;; ;; ; ; ; ; ; ;
;;
;;; ; ;; ;;
(defcustom musicbrainz-api-url "https://musicbrainz.org/ws/2"
- "URL for musicbrainz API.
+ "URL for MusicBrainz API.
Documentation available at https://musicbrainz.org/doc/MusicBrainz_API"
:type 'string
:group 'musicbrainz)
+(defcustom musicbrainz-coverart-api-url "http://coverartarchive.org"
+ "URL for MusicBrainz Cover Art Archive API.
+Documentation available at https://musicbrainz.org/doc/Cover_Art_Archive/API"
+ :type 'string
+ :group 'musicbrainz)
+
(defcustom musicbrainz-api-token ""
"An auth token is required for some functions."
:type 'string
"series" "tag" "work" "url")
"Valid TYPE parameters for MusicBrainz searches.")
+(defconst musicbrainz-relationships
+ (list "area-rels" "artist-rels" "event-rels" "instrument-rels"
+ "label-rels" "place-rels" "recording-rels" "release-rels"
+ "release-group-rels" "series-rels" "url-rels" "work-rels")
+ "Valid relationships for lookups.")
+
;; entity checks
mbid))
t nil))
+;; https://lucene.apache.org/core/4_3_0/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters
+(defconst musicbrainz-qeury-special-chars
+ (list "+" "-" "&" "|" "!" "(" ")" "{" "}" "[" "]" "^" "\"" "~" "*" "?" ":" "\\" "/"))
+
(defun musicbrainz-format (response)
"Format a generic RESPONSE."
can be found near https://musicbrainz.org/doc/MusicBrainz_API/Search
or in the Lucene docs."
+ (interactive "sMusicBrainz search type: \nsMusicBrainz search query: ")
(message "MusicBrainz: searching %s=%s" type query)
+ ;; queries may need to be escaped
(let* ((max (if limit limit 1))
(from (if offset offset ""))
(response
:headers (list `("User-Agent" . ,musicbrainz-user-agent))
:parser 'json-read
:sync t
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (message "ok")))))))
- response))
+ :sucess (cl-function
+ (lambda (&key data &allow-other-keys)
+ (message "ok: %s" data)))))))
+ (if (called-interactively-p 'any)
+ (message "%s" (pp response))
+ response)))
;;;###autoload
- if QUERY is an MBID, check artist, recording, etc
- if QUERY is text, search for artists or recordings, etc"
- (message "MusicBrainz: query %s" query)
+ (message "MusicBrainz: query %s %s" query (if extras extras ""))
(if (musicbrainz-mbid-p query)
;; search (lookup) for things that could have an mbid
(let ((mbid query))
- (message "searching mbid: %s" mbid))
- ;; search (search/browse/query) for other things
- (progn
- (message "searching other: %s" mbid)
- )))
+ (message "searching mbid: %s" mbid)
+ ;; search (search/browse/query) for other things
+ (progn
+ (message "searching other: %s" mbid)))))
+
+
+;; generate search functions
+(defmacro musicbrainz--defsearch-1 (name format-string format-args)
+ "Generate search function to format a single item.
+NAME FORMAT-STRING FORMAT-ARGS
+See listenbrainz--deformatter for details."
+ (let ((f (intern (concat "musicbrainz-search-" name)))
+ (doc (format "Search for %s using QUERY and show matches.
+Optionally return LIMIT number of results." name)))
+ `(defun ,f (query &optional limit) ,doc
+ (let* ((max (if limit limit 1))
+ (response
+ (musicbrainz-search ,name query max)))
+ (let-alist response
+ (format ,format-string ,@format-args))))))
+
+
+(defmacro musicbrainz--defsearch-2 (name format-string format-args alist)
+ "Generate lookup function to format multiple items.
+NAME FORMAT-STRING FORMAT-ARGS ALIST
+See listenbrainz--deformatter for details."
+ (let ((f (intern (concat "musicbrainz-search-" name)))
+ (doc (format "Search for %s using QUERY and show matches.
+Optionally return LIMIT number of results." name)))
+ `(defun ,f (query &optional limit) ,doc
+ (let* ((max (if limit limit 1))
+ (response
+ (musicbrainz-search ,name query max)))
+ (let-alist response
+ (seq-map
+ (lambda (i)
+ (let-alist i
+ (format ,format-string ,@format-args)))
+ ,alist))))))
;; various specific searches
+;; search -> musicbrainz-search-annotation
+(musicbrainz--defsearch-2 "annotation"
+ "%s | %s | %s | %s | [[https://musicbrainz.org/%s/%s][%s]] |\n"
+ (.score .type .name .text .type .entity .entity)
+ .annotations)
+
+;; search -> musicbrainz-search-area
+(musicbrainz--defsearch-2 "area"
+ "%s | [[https://musicbrainz.org/area/%s][%s]] |\n"
+ (.name .id .id)
+ .areas)
+
+
(defun musicbrainz-search-artist (artist &optional limit)
"Search for an ARTIST and show matches.
Optionally return LIMIT number of results."
.artists)))))
+;; search -> musicbrainz-search-event
+(musicbrainz--defsearch-2 "event"
+ "%s | [[https://musicbrainz.org/event/%s][%s]] |\n"
+ (.name .id .id)
+ .events)
+
+;; search -> musicbrainz-search-instrument
+(musicbrainz--defsearch-2 "instrument"
+ "| %s | %s | [[https://musicbrainz.org/instrument/%s][%s]] |\n"
+ (.name .type .id .id)
+ .instruments)
+
+
(defun musicbrainz-search-label (label &optional limit)
"Search for a LABEL and show matches.
Optionally return LIMIT number of results."
.labels))))
-(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))))
+;; search -> musicbrainz-search-place
+(musicbrainz--defsearch-2 "place"
+ "%s | [[https://musicbrainz.org/place/%s][%s]] |\n"
+ (.name .id .id)
+ .places)
+;; search -> musicbrainz-search-recording
+(musicbrainz--defsearch-2 "recording"
+ "%s | %s, %s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
+ (.score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)
+ .recordings)
-(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))))
+;; search -> musicbrainz-search-release
+(musicbrainz--defsearch-2 "release"
+ "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
+ (.score .title (musicbrainz--unwrap-0 .artist-credit) .id .id)
+ .releases)
+
+;; search -> musicbrainz-search-release-group
+(musicbrainz--defsearch-2 "release-group"
+ "%s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
+ (.first-release-date .title .primary-type .id .id)
+ .release-groups)
+
+;; search -> musicbrainz-search-series
+(musicbrainz--defsearch-2 "series"
+ "%s | [[https://musicbrainz.org/series/%s][%s]] |\n"
+ (.name .id .id)
+ .series)
+
+;; search -> musicbrainz-search-work
+(musicbrainz--defsearch-2 "work"
+ "%s | %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
+ (.score .title .id .id)
+ .works)
+
+;; search -> musicbrainz-search-url
+(musicbrainz--defsearch-2 "url"
+ "%s | [[%s][%s]] | [[https://musicbrainz.org/url/%s][%s]] |\n"
+ (.score .resource .resource .id .id)
+ .urls)
;;; ;; ;; ; ; ; ; ; ;
/ws/2/work
/ws/2/url"
+ (interactive "sMusicBrainz entity type: \nsMusicBrainz MBID for entity: ")
(message "MusicBrainz: lookup: %s/%s" entity mbid)
(if (and (musicbrainz-core-entity-p entity)
(musicbrainz-mbid-p mbid))
: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)))
+ (when data
+ (message "%s data: %s" entity mbid))))))))
+ (if (called-interactively-p 'any)
+ (message "%s" (pp response))
+ response))
+ (user-error "MusicBrainz: search requires a valid MBID and entity (i.e. one of %s)"
+ musicbrainz-entities-core)))
+
+;; relationship lookups
+
+(defun musicbrainz-relations (entity relation mbid)
+ "Lookup relationships of type RELATION to ENTITY with MBID."
+ ;; no sanity and/or error checks
+ (musicbrainz-lookup entity mbid (format "%s-rels" relation)))
;; specific MBID lookup requests & subrequests (limited to 25 results?)
NAME FORMAT-STRING FORMAT-ARGS
See listenbrainz--deformatter for details."
(let ((f (intern (concat "musicbrainz-lookup-" name)))
- (doc "MusicBrainz lookup."))
+ (doc "MusicBrainz lookup.")
+ (prompt (format "sMusicBrainz lookup %s by MBID: " name)))
`(defun ,f (mbid) ,doc
+ (interactive ,prompt)
(let ((response
(musicbrainz-lookup ,name mbid)))
(let-alist response
QUERY SUBQUERY FORMAT-STRING FORMAT-ARGS ALIST
See listenbrainz--deformatter for details."
(let ((f (intern (format "musicbrainz-lookup-%s-%s" query subquery)))
- (doc "MusicBrainz lookup."))
+ (doc "MusicBrainz lookup.")
+ (prompt (format "sMusicBrainz lookup %s %s by MBID: " query subquery)))
`(defun ,f (mbid) ,doc
+ (interactive ,prompt)
(let ((response
(musicbrainz-lookup ,query mbid ,subquery)))
(let-alist response
;; lookup -> musicbrainz-lookup-area
(musicbrainz--deflookup-1 "area"
- "| %s | [[https://musicbrainz.org/area/%s][%s]] |\n"
+ "| %s | [[https://musicbrainz.org/area/%s][%s]] |\n"
(.name .id .id))
;; lookup -> musicbrainz-lookup-artist
(musicbrainz--deflookup-1 "artist"
- "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
+ "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
(.name .disambiguation .type .id .id))
;; lookup -> musicbrainz-lookup-artist-recordings
;; lookup -> musicbrainz-lookup-artist-releases
(musicbrainz--deflookup-2 "artist" "releases"
- "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
+ "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
(.date .title .packaging .id .id)
.releases)
;; lookup -> musicbrainz-lookup-artist-release-groups
(musicbrainz--deflookup-2 "artist" "release-groups"
- "%s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
+ "%s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
(.first-release-date .title .primary-type .id .id)
.release-groups)
;; lookup -> musicbrainz-lookup-artist-works
(musicbrainz--deflookup-2 "artist" "works"
- " %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
+ " %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
(.title .id .id)
.works)
;; lookup -> musicbrainz-lookup-collection-user-collections (requires authentication)
(musicbrainz--deflookup-2 "collection" "user-collections"
- " %s | [[https://musicbrainz.org/collection/%s][%s]] |\n"
+ " %s | [[https://musicbrainz.org/collection/%s][%s]] |\n"
(.name .id .id)
.collection)
;; lookup -> musicbrainz-lookup-event
(musicbrainz--deflookup-1 "event"
- "| %s | [[https://musicbrainz.org/event/%s][%s]] |\n"
+ "| %s | [[https://musicbrainz.org/event/%s][%s]] |\n"
(.name .id .id))
;; lookup -> musicbrainz-lookup-genre
(musicbrainz--deflookup-1 "genre"
- "| %s | [[https://musicbrainz.org/genre/%s][%s]] |\n"
+ "| %s | [[https://musicbrainz.org/genre/%s][%s]] |\n"
(.name .id .id))
;; lookup -> musicbrainz-lookup-instrument
(musicbrainz--deflookup-1 "instrument"
- "| %s | %s | [[https://musicbrainz.org/instrument/%s][%s]] |\n"
+ "| %s | %s | [[https://musicbrainz.org/instrument/%s][%s]] |\n"
(.name .type .id .id))
;; lookup -> musicbrainz-lookup-label
(musicbrainz--deflookup-1 "label"
- "| %s | %s | [[https://musicbrainz.org/label/%s][%s]] |\n"
+ "| %s | %s | [[https://musicbrainz.org/label/%s][%s]] |\n"
(.name .disambiguation .id .id))
;; lookup -> musicbrainz-lookup-label-releases
(musicbrainz--deflookup-2 "label" "releases"
- "%s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
+ "%s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
(.date .title .id .id)
.releases)
;; lookup -> musicbrainz-lookup-place
(musicbrainz--deflookup-1 "place"
- "| %s | [[https://musicbrainz.org/place/%s][%s]] |\n"
+ "| %s | [[https://musicbrainz.org/place/%s][%s]] |\n"
(.name .id .id))
;; lookup -> musicbrainz-lookup-recording
(musicbrainz--deflookup-1 "recording"
- "| %s | %s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
+ "| %s | %s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
(.first-release-date .title .id .id))
;; lookup -> musicbrainz-lookup-recording-artists
(musicbrainz--deflookup-2 "recording" "artists"
- "%s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
+ "%s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
(.artist.name .artist.id .artist.id)
.artist-credit)
;; lookup -> musicbrainz-lookup-recording-releases
(musicbrainz--deflookup-2 "recording" "releases"
- "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
+ "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
(.date .title .packaging .id .id)
.releases)
;; lookup -> musicbrainz-lookup-recording-isrcs
(musicbrainz--deflookup-2 "recording" "isrcs"
- "%s | [[https://musicbrainz.org/isrc/%s][%s]] |\n"
+ "%s | [[https://musicbrainz.org/isrc/%s][%s]] |\n"
(.name .id .id)
.isrcs)
;; lookup -> musicbrainz-lookup-recording-url-rels
(musicbrainz--deflookup-2 "recording" "url-rels"
- "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
+ "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
(.name .id .id)
.relations)
;; lookup -> musicbrainz-lookup-release-group
(musicbrainz--deflookup-1 "release-group"
- "| %s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
+ "| %s | %s | %s | [[https://musicbrainz.org/release-group/%s][%s]] |\n"
(.first-release-date .title .primary-type .id .id))
;; lookup -> musicbrainz-lookup-release-group-artists
;; lookup -> musicbrainz-lookup-series
(musicbrainz--deflookup-1 "series"
- "| %s | [[https://musicbrainz.org/series/%s][%s]] |\n"
- (.name .id .id))
+ "| %s | [[https://musicbrainz.org/series/%s][%s]] |\n"
+ (.title .id .id))
;; lookup -> musicbrainz-lookup-work
(musicbrainz--deflookup-1 "work"
- "| %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
- (.name .id .id))
+ "| %s | [[https://musicbrainz.org/work/%s][%s]] |\n"
+ (.title .id .id))
;; lookup -> musicbrainz-lookup-url
(musicbrainz--deflookup-1 "url"
- "| %s | [[https://musicbrainz.org/url/%s][%s]] |\n"
+ "| %s | [[https://musicbrainz.org/url/%s][%s]] |\n"
(.name .id .id))
:sync t
:success (cl-function
(lambda (&key data &allow-other-keys)
- (message "ok")))))))
+ (message "ok: %s" (if data data ""))))))))
+ response))
+
+
+
+;;;;;; ; ; ;; ; ; ; ; ; ;; ;
+;;
+;; Cover Art Archive API
+;; https://musicbrainz.org/doc/Cover_Art_Archive/API
+;;
+;;;; ; ; ; ; ;
+
+;; /release/{mbid}/
+;; /release/{mbid}/front
+;; /release/{mbid}/back
+;; /release/{mbid}/{id}
+;; /release/{mbid}/({id}|front|back)-(250|500|1200)
+;;
+;; /release-group/{mbid}/
+;; /release-group/{mbid}/front[-(250|500|1200)]
+
+;;;###autoload
+(defun musicbrainz-coverart (mbid &optional release-group)
+ "Search MusicBrainz Cover Art Archive for release MBID.
+When RELEASE-GROUP is non-nil MBID is for a release group, rather than release."
+ (message "MusicBrainz: cover art for %s" mbid)
+ (message "url: %s/release/%s" musicbrainz-coverart-api-url mbid)
+ (let ((response
+ (request-response-data
+ (request
+ (url-encode-url
+ (format "%s/release%s/%s"
+ musicbrainz-coverart-api-url
+ (if release-group "-group" "")
+ mbid))
+ :type "GET"
+ :header (list `("User-Agent" . ,musicbrainz-user-agent))
+ :parser 'json-read
+ :sync t
+ :success (cl-function
+ (lambda (&key data &allow-other-keys)
+ (message "ok: %s" (if data data ""))))))))
response))
+(defun musicbrainz-coverart-file-front (mbid)
+ "Get the MusicBrainz Cover Art front cover file for MBID."
+ (message "MusicBrainz: cover art (front) for %s" mbid)
+ (message "url: %s/release/%s/front" musicbrainz-coverart-api-url mbid)
+ (let ((response
+ (request-response-data
+ (request
+ (url-encode-url
+ (format "%s/release/%s/front" musicbrainz-coverart-api-url mbid))
+ :type "GET"
+ :header (list `("User-Agent" . ,musicbrainz-user-agent))
+ :sync t
+ :success (cl-function
+ (lambda (&key data &allow-other-keys)
+ (message "ok: %s" (if data data ""))))))))
+ response))
+
+(defun musicbrainz-coverart-file-back (mbid)
+ "Get the MusicBrainz Cover Art back cover file for MBID."
+ (message "MusicBrainz: cover art (back) for %s" mbid)
+ (message "url: %s/release/%s/back" musicbrainz-coverart-api-url mbid)
+ (let ((response
+ (request-response-data
+ (request
+ (url-encode-url
+ (format "%s/release/%s/back" musicbrainz-coverart-api-url mbid))
+ :type "GET"
+ :header (list `("User-Agent" . ,musicbrainz-user-agent))
+ :sync t
+ :success (cl-function
+ (lambda (&key data &allow-other-keys)
+ (message "ok: %s" (if data data ""))))))))
+ response))
-;;;
(provide 'musicbrainz)