/* This file is part of Subsonic. Subsonic 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. Subsonic 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 Subsonic. If not, see . Copyright 2009 (C) Sindre Mehus */ package net.sourceforge.subsonic.service; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.subsonic.domain.User; import net.sourceforge.subsonic.util.Pair; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringEscapeUtils; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import org.jdom.input.SAXBuilder; import net.sourceforge.subsonic.Logger; import net.sourceforge.subsonic.dao.MediaFileDao; import net.sourceforge.subsonic.dao.PlaylistDao; import net.sourceforge.subsonic.domain.MediaFile; import net.sourceforge.subsonic.domain.Playlist; import net.sourceforge.subsonic.util.StringUtil; /** * Provides services for loading and saving playlists to and from persistent storage. * * @author Sindre Mehus * @see net.sourceforge.subsonic.domain.PlayQueue */ public class PlaylistService { private static final Logger LOG = Logger.getLogger(PlaylistService.class); private MediaFileService mediaFileService; private MediaFileDao mediaFileDao; private PlaylistDao playlistDao; private SecurityService securityService; private SettingsService settingsService; public void init() { try { importPlaylists(); } catch (Throwable x) { LOG.warn("Failed to import playlists: " + x, x); } } public List getReadablePlaylistsForUser(String username) { return playlistDao.getReadablePlaylistsForUser(username); } public List getWritablePlaylistsForUser(String username) { // Admin users are allowed to modify all playlists that are visible to them. if (securityService.isAdmin(username)) { return getReadablePlaylistsForUser(username); } return playlistDao.getWritablePlaylistsForUser(username); } public Playlist getPlaylist(int id) { return playlistDao.getPlaylist(id); } public List getPlaylistUsers(int playlistId) { return playlistDao.getPlaylistUsers(playlistId); } public List getFilesInPlaylist(int id) { return mediaFileDao.getFilesInPlaylist(id); } public void setFilesInPlaylist(int id, List 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.isPublic()) { 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, String format, InputStream inputStream) throws Exception { PlaylistFormat playlistFormat = PlaylistFormat.getPlaylistFormat(format); if (playlistFormat == null) { throw new Exception("Unsupported playlist format: " + format); } Pair, List> result = parseFiles(IOUtils.toByteArray(inputStream), playlistFormat); if (result.getFirst().isEmpty() && !result.getSecond().isEmpty()) { throw new Exception("No songs in the playlist were found."); } for (String error : result.getSecond()) { LOG.warn("File in playlist '" + fileName + "' not found: " + error); } Date now = new Date(); Playlist playlist = new Playlist(); playlist.setUsername(username); playlist.setCreated(now); playlist.setChanged(now); playlist.setPublic(true); playlist.setName(playlistName); playlist.setImportedFrom(fileName); createPlaylist(playlist); setFilesInPlaylist(playlist.getId(), result.getFirst()); return playlist; } private Pair, List> parseFiles(byte[] playlist, PlaylistFormat playlistFormat) throws IOException { Pair, List> result = null; // Try with multiple encodings; use the one that finds the most files. String[] encodings = {StringUtil.ENCODING_LATIN, StringUtil.ENCODING_UTF8, Charset.defaultCharset().name()}; for (String encoding : encodings) { Pair, List> files = parseFilesWithEncoding(playlist, playlistFormat, encoding); if (result == null || result.getFirst().size() < files.getFirst().size()) { result = files; } } return result; } private Pair, List> parseFilesWithEncoding(byte[] playlist, PlaylistFormat playlistFormat, String encoding) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(playlist), encoding)); return playlistFormat.parse(reader, mediaFileService); } public void exportPlaylist(int id, OutputStream out) throws Exception { PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StringUtil.ENCODING_UTF8)); new M3UFormat().format(getFilesInPlaylist(id), writer); } /** * Implementation of M3U playlist format. */ private void importPlaylists() throws Exception { String playlistFolderPath = settingsService.getPlaylistFolder(); if (playlistFolderPath == null) { return; } File playlistFolder = new File(playlistFolderPath); if (!playlistFolder.exists()) { return; } List allPlaylists = playlistDao.getAllPlaylists(); for (File file : playlistFolder.listFiles()) { try { importPlaylistIfNotExisting(file, allPlaylists); } catch (Exception x) { LOG.warn("Failed to auto-import playlist " + file + ". " + x.getMessage()); } } } private void importPlaylistIfNotExisting(File file, List allPlaylists) throws Exception { String format = FilenameUtils.getExtension(file.getPath()); if (PlaylistFormat.getPlaylistFormat(format) == null) { return; } String fileName = file.getName(); for (Playlist playlist : allPlaylists) { if (fileName.equals(playlist.getImportedFrom())) { return; // Already imported. } } InputStream in = new FileInputStream(file); try { importPlaylist(User.USERNAME_ADMIN, FilenameUtils.getBaseName(fileName), fileName, format, in); LOG.info("Auto-imported playlist " + file); } finally { IOUtils.closeQuietly(in); } } public void setPlaylistDao(PlaylistDao playlistDao) { this.playlistDao = playlistDao; } public void setMediaFileDao(MediaFileDao mediaFileDao) { this.mediaFileDao = mediaFileDao; } public void setMediaFileService(MediaFileService mediaFileService) { this.mediaFileService = mediaFileService; } public void setSecurityService(SecurityService securityService) { this.securityService = securityService; } public void setSettingsService(SettingsService settingsService) { this.settingsService = settingsService; } /** * Abstract superclass for playlist formats. */ private abstract static class PlaylistFormat { public abstract Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException; public abstract void format(List files, PrintWriter writer) throws IOException; public static PlaylistFormat getPlaylistFormat(String format) { if (format == null) { return null; } if (format.equalsIgnoreCase("m3u") || format.equalsIgnoreCase("m3u8")) { return new M3UFormat(); } if (format.equalsIgnoreCase("pls")) { return new PLSFormat(); } if (format.equalsIgnoreCase("xspf")) { return new XSPFFormat(); } return null; } protected MediaFile getMediaFile(MediaFileService mediaFileService, String path) { try { MediaFile file = mediaFileService.getMediaFile(path); if (file != null && file.exists()) { return file; } } catch (SecurityException x) { // Ignored } return null; } } private static class M3UFormat extends PlaylistFormat { public Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { List ok = new ArrayList(); List error = new ArrayList(); String line = reader.readLine(); while (line != null) { if (!line.startsWith("#")) { MediaFile file = getMediaFile(mediaFileService, line); if (file != null) { ok.add(file); } else { error.add(line); } } line = reader.readLine(); } return new Pair, List>(ok, error); } public void format(List files, PrintWriter writer) throws IOException { writer.println("#EXTM3U"); for (MediaFile file : files) { writer.println(file.getPath()); } if (writer.checkError()) { throw new IOException("Error when writing playlist"); } } } /** * Implementation of PLS playlist format. */ private static class PLSFormat extends PlaylistFormat { public Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { List ok = new ArrayList(); List error = new ArrayList(); Pattern pattern = Pattern.compile("^File\\d+=(.*)$"); String line = reader.readLine(); while (line != null) { Matcher matcher = pattern.matcher(line); if (matcher.find()) { String path = matcher.group(1); MediaFile file = getMediaFile(mediaFileService, path); if (file != null) { ok.add(file); } else { error.add(path); } } line = reader.readLine(); } return new Pair, List>(ok, error); } public void format(List files, PrintWriter writer) throws IOException { writer.println("[playlist]"); int counter = 0; for (MediaFile file : files) { counter++; writer.println("File" + counter + '=' + file.getPath()); } writer.println("NumberOfEntries=" + counter); writer.println("Version=2"); if (writer.checkError()) { throw new IOException("Error when writing playlist."); } } } /** * Implementation of XSPF (http://www.xspf.org/) playlist format. */ private static class XSPFFormat extends PlaylistFormat { public Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { List ok = new ArrayList(); List error = new ArrayList(); SAXBuilder builder = new SAXBuilder(); Document document; try { document = builder.build(reader); } catch (JDOMException x) { LOG.warn("Failed to parse XSPF playlist.", x); throw new IOException("Failed to parse XSPF playlist."); } Element root = document.getRootElement(); Namespace ns = root.getNamespace(); Element trackList = root.getChild("trackList", ns); List tracks = trackList.getChildren("track", ns); for (Object obj : tracks) { Element track = (Element) obj; String location = track.getChildText("location", ns); if (location != null && location.startsWith("file://")) { location = location.replaceFirst("file://", ""); MediaFile file = getMediaFile(mediaFileService, location); if (file != null) { ok.add(file); } else { error.add(location); } } } return new Pair, List>(ok, error); } public void format(List files, PrintWriter writer) throws IOException { writer.println(""); writer.println(""); writer.println(" "); for (MediaFile file : files) { writer.println(" file://" + StringEscapeUtils.escapeXml(file.getPath()) + ""); } writer.println(" "); writer.println(""); if (writer.checkError()) { throw new IOException("Error when writing playlist."); } } } }