View Javadoc

1   package org.rpi.plugin.lastfm;
2   
3   /***
4    * LastFM Scrobbler Plugin
5    * Written by Markus M May
6    * Contribution by Pete Hoyle
7    * Feb 2014
8    */
9   
10  import java.io.File;
11  import java.net.InetSocketAddress;
12  import java.net.Proxy;
13  import java.net.SocketAddress;
14  import java.nio.charset.Charset;
15  import java.util.ArrayList;
16  import java.util.List;
17  import java.util.Observable;
18  import java.util.Observer;
19  
20  import javax.crypto.Cipher;
21  import javax.crypto.spec.IvParameterSpec;
22  import javax.crypto.spec.SecretKeySpec;
23  import javax.xml.parsers.DocumentBuilder;
24  import javax.xml.parsers.DocumentBuilderFactory;
25  import javax.xml.transform.Transformer;
26  import javax.xml.transform.TransformerFactory;
27  import javax.xml.transform.dom.DOMSource;
28  import javax.xml.transform.stream.StreamResult;
29  
30  import net.xeoh.plugins.base.annotations.PluginImplementation;
31  
32  import org.apache.log4j.Logger;
33  import org.rpi.os.OSManager;
34  import org.rpi.player.PlayManager;
35  import org.rpi.player.events.EventBase;
36  import org.rpi.player.events.EventTrackChanged;
37  import org.rpi.player.events.EventUpdateTrackMetaText;
38  import org.rpi.playlist.CustomTrack;
39  import org.rpi.utils.Utils;
40  import org.w3c.dom.Document;
41  import org.w3c.dom.Element;
42  import org.w3c.dom.Node;
43  import org.w3c.dom.NodeList;
44  
45  import de.umass.lastfm.Authenticator;
46  import de.umass.lastfm.Caller;
47  import de.umass.lastfm.Session;
48  import de.umass.lastfm.Track;
49  import de.umass.lastfm.scrobble.ScrobbleData;
50  import de.umass.lastfm.scrobble.ScrobbleResult;
51  
52  /**
53   *
54   */
55  @PluginImplementation
56  public class LastFmPluginImpl implements LastFmPluginInterface, Observer {
57  
58      private static Logger log = Logger.getLogger(LastFmPluginImpl.class);
59  
60      private static final String lastfm_api_key = "003dc9a812e87f5058f53439dd26038e";
61      private static final String lastfm_secret = "5a9a78a8442187172d136a84568309f8";
62      private static final String userAgent = "MediaPlayer";
63  
64      private String lastfm_username = null;
65      private String lastfm_password = null;
66      private String key = "Ofewtraincrvheg!";
67  
68      private Boolean lastfm_debugmode = false;
69  
70      private Proxy.Type lastfm_proxymode = Proxy.Type.DIRECT;
71      private String lastfm_proxy_ip = null;
72      private Integer lastfm_proxy_port = null;
73      private String title = "";
74      private String artist = "";
75      private List<BlackList> blackList = new ArrayList<BlackList>();
76  
77      private static Session session = null;
78  
79      /**
80       *
81       */
82      public LastFmPluginImpl() {
83          log.info("Init LastFmPluginImpl");
84          getConfig();
85          init();
86          PlayManager.getInstance().observInfoEvents(this);
87          PlayManager.getInstance().observeProductEvents(this);
88          log.info("Finished LastFmPluginImpl");
89      }
90  
91      @Override
92      public void update(Observable o, Object e) {
93          EventBase base = (EventBase) e;
94          switch (base.getType()) {
95              case EVENTTRACKCHANGED:
96                  EventTrackChanged etc = (EventTrackChanged) e;
97                  CustomTrack track = etc.getTrack();
98                  if (track != null) {
99                      scrobble(track.getTitle(), track.getPerformer(), track.getAlbum());
100                 } else {
101                     log.debug("Track was NULL");
102                 }
103 
104                 break;
105             case EVENTUPDATETRACKMETATEXT:
106                 EventUpdateTrackMetaText et = (EventUpdateTrackMetaText) e;
107 
108                 if (et != null)
109                     scrobble(et.getTitle(), et.getArtist());
110                 break;
111         }
112     }
113 
114     /**
115      * Convenience method to call scrobble without an album.
116      *
117      * @param title
118      * @param artist
119      */
120     private void scrobble(String title, String artist) {
121         scrobble(title, artist, "");
122     }
123 
124     /**
125      * Sends the track data to lastfm (scrobbles the track), if the track and/or the artist is not in the blacklist.
126      *
127      * @param title
128      * @param artist
129      * @param album
130      */
131     private void scrobble(String title, String artist, String album) {
132         if(session == null)
133             return;
134         if (!changedTrack(title, artist))
135             return;
136         if (title.equalsIgnoreCase("") || artist.equalsIgnoreCase("")) {
137             log.debug("One is a blank Title: " + title + " Artist: " + artist);
138             return;
139         }
140 
141         for (BlackList bl : blackList) {
142             if (bl.matches(artist, title)) {
143                 log.debug("BlackList Found Title: " + title + " : Artist: " + artist + "Rule: " + bl.toString());
144                 return;
145             }
146         }
147 
148         log.debug("TrackChanged: " + title + " : " + artist + " Album: " + album);
149         int now = (int) (System.currentTimeMillis() / 1000);
150         ScrobbleData data = new ScrobbleData();
151         data.setTimestamp(now);
152         data.setArtist(artist);
153         data.setTrack(title);
154         if (!Utils.isEmpty(album)) {
155             data.setAlbum(album);
156         }
157         ScrobbleResult sres = Track.scrobble(data, session);
158         if (!sres.isSuccessful()|| sres.isIgnored())
159         {
160             log.debug(sres.toString());
161         }
162     }
163 
164     /**
165      * Initializes the connection to lastfm, it stores the details of the connection (session) inside the local
166      * session member.
167      */
168     private void init() {
169         try {
170             log.debug("INIT");
171             Caller.getInstance().setUserAgent(userAgent);
172 
173             if (lastfm_proxymode != Proxy.Type.DIRECT) {
174                 SocketAddress sa = new InetSocketAddress(lastfm_proxy_ip, lastfm_proxy_port);
175                 Proxy proxy = new Proxy(lastfm_proxymode, sa);
176                 Caller.getInstance().setProxy(proxy);
177             }
178 
179             if (lastfm_username.equalsIgnoreCase("") || lastfm_password == null || lastfm_password.equalsIgnoreCase("")) {
180                 log.error("LastFM User Credentials not supplied");
181             } else {
182                 session = Authenticator.getMobileSession(lastfm_username, lastfm_password, lastfm_api_key, lastfm_secret);
183                 log.debug("SessionKey: " + session.getKey());
184             }
185         } catch (Exception e) {
186             log.error(e);
187         }
188         log.debug("End of INIT");
189     }
190 
191     /**
192      *
193      */
194     private void getConfig() {
195         try {
196             String class_name = this.getClass().getName();
197             log.debug("Find Class, ClassName: " + class_name);
198             String path = OSManager.getInstance().getFilePath(this.getClass(), false);
199             log.debug("Getting LastFM.xml from Directory: " + path);
200             DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
201             DocumentBuilder builder = factory.newDocumentBuilder();
202             File file = new File(path + "LastFM.xml");
203             Document doc = builder.parse(file);
204             NodeList listOfConfig = doc.getElementsByTagName("Config");
205             int i = 1;
206             String encrypted_password = "";
207             for (int s = 0; s < listOfConfig.getLength(); s++) {
208                 Node config = listOfConfig.item(s);
209                 if (config.getNodeType() == Node.ELEMENT_NODE) {
210                     Element element = (Element) config;
211                     lastfm_username = getElementTest(element, "UserName", "");
212                     String password = "";
213                     password = getElementTest(element, "Password", "");
214                     if (!password.equalsIgnoreCase("")) {
215                         encrypted_password = encrypt(key, password);
216                         lastfm_password = password;
217                     } else {
218                         String enc_password = getElementTest(element, "Password_ENC", "");
219                         if (!enc_password.equalsIgnoreCase("")) {
220                             lastfm_password = decrypt(key, enc_password);
221                         }
222                     }
223                     String proxymode = getElementTest(element, "ProxyType", "DIRECT");
224                     lastfm_proxymode = Proxy.Type.valueOf(proxymode);
225                     lastfm_proxy_ip = getElementTest(element, "Proxy_IP", "");
226                     String proxy_port = getElementTest(element, "Proxy_Port", "-1");
227                     lastfm_proxy_port = Integer.parseInt(proxy_port);
228                 }
229             }
230 
231             NodeList listOfBlackList = doc.getElementsByTagName("BlackListItem");
232             for (int s = 0; s < listOfBlackList.getLength(); s++) {
233                 Node bl = listOfBlackList.item(s);
234                 if (bl.getNodeType() == Node.ELEMENT_NODE) {
235                     Element element = (Element) bl;
236                     String artist = getElementTest(element, "artist", "");
237                     String title = getElementTest(element, "title", "");
238                     BlackList bli = new BlackList();
239                     bli.setArtist(artist);
240                     bli.setTitle(title);
241                     log.debug("Adding BlackList: " + bli.toString());
242                     blackList.add(bli);
243                 }
244             }
245 
246             if (!encrypted_password.equalsIgnoreCase("")) {
247                 updateMyXML(doc, "LastFM/Config/Password", "");
248                 updateMyXML(doc, "LastFM/Config/Password_ENC", encrypted_password);
249                 TransformerFactory transformerFactory = TransformerFactory.newInstance();
250                 Transformer transformer = transformerFactory.newTransformer();
251                 transformer.transform(new DOMSource(doc), new StreamResult(file));
252 
253             }
254 
255         } catch (Exception e) {
256             log.error("Error Reading LastFM.xml", e);
257         }
258     }
259 
260     /**
261      *
262      * @param doc
263      * @param path
264      * @param def
265      */
266     public void updateMyXML(Document doc, String path, String def) {
267         String p[] = path.split("/");
268         // search nodes or create them if they do not exist
269         Node n = doc;
270         for (int i = 0; i < p.length; i++) {
271             NodeList kids = n.getChildNodes();
272             Node nfound = null;
273             for (int j = 0; j < kids.getLength(); j++)
274                 if (kids.item(j).getNodeName().equals(p[i])) {
275                     nfound = kids.item(j);
276                     break;
277                 }
278             if (nfound == null) {
279                 nfound = doc.createElement(p[i]);
280                 n.appendChild(nfound);
281                 n.appendChild(doc.createTextNode("\n"));
282             }
283             n = nfound;
284         }
285         NodeList kids = n.getChildNodes();
286         for (int i = 0; i < kids.getLength(); i++)
287             if (kids.item(i).getNodeType() == Node.TEXT_NODE) {
288                 // text node exists
289                 kids.item(i).setNodeValue(def); // override
290                 return;
291             }
292         n.appendChild(doc.createTextNode(def));
293     }
294 
295     /**
296      *
297      * @param title
298      * @param artist
299      * @return
300      */
301     private boolean changedTrack(String title, String artist) {
302         if (this.title.equalsIgnoreCase(title) && this.artist.equalsIgnoreCase(artist)) {
303             log.debug("Track didn't Change: " + title + " : " + artist);
304             return false;
305         }
306 
307         this.title = title;
308         this.artist = artist;
309         return true;
310     }
311 
312     /***
313      *
314      * @param element
315      * @param name
316      * @return
317      */
318     private String getElementTest(Element element, String name, String default_value) {
319         String res = default_value;
320         NodeList nid = element.getElementsByTagName(name);
321         if (nid != null) {
322             Element fid = (Element) nid.item(0);
323             if (fid != null) {
324                 res = fid.getTextContent();
325                 if (res.equalsIgnoreCase(""))
326                     res = default_value;
327                 return res;
328             }
329         }
330         return res;
331     }
332 
333     // Simple attempt to encode the password...
334     private  String encrypt(String key, String value) {
335         try {
336             byte[] raw = key.getBytes(Charset.forName("UTF-8"));
337             SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
338             Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
339             cipher.init(Cipher.ENCRYPT_MODE, skeySpec, new IvParameterSpec(new byte[16]));
340             byte[] encrypted = cipher.doFinal(value.getBytes());
341             return Base64.encode(encrypted);
342         } catch (Exception ex) {
343             log.error("Error encrypt: " ,ex);
344         }
345         return null;
346     }
347 
348     private  String decrypt(String key, String encrypted) {
349         try {
350             SecretKeySpec skeySpec = new SecretKeySpec(key.getBytes(), "AES");
351             Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5PADDING");
352             cipher.init(Cipher.DECRYPT_MODE, skeySpec, new IvParameterSpec(new byte[16]));
353             byte[] original = cipher.doFinal(Base64.decode(encrypted));
354 
355             return new String(original);
356         } catch (Exception ex) {
357             log.error("Error decrypt: " ,ex);
358         }
359         return null;
360     }
361 }