/* This file is part of Airsonic. Airsonic 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. Airsonic 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 Airsonic. If not, see . Copyright 2016 (C) Airsonic Authors Based upon Subsonic, Copyright 2009 (C) Sindre Mehus */ package org.airsonic.player.service; import org.airsonic.player.dao.AlbumDao; import org.airsonic.player.dao.ArtistDao; import org.airsonic.player.dao.MediaFileDao; import org.airsonic.player.domain.*; import org.airsonic.player.service.search.IndexManager; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang3.time.DateUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.io.File; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * Provides services for scanning the music library. * * @author Sindre Mehus */ @Service public class MediaScannerService { private static final Logger LOG = LoggerFactory.getLogger(MediaScannerService.class); private boolean scanning; private ScheduledExecutorService scheduler; @Autowired private SettingsService settingsService; @Autowired private IndexManager indexManager; @Autowired private PlaylistService playlistService; @Autowired private MediaFileService mediaFileService; @Autowired private MediaFileDao mediaFileDao; @Autowired private ArtistDao artistDao; @Autowired private AlbumDao albumDao; private int scanCount; @PostConstruct public void init() { indexManager.initializeIndexDirectory(); schedule(); } public void initNoSchedule() { indexManager.deleteOldIndexFiles(); } /** * Schedule background execution of media library scanning. */ public synchronized void schedule() { if (scheduler != null) { scheduler.shutdown(); } long daysBetween = settingsService.getIndexCreationInterval(); int hour = settingsService.getIndexCreationHour(); if (daysBetween == -1) { LOG.info("Automatic media scanning disabled."); return; } scheduler = Executors.newSingleThreadScheduledExecutor(); LocalDateTime now = LocalDateTime.now(); LocalDateTime nextRun = now.withHour(hour).withMinute(0).withSecond(0); if (now.compareTo(nextRun) > 0) nextRun = nextRun.plusDays(1); long initialDelay = ChronoUnit.MILLIS.between(now, nextRun); scheduler.scheduleAtFixedRate(() -> scanLibrary(), initialDelay, TimeUnit.DAYS.toMillis(daysBetween), TimeUnit.MILLISECONDS); LOG.info("Automatic media library scanning scheduled to run every {} day(s), starting at {}", daysBetween, nextRun); // In addition, create index immediately if it doesn't exist on disk. if (neverScanned()) { LOG.info("Media library never scanned. Doing it now."); scanLibrary(); } } boolean neverScanned() { return indexManager.getStatistics() == null; } /** * 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(); mediaFileDao.checkpoint(); } }; thread.setPriority(Thread.MIN_PRIORITY); thread.start(); } private void doScanLibrary() { LOG.info("Starting to scan media library."); MediaLibraryStatistics statistics = new MediaLibraryStatistics( DateUtils.truncate(new Date(), Calendar.SECOND)); LOG.debug("New last scan date is " + statistics.getScanDate()); try { // Maps from artist name to album count. Map albumCount = new HashMap(); Genres genres = new Genres(); scanCount = 0; mediaFileService.setMemoryCacheEnabled(false); indexManager.startIndexing(); mediaFileService.clearMemoryCache(); // Recurse through all files on disk. for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false); scanFile(root, musicFolder, statistics, 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), statistics, albumCount, genres, true); } LOG.info("Scanned media library with " + scanCount + " entries."); LOG.info("Marking non-present files."); mediaFileDao.markNonPresent(statistics.getScanDate()); LOG.info("Marking non-present artists."); artistDao.markNonPresent(statistics.getScanDate()); LOG.info("Marking non-present albums."); albumDao.markNonPresent(statistics.getScanDate()); // Update statistics statistics.incrementArtists(albumCount.size()); for (Integer albums : albumCount.values()) { statistics.incrementAlbums(albums); } // Update genres mediaFileDao.updateGenres(genres.getGenres()); LOG.info("Completed media library scan."); } catch (Throwable x) { LOG.error("Failed to scan media library.", x); } finally { mediaFileService.setMemoryCacheEnabled(true); indexManager.stopIndexing(statistics); scanning = false; } } private void scanFile(MediaFile file, MusicFolder musicFolder, MediaLibraryStatistics statistics, Map albumCount, Genres genres, boolean isPodcast) { scanCount++; if (scanCount % 250 == 0) { LOG.info("Scanned media library with " + scanCount + " entries."); } LOG.trace("Scanning file {}", file.getPath()); // Update the root folder if it has changed. if (!musicFolder.getPath().getPath().equals(file.getFolder())) { file.setFolder(musicFolder.getPath().getPath()); mediaFileDao.createOrUpdateMediaFile(file); } indexManager.index(file); if (file.isDirectory()) { for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) { scanFile(child, musicFolder, statistics, albumCount, genres, isPodcast); } for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) { scanFile(child, musicFolder, statistics, albumCount, genres, isPodcast); } } else { if (!isPodcast) { updateAlbum(file, musicFolder, statistics.getScanDate(), albumCount); updateArtist(file, musicFolder, statistics.getScanDate(), albumCount); } statistics.incrementSongs(1); } updateGenres(file, genres); mediaFileDao.markPresent(file.getPath(), statistics.getScanDate()); artistDao.markPresent(file.getAlbumArtist(), statistics.getScanDate()); 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 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.getMusicBrainzReleaseId() != null) { album.setMusicBrainzReleaseId(file.getMusicBrainzReleaseId()); } 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) { indexManager.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 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) { indexManager.index(artist, musicFolder); } } public void setSettingsService(SettingsService settingsService) { this.settingsService = settingsService; } 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; } }