/* This file is part of Libresonic. Libresonic 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. Libresonic 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 Libresonic. If not, see <http://www.gnu.org/licenses/>. Copyright 2016 (C) Libresonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.libresonic.player.service; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.time.DateUtils; import org.libresonic.player.Logger; import org.libresonic.player.dao.AlbumDao; import org.libresonic.player.dao.ArtistDao; import org.libresonic.player.dao.MediaFileDao; import org.libresonic.player.domain.*; import org.libresonic.player.util.FileUtil; import java.io.File; import java.util.*; /** * 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 PlaylistService playlistService; private MediaFileService mediaFileService; private MediaFileDao mediaFileDao; private ArtistDao artistDao; private AlbumDao albumDao; private int scanCount; public void init() { deleteOldIndexFiles(); statistics = settingsService.getMediaLibraryStatistics(); schedule(); } public void initNoSchedule() { deleteOldIndexFiles(); statistics = settingsService.getMediaLibraryStatistics(); } /** * 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(); playlistService.importPlaylists(); } }; thread.setPriority(Thread.MIN_PRIORITY); thread.start(); } private void doScanLibrary() { LOG.info("Starting to scan media library."); Date lastScanned = DateUtils.truncate(new Date(), Calendar.SECOND); LOG.debug("New last scan date is " + lastScanned); try { // Maps from artist name to album count. Map<String, Integer> albumCount = new HashMap<String, Integer>(); Genres genres = new Genres(); scanCount = 0; statistics.reset(); mediaFileService.setMemoryCacheEnabled(false); searchService.startIndexing(); mediaFileService.clearMemoryCache(); // Recurse through all files on disk. for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false); scanFile(root, musicFolder, lastScanned, albumCount, genres, false); } // Scan podcast folder. File podcastFolder = new File(settingsService.getPodcastFolder()); if (podcastFolder.exists()) { scanFile(mediaFileService.getMediaFile(podcastFolder), new MusicFolder(podcastFolder, null, true, null), lastScanned, albumCount, genres, true); } LOG.info("Scanned media library with " + scanCount + " entries."); LOG.info("Marking non-present files."); mediaFileDao.markNonPresent(lastScanned); LOG.info("Marking non-present artists."); artistDao.markNonPresent(lastScanned); LOG.info("Marking non-present albums."); albumDao.markNonPresent(lastScanned); // Update statistics statistics.incrementArtists(albumCount.size()); for (Integer albums : albumCount.values()) { statistics.incrementAlbums(albums); } // Update genres mediaFileDao.updateGenres(genres.getGenres()); settingsService.setMediaLibraryStatistics(statistics); settingsService.setLastScanned(lastScanned); settingsService.save(false); LOG.info("Completed media library scan."); } catch (Throwable x) { LOG.error("Failed to scan media library.", x); } finally { mediaFileService.setMemoryCacheEnabled(true); searchService.stopIndexing(); scanning = false; } } private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, Map<String, Integer> albumCount, Genres genres, boolean isPodcast) { 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, genres, isPodcast); } for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) { scanFile(child, musicFolder, lastScanned, albumCount, genres, isPodcast); } } else { if (!isPodcast) { updateAlbum(file, musicFolder, lastScanned, albumCount); updateArtist(file, musicFolder, lastScanned, albumCount); } statistics.incrementSongs(1); } updateGenres(file, genres); mediaFileDao.markPresent(file.getPath(), lastScanned); artistDao.markPresent(file.getAlbumArtist(), lastScanned); if (file.getDurationSeconds() != null) { statistics.incrementTotalDurationInSeconds(file.getDurationSeconds()); } if (file.getFileSize() != null) { statistics.incrementTotalLengthInBytes(file.getFileSize()); } } private void updateGenres(MediaFile file, Genres genres) { String genre = file.getGenre(); if (genre == null) { return; } if (file.isAlbum()) { genres.incrementAlbumCount(genre); } else if (file.isAudio()) { genres.incrementSongCount(genre); } } private void updateAlbum(MediaFile file, MusicFolder musicFolder, Date lastScanned, Map<String, Integer> albumCount) { String artist = file.getAlbumArtist() != null ? file.getAlbumArtist() : file.getArtist(); if (file.getAlbumName() == null || artist == 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(artist); album.setCreated(file.getChanged()); } if (file.getYear() != null) { album.setYear(file.getYear()); } if (file.getGenre() != null) { album.setGenre(file.getGenre()); } MediaFile parent = mediaFileService.getParentOf(file); if (parent != null && parent.getCoverArtPath() != null) { album.setCoverArtPath(parent.getCoverArtPath()); } boolean firstEncounter = !lastScanned.equals(album.getLastScanned()); if (firstEncounter) { album.setFolderId(musicFolder.getId()); album.setDurationSeconds(0); album.setSongCount(0); Integer n = albumCount.get(artist); albumCount.put(artist, 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, MusicFolder musicFolder, Date lastScanned, Map<String, Integer> albumCount) { if (file.getAlbumArtist() == null || !file.isAudio()) { return; } Artist artist = artistDao.getArtist(file.getAlbumArtist()); if (artist == null) { artist = new Artist(); artist.setName(file.getAlbumArtist()); } if (artist.getCoverArtPath() == null) { MediaFile parent = mediaFileService.getParentOf(file); if (parent != null) { artist.setCoverArtPath(parent.getCoverArtPath()); } } boolean firstEncounter = !lastScanned.equals(artist.getLastScanned()); if (firstEncounter) { artist.setFolderId(musicFolder.getId()); } 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, musicFolder); } } /** * 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.getLibresonicHome(); return new File(home, "libresonic" + 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; } public void setPlaylistService(PlaylistService playlistService) { this.playlistService = playlistService; } }