;; Author: nik gaffney <nik@fo.am>
;; Created: 2023-05-05
;; Version: 0.1
-;; Package-Requires: ((emacs "27.1") (request "0.3"))
+;; Package-Requires: ((emacs "28.1") (request "0.3"))
;; Keywords: music, scrobbling, multimedia
;; URL: https://github.com/zzkt/metabrainz
"recording" "release" "release-group" "series" "work" "url")
"API resources for linked entites in the MusicBrainz database.")
+(defconst musicbrainz-search-types
+ (list "annotation" "area" "artist" "cdstub" "event" "instrument"
+ "label" "place" "recording" "release" "release-group"
+ "series" "tag" "work" "url")
+ "Valid TYPE parameters for MusicBrainz searches.")
+
+
+;; entity checks
+
+(defun musicbrainz-core-entity-p (entity)
+ "Check if ENTITY is a core entity."
+ (if (member entity musicbrainz-entities-core) t nil))
+
+(defun musicbrainz-non-core-entity-p (entity)
+ "Check if ENTITY is a non-core entity."
+ (if (member entity musicbrainz-entities-non-core) t nil))
+
+(defun musicbrainz-uid-entity-p (entity)
+ "Check if ENTITY is a unique identifier entity."
+ (if (member entity musicbrainz-entities-uids) t nil))
+
+(defun musicbrainz-search-type-p (type)
+ "Check if TYPE is a valid search type."
+ (if (member type musicbrainz-search-types) t nil))
+
;; Linked entities
(defun musicbrainz-mbid-p (mbid)
"Check (permissive) if MBID is valid and/or well formatted.
An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz.org/doc/MusicBrainz_Identifier for details."
- (if (and (length= mbid 36)
+
+ (if (and (length= mbid 36) ;; length= requires emacs > 28.1
(string-match-p
(rx (repeat 8 hex) ;; [A-F0-9]{8}
"-" (repeat 4 hex) ;; -[A-F0-9]{4}
t nil))
+(defun musicbrainz-format (response)
+ "Format a generic RESPONSE."
+ (format "%s" (pp response)))
+
+
;;; ;; ;; ; ; ; ; ; ;
;;
;; Search API
;; https://musicbrainz.org/doc/MusicBrainz_API/Search
;;
+;; The MusicBrainz API search requests provide a way to search for MusicBrainz
+;; entities based on different sorts of queries and are provided by a search
+;; server built using Lucene technology.
+;;
+;; Parameters common to all resources
+;;
+;; type Selects the entity index to be searched: annotation, area, artist,
+;; cdstub, event, instrument, label, place, recording, release,
+;; release-group, series, tag, work, url
+;;
+;; query Lucene search query. This is mandatory
+;;
+;; limit An integer value defining how many entries should be returned.
+;; Only values between 1 and 100 (both inclusive) are allowed.
+;; If not given, this defaults to 25.
+;;
+;; offset Return search results starting at a given offset.
+;; Used for paging through more than one page of results.
+;;
+;; dismax If set to "true", switches the Solr query parser from edismax to dismax,
+;; which will escape certain special query syntax characters by default
+;; for ease of use. This is equivalent to switching from the "Indexed search
+;; with advanced query syntax" method to the plain "Indexed search" method
+;; on the website. Defaults to "false".
+;;
;; ;; ; ; ;
-
;;;###autoload
-(defun musicbrainz-search (entity query &optional limit)
- "Search the MusicBrainz database for ENTITY matching QUERY.
-Optionally return only LIMIT number of results.
+(defun musicbrainz-search (type query &optional limit offset)
+ "Search the MusicBrainz database for TYPE matching QUERY.
+Optionally return only LIMIT number of results from OFFSET.
The QUERY field supports the full Lucene Search syntax, some details
can be found near https://musicbrainz.org/doc/MusicBrainz_API/Search
or in the Lucene docs."
- (message "musicbrainz: searching %s=%s" entity query)
+ (message "musicbrainz: searching %s=%s" type query)
(let* ((max (if limit limit 1))
+ (from (if offset offset ""))
(response
- (request-response-data
- (request
- (url-encode-url
- (format "%s/%s?query=%s&fmt=json&limit=%s"
- musicbrainz-api-url entity query max))
- :type "GET"
- :parser 'json-read
- :sync t
- :success (cl-function
- (lambda (&key data &allow-other-keys)
- (if (eq t (assoc-default 'valid data))
- (message "Token is valid for user: %s"
- (assoc-default 'user_name data))
- (message "Not a valid user token"))))))))
+ (request-response-data
+ (request
+ (url-encode-url
+ (format "%s/%s?query=%s&fmt=json&limit=%s&offset=%s"
+ musicbrainz-api-url type query max from))
+ :type "GET"
+ :parser 'json-read
+ :sync t
+ :success (cl-function
+ (lambda (&key data &allow-other-keys)
+ (message "ok")))))))
response))
+
+;;;###autoload
+(defun musicbrainz-find (query &rest extras)
+ "Search the MusicBrainz database for QUERY or recommend a more specific search.
+MusicBrainz makes a distinction between `search' and `browse' this a more general
+entry point to searching/browsing the database.
+
+Heuristics.
+- if QUERY is an MBID, check artist, recording, etc
+- if QUERY is text, search for artists or recordings, etc"
+
+ (message "musicbrainz: finding: %s" query)
+ (if (musicbrainz-mbid-p query)
+ ;; search (lookup) for things that could have an mbid
+ (let ((mbid query))
+ (message "searching mbid: %s" mbid))
+ ;; search (query) for other things
+ (progn
+ (message "searching other: %s" mbid)
+ ;; (message "searching artist: %s" query)
+ ;; (musicbrainz-format (musicbrainz-search "artist" query))
+ ;; (message "searching label: %s" query)
+ ;; (musicbrainz-format (musicbrainz-search "label" query))
+ ;; (message "searching release: %s" query)
+ ;; (musicbrainz-format (musicbrainz-search "release" query))
+ )))
+
+
+
;; various specific searches
;;;###autoload
Optionally return LIMIT number of results."
(let ((data (musicbrainz-search "artist" artist limit)))
(let-alist
- data
- (seq-map
- (lambda (i)
- (let-alist i
- (if (not limit)
- (format "%s | %s |\n" .name .id)
- (format "%s | %s | %s |\n"
- .score .name .id))))
- .artists))))
+ data
+ (seq-map
+ (lambda (i)
+ (let-alist i
+ (if (not limit)
+ (format "%s | %s |\n" .name .id)
+ (format "%s | %s | %s |\n"
+ .score .name .id))))
+ .artists))))
;;;###autoload
(let ((data (musicbrainz-search "artist" artist)))
(let-alist data
(car (remove nil (seq-map
- (lambda (i)
- (let-alist i
- (when (= 100 .score)
- (format "%s" .id))))
- .artists))))))
+ (lambda (i)
+ (let-alist i
+ (when (= 100 .score)
+ (format "%s" .id))))
+ .artists))))))
;;;###autoload
(defun musicbrainz-disambiguate-artist (artist &optional limit)
"More ARTIST data. less ambiguity (with optional LIMIT).
Outputs an `org-mode' table with descriptions and MBID link to artists pages."
- (let ((data (musicbrainz-search "artist" artist limit)))
+ (let* ((max (if limit limit 11))
+ (data (musicbrainz-search "artist" artist max)))
(let-alist data
(cons (format "| Artist: %s| MBID |\n" artist)
- (seq-map
- (lambda (i)
- (let-alist i
- (format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
- .score .name .disambiguation .id .id)))
- .artists)))))
+ (seq-map
+ (lambda (i)
+ (let-alist i
+ (format "%s | %s, %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
+ .score .name .disambiguation .id .id)))
+ .artists)))))
;;;###autoload
Optionally return LIMIT number of results."
(let ((data (musicbrainz-search "label" label limit)))
(let-alist
- data
- (seq-map
- (lambda (i)
- (let-alist i
- (if (not limit)
- (format "%s | %s |\n" .name .id)
- (format "%s | %s | %s (%s%s) | %s |\n"
- .score .name
- (if .disambiguation .disambiguation "")
- (if .life-span.begin
- (format "%s " .life-span.begin) "")
- (if .life-span.end
- (format "—%s" .life-span.end)
- "ongoing")
- .id))))
- .labels))))
+ data
+ (seq-map
+ (lambda (i)
+ (let-alist i
+ (if (not limit)
+ (format "%s | [[https://musicbrainz.org/label/%s][%s]] |\n" .name .id .id)
+ (format "%s | %s | %s (%s%s) | [[https://musicbrainz.org/label/%s][%s]] |\n"
+ .score .name
+ (if .disambiguation .disambiguation "")
+ (if .life-span.begin
+ (format "%s " .life-span.begin) "")
+ (if .life-span.end
+ (format "—%s" .life-span.end)
+ "ongoing")
+ .id .id))))
+ .labels))))
+
+
+;;;###autoload
+(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))))
+
+
+(defun musicbrainz--unwrap-0 (entity)
+ "Unwrap (fragile) .artist-credit ENTITY -> .name more or less."
+ (format "%s" (cdar (aref entity 0))))
+
+;;;###autoload
+(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))))
+
+
+;;; ;; ;; ; ; ; ; ; ;
+;;
+;; Lookups
+;; https://musicbrainz.org/doc/MusicBrainz_API#Lookups
+;;
+;;; ;; ;; ; ;
+
+;;;###autoload
+(defun musicbrainz-lookup (entity mbid &optional inc)
+ "Search (lookup not browse) the MusicBrainz database for ENTITY with MBID.
+Optionally add an INC list.
+
+Subqueries
+ /ws/2/area
+ /ws/2/artist recordings, releases, release-groups, works
+ /ws/2/collection user-collections (includes private collections, requires authentication)
+ /ws/2/event
+ /ws/2/genre
+ /ws/2/instrument
+ /ws/2/label releases
+ /ws/2/place
+ /ws/2/recording artists, releases, isrcs, url-rels
+ /ws/2/release artists, collections, labels, recordings, release-groups
+ /ws/2/release-group artists, releases
+ /ws/2/series
+ /ws/2/work
+ /ws/2/url"
+
+ (message "musicbrainz: lookup: %s/%s" entity mbid)
+ (if (and (musicbrainz-core-entity-p entity)
+ (musicbrainz-mbid-p mbid))
+ (let* ((add (if inc inc ""))
+ (response
+ (request-response-data
+ (request
+ (url-encode-url
+ (format "%s/%s/%s?inc=%s&fmt=json"
+ musicbrainz-api-url entity mbid add))
+ :type "GET"
+ :parser 'json-read
+ :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)))
+
+
+;; specific MBID subrequests (limited to 25 results?)
+
+(defun musicbrainz-lookup-artist (mbid)
+ "MusicBrainz lookup for artist with MBID."
+ (let ((response
+ (musicbrainz-lookup "artist" mbid)))
+ (let-alist response
+ (format "| %s | %s | %s | [[https://musicbrainz.org/artist/%s][%s]] |\n"
+ .name .disambiguation .type .id .id))))
+
+
+(defun musicbrainz-lookup-release (mbid)
+ "MusicBrainz lookup for release with MBID."
+ (let ((response
+ (musicbrainz-lookup "release" mbid)))
+ (let-alist response
+ (format "| %s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
+ .date .title .packaging .id .id))))
+
+(defun musicbrainz-lookup-recording (mbid)
+ "MusicBrainz lookup for recording with MBID."
+ (let ((response
+ (musicbrainz-lookup "recording" mbid)))
+ (let-alist response
+ (format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
+ .title .id .id))))
+
+
+(defun musicbrainz-lookup-artist-releases (mbid)
+ "MusicBrainz lookup for releases from artist with MBID."
+ (let ((response
+ (musicbrainz-lookup "artist" mbid "releases")))
+ (let-alist response
+ (seq-map
+ (lambda (i)
+ (let-alist i
+ (format "%s | %s | %s | [[https://musicbrainz.org/release/%s][%s]] |\n"
+ .date .title .packaging .id .id)))
+ .releases))))
+
+
+(defun musicbrainz-lookup-artist-recordings (mbid)
+ "MusicBrainz lookup for recordings from artist with MBID."
+ (let ((response
+ (musicbrainz-lookup "artist" mbid "recordings")))
+ (let-alist response
+ (seq-map
+ (lambda (i)
+ (let-alist i
+ (format "%s | [[https://musicbrainz.org/recording/%s][%s]] |\n"
+ .title .id .id)))
+ .recordings))))
;;;###autoload
-(defun musicbrainz-browse (entity link lookup &optional type)
- "Search the MusicBrainz database for ENTITY with LINK matching LOOKUP.
+(defun musicbrainz-browse (entity link query &optional type)
+ "Search the MusicBrainz database for ENTITY with LINK matching QUERY.
Optionally limit the search to TYPE results for ENTITY."
(message "musicbrainz: browsing %s linked to %s" entity link)
- (message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link lookup type)
+ (message "url: %s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type)
(let ((response
(request-response-data
(request
(url-encode-url
- (format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link lookup type))
+ (format "%s/%s?%s=%s&type=%s&fmt=json" musicbrainz-api-url entity link query type))
:type "GET"
:parser 'json-read
:sync t