001    /**
002     * iFish - An iRiver iHP jukebox database creation tool
003     *
004     * Copyright (C) 2009 Richard "Shred" Körber
005     *   http://ifish.shredzone.org
006     *
007     * This program is free software: you can redistribute it and/or modify
008     * it under the terms of the GNU General Public License as published by
009     * the Free Software Foundation, either version 3 of the License, or
010     * (at your option) any later version.
011     *
012     * This program is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
015     * GNU General Public License for more details.
016     *
017     * You should have received a copy of the GNU General Public License
018     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
019     */
020    package net.shredzone.ifish.db;
021    
022    import java.io.*;
023    import java.util.*;
024    import javax.swing.AbstractListModel;
025    
026    /**
027     * This class represents a single playlist. A playlist can contain a
028     * number of Entry elements, which are kept in sequence.
029     *
030     * @author    Richard Körber &lt;dev@shredzone.de&gt;
031     * @version   $Id: Playlist.java 291 2009-04-28 21:29:27Z shred $
032     */
033    public class Playlist extends AbstractListModel implements Comparable<Playlist> {
034      private static final long serialVersionUID = 4050478997970432308L;
035    
036      private String name;
037      private final List<Entry> lEntries = new ArrayList<Entry>();
038      private int mark;
039      private boolean staticList = false;
040      private boolean customerList = false;
041      
042      /**
043       * Converts a name to an internal name.
044       * <p>
045       * Currently this method just returns the playlist name converted
046       * to lower case. This might change in future releases though.
047       *
048       * @param   name        Name of the playlist
049       * @return  Internal name of the playlist.
050       */
051      public static String internalName( String name ) {
052        return name.toLowerCase();
053      }
054      
055      /**
056       * Create a new, empty playlist.
057       *
058       * @param   name        Name of the playlist.
059       */
060      public Playlist( String name ) {
061        this.name = name;
062        mark();
063      }
064      
065      /**
066       * Change the name of the playlist. This is only possible for static
067       * playlists. Do not invoke this method directly, but use the
068       * PlaylistDb.renamePlaylist() method instead!
069       * 
070       * @param   name        New name of the playlist.
071       */
072      void setName( String name ) {  // default by intention, only available for this package!
073        if(! isStatic() )
074          throw new IllegalStateException("cannot rename non-static playlists");
075        this.name = name;
076      }
077      
078      /**
079       * Get the name of the playlist.
080       *
081       * @return  Playlist name
082       */
083      public String getName() {
084        return name;
085      }
086      
087      /**
088       * Get the internal, case insensitive name. A playlist is considered
089       * to be equal to another playlist, if both have the same internal
090       * name.
091       *
092       * @return  Internal playlist name.
093       * @see     #internalName(java.lang.String)
094       */
095      public String getInternalName() {
096        return internalName( name );
097      }
098      
099      /**
100       * Set if this is a static playlist. Static playlists are just generated
101       * once, from statical data, and won't be changed on subsequent
102       * synchronisations.
103       * <p>
104       * By default, playlists are not static.
105       * 
106       * @param  b         true: static playlist.
107       */
108      public void setStatic( boolean b ) {
109        staticList = b;
110      }
111      
112      /**
113       * Check if this is a static playlist. Static playlists are just generated
114       * once, from statical data, and won't be changed on subsequent
115       * synchronisations.
116       * <p>
117       * E.g. a playlist about new entries is static, since it is created once
118       * and won't change after that. A comment playlist is not static, since it
119       * may chance on every synchronisation.
120       * <p>
121       * Static playlists can be deleted by the user, since they won't be recreated
122       * on the next synchronisation.
123       * 
124       * @return  true: static playlist, false: dynamic playlist
125       */
126      public boolean isStatic() {
127        return staticList;
128      }
129      
130      /**
131       * Set if this is a customer playlist. A customer playlist was generated
132       * by a customer himself.
133       * <p>
134       * By default, playlists are not made by customers.
135       * 
136       * @param  b         true: customer playlist.
137       */
138      public void setCustomer( boolean b ) {
139        customerList = b;
140      }
141      
142      /**
143       * Set if this is a customer playlist. A customer playlist was generated
144       * by a customer himself.
145       * <p>
146       * Customer playlists are always static playlists, but the static flag
147       * is not automatically set!
148       * 
149       * @return  true: customer playlist
150       */
151      public boolean isCustomer() {
152        return customerList;
153      }
154    
155      /**
156       * Get the file that holds the playlist. The physical file does
157       * not necessarily need to exist if the playlist has not been
158       * written yet.
159       * <p>
160       * The file is always located in the pldir directory, and
161       * is named like the playlist name with ".m3u" appended to it.
162       *
163       * @param   base      iHP base directory
164       * @param   pldir     Playlist directory, null means root
165       * @return  Playlist file
166       */
167      public File getFile( File base, String pldir ) {
168        File dir = base;
169        if( pldir!=null )
170          dir = new File( base, pldir );
171        return new File( dir, getName()+".m3u" );
172      }
173      
174      /**
175       * Add an Entry to the bottom of the playlist.
176       *
177       * @param   entry       Entry to be added.
178       */
179      public void addEntry( Entry entry ) {
180        if( lEntries.contains( entry ) ) return;
181        
182        lEntries.add( entry );
183        
184        //--- Inform everyone ---
185        int line = lEntries.indexOf( entry );
186        if( line>=0 ) {
187          fireIntervalAdded( this, line, line );
188        }
189      }
190      
191      /**
192       * Remove an Entry from the playlist. If the playlist did not contain
193       * the Entry, nothing will happen.
194       *
195       * @param   entry       Entry to be removed.
196       */
197      public void removeEntry( Entry entry ) {
198        int line = lEntries.indexOf( entry );
199        if( line>=0 ) {
200          lEntries.remove( line );
201          fireIntervalRemoved( this, line, line );
202        }
203        if( line<mark ) {
204          mark--;
205        }
206      }
207      
208      /**
209       * Insert an Entry to a certain position.
210       * <p>
211       * If the playlist already contains the Entry, it will be moved to the new
212       * position (i.e. it will still only be listed once).
213       * 
214       * @param   index         Index to move the entry to.
215       * @param   entry         Entry to be added.
216       */
217      public int insertEntry( int index, Entry entry ) {
218        if( index<0 || index>lEntries.size() )
219          throw new ArrayIndexOutOfBoundsException();
220        
221        int oldpos = lEntries.indexOf( entry );
222        
223        if( oldpos<0 ) {
224          //--- Not in this list yet ---
225          // Just insert it at the desired position.
226    
227          lEntries.add( index, entry );
228          fireIntervalAdded( this, index, index );
229          return index;
230          
231        }else if( oldpos==index ) {
232          //--- In this list, at the position ---
233          // We're done...
234          return index;
235    
236        }else if( oldpos < index ) {
237          //--- In this list, before the new position ---
238          // Remove it and insert it at index-1
239    
240          lEntries.remove( oldpos );
241          lEntries.add( index-1, entry );
242          fireContentsChanged( this, oldpos, index );
243          return index;
244          
245        }
246    
247        //--- In this list, after the new position ---
248        // Remove it and just insert it at the position
249          
250        lEntries.remove( oldpos );
251        lEntries.add( index, entry );
252        fireContentsChanged( this, index, oldpos );
253        return index+1;
254      }
255      
256      /**
257       * Check if this Playlist contains an Entry.
258       * 
259       * @param   entry         Entry to check for
260       * @return  true if this Entry is part of the playlist.
261       */
262      public boolean contains( Entry entry ) {
263        return lEntries.contains( entry );
264      }
265      
266      /**
267       * Mark the current ending position.
268       */
269      public void mark() {
270        mark = lEntries.size();
271      }
272      
273      /**
274       * Sort everything starting from the marked position to the end of the list.
275       * The purpose is to sort new entries by their file name. First use
276       * <code>mark()</code> to mark the current state, then use <code>add()</code>
277       * to add new entries to the end of the list. After that, use <code>sortMark()</code>
278       * to sort those new entries.
279       */
280      public void sortMark() {
281        if( mark>=0 ) {
282          sort( mark, lEntries.size(), false );
283          mark();
284        }
285      }
286      
287      /**
288       * Sort the entire playlist. If <code>desc</code> is set to true, the entries
289       * will be sorted in a descending order.
290       * 
291       * @param desc      true: descending order
292       */
293      public void sort( boolean desc ) {
294        sort( 0, lEntries.size(), desc );
295      }
296      
297      /**
298       * Sort a sublist of this playlist. The entries between <code>first</code>
299       * (including) and <code>last</code> (excluding) will be sorted according to
300       * their file names. If <code>desc</code> is set to true, the entries will
301       * be sorted in a descending order.
302       * 
303       * @param first     First index, including
304       * @param last      Last index, excluding
305       * @param desc      true: descending order
306       */
307      public void sort( int first, int last, boolean desc ) {
308        List<Entry> sublist = lEntries.subList( first, last );
309        Collections.sort( sublist );
310        if( desc ) {
311          Collections.reverse( sublist );
312        }
313        fireContentsChanged( this, first, last-1 );
314      }
315      
316      /**
317       * Shuffle the entire playlist.
318       */
319      public void shuffle() {
320        shuffle( 0, lEntries.size() );
321      }
322      
323      /**
324       * Shuffle a sublist of this playlist. The entries between <code>first</code>
325       * (including) and <code>last</code> (excluding) will be brought into a
326       * random order.
327       * 
328       * @param first     First index, including
329       * @param last      Last index, excluding
330       */
331      public void shuffle( int first, int last ) {
332        List<Entry> sublist = lEntries.subList( first, last );
333        Collections.shuffle( sublist );
334        fireContentsChanged( this, first, last-1 );
335      }
336    
337      /**
338       * Check if the playlist is empty.
339       *
340       * @return    true: empty, false: at least one entry
341       */
342      public boolean isEmpty() {
343        return lEntries.isEmpty();
344      }
345      
346      /**
347       * Get all entries in a unmodifiable List.
348       *
349       * @return    List of all entries
350       */
351      public List<Entry> getEntries() {
352        return Collections.unmodifiableList( lEntries );
353      }
354      
355      /**
356       * Get the number of entries.
357       *
358       * @return    Number of entries
359       */
360      @Override
361      public int getSize() {
362        return lEntries.size();
363      }
364      
365      /**
366       * Get an Entry element at a certain index. The purpose of this
367       * method is solely to fulfil the ListModel interface.
368       *
369       * @param   index         Index to fetch
370       * @return  The Entry object at this index
371       */
372      @Override
373      public Object getElementAt( int index ) {
374        return lEntries.get( index );
375      }
376      
377      /**
378       * Read the playlist from the jukebox. If there is no playlist file
379       * with that name, nothing will happen. Note: the files from this
380       * playlist are appended to this playlist object.
381       * 
382       * @param   base          iHP base directory
383       * @param   pldir         Name of the playlist directory
384       * @param   navi          NaviDb that is used to find the Entry for
385       *                        each file of this playlist
386       * @param   charset       Charset to write the playlist in
387       */
388      public void readPlaylist( File base, String pldir, NaviDb navi, String charset )
389      throws IOException, UnsupportedEncodingException {
390        staticList = false;
391        
392        //--- Open a reader ---
393        // This is going to be ugly...
394        FileInputStream   fis = new FileInputStream( getFile(base, pldir) );
395        InputStreamReader isr = new InputStreamReader( fis, charset );
396        BufferedReader    in  = new BufferedReader( isr );
397        
398        //--- Read all filenames ---
399        try {
400          String line;
401          while( (line = in.readLine()) != null ) {
402            if( line.length()==0 )    continue;     // skip empty lines
403    
404            if( line.startsWith("#STATIC") )        // remember the static state
405              staticList = true;
406    
407            if( line.startsWith("#CUSTOMER") )      // remember the customer state
408              customerList = true;
409    
410            if( line.charAt(0)=='#' ) continue;     // skip comments
411            
412            // Fetch the Entry for the file name in that line.
413            Entry entry = navi.getEntry( base, line );
414            if( entry!=null ) {
415              // Quickly add directly to the list. We will inform the
416              // listeners later when we are finished.
417              lEntries.add( entry );
418            }
419          }
420          
421        }finally {
422          in.close();
423        }
424    
425        //--- Inform everyone ---
426        fireContentsChanged( this, 0, lEntries.size()-1 );
427      }
428      
429      /**
430       * Write the playlist to the jukebox harddisk. If the playlist
431       * file already exists, it will be overwritten.
432       *
433       * @param   base          iHP base directory
434       * @param   pldir         Name of the playlist directory
435       * @param   charset       Charset to write the playlist in
436       */
437      public void writePlaylist( File base, String pldir, String charset )
438      throws IOException, UnsupportedEncodingException {
439        //--- Write the playlist ---
440        OutputStream out = new BufferedOutputStream( new FileOutputStream( getFile(base,pldir) ) );
441        try {
442          //--- Write Header ---
443          out.write( "#EXTM3U\r\n".getBytes(charset ) );
444          if( staticList )
445            out.write( "#STATIC\r\n".getBytes( charset ) );
446          if( customerList )
447            out.write( "#CUSTOMER\r\n".getBytes( charset ) );
448          out.write( "# This playlist was automatically created by IFish.\r\n".getBytes( charset ) );
449          out.write( "# http://www.shredzone.net/go/ifish\r\n".getBytes( charset ) );
450          if( !staticList )
451            out.write( "# Do not edit manually! All changes may be lost.\r\n".getBytes( charset ) );
452    
453          //--- Entries ---
454          for (Entry entry : lEntries) {
455            out.write( entry.getFileName().getBytes( charset ) );
456            out.write( "\r\n".getBytes( charset ) );
457          }
458        }finally {
459          out.close();
460        }
461      }
462      
463      /**
464       * A String representation of the Playlist. It will return the name.
465       * 
466       * @return    String representation
467       */
468      @Override
469      public String toString() {
470        return getName();
471      }
472      
473      /**
474       * Calculate the hashCode of this playlist.
475       *
476       * @return    hash code
477       */
478      @Override
479      public int hashCode() {
480        return getInternalName().hashCode();
481      }
482      
483      /**
484       * Compare this playlist to another playlist. Two playlists are
485       * considered being equal if their internal names are equal. If
486       * the passed obj is null or is not a Playlist object, false will
487       * be returned.
488       *
489       * @param   obj       Object to compare this Playlist with.
490       * @return  true: equal, false: not equal
491       */
492      @Override
493      public boolean equals( Object obj ) {
494        if( obj==null || !(obj instanceof Playlist) )
495          return false;
496        
497        Playlist pl = (Playlist) obj;
498        return getInternalName().equals( pl.getInternalName() );
499      }
500    
501      /**
502       * Compares this Playlist to another playlist. The internal names will
503       * be compared.
504       * 
505       * @param   o         Object to compare this Playlist with.
506       * @return  negative, zero or positive integer.
507       * @throws  ClassCastException
508       * @see java.lang.Comparable#compareTo(java.lang.Object)
509       */
510      @Override
511      public int compareTo( Playlist o ) {
512        return getInternalName().compareTo( o.getInternalName() );
513      }
514    }
515