Browse Source

added artist.getinfo and artist.getsimilar APIs

Abhinav Sarkar 10 years ago
commit
e8f36cecd8
5 changed files with 219 additions and 0 deletions
  1. 4
    0
      .gitignore
  2. 8
    0
      project.clj
  3. 159
    0
      src/clj_lastfm/core.clj
  4. 38
    0
      src/clj_lastfm/filecache.clj
  5. 10
    0
      src/log4j.properties

+ 4
- 0
.gitignore View File

@@ -0,0 +1,4 @@
1
+pom.xml
2
+*jar
3
+lib
4
+classes

+ 8
- 0
project.clj View File

@@ -0,0 +1,8 @@
1
+(defproject clj-lastfm "1.0.0-SNAPSHOT"
2
+  :description "Clojure interface to last.fm API"
3
+  :dependencies [[org.clojure/clojure "1.1.0"]
4
+                 [org.clojure/clojure-contrib "1.1.0"]
5
+                 [log4j "1.2.15" :exclusions [javax.mail/mail
6
+                                              javax.jms/jms
7
+                                              com.sun.jdmk/jmxtools
8
+                                              com.sun.jmx/jmxri]]])

+ 159
- 0
src/clj_lastfm/core.clj View File

@@ -0,0 +1,159 @@
1
+(ns clj-lastfm.core
2
+  (:import (java.net URI)
3
+           (java.text SimpleDateFormat)
4
+           (java.util TimeZone))
5
+  (:use [clj-lastfm.filecache]
6
+        [clojure.contrib.json.read :only (read-json)]
7
+        [clojure.walk :only (keywordize-keys)]
8
+        [clojure.contrib.import-static]
9
+        [clojure.contrib.logging]))
10
+
11
+(import-static java.lang.Integer parseInt)
12
+
13
+;;;;;;;;;; Basic ;;;;;;;;;;
14
+
15
+(def #^{:private true}
16
+  api-root-url ["http" "ws.audioscrobbler.com" "/2.0/"])
17
+
18
+(defn- api-key []
19
+  (let [lastfm-api-key (resolve '*lastfm-api-key*)]
20
+    (if (nil? lastfm-api-key)
21
+      (throw (IllegalStateException. "lastfm API key is not set"))
22
+      lastfm-api-key)))
23
+
24
+(def guid-pattern
25
+  #"^[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}$")
26
+
27
+(def #^{:private true} sdf
28
+  (doto (SimpleDateFormat. "EEE, dd MMMM yyyy HH:mm:ss +0000")
29
+    (.setTimeZone (TimeZone/getTimeZone "GMT"))))
30
+
31
+(defn parse-date [date-str]
32
+  (.parse sdf date-str))
33
+
34
+(defn- remove-nil-values [m]
35
+  (apply hash-map (apply concat (filter #(not (nil? (fnext %))) m))))
36
+
37
+(defn- create-url-query [query-params]
38
+  (apply str
39
+    (interpose
40
+      "&"
41
+      (map
42
+        #(str (name (first %)) "=" (second %))
43
+        (remove-nil-values query-params)))))
44
+
45
+(defn- create-url [query-params]
46
+  (.toURL
47
+    (apply
48
+      #(URI. %1 %2 %3 %4 %5)
49
+      (conj
50
+        api-root-url
51
+        (create-url-query
52
+          (merge query-params {:api_key @(api-key) :format "json"}))
53
+        nil))))
54
+
55
+(def #^{:private true} default-cache (create-file-cache))
56
+
57
+(defn- get-url
58
+  ([url] (get-url url default-cache))
59
+  ([url cache]
60
+    (do
61
+      (debug (str "URL: " url))
62
+      (get-file-content cache url))))
63
+
64
+(defn- get-data [params]
65
+  (let [url (create-url params)]
66
+    (-> url get-url read-json keywordize-keys)))
67
+
68
+(defn create-get-obj-fn [fixed-params parse-fn]
69
+  (fn [more-params]
70
+    (parse-fn (get-data (merge fixed-params more-params)))))
71
+
72
+(defn create-get-obj-field-fn [create-obj-fn extract-obj-id-fields-fn]
73
+  (fn [obj field-kw]
74
+    (let [field-val (obj field-kw)]
75
+      (if (nil? field-val)
76
+        ((apply create-obj-fn (extract-obj-id-fields-fn obj)) field-kw)
77
+        field-val))))
78
+
79
+;;;;;;;;;; Bio/Wiki ;;;;;;;;;;
80
+
81
+(defstruct bio-struct :published :summary :content)
82
+
83
+(defn parse-bio [data]
84
+  (struct
85
+    bio-struct
86
+    (-> data :published parse-date)
87
+    (-> data :summary)
88
+    (-> data :content)))
89
+
90
+;;;;;;;;;; Artist ;;;;;;;;;;
91
+
92
+(defstruct artist-struct
93
+  :name :url :mbid :streamable :listeners :playcount :bio)
94
+
95
+(defn parse-artist [data]
96
+  (struct
97
+    artist-struct
98
+    (-> data :artist :name)
99
+    (-> data :artist :url)
100
+    (-> data :artist :mbid)
101
+    (= 1 (-> data :artist :streamable parseInt))
102
+    (-> data :artist :stats :listeners parseInt)
103
+    (-> data :artist :stats :playcount parseInt)
104
+    (-> data :artist :bio parse-bio)))
105
+
106
+(def #^{:private true}
107
+  get-artist (create-get-obj-fn {:method "artist.getinfo"} parse-artist))
108
+
109
+(defmulti artist
110
+  (fn [artist-or-mbid & _]
111
+    (if (re-matches guid-pattern artist-or-mbid) :mbid :artist)))
112
+
113
+(defmethod artist :artist
114
+  ([artist-name] (artist artist-name nil nil))
115
+  ([artist-name username] (artist artist-name username nil))
116
+  ([artist-name username lang]
117
+    (get-artist {:artist artist-name :username username :lang lang})))
118
+
119
+(defmethod artist :mbid
120
+  ([mbid] (artist mbid nil nil))
121
+  ([mbid username] (artist mbid username nil))
122
+  ([mbid username lang]
123
+    (get-artist {:mbid mbid :username username :lang lang})))
124
+
125
+(def artist-info
126
+  (create-get-obj-field-fn artist #(vector (% :name))))
127
+
128
+(defn parse-artist-similar [data]
129
+  (map
130
+    #(struct artist-struct
131
+      (% :name)
132
+      (% :url)
133
+      (% :mbid)
134
+      (= 1 (-> % :streamable parseInt))
135
+      nil nil nil)
136
+    (-> data :similarartists :artist)))
137
+
138
+(def #^{:private true} get-artist-similar
139
+  (create-get-obj-fn {:method "artist.getsimilar"} parse-artist-similar))
140
+
141
+(defmulti artist-similar 
142
+  (fn [artst-or-name & _] 
143
+    (instance? clojure.lang.PersistentStructMap artst-or-name)))
144
+
145
+(defmethod artist-similar true 
146
+  ([artst] (-> artst :name artist-similar))
147
+  ([artst limit] (artist-similar (artst :name) limit)))
148
+
149
+(defmethod artist-similar false 
150
+  ([artist-name] (get-artist-similar {:artist artist-name}))
151
+  ([artist-name limit] (get-artist-similar {:artist artist-name :limit limit})))  
152
+
153
+(comment
154
+
155
+(def *lastfm-api-key* "23caa86333d2cb2055fa82129802780a")
156
+(def u2 (artist "u2"))
157
+(println (artist-info u2 :url))
158
+
159
+)

+ 38
- 0
src/clj_lastfm/filecache.clj View File

@@ -0,0 +1,38 @@
1
+(ns clj-lastfm.filecache
2
+  (:import (java.io File))
3
+  (:use clojure.contrib.duck-streams))
4
+
5
+(def default-cache-dir (File. (System/getProperty "java.io.tmpdir")))
6
+(def default-expiry-time (* 24 60)) ;in minutes
7
+
8
+(defn- minutes-to-millis [mins] (* mins 1000 60))
9
+(defn- recently-modified? [#^File file expiry-time]
10
+  (> (.lastModified file)
11
+    (- (System/currentTimeMillis) (minutes-to-millis expiry-time))))
12
+
13
+(defn create-file-cache
14
+  "Creates a file cache"
15
+  ([] (create-file-cache default-cache-dir default-expiry-time))
16
+  ([cache-dir] (create-file-cache cache-dir default-expiry-time))
17
+  ([cache-dir expiry-time]
18
+    {:cache-dir cache-dir :expiry-time expiry-time}))
19
+
20
+(defn get-file-content
21
+  "Gets the content of the URL provided from cache if present, else fetches and
22
+  caches it and returns the content"
23
+  [{#^File cache-dir :cache-dir expiry-time :expiry-time} url]
24
+  (let [url-hash (hash url)
25
+        cache-file (File. (str (.getCanonicalPath cache-dir)
26
+                               File/separator "clj-lastfm-" url-hash))]
27
+    (do
28
+      (when-not (and (.exists cache-file) (recently-modified? cache-file expiry-time))
29
+        (copy (reader url) cache-file))
30
+      (slurp (.getCanonicalPath cache-file)))))
31
+
32
+(comment
33
+
34
+(def cache (create-file-cache))
35
+(def url "http://ws.audioscrobbler.com/2.0/?method=artist.gettoptracks&artist=bon%20jovi&api_key=23caa86333d2cb2055fa82129802780a&format=json")
36
+(println (get-file-content cache url))
37
+
38
+)

+ 10
- 0
src/log4j.properties View File

@@ -0,0 +1,10 @@
1
+# Based on the example properties given at http://logging.apache.org/log4j/1.2/manual.html
2
+# Set root logger level to DEBUG and its only appender to A1.
3
+log4j.rootLogger=DEBUG,A1
4
+
5
+# A1 is set to be a ConsoleAppender.
6
+log4j.appender.A1=org.apache.log4j.ConsoleAppender
7
+
8
+# A1 uses PatternLayout.
9
+log4j.appender.A1.layout=org.apache.log4j.PatternLayout
10
+log4j.appender.A1.layout.ConversionPattern= %-5p %c - %m%n

Loading…
Cancel
Save