View Javadoc

1   package org.musicontroller.rss;
2   
3   import java.io.File;
4   import java.io.IOException;
5   import java.io.InputStream;
6   import java.io.OutputStream;
7   import java.io.OutputStreamWriter;
8   import java.io.UnsupportedEncodingException;
9   import java.io.Writer;
10  import java.net.MalformedURLException;
11  import java.net.URI;
12  import java.net.URISyntaxException;
13  import java.net.URLConnection;
14  import java.util.HashMap;
15  import java.util.Iterator;
16  import java.util.Map;
17  
18  import javax.xml.transform.OutputKeys;
19  import javax.xml.transform.Transformer;
20  import javax.xml.transform.TransformerConfigurationException;
21  import javax.xml.transform.sax.SAXTransformerFactory;
22  import javax.xml.transform.sax.TransformerHandler;
23  import javax.xml.transform.stream.StreamResult;
24  
25  import org.apache.log4j.Logger;
26  import org.musicontroller.core.Band;
27  import org.musicontroller.core.Contract_PS;
28  import org.musicontroller.core.Keywordbag;
29  import org.musicontroller.core.Link;
30  import org.musicontroller.core.Playlist;
31  import org.musicontroller.core.Song;
32  import org.musicontroller.dao.BagAndKeywordUtils;
33  import org.musicontroller.dao.Dao;
34  import org.varienaja.util.DateTools;
35  import org.xml.sax.Attributes;
36  import org.xml.sax.InputSource;
37  import org.xml.sax.SAXException;
38  import org.xml.sax.XMLReader;
39  import org.xml.sax.helpers.AttributesImpl;
40  import org.xml.sax.helpers.DefaultHandler;
41  import org.xml.sax.helpers.XMLReaderFactory;
42  
43  /**
44   * Class for reading and writing PodCast-RSS.
45   * @author Varienaja
46   */
47  public class RssDAO {
48  	private static final Logger LOG = Logger.getLogger(RssDAO.class);
49  	private static Dao DAO;
50  	private static final String ENCLOSURE = "enclosure";
51  	private static final String AUTHOR = "itunes:author";
52  	private static final String TITLE = "itunes:title";
53  	private static final String ITEM = "item";
54  	private static final String DURATION = "itunes:duration";
55  	private static final String URL = "url";
56  	private static final String KEYWORDS = "itunes:keywords";
57  	private static final String PLTITLE = "title";
58  	
59  	/**
60  	 * Class for handling PodCast RSS.
61  	 * @author Varienaja
62  	 */
63  	class PodCastRSSHandler extends DefaultHandler {
64  		private Playlist _playlist;
65  		private StringBuilder _content;
66  		private Map<String,String> _contentMap;
67  		private boolean _playlistnameset;
68  		private boolean _playlistkeywordsset;
69  		private String _playlistKeywords;
70  		private String _podcastAuthor;
71  		private int _songidx;
72  		
73  		/**
74  		 * Creates a new RSSHandler.
75  		 */
76  		public PodCastRSSHandler() {
77  			_playlist = null;
78  			_content = new StringBuilder();
79  			_contentMap = new HashMap<String,String>();
80  			_playlistnameset = false;
81  			_playlistkeywordsset = false;
82  			_playlistKeywords = null;
83  			_podcastAuthor = null;
84  			_songidx = 0;
85  		}
86  		
87  		/**
88  		 * Returns the Playlist. Caution: this property is only valid after a
89  		 * rss-feed has been read.
90  		 * @return The Playlist.
91  		 */
92  		protected Playlist getPlaylist() {
93  			return _playlist;
94  		}
95  		
96  		/*
97  		 * (non-Javadoc)
98  		 * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String, org.xml.sax.Attributes)
99  		 */
100 		public void startElement(String uri, String name, String qName, Attributes atts) {
101 			if (ITEM.equals(qName)) { //A new Song
102 				_contentMap = new HashMap<String,String>();
103 			}
104 			if (ENCLOSURE.equals(qName)) { //Capture the URL separately.
105 				_contentMap.put(URL, atts.getValue(URL));
106 			}
107 			_content = new StringBuilder();
108 		}
109 		
110 		/*
111 		 * (non-Javadoc)
112 		 * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
113 		 */
114 		public void endElement(String uri, String name, String qName) {
115 			//Capture all data into a HashMap.
116 			_contentMap.put(qName, _content.toString());
117 			//We should have the Playlist-title before we read the very first song.
118 			if (PLTITLE.equals(qName) && !_playlistnameset) {
119 				String playlistname = _contentMap.get(PLTITLE);
120 				_playlist = DAO.getPlaylistByName(playlistname);
121 				if (_playlist==null) {
122 					_playlist = new Playlist();
123 					_playlist.setName(playlistname);
124 				}
125 				_playlistnameset = true;
126 			}
127 			_content = new StringBuilder();
128 			
129 			if (AUTHOR.equals(qName)) {
130 				if (_podcastAuthor==null) {
131 					_podcastAuthor = _contentMap.get(AUTHOR);
132 				}
133 			}
134 
135 			if (KEYWORDS.equals(qName)) {
136 				if (!_playlistkeywordsset) {
137 					_playlistKeywords = _contentMap.get(KEYWORDS);
138 					_playlistkeywordsset = true;
139 				}
140 			}
141 
142 			if (ITEM.equals(qName)) { //Song done, add to Playlist.
143 				_songidx++;
144 				String bandname = _contentMap.get(AUTHOR);
145 				if (bandname==null) {
146 					//Some Podcasts do not provide a per-item author. In that case we take the Podcasts author.
147 					bandname = _podcastAuthor;
148 				}
149 				Band band = null;
150 				if (bandname!=null) {
151 					band = DAO.getBandByName(bandname);
152 				}
153 				if (band==null) {
154 					//First try to se if we did already construct a new band with this name
155 					for (Contract_PS contract : _playlist.getSongs()) {
156 						Band candidate = contract.getSong().getBand(); 
157 						if (candidate.getName().equals(bandname)) {
158 							band = candidate;
159 						}
160 					}
161 					if (band==null) { //Okay it really is a new band.
162 						band = new Band();
163 						band.setName(bandname);
164 					}
165 				}
166 				
167 				String songname = _contentMap.get(TITLE);
168 				if (songname == null) {
169 					//Fallback to other name-property if title==null.
170 					songname = _contentMap.get(PLTITLE);
171 				}
172 				//Only search for a Song, if the Band is in the DB.
173 				Song song = band.getId()==-1L ? null : DAO.getSong(band, songname);
174 				if (song==null) {
175 					song = new Song();
176 					song.setName(songname);
177 					song.setLength(_contentMap.get(DURATION));
178 					song.setBand(band);
179 
180 					Link link = new Link();
181 					link.setUrl(_contentMap.get(URL));
182 					song.setLink(link);
183 					
184 					String allKeywords = _contentMap.get(KEYWORDS);
185 					if (allKeywords==null || "".equals(allKeywords)) {
186 						//If the Song itself has no keywords defined, take those
187 						//from the Playlist.
188 						allKeywords = _playlistKeywords; 
189 					}
190 					if (allKeywords!=null) {
191 						Keywordbag bag = cachedGetKeywordbag(allKeywords);
192 						song.setKeywordbag(bag);
193 					}
194 				}
195 				
196 				_playlist.addSongIfNew(song, _songidx);
197 			}
198 		}
199 		
200 
201 		/*
202 		 * (non-Javadoc)
203 		 * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
204 		 */
205 		public void characters(char ch[], int start, int length) {
206 			_content.append(ch, start, length);
207 		}
208 	}
209 
210 	private HashMap<String, Keywordbag> _cache;
211 
212 	/**
213 	 * Reads an InputStream (which has to contain a podcast-rss stream), converts
214 	 * it into a Playlist.
215 	 * @param in The inputstream to read the rss from.
216 	 * @return The resulting Playlist, or null if this method could make no
217 	 * sense of the input.
218 	 */
219 	public Playlist readPlaylistFromRss(InputStream in) {
220 		_cache = new HashMap<String, Keywordbag>();
221 		try {
222 			XMLReader xr = XMLReaderFactory.createXMLReader();
223 			PodCastRSSHandler handler = new PodCastRSSHandler();
224 			xr.setContentHandler(handler);
225 			xr.setErrorHandler(handler);
226 			xr.parse(new InputSource(in));
227 			Playlist playlist = handler.getPlaylist();
228 			LOG.debug("Read "+playlist.getSongs().size()+" Songs from RSS-feed.");
229 			return playlist;
230 		} catch (SAXException e) {
231 			LOG.error("Error parsing RSS: "+e);
232 		} catch (IOException e) {
233 			LOG.error("I/O Exception reading RSS: "+e);
234 		} finally {
235 			_cache = null;
236 		}
237 		return null;
238 	}
239 	
240 	/**
241 	 * Reads a Playlist from a URL, which contains a podcast-rss.
242 	 * @param url The URL to read from.
243 	 * @return The Playlist, or null if nothing could be read.
244 	 */
245 	public Playlist readPlaylistFromRss(String url) {
246 		Playlist result = null;
247 		InputStream in = null;
248 		try {
249 			URLConnection urlc = new URI(url).toURL().openConnection();
250 			urlc.setRequestProperty("user-agent","Mozilla/5.0"); //Google wants a user-agent.
251 			
252 			in = urlc.getInputStream();
253 			result = readPlaylistFromRss(in);
254 			if (result.getLink()==null) {
255 				Link link = new Link();
256 				link.setUrl(url);
257 				result.setLink(link);
258 			}
259 		} catch (URISyntaxException e) {
260 			LOG.error(e);
261 		} catch (MalformedURLException e) {
262 			LOG.error(e);
263 		} catch (IOException e) {
264 			LOG.error(e);
265 		} finally {
266 			if (in!=null) {
267 				try {
268 					in.close();
269 				} catch (IOException e) {
270 					LOG.error(e);
271 				}
272 			}
273 		}
274 		return result;
275 	}
276 
277 
278 	/**
279 	 * Writes a Playlist to an Outputstream as a Podcast-xml.
280 	 * @param playlist The Playlist to export.
281 	 * @param out The Stream to write the XML data to.
282 	 * @param provider The Provider for metadata to this Playlist and its Songs.
283 	 */
284 	public void writePlaylistRSS(Playlist playlist, OutputStream out, MetadataProvider provider) {
285     	Writer writer = null;
286 		try {
287 			try {
288 				writer = new OutputStreamWriter(out, "UTF-8");
289 			} catch (UnsupportedEncodingException e) {
290 				LOG.error("Error setting output-charset to UTF-8! " + e);
291 			}
292 			StreamResult streamResult = new StreamResult(writer);
293 			SAXTransformerFactory tf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
294 			TransformerHandler hd = tf.newTransformerHandler();
295 			Transformer t = hd.getTransformer();
296 			t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
297 			t.setOutputProperty(OutputKeys.INDENT, "yes");
298 			hd.setResult(streamResult);
299 			hd.startDocument();
300 			
301 			AttributesImpl atts = new AttributesImpl();
302 			atts.addAttribute("", "", "version", "CDATA", "2.0");
303 			atts.addAttribute("", "", "xmlns:itunes", "CDATA", "http://www.itunes.com/dtds/podcast-1.0.dtd");
304 			hd.startElement("", "", "rss", atts); //rss-tag
305 			
306 			atts.clear();
307 			hd.startElement("", "", "channel", atts); //channel-tag
308 			writeElement(PLTITLE, playlist.getName(), hd);
309 			writeElement("link", provider.getUrl(playlist), hd);
310 
311 			writeElement("language", "en-us", hd);
312 			writeElement("copyright", "todo", hd);
313 			writeElement("itunes:subtitle", "The subtitle of " + playlist.getName(), hd);
314 			writeElement(AUTHOR, playlist.getBandname(), hd);
315 			writeElement("itunes:summary", "The summary of " + playlist.getName(), hd);
316 			writeElement("description", "The description of " + playlist.getName(), hd);
317 
318 			atts.clear();
319 			atts.addAttribute("", "", "href", "CDATA", provider.getCoverUrl(playlist));
320 			hd.startElement("", "", "itunes:image", atts);
321 			hd.endElement("", "", "itunes:image");
322 
323 			atts.clear();
324 			hd.startElement("", "", "itunes:owner", atts);
325 				writeElement("itunes:name", "Someone Else", hd);
326 				writeElement("itunes:email", "someone@example.com", hd);
327 			hd.endElement("", "", "itunes:owner");
328 			
329 			//It is music
330 			atts.clear();
331 			atts.addAttribute("", "", "text", "CDATA", "Music");
332 			hd.startElement("", "", "itunes:category", atts);
333 			hd.endElement("", "", "itunes:category");
334 			
335 			//default-value for explicit.
336 			writeElement("itunes:explicit", "no", hd);
337 			
338 			//Self-link
339 			atts.clear();
340 			atts.addAttribute("", "", "rel", "CDATA", "self");
341 			atts.addAttribute("", "", "type", "CDATA", "application/rss+xml");
342 			atts.addAttribute("", "", "href", "CDATA", provider.getRssUrl(playlist));
343 			
344 			for (Contract_PS cps : playlist.getSongs()) {
345 				atts.clear();
346 				hd.startElement("", "", "item", atts);
347 					writeElement(PLTITLE, cps.getSong().getBand().getName() + " - " + 
348 							cps.getSong().getName(), hd);
349 					writeElement("description", "A " + cps.getSong().getKeywordbag() + "song.", hd);
350 					writeElement("link", provider.getUrl(cps.getSong()), hd);
351 					
352 					writeElement(TITLE, cps.getSong().getName(), hd);
353 					writeElement(AUTHOR, cps.getSong().getBand().getName(), hd);
354 					writeElement("itunes:album", playlist.getName(), hd);
355 					writeElement("itunes:subtitle", "Subtitle of this song", hd);
356 					writeElement("itunes:summary", "Summary of this song", hd);
357 
358 					atts.clear();
359 					File file = cps.getSong().getLink().getFile();
360 					atts.addAttribute("", "", "length", "CDATA",Long.toString(file.length()));
361 					
362 					String downloadurl = provider.getDownloadUrl(cps.getSong());
363 					atts.addAttribute("", "", URL, "CDATA", downloadurl);
364 					atts.addAttribute("", "", "type", "CDATA", "audio/mpeg");
365 					hd.startElement("", "", ENCLOSURE, atts);
366 					hd.endElement("", "", ENCLOSURE);
367 					
368 					writeElement("guid", downloadurl, hd);
369 					//Wed, 15 Jun 2005 19:00:00 +0000
370 					writeElement("pubDate", DateTools.formatDate(cps.getSong().getInserted(), "EEE, d MMM yyyy HH:mm:ss")+" +0000", hd);
371 					writeElement(DURATION, cps.getSong().getFormattedLength(), hd);
372 					writeElement(KEYWORDS, ""+cps.getSong().getKeywordbag(), hd);
373 				hd.endElement("", "", "item");
374 			}
375 			
376 			hd.endElement("", "", "channel");
377 			hd.endElement("", "", "rss");
378 			hd.endDocument();
379 		} catch (TransformerConfigurationException e) {
380 			LOG.error("TransformerConfigurationException during podcast creation: " + e);
381 		} catch (SAXException e) {
382 			LOG.error("SAXException during podcast creation: " + e);
383 		} finally {
384 			try {
385 				if (writer!=null) writer.close();
386 			} catch (IOException e) {
387 				LOG.error("Error closing writer "+e);
388 			}
389 		}
390 	}
391 	
392 	/**
393 	 * Convenience method for writing an element with a text-attribute.
394 	 * @param eltName Element name
395 	 * @param content The attribute
396 	 * @param hd The TransformerHandler
397 	 * @throws SAXException When an error occurred
398 	 */
399 	private void writeElement(String eltName, String content, TransformerHandler hd) throws SAXException {
400 		AttributesImpl atts = new AttributesImpl();
401 		hd.startElement("", "", eltName, atts);
402 		hd.characters(content.toCharArray(), 0, content.length());
403 		hd.endElement("", "", eltName);
404 	}
405 
406 	/**
407 	 * Set the MusiController-dao, which is used for looking up songs and playlists.
408 	 * @param dao The Dao.
409 	 */
410 	public void setDao(Dao dao) {
411 		DAO = dao;
412 	}
413 
414 	/**
415 	 * Merges two Playlists. The original Playlist will be modified such, that
416 	 * afterwards it has the same Songs in it as the fromrss-Playlist. Songs
417 	 * in the fromrss-Playlist that were already known in the Dao, are reused.
418 	 * This means, that the modified original Playlist can be safely persisted
419 	 * to the Database. 
420 	 * 
421 	 * @param original The original-Playlist.
422 	 * @param fromrss The Playlist that resulted from downloading a Podcast.
423 	 */
424 	public void merge(Playlist original, Playlist fromrss) {
425 		//Reuse the existing contracts, so we do not create/delete records in
426 		//the DB all the time
427 		Iterator<Contract_PS> it = original.getSongs().iterator();
428 		for (Contract_PS contract : fromrss.getSongs()) {
429 			Contract_PS orig = it.hasNext() ? it.next() : null;
430 			if (orig==null) {
431 				original.addSong(contract.getSong(), contract.getRowno());
432 			} else {
433 				orig.setSong(contract.getSong());
434 				orig.setRowno(contract.getRowno());
435 			}
436 		}
437 		//Remove superfluous Songs in case fromrss was smaller than the original
438 		while (it.hasNext()) { 
439 			it.next();
440 			it.remove();
441 		}
442 	}
443 	
444 	/**
445 	 * Updates a Playlist with the latest info from the Podcast. This method
446 	 * only succeeds when the parameter points to a Playlist that is a Podcast.
447 	 * Playlists are Podcasts when their Link-property points to a valid URL.
448 	 * @param playlistid The id of the Playlist to update.
449 	 */
450 	public void updatePlaylist(long playlistid) {
451 		Playlist playlist = DAO.getPlaylistById(playlistid, null);
452 		if (playlist.getLink()==null) {
453 			LOG.debug("Playlist: " + playlist.getName() + " cannot be updated because it does not point to a URL.");
454 			return;
455 		}
456 		String url = playlist.getLink().getUrl();
457 		LOG.debug("Updating podcast: " + playlist.getName() + " from: " + url);
458 		Playlist fromrss = readPlaylistFromRss(url);
459 		if (fromrss!=null) {
460 			merge(playlist, fromrss);
461 			DAO.save(playlist);
462 		} else {
463 			LOG.debug("Could not read Playlist from: " + url);
464 		}
465 		LOG.debug("Done updating podcast.");
466 	}
467 	
468 	/**
469 	 * Because many RSS-feeds contain Song which all have the same Keywordbag,
470 	 * we cache the result. This results in less DB-access.
471 	 * @param allKeywords The keywords to get the Keywrdbag for as a comma-separated String.
472 	 * @return The persistent Keywordbag.
473 	 */
474 	private Keywordbag cachedGetKeywordbag(String allKeywords) {
475 		Keywordbag result = _cache.get(allKeywords);
476 		if (result==null) {
477 			result = BagAndKeywordUtils.getKeywordBag(allKeywords);
478 			_cache.put(allKeywords, result);
479 		}
480 		return result;
481 	}
482 
483 }