View Javadoc

1   package com.melloware.jukes.file;
2   
3   import java.beans.PropertyChangeEvent;
4   import java.beans.PropertyChangeListener;
5   import java.io.File;
6   import java.io.FileOutputStream;
7   import java.io.IOException;
8   import java.sql.Timestamp;
9   import java.text.MessageFormat;
10  import java.util.ArrayList;
11  import java.util.Collections;
12  import java.util.Iterator;
13  import java.util.List;
14  import java.util.Random;
15  
16  import org.apache.commons.io.FileUtils;
17  import org.apache.commons.io.FilenameUtils;
18  import org.apache.commons.lang.StringUtils;
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.dom4j.Document;
22  import org.dom4j.DocumentHelper;
23  import org.dom4j.io.OutputFormat;
24  import org.dom4j.io.XMLWriter;
25  
26  import com.jgoodies.binding.beans.Model;
27  import com.jgoodies.uif.action.ActionManager;
28  import com.jgoodies.uif.util.ResourceUtils;
29  import com.melloware.jspiff.jaxp.XspfPlaylist;
30  import com.melloware.jspiff.jaxp.XspfPlaylistTrackList;
31  import com.melloware.jspiff.jaxp.XspfTrack;
32  import com.melloware.jukes.db.HibernateDao;
33  import com.melloware.jukes.db.orm.Artist;
34  import com.melloware.jukes.db.orm.Catalog;
35  import com.melloware.jukes.db.orm.Disc;
36  import com.melloware.jukes.db.orm.Track;
37  import com.melloware.jukes.file.filter.M3uFilter;
38  import com.melloware.jukes.file.filter.XspfFilter;
39  import com.melloware.jukes.gui.tool.Actions;
40  import com.melloware.jukes.gui.tool.MainModule;
41  import com.melloware.jukes.util.TimeSpan;
42  
43  /**
44   * The playlist of the application. Stores history so you can go back and forth
45   * between previous and next tracks. The history and current list can also be
46   * saved as M3U playlists.
47   * <p>
48   * Copyright (c) 2006 Melloware, Inc. <http://www.melloware.com>
49   * @author Emil A. Lefkof III <info@melloware.com>
50   * @version 4.0
51   */
52  @SuppressWarnings("unchecked")
53  public final class Playlist extends Model implements PropertyChangeListener {
54  
55     private static final Log LOG = LogFactory.getLog(Playlist.class);
56     public static final String PROPERTYNAME_CURRENT_LIST = "currentList";
57     public static final String PROPERTYNAME_HISTORY_LIST = "historyList";
58     private boolean current = true;
59     private boolean shuffleCatalog = false;
60     private boolean shufflePlaylist = false;
61     private Catalog catalog = null;
62     private final List currentList;
63     private final List historyList;
64     private Track currentTrack;
65  
66     /**
67      * Default constructor constructs a list of 100 items.
68      */
69     public Playlist() {
70        super();
71        LOG.debug("Playlist created.");
72        this.historyList = new ArrayList();
73        this.currentList = new ArrayList();
74     }
75  
76     /**
77      * Returns an <code>Iterator</code> for the available backward elements.
78      * @return an iterator that iterates over the available backward elements
79      */
80     public Iterator getBackIterator() {
81        return historyList.iterator();
82     }
83  
84     /**
85      * Gets the current running time.
86      * <p>
87      * @return Returns the current running time.
88      */
89     public String getCurrentDuration() {
90        long duration = 0;
91        final Iterator iter = getNextIterator();
92        while (iter.hasNext()) {
93           final Track track = (Track) iter.next();
94           duration += track.getDuration() * 1000;
95        }
96        return new TimeSpan(duration).getMusicDuration();
97     }
98  
99     /**
100     * Gets the currentList.
101     * <p>
102     * @return Returns the currentList.
103     */
104    public List getCurrentList() {
105       return this.currentList;
106    }
107 
108    /**
109     * Gets the currentTrack.
110     * <p>
111     * @return Returns the currentTrack.
112     */
113    public Track getCurrentTrack() {
114       synchronized (this) {
115          return this.currentTrack;
116       }
117    }
118 
119    /**
120     * Gets the history running time.
121     * <p>
122     * @return Returns the history running time.
123     */
124    public String getHistoryDuration() {
125       long duration = 0;
126       final Iterator iter = getBackIterator();
127       while (iter.hasNext()) {
128          final Track track = (Track) iter.next();
129          duration += track.getDuration() * 1000;
130       }
131       return new TimeSpan(duration).getMusicDuration();
132    }
133 
134    /**
135     * Gets the historyList.
136     * <p>
137     * @return Returns the historyList.
138     */
139    public List getHistoryList() {
140       return this.historyList;
141    }
142 
143    /**
144     * Gets the correct list.
145     * <p>
146     * @return Returns the correct list
147     */
148    public List getList() {
149       if (isCurrent()) {
150          return this.currentList;
151       } else {
152          return this.historyList;
153       }
154    }
155 
156    /**
157     * Returns the next element no matter if shuffling.
158     * @return the next element
159     */
160    public Object getNextImmediate() {
161       Object next = null;
162       next = ((hasNext()) ? currentList.get(0) : null);
163       if (next != null) {
164          synchronized (this) {
165             currentList.remove(next);
166             historyList.add(next);
167             currentTrack = (Track) next;
168          }
169 
170       }
171       updateState();
172       return next;
173 
174    }
175 
176    /**
177     * Returns the next element.
178     * @return the next element
179     */
180    public Object getNext() {
181       Object next = null;
182       if (isShufflePlaylist()) {
183          final Random random = new Random();
184          next = ((hasNext()) ? currentList.get(random.nextInt(currentList.size())) : null);
185       } else if (isShuffleCatalog()) {
186          final Random random = new Random();
187 
188          // grab a random artist
189          final List artists = catalog.getArtists();
190          if (artists == null) {
191             return null;
192          }
193          final Artist artist = (Artist) artists.get(random.nextInt(artists.size()));
194 
195          // grab a random disc from that artist
196          Object[] discs = null;
197          final String filter = MainModule.SETTINGS.getFilter();
198 
199          // if a filter was applied the use the filter HQL
200          if (StringUtils.isNotBlank(filter)) {
201             final String resource = ResourceUtils.getString("hql.filter.disc");
202             final String hql = MessageFormat.format(resource, new Object[] { artist.getId(), filter });
203             discs = HibernateDao.findByQuery(hql).toArray();
204          } else {
205             // else just get all discs for this artist
206             discs = artist.getDiscs().toArray();
207          }
208 
209          final Disc disc = (Disc) discs[(random.nextInt(discs.length))];
210 
211          // grab a random track from that disc
212          final Object[] tracks = disc.getTracks().toArray();
213          next = (Track) tracks[(random.nextInt(tracks.length))];
214       } else {
215          next = ((hasNext()) ? currentList.get(0) : null);
216       }
217 
218       if (next != null) {
219          synchronized (this) {
220             currentList.remove(next);
221             historyList.add(next);
222             currentTrack = (Track) next;
223          }
224 
225       }
226       updateState();
227       return next;
228    }
229 
230    /**
231     * Returns an <code>Iterator</code> for the available next elements.
232     * @return an iterator that iterates over the available next elements
233     */
234    public Iterator getNextIterator() {
235       return currentList.iterator();
236    }
237 
238    /**
239     * Returns the previous element.
240     * @return the previous element
241     */
242    public Object getPrevious() {
243       final Object prev = ((hasPrevious()) ? historyList.get(historyList.size() - 1) : null);
244       if (prev != null) {
245          this.addNext(prev);
246          synchronized (this) {
247             historyList.remove(prev);
248             currentTrack = (Track) prev;
249          }
250       }
251       updateState();
252       return prev;
253    }
254 
255    /**
256     * Sets the current.
257     * <p>
258     * @param aCurrent The current to set.
259     */
260    public void setCurrent(final boolean aCurrent) {
261       this.current = aCurrent;
262       updateState();
263    }
264 
265    /**
266     * Sets the shuffleCatalog.
267     * <p>
268     * @param aShuffleCatalog The shuffleCatalog to set.
269     */
270    public void setShuffleCatalog(final boolean aShuffleCatalog) {
271       this.shuffleCatalog = aShuffleCatalog;
272    }
273 
274    /**
275     * Sets the shufflePlaylist.
276     * <p>
277     * @param aShufflePlaylist The shufflePlaylist to set.
278     */
279    public void setShufflePlaylist(final boolean aShufflePlaylist) {
280       this.shufflePlaylist = aShufflePlaylist;
281    }
282 
283    /**
284     * Gets the current.
285     * <p>
286     * @return Returns the current.
287     */
288    public boolean isCurrent() {
289       return this.current;
290    }
291 
292    /**
293     * Gets the shuffleCatalog.
294     * <p>
295     * @return Returns the shuffleCatalog.
296     */
297    public boolean isShuffleCatalog() {
298       return this.shuffleCatalog;
299    }
300 
301    /**
302     * Gets the shufflePlaylist.
303     * <p>
304     * @return Returns the shufflePlaylist.
305     */
306    public boolean isShufflePlaylist() {
307       return this.shufflePlaylist;
308    }
309 
310    /**
311     * Adds an element to the history if it is not the previous element. Returns
312     * whether the history changed.
313     * @param o the object to add
314     */
315    public void add(final Object o) {
316       if (LOG.isDebugEnabled()) {
317          LOG.debug("Adding to playlist bottom: " + o);
318       }
319 
320       synchronized (this) {
321          final ArrayList tracks = new ArrayList();
322          if (o instanceof Artist) {
323             final Artist artist = (Artist) o;
324             final ArrayList discs = new ArrayList();
325             discs.addAll(artist.getDiscs());
326             Collections.sort(discs);
327             for (final Iterator iter = discs.iterator(); iter.hasNext();) {
328                final Disc disc = (Disc) iter.next();
329                tracks.clear();
330                tracks.addAll(disc.getTracks());
331                Collections.sort(tracks);
332                for (final Iterator iterator = tracks.iterator(); iterator.hasNext();) {
333                   final Track track = (Track) iterator.next();
334                   currentList.add(track);
335                }
336             }
337          } else if (o instanceof Disc) {
338             final Disc disc = (Disc) o;
339             tracks.clear();
340             tracks.addAll(disc.getTracks());
341             Collections.sort(tracks);
342             for (final Iterator iterator = tracks.iterator(); iterator.hasNext();) {
343                final Track track = (Track) iterator.next();
344                currentList.add(track);
345             }
346 
347          } else if (o instanceof Track) {
348             currentList.add(o);
349          }
350          if (currentTrack == null) {
351             currentTrack = (Track) currentList.get(0);
352             ActionManager.get(Actions.PLAYER_PLAY_ID).setEnabled(true);
353          }
354          updateState();
355       }
356    }
357 
358    /**
359     * Adds an element to the history if it is not the previous element. Returns
360     * whether the history changed.
361     * @param o the object to add
362     */
363    public void addNext(final Object o) {
364       if (LOG.isDebugEnabled()) {
365          LOG.debug("Adding to playlist top: " + o);
366       }
367       synchronized (this) {
368          final ArrayList tracks = new ArrayList();
369          if (o instanceof Artist) {
370             final Artist artist = (Artist) o;
371             final ArrayList discs = new ArrayList();
372             discs.addAll(artist.getDiscs());
373             Collections.reverse(discs);
374             for (final Iterator iter = discs.iterator(); iter.hasNext();) {
375                final Disc disc = (Disc) iter.next();
376                tracks.clear();
377                tracks.addAll(disc.getTracks());
378                Collections.reverse(tracks);
379                for (final Iterator iterator = tracks.iterator(); iterator.hasNext();) {
380                   final Track track = (Track) iterator.next();
381                   currentList.add(0, track);
382                }
383             }
384          } else if (o instanceof Disc) {
385             final Disc disc = (Disc) o;
386             tracks.clear();
387             tracks.addAll(disc.getTracks());
388             Collections.reverse(tracks);
389             for (final Iterator iterator = tracks.iterator(); iterator.hasNext();) {
390                final Track track = (Track) iterator.next();
391                currentList.add(0, track);
392             }
393 
394          } else if (o instanceof Track) {
395             currentList.add(0, o);
396          }
397 
398          if (currentTrack == null) {
399             currentTrack = (Track) currentList.get(0);
400          }
401          updateState();
402       }
403    }
404 
405    /**
406     * Checks and answer if the object o is contained in the 'previous' list.
407     * @return true if this item is in the previous list
408     */
409    public boolean contains(final Object o) {
410       return (containsNext(o) || containsPrevious(o));
411    }
412 
413    /**
414     * Checks and answer if the object o is contained in the 'next' list.
415     * @return true if this item is in the next list
416     */
417    public boolean containsNext(final Object o) {
418       return (hasNext()) ? currentList.contains(o) : false;
419    }
420 
421    /**
422     * Checks and answer if the object o is contained in the 'previous' list.
423     * @return true if this item is in the previous list
424     */
425    public boolean containsPrevious(final Object o) {
426       return (hasPrevious()) ? historyList.contains(o) : false;
427    }
428 
429    /**
430     * Checks and answer if there's a next element.
431     * @return true if there's a next element
432     */
433    public boolean hasNext() {
434       synchronized (this) {
435          return ((!currentList.isEmpty()) || this.shuffleCatalog);
436       }
437    }
438 
439    /**
440     * Checks and answers if there's a previous element.
441     * @return true if there's a previous element
442     */
443    public boolean hasPrevious() {
444       synchronized (this) {
445          return (!historyList.isEmpty());
446       }
447    }
448 
449    /**
450     * Move an item down on the list.
451     * <p>
452     * @param index the index to move
453     */
454    public void moveDown(final int index) {
455       synchronized (this) {
456          final Object temp = getList().remove(index);
457          getList().add(index + 1, temp);
458       }
459    }
460 
461    /**
462     * Move an item to the other list.
463     * <p>
464     * @param index the index to move
465     */
466    public void moveOver(final int index) {
467       synchronized (this) {
468          final Object temp = getList().get(index);
469          if (isCurrent()) {
470             historyList.add(temp);
471          } else {
472             currentList.add(temp);
473          }
474       }
475    }
476 
477    /**
478     * Move an item up on the list.
479     * <p>
480     * @param index the index to move
481     */
482    public void moveUp(final int index) {
483       synchronized (this) {
484          final Object temp = getList().remove(index);
485          getList().add(index - 1, temp);
486       }
487    }
488 
489    /**
490     * Plays this track immediately.
491     * @param track the track to play immediately
492     */
493    public void playImmediate(final Track track) {
494       synchronized (this) {
495          getList().remove(track);
496          historyList.add(track);
497       }
498       updateState();
499    }
500 
501    /**
502     * The Catalog has changed.
503     * @param evt describes the property change
504     */
505    public void propertyChange(final PropertyChangeEvent evt) {
506       final String propertyName = evt.getPropertyName();
507       if (MainModule.PROPERTYNAME_CATALOG.equals(propertyName)) {
508          catalog = ((Catalog) evt.getNewValue());
509       }
510    }
511 
512    /**
513     * Removes an item from the list.
514     * <p>
515     * @param index the index to remove from the list
516     */
517    public void remove(final int index) {
518       synchronized (this) {
519          if (index < getList().size()) {
520             getList().remove(index);
521          }
522       }
523    }
524 
525    /**
526     * Saves a playlist to a file.
527     * <p>
528     * @param aFile the file to save.
529     * @throws Exception if any error occurs
530     */
531    public void save(final File aFile) throws Exception {
532 
533       if (FilenameUtils.isExtension(aFile.getName(), M3uFilter.M3U)) {
534          saveM3U(aFile);
535       } else if (FilenameUtils.isExtension(aFile.getName(), XspfFilter.XSPF)) {
536          saveXSPF(aFile);
537       }
538 
539    }
540 
541    /**
542     * Returns the size of the correct list.
543     * @return the size of the correct list
544     */
545    public int size() {
546       if (isCurrent()) {
547          return sizeNext();
548       } else {
549          return sizePrevious();
550       }
551    }
552 
553    /**
554     * Returns the size of the next list.
555     * @return the size of the next list
556     */
557    public int sizeNext() {
558       return currentList.size();
559    }
560 
561    /**
562     * Returns the size of the previous list.
563     * @return the size of the previous list
564     */
565    public int sizePrevious() {
566       return historyList.size();
567    }
568 
569    public String toString() {
570       return "(Current: " + currentList.size() + "; History: " + historyList.size() + ")";
571    }
572 
573    /**
574     * Updates the state of the previous next buttons.
575     */
576    public void updateState() {
577       firePropertyChange(PROPERTYNAME_CURRENT_LIST, null, currentList);
578       firePropertyChange(PROPERTYNAME_HISTORY_LIST, null, historyList);
579       ActionManager.get(Actions.PLAYER_NEXT_ID).setEnabled(hasNext());
580       ActionManager.get(Actions.PLAYER_PREVIOUS_ID).setEnabled(hasPrevious());
581    }
582 
583    /**
584     * Saves an M3U type winamp playlist.
585     * <p>
586     * @param aFile the file to save
587     * @throws IOException if any error occrus writing file
588     */
589    private void saveM3U(final File aFile) throws IOException {
590       final ArrayList results = new ArrayList();
591 
592       results.add("#EXTM3U");
593 
594       final StringBuffer sb = new StringBuffer();
595 
596       // add the currently playing track to the playlist
597       if (this.getCurrentTrack() != null) {
598          final Track track = this.getCurrentTrack();
599          sb.delete(0, sb.length());
600          sb.append("#EXTINF:");
601          sb.append(track.getDuration());
602          sb.append(',');
603          sb.append(track.getDisc().getArtist().getName());
604          sb.append(" - ");
605          sb.append(track.getDisc().getName());
606          sb.append(" - ");
607          sb.append(track.getName());
608          results.add(sb.toString());
609          results.add(track.getTrackUrl());
610       }
611 
612       // now loop through the tracks adding them
613       for (final Iterator iter = getList().iterator(); iter.hasNext();) {
614          final Track track = (Track) iter.next();
615          sb.delete(0, sb.length());
616          sb.append("#EXTINF:");
617          sb.append(track.getDuration());
618          sb.append(',');
619          sb.append(track.getDisc().getArtist().getName());
620          sb.append(" - ");
621          sb.append(track.getDisc().getName());
622          sb.append(" - ");
623          sb.append(track.getName());
624          results.add(sb.toString());
625          results.add(track.getTrackUrl());
626       }
627 
628       FileUtils.writeLines(aFile, null, results);
629    }
630 
631    /**
632     * Saves an XSPF type 'Spiff' playlist.
633     * <p>
634     * @param aFile the file to save
635     * @throws Exception if any error occrus writing file
636     */
637    private void saveXSPF(final File aFile) throws Exception {
638 
639       final XspfPlaylist playlist = new XspfPlaylist();
640       playlist.setTitle(System.getProperty("application.name") + " Playlist");
641       playlist.setCreator(System.getProperty("user.name"));
642       playlist.setDate(new Timestamp(System.currentTimeMillis()));
643       playlist.setInfo("http://melloware.com/");
644       playlist.setVersion("1");
645       final XspfPlaylistTrackList tracks = new XspfPlaylistTrackList();
646       XspfTrack out = null;
647 
648       // add the currently playing track to the playlist
649       if (this.getCurrentTrack() != null) {
650          final Track track = this.getCurrentTrack();
651          out = new XspfTrack();
652          out.setLocation(new File(track.getTrackUrl()).toURI().toASCIIString());
653          out.setCreator(track.getDisc().getArtist().getName());
654          out.setAlbum(track.getDisc().getName());
655          out.setTitle(track.getName());
656          out.setDurationByString(String.valueOf(track.getDuration() * 1000)); // milliseconds
657          out.setTrackNumByString(track.getTrackNumber());
658          tracks.addTrack(out);
659       }
660 
661       // now loop through the queue and add tracks
662       for (final Iterator iter = getList().iterator(); iter.hasNext();) {
663          final Track track = (Track) iter.next();
664          out = new XspfTrack();
665          out.setLocation(new File(track.getTrackUrl()).toURI().toASCIIString());
666          out.setCreator(track.getDisc().getArtist().getName());
667          out.setAlbum(track.getDisc().getName());
668          out.setTitle(track.getName());
669          out.setDurationByString(String.valueOf(track.getDuration() * 1000));// milliseconds
670          out.setTrackNumByString(track.getTrackNumber());
671          tracks.addTrack(out);
672       }
673       playlist.setPlaylistTrackList(tracks);
674       final OutputFormat format = OutputFormat.createPrettyPrint();
675       format.setEncoding("UTF-8");
676       final XMLWriter writer = new XMLWriter(new FileOutputStream(aFile), format);
677       final String xml = new String(playlist.makeTextDocument().getBytes("UTF-8"), "UTF-8");
678       final Document doc = DocumentHelper.parseText(xml);
679       writer.write(doc);
680       writer.close();
681    }
682 
683 }