added artist.getinfo and artist.getsimilar APIs
This commit is contained in:
commit
e8f36cecd8
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
pom.xml
|
||||||
|
*jar
|
||||||
|
lib
|
||||||
|
classes
|
8
project.clj
Normal file
8
project.clj
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
(defproject clj-lastfm "1.0.0-SNAPSHOT"
|
||||||
|
:description "Clojure interface to last.fm API"
|
||||||
|
:dependencies [[org.clojure/clojure "1.1.0"]
|
||||||
|
[org.clojure/clojure-contrib "1.1.0"]
|
||||||
|
[log4j "1.2.15" :exclusions [javax.mail/mail
|
||||||
|
javax.jms/jms
|
||||||
|
com.sun.jdmk/jmxtools
|
||||||
|
com.sun.jmx/jmxri]]])
|
159
src/clj_lastfm/core.clj
Normal file
159
src/clj_lastfm/core.clj
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
(ns clj-lastfm.core
|
||||||
|
(:import (java.net URI)
|
||||||
|
(java.text SimpleDateFormat)
|
||||||
|
(java.util TimeZone))
|
||||||
|
(:use [clj-lastfm.filecache]
|
||||||
|
[clojure.contrib.json.read :only (read-json)]
|
||||||
|
[clojure.walk :only (keywordize-keys)]
|
||||||
|
[clojure.contrib.import-static]
|
||||||
|
[clojure.contrib.logging]))
|
||||||
|
|
||||||
|
(import-static java.lang.Integer parseInt)
|
||||||
|
|
||||||
|
;;;;;;;;;; Basic ;;;;;;;;;;
|
||||||
|
|
||||||
|
(def #^{:private true}
|
||||||
|
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)))
|
||||||
|
|
||||||
|
(def guid-pattern
|
||||||
|
#"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
|
||||||
|
|
||||||
|
(def #^{:private true} sdf
|
||||||
|
(doto (SimpleDateFormat. "EEE, dd MMMM yyyy HH:mm:ss +0000")
|
||||||
|
(.setTimeZone (TimeZone/getTimeZone "GMT"))))
|
||||||
|
|
||||||
|
(defn parse-date [date-str]
|
||||||
|
(.parse sdf date-str))
|
||||||
|
|
||||||
|
(defn- remove-nil-values [m]
|
||||||
|
(apply hash-map (apply concat (filter #(not (nil? (fnext %))) m))))
|
||||||
|
|
||||||
|
(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
|
||||||
|
(apply
|
||||||
|
#(URI. %1 %2 %3 %4 %5)
|
||||||
|
(conj
|
||||||
|
api-root-url
|
||||||
|
(create-url-query
|
||||||
|
(merge query-params {:api_key @(api-key) :format "json"}))
|
||||||
|
nil))))
|
||||||
|
|
||||||
|
(def #^{:private true} default-cache (create-file-cache))
|
||||||
|
|
||||||
|
(defn- get-url
|
||||||
|
([url] (get-url url default-cache))
|
||||||
|
([url cache]
|
||||||
|
(do
|
||||||
|
(debug (str "URL: " url))
|
||||||
|
(get-file-content cache url))))
|
||||||
|
|
||||||
|
(defn- get-data [params]
|
||||||
|
(let [url (create-url params)]
|
||||||
|
(-> url get-url read-json keywordize-keys)))
|
||||||
|
|
||||||
|
(defn create-get-obj-fn [fixed-params parse-fn]
|
||||||
|
(fn [more-params]
|
||||||
|
(parse-fn (get-data (merge fixed-params more-params)))))
|
||||||
|
|
||||||
|
(defn create-get-obj-field-fn [create-obj-fn extract-obj-id-fields-fn]
|
||||||
|
(fn [obj field-kw]
|
||||||
|
(let [field-val (obj field-kw)]
|
||||||
|
(if (nil? field-val)
|
||||||
|
((apply create-obj-fn (extract-obj-id-fields-fn obj)) field-kw)
|
||||||
|
field-val))))
|
||||||
|
|
||||||
|
;;;;;;;;;; Bio/Wiki ;;;;;;;;;;
|
||||||
|
|
||||||
|
(defstruct bio-struct :published :summary :content)
|
||||||
|
|
||||||
|
(defn parse-bio [data]
|
||||||
|
(struct
|
||||||
|
bio-struct
|
||||||
|
(-> data :published parse-date)
|
||||||
|
(-> data :summary)
|
||||||
|
(-> data :content)))
|
||||||
|
|
||||||
|
;;;;;;;;;; Artist ;;;;;;;;;;
|
||||||
|
|
||||||
|
(defstruct artist-struct
|
||||||
|
:name :url :mbid :streamable :listeners :playcount :bio)
|
||||||
|
|
||||||
|
(defn parse-artist [data]
|
||||||
|
(struct
|
||||||
|
artist-struct
|
||||||
|
(-> data :artist :name)
|
||||||
|
(-> data :artist :url)
|
||||||
|
(-> data :artist :mbid)
|
||||||
|
(= 1 (-> data :artist :streamable parseInt))
|
||||||
|
(-> data :artist :stats :listeners parseInt)
|
||||||
|
(-> data :artist :stats :playcount parseInt)
|
||||||
|
(-> data :artist :bio parse-bio)))
|
||||||
|
|
||||||
|
(def #^{:private true}
|
||||||
|
get-artist (create-get-obj-fn {:method "artist.getinfo"} parse-artist))
|
||||||
|
|
||||||
|
(defmulti artist
|
||||||
|
(fn [artist-or-mbid & _]
|
||||||
|
(if (re-matches guid-pattern artist-or-mbid) :mbid :artist)))
|
||||||
|
|
||||||
|
(defmethod artist :artist
|
||||||
|
([artist-name] (artist artist-name nil nil))
|
||||||
|
([artist-name username] (artist artist-name username nil))
|
||||||
|
([artist-name username lang]
|
||||||
|
(get-artist {:artist artist-name :username username :lang lang})))
|
||||||
|
|
||||||
|
(defmethod artist :mbid
|
||||||
|
([mbid] (artist mbid nil nil))
|
||||||
|
([mbid username] (artist mbid username nil))
|
||||||
|
([mbid username lang]
|
||||||
|
(get-artist {:mbid mbid :username username :lang lang})))
|
||||||
|
|
||||||
|
(def artist-info
|
||||||
|
(create-get-obj-field-fn artist #(vector (% :name))))
|
||||||
|
|
||||||
|
(defn parse-artist-similar [data]
|
||||||
|
(map
|
||||||
|
#(struct artist-struct
|
||||||
|
(% :name)
|
||||||
|
(% :url)
|
||||||
|
(% :mbid)
|
||||||
|
(= 1 (-> % :streamable parseInt))
|
||||||
|
nil nil nil)
|
||||||
|
(-> data :similarartists :artist)))
|
||||||
|
|
||||||
|
(def #^{:private true} get-artist-similar
|
||||||
|
(create-get-obj-fn {:method "artist.getsimilar"} parse-artist-similar))
|
||||||
|
|
||||||
|
(defmulti artist-similar
|
||||||
|
(fn [artst-or-name & _]
|
||||||
|
(instance? clojure.lang.PersistentStructMap artst-or-name)))
|
||||||
|
|
||||||
|
(defmethod artist-similar true
|
||||||
|
([artst] (-> artst :name artist-similar))
|
||||||
|
([artst limit] (artist-similar (artst :name) limit)))
|
||||||
|
|
||||||
|
(defmethod artist-similar false
|
||||||
|
([artist-name] (get-artist-similar {:artist artist-name}))
|
||||||
|
([artist-name limit] (get-artist-similar {:artist artist-name :limit limit})))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
|
||||||
|
(def *lastfm-api-key* "23caa86333d2cb2055fa82129802780a")
|
||||||
|
(def u2 (artist "u2"))
|
||||||
|
(println (artist-info u2 :url))
|
||||||
|
|
||||||
|
)
|
38
src/clj_lastfm/filecache.clj
Normal file
38
src/clj_lastfm/filecache.clj
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
(ns clj-lastfm.filecache
|
||||||
|
(:import (java.io File))
|
||||||
|
(:use clojure.contrib.duck-streams))
|
||||||
|
|
||||||
|
(def default-cache-dir (File. (System/getProperty "java.io.tmpdir")))
|
||||||
|
(def default-expiry-time (* 24 60)) ;in minutes
|
||||||
|
|
||||||
|
(defn- minutes-to-millis [mins] (* mins 1000 60))
|
||||||
|
(defn- recently-modified? [#^File file expiry-time]
|
||||||
|
(> (.lastModified file)
|
||||||
|
(- (System/currentTimeMillis) (minutes-to-millis expiry-time))))
|
||||||
|
|
||||||
|
(defn create-file-cache
|
||||||
|
"Creates a file cache"
|
||||||
|
([] (create-file-cache default-cache-dir default-expiry-time))
|
||||||
|
([cache-dir] (create-file-cache cache-dir default-expiry-time))
|
||||||
|
([cache-dir expiry-time]
|
||||||
|
{:cache-dir cache-dir :expiry-time expiry-time}))
|
||||||
|
|
||||||
|
(defn get-file-content
|
||||||
|
"Gets the content of the URL provided from cache if present, else fetches and
|
||||||
|
caches it and returns the content"
|
||||||
|
[{#^File cache-dir :cache-dir expiry-time :expiry-time} url]
|
||||||
|
(let [url-hash (hash url)
|
||||||
|
cache-file (File. (str (.getCanonicalPath cache-dir)
|
||||||
|
File/separator "clj-lastfm-" url-hash))]
|
||||||
|
(do
|
||||||
|
(when-not (and (.exists cache-file) (recently-modified? cache-file expiry-time))
|
||||||
|
(copy (reader url) cache-file))
|
||||||
|
(slurp (.getCanonicalPath cache-file)))))
|
||||||
|
|
||||||
|
(comment
|
||||||
|
|
||||||
|
(def cache (create-file-cache))
|
||||||
|
(def url "http://ws.audioscrobbler.com/2.0/?method=artist.gettoptracks&artist=bon%20jovi&api_key=23caa86333d2cb2055fa82129802780a&format=json")
|
||||||
|
(println (get-file-content cache url))
|
||||||
|
|
||||||
|
)
|
10
src/log4j.properties
Normal file
10
src/log4j.properties
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Based on the example properties given at http://logging.apache.org/log4j/1.2/manual.html
|
||||||
|
# Set root logger level to DEBUG and its only appender to A1.
|
||||||
|
log4j.rootLogger=DEBUG,A1
|
||||||
|
|
||||||
|
# A1 is set to be a ConsoleAppender.
|
||||||
|
log4j.appender.A1=org.apache.log4j.ConsoleAppender
|
||||||
|
|
||||||
|
# A1 uses PatternLayout.
|
||||||
|
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
|
||||||
|
log4j.appender.A1.layout.ConversionPattern= %-5p %c - %m%n
|
Loading…
Reference in New Issue
Block a user