From a1a18f77a50804e0127dfa4b0f5240c49c541184 Mon Sep 17 00:00:00 2001 From: Scott Jackson Date: Mon, 2 Jul 2012 21:24:02 -0700 Subject: Initial Commit --- .../subsonic/service/MediaScannerService.java | 354 +++++++++++++++++++++ 1 file changed, 354 insertions(+) create mode 100644 subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java') diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java new file mode 100644 index 00000000..84f2d31c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java @@ -0,0 +1,354 @@ +/* + 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; + } +} -- cgit v1.2.3