/* * Copyright 2008-2013, ETH Zürich, Samuel Welten, Michael Kuhn, Tobias Langner, * Sandro Affentranger, Lukas Bossard, Michael Grob, Rahul Jain, * Dominic Langenegger, Sonia Mayor Alonso, Roger Odermatt, Tobias Schlueter, * Yannick Stucki, Sebastian Wendland, Samuel Zehnder, Samuel Zihlmann, * Samuel Zweifel * * This file is part of Jukefox. * * Jukefox is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or any later version. Jukefox is * distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * Jukefox. If not, see <http://www.gnu.org/licenses/>. */ package ch.ethz.dcg.jukefox.manager.libraryimport; import java.io.File; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import android.content.ContentResolver; import android.database.Cursor; import android.net.Uri; import android.provider.MediaStore; import ch.ethz.dcg.jukefox.commons.AndroidConstants; import ch.ethz.dcg.jukefox.commons.DataUnavailableException; import ch.ethz.dcg.jukefox.commons.DataWriteException; import ch.ethz.dcg.jukefox.commons.utils.AndroidUtils; import ch.ethz.dcg.jukefox.commons.utils.JoinableThread; import ch.ethz.dcg.jukefox.commons.utils.Log; import ch.ethz.dcg.jukefox.model.AbstractCollectionModelManager; import ch.ethz.dcg.jukefox.model.libraryimport.ContentProviderId; import ch.ethz.dcg.jukefox.model.libraryimport.ContentProviderId.Type; import ch.ethz.dcg.jukefox.model.libraryimport.ImportAlbum; import ch.ethz.dcg.jukefox.model.libraryimport.ImportSong; import ch.ethz.dcg.jukefox.model.libraryimport.ImportState; import ch.ethz.dcg.pancho3.model.JukefoxApplication; import entagged.audioformats.AudioFile; import entagged.audioformats.AudioFileIO; import entagged.audioformats.Tag; import entagged.audioformats.mp3.util.id3frames.TextId3Frame; public class AndroidLibraryScanner extends AbstractLibraryScanner { private final static String TAG = AndroidLibraryScanner.class.getSimpleName(); private HashSet<String> readUsingTagLibrary; private HashMap<String, String> albumArtistMapping; private JukefoxApplication application; public AndroidLibraryScanner(AbstractCollectionModelManager collectionModelManager, ImportState importState, JukefoxApplication application) { super(collectionModelManager, importState); this.application = application; } @Override public boolean reducedScan() throws DataUnavailableException { aborted = false; directoryBlackList = readDirectoryBlacklist(); fileBlackList = readFileBlacklist(); HashSet<String> dbPaths = otherDataProvider.getAllSongsPaths(); return processMediaProviderInfoReduced(dbPaths); } @Override public void scan() throws DataUnavailableException { scannedFiles = 0; // TODO: check MediaProvider.AudioColumns.DATA_MODIFIED und // MediaProvider.AudioColumns.DATA_ADDED to improve performance? aborted = false; libraryChanges = new LibraryChanges(); directoryBlackList = readDirectoryBlacklist(); fileBlackList = readFileBlacklist(); HashSet<String> albumNamesToGroup; try { albumNamesToGroup = modelSettingsManager.getAlbumNamesToGroup(); } catch (Exception e) { Log.w(TAG, e); albumNamesToGroup = new HashSet<String>(); // TODO: can we inform // the user?? } // Read all paths currently in the db. During // processMediaProviderInfo, subtract all songs that are still // available from this list to keep only those that need to be // removed from the db. HashMap<String, ImportSong> dbSongs = songProvider.getAllImportSongs(); // if db empty, already insert songs during scan, such that a first song // is available as soon as possible boolean earlyInsert = dbSongs.size() == 0; readAlbumArtistCandidates(); processMediaProviderInfo(dbSongs, albumNamesToGroup, earlyInsert); for (ImportSong s : dbSongs.values()) { libraryChanges.addSongToRemove(s); } } private void readAlbumArtistCandidates() { String[] projection = new String[] { android.provider.MediaStore.Audio.AudioColumns.ALBUM, android.provider.MediaStore.Audio.AudioColumns.ARTIST, android.provider.MediaStore.Audio.AudioColumns.DATA }; albumArtistMapping = new HashMap<String, String>(); readUsingTagLibrary = new HashSet<String>(); Cursor cur = null; try { Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; Log.v(TAG, "uri: " + uri); ContentResolver cr = application.getContentResolver(); // cur = cr.query(uri, projection, null, null, // MediaStore.Audio.Media._ID); cur = cr.query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Audio.Media.TRACK); Log.v(TAG, "cur == null: " + (cur == null)); int numSongs = cur.getCount(); Log.v(TAG, "Total number of songs on device: " + numSongs); while (cur.moveToNext() && !aborted) { scannedFiles++; // there are 3 steps (prescan, scan, commit) for the base data // import (thus 3*numSongs) String artist = cur.getString(1); String album = cur.getString(0); String path = cur.getString(2); if (album != null) { importState.setBaseDataProgress(scannedFiles, 3 * numSongs, "Importing: " + album); } if (pathIsBlacklisted(path)) { continue; } if (album == null) { continue; } if (artist == null) { readUsingTagLibrary.add(album); continue; } // Log.v(TAG, "Prescan song at path: " + path); if (!readUsingTagLibrary.contains(album)) { if (albumArtistMapping.containsKey(album)) { if (!albumArtistMapping.get(album).equals(artist)) { readUsingTagLibrary.add(album); Log.v(TAG, "Added album to read with tag library: " + album); } } else { albumArtistMapping.put(album, artist); } } } } finally { if (cur != null) { cur.close(); } } } private void processMediaProviderInfo(HashMap<String, ImportSong> dbSongs, HashSet<String> albumNamesToGroup, boolean earlyInsert) { String[] projection = new String[] { android.provider.MediaStore.Audio.AudioColumns.TITLE, android.provider.MediaStore.Audio.AudioColumns.ALBUM, android.provider.MediaStore.Audio.AudioColumns.ARTIST, android.provider.MediaStore.Audio.AudioColumns.DATA, android.provider.MediaStore.Audio.AudioColumns.DURATION, android.provider.MediaStore.Audio.AudioColumns.TRACK, android.provider.MediaStore.Audio.AudioColumns._ID }; processMediaProviderInfo(projection, dbSongs, albumNamesToGroup, Type.EXTERNAL, earlyInsert); // processMediaProviderInfo(projection, dbSongs, Type.INTERNAL); } /** * * @param dbPaths * @return true if there are changes */ private boolean processMediaProviderInfoReduced(HashSet<String> dbPaths) { String[] projection = new String[] { android.provider.MediaStore.Audio.AudioColumns.DATA }; boolean ret; ret = processMediaProviderInfoReduced(projection, dbPaths, Type.EXTERNAL); if (ret) { return true; } // ret = processMediaProviderInfoReduced(projection, dbPaths, // Type.INTERNAL); return ret; } private boolean processMediaProviderInfoReduced(String[] projection, HashSet<String> dbPaths, Type type) { Cursor cur = null; try { Uri uri; if (type == Type.EXTERNAL) { uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } else { uri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI; } Log.v(TAG, "uri: " + uri); ContentResolver cr = application.getContentResolver(); cur = cr.query(uri, projection, null, null, MediaStore.Audio.Media._ID); Log.v(TAG, "cur == null: " + (cur == null)); Log.v(TAG, "numPaths from dbProvider: " + cur.getCount()); while (cur.moveToNext() && !aborted) { String path = cur.getString(0); if (pathIsBlacklisted(path)) { continue; } if (!dbPaths.remove(path)) { Log.v(TAG, "reduced scan: new song path found: " + path); return true; // there is a change } } // if there are paths remaining, we have a change => return true Log.v(TAG, "end of reduced scan: dbPaths.size(): " + dbPaths.size()); return dbPaths.size() > 0; } finally { if (cur != null) { cur.close(); } } } private void processMediaProviderInfo(String[] projection, HashMap<String, ImportSong> dbSongs, HashSet<String> albumNamesToGroup, Type type, boolean initialImport) { Cursor cur = null; try { Uri uri; if (type == Type.EXTERNAL) { uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; } else { uri = MediaStore.Audio.Media.INTERNAL_CONTENT_URI; } Log.v(TAG, "uri: " + uri); ContentResolver cr = application.getContentResolver(); cur = cr.query(uri, projection, null, null, MediaStore.Audio.Media._ID); Log.v(TAG, "cur == null: " + (cur == null)); int numSongs = cur.getCount(); int count = 0; while (cur.moveToNext() && !aborted) { count++; boolean earlyInsert = initialImport && scannedFiles < Math.min( AbstractLibraryScanner.NUM_SONGS_EARLY_INSERT, numSongs); processSongInfoRow(cur, dbSongs, type, albumNamesToGroup, earlyInsert); scannedFiles++; // there are 3 steps (prescan, scan, commit) for the base data // import (thus 3*numSongs) importState.setBaseDataProgress(scannedFiles, 3 * numSongs, "Importing Song Nr: " + count + "/" + numSongs); if (scannedFiles % 500 == 0) { JoinableThread.sleepWithoutThrowing(10); } } importState.setBaseDataProgress(scannedFiles, 3 * numSongs, "Got " + numSongs + " songs"); return; } finally { if (cur != null) { cur.close(); } } } private void processSongInfoRow(Cursor cur, HashMap<String, ImportSong> dbSongs, Type type, HashSet<String> albumNamesToGroup, boolean earlyInsert) { ImportSong song = getSongFromCursor(cur, type); if (pathIsBlacklisted(song.getPath())) { // Ignore songs in blacklisted paths return; } if (!isSongReadCorrectly(song) || readUsingTagLibrary.contains(song.getAlbum().getName()) || isWmaFile(song)) { readSongInfoWithTagLibrary(song); } replaceEmptyFieldsWithAlias(song); groupAlbumsIfNecessary(song, albumNamesToGroup); getSongToAddRemoveAndChange(dbSongs, song, earlyInsert); if (earlyInsert) { try { modifyProvider.insertSong(song); } catch (DataWriteException e) { e.printStackTrace(); libraryChanges.addSongToAdd(song); } } } @SuppressWarnings("unchecked") private void readSongInfoWithTagLibrary(ImportSong song) { try { // Log.v("Tag Reader", "Reading tags from " + song.getName()); File f = new File(song.getPath()); AudioFile af = AudioFileIO.read(f); Tag tag = af.getTag(); ImportAlbum album = null; String albumName = tag.getFirstAlbum(); if (AndroidUtils.isNullOrEmpty(albumName, true)) { // albumName = JukefoxApplication.unknownAlbumAlias; albumName = AndroidConstants.UNKOWN_ALBUM; } if (tag.hasField("TCMP")) { // iTunes compilation album marker field try { Log.v("TCMP", "TCMP tag field found: " + song.getPath()); Log.v(TAG, "TCMP tag field found: " + song.getPath()); String content = readFieldContent(tag, "TCMP"); Log.v("TCMP", "content: " + content); Log.v(TAG, "content: " + content); if (!content.contains("0")) { // album = new ImportAlbum(albumName, // JukefoxApplication.albumArtistAlias); album = new ImportAlbum(albumName, AndroidConstants.ALBUM_ARTIST_ALIAS); } } catch (Exception e) { Log.w(TAG, e); } } if (album == null && tag.hasField("TPE2")) { try { List<TextId3Frame> albumArtists = tag.get("TPE2"); String albumArtistName = albumArtists.get(0).getContent(); if (AndroidUtils.isNullOrEmpty(albumArtistName, true)) { // albumArtistName = // JukefoxApplication.unknownArtistAlias; albumArtistName = AndroidConstants.UNKOWN_ARTIST; } album = new ImportAlbum(albumName, albumArtistName); Log.v(TAG, "Read TPE2: " + albumArtists.get(0)); } catch (Exception e) { Log.w(TAG, e); } } if (album != null) { song.setAlbum(album); } String title = tag.getFirstTitle(); if (AndroidUtils.isNullOrEmpty(title, true)) { // title = JukefoxApplication.unknownTitleAlias; title = AndroidConstants.UNKOWN_TITLE; } song.setName(title); song.setArtist(tag.getFirstArtist()); if (album == null) { album = new ImportAlbum(albumName, song.getArtist()); } song.setAlbum(album); // Only try to read track if it was not read correctly before. if (song.getTrack() < 1) { int track = 0; try { track = Integer.parseInt(tag.getFirstTrack()); } catch (Exception e) { } song.setTrack(track); } if (song.getDuration() < 1000) { song.setDuration(af.getLength()); } Log.v(TAG, "Read from tag " + song.getPath() + " : (artist: " + song.getArtist() + " album: " + song.getAlbum().getName() + " title: " + song.getName() + " track: " + song.getTrack()); } catch (Throwable e) { Log.w(TAG, e); } } private ImportSong getSongFromCursor(Cursor cur, Type type) { String name = cur.getString(0); String album = cur.getString(1); String artist = cur.getString(2); String path = cur.getString(3); int duration = cur.getInt(4); int track = cur.getInt(5); ContentProviderId cpId = new ContentProviderId(cur.getInt(6), type); // int track = 0; // ContentProviderId cpId = new ContentProviderId(cur.getInt(5), type); if (artist == null) { Log.v(TAG, "getSongFromCursor(): artist == null"); } // Log.v(TAG, "artist: '" + artist + "'"); ImportAlbum importAlbum = new ImportAlbum(album, artist); ImportSong song = new ImportSong(name, importAlbum, artist, path, duration, track, cpId, null, new Date()); return song; } private boolean isWmaFile(ImportSong song) { String path = song.getPath().toLowerCase(); if (path.endsWith(".wma")) { return true; } else { return false; } } @Override public void clearData() { libraryChanges = null; readUsingTagLibrary = null; albumArtistMapping = null; } }