/*
This file is part of Subsonic.
Subsonic 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
(at your option) any later version.
Subsonic 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 Subsonic. If not, see .
Copyright 2009 (C) Sindre Mehus
*/
package net.sourceforge.subsonic.service;
import java.io.File;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import net.sourceforge.subsonic.Logger;
import net.sourceforge.subsonic.dao.AlbumDao;
import net.sourceforge.subsonic.dao.ArtistDao;
import net.sourceforge.subsonic.dao.MediaFileDao;
import net.sourceforge.subsonic.domain.Album;
import net.sourceforge.subsonic.domain.Artist;
import net.sourceforge.subsonic.domain.MediaFile;
import net.sourceforge.subsonic.domain.MediaLibraryStatistics;
import net.sourceforge.subsonic.domain.MusicFolder;
import net.sourceforge.subsonic.util.FileUtil;
import org.apache.commons.lang.ObjectUtils;
/**
* Provides services for scanning the music library.
*
* @author Sindre Mehus
*/
public class MediaScannerService {
private static final int INDEX_VERSION = 15;
private static final Logger LOG = Logger.getLogger(MediaScannerService.class);
private MediaLibraryStatistics statistics;
private boolean scanning;
private Timer timer;
private SettingsService settingsService;
private SearchService searchService;
private MediaFileService mediaFileService;
private MediaFileDao mediaFileDao;
private ArtistDao artistDao;
private AlbumDao albumDao;
private int scanCount;
public void init() {
deleteOldIndexFiles();
statistics = mediaFileDao.getStatistics();
schedule();
}
/**
* Schedule background execution of media library scanning.
*/
public synchronized void schedule() {
if (timer != null) {
timer.cancel();
}
timer = new Timer(true);
TimerTask task = new TimerTask() {
@Override
public void run() {
scanLibrary();
}
};
long daysBetween = settingsService.getIndexCreationInterval();
int hour = settingsService.getIndexCreationHour();
if (daysBetween == -1) {
LOG.info("Automatic media scanning disabled.");
return;
}
Date now = new Date();
Calendar cal = Calendar.getInstance();
cal.setTime(now);
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
if (cal.getTime().before(now)) {
cal.add(Calendar.DATE, 1);
}
Date firstTime = cal.getTime();
long period = daysBetween * 24L * 3600L * 1000L;
timer.schedule(task, firstTime, period);
LOG.info("Automatic media library scanning scheduled to run every " + daysBetween + " day(s), starting at " + firstTime);
// In addition, create index immediately if it doesn't exist on disk.
if (settingsService.getLastScanned() == null) {
LOG.info("Media library never scanned. Doing it now.");
scanLibrary();
}
}
/**
* Returns whether the media library is currently being scanned.
*/
public synchronized boolean isScanning() {
return scanning;
}
/**
* Returns the number of files scanned so far.
*/
public int getScanCount() {
return scanCount;
}
/**
* Scans the media library.
* The scanning is done asynchronously, i.e., this method returns immediately.
*/
public synchronized void scanLibrary() {
if (isScanning()) {
return;
}
scanning = true;
Thread thread = new Thread("MediaLibraryScanner") {
@Override
public void run() {
doScanLibrary();
}
};
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
private void doScanLibrary() {
LOG.info("Starting to scan media library.");
try {
Date lastScanned = new Date();
Map albumCount = new HashMap();
scanCount = 0;
searchService.startIndexing();
// Recurse through all files on disk.
for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) {
MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false);
scanFile(root, musicFolder, lastScanned, albumCount);
}
mediaFileDao.markNonPresent(lastScanned);
artistDao.markNonPresent(lastScanned);
albumDao.markNonPresent(lastScanned);
// Update statistics
statistics = mediaFileDao.getStatistics();
settingsService.setLastScanned(lastScanned);
settingsService.save(false);
LOG.info("Scanned media library with " + scanCount + " entries.");
} catch (Throwable x) {
LOG.error("Failed to scan media library.", x);
} finally {
scanning = false;
searchService.stopIndexing();
}
}
private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, Map albumCount) {
scanCount++;
if (scanCount % 250 == 0) {
LOG.info("Scanned media library with " + scanCount + " entries.");
}
searchService.index(file);
// Update the root folder if it has changed.
if (!musicFolder.getPath().getPath().equals(file.getFolder())) {
file.setFolder(musicFolder.getPath().getPath());
mediaFileDao.createOrUpdateMediaFile(file);
}
if (file.isDirectory()) {
for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) {
scanFile(child, musicFolder, lastScanned, albumCount);
}
for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) {
scanFile(child, musicFolder, lastScanned, albumCount);
}
} else {
updateAlbum(file, lastScanned, albumCount);
updateArtist(file, lastScanned, albumCount);
}
mediaFileDao.markPresent(file.getPath(), lastScanned);
artistDao.markPresent(file.getArtist(), lastScanned);
}
private void updateAlbum(MediaFile file, Date lastScanned, Map albumCount) {
if (file.getAlbumName() == null || file.getArtist() == null || file.getParentPath() == null || !file.isAudio()) {
return;
}
Album album = albumDao.getAlbumForFile(file);
if (album == null) {
album = new Album();
album.setPath(file.getParentPath());
album.setName(file.getAlbumName());
album.setArtist(file.getArtist());
album.setCreated(file.getChanged());
}
if (album.getCoverArtPath() == null) {
MediaFile parent = mediaFileService.getParentOf(file);
if (parent != null) {
album.setCoverArtPath(parent.getCoverArtPath());
}
}
boolean firstEncounter = !lastScanned.equals(album.getLastScanned());
if (firstEncounter) {
album.setDurationSeconds(0);
album.setSongCount(0);
Integer n = albumCount.get(file.getArtist());
albumCount.put(file.getArtist(), n == null ? 1 : n + 1);
}
if (file.getDurationSeconds() != null) {
album.setDurationSeconds(album.getDurationSeconds() + file.getDurationSeconds());
}
if (file.isAudio()) {
album.setSongCount(album.getSongCount() + 1);
}
album.setLastScanned(lastScanned);
album.setPresent(true);
albumDao.createOrUpdateAlbum(album);
if (firstEncounter) {
searchService.index(album);
}
// Update the file's album artist, if necessary.
if (!ObjectUtils.equals(album.getArtist(), file.getAlbumArtist())) {
file.setAlbumArtist(album.getArtist());
mediaFileDao.createOrUpdateMediaFile(file);
}
}
private void updateArtist(MediaFile file, Date lastScanned, Map albumCount) {
if (file.getArtist() == null || !file.isAudio()) {
return;
}
Artist artist = artistDao.getArtist(file.getArtist());
if (artist == null) {
artist = new Artist();
artist.setName(file.getArtist());
}
if (artist.getCoverArtPath() == null) {
MediaFile parent = mediaFileService.getParentOf(file);
if (parent != null) {
artist.setCoverArtPath(parent.getCoverArtPath());
}
}
boolean firstEncounter = !lastScanned.equals(artist.getLastScanned());
Integer n = albumCount.get(artist.getName());
artist.setAlbumCount(n == null ? 0 : n);
artist.setLastScanned(lastScanned);
artist.setPresent(true);
artistDao.createOrUpdateArtist(artist);
if (firstEncounter) {
searchService.index(artist);
}
}
/**
* Returns media library statistics, including the number of artists, albums and songs.
*
* @return Media library statistics.
*/
public MediaLibraryStatistics getStatistics() {
return statistics;
}
/**
* Deletes old versions of the index file.
*/
private void deleteOldIndexFiles() {
for (int i = 2; i < INDEX_VERSION; i++) {
File file = getIndexFile(i);
try {
if (FileUtil.exists(file)) {
if (file.delete()) {
LOG.info("Deleted old index file: " + file.getPath());
}
}
} catch (Exception x) {
LOG.warn("Failed to delete old index file: " + file.getPath(), x);
}
}
}
/**
* Returns the index file for the given index version.
*
* @param version The index version.
* @return The index file for the given index version.
*/
private File getIndexFile(int version) {
File home = SettingsService.getSubsonicHome();
return new File(home, "subsonic" + version + ".index");
}
public void setSettingsService(SettingsService settingsService) {
this.settingsService = settingsService;
}
public void setSearchService(SearchService searchService) {
this.searchService = searchService;
}
public void setMediaFileService(MediaFileService mediaFileService) {
this.mediaFileService = mediaFileService;
}
public void setMediaFileDao(MediaFileDao mediaFileDao) {
this.mediaFileDao = mediaFileDao;
}
public void setArtistDao(ArtistDao artistDao) {
this.artistDao = artistDao;
}
public void setAlbumDao(AlbumDao albumDao) {
this.albumDao = albumDao;
}
}