commit e8f36cecd86888cb062b37756b07e750cbe20a7c Author: Abhinav Sarkar Date: Sat Jul 24 02:54:13 2010 +0530 added artist.getinfo and artist.getsimilar APIs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d9148e9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +pom.xml +*jar +lib +classes \ No newline at end of file diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..0136a19 --- /dev/null +++ b/project.clj @@ -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]]]) \ No newline at end of file diff --git a/src/clj_lastfm/core.clj b/src/clj_lastfm/core.clj new file mode 100644 index 0000000..5a70fdf --- /dev/null +++ b/src/clj_lastfm/core.clj @@ -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)) + +) \ No newline at end of file diff --git a/src/clj_lastfm/filecache.clj b/src/clj_lastfm/filecache.clj new file mode 100644 index 0000000..98f8749 --- /dev/null +++ b/src/clj_lastfm/filecache.clj @@ -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)) + +) \ No newline at end of file diff --git a/src/log4j.properties b/src/log4j.properties new file mode 100644 index 0000000..fe7183f --- /dev/null +++ b/src/log4j.properties @@ -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