X-Git-Url: http://git.vanrenterghem.biz/musicbrainz.git/blobdiff_plain/6a27268134e2e51391fadffefef2baae8988dd87..986690a515e67526598eaa4200bd383f03a007bd:/musicbrainz.el diff --git a/musicbrainz.el b/musicbrainz.el index 068cbb7..e49f6ae 100644 --- a/musicbrainz.el +++ b/musicbrainz.el @@ -27,21 +27,23 @@ ;;; 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) ;;; ;; ;; ; ; ; ; ; ; ;; @@ -50,11 +52,17 @@ ;;; ; ;; ;; (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 @@ -113,6 +121,12 @@ As seen in https://wiki.musicbrainz.org/MusicBrainz_API/Rate_Limiting" "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 @@ -175,6 +189,10 @@ An MBID is a 36 character Universally Unique Identifier, see https://musicbrainz 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." @@ -227,7 +245,9 @@ 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." + (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 @@ -240,10 +260,12 @@ or in the Lucene docs." :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 @@ -257,19 +279,66 @@ Heuristics. - 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." @@ -314,6 +383,19 @@ Outputs an `org-mode' table with descriptions and MBID link to artists pages." .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." @@ -337,32 +419,47 @@ 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) ;;; ;; ;; ; ; ; ; ; ; @@ -393,6 +490,7 @@ Subqueries /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)) @@ -408,10 +506,20 @@ Subqueries :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?) @@ -421,8 +529,10 @@ Subqueries 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 @@ -434,8 +544,10 @@ See listenbrainz--deformatter for details." 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 @@ -470,12 +582,12 @@ See listenbrainz--deformatter for details." ;; 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 @@ -486,19 +598,19 @@ See listenbrainz--deformatter for details." ;; 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) @@ -509,69 +621,69 @@ See listenbrainz--deformatter for details." ;; 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) @@ -596,7 +708,7 @@ See listenbrainz--deformatter for details." ;; 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 @@ -609,17 +721,17 @@ See listenbrainz--deformatter for details." ;; 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)) @@ -668,11 +780,84 @@ Optionally limit the search to TYPE results for ENTITY." :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)