/*
* 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.Iterator;
import java.util.List;
import ch.ethz.dcg.jukefox.commons.DataUnavailableException;
import ch.ethz.dcg.jukefox.commons.DataWriteException;
import ch.ethz.dcg.jukefox.commons.LanguageHelper;
import ch.ethz.dcg.jukefox.commons.utils.JoinableThread;
import ch.ethz.dcg.jukefox.commons.utils.Log;
import ch.ethz.dcg.jukefox.commons.utils.Utils;
import ch.ethz.dcg.jukefox.io.IoUtils;
import ch.ethz.dcg.jukefox.model.AbstractCollectionModelManager;
import ch.ethz.dcg.jukefox.model.collection.Genre;
import ch.ethz.dcg.jukefox.model.libraryimport.ContentProviderId;
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 entagged.audioformats.AudioFile;
import entagged.audioformats.AudioFileFilter;
import entagged.audioformats.AudioFileIO;
import entagged.audioformats.Tag;
import entagged.audioformats.mp3.util.id3frames.TextId3Frame;
public class LibraryScanner extends AbstractLibraryScanner {
private final static String TAG = LibraryScanner.class.getSimpleName();
private static AudioFileFilter audioFileFilter = new AudioFileFilter();
private static LanguageHelper languageHelper = new LanguageHelper();
private HashMap<String, Integer> genreTable;
public LibraryScanner(AbstractCollectionModelManager collectionModelManager, ImportState importState) {
super(collectionModelManager, importState);
}
@Override
public boolean reducedScan() throws DataUnavailableException {
aborted = false;
directoryBlackList = readDirectoryBlacklist();
fileBlackList = readFileBlacklist();
HashSet<String> dbPaths = otherDataProvider.getAllSongsPaths();
HashMap<String, File> audioCollection;
audioCollection = collectSongsFromLibraryFolders();
return checkForChanges(dbPaths, audioCollection);
}
@Override
public void scan() throws DataUnavailableException {
scannedFiles = 0;
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 initialImport = dbSongs.size() == 0;
HashMap<String, File> audioCollection;
audioCollection = collectSongsFromLibraryFolders();
processCollection(dbSongs, audioCollection, albumNamesToGroup, initialImport);
for (ImportSong s : dbSongs.values()) {
libraryChanges.addSongToRemove(s);
}
}
private HashMap<String, File> collectSongsFromLibraryFolders() {
HashSet<String> libraryPaths = modelSettingsManager.getLibraryPaths();
Iterator<String> libraryFolders = libraryPaths.iterator();
HashMap<String, File> audioCollection = new HashMap<String, File>();
while (libraryFolders.hasNext()) {
File folder = new File(libraryFolders.next());
if (folder.exists()) {
addFilesRecursively(folder, audioCollection);
}
}
return audioCollection;
}
private void addFilesRecursively(File file, HashMap<String, File> audioCollection) {
final File[] songs = file.listFiles(audioFileFilter);
if (songs != null) {
for (File song : songs) {
if (pathIsBlacklisted(song.getAbsolutePath())) {
continue;
}
if (song.isDirectory()) {
addFilesRecursively(song, audioCollection);
} else {
audioCollection.put(song.getAbsolutePath(), song);
}
}
}
}
private boolean checkForChanges(HashSet<String> dbPaths, HashMap<String, File> audioCollection) {
Iterator<File> songs = audioCollection.values().iterator();
while (songs.hasNext() && !aborted) {
String path = songs.next().getAbsolutePath();
if (!dbPaths.remove(path)) {
Log.v(TAG, "reduced scan: new song path found: " + path);
return true; // there is a change
}
}
Log.v(TAG, "end of reduced scan: dbPaths.size(): " + dbPaths.size());
return dbPaths.size() > 0;
}
@Override
protected void processCollection(HashMap<String, ImportSong> dbSongs, HashMap<String, File> audioCollection,
HashSet<String> albumNamesToGroup, boolean initialImport) {
createGenreTable();
int numSongs = audioCollection.size();
Iterator<File> songs = audioCollection.values().iterator();
int count = 0;
while (songs.hasNext() && !aborted) {
count++;
ImportSong song = null;
song = readSongInfoWithTagLibrary(songs.next());
if (song == null) {
continue;
}
updateSongGenreMap(song);
replaceEmptyFieldsWithAlias(song);
groupAlbumsIfNecessary(song, albumNamesToGroup);
boolean earlyInsert = initialImport && scannedFiles < AbstractLibraryScanner.NUM_SONGS_EARLY_INSERT;
getSongToAddRemoveAndChange(dbSongs, song, earlyInsert);
if (initialImport && earlyInsertSongs.size() >= Math.min(numSongs,
AbstractLibraryScanner.NUM_SONGS_EARLY_INSERT)) {
// batch insert the songs
try {
modifyProvider.batchInsertSongs(earlyInsertSongs);
} catch (DataWriteException e) {
for (ImportSong importSong : earlyInsertSongs) {
libraryChanges.addSongToAdd(importSong);
}
}
earlyInsertSongs.clear();
}
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);
// TODO do we need this in PC version
if (scannedFiles % 500 == 0) {
JoinableThread.sleepWithoutThrowing(10);
}
}
}
@Override
protected void createGenreTable() {
List<Genre> genreList = genreProvider.getAllGenres();
genreTable = new HashMap<String, Integer>();
for (Genre genre : genreList) {
genreTable.put(genre.getName(), genre.getId());
}
}
private void updateSongGenreMap(ImportSong song) {
HashSet<Integer> genreIds = new HashSet<Integer>();
for (String genre : song.getGenres()) {
int genreId;
if (genreTable.isEmpty() || !genreTable.containsKey(genre)) {
try {
genreId = modifyProvider.insertGenre(genre);
} catch (DataWriteException e) {
Log.e(TAG, "insert genre failed: " + genre);
return;
}
} else {
genreId = genreTable.get(genre);
}
genreIds.add(genreId);
}
libraryChanges.getCollectionSongGenreMap().put(song.getPath(), genreIds);
}
/*
* Added the suppression below to get rid of the two warnings that occur
* in relation assigning the lists returned by the Tag-class to
* typed references.
*/
@SuppressWarnings("unchecked")
private ImportSong readSongInfoWithTagLibrary(File songPath) {
ImportSong song = null;
String name = null;
String artist = null;
String albumName = null;
String path = songPath.getAbsolutePath();
int duration;
int track = 0;
ContentProviderId cpId = null;
List<TextId3Frame> genres;
ImportAlbum album = null;
try {
Log.d(TAG, "Reading Tags of '" + songPath + "'.");
// Needed suspend method because the read-method prints an exception in case one occurs instead
// of throwing it again.
IoUtils.suspendSysErr();
AudioFile af = AudioFileIO.read(songPath);
IoUtils.resumeSysErr();
Tag tag = af.getTag();
albumName = tag.getFirstAlbum();
if (Utils.isNullOrEmpty(albumName, true)) {
albumName = languageHelper.getUnknownAlbumAlias();
}
if (tag.hasField("TCMP")) { // iTunes compilation album marker field
try {
Log.v("TCMP", "TCMP tag field found: " + songPath.getAbsolutePath());
Log.v(TAG, "TCMP tag field found: " + songPath.getAbsolutePath());
String content = readFieldContent(tag, "TCMP");
Log.v("TCMP", "content: " + content);
Log.v(TAG, "content: " + content);
if (!content.contains("0")) {
album = new ImportAlbum(albumName, languageHelper.getAlbumArtistAlias());
}
} 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 (Utils.isNullOrEmpty(albumArtistName, true)) {
albumArtistName = languageHelper.getUnknownArtistAlias();
}
album = new ImportAlbum(albumName, albumArtistName);
Log.v(TAG, "Read TPE2: " + albumArtists.get(0));
} catch (Exception e) {
Log.w(TAG, e);
}
}
name = tag.getFirstTitle();
if (Utils.isNullOrEmpty(name, true)) {
name = languageHelper.getUnknownTitleAlias();
}
artist = tag.getFirstArtist();
if (album == null) {
album = new ImportAlbum(albumName, artist);
}
try {
track = Integer.parseInt(tag.getFirstTrack());
} catch (Exception e) {
}
duration = (int) (af.getPreciseLength() * 1000);
song = new ImportSong(name, album, artist, path, duration, track, cpId, null, new Date());
try {
genres = tag.getGenre();
for (TextId3Frame genre : genres) {
song.addGenre(genre.getContent());
}
} catch (Exception e) {
}
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);
}
return song;
}
@Override
public void clearData() {
libraryChanges = null;
}
}