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.util.Date; 025 import java.text.Format; 026 import java.text.SimpleDateFormat; 027 import java.util.ArrayList; 028 import java.util.Iterator; 029 import java.util.List; 030 031 import net.shredzone.ifish.IFishPrefs; 032 import net.shredzone.ifish.Util; 033 import net.shredzone.ifish.ltr.LTR; 034 import net.shredzone.ifish.ltr.LTRFactory; 035 036 037 /** 038 * The purpose of this class is to synchronize the database with the iHP 039 * harddisk. 040 * 041 * @author Richard Körber <dev@shredzone.de> 042 * @version $Id: Sync.java 291 2009-04-28 21:29:27Z shred $ 043 */ 044 public class Sync { 045 private NaviDb navi; 046 private PlaylistDb playlist; 047 private Playlist newEntryPlaylist = null; 048 private boolean checkModified = false; 049 private boolean deleteMacResource = false; 050 private boolean doCommentPlaylist = false; 051 private boolean doNewEntrantPlaylist = false; 052 private boolean checkTrackNumbers = false; 053 private String playlistName = "{s}"; 054 055 /** 056 * Create a new synchronizer. It only synchronizes the directory with the 057 * database. The playlists are not touched at all. 058 * 059 * @param navi The NaviDb to synchronize with. 060 */ 061 public Sync( NaviDb navi ) { 062 this( navi, null ); 063 } 064 065 /** 066 * Create a new synchronizer. It synchronizes both the directory with the 067 * database, and the playlists. 068 * 069 * @param navi The NaviDb to synchronize with. 070 * @param playlist The PlaylistDb to synchronize with. 071 */ 072 public Sync( NaviDb navi, PlaylistDb playlist ) { 073 this.navi = navi; 074 this.playlist = playlist; 075 } 076 077 /** 078 * Set if the modification date is to be checked. 079 * 080 * @param b true: check modification date, false: ignore 081 */ 082 public void setCheckModified( boolean b ) { 083 checkModified = b; 084 } 085 086 /** 087 * Check whether the modification date is to be checked. 088 * 089 * @return true: check modification date, false: ignore 090 */ 091 public boolean isCheckModified() { 092 return checkModified; 093 } 094 095 /** 096 * Set if the track numbers are to be checked in the file names. 097 * 098 * @param b true: check track numbers, false: ignore 099 */ 100 public void setCheckTrackNumbers( boolean b ) { 101 checkTrackNumbers = b; 102 } 103 104 /** 105 * Check whether the track numbers are to be checked in the file names. 106 * 107 * @return true: check track numbers, false: ignore 108 */ 109 public boolean isCheckTrackNumbers() { 110 return checkTrackNumbers; 111 } 112 113 /** 114 * Set if MacOS X resource fork emulation files are to be deleted 115 * 116 * @param b true: delete resource fork files, false: keep 117 */ 118 public void setDeleteResourceFork( boolean b ) { 119 deleteMacResource = b; 120 } 121 122 /** 123 * Check whether MacOS X resource fork emulation files are to be deleted 124 * 125 * @return true: check modification date, false: ignore 126 */ 127 public boolean isDeleteResourceFork() { 128 return deleteMacResource; 129 } 130 131 /** 132 * Set if playlists are to be created from tag comments. 133 * 134 * @param b true: Create Playlists from tag comments. 135 */ 136 public void setCreateCommentPlaylist( boolean b ) { 137 doCommentPlaylist = b; 138 } 139 140 /** 141 * Chech whether playlists are to be created from tag comments. 142 * 143 * @return true: Create Playlists from tag comments. 144 */ 145 public boolean isCreateCommentPlaylist() { 146 return doCommentPlaylist; 147 } 148 149 /** 150 * Set if playlists are to be created from all new entrants. 151 * 152 * @param b true: Create Playlists from all new entrants 153 */ 154 public void setCreateNewEntrantPlaylist( boolean b ) { 155 doNewEntrantPlaylist = b; 156 } 157 158 /** 159 * Check whether playlists are to be created from all new entrants. 160 * 161 * @return true: Create Playlists from all new entrants 162 */ 163 public boolean isCreateNewEntrantPlaylist() { 164 return doNewEntrantPlaylist; 165 } 166 167 /** 168 * Set the playlist name of the New Entrant playlist. A "{s}" will be replaced 169 * by the last synchronization date, a "{d}" will be replaced by the current 170 * date. 171 * <p> 172 * Default is "{s}". 173 * 174 * @param s The New Entrant playlist name 175 */ 176 public void setNewEntrantName( String s ) { 177 playlistName = s; 178 } 179 180 /** 181 * Get the playlist name of the New Entrant playlist. A "{s}" will be replaced 182 * by the last synchronization date, a "{d}" will be replaced by the current 183 * date. 184 * 185 * @return The new entrant playlist name 186 */ 187 public String getNewEntrantName() { 188 return playlistName; 189 } 190 191 /** 192 * Construct a playlist name for the given model and playlist. If no 193 * PlaylistDb was set, null will be returned. 194 * <p> 195 * First a short date format ("yyyyMMdd") is tried. If there already is a 196 * playlist with that name, a long date format ("yyyyMMdd-HHmmss") is tried 197 * instead. 198 * 199 * @return Playlist name 200 */ 201 protected String createPlaylistName() { 202 if( playlist!=null ) { 203 final Format fmtDateShort = new SimpleDateFormat("yyyyMMdd"); 204 final Format fmtDateLong = new SimpleDateFormat("yyyyMMdd-HHmmss"); 205 final Date dateSync = navi.getCreationDate(); 206 final Date dateNow = new Date(); 207 208 String name = playlistName; 209 name = name.replaceAll( "\\{s\\}", fmtDateShort.format(dateSync) ); 210 name = name.replaceAll( "\\{d\\}", fmtDateShort.format(dateNow ) ); 211 if( playlist.containsPlaylist( name ) ) { 212 name = playlistName; 213 name = name.replaceAll( "\\{s\\}", fmtDateLong.format(dateSync) ); 214 name = name.replaceAll( "\\{d\\}", fmtDateLong.format(dateNow ) ); 215 } 216 217 return name; 218 }else { 219 return null; 220 } 221 } 222 223 /** 224 * Synchronizes a directory with this database. Usually this is the 225 * same directory the database file was located in. 226 * 227 * @param base Base directory 228 */ 229 public void syncDir( File base ) 230 throws IOException, DatabaseException { 231 syncDir( base, null, null ); 232 } 233 234 /** 235 * Synchronizes a directory with this database. Usually this is the 236 * same directory the database file was located in. 237 * 238 * @param base Base directory 239 * @param cb StatusCallback to be used, null for none 240 * @param rcb RenameCallback to be used, null for default 241 */ 242 public void syncDir( File base, StatusCallback cb, RenameCallback rcb ) 243 throws IOException, DatabaseException { 244 int[] cnt = new int[1]; 245 246 //--- Create LTRFactory --- 247 final LTRFactory factory = new LTRFactory(); 248 factory.setID3v1Charset( IFishPrefs.instance().getID3v1Charset() ); 249 factory.setID3v2Charset( IFishPrefs.instance().getID3v2Charset() ); 250 251 //--- Set to indeterminate state --- 252 if( cb!=null ) { 253 cb.setCurrentIndex( -1 ); 254 } 255 256 //--- Find all entries without file on HD --- 257 List<Entry> lDelinquents = new ArrayList<Entry>(); 258 Iterator<Entry> it = navi.iterator(); 259 while( it.hasNext() ) { 260 Entry entry = (Entry) it.next(); 261 File file = entry.getFile(); 262 if( !file.exists() ) { 263 lDelinquents.add( entry ); 264 } 265 } 266 267 //--- Count number of files --- 268 if( cb!=null ) { 269 cnt[0] = 0; 270 scancnt( base, base, cnt ); 271 cb.setMaxEntries( cnt[0]+lDelinquents.size() ); 272 } 273 274 //--- Remove those orphaned entries --- 275 cnt[0] = 0; 276 if( !lDelinquents.isEmpty() ) { 277 while( !lDelinquents.isEmpty() ) { 278 Entry entry = (Entry) lDelinquents.remove(0); 279 cb.setCurrentIndex( cnt[0]++ ); 280 cb.setCurrentEntry( entry ); 281 navi.removeEntry( entry ); 282 if( playlist!=null ) 283 playlist.removeEntry( entry ); 284 } 285 } 286 287 //--- Create a Playlist for all new entries --- 288 if( playlist!=null && doNewEntrantPlaylist ) { 289 newEntryPlaylist = playlist.getPlaylist( createPlaylistName() ); 290 newEntryPlaylist.setStatic( true ); 291 }else { 292 newEntryPlaylist = null; 293 } 294 295 //--- Recursively scan directory --- 296 if( playlist!=null ) playlist.markAll(); // Mark all playlists 297 scan( base, base, cnt, cb, rcb, factory ); 298 if( playlist!=null ) playlist.sortMarkAll(); // Sort the new entries 299 300 //--- Remove new entry playlist if empty --- 301 if( newEntryPlaylist!=null ) { 302 if( newEntryPlaylist.isEmpty() ) { 303 playlist.removePlaylist( newEntryPlaylist ); 304 newEntryPlaylist = null; 305 } 306 } 307 } 308 309 /** 310 * Recursively scan a sub directory of the base directory. 311 * 312 * @param base Base directory 313 * @param dir Current directory, must be a subdirectory 314 * of the base directory. 315 * @param cnt File counter (by reference, as array) 316 * @param cb StatusCallback to be used, or null 317 * @param rcb RenameCallback to be used, or null 318 * @param factory LTRFactory to be used 319 */ 320 private void scan( File base, File dir, int[] cnt, StatusCallback cb, RenameCallback rcb, LTRFactory factory ) 321 throws IOException, DatabaseException { 322 if( cb!=null ) { 323 cb.setCurrentDir( base, dir ); 324 } 325 326 File[] files = dir.listFiles(); 327 328 if( files!=null ) { 329 for( int ix=0; ix<files.length; ix++ ) { 330 File current = files[ix]; 331 files[ix] = null; // make it eligible for GC 332 if( current.isDirectory() ) { 333 scan( base, current, cnt, cb, rcb, factory ); 334 }else if( current.isFile() ) { 335 if( cb!=null ) { 336 cb.setCurrentIndex( cnt[0]++ ); 337 } 338 339 if( deleteMacResource && current.getName().startsWith("._") ) { 340 //--- This is a MacOS X Resource Fork Emulation file --- 341 // We are going to delete it, but we won't bother if it should 342 // fail. 343 current.delete(); 344 345 }else if( (checkModified && navi.modifiedFile( current )) 346 || !navi.knowsFile( current ) ) { 347 //--- Create a new entry --- 348 try { 349 boolean skipFile = false; 350 boolean skipNewEntry = false; 351 352 //--- Decode the file --- 353 LTR ltr = factory.create( current ); 354 if( ltr!=null ) { 355 // This is a music file to be added to the database! 356 357 //--- Check for the track number --- 358 if( checkTrackNumbers ) { 359 File old = current; 360 current = checkTrackNumbers( ltr, current ); 361 if( current!=old ) { 362 // File has been renamed! 363 Entry entry = navi.getEntry( old ); 364 if( entry!=null ) { 365 // The old name was known to the database 366 if( playlist!=null ) 367 playlist.removeEntry( entry ); 368 navi.removeEntry( entry ); 369 skipNewEntry = true; 370 } 371 ltr = factory.create( current ); 372 } 373 } 374 375 //--- Use the RenameCallback if present --- 376 if( rcb!=null ) { 377 //--- Check Directory --- 378 if( !FileEntry.isDirNameFitting( base, current ) ) { 379 File newDir = rcb.renameDirectory( base, current.getParentFile() ); 380 if( newDir==null ) { 381 skipFile = true; 382 }else { 383 throw new DatabaseException( "directory renaming is not supported yet" ); 384 } 385 } 386 387 //--- Check File --- 388 if( !FileEntry.isFileNameFitting( base, current ) ) { 389 File newFile = rcb.renameFile( base, current ); 390 if( newFile==null ) { 391 skipFile = true; 392 }else { 393 current = newFile; 394 ltr = factory.create( current ); 395 } 396 } 397 } 398 399 //--- Add the file to the database --- 400 if( !skipFile ) { 401 FileEntry entry = new FileEntry( base, current, ltr ); 402 403 //--- Update callback --- 404 if( cb!=null ) cb.setCurrentEntry( entry ); 405 406 //--- Add entry to database --- 407 navi.addEntry( entry ); 408 409 //--- Add entry to comment playlists --- 410 if( playlist!=null && doCommentPlaylist ) { 411 playlist.addEntryByComment( entry ); 412 } 413 414 //--- Add entry to new entrant playlist --- 415 if( newEntryPlaylist!=null && !skipNewEntry ) { 416 newEntryPlaylist.addEntry( entry ); 417 } 418 } 419 } 420 }catch( IllegalArgumentException e ) {} 421 } 422 } 423 } 424 } 425 } 426 427 /** 428 * Recursively scan a sub directory of the base directory. 429 * 430 * @param base Base directory 431 * @param dir Current directory, must be a subdirectory 432 * of the base directory. 433 * @param cnt File counter (by reference, as array) 434 */ 435 private void scancnt( File base, File dir, int[] cnt ) 436 throws IOException { 437 /* It's a little stupid that this method is required. It's wasting 438 time, only for showing a proper progress bar that needs to know 439 the total number of files in advance. This is the quickest 440 solution I could think of. Anyhow it's still a feelable delay, 441 so better ideas are welcome! 442 */ 443 File[] files = dir.listFiles(); 444 if( files!=null ) { 445 cnt[0] += files.length; 446 for( int ix=0; ix<files.length; ix++ ) { 447 File current = files[ix]; 448 files[ix] = null; 449 if( current.isDirectory() ) { 450 cnt[0]--; 451 scancnt( base, current, cnt ); 452 } 453 } 454 } 455 } 456 457 /** 458 * Check if the file name starts with a track number. If not, rename 459 * the file according to the track number given in the tag's track 460 * information. This method will silently fail if an error occurs. 461 * 462 * @param ltr LTR information about the file 463 * @param file The file to be checked 464 * @return The new file with track name. If the file name was unchanged, 465 * the file reference will be returned unchanged. 466 */ 467 private File checkTrackNumbers( LTR ltr, File file ) { 468 try { 469 String name = file.getName(); 470 471 //--- Check the file name --- 472 // If the file starts with a number, do nothing. 473 if(! Character.isDigit( name.charAt(0) ) ) { 474 //--- Evaluate the track number --- 475 // The mechanism only works for integer results between 1 and 99. 476 String[] tay = ltr.getTrack().trim().split("\\D+"); 477 if( tay.length>0 ) { 478 String tnr = tay[0].trim(); 479 480 if( tnr.length()>0 ) { 481 int track = Integer.parseInt( tnr, 10 ); 482 if( track>=1 && track<=99 ) { 483 String num = (track<10 ? "0"+track : String.valueOf(track) ); // two digits 484 485 //--- Compose new filename --- 486 String newName = num+" - "+name; 487 File newFile = new File( file.getParent(), newName ); 488 489 //--- Does it exist? --- 490 // If the file does already exist, do nothing. 491 if(! newFile.exists() ) { 492 //--- Rename the file --- 493 if( Util.renameFile( file, newFile ) ) { 494 return newFile; 495 } 496 } 497 } 498 } 499 } 500 } 501 }catch( Exception ignore ) { 502 // If something fails, silently ignore the exception and do not 503 // rename the file. 504 } 505 506 return file; 507 } 508 509 }