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 chameleon.playlist.SpecificPlaylist;
  import chameleon.playlist.SpecificPlaylistFactory;
  import chameleon.playlist.SpecificPlaylistProvider;
  import org.airsonic.player.dao.MediaFileDao;
  import org.airsonic.player.dao.PlaylistDao;
  import org.airsonic.player.domain.MediaFile;
  import org.airsonic.player.domain.PlayQueue;
  import org.airsonic.player.domain.Playlist;
  import org.airsonic.player.service.playlist.PlaylistExportHandler;
  import org.airsonic.player.service.playlist.PlaylistImportHandler;
  import org.airsonic.player.util.FileUtil;
  import org.airsonic.player.util.StringUtil;
  import org.apache.commons.io.FilenameUtils;
  import org.apache.commons.lang3.tuple.Pair;
  import org.slf4j.Logger;
  import org.slf4j.LoggerFactory;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.stereotype.Service;
  import org.springframework.util.Assert;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.InputStream;
  import java.io.OutputStream;
  import java.util.*;
  
  /**
   * Provides services for loading and saving playlists to and from persistent storage.
   *
   * @author Sindre Mehus
   * @see PlayQueue
   */
  @Service
  public class PlaylistService {
  
      private static final Logger LOG = LoggerFactory.getLogger(PlaylistService.class);
      @Autowired
      private MediaFileDao mediaFileDao;
      @Autowired
      private PlaylistDao playlistDao;
      @Autowired
      private SecurityService securityService;
      @Autowired
      private SettingsService settingsService;
      @Autowired
      private List<PlaylistExportHandler> exportHandlers;
      @Autowired
      private List<PlaylistImportHandler> importHandlers;
  
      public PlaylistService(
              MediaFileDao mediaFileDao,
              PlaylistDao playlistDao,
              SecurityService securityService,
              SettingsService settingsService,
              List<PlaylistExportHandler> exportHandlers,
              List<PlaylistImportHandler> importHandlers
      ) {
          Assert.notNull(mediaFileDao, "mediaFileDao must not be null");
          Assert.notNull(playlistDao, "playlistDao must not be null");
          Assert.notNull(securityService, "securityservice must not be null");
          Assert.notNull(settingsService, "settingsService must not be null");
          Assert.notNull(exportHandlers, "exportHandlers must not be null");
          Assert.notNull(importHandlers, "importHandlers must not be null");
          this.mediaFileDao = mediaFileDao;
          this.playlistDao = playlistDao;
          this.securityService = securityService;
          this.settingsService = settingsService;
          this.exportHandlers = exportHandlers;
          this.importHandlers = importHandlers;
      }
  
      public List<Playlist> getAllPlaylists() {
          return sort(playlistDao.getAllPlaylists());
      }
  
      public List<Playlist> getReadablePlaylistsForUser(String username) {
          return sort(playlistDao.getReadablePlaylistsForUser(username));
      }
  
      public List<Playlist> getWritablePlaylistsForUser(String username) {
  
          // Admin users are allowed to modify all playlists that are visible to them.
          if (securityService.isAdmin(username)) {
              return getReadablePlaylistsForUser(username);
          }
  
          return sort(playlistDao.getWritablePlaylistsForUser(username));
      }
  
      private List<Playlist> sort(List<Playlist> playlists) {
          Collections.sort(playlists, new PlaylistComparator());
          return playlists;
      }
  
      public Playlist getPlaylist(int id) {
          return playlistDao.getPlaylist(id);
      }
  
      public List<String> getPlaylistUsers(int playlistId) {
          return playlistDao.getPlaylistUsers(playlistId);
      }
  
      public List<MediaFile> getFilesInPlaylist(int id) {
          return getFilesInPlaylist(id, false);
      }
  
      public List<MediaFile> getFilesInPlaylist(int id, boolean includeNotPresent) {
          List<MediaFile> files = mediaFileDao.getFilesInPlaylist(id);
          if (includeNotPresent) {
              return files;
          }
          List<MediaFile> presentFiles = new ArrayList<MediaFile>(files.size());
          for (MediaFile file : files) {
              if (file.isPresent()) {
                  presentFiles.add(file);
              }
          }
          return presentFiles;
      }
  
      public void setFilesInPlaylist(int id, List<MediaFile> files) {
          playlistDao.setFilesInPlaylist(id, files);
      }
  
      public void createPlaylist(Playlist playlist) {
          playlistDao.createPlaylist(playlist);
      }
  
      public void addPlaylistUser(int playlistId, String username) {
          playlistDao.addPlaylistUser(playlistId, username);
      }
  
      public void deletePlaylistUser(int playlistId, String username) {
          playlistDao.deletePlaylistUser(playlistId, username);
      }
  
      public boolean isReadAllowed(Playlist playlist, String username) {
          if (username == null) {
              return false;
          }
          if (username.equals(playlist.getUsername()) || playlist.isShared()) {
              return true;
          }
          return playlistDao.getPlaylistUsers(playlist.getId()).contains(username);
      }
  
      public boolean isWriteAllowed(Playlist playlist, String username) {
          return username != null && username.equals(playlist.getUsername());
      }
  
      public void deletePlaylist(int id) {
          playlistDao.deletePlaylist(id);
      }
  
      public void updatePlaylist(Playlist playlist) {
          playlistDao.updatePlaylist(playlist);
      }
  
      public Playlist importPlaylist(
              String username, String playlistName, String fileName, InputStream inputStream, Playlist existingPlaylist
      ) throws Exception {
  
          // TODO: handle other encodings
          final SpecificPlaylist inputSpecificPlaylist = SpecificPlaylistFactory.getInstance().readFrom(inputStream, "UTF-8");
          if (inputSpecificPlaylist == null) {
              throw new Exception("Unsupported playlist " + fileName);
          }
          PlaylistImportHandler importHandler = getImportHandler(inputSpecificPlaylist);
          LOG.debug("Using " + importHandler.getClass().getSimpleName() + " playlist import handler");
  
          Pair<List<MediaFile>, List<String>> result = importHandler.handle(inputSpecificPlaylist);
  
          if (result.getLeft().isEmpty() && !result.getRight().isEmpty()) {
              throw new Exception("No songs in the playlist were found.");
          }
  
          for (String error : result.getRight()) {
              LOG.warn("File in playlist '" + fileName + "' not found: " + error);
          }
          Date now = new Date();
          Playlist playlist;
          if (existingPlaylist == null) {
              playlist = new Playlist();
              playlist.setUsername(username);
              playlist.setCreated(now);
              playlist.setChanged(now);
              playlist.setShared(true);
              playlist.setName(playlistName);
              playlist.setComment("Auto-imported from " + fileName);
              playlist.setImportedFrom(fileName);
              createPlaylist(playlist);
          } else {
              playlist = existingPlaylist;
          }
  
          setFilesInPlaylist(playlist.getId(), result.getLeft());
  
          return playlist;
      }
  
      public String getExportPlaylistExtension() {
          String format = settingsService.getPlaylistExportFormat();
          SpecificPlaylistProvider provider = SpecificPlaylistFactory.getInstance().findProviderById(format);
          return provider.getContentTypes()[0].getExtensions()[0];
      }
  
      public void exportPlaylist(int id, OutputStream out) throws Exception {
          String format = settingsService.getPlaylistExportFormat();
          SpecificPlaylistProvider provider = SpecificPlaylistFactory.getInstance().findProviderById(format);
          PlaylistExportHandler handler = getExportHandler(provider);
          SpecificPlaylist specificPlaylist = handler.handle(id, provider);
          specificPlaylist.writeTo(out, StringUtil.ENCODING_UTF8);
      }
  
      private PlaylistImportHandler getImportHandler(SpecificPlaylist playlist) {
          return importHandlers.stream()
                               .filter(handler -> handler.canHandle(playlist.getClass()))
                               .findFirst()
                               .orElseThrow(() -> new RuntimeException("No import handler for " + playlist.getClass()
                                                                                                          .getName()));
  
      }
  
      private PlaylistExportHandler getExportHandler(SpecificPlaylistProvider provider) {
          return exportHandlers.stream()
                               .filter(handler -> handler.canHandle(provider.getClass()))
                               .findFirst()
                               .orElseThrow(() -> new RuntimeException("No export handler for " + provider.getClass()
                                                                                                          .getName()));
      }
  
      public void importPlaylists() {
          try {
              LOG.info("Starting playlist import.");
              doImportPlaylists();
              LOG.info("Completed playlist import.");
          } catch (Throwable x) {
              LOG.warn("Failed to import playlists: " + x, x);
          }
      }
  
      private void doImportPlaylists() {
          String playlistFolderPath = settingsService.getPlaylistFolder();
          if (playlistFolderPath == null) {
              return;
          }
          File playlistFolder = new File(playlistFolderPath);
          if (!playlistFolder.exists()) {
              return;
          }
  
          List<Playlist> allPlaylists = playlistDao.getAllPlaylists();
          for (File file : playlistFolder.listFiles()) {
              try {
                  importPlaylistIfUpdated(file, allPlaylists);
              } catch (Exception x) {
                  LOG.warn("Failed to auto-import playlist " + file + ". " + x.getMessage());
              }
          }
      }
  
      private void importPlaylistIfUpdated(File file, List<Playlist> allPlaylists) throws Exception {
  
          String fileName = file.getName();
          Playlist existingPlaylist = null;
          for (Playlist playlist : allPlaylists) {
              if (fileName.equals(playlist.getImportedFrom())) {
                  existingPlaylist = playlist;
                  if (file.lastModified() <= playlist.getChanged().getTime()) {
                      // Already imported and not changed since.
                      return;
                  }
              }
          }
          InputStream in = new FileInputStream(file);
          try {
              // With the transition away from a hardcoded admin account to Admin Roles, there is no longer
              //   a specific account to use for auto-imported playlists, so use the first admin account
              importPlaylist(securityService.getAdminUsername(), FilenameUtils.getBaseName(fileName), fileName, in, existingPlaylist);
              LOG.info("Auto-imported playlist " + file);
          } finally {
              FileUtil.closeQuietly(in);
          }
      }
  
      private static class PlaylistComparator implements Comparator<Playlist> {
          @Override
          public int compare(Playlist p1, Playlist p2) {
              return p1.getName().compareTo(p2.getName());
          }
      }
  }
  