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