View Javadoc

1   /***
2    *     Ambient - A music player for the Android platform
3    Copyright (C) 2007 Martin Vysny
4    
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU General Public License as published by
7    the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9    
10   This program is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14  
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17   */
18  package sk.baka.ambient.library;
19  
20  import java.io.File;
21  import java.io.FileFilter;
22  import java.io.FilenameFilter;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.regex.Pattern;
28  
29  import sk.baka.ambient.AmbientApplication;
30  import sk.baka.ambient.R;
31  import sk.baka.ambient.collection.TrackMetadataBean;
32  import sk.baka.ambient.collection.TrackOriginEnum;
33  import sk.baka.ambient.collection.TrackMetadataBean.Builder;
34  import sk.baka.ambient.commons.MiscUtils;
35  import android.content.ContentValues;
36  import android.database.Cursor;
37  
38  import com.es.skreemr.SkreemRSong;
39  
40  import entagged.audioformats.AudioFile;
41  import entagged.audioformats.AudioFileIO;
42  import entagged.audioformats.Tag;
43  import entagged.audioformats.exceptions.CannotReadException;
44  
45  /***
46   * Library utility methods.
47   * 
48   * @author Martin Vysny
49   */
50  public final class LibraryUtils {
51  	private LibraryUtils() {
52  		// prevent instantiation
53  	}
54  
55  	/***
56  	 * All column names in the <code>tracks</code> table.
57  	 */
58  	public static final String[] COLUMN_NAMES = new String[] { "id", "origin",
59  			"title", "artist", "composer", "album", "genre", "trackNumber",
60  			"location", "length", "bitrate", "fileSize", "yearReleased",
61  			"frequency", "buy_url", "license", "artist_url", "artist_desc" };
62  
63  	/***
64  	 * All {@link #COLUMN_NAMES columns} separated by commas.
65  	 */
66  	public static final String COLUMNS;
67  
68  	static {
69  		final StringBuilder b = new StringBuilder();
70  		boolean first = true;
71  		for (final String column : COLUMN_NAMES) {
72  			if (first) {
73  				first = false;
74  			} else {
75  				b.append(',');
76  			}
77  			b.append("tracks.");
78  			b.append(column);
79  		}
80  		COLUMNS = b.toString();
81  	}
82  
83  	/***
84  	 * Converts the sqlite3 {@link ContentValues} to the tag bean.
85  	 * 
86  	 * @param cursor
87  	 *            the content values object, must not be <code>null</code>.
88  	 *            Expects columns in {@link #COLUMN_NAMES} ordering.
89  	 * @return the bean instance, never <code>null</code>
90  	 */
91  	public static TrackMetadataBean cursorToTrackBean(final Cursor cursor) {
92  		final long id = cursor.getInt(0);
93  		final Builder track = TrackMetadataBean.newBuilder();
94  		track.setOrigin(TrackOriginEnum.fromDb(cursor.getInt(1))).setTitle(
95  				cursor.getString(2)).setArtist(cursor.getString(3));
96  		track.composer = cursor.getString(4);
97  		track.album = cursor.getString(5);
98  		track.genre = cursor.getString(6);
99  		track.trackNumber = cursor.getString(7);
100 		track.location = cursor.getString(8);
101 		track.length = cursor.getInt(9);
102 		track.bitrate = cursor.getInt(10);
103 		track.fileSize = cursor.getLong(11);
104 		track.yearReleased = cursor.getString(12);
105 		track.frequency = cursor.getInt(13);
106 		track.buyURL = cursor.getString(14);
107 		track.license = cursor.getString(15);
108 		track.artistURL = cursor.getString(16);
109 		track.artistDesc = cursor.getString(17);
110 		return track.build(id);
111 	}
112 
113 	private static final Pattern time = Pattern
114 			.compile("[0-9]{2}:[0-9]{2}:[0-9]{2}");
115 
116 	/***
117 	 * Converts the {@link SkreemRSong} to the tag bean.
118 	 * 
119 	 * @param song
120 	 *            the song.
121 	 * @return the bean instance, never <code>null</code>
122 	 */
123 	public static TrackMetadataBean toTrackBean(final SkreemRSong song) {
124 		final Builder track = TrackMetadataBean.newBuilder();
125 		if (time.matcher(song.getDuration()).matches()) {
126 			try {
127 				track.length = Integer.valueOf(song.getDuration().substring(0,
128 						2))
129 						* 3600
130 						+ Integer.valueOf(song.getDuration().substring(3, 5))
131 						* 60
132 						+ Integer.valueOf(song.getDuration().substring(6, 8));
133 			} catch (NumberFormatException ex) {
134 				track.length = 0;
135 			}
136 		}
137 		track.genre = fixId3Genre(MiscUtils.nullIfEmptyOrWhitespace(song
138 				.getGenre()));
139 		try {
140 			track.bitrate = Integer.parseInt(song.getBitrate()) / 1000;
141 		} catch (NumberFormatException ex) {
142 			track.bitrate = 0;
143 		}
144 		try {
145 			track.frequency = Integer.parseInt(song.getFrequency());
146 		} catch (NumberFormatException ex) {
147 			track.frequency = 0;
148 		}
149 		track.origin = TrackOriginEnum.SkreemR;
150 		track.title = song.getSong();
151 		track.artist = song.getArtist();
152 		track.album = song.getAlbum();
153 		track.location = song.getUrl();
154 		track.yearReleased = song.getYear();
155 		track.fileSize = -1;
156 		final TrackMetadataBean result = track.build(-1);
157 		return result;
158 	}
159 
160 	/***
161 	 * Converts a list of {@link SkreemRSong} to a list of tag bean.
162 	 * 
163 	 * @param songs
164 	 *            the song list.
165 	 * @return the bean instance, never <code>null</code>
166 	 */
167 	public static List<TrackMetadataBean> toTrackBean(
168 			final List<SkreemRSong> songs) {
169 		final List<TrackMetadataBean> result = new ArrayList<TrackMetadataBean>(
170 				songs.size());
171 		for (final SkreemRSong song : songs) {
172 			result.add(toTrackBean(song));
173 		}
174 		return result;
175 	}
176 
177 	/***
178 	 * Converts the track to {@link ContentValues} suitable for database insert.
179 	 * 
180 	 * @param track
181 	 *            the track to convert.
182 	 * @return values
183 	 */
184 	public static ContentValues trackBeanToValues(final TrackMetadataBean track) {
185 		final ContentValues values = new ContentValues();
186 		values.put("title", track.getTitle());
187 		values.put("origin", track.getOrigin().ordinal());
188 		values.put("artist", track.getArtist());
189 		values.put("composer", track.getComposer());
190 		values.put("album", track.getAlbum());
191 		values.put("genre", track.getGenre());
192 		values.put("trackNumber", track.getTrackNumber());
193 		values.put("location", track.getLocation());
194 		values.put("length", track.getLength());
195 		values.put("bitrate", track.getBitrate());
196 		values.put("fileSize", track.getFileSize());
197 		values.put("yearReleased", track.getYearReleased());
198 		values.put("frequency", track.getFrequency());
199 		values.put("buy_url", track.getBuyURL());
200 		values.put("license", track.getLicense());
201 		values.put("artist_url", track.getArtistURL());
202 		values.put("artist_desc", track.getArtistDesc());
203 		return values;
204 	}
205 
206 	/***
207 	 * Retrieves a music file tag from given file name.
208 	 * 
209 	 * @param filename
210 	 *            the file to retrieve tags from.
211 	 * @return bean instance, never <code>null</code>.
212 	 */
213 	public static TrackMetadataBean getTag(final String filename) {
214 		if (TRACK_META_NAME_FILTER.accept(null, filename)) {
215 			// try to read the tag
216 			try {
217 				final AudioFile f = AudioFileIO.read(new File(filename));
218 				final Tag tag = f.getTag();
219 				final Builder track = TrackMetadataBean.newBuilder();
220 				track.trackNumber = MiscUtils.nullIfEmptyOrWhitespace(tag
221 						.getFirstTrack());
222 				if (track.trackNumber != null) {
223 					// strip anything after slash, including the slash
224 					final int slash = track.trackNumber.indexOf('/');
225 					if (slash >= 0)
226 						track.trackNumber = track.trackNumber.substring(0,
227 								slash);
228 				}
229 				track.title = MiscUtils.nullIfEmptyOrWhitespace(tag
230 						.getFirstTitle());
231 				track.artist = MiscUtils.fixArtistAlbumName(MiscUtils
232 						.nullIfEmptyOrWhitespace(tag.getFirstArtist()));
233 				track.album = MiscUtils.fixArtistAlbumName(MiscUtils
234 						.nullIfEmptyOrWhitespace(tag.getFirstAlbum()));
235 				track.genre = fixId3Genre(MiscUtils.nullIfEmptyOrWhitespace(tag
236 						.getFirstGenre()));
237 				track.yearReleased = MiscUtils.nullIfEmptyOrWhitespace(tag
238 						.getFirstYear());
239 				track.frequency = f.getSamplingRate();
240 				track.origin = TrackOriginEnum.LocalFs;
241 				track.length = f.getLength();
242 				track.bitrate = f.getBitrate();
243 				track.fileSize = new File(filename).length();
244 				track.location = filename;
245 				return track.build(-1);
246 			} catch (final CannotReadException e) {
247 				final String text = MiscUtils.format(R.string.errTagRead,
248 						filename);
249 				AmbientApplication.getInstance().error(LibraryUtils.class,
250 						true, text, e);
251 			}
252 		}
253 		// not a known file, construct a simple tag bean
254 		final Builder track = TrackMetadataBean.newBuilder();
255 		track.origin = TrackOriginEnum.LocalFs;
256 		track.location = filename;
257 		track.fileSize = new File(filename).length();
258 		return track.build(-1);
259 	}
260 
261 	/***
262 	 * If the genre is in the form of <code>(number)</code>, the string is
263 	 * replaced by appropriate genre name.
264 	 * 
265 	 * @param genre
266 	 *            the genre to check
267 	 * @return fixed genre string representation.
268 	 */
269 	public static String fixId3Genre(final String genre) {
270 		if (genre == null)
271 			return null;
272 		if (!genre.startsWith("(") || !genre.endsWith(")"))
273 			return genre;
274 		try {
275 			final int genreIndex = Integer.parseInt(genre.substring(1, genre
276 					.length() - 1));
277 			if ((genreIndex < 0) || (genreIndex >= Tag.DEFAULT_GENRES.length))
278 				return genre;
279 			return Tag.DEFAULT_GENRES[genreIndex];
280 		} catch (NumberFormatException ex) {
281 			return genre;
282 		}
283 	}
284 
285 	/***
286 	 * Polls all tracks from given cursor. The cursor is closed afterwards.
287 	 * Usable for example on the
288 	 * {@link DBStrategy#findByCriteria(java.util.Map, boolean, String)}
289 	 * method output.
290 	 * 
291 	 * @param c
292 	 *            the cursor
293 	 * @return list of track meta beans, never <code>null</code>, may be empty.
294 	 */
295 	@SuppressWarnings("unchecked")
296 	public static List<TrackMetadataBean> pollTracks(final Cursor c) {
297 		if (!c.moveToFirst()) {
298 			c.close();
299 			return Collections.EMPTY_LIST;
300 		}
301 		final List<TrackMetadataBean> result = new ArrayList<TrackMetadataBean>();
302 		try {
303 			do {
304 				if (Thread.currentThread().isInterrupted()) {
305 					throw new RuntimeException("interrupted");
306 				}
307 				final TrackMetadataBean track = cursorToTrackBean(c);
308 				result.add(track);
309 			} while (c.moveToNext());
310 		} finally {
311 			c.close();
312 		}
313 		return result;
314 	}
315 
316 	/***
317 	 * Polls all tracks from given cursor. The cursor is closed afterwards.
318 	 * Usable for example on the
319 	 * {@link DBStrategy#getCriteriaList(sk.baka.ambient.collection.CategoryEnum[], java.util.EnumMap)}
320 	 * method result.
321 	 * 
322 	 * @param c
323 	 *            the cursor
324 	 * @param separator
325 	 *            if multiple columns are required, this character separates
326 	 *            them. In case of a single column, this parameter is ignored.
327 	 * @param column
328 	 *            put values of this column into the <code>columnValues</code>
329 	 *            list. Ignored when <code>columnValues</code> is
330 	 *            <code>null</code>.
331 	 * @param columnValues
332 	 *            put values from <code>column</code> here.
333 	 * @param useAsNull
334 	 *            if not <code>null</code> then this value is used instead of
335 	 *            <code>null</code> value.
336 	 * @return values of first selected column, never <code>null</code>, may be
337 	 *         empty.
338 	 */
339 	@SuppressWarnings("unchecked")
340 	public static List<String> pollStrings(final Cursor c,
341 			final char separator, final int column,
342 			final List<String> columnValues, final String... useAsNull) {
343 		if (!c.moveToFirst()) {
344 			c.close();
345 			return Collections.EMPTY_LIST;
346 		}
347 		final List<String> result = new ArrayList<String>();
348 		final StringBuilder b = new StringBuilder();
349 		do {
350 			b.delete(0, b.length());
351 			for (int i = 0; i < useAsNull.length; i++) {
352 				if (i != 0) {
353 					b.append(separator);
354 				}
355 				String val = c.getString(i);
356 				if ((columnValues != null) && (i == column)) {
357 					columnValues.add(val);
358 				}
359 				if (val == null)
360 					val = useAsNull[i];
361 				b.append(val);
362 			}
363 			result.add(b.toString());
364 		} while (c.moveToNext());
365 		c.close();
366 		return result;
367 	}
368 
369 	/***
370 	 * Set of supported extensions of track files, lower-case.
371 	 */
372 	public static final List<String> trackExtensions = Collections
373 			.unmodifiableList(Arrays.asList(new String[] { ".mp3", ".m4a",
374 					".amr", ".wma", ".mid", ".smf", ".wav", ".ogg" }));
375 
376 	/***
377 	 * MIME types for {@link #trackExtensions}.
378 	 */
379 	public static final List<String> trackMime = Collections
380 			.unmodifiableList(Arrays.asList(new String[] { "audio/mpeg",
381 					"audio/mp4", "audio/AMR", "audio/x-ms-wma", "audio/mid",
382 					"audio/mid", "audio/wav", "application/ogg" }));
383 
384 	/***
385 	 * Returns MIME type for given file. Detects only audio file types.
386 	 * 
387 	 * @param fileName
388 	 *            the file name.
389 	 * @return the MIME type or "application/octet-stream" if unknown.
390 	 */
391 	public static String getMime(final String fileName) {
392 		for (int i = 0; i < trackExtensions.size(); i++) {
393 			if (fileName.toLowerCase().endsWith(trackExtensions.get(i))) {
394 				return trackMime.get(i);
395 			}
396 		}
397 		return "application/octet-stream";
398 	}
399 
400 	/***
401 	 * Set of supported extensions of track files, lower-case, which is the
402 	 * Entagged library able to process.
403 	 */
404 	public static final List<String> trackMetaExtensions = Collections
405 			.unmodifiableList(Arrays.asList(new String[] { ".mp3", ".ogg",
406 					".flac", ".wav", ".mpc", ".mp+", ".ape", ".wma" }));
407 
408 	/***
409 	 * Set of supported extensions of playlist files, lower-case.
410 	 */
411 	public static final List<String> playlistExtensions = Collections
412 			.unmodifiableList(Arrays.asList(new String[] { ".m3u", ".m3u8",
413 					".pls", ".wpl", ".xspf" }));
414 
415 	/***
416 	 * Set of supported extensions of music (track and playlist) files,
417 	 * lower-case.
418 	 */
419 	public static final List<String> musicExtensions = new ArrayList<String>(
420 			trackExtensions);
421 	static {
422 		musicExtensions.addAll(playlistExtensions);
423 	}
424 
425 	private final static class ExtensionFilter implements FilenameFilter {
426 		private final List<String> extensions;
427 
428 		/***
429 		 * Creates new filter.
430 		 * @param extensions extends to accept.
431 		 */
432 		public ExtensionFilter(final List<String> extensions) {
433 			this.extensions = extensions;
434 		}
435 
436 		public boolean accept(File dir, String filename) {
437 			final String name = filename.toLowerCase();
438 			for (final String ext : extensions) {
439 				if (name.toLowerCase().endsWith(ext))
440 					return true;
441 			}
442 			return false;
443 		}
444 	}
445 
446 	private final static class FilenameFilterWrapper implements FileFilter {
447 		private final ExtensionFilter extensions;
448 
449 		/***
450 		 * Creates new wrapper.
451 		 * @param extensions wrap this filter.
452 		 */
453 		public FilenameFilterWrapper(final ExtensionFilter extensions) {
454 			this.extensions = extensions;
455 		}
456 
457 		public boolean accept(File pathname) {
458 			if (pathname.isDirectory()) {
459 				return true;
460 			}
461 			return extensions.accept(null, pathname.getName());
462 		}
463 	}
464 
465 	/***
466 	 * Filters out all files except track files.
467 	 */
468 	public final static FilenameFilter TRACK_NAME_FILTER = new ExtensionFilter(
469 			trackExtensions);
470 
471 	/***
472 	 * Filters out all files except track files which the Entagged library
473 	 * supports.
474 	 */
475 	public final static FilenameFilter TRACK_META_NAME_FILTER = new ExtensionFilter(
476 			trackMetaExtensions);
477 
478 	/***
479 	 * Filters out all files except directories and track files.
480 	 */
481 	public final static FileFilter TRACK_FILTER = new FilenameFilterWrapper(
482 			new ExtensionFilter(trackExtensions));
483 
484 	/***
485 	 * Filters out all files except playlist files.
486 	 */
487 	public final static FilenameFilter PLAYLIST_NAME_FILTER = new ExtensionFilter(
488 			playlistExtensions);
489 
490 	/***
491 	 * Filters out all files except directories and playlist files.
492 	 */
493 	public final static FileFilter PLAYLIST_FILTER = new FilenameFilterWrapper(
494 			new ExtensionFilter(playlistExtensions));
495 
496 	/***
497 	 * Filters out all files except playlist and track files.
498 	 */
499 	public final static FilenameFilter MUSIC_NAME_FILTER = new ExtensionFilter(
500 			musicExtensions);
501 
502 	/***
503 	 * Filters out all files except directories, playlist and track files.
504 	 */
505 	public final static FileFilter MUSIC_FILTER = new FilenameFilterWrapper(
506 			new ExtensionFilter(musicExtensions));
507 }