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
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
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
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
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 }