View Javadoc

1   package com.melloware.jukes.file;
2   
3   import java.io.File;
4   import java.text.MessageFormat;
5   import java.util.ArrayList;
6   import java.util.Collection;
7   import java.util.Collections;
8   import java.util.Date;
9   import java.util.Iterator;
10  
11  import org.apache.commons.io.FileUtils;
12  import org.apache.commons.lang.StringEscapeUtils;
13  import org.apache.commons.logging.Log;
14  import org.apache.commons.logging.LogFactory;
15  
16  import com.jgoodies.uif.application.Application;
17  import com.jgoodies.uif.util.ResourceUtils;
18  import com.jgoodies.validation.Severity;
19  import com.melloware.jukes.db.HibernateDao;
20  import com.melloware.jukes.db.HibernateUtil;
21  import com.melloware.jukes.db.orm.Artist;
22  import com.melloware.jukes.db.orm.Disc;
23  import com.melloware.jukes.db.orm.Track;
24  import com.melloware.jukes.exception.InfrastructureException;
25  import com.melloware.jukes.exception.MusicTagException;
26  import com.melloware.jukes.file.filter.FilterFactory;
27  import com.melloware.jukes.file.tag.MusicTag;
28  import com.melloware.jukes.file.tag.TagFactory;
29  import com.melloware.jukes.file.image.ImageFactory;
30  import com.melloware.jukes.gui.tool.Resources;
31  import com.melloware.jukes.gui.view.MainFrame;
32  import com.melloware.jukes.util.JukesValidationMessage;
33  import com.melloware.jukes.util.MessageUtil;
34  import com.melloware.jukes.util.TimeSpan;
35  
36  /**
37   * This class is used for working with music file directories. Functions said as
38   * loading a directory into the database, updating ID3 tags, etc.
39   * <p>
40   * Copyright (c) 2006 Melloware, Inc. <http://www.melloware.com>
41   * @author Emil A. Lefkof III <info@melloware.com>
42   * @version 4.0
43   * @see MusicTag
44   * AZ - some modifications 2009, 2010
45   */
46  public final class MusicDirectory {
47  
48     private static final Log LOG = LogFactory.getLog(MusicDirectory.class);
49  
50     /**
51      * Private constructor for no instantiation.
52      */
53     private MusicDirectory() {
54        super();
55     }
56  
57     /**
58      * Creates a new disc in the catalog and optionally updates its ID3 tags.
59      * <p>
60      * @param aTags the array of MusicTag objects
61      * @param aCoverImage the cover image
62      * @param aDirectory the directory where these files are located
63      * @param aUpdateTags true to update tags, false to not modify them
64      * @return a validation message containing the result
65      */
66     @SuppressWarnings("unused")
67     public static JukesValidationMessage createNewDisc(final Object[] aTags, final File aCoverImage,
68            final File aDirectory, final JukesValidationMessage aMessage, final boolean aUpdateTags) {
69        JukesValidationMessage result = null;
70        final MainFrame mainFrame = (MainFrame) Application.getDefaultParentFrame();
71        boolean setDiscComment = true; //AZ: Fill Disc.Notes on the basis of Track.Comment
72        String previousComment = "";
73        if (aMessage == null) {
74           result = new JukesValidationMessage("Disc created successfully", Severity.OK);
75        } else {
76           result = aMessage;
77        }
78        String message = null;
79        try {
80           HibernateUtil.beginTransaction();
81           Artist artist = null;
82           Disc disc = null;
83           String artistName, discName, sGenre, sYear, sTitle, sComment;
84           long totalDuration = 0;
85           final int padding = ((aTags.length >= 100) ? 3 : 2);
86           
87           final MusicTag firstMusicFile = (MusicTag) aTags[0];
88           /** AZ **/
89           // find or create the artist
90              artistName = firstMusicFile.getArtist();
91              if (artistName.length()>100) {
92              	artistName = artistName.substring(0, 100);
93                  message = "Artist Name was too long: " + artistName;
94                  result.setMessage(message);
95                  result.setSeverity(Severity.WARNING);
96                  result.setToolTip(message);
97                  LOG.warn(message);
98              }
99  
100             String resource = ResourceUtils.getString("hql.artist.find");
101             String hql = MessageFormat.format(resource,
102                      new Object[] { StringEscapeUtils.escapeSql(artistName) });
103             artist = (Artist) HibernateDao.findUniqueByQuery(hql);
104             if (artist == null) {
105                artist = new Artist();
106                artist.setName(artistName);
107             }
108             // find or create the disc
109                discName = firstMusicFile.getDisc();
110                if (discName.length()>100) {
111             	   discName = discName.substring(0, 100);
112                    message = "Disc Name was too long: " + discName;
113                    result.setMessage(message);
114                    result.setSeverity(Severity.WARNING);
115                    result.setToolTip(message);
116                    LOG.warn(message);
117                }
118                resource = ResourceUtils.getString("hql.disc.find");
119                hql = MessageFormat.format(resource, new Object[] {
120                         StringEscapeUtils.escapeSql(artist.getName()), StringEscapeUtils.escapeSql(discName) });
121                disc = (Disc) HibernateDao.findUniqueByQuery(hql);
122                if (disc == null) {
123                   disc = new Disc();
124                   disc.setArtist(artist);
125                   disc.setName(discName);
126                   artist.addDisc(disc);     
127                   sGenre = firstMusicFile.getGenre();
128                   if (sGenre.length()>100) {
129                 	  sGenre = sGenre.substring(0, 100);
130                   }
131                   disc.setGenre(sGenre);
132                   sYear = firstMusicFile.getYear();
133                   if (sYear.length()>4) {
134                 	  sYear = sYear.substring(0, 4);
135                   }                 
136                   disc.setYear(sYear);
137                   disc.setBitrate(firstMusicFile.getBitRate());              
138                   
139                   if (aCoverImage != null) {
140                      disc.setCoverUrl(aCoverImage.getAbsolutePath());
141                      disc.setCoverSize(aCoverImage.length());
142             	     /** AZ 
143              	     Scale and copy images to user defined directory **/
144                      String imageLocation = ImageFactory.saveImageToUserDefinedDirectory(aCoverImage,
145                     		  disc.getArtist().getName(),
146               	              disc.getName(),
147               	              disc.getYear());
148                   } else {
149                       message = "No images found in " + aDirectory;
150                       result.setMessage(message);
151                       result.setSeverity(Severity.WARNING);
152                       result.setToolTip(message);
153                       LOG.warn(message);
154                   }
155                   disc.setCreatedDate(new Date(aDirectory.lastModified()));
156                   disc.setLocation(aDirectory.getAbsolutePath());
157          // Loop by tracks
158          MUSIC_LOOP: for (int i = 0; i < aTags.length; i++) {
159             final MusicTag musicFile = (MusicTag) aTags[i];
160                musicFile.setArtist(artist.getName());
161                musicFile.setDisc(discName);
162  
163                // clear out the old tracks
164                /** AZ ** HibernateDao.deleteAll(disc.getTracks()); **/
165                       
166             // now create the track
167             final Track track = new Track();
168             disc.addTrack(track);
169             track.setBitrate(musicFile.getBitRate());
170             track.setDuration(musicFile.getTrackLength());
171             track.setDurationTime(musicFile.getTrackLengthAsString());
172             sTitle = musicFile.getTitle();
173             if (sTitle.length()>100) {
174             	sTitle = sTitle.substring(0, 100);
175             }   
176             track.setName(sTitle);
177             track.setComment(musicFile.getComment());
178             track.setTrackUrl(musicFile.getAbsolutePath());
179             track.setTrackSize(musicFile.getFile().length());
180             musicFile.setTrack(Integer.toString(i + 1), padding);
181             track.setTrackNumber(musicFile.getTrack());
182             track.setCreatedDate(new Date(musicFile.getFile().lastModified()));
183             totalDuration = totalDuration + musicFile.getTrackLength();
184             
185             // save the tag back out if the flag is set
186             if (aUpdateTags) {
187                musicFile.save();
188             }
189          } // MUSIC_LOOP_LOOP
190 
191            //Look for identical comments, set Disc.Notes and purge Track.Comments
192            final MusicTag musicFileFirst = (MusicTag) aTags[0]; 
193            previousComment = musicFileFirst.getComment();
194            for (int i = 1; i < aTags.length; i++) {
195          	   final MusicTag musicFile = (MusicTag) aTags[i];
196                   if (!(previousComment.equals(musicFile.getComment()))) {
197                 	  setDiscComment = false;
198                   }
199                previousComment = musicFile.getComment();
200            }
201            
202            if (setDiscComment) {
203         	 if (previousComment.length()>500) {
204         		 previousComment = previousComment.substring(0, 500);
205         	 }
206         	 disc.setNotes(previousComment);
207         	 final Collection tracks = disc.getTracks();
208         	 Track track;
209         	 for (final Iterator iter = tracks.iterator(); iter.hasNext();) {
210         		 track = (Track) iter.next();
211         		 track.setComment("");
212         	 }
213            } else {
214           	 final Collection tracks = disc.getTracks();
215         	 Track track;
216         	 for (final Iterator iter = tracks.iterator(); iter.hasNext();) {
217         		 track = (Track) iter.next();
218         		 sComment = track.getComment();
219         		 if (sComment.length()>254) {
220         			 sComment = sComment.substring(0, 254);
221         			 track.setComment(sComment);
222         		 }
223         	 }       	   
224            }
225 
226          // update the discs total duration and time
227          disc.setDuration(totalDuration);
228          final TimeSpan timespan = new TimeSpan(totalDuration * 1000);
229          disc.setDurationTime(timespan.getMusicDuration());
230        
231          // commit changes
232          HibernateDao.saveOrUpdate(artist);
233          HibernateUtil.commitTransaction();
234          } //If Disc not found
235          /** AZ **/
236          else {
237       	   message = Resources.getString("messages.DiscAlreadyExist") + disc.getArtist().getName() + " [" + disc.getYear() + "] " + disc.getName() + " at " + disc.getLocation();
238              result.setSeverity(Severity.ERROR);
239              result.setMessage(message);
240              result.setToolTip(message);
241              LOG.warn(message);
242          } 
243       } catch (MusicTagException ex) {
244          message = Resources.getString("messages.ErrorWritingMusicTag") + ex.getMessage();
245          result.setMessage(message);
246          result.setSeverity(Severity.ERROR);
247          result.setToolTip(message);
248          LOG.error(message, ex);
249          HibernateUtil.rollbackTransaction();
250       } catch (InfrastructureException ex) {
251          message = Resources.getString("messages.ErrorDB") + ex.getMessage();
252          result.setMessage(message);
253          result.setSeverity(Severity.ERROR);
254          result.setToolTip(message);
255          LOG.error(message, ex);
256          HibernateUtil.rollbackTransaction();
257       } catch (Throwable ex) {
258          message = Resources.getString("messages.UnexpectedError") + ex.getMessage();
259          result.setMessage(message);
260          result.setSeverity(Severity.ERROR);
261          result.setToolTip(message);
262          LOG.error(message, ex);
263          HibernateUtil.rollbackTransaction();
264       }
265 
266       return result;
267    }
268 
269    /**
270     * Loops through an array of files and determines the largest one in bytes.
271     * <p>
272     * @param aDirectory the directory to look for the largest file in.
273     * @return the file found to be the largest
274     */
275    public static File findLargestImageFile(final File aDirectory) {
276       return findLargestImageFile(aDirectory, null);
277    }
278 
279    /**
280     * For a directory gets all of its music type files into a collection. If
281     * none are found return null, and set the validation message.
282     * <p>
283     * @param aDirectory the directory to scan
284     * @return a collection of files or NULL if not found
285     */
286    public static Collection findMusicFiles(final File aDirectory) {
287       if (LOG.isDebugEnabled()) {
288          LOG.debug("Finding music files in directory:" + aDirectory);
289       }
290       return findMusicFiles(aDirectory, null);
291    }
292 
293    /**
294     * Silently loads all tracks and one image file from the aDirectory. It must
295     * be a directory and if any exception is thrown all we do is return false to
296     * silently report the error.
297     * <p>
298     * @param aDirectory the directory to add
299     * @param aUpdateTags boolean whether to overwrite new tags or not
300     * @return true if successful, false if any error
301     */
302    @SuppressWarnings( { "unused", "unchecked" })
303    public static JukesValidationMessage loadDiscFromDirectory(final File aDirectory, final boolean aUpdateTags) {
304 
305       if (aDirectory == null) {
306          throw new IllegalArgumentException("A directory must be selected");
307       }
308 
309       JukesValidationMessage result = new JukesValidationMessage(aDirectory.getAbsolutePath(), Severity.OK);
310       String message = null;
311       final File directory = aDirectory;
312       // make sure it is a directory
313       if ((!directory.isDirectory()) || (!directory.exists())) {
314          throw new IllegalArgumentException("A directory must be selected");
315       }
316 
317       try {
318          if (LOG.isDebugEnabled()) {
319             LOG.debug("Loading music directory: " + directory);
320          }
321 
322          // first get all the music files in this dir
323          final Collection musicFiles = findMusicFiles(directory, result);
324          if ((musicFiles == null) || (musicFiles.isEmpty())) {
325             return result;
326          }
327 
328          // now get a list of all the image files for disc cover
329          final File imageFile = findLargestImageFile(aDirectory, result);
330          if (LOG.isDebugEnabled()) {
331             LOG.debug("Files found = " + musicFiles.size());
332             LOG.debug("Image selected = " + imageFile);
333          }
334 
335          final ArrayList tagList = new ArrayList();
336          MUSIC_LOOP: for (final Iterator iter = musicFiles.iterator(); iter.hasNext();) {
337             final File musicFile = (File) iter.next();
338             if (LOG.isDebugEnabled()) {
339                LOG.debug("Music File: " + musicFile);
340             }
341             // load the tag
342             final MusicTag tag = TagFactory.getTag(musicFile);
343             tagList.add(tag);
344          } // MUSIC_LOOP
345          
346          // sort the tags by track number
347          Collections.sort(tagList);
348 
349          result = createNewDisc(tagList.toArray(), imageFile, aDirectory, result, aUpdateTags);
350 
351       } catch (MusicTagException ex) {
352          message = "Error opening music file: " + ex.getMessage();
353          result.setSeverity(Severity.ERROR);
354          result.setToolTip(message);
355          LOG.error(message, ex);
356       } catch (Throwable ex) {
357          message = "Unexpected Error: " + ex.getMessage();
358          result.setSeverity(Severity.ERROR);
359          result.setToolTip(message);
360          LOG.error(message, ex);
361       }
362 
363       return result;
364    }
365 
366    /**
367     * Checks for the disc at the location on the hard drive. If that location no
368     * longer exists on the hard drive then this disc is removed from the
369     * database. This is useful for when using the Jukes with portable storage
370     * devices like Archos Jukebox. Thanks to Bill Farkas for suggesting this
371     * feature.
372     * @param aDisc the disc to check the hard drive for
373     * @return true if the disc was OK, false if the disc was removed
374     */
375    public static boolean removeDiscIfNoLongerExists(final Disc aDisc) {
376       boolean result = false;
377       final MainFrame mainFrame = (MainFrame) Application.getDefaultParentFrame();
378 
379       if (aDisc == null) {
380          throw new IllegalArgumentException("Disc may not be null");
381       }
382 
383       if (LOG.isDebugEnabled()) {
384          LOG.debug("Checking disc for validity: " + aDisc.getName());
385       }
386 
387       // get the file location for this disc
388       final File discDirectory = new File(aDisc.getLocation());
389 
390       // return true if the directory still exists
391       result = discDirectory.exists();
392 
393       // remove the directory if it no longer exists
394       try {
395          if (!result) {
396             // try to delete from database
397             HibernateUtil.beginTransaction();
398             final Artist artist = aDisc.getArtist();
399             artist.getDiscs().remove(aDisc);
400             HibernateDao.delete(aDisc);
401             HibernateUtil.commitTransaction();
402          }
403       } catch (InfrastructureException ex) {
404          final String errorMessage = ResourceUtils.getString("messages.ErrorDeletingDisc");
405          LOG.error(errorMessage, ex);
406          MessageUtil.showError(mainFrame, errorMessage); //AZ
407          HibernateUtil.rollbackTransaction();
408          result = false;
409       }
410 
411       return result;
412    }
413 
414    /**
415     * Loops through an array of files and determines the largest one in bytes.
416     * <p>
417     * @param aDirectory the directory to look for the largest file in.
418     * @param aMessage a JukesValidationMessage to contain any errors, may be
419     *           null
420     * @return the file found to be the largest
421     */
422    private static File findLargestImageFile(final File aDirectory, final JukesValidationMessage aMessage) {
423       if (LOG.isDebugEnabled()) {
424          LOG.debug("Finding largest image in directory: " + aDirectory);
425       }
426 
427       File result = null;
428       final Collection imageFiles = FileUtils.listFiles(aDirectory, FilterFactory.IMAGE_FILTER, null);
429 
430       // get the largest of the images if there are more than one
431       if ((imageFiles != null) && (!imageFiles.isEmpty())) {
432          LOG.debug("Images found in directory");
433          final Object[] files = (Object[]) imageFiles.toArray();
434 
435          // if only one image then just return that image
436          if (imageFiles.size() == 1) {
437             result = (File) files[0];
438          } else {
439             // need to pick the largest file for the biggest resolution cover
440             long filesize = 0;
441             /** AZ - find "cover.*" and "folder.*" files
442              * 
443              */ File coverFile = null;
444             for (int i = 0; i < files.length; i++) {
445                final File file = (File) files[i];
446                final long size = file.length();
447                if (size > filesize) {
448                   filesize = size;
449                   result = file;
450                }
451                final String shortName;
452                if (file.getName().lastIndexOf(".") != -1) {
453             	   shortName = file.getName().substring(0,file.getName().lastIndexOf(".")).toUpperCase();
454                } else {
455             	   shortName = file.getName().toUpperCase();
456                }
457                if ((shortName.equals("COVER") ||
458             	   shortName.equals("FOLDER") ||
459             	   shortName.equals("FRONT") ) &
460             	   (coverFile == null ) ) {
461             	   coverFile = file;
462             	   }
463             }
464      	   if (coverFile != null) {
465      		   result = coverFile;
466       	   }
467      	   else {
468      		  if (aMessage != null) {
469                   LOG.info("More than one image found.");
470                   aMessage.setSeverity(Severity.WARNING);
471                   aMessage.setToolTip("Directory contained more than one image so largest was selected. ");
472                }
473      	   }
474          }
475       }     
476       return result;
477    }
478 
479    /**
480     * For a directory gets all of its music type files into a collection. If
481     * none are found return null, and set the validation message.
482     * AZ: if sub-directories are found no warning messages to be set  
483     * <p>
484     * @param aDirectory the directory to scan
485     * @param aMessage the validation message to fill
486     * @return a collection of files or NULL if not found
487     */
488    private static Collection findMusicFiles(final File aDirectory, final JukesValidationMessage aMessage) {
489       // first get all the music files and subDirs in this dir
490       final Collection results = FileUtils.listFiles(aDirectory, FilterFactory.MUSIC_FILTER, null);
491       final Collection resultsDir = FileUtils.listFiles(aDirectory, FilterFactory.dirIOFilter(), null);//AZ
492 
493       // if collection is null just return true
494       if (((results == null) || (results.isEmpty())) && ((resultsDir==null) || (resultsDir.isEmpty()) ) && (aMessage != null)) {
495          final String message = "Directory contained no music files. " + aDirectory;
496          LOG.warn(message);
497          aMessage.setSeverity(Severity.WARNING);
498          aMessage.setToolTip(message);
499       }
500 
501       return results;
502    }
503 }