From a1a18f77a50804e0127dfa4b0f5240c49c541184 Mon Sep 17 00:00:00 2001 From: Scott Jackson Date: Mon, 2 Jul 2012 21:24:02 -0700 Subject: Initial Commit --- .../subsonic/service/PlaylistService.java | 426 +++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java') diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java new file mode 100644 index 00000000..6208c3dc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java @@ -0,0 +1,426 @@ +/* + 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."); + } + } + } +} -- cgit v1.2.3