You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
390 lines
13 KiB
390 lines
13 KiB
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
|
|
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<String, Integer> albumCount = new HashMap<String, Integer>();
|
|
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<String, Integer> 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<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.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<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) {
|
|
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;
|
|
}
|
|
}
|
|
|