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 <dev@shredzone.de> 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