content: /*
   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;
      }
  }
  