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.File;
023    import java.io.IOException;
024    import java.io.UnsupportedEncodingException;
025    import java.util.HashMap;
026    import java.util.HashSet;
027    import java.util.Iterator;
028    import java.util.Map;
029    import java.util.Set;
030    
031    import javax.swing.AbstractListModel;
032    
033    import net.shredzone.jshred.util.SortedList;
034    
035    /**
036     * This class contains the playlist database.
037     *
038     * @author    Richard Körber &lt;dev@shredzone.de&gt;
039     * @version   $Id: PlaylistDb.java 291 2009-04-28 21:29:27Z shred $
040     */
041    public class PlaylistDb extends AbstractListModel {
042      private static final long serialVersionUID = 3833750992515971128L;
043    
044      private final SortedList<Playlist> lPL = new SortedList<Playlist>();
045      private final Map<String, Playlist> mNames = new HashMap<String, Playlist>();
046      private final String     basename;
047      
048      /**
049       * Create an empty database.
050       */
051      public PlaylistDb( String basedir ) {
052        basename = basedir;
053      }
054    
055      /**
056       * Add a playlist to the database.
057       *
058       * @param   playlist      Playlist to be added
059       */
060      public void addPlaylist( Playlist playlist ) {
061        lPL.add( playlist );
062        mNames.put( playlist.getInternalName(), playlist );
063        int ix = lPL.indexOf( playlist );
064        fireIntervalAdded( this, ix, ix );
065      }
066      
067      /**
068       * Remove a playlist from the database. If the entry does not exist,
069       * nothing will happen.
070       *
071       * @param   playlist      Playlist to be removed
072       */
073      public void removePlaylist( Playlist playlist ) {
074        int ix = lPL.indexOf( playlist );
075        mNames.remove( playlist.getInternalName() );
076        lPL.remove( playlist );
077        fireIntervalRemoved( this, ix, ix );
078      }
079      
080      /**
081       * Rename a playlist. This is only possible for static playlists.
082       * 
083       * @param   playlist      Playlist to be renamed
084       * @param   name          New name of the playlist
085       */
086      public void renamePlaylist( Playlist playlist, String name ) {
087        removePlaylist( playlist );
088        try {
089          playlist.setName( name );
090        }finally {
091          addPlaylist( playlist );
092        }
093      }
094      
095      /**
096       * Checks if a playlist with a certain name is known.
097       * 
098       * @param name        Name of the playlist (not the internal name)
099       * @return  true: there already is a playlist known with that name.
100       */
101      public final boolean containsPlaylist( String name ) {
102        return mNames.containsKey( Playlist.internalName( name ) );
103      }
104      
105      /**
106       * Get a playlist with a certain name. If there is no playlist
107       * with that name known, a new empty Playlist will be created.
108       * I.e. the method will always return a Playlist unless the memory
109       * is filled or so...
110       *
111       * @param   name        Name of the playlist (not the internal name)
112       * @return  Playlist with that internal name.
113       */
114      public Playlist getPlaylist( String name ) {
115        String iname = Playlist.internalName( name );
116        if( !mNames.containsKey(iname) ) {
117          addPlaylist( new Playlist( name ) );
118        }
119        return (Playlist) mNames.get( iname );
120      }
121      
122      /**
123       * Add a FileEntry to all the target playlists. The file tag's comment
124       * will be evaluated for the playlist names the file will belong to,
125       * and the FileEntry will then be added to the appropriate playlists.
126       * <p>
127       * <em>Note</em> that only FileEntry objects can be added to playlists,
128       * since the comment is not stored in an DbEntry.
129       *
130       * @param   entry         FileEntry to be added to the playlists
131       */
132      public void addEntryByComment( FileEntry entry ) {
133        
134        //--- Get the comment ---
135        String cmt = entry.getComment();
136        
137        //--- Extract text between square brackets ---
138        // If there is no pair of square brackets, or the brackets
139        // are not in the right order, the method will be left.
140        // The for loop will set start to '[' and end to ']' of each
141        // pair of brackets.
142        for( int start=cmt.indexOf('['), end=cmt.indexOf(']',start+1);
143             start>=0 && end>=0;
144             start=cmt.indexOf('[',end+1), end=cmt.indexOf(']',start+1) ) {
145    
146          //--- Get the playlist name ---
147          String plname = cmt.substring( start+1, end ).trim();
148          if( plname.equals("") ) continue;
149          if( plname.length() > 30 ) continue;
150          
151          //--- Validate it ---
152          boolean valid = true;
153          for( int ix=0; ix<plname.length(); ix++ ) {
154            char ch = plname.charAt( ix );
155            if(! ( Character.isWhitespace(ch) || Character.isJavaIdentifierPart(ch) )) {
156              valid = false;
157              break;
158            }
159          }
160          if( !valid ) continue;
161          
162          //--- Split into single words ---
163          String[] names = plname.split("\\s+");
164          if( names.length==0 ) continue;
165          
166          //--- Add entry to each name ---
167          for( int ix=0; ix<names.length; ix++ ) {
168            String name = names[ix];
169            if( name.equals("") ) continue;
170            name = name.replace('/','_').replace('\\','_');  // replace path chars
171            Playlist pl = getPlaylist( name );
172            pl.addEntry( entry );
173          }
174        }
175         
176      }
177      
178      /**
179       * Remove an Entry from all playlists.
180       *
181       * @param   entry         Entry to be removed from all playlists
182       */
183      public void removeEntry( Entry entry ) {
184        Iterator<Playlist> it = lPL.iterator();
185        while( it.hasNext() ) {
186          Playlist pl = it.next();
187          pl.removeEntry( entry );
188        }
189      }
190      
191      /**
192       * Removes all empty playlists.
193       */
194      public void cleanup() {
195        Iterator<Playlist> it = lPL.iterator();
196        while( it.hasNext() ) {
197          Playlist pl = it.next();
198          if( pl.isEmpty() ) {
199            mNames.remove( pl.getInternalName() );
200            it.remove();
201          }
202        }
203        fireContentsChanged( this, 0, getSize()-1 );
204      }
205      
206      /**
207       * Get the size of all Playlists stored in this PlaylistDB.
208       *
209       * @return    Number of playlists
210       */
211      @Override
212      public int getSize() {
213        return lPL.size();
214      }
215      
216      /**
217       * Get a playlist at a certain index.
218       *
219       * @param   index       Index to get the Playlist from
220       * @return  Playlist object or null if there was none at the index
221       */
222      @Override
223      public Object getElementAt( int index ) {
224        return lPL.get( index );
225      }
226      
227      /**
228       * Get an interator for all Playlist in this PlaylistDb.
229       * 
230       * @return    Iterator
231       */
232      public Iterator<Playlist> iterator() {
233        return lPL.iterator();
234      }
235      
236      /**
237       * Mark all playlists for sorting.
238       */
239      public void markAll() {
240        for (Playlist pl : lPL) {
241          pl.mark();
242        }
243      }
244      
245      /**
246       * Sort all marked parts in all playlists.
247       */
248      public void sortMarkAll() {
249        for (Playlist pl : lPL) {
250          pl.sortMark();
251        }
252      }
253      
254      /**
255       * Read all playlists from the ifish directory of the base directory, in
256       * the given charset. If a StatusCallback is provided, it is used for
257       * showing the current write state.
258       * <p>
259       * The Jukebox software is only able to handle ISO-8859-1 encoded playlists.
260       * For the time being, the charset passed in will be ignored and ISO-8859-1
261       * will always be used.
262       * 
263       * @param base          iHP base directory
264       * @param navi          NaviDb where to find the Entry objects for each file
265       * @param charset       Charset to be used
266       * @param cb            StatusCallback to be used, or null
267       */
268      public void readPlaylists( File base, NaviDb navi, String charset, StatusCallback cb )
269      throws IOException, UnsupportedEncodingException {
270        charset = "ISO-8859-1";
271        
272        //--- Open the directory ---
273        File dir = new File( base, basename );
274        if( dir.exists() && dir.isDirectory() ) {
275          //--- Read directory ---
276          File[] files = dir.listFiles();
277          if( cb!=null )
278            cb.setMaxEntries( files.length );
279          
280          //--- Create each playlist ---
281          for( int ix=0; ix<files.length; ix++ ) {
282            if( cb!=null )
283              cb.setCurrentIndex( ix );
284            
285            //--- Check the file ---
286            File file = (File) files[ix];
287            if( !file.isFile() ) continue;
288            if( !file.getName().toLowerCase().endsWith(".m3u") ) continue;
289            
290            //--- Extract the playlist name ---
291            String name = file.getName();
292            int pos = name.lastIndexOf( '.' );
293            name = name.substring( 0, pos );
294            
295            //--- Create a playlist ---
296            Playlist pl = new Playlist( name );
297            pl.readPlaylist( base, basename, navi, charset );
298            addPlaylist( pl );
299          }
300        }
301      }
302      
303      /**
304       * Write all playlists to the ifish directory of the base directory, in
305       * the given charset. If a StatusCallback is provided, it is used for
306       * showing the current write state.
307       * <p>
308       * The Jukebox software is only able to handle ISO-8859-1 encoded playlists.
309       * For the time being, the charset passed in will be ignored and ISO-8859-1
310       * will always be used.
311       * 
312       * @param base          iHP base directory
313       * @param charset       Charset to be used
314       * @param cb            StatusCallback to be used, or null
315       */
316      public void writePlaylists( File base, String charset, StatusCallback cb )
317      throws IOException, UnsupportedEncodingException {
318        charset = "ISO-8859-1";
319    
320        File dir = new File( base, basename );
321    
322        //--- Skip test ---
323        // If there is no PLBASENAME directory and also no playlists, skip this
324        // part completely.
325        if( !dir.exists() && lPL.isEmpty() )
326          return;
327        
328        //--- Make directory ---
329        dir.mkdir();
330        
331        //--- Get a list of all existing playlists ---
332        Set<String> sDelinquents = new HashSet<String>();
333        File[] files = dir.listFiles();
334        for( int ix=0; ix<files.length; ix++ ) {
335          if( files[ix].isFile() ) {
336            String name = files[ix].getName().toLowerCase();
337            if( name.endsWith(".m3u") ) {
338              sDelinquents.add( name.substring( 0, name.length()-4 ) );
339            }
340          }
341        }
342        
343        //--- Subtract all lPL playlists ---
344        for (Playlist pl : lPL) {
345          sDelinquents.remove( pl.getName().toLowerCase() );
346        }
347        
348        //--- Set the Status Callback ---
349        if( cb!=null )
350          cb.setMaxEntries( lPL.size() + sDelinquents.size() );
351        
352        //--- Write all playlists ---
353        int cnt = 0;
354        for (Playlist pl : lPL) {
355          pl.writePlaylist( base, basename, charset );
356          if( cb!=null )
357            cb.setCurrentIndex( cnt++ );
358        }
359        
360        //--- Delete unused playlists ---
361        for (String name : sDelinquents) {
362          File file = new File( dir, name+".m3u" );
363          file.delete();
364          if( cb!=null )
365            cb.setCurrentIndex( cnt++ );
366        }
367      }
368      
369    }