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
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        if (aMessage == null) {
72           result = new JukesValidationMessage("Disc created successfully", Severity.OK);
73        } else {
74           result = aMessage;
75        }
76        String message = null;
77        try {
78           HibernateUtil.beginTransaction();
79           Artist artist = null;
80           Disc disc = null;
81           long totalDuration = 0;
82           final int padding = ((aTags.length >= 100) ? 3 : 2);
83           
84           final MusicTag firstMusicFile = (MusicTag) aTags[0];
85           /** AZ **/
86           // find or create the artist
87              final String artistName = firstMusicFile.getArtist();
88              String resource = ResourceUtils.getString("hql.artist.find");
89              String hql = MessageFormat.format(resource,
90                       new Object[] { StringEscapeUtils.escapeSql(artistName) });
91              artist = (Artist) HibernateDao.findUniqueByQuery(hql);
92              if (artist == null) {
93                 artist = new Artist();
94                 artist.setName(artistName);
95              }
96              // find or create the disc
97   
98                 final String discName = firstMusicFile.getDisc();
99                 resource = ResourceUtils.getString("hql.disc.find");
100                hql = MessageFormat.format(resource, new Object[] {
101                         StringEscapeUtils.escapeSql(artist.getName()), StringEscapeUtils.escapeSql(discName) });
102                disc = (Disc) HibernateDao.findUniqueByQuery(hql);
103                if (disc == null) {
104                   disc = new Disc();
105                   disc.setArtist(artist);
106                   disc.setName(discName);
107                   artist.addDisc(disc);     
108                   disc.setGenre(firstMusicFile.getGenre());
109                   disc.setYear(firstMusicFile.getYear());
110                   disc.setBitrate(firstMusicFile.getBitRate());
111                   if (aCoverImage != null) {
112                      disc.setCoverUrl(aCoverImage.getAbsolutePath());
113                      disc.setCoverSize(aCoverImage.length());
114                   }
115                   disc.setCreatedDate(new Date(aDirectory.lastModified()));
116                   disc.setLocation(aDirectory.getAbsolutePath());
117          // Loop by tracks
118          MUSIC_LOOP: for (int i = 0; i < aTags.length; i++) {
119             final MusicTag musicFile = (MusicTag) aTags[i];
120                musicFile.setArtist(artist.getName());
121                musicFile.setDisc(discName);
122  
123                // clear out the old tracks
124                /** AZ ** HibernateDao.deleteAll(disc.getTracks()); **/
125                       
126             // now create the track
127             final Track track = new Track();
128             disc.addTrack(track);
129             track.setBitrate(musicFile.getBitRate());
130             track.setDuration(musicFile.getTrackLength());
131             track.setDurationTime(musicFile.getTrackLengthAsString());
132             track.setName(musicFile.getTitle());
133             track.setComment(musicFile.getComment());
134             track.setTrackUrl(musicFile.getAbsolutePath());
135             track.setTrackSize(musicFile.getFile().length());
136             musicFile.setTrack(Integer.toString(i + 1), padding);
137             track.setTrackNumber(musicFile.getTrack());
138             track.setCreatedDate(new Date(musicFile.getFile().lastModified()));
139             totalDuration = totalDuration + musicFile.getTrackLength();
140 
141             // save the tag back out if the flag is set
142             if (aUpdateTags) {
143                musicFile.save();
144             }
145          } // MUSIC_LOOP_LOOP
146 
147          // update the discs total duration and time
148          disc.setDuration(totalDuration);
149          final TimeSpan timespan = new TimeSpan(totalDuration * 1000);
150          disc.setDurationTime(timespan.getMusicDuration());
151 
152          // commit changes
153          HibernateDao.saveOrUpdate(artist);
154          HibernateUtil.commitTransaction();
155          } //If Disc not found
156          /** AZ **/
157          else {
158       	   message = Resources.getString("messages.DiscAlreadyExist") + disc.getArtist().getName() + " [" + disc.getYear() + "] " + disc.getName() + " at " + disc.getLocation();
159              result.setSeverity(Severity.ERROR);
160              result.setMessage(message);
161              result.setToolTip(message);
162              LOG.warn(message);
163          } 
164       } catch (MusicTagException ex) {
165          message = Resources.getString("messages.ErrorWritingMusicTag") + ex.getMessage();
166          result.setMessage(message);
167          result.setSeverity(Severity.ERROR);
168          result.setToolTip(message);
169          LOG.error(message, ex);
170          HibernateUtil.rollbackTransaction();
171       } catch (InfrastructureException ex) {
172          message = Resources.getString("messages.ErrorDB") + ex.getMessage();
173          result.setMessage(message);
174          result.setSeverity(Severity.ERROR);
175          result.setToolTip(message);
176          LOG.error(message, ex);
177          HibernateUtil.rollbackTransaction();
178       } catch (Throwable ex) {
179          message = Resources.getString("messages.UnexpectedError") + ex.getMessage();
180          result.setMessage(message);
181          result.setSeverity(Severity.ERROR);
182          result.setToolTip(message);
183          LOG.error(message, ex);
184          HibernateUtil.rollbackTransaction();
185       }
186 
187       return result;
188    }
189 
190    /**
191     * Loops through an array of files and determines the largest one in bytes.
192     * <p>
193     * @param aDirectory the directory to look for the largest file in.
194     * @return the file found to be the largest
195     */
196    public static File findLargestImageFile(final File aDirectory) {
197       return findLargestImageFile(aDirectory, null);
198    }
199 
200    /**
201     * For a directory gets all of its music type files into a collection. If
202     * none are found return null, and set the validation message.
203     * <p>
204     * @param aDirectory the directory to scan
205     * @return a collection of files or NULL if not found
206     */
207    public static Collection findMusicFiles(final File aDirectory) {
208       if (LOG.isDebugEnabled()) {
209          LOG.debug("Finding music files in directory:" + aDirectory);
210       }
211       return findMusicFiles(aDirectory, null);
212    }
213 
214    /**
215     * Silently loads all tracks and one image file from the aDirectory. It must
216     * be a directory and if any exception is thrown all we do is return false to
217     * silently report the error.
218     * <p>
219     * @param aDirectory the directory to add
220     * @param aUpdateTags boolean whether to overwrite new tags or not
221     * @return true if successful, false if any error
222     */
223    @SuppressWarnings( { "unused", "unchecked" })
224    public static JukesValidationMessage loadDiscFromDirectory(final File aDirectory, final boolean aUpdateTags) {
225 	String sArtist = "";
226 	String sDisc = "";
227 	String sYear = "";
228 	File new_imageFile;
229 
230       if (aDirectory == null) {
231          throw new IllegalArgumentException("A directory must be selected");
232       }
233 
234       JukesValidationMessage result = new JukesValidationMessage(aDirectory.getAbsolutePath(), Severity.OK);
235       String message = null;
236       final File directory = aDirectory;
237 
238       // make sure it is a directory
239       if ((!directory.isDirectory()) || (!directory.exists())) {
240          throw new IllegalArgumentException("A directory must be selected");
241       }
242 
243       try {
244          if (LOG.isDebugEnabled()) {
245             LOG.debug("Loading music directory: " + directory);
246          }
247 
248          // first get all the music files in this dir
249          final Collection musicFiles = findMusicFiles(directory, result);
250          if ((musicFiles == null) || (musicFiles.isEmpty())) {
251             return result;
252          }
253 
254          // now get a list of all the image files for disc cover
255          final File imageFile = findLargestImageFile(aDirectory, result);
256          if (LOG.isDebugEnabled()) {
257             LOG.debug("Files found = " + musicFiles.size());
258             LOG.debug("Image selected = " + imageFile);
259          }
260 
261          final ArrayList tagList = new ArrayList();
262          MUSIC_LOOP: for (final Iterator iter = musicFiles.iterator(); iter.hasNext();) {
263             final File musicFile = (File) iter.next();
264             if (LOG.isDebugEnabled()) {
265                LOG.debug("Music File: " + musicFile);
266             }
267             // load the tag
268             final MusicTag tag = TagFactory.getTag(musicFile);
269             sArtist = tag.getArtist();
270             sDisc = tag.getDisc();
271             sYear = tag.getYear();
272             tagList.add(tag);
273          } // MUSIC_LOOP
274          
275   	      /** AZ 
276   	       Scale and copy images to user defined directory **/
277          if (imageFile != null) {
278          String imageLocation = ImageFactory.saveImageToUserDefinedDirectory(imageFile,
279 	              sArtist,
280 	              sDisc,
281 	              sYear);
282 
283 	        new_imageFile = new File(imageLocation);
284          }
285          else {
286              message = "No images found in " + aDirectory;
287              result.setMessage(message);
288              result.setSeverity(Severity.WARNING);
289              result.setToolTip(message);
290              LOG.warn(message);
291         	 new_imageFile = imageFile; 
292          }
293 
294          // sort the tags by track number
295          Collections.sort(tagList);
296 
297          result = createNewDisc(tagList.toArray(), new_imageFile, aDirectory, result, aUpdateTags);
298 
299       } catch (MusicTagException ex) {
300          message = "Error opening music file: " + ex.getMessage();
301          result.setSeverity(Severity.ERROR);
302          result.setToolTip(message);
303          LOG.error(message, ex);
304       } catch (Throwable ex) {
305          message = "Unexpected Error: " + ex.getMessage();
306          result.setSeverity(Severity.ERROR);
307          result.setToolTip(message);
308          LOG.error(message, ex);
309       }
310 
311       return result;
312    }
313 
314    /**
315     * Checks for the disc at the location on the hard drive. If that location no
316     * longer exists on the hard drive then this disc is removed from the
317     * database. This is useful for when using the Jukes with portable storage
318     * devices like Archos Jukebox. Thanks to Bill Farkas for suggesting this
319     * feature.
320     * @param aDisc the disc to check the hard drive for
321     * @return true if the disc was OK, false if the disc was removed
322     */
323    public static boolean removeDiscIfNoLongerExists(final Disc aDisc) {
324       boolean result = false;
325       final MainFrame mainFrame = (MainFrame) Application.getDefaultParentFrame();
326 
327       if (aDisc == null) {
328          throw new IllegalArgumentException("Disc may not be null");
329       }
330 
331       if (LOG.isDebugEnabled()) {
332          LOG.debug("Checking disc for validity: " + aDisc.getName());
333       }
334 
335       // get the file location for this disc
336       final File discDirectory = new File(aDisc.getLocation());
337 
338       // return true if the directory still exists
339       result = discDirectory.exists();
340 
341       // remove the directory if it no longer exists
342       try {
343          if (!result) {
344             // try to delete from database
345             HibernateUtil.beginTransaction();
346             final Artist artist = aDisc.getArtist();
347             artist.getDiscs().remove(aDisc);
348             HibernateDao.delete(aDisc);
349             HibernateUtil.commitTransaction();
350          }
351       } catch (InfrastructureException ex) {
352          final String errorMessage = ResourceUtils.getString("messages.ErrorDeletingDisc");
353          LOG.error(errorMessage, ex);
354          MessageUtil.showError(mainFrame, errorMessage); //AZ
355          HibernateUtil.rollbackTransaction();
356          result = false;
357       }
358 
359       return result;
360    }
361 
362    /**
363     * Loops through an array of files and determines the largest one in bytes.
364     * <p>
365     * @param aDirectory the directory to look for the largest file in.
366     * @param aMessage a JukesValidationMessage to contain any errors, may be
367     *           null
368     * @return the file found to be the largest
369     */
370    private static File findLargestImageFile(final File aDirectory, final JukesValidationMessage aMessage) {
371       if (LOG.isDebugEnabled()) {
372          LOG.debug("Finding largest image in directory: " + aDirectory);
373       }
374 
375       File result = null;
376       final Collection imageFiles = FileUtils.listFiles(aDirectory, FilterFactory.IMAGE_FILTER, null);
377 
378       // get the largest of the images if there are more than one
379       if ((imageFiles != null) && (!imageFiles.isEmpty())) {
380          LOG.debug("Images found in directory");
381          final Object[] files = (Object[]) imageFiles.toArray();
382 
383          // if only one image then just return that image
384          if (imageFiles.size() == 1) {
385             result = (File) files[0];
386          } else {
387             // need to pick the largest file for the biggest resolution cover
388             long filesize = 0;
389             /** AZ - find "cover.*" and "folder.*" files
390              * 
391              */ File coverFile = null;
392             for (int i = 0; i < files.length; i++) {
393                final File file = (File) files[i];
394                final long size = file.length();
395                if (size > filesize) {
396                   filesize = size;
397                   result = file;
398                }
399                final String shortName;
400                if (file.getName().lastIndexOf(".") != -1) {
401             	   shortName = file.getName().substring(0,file.getName().lastIndexOf(".")).toUpperCase();
402                } else {
403             	   shortName = file.getName().toUpperCase();
404                }
405                if ((shortName.equals("COVER") ||
406             	   shortName.equals("FOLDER") ||
407             	   shortName.equals("FRONT") ) &
408             	   (coverFile == null ) ) {
409             	   coverFile = file;
410             	   }
411             }
412      	   if (coverFile != null) {
413      		   result = coverFile;
414       	   }
415      	   else {
416      		  if (aMessage != null) {
417                   LOG.info("More than one image found.");
418                   aMessage.setSeverity(Severity.WARNING);
419                   aMessage.setToolTip("Directory contained more than one image so largest was selected. ");
420                }
421      	   }
422          }
423       }     
424       return result;
425    }
426 
427    /**
428     * For a directory gets all of its music type files into a collection. If
429     * none are found return null, and set the validation message.
430     * <p>
431     * @param aDirectory the directory to scan
432     * @param aMessage the validation message to fill
433     * @return a collection of files or NULL if not found
434     */
435    private static Collection findMusicFiles(final File aDirectory, final JukesValidationMessage aMessage) {
436       // first get all the music files in this dir
437       final Collection results = FileUtils.listFiles(aDirectory, FilterFactory.MUSIC_FILTER, null);
438 
439       // if collection is null just return true
440       if (((results == null) || (results.isEmpty())) && (aMessage != null)) {
441          final String message = "Directory contained no music files. " + aDirectory;
442          LOG.warn(message);
443          aMessage.setSeverity(Severity.WARNING);
444          aMessage.setToolTip(message);
445       }
446 
447       return results;
448    }
449 }