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.util.ResourceUtils;
17  import com.jgoodies.validation.Severity;
18  import com.melloware.jukes.db.HibernateDao;
19  import com.melloware.jukes.db.HibernateUtil;
20  import com.melloware.jukes.db.orm.Artist;
21  import com.melloware.jukes.db.orm.Disc;
22  import com.melloware.jukes.db.orm.Track;
23  import com.melloware.jukes.exception.InfrastructureException;
24  import com.melloware.jukes.exception.MusicTagException;
25  import com.melloware.jukes.file.filter.FilterFactory;
26  import com.melloware.jukes.file.tag.MusicTag;
27  import com.melloware.jukes.file.tag.TagFactory;
28  import com.melloware.jukes.util.JukesValidationMessage;
29  import com.melloware.jukes.util.TimeSpan;
30  
31  /**
32   * This class is used for working with music file directories. Functions said as
33   * loading a directory into the database, updating ID3 tags, etc.
34   * <p>
35   * Copyright (c) 2006 Melloware, Inc. <http://www.melloware.com>
36   * @author Emil A. Lefkof III <info@melloware.com>
37   * @version 4.0
38   * @see MusicTag
39   */
40  public final class MusicDirectory {
41  
42     private static final Log LOG = LogFactory.getLog(MusicDirectory.class);
43  
44     /**
45      * Private constructor for no instantiation.
46      */
47     private MusicDirectory() {
48        super();
49     }
50  
51     /**
52      * Creates a new disc in the catalog and optionally updates its ID3 tags.
53      * <p>
54      * @param aTags the array of MusicTag objects
55      * @param aCoverImage the cover image
56      * @param aDirectory the directory where these files are located
57      * @param aUpdateTags true to update tags, false to not modify them
58      * @return a validation message containing the result
59      */
60     @SuppressWarnings("unused")
61     public static JukesValidationMessage createNewDisc(final Object[] aTags, final File aCoverImage,
62              final File aDirectory, final JukesValidationMessage aMessage, final boolean aUpdateTags) {
63        JukesValidationMessage result = null;
64        if (aMessage == null) {
65           result = new JukesValidationMessage("Disc created successfully", Severity.OK);
66        } else {
67           result = aMessage;
68        }
69        String message = null;
70        try {
71           HibernateUtil.beginTransaction();
72           Artist artist = null;
73           Disc disc = null;
74           long totalDuration = 0;
75           final int padding = ((aTags.length >= 100) ? 3 : 2);
76           MUSIC_LOOP: for (int i = 0; i < aTags.length; i++) {
77              final MusicTag musicFile = (MusicTag) aTags[i];
78  
79              // find or create the artist
80              if (artist == null) {
81                 final String artistName = musicFile.getArtist();
82                 final String resource = ResourceUtils.getString("hql.artist.find");
83                 final String hql = MessageFormat.format(resource,
84                          new Object[] { StringEscapeUtils.escapeSql(artistName) });
85                 artist = (Artist) HibernateDao.findUniqueByQuery(hql);
86                 if (artist == null) {
87                    artist = new Artist();
88                    artist.setName(artistName);
89                 }
90                 musicFile.setArtist(artistName);
91              } else {
92                 musicFile.setArtist(artist.getName());
93              }
94  
95              // find or create the disc
96              if (disc == null) {
97                 final String discName = musicFile.getDisc();
98                 final String resource = ResourceUtils.getString("hql.disc.find");
99                 final String hql = MessageFormat.format(resource, new Object[] {
100                         StringEscapeUtils.escapeSql(artist.getName()), StringEscapeUtils.escapeSql(discName) });
101                disc = (Disc) HibernateDao.findUniqueByQuery(hql);
102                if (disc == null) {
103                   disc = new Disc();
104                   disc.setArtist(artist);
105                   disc.setName(discName);
106                   artist.addDisc(disc);
107                }
108                musicFile.setDisc(discName);
109                disc.setGenre(musicFile.getGenre());
110                disc.setYear(musicFile.getYear());
111                disc.setBitrate(musicFile.getBitRate());
112                if (aCoverImage != null) {
113                   disc.setCoverUrl(aCoverImage.getAbsolutePath());
114                   disc.setCoverSize(aCoverImage.length());
115                }
116                disc.setCreatedDate(new Date(aDirectory.lastModified()));
117                disc.setLocation(aDirectory.getAbsolutePath());
118                // clear out the old tracks
119                HibernateDao.deleteAll(disc.getTracks());
120             } else {
121                musicFile.setDisc(disc.getName());
122             }
123 
124             // now create the track
125             final Track track = new Track();
126             disc.addTrack(track);
127             track.setBitrate(musicFile.getBitRate());
128             track.setDuration(musicFile.getTrackLength());
129             track.setDurationTime(musicFile.getTrackLengthAsString());
130             track.setName(musicFile.getTitle());
131             track.setComment(musicFile.getComment());
132             track.setTrackUrl(musicFile.getAbsolutePath());
133             track.setTrackSize(musicFile.getFile().length());
134             musicFile.setTrack(Integer.toString(i + 1), padding);
135             track.setTrackNumber(musicFile.getTrack());
136             track.setCreatedDate(new Date(musicFile.getFile().lastModified()));
137             totalDuration = totalDuration + musicFile.getTrackLength();
138 
139             // save the tag back out if the flag is set
140             if (aUpdateTags) {
141                musicFile.save();
142             }
143          } // MUSIC_LOOP_LOOP
144 
145          // update the discs total duration and time
146          disc.setDuration(totalDuration);
147          final TimeSpan timespan = new TimeSpan(totalDuration * 1000);
148          disc.setDurationTime(timespan.getMusicDuration());
149 
150          // commit changes
151          HibernateDao.saveOrUpdate(artist);
152          HibernateUtil.commitTransaction();
153       } catch (MusicTagException ex) {
154          message = "Error writing music tag: " + ex.getMessage();
155          result.setMessage(message);
156          result.setSeverity(Severity.ERROR);
157          result.setToolTip(message);
158          LOG.error(message, ex);
159          HibernateUtil.rollbackTransaction();
160       } catch (InfrastructureException ex) {
161          message = "Error persisting to the database: " + ex.getMessage();
162          result.setMessage(message);
163          result.setSeverity(Severity.ERROR);
164          result.setToolTip(message);
165          LOG.error(message, ex);
166          HibernateUtil.rollbackTransaction();
167       } catch (Throwable ex) {
168          message = "Unexpected Error: " + ex.getMessage();
169          result.setMessage(message);
170          result.setSeverity(Severity.ERROR);
171          result.setToolTip(message);
172          LOG.error(message, ex);
173          HibernateUtil.rollbackTransaction();
174       }
175 
176       return result;
177    }
178 
179    /**
180     * Loops through an array of files and determines the largest one in bytes.
181     * <p>
182     * @param aDirectory the directory to look for the largest file in.
183     * @return the file found to be the largest
184     */
185    public static File findLargestImageFile(final File aDirectory) {
186       return findLargestImageFile(aDirectory, null);
187    }
188 
189    /**
190     * For a directory gets all of its music type files into a collection. If
191     * none are found return null, and set the validation message.
192     * <p>
193     * @param aDirectory the directory to scan
194     * @return a collection of files or NULL if not found
195     */
196    public static Collection findMusicFiles(final File aDirectory) {
197       if (LOG.isDebugEnabled()) {
198          LOG.debug("Finding music files in directory:" + aDirectory);
199       }
200       return findMusicFiles(aDirectory, null);
201    }
202 
203    /**
204     * Silently loads all tracks and one image file from the aDirectory. It must
205     * be a directory and if any exception is thrown all we do is return false to
206     * silently report the error.
207     * <p>
208     * @param aDirectory the directory to add
209     * @param aUpdateTags boolean whether to overwrite new tags or not
210     * @return true if successful, false if any error
211     */
212    @SuppressWarnings( { "unused", "unchecked" })
213    public static JukesValidationMessage loadDiscFromDirectory(final File aDirectory, final boolean aUpdateTags) {
214       if (aDirectory == null) {
215          throw new IllegalArgumentException("A directory must be selected");
216       }
217 
218       JukesValidationMessage result = new JukesValidationMessage(aDirectory.getAbsolutePath(), Severity.OK);
219       String message = null;
220       final File directory = aDirectory;
221 
222       // make sure it is a directory
223       if ((!directory.isDirectory()) || (!directory.exists())) {
224          throw new IllegalArgumentException("A directory must be selected");
225       }
226 
227       try {
228          if (LOG.isDebugEnabled()) {
229             LOG.debug("Loading music directory: " + directory);
230          }
231 
232          // first get all the music files in this dir
233          final Collection musicFiles = findMusicFiles(directory, result);
234          if ((musicFiles == null) || (musicFiles.isEmpty())) {
235             return result;
236          }
237 
238          // now get a list of all the image files for disc cover
239          final File imageFile = findLargestImageFile(aDirectory, result);
240 
241          if (LOG.isDebugEnabled()) {
242             LOG.debug("Files found = " + musicFiles.size());
243             LOG.debug("Image selected = " + imageFile);
244          }
245 
246          final ArrayList tagList = new ArrayList();
247          MUSIC_LOOP: for (final Iterator iter = musicFiles.iterator(); iter.hasNext();) {
248             final File musicFile = (File) iter.next();
249             if (LOG.isDebugEnabled()) {
250                LOG.debug("Music File: " + musicFile);
251             }
252             // load the tag
253             final MusicTag tag = TagFactory.getTag(musicFile);
254             tagList.add(tag);
255          } // MUSIC_LOOP
256          
257          // sort the tags by track number
258          Collections.sort(tagList);
259 
260          result = createNewDisc(tagList.toArray(), imageFile, aDirectory, result, aUpdateTags);
261 
262       } catch (MusicTagException ex) {
263          message = "Error opening music file: " + ex.getMessage();
264          result.setSeverity(Severity.ERROR);
265          result.setToolTip(message);
266          LOG.error(message, ex);
267       } catch (Throwable ex) {
268          message = "Unexpected Error: " + ex.getMessage();
269          result.setSeverity(Severity.ERROR);
270          result.setToolTip(message);
271          LOG.error(message, ex);
272       }
273 
274       return result;
275    }
276 
277    /**
278     * Checks for the disc at the location on the hard drive. If that location no
279     * longer exists on the hard drive then this disc is removed from the
280     * database. This is useful for when using the Jukes with portable storage
281     * devices like Archos Jukebox. Thanks to Bill Farkas for suggesting this
282     * feature.
283     * @param aDisc the disc to check the hard drive for
284     * @return true if the disc was OK, false if the disc was removed
285     */
286    public static boolean removeDiscIfNoLongerExists(final Disc aDisc) {
287       boolean result = false;
288 
289       if (aDisc == null) {
290          throw new IllegalArgumentException("Disc may not be null");
291       }
292 
293       if (LOG.isDebugEnabled()) {
294          LOG.debug("Checking disc for validity: " + aDisc.getName());
295       }
296 
297       // get the file location for this disc
298       final File discDirectory = new File(aDisc.getLocation());
299 
300       // return true if the directory still exists
301       result = discDirectory.exists();
302 
303       // remove the directory if it no longer exists
304       try {
305          if (!result) {
306             // try to delete from database
307             HibernateUtil.beginTransaction();
308             final Artist artist = aDisc.getArtist();
309             artist.getDiscs().remove(aDisc);
310             HibernateDao.delete(aDisc);
311             HibernateUtil.commitTransaction();
312          }
313       } catch (InfrastructureException ex) {
314          LOG.error("Error deleting disc from database.", ex);
315          HibernateUtil.rollbackTransaction();
316          result = false;
317       }
318 
319       return result;
320    }
321 
322    /**
323     * Loops through an array of files and determines the largest one in bytes.
324     * <p>
325     * @param aDirectory the directory to look for the largest file in.
326     * @param aMessage a JukesValidationMessage to contain any errors, may be
327     *           null
328     * @return the file found to be the largest
329     */
330    private static File findLargestImageFile(final File aDirectory, final JukesValidationMessage aMessage) {
331       if (LOG.isDebugEnabled()) {
332          LOG.debug("Finding largest image in directory: " + aDirectory);
333       }
334 
335       File result = null;
336       final Collection imageFiles = FileUtils.listFiles(aDirectory, FilterFactory.IMAGE_FILTER, null);
337 
338       // get the largest of the images if there are more than one
339       if ((imageFiles != null) && (!imageFiles.isEmpty())) {
340          LOG.debug("Images found in directory");
341          final Object[] files = (Object[]) imageFiles.toArray();
342 
343          // if only one image then just return that image
344          if (imageFiles.size() == 1) {
345             result = (File) files[0];
346          } else {
347             if (aMessage != null) {
348                LOG.info("More than one image found.");
349                aMessage.setSeverity(Severity.WARNING);
350                aMessage.setToolTip("Directory contained more than one image so largest was selected. ");
351             }
352 
353             // need to pick the largest file for the biggest resolution cover
354             long filesize = 0;
355             for (int i = 0; i < files.length; i++) {
356                final File file = (File) files[i];
357                final long size = file.length();
358                if (size > filesize) {
359                   filesize = size;
360                   result = file;
361                }
362             }
363          }
364       }
365       return result;
366    }
367 
368    /**
369     * For a directory gets all of its music type files into a collection. If
370     * none are found return null, and set the validation message.
371     * <p>
372     * @param aDirectory the directory to scan
373     * @param aMessage the validation message to fill
374     * @return a collection of files or NULL if not found
375     */
376    private static Collection findMusicFiles(final File aDirectory, final JukesValidationMessage aMessage) {
377       // first get all the music files in this dir
378       final Collection results = FileUtils.listFiles(aDirectory, FilterFactory.MUSIC_FILTER, null);
379 
380       // if collection is null just return true
381       if (((results == null) || (results.isEmpty())) && (aMessage != null)) {
382          final String message = "Directory contained no music files. " + aDirectory;
383          LOG.warn(message);
384          aMessage.setSeverity(Severity.WARNING);
385          aMessage.setToolTip(message);
386       }
387 
388       return results;
389    }
390 }