(ns clj-lastfm.core (:import (java.net URI) (java.text SimpleDateFormat) (java.util TimeZone)) (:use [clj-lastfm.filecache] [clojure.contrib.core :only (-?> -?>>)] [clojure.contrib.def :only (defvar-)] [clojure.contrib.json :only (read-json)] [clojure.contrib.logging] [clojure.contrib.math :only (ceil)] [clojure.string :only (blank? trim)])) ;;;;;;;;;; Basic ;;;;;;;;;; (defvar- api-root-url ["http" "ws.audioscrobbler.com" "/2.0/"]) (defn- api-key [] (let [lastfm-api-key (resolve '*lastfm-api-key*)] (if (nil? lastfm-api-key) (throw (IllegalStateException. "lastfm API key is not set")) lastfm-api-key))) (defvar- guid-pattern (let [an "[0-9a-fA-F]"] (re-pattern (str "^" an "{8}-" an "{4}-" an "{4}-" an "{4}-" an "{12}$")))) (defn- mbid? [s] (if (re-matches guid-pattern s) true false)) (defvar- attr-kw (keyword "@attr")) (defvar- text-kw (keyword "#text")) (defn- safe-parse-int [n] (if (nil? n) nil (try (Integer/parseInt n) (catch NumberFormatException nfe nil)))) (defn- safe-parse-double [n] (if (nil? n) nil (try (Double/parseDouble n) (catch NumberFormatException nfe nil)))) (defn- str-1? [n] (-> n safe-parse-int (= 1))) (defn- struct? [obj] (instance? clojure.lang.PersistentStructMap obj)) (def #^{:private true :tag SimpleDateFormat} full-date-format (doto (SimpleDateFormat. "EEE, dd MMMM yyyy HH:mm:ss +0000") (.setTimeZone (TimeZone/getTimeZone "GMT")))) (def #^{:private true :tag SimpleDateFormat} date-format-wo-tz (doto (SimpleDateFormat. "EEE, dd MMMM yyyy HH:mm:ss") (.setTimeZone (TimeZone/getTimeZone "GMT")))) (def #^{:private true :tag SimpleDateFormat} short-date-format-wo-tz (doto (SimpleDateFormat. "dd MMM yyyy, HH:mm") (.setTimeZone (TimeZone/getTimeZone "GMT")))) (defn- parse-date [date-str] (if (some #(% date-str) [nil? blank?]) nil (let [clean-date-str (trim date-str)] (try (.parse full-date-format clean-date-str) (catch java.text.ParseException e (try (.parse date-format-wo-tz clean-date-str) (catch java.text.ParseException e (.parse short-date-format-wo-tz clean-date-str)))))))) (defn- remove-nil-values [m] (apply hash-map (apply concat (filter #(-> % fnext nil? not) m)))) (defn- lastfm-url [path] (.toString (URI. "http" "www.last.fm" path nil nil))) (defn- arg-types [& args] (->> args (map class) vec)) (defn- first-arg-type [& args] (->> args first class)) (defn- create-url-query [query-params] (apply str (interpose "&" (map #(str (name (first %)) "=" (second %)) (remove-nil-values query-params))))) (defn- create-url [query-params] (.toURL (#^URI apply #(URI. %1 %2 %3 %4 %5) (conj api-root-url (create-url-query (merge query-params {:api_key @(api-key) :format "json"})) nil)))) (defvar- default-cache (create-file-cache)) (defn- get-url ([url] (get-url url default-cache)) ([url cache] (do (info (str "URL: " url)) (get-file-content cache url)))) (defn- get-data [params] (let [url (create-url params) data (-> url get-url read-json)] (if (-> data :error nil?) data (throw (IllegalArgumentException. (-> data :message str)))))) (defn- create-get-obj-fn [fixed-params parse-fn] (fn [more-params] (parse-fn #(get-data (merge fixed-params more-params))))) (defn- create-paged-get-obj-fn [fixed-params parse-fn] (fn ([more-params] ((create-get-obj-fn fixed-params parse-fn) more-params)) ([more-params page] ((create-get-obj-fn fixed-params parse-fn) (assoc more-params :page page))))) (defn- create-parse-one-or-more-fn [parse-one-fn extractor-fn] (fn [data-fn] (lazy-seq (let [data (data-fn) one-or-more (extractor-fn data)] (do (debug (str "parsing: " data)) (if (map? one-or-more) (vector (parse-one-fn one-or-more)) (vec (map parse-one-fn one-or-more)))))))) (defn- create-parse-string-or-list-fn [obj-from-name-fn extractor-fn] (fn [data] (let [string-or-list (extractor-fn data)] (if (string? string-or-list) (vector (obj-from-name-fn string-or-list)) (vec (map obj-from-name-fn string-or-list)))))) (defn- create-paged-parse-fn [pages-fn page-fn perpage-fn param-fn parse-unpaged-fn get-fn] (fn [data-fn] (lazy-seq (let [data (data-fn) pages (-> data pages-fn safe-parse-int) page (-> data page-fn safe-parse-int) limit (-> data perpage-fn safe-parse-int) params (param-fn data)] (if (= page pages) (parse-unpaged-fn data-fn) (lazy-cat (parse-unpaged-fn data-fn) (get-fn (merge params {:page (inc page) :limit limit})))))))) (defn- create-paged-result-parse-fn [page-info-fn param-fn parse-unpaged-fn get-fn] (create-paged-parse-fn #(-> % page-info-fn :totalPages) #(-> % page-info-fn :page) #(-> % page-info-fn :perPage) param-fn parse-unpaged-fn get-fn)) (defn- create-paged-search-parse-fn [parse-unpaged-fn search-key get-fn] (create-paged-parse-fn #(-> (/ (-> % :results :opensearch:totalResults safe-parse-int) (-> % :results :opensearch:itemsPerPage safe-parse-double)) ceil int str) #(-> % :results :opensearch:Query :startPage) #(-> % :results :opensearch:itemsPerPage) #(hash-map search-key (-> % :results :opensearch:Query :searchTerms)) parse-unpaged-fn get-fn)) (defn- record-from-map [record-class no-of-fields field-map] (merge (eval `(new ~record-class ~@(repeat no-of-fields nil))) field-map)) ;;;;;;;;;; record defs ;;;;;;;;;; (defrecord Bio [published summary content]) (defrecord Location [latitude longitude street postalcode city country]) (defrecord Venue [id name location url website phonenumber]) (defrecord Event [id title artists headliner venue start description attendence reviews tag url website cancelled tags]) (defrecord Shout [body author date]) (defrecord Artist [name url mbid streamable listeners playcount bio]) (defrecord Album [name id url mbid artist listeners playcount release-date wiki]) (defrecord Track [name url mbid artist playcount listeners streamable]) (defrecord Tag [name url]) (defrecord User [name url realname]) ;;;;;;;;;; forward declaration ;;;;;;;;;; (declare artist-from-name tag-from-name user-from-name album-from-map user-from-map track-from-map tag-from-map) ;;;;;;;;;; Bio/Wiki ;;;;;;;;;; (defn- parse-bio [data] (do (debug (str "parse-bio: " data)) (Bio. (-> data :published parse-date) (data :summary) (data :content)))) ;;;;;;;;;; Location ;;;;;;;;;; (defn- parse-location [data] (do (debug (str "parse-location: " data)) (Location. (-> data :geo:point :geo:lat safe-parse-double) (-> data :geo:point :geo:long safe-parse-double) (data :street) (data :postalcode) (data :city) (data :country)))) ;;;;;;;;;; Venue ;;;;;;;;;; (defn- parse-venue [data] (do (debug (str "parse-venue: " data)) (Venue. (data :id) (data :name) (-> data :location parse-location) (data :url) (data :website) (data :phonenumber)))) ;;;;;;;;;; Event ;;;;;;;;;; (defvar- parse-event-artists (create-parse-string-or-list-fn #(artist-from-name %) #(-> % :artists :artist))) (defvar- parse-event-tags (create-parse-string-or-list-fn #(tag-from-name %) #(-> % :tags :tag))) (defn- parse-event [data] (do (debug (str "parse-event: " data)) (Event. (data :id) (data :title) (parse-event-artists data) (artist-from-name (-> data :artists :headliner)) (-> data :venue parse-venue) (-> data :startDate parse-date) (data :description) (-> data :attendence safe-parse-int) (-> data :reviews safe-parse-int) (data :tag) (data :url) (data :website) (-> data :cancelled str-1?) (parse-event-tags data)))) ;;;;;;;;;; Shout ;;;;;;;;;; (defn- parse-shout [data] (do (debug (str "parse-shout: " data)) (Shout. (data :body) (-> data :author user-from-name) (-> data :date parse-date)))) ;;;;;;;;;; Artist ;;;;;;;;;; (defn- parse-artist [data] (do (debug (str "parse-artist: " data)) (Artist. (data :name) (data :url) (data :mbid) (-> data :streamable str-1?) (-> data :stats :listeners safe-parse-int) (-> data :stats :playcount safe-parse-int) (-> data :bio parse-bio)))) (defvar- artist-from-map (partial record-from-map Artist 7)) (defn- artist-from-name [artist-name] (artist-from-map {:name artist-name})) ;;;;;;;;;; artist.getinfo ;;;;;;;;;; (defn- parse-artist-getinfo [data-fn] (let [data (data-fn)] (do (debug (str "parse-artist-getinfo: " data)) (-> data :artist parse-artist)))) (defvar- get-artist (create-get-obj-fn {:method "artist.getinfo"} parse-artist-getinfo)) (defmulti artist-info (fn [arg1 & _] (cond (instance? Artist arg1) :artist (mbid? arg1) :mbid :else :artist-name))) (defmethod artist-info :artist ([artist] (-> artist :name artist-info)) ([artist username] (-> artist :name (artist-info username))) ([artist username lang] (-> artist :name (artist-info username lang)))) (defmethod artist-info :artist-name ([artist-name] (artist-info artist-name nil nil)) ([artist-name username] (artist-info artist-name username nil)) ([artist-name username lang] (get-artist {:artist artist-name :username username :lang lang}))) (defmethod artist-info :mbid ([mbid] (artist-info mbid nil nil)) ([mbid username] (artist-info mbid username nil)) ([mbid username lang] (get-artist {:mbid mbid :username username :lang lang}))) ;;;;;;;;;; artist.getsimilar ;;;;;;;;;; (defn- parse-artist-similar-1 [data] (artist-from-map {:name (data :name) :url (data :url) :mbid (data :mbid) :streamable (-> data :streamable str-1?) :match (-> data :match safe-parse-double)})) (defvar- parse-artist-similar (create-parse-one-or-more-fn parse-artist-similar-1 #(-> % :similarartists :artist))) (defvar- get-artist-similar (create-get-obj-fn {:method "artist.getsimilar"} parse-artist-similar)) (defmulti artist-similar first-arg-type) (defmethod artist-similar Artist ([artist] (-> artist :name artist-similar)) ([artist limit] (-> artist :name (artist-similar limit)))) (defmethod artist-similar String ([artist-name] (get-artist-similar {:artist artist-name})) ([artist-name limit] (get-artist-similar {:artist artist-name :limit limit}))) ;;;;;;;;;; artist.gettoptags ;;;;;;;;;; (defvar- parse-artist-toptags (create-parse-one-or-more-fn #(Tag. (% :name) (% :url)) #(-> % :toptags :tag))) (defvar- get-artist-toptags (create-get-obj-fn {:method "artist.gettoptags"} parse-artist-toptags)) (defmulti artist-toptags first-arg-type) (defmethod artist-toptags Artist [artist] (-> artist :name artist-toptags)) (defmethod artist-toptags String [artist-name] (get-artist-toptags {:artist artist-name})) ;;;;;;;;;; artist.gettopalbums ;;;;;;;;;; (defn- parse-artist-topalbums-1 [data] (album-from-map {:name (data :name) :url (data :url) :mbid (data :mbid) :artist (artist-from-map {:name (-> data :artist :name) :url (-> data :artist :url) :mbid (-> data :artist :mbid)}) :playcount (-> data :playcount safe-parse-int) :rank (-> data attr-kw :rank safe-parse-int)})) (defvar- parse-artist-topalbums (create-parse-one-or-more-fn parse-artist-topalbums-1 #(-> % :topalbums :album))) (defvar- get-artist-topalbums (create-get-obj-fn {:method "artist.gettopalbums"} parse-artist-topalbums)) (defmulti artist-topalbums first-arg-type) (defmethod artist-topalbums Artist [artist] (-> artist :name artist-topalbums)) (defmethod artist-topalbums String [artist-name] (get-artist-topalbums {:artist artist-name})) ;;;;;;;;;; artist.gettopfans ;;;;;;;;;; (defn- parse-artist-topfans-1 [data] (user-from-map {:name (data :name) :url (data :url) :realname (data :realname) :weight (-> data :weight safe-parse-int)})) (defvar- parse-artist-topfans (create-parse-one-or-more-fn parse-artist-topfans-1 #(-> % :topfans :user))) (defvar- get-artist-topfans (create-get-obj-fn {:method "artist.gettopfans"} parse-artist-topfans)) (defmulti artist-topfans first-arg-type) (defmethod artist-topfans Artist [artist] (-> artist :name artist-topfans)) (defmethod artist-topfans String [artist-name] (get-artist-topfans {:artist artist-name})) ;;;;;;;;;; artist.gettoptracks ;;;;;;;;;; (defn- parse-artist-toptracks-1 [data] (track-from-map {:name (data :name) :url (data :url) :mbid (data :mbid) :artist (artist-from-map {:name (-> data :artist :name) :url (-> data :artist :url) :mbid (-> data :artist :mbid)}) :playcount (-> data :playcount safe-parse-int) :listeners (-> data :listeners safe-parse-int) :streamable (-> data :streamable text-kw str-1?) :streamable-full (-> data :streamable :fulltrack str-1?)})) (defvar- parse-artist-toptracks (create-parse-one-or-more-fn parse-artist-toptracks-1 #(-> % :toptracks :track))) (defvar- get-artist-toptracks (create-get-obj-fn {:method "artist.gettoptracks"} parse-artist-toptracks)) (defmulti artist-toptracks first-arg-type) (defmethod artist-toptracks Artist [artist] (-> artist :name artist-toptracks)) (defmethod artist-toptracks String [artist-name] (get-artist-toptracks {:artist artist-name})) ;;;;;;;;;; artist.getevents ;;;;;;;;;; (defvar- parse-artist-events (create-parse-one-or-more-fn parse-event #(-> % :events :event))) (defvar- get-artist-events (create-get-obj-fn {:method "artist.getevents"} parse-artist-events)) (defmulti artist-events first-arg-type) (defmethod artist-events Artist [artist] (-> artist :name artist-events)) (defmethod artist-events String [artist-name] (get-artist-events {:artist artist-name})) ;;;;;;;;;; artist.getpastevents ;;;;;;;;;; (declare get-artist-pastevents) (defvar- parse-artist-pastevents (create-paged-result-parse-fn #(-> % :events attr-kw) #(hash-map :artist (-> % :events attr-kw :artist)) parse-artist-events #(get-artist-pastevents %))) (defvar- get-artist-pastevents (create-paged-get-obj-fn {:method "artist.getpastevents"} parse-artist-pastevents)) (defmulti artist-pastevents first-arg-type) (defmethod artist-pastevents Artist ([artist] (-> artist :name artist-pastevents)) ([artist limit] (-> artist :name (artist-pastevents limit)))) (defmethod artist-pastevents String ([artist-name] (get-artist-pastevents {:artist artist-name})) ([artist-name limit] (get-artist-pastevents {:artist artist-name :limit limit}))) ;;;;;;;;;; artist.getshouts ;;;;;;;;;; (declare get-artist-shouts) (defvar- parse-artist-shouts-unpaged (create-parse-one-or-more-fn parse-shout #(-> % :shouts :shout))) (defvar- parse-artist-shouts (create-paged-result-parse-fn #(-> % :shouts attr-kw) #(hash-map :artist (-> % :shouts attr-kw :artist)) parse-artist-shouts-unpaged #(get-artist-shouts %))) (defvar- get-artist-shouts (create-paged-get-obj-fn {:method "artist.getshouts"} parse-artist-shouts)) (defmulti artist-shouts first-arg-type) (defmethod artist-shouts Artist ([artist] (-> artist :name artist-shouts)) ([artist limit] (-> artist :name (artist-shouts limit)))) (defmethod artist-shouts String ([artist-name] (get-artist-shouts {:artist artist-name})) ([artist-name limit] (get-artist-shouts {:artist artist-name :limit limit}))) ;;;;;;;;;; artist.search ;;;;;;;;;; (declare get-artist-search) (defvar- parse-artist-search-unpaged (create-parse-one-or-more-fn #(artist-from-map {:name (% :name) :url (% :url) :mbid (% :mbid) :streamable (-> % :streamable str-1?)}) #(-> % :results :artistmatches :artist))) (defvar- parse-artist-search (create-paged-search-parse-fn parse-artist-search-unpaged :artist #(get-artist-search %))) (defvar- get-artist-search (create-paged-get-obj-fn {:method "artist.search"} parse-artist-search)) (defn artist-search ([artist-name] (get-artist-search {:artist artist-name})) ([artist-name limit] (get-artist-search {:artist artist-name :limit limit}))) ;;;;;;;;;; Album ;;;;;;;;;; (defvar- album-from-map (partial record-from-map Album 9)) (defn- parse-album [data] (merge (Album. (:name data) (-> data :id safe-parse-int) (:url data) (:mbid data) (-> data :artist artist-from-name) (-> data :listeners safe-parse-int) (-> data :playcount safe-parse-int) (-> data :releasedate parse-date) (-?> data :wiki parse-bio)) {:user-playcount (-?> data :userplaycount safe-parse-int)})) ;;;;;;;;;; album.getinfo ;;;;;;;;;; (defn- parse-album-getinfo [data-fn] (let [data (data-fn)] (do (debug (str "parse-album-getinfo: " data)) (-> data :album parse-album)))) (defvar- get-album (create-get-obj-fn {:method "album.getinfo"} parse-album-getinfo)) (defmulti album-info (fn [arg1 & rest-args] (cond (instance? Album arg1) :album (mbid? arg1) :mbid (and (string? arg1) (-?>> rest-args first (instance? Artist))) :album-name-artist :else :album-name-artist-name))) (defmethod album-info :album ([album] (album-info album nil nil)) ([album username] (album-info album username nil)) ([album username lang] (album-info (-> album :name) (-> album :artist) username lang))) (defmethod album-info :mbid ([mbid] (album-info mbid nil nil)) ([mbid username] (album-info mbid username nil)) ([mbid username lang] (get-album {:mbid mbid :username username :lang lang}))) (defmethod album-info :album-name-artist ([album-name artist] (album-info album-name artist nil nil)) ([album-name artist username] (album-info album-name artist username nil)) ([album-name artist username lang] (album-info album-name (:name artist) username lang))) (defmethod album-info :album-name-artist-name ([album-name artist-name] (album-info album-name artist-name nil nil)) ([album-name artist-name username] (album-info album-name artist-name username nil)) ([album-name artist-name username lang] (get-album {:album album-name :artist artist-name :username username :lang lang}))) ;;;;;;;;;; album.gettoptags ;;;;;;;;;; (defvar- parse-album-toptags (create-parse-one-or-more-fn #(tag-from-map {:name (% :name) :url (% :url) :count (% :count)}) #(-> % :toptags :tag))) (defvar- get-album-toptags (create-get-obj-fn {:method "album.gettoptags"} parse-album-toptags)) (defmulti album-toptags arg-types) (defmethod album-toptags [Album] [album] (album-toptags (-> album :name) (-> album :artist :name))) (defmethod album-toptags [String Artist] [album-name artist] (album-toptags album-name (-> artist :name))) (defmethod album-toptags [String String] [album-name artist-name] (get-album-toptags {:album album-name :artist artist-name})) ;;;;;;;;;; album.search ;;;;;;;;;; (declare get-album-search) (defvar- parse-album-search-unpaged (create-parse-one-or-more-fn #(album-from-map {:name (% :name) :url (% :url) :id (-> % :id safe-parse-int) :artist (-> % :artist artist-from-name) :streamable (-> % :streamable str-1?)}) #(-> % :results :albummatches :album))) (defvar- parse-album-search (create-paged-search-parse-fn parse-album-search-unpaged :album #(get-album-search %))) (defvar- get-album-search (create-paged-get-obj-fn {:method "album.search"} parse-album-search)) (defn album-search ([album-name] (get-album-search {:album album-name})) ([album-name limit] (get-album-search {:album album-name :limit limit}))) ;;;;;;;;;; Tag ;;;;;;;;;; (defvar- tag-from-map (partial record-from-map Tag 2)) (defn- tag-from-name [tag-name] (Tag. tag-name (lastfm-url (str "/tag/" tag-name)))) ;;;;;;;;;; Track ;;;;;;;;;; (defvar- track-from-map (partial record-from-map Track 7)) ;;;;;;;;;; User ;;;;;;;;;; (defvar- user-from-map (partial record-from-map User 3)) (defn- user-from-name [user-name] (user-from-map {:name user-name})) (comment (def *lastfm-api-key* "23caa86333d2cb2055fa82129802780a") (def u2 (artist "u2")) (println (artist-info u2 :url)) )