diff options
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/controller')
57 files changed, 9584 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java new file mode 100644 index 00000000..f163f82d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java @@ -0,0 +1,60 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.support.*; +import org.springframework.web.servlet.mvc.*; +import org.springframework.ui.context.*; + +import javax.servlet.http.*; +import java.awt.*; +import java.util.*; + +/** + * Abstract super class for controllers which generate charts. + * + * @author Sindre Mehus + */ +public abstract class AbstractChartController implements Controller { + + /** + * Returns the chart background color for the current theme. + * @param request The servlet request. + * @return The chart background color. + */ + protected Color getBackground(HttpServletRequest request) { + return getColor("backgroundColor", request); + } + + /** + * Returns the chart foreground color for the current theme. + * @param request The servlet request. + * @return The chart foreground color. + */ + protected Color getForeground(HttpServletRequest request) { + return getColor("textColor", request); + } + + private Color getColor(String code, HttpServletRequest request) { + Theme theme = RequestContextUtils.getTheme(request); + Locale locale = RequestContextUtils.getLocale(request); + String colorHex = theme.getMessageSource().getMessage(code, new Object[0], locale); + return new Color(Integer.parseInt(colorHex, 16)); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java new file mode 100644 index 00000000..0b43f4eb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java @@ -0,0 +1,91 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.AdvancedSettingsCommand; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.mvc.SimpleFormController; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * Controller for the page used to administrate advanced settings. + * + * @author Sindre Mehus + */ +public class AdvancedSettingsController extends SimpleFormController { + + private SettingsService settingsService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + AdvancedSettingsCommand command = new AdvancedSettingsCommand(); + command.setCoverArtLimit(String.valueOf(settingsService.getCoverArtLimit())); + command.setDownsampleCommand(settingsService.getDownsamplingCommand()); + command.setDownloadLimit(String.valueOf(settingsService.getDownloadBitrateLimit())); + command.setUploadLimit(String.valueOf(settingsService.getUploadBitrateLimit())); + command.setStreamPort(String.valueOf(settingsService.getStreamPort())); + command.setLdapEnabled(settingsService.isLdapEnabled()); + command.setLdapUrl(settingsService.getLdapUrl()); + command.setLdapSearchFilter(settingsService.getLdapSearchFilter()); + command.setLdapManagerDn(settingsService.getLdapManagerDn()); + command.setLdapAutoShadowing(settingsService.isLdapAutoShadowing()); + command.setBrand(settingsService.getBrand()); + + return command; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + AdvancedSettingsCommand command = (AdvancedSettingsCommand) comm; + + command.setReloadNeeded(false); + settingsService.setDownsamplingCommand(command.getDownsampleCommand()); + + try { + settingsService.setCoverArtLimit(Integer.parseInt(command.getCoverArtLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setDownloadBitrateLimit(Long.parseLong(command.getDownloadLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setUploadBitrateLimit(Long.parseLong(command.getUploadLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setStreamPort(Integer.parseInt(command.getStreamPort())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + + settingsService.setLdapEnabled(command.isLdapEnabled()); + settingsService.setLdapUrl(command.getLdapUrl()); + settingsService.setLdapSearchFilter(command.getLdapSearchFilter()); + settingsService.setLdapManagerDn(command.getLdapManagerDn()); + settingsService.setLdapAutoShadowing(command.isLdapAutoShadowing()); + + if (StringUtils.isNotEmpty(command.getLdapManagerPassword())) { + settingsService.setLdapManagerPassword(command.getLdapManagerPassword()); + } + + settingsService.save(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java new file mode 100644 index 00000000..8b34f383 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java @@ -0,0 +1,38 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; + +/** + * Controller for the page which forwards to allmusic.com. + * + * @author Sindre Mehus + */ +public class AllmusicController extends ParameterizableViewController { + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("album", request.getParameter("album")); + return result; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java new file mode 100644 index 00000000..100fcedb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java @@ -0,0 +1,82 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Controller which produces avatar images. + * + * @author Sindre Mehus + */ +public class AvatarController implements Controller, LastModified { + + private SettingsService settingsService; + + public long getLastModified(HttpServletRequest request) { + Avatar avatar = getAvatar(request); + return avatar == null ? -1L : avatar.getCreatedDate().getTime(); + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + Avatar avatar = getAvatar(request); + + if (avatar == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + // TODO: specify caching filter. + + response.setContentType(avatar.getMimeType()); + response.getOutputStream().write(avatar.getData()); + return null; + } + + private Avatar getAvatar(HttpServletRequest request) { + String id = request.getParameter("id"); + if (id != null) { + return settingsService.getSystemAvatar(Integer.parseInt(id)); + } + + String username = request.getParameter("username"); + if (username == null) { + return null; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + if (userSettings.getAvatarScheme() == AvatarScheme.SYSTEM) { + return settingsService.getSystemAvatar(userSettings.getSystemAvatarId()); + } + return settingsService.getCustomAvatar(username); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java new file mode 100644 index 00000000..a22cd9a9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java @@ -0,0 +1,141 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller which receives uploaded avatar images. + * + * @author Sindre Mehus + */ +public class AvatarUploadController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(AvatarUploadController.class); + private static final int MAX_AVATAR_SIZE = 64; + + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String username = securityService.getCurrentUsername(request); + + // Check that we have a file upload request. + if (!ServletFileUpload.isMultipartContent(request)) { + throw new Exception("Illegal request."); + } + + Map<String, Object> map = new HashMap<String, Object>(); + FileItemFactory factory = new DiskFileItemFactory(); + ServletFileUpload upload = new ServletFileUpload(factory); + List<?> items = upload.parseRequest(request); + + // Look for file items. + for (Object o : items) { + FileItem item = (FileItem) o; + + if (!item.isFormField()) { + String fileName = item.getName(); + byte[] data = item.get(); + + if (StringUtils.isNotBlank(fileName) && data.length > 0) { + createAvatar(fileName, data, username, map); + } else { + map.put("error", new Exception("Missing file.")); + LOG.warn("Failed to upload personal image. No file specified."); + } + break; + } + } + + map.put("username", username); + map.put("avatar", settingsService.getCustomAvatar(username)); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private void createAvatar(String fileName, byte[] data, String username, Map<String, Object> map) throws IOException { + + BufferedImage image; + try { + image = ImageIO.read(new ByteArrayInputStream(data)); + if (image == null) { + throw new Exception("Failed to decode incoming image: " + fileName + " (" + data.length + " bytes)."); + } + int width = image.getWidth(); + int height = image.getHeight(); + String mimeType = StringUtil.getMimeType(FilenameUtils.getExtension(fileName)); + + // Scale down image if necessary. + if (width > MAX_AVATAR_SIZE || height > MAX_AVATAR_SIZE) { + double scaleFactor = (double) MAX_AVATAR_SIZE / (double) Math.max(width, height); + height = (int) (height * scaleFactor); + width = (int) (width * scaleFactor); + image = CoverArtController.scale(image, width, height); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(image, "jpeg", out); + data = out.toByteArray(); + mimeType = StringUtil.getMimeType("jpeg"); + map.put("resized", true); + } + Avatar avatar = new Avatar(0, fileName, new Date(), mimeType, width, height, data); + settingsService.setCustomAvatar(avatar, username); + LOG.info("Created avatar '" + fileName + "' (" + data.length + " bytes) for user " + username); + + } catch (Exception x) { + LOG.warn("Failed to upload personal image: " + x, x); + map.put("error", x); + } + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java new file mode 100644 index 00000000..94c88656 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java @@ -0,0 +1,72 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; + +/** + * Controller for changing cover art. + * + * @author Sindre Mehus + */ +public class ChangeCoverArtController extends ParameterizableViewController { + + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + String artist = request.getParameter("artist"); + String album = request.getParameter("album"); + MediaFile dir = mediaFileService.getMediaFile(id); + + if (artist == null) { + artist = dir.getArtist(); + } + if (album == null) { + album = dir.getAlbumName(); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("id", id); + map.put("artist", artist); + map.put("album", album); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + + return result; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java new file mode 100644 index 00000000..a5093024 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java @@ -0,0 +1,294 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Controller which produces cover art images. + * + * @author Sindre Mehus + */ +public class CoverArtController implements Controller, LastModified { + + public static final String ALBUM_COVERART_PREFIX = "al-"; + public static final String ARTIST_COVERART_PREFIX = "ar-"; + + private static final Logger LOG = Logger.getLogger(CoverArtController.class); + + private SecurityService securityService; + private MediaFileService mediaFileService; + private ArtistDao artistDao; + private AlbumDao albumDao; + + public long getLastModified(HttpServletRequest request) { + try { + File file = getImageFile(request); + if (file == null) { + return 0; // Request for the default image. + } + if (!FileUtil.exists(file)) { + return -1; + } + + return FileUtil.lastModified(file); + } catch (Exception e) { + return -1; + } + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + File file = getImageFile(request); + + if (file != null && !FileUtil.exists(file)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + // Check access. + if (file != null && !securityService.isReadAllowed(file)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + } + + // Send default image if no path is given. (No need to cache it, since it will be cached in browser.) + Integer size = ServletRequestUtils.getIntParameter(request, "size"); + if (file == null) { + sendDefault(size, response); + return null; + } + + // Optimize if no scaling is required. + if (size == null) { + sendUnscaled(file, response); + return null; + } + + // Send cached image, creating it if necessary. + try { + File cachedImage = getCachedImage(file, size); + sendImage(cachedImage, response); + } catch (IOException e) { + sendDefault(size, response); + } + + return null; + } + + private File getImageFile(HttpServletRequest request) { + String id = request.getParameter("id"); + if (id != null) { + if (id.startsWith(ALBUM_COVERART_PREFIX)) { + return getAlbumImage(Integer.valueOf(id.replace(ALBUM_COVERART_PREFIX, ""))); + } + if (id.startsWith(ARTIST_COVERART_PREFIX)) { + return getArtistImage(Integer.valueOf(id.replace(ARTIST_COVERART_PREFIX, ""))); + } + return getMediaFileImage(Integer.valueOf(id)); + } + + String path = StringUtils.trimToNull(request.getParameter("path")); + return path != null ? new File(path) : null; + } + + private File getArtistImage(int id) { + Artist artist = artistDao.getArtist(id); + return artist == null || artist.getCoverArtPath() == null ? null : new File(artist.getCoverArtPath()); + } + + private File getAlbumImage(int id) { + Album album = albumDao.getAlbum(id); + return album == null || album.getCoverArtPath() == null ? null : new File(album.getCoverArtPath()); + } + + private File getMediaFileImage(int id) { + MediaFile mediaFile = mediaFileService.getMediaFile(id); + return mediaFile == null ? null : mediaFileService.getCoverArt(mediaFile); + } + + private void sendImage(File file, HttpServletResponse response) throws IOException { + InputStream in = new FileInputStream(file); + try { + IOUtils.copy(in, response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private void sendDefault(Integer size, HttpServletResponse response) throws IOException { + InputStream in = null; + try { + in = getClass().getResourceAsStream("default_cover.jpg"); + BufferedImage image = ImageIO.read(in); + if (size != null) { + image = scale(image, size, size); + } + ImageIO.write(image, "jpeg", response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private void sendUnscaled(File file, HttpServletResponse response) throws IOException { + InputStream in = null; + try { + in = getImageInputStream(file); + IOUtils.copy(in, response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private File getCachedImage(File file, int size) throws IOException { + String md5 = DigestUtils.md5Hex(file.getPath()); + File cachedImage = new File(getImageCacheDirectory(size), md5 + ".jpeg"); + + // Is cache missing or obsolete? + if (!cachedImage.exists() || FileUtil.lastModified(file) > cachedImage.lastModified()) { + InputStream in = null; + OutputStream out = null; + try { + in = getImageInputStream(file); + out = new FileOutputStream(cachedImage); + BufferedImage image = ImageIO.read(in); + if (image == null) { + throw new Exception("Unable to decode image."); + } + + image = scale(image, size, size); + ImageIO.write(image, "jpeg", out); + + } catch (Throwable x) { + // Delete corrupt (probably empty) thumbnail cache. + LOG.warn("Failed to create thumbnail for " + file, x); + IOUtils.closeQuietly(out); + cachedImage.delete(); + throw new IOException("Failed to create thumbnail for " + file + ". " + x.getMessage()); + + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + } + } + return cachedImage; + } + + /** + * Returns an input stream to the image in the given file. If the file is an audio file, + * the embedded album art is returned. + */ + private InputStream getImageInputStream(File file) throws IOException { + JaudiotaggerParser parser = new JaudiotaggerParser(); + if (parser.isApplicable(file)) { + MediaFile mediaFile = mediaFileService.getMediaFile(file); + return new ByteArrayInputStream(parser.getImageData(mediaFile)); + } else { + return new FileInputStream(file); + } + } + + private synchronized File getImageCacheDirectory(int size) { + File dir = new File(SettingsService.getSubsonicHome(), "thumbs"); + dir = new File(dir, String.valueOf(size)); + if (!dir.exists()) { + if (dir.mkdirs()) { + LOG.info("Created thumbnail cache " + dir); + } else { + LOG.error("Failed to create thumbnail cache " + dir); + } + } + + return dir; + } + + public static BufferedImage scale(BufferedImage image, int width, int height) { + int w = image.getWidth(); + int h = image.getHeight(); + BufferedImage thumb = image; + + // For optimal results, use step by step bilinear resampling - halfing the size at each step. + do { + w /= 2; + h /= 2; + if (w < width) { + w = width; + } + if (h < height) { + h = height; + } + + BufferedImage temp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = temp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null); + g2.dispose(); + + thumb = temp; + } while (w != width); + + return thumb; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java new file mode 100644 index 00000000..17d06497 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java @@ -0,0 +1,66 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.dao.DaoHelper; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the DB admin page. + * + * @author Sindre Mehus + */ +public class DBController extends ParameterizableViewController { + + private DaoHelper daoHelper; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + String query = request.getParameter("query"); + if (query != null) { + map.put("query", query); + + try { + List<?> result = daoHelper.getJdbcTemplate().query(query, new ColumnMapRowMapper()); + map.put("result", result); + } catch (DataAccessException x) { + map.put("error", ExceptionUtils.getRootCause(x).getMessage()); + } + } + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setDaoHelper(DaoHelper daoHelper) { + this.daoHelper = daoHelper; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java new file mode 100644 index 00000000..144d3327 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java @@ -0,0 +1,74 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.DonateCommand; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; + +/** + * Controller for the donation page. + * + * @author Sindre Mehus + */ +public class DonateController extends SimpleFormController { + + private SettingsService settingsService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + DonateCommand command = new DonateCommand(); + command.setPath(request.getParameter("path")); + + command.setEmailAddress(settingsService.getLicenseEmail()); + command.setLicenseDate(settingsService.getLicenseDate()); + command.setLicenseValid(settingsService.isLicenseValid()); + command.setLicense(settingsService.getLicenseCode()); + command.setBrand(settingsService.getBrand()); + + return command; + } + + protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors) + throws Exception { + DonateCommand command = (DonateCommand) com; + Date now = new Date(); + + settingsService.setLicenseCode(command.getLicense()); + settingsService.setLicenseEmail(command.getEmailAddress()); + settingsService.setLicenseDate(now); + settingsService.save(); + settingsService.validateLicenseAsync(); + + // Reflect changes in view. The validator has already validated the license. + command.setLicenseValid(true); + command.setLicenseDate(now); + + return new ModelAndView(getSuccessView(), errors.getModel()); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java new file mode 100644 index 00000000..0125d3bb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java @@ -0,0 +1,453 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.io.RangeOutputStream; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.math.LongRange; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipOutputStream; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; + +/** + * A controller used for downloading files to a remote client. If the requested path refers to a file, the + * given file is downloaded. If the requested path refers to a directory, the entire directory (including + * sub-directories) are downloaded as an uncompressed zip-file. + * + * @author Sindre Mehus + */ +public class DownloadController implements Controller, LastModified { + + private static final Logger LOG = Logger.getLogger(DownloadController.class); + + private PlayerService playerService; + private StatusService statusService; + private SecurityService securityService; + private PlaylistService playlistService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + + public long getLastModified(HttpServletRequest request) { + try { + MediaFile mediaFile = getSingleFile(request); + if (mediaFile == null || mediaFile.isDirectory() || mediaFile.getChanged() == null) { + return -1; + } + return mediaFile.getChanged().getTime(); + } catch (ServletRequestBindingException e) { + return -1; + } + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + TransferStatus status = null; + try { + + status = statusService.createDownloadStatus(playerService.getPlayer(request, response, false, false)); + + MediaFile mediaFile = getSingleFile(request); + String dir = request.getParameter("dir"); + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + String playerId = request.getParameter("player"); + int[] indexes = ServletRequestUtils.getIntParameters(request, "i"); + + if (mediaFile != null) { + response.setIntHeader("ETag", mediaFile.getId()); + response.setHeader("Accept-Ranges", "bytes"); + } + + LongRange range = StringUtil.parseRange(request.getHeader("Range")); + if (range != null) { + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + LOG.info("Got range: " + range); + } + + if (mediaFile != null) { + File file = mediaFile.getFile(); + if (!securityService.isReadAllowed(file)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + } + + if (file.isFile()) { + downloadFile(response, status, file, range); + } else { + downloadDirectory(response, status, file, range); + } + } else if (dir != null) { + File file = new File(dir); + if (!securityService.isReadAllowed(file)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + } + downloadFiles(response, status, file, indexes); + + } else if (playlistId != null) { + List<MediaFile> songs = playlistService.getFilesInPlaylist(playlistId); + downloadFiles(response, status, songs, null, range); + + } else if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + PlayQueue playQueue = player.getPlayQueue(); + playQueue.setName("Playlist"); + downloadFiles(response, status, playQueue.getFiles(), indexes.length == 0 ? null : indexes, range); + } + + + } finally { + if (status != null) { + statusService.removeDownloadStatus(status); + User user = securityService.getCurrentUser(request); + securityService.updateUserByteCounts(user, 0L, status.getBytesTransfered(), 0L); + } + } + + return null; + } + + private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException { + String path = request.getParameter("path"); + if (path != null) { + return mediaFileService.getMediaFile(path); + } + Integer id = ServletRequestUtils.getIntParameter(request, "id"); + if (id != null) { + return mediaFileService.getMediaFile(id); + } + return null; + } + + /** + * Downloads a single file. + * + * @param response The HTTP response. + * @param status The download status. + * @param file The file to download. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void downloadFile(HttpServletResponse response, TransferStatus status, File file, LongRange range) throws IOException { + LOG.info("Starting to download '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer()); + status.setFile(file); + + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + '\"'); + if (range == null) { + Util.setContentLength(response, file.length()); + } + + copyFileToStream(file, RangeOutputStream.wrap(response.getOutputStream(), range), status, range); + LOG.info("Downloaded '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer()); + } + + /** + * Downloads a collection of files within a directory. + * + * @param response The HTTP response. + * @param status The download status. + * @param dir The directory. + * @param indexes Only download files with these indexes within the directory. + * @throws IOException If an I/O error occurs. + */ + private void downloadFiles(HttpServletResponse response, TransferStatus status, File dir, int[] indexes) throws IOException { + String zipFileName = dir.getName() + ".zip"; + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + status.setFile(dir); + + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + "\""); + + ZipOutputStream out = new ZipOutputStream(response.getOutputStream()); + out.setMethod(ZipOutputStream.STORED); // No compression. + + List<MediaFile> allChildren = mediaFileService.getChildrenOf(dir, true, true, true); + List<MediaFile> mediaFiles = new ArrayList<MediaFile>(); + for (int index : indexes) { + mediaFiles.add(allChildren.get(index)); + } + + for (MediaFile mediaFile : mediaFiles) { + zip(out, mediaFile.getParentFile(), mediaFile.getFile(), status, null); + } + + out.close(); + LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer()); + } + + /** + * Downloads all files in a directory (including sub-directories). The files are packed together in an + * uncompressed zip-file. + * + * @param response The HTTP response. + * @param status The download status. + * @param file The file to download. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void downloadDirectory(HttpServletResponse response, TransferStatus status, File file, LongRange range) throws IOException { + String zipFileName = file.getName() + ".zip"; + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + '"'); + + ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range)); + out.setMethod(ZipOutputStream.STORED); // No compression. + + zip(out, file.getParentFile(), file, status, range); + out.close(); + LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer()); + } + + /** + * Downloads the given files. The files are packed together in an + * uncompressed zip-file. + * + * @param response The HTTP response. + * @param status The download status. + * @param files The files to download. + * @param indexes Only download songs at these indexes. May be <code>null</code>. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void downloadFiles(HttpServletResponse response, TransferStatus status, List<MediaFile> files, int[] indexes, LongRange range) throws IOException { + if (indexes != null && indexes.length == 1) { + downloadFile(response, status, files.get(indexes[0]).getFile(), range); + return; + } + + String zipFileName = "download.zip"; + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + '"'); + + ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range)); + out.setMethod(ZipOutputStream.STORED); // No compression. + + List<MediaFile> filesToDownload = new ArrayList<MediaFile>(); + if (indexes == null) { + filesToDownload.addAll(files); + } else { + for (int index : indexes) { + try { + filesToDownload.add(files.get(index)); + } catch (IndexOutOfBoundsException x) { /* Ignored */} + } + } + + for (MediaFile mediaFile : filesToDownload) { + zip(out, mediaFile.getParentFile(), mediaFile.getFile(), status, range); + } + + out.close(); + LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer()); + } + + /** + * Utility method for writing the content of a given file to a given output stream. + * + * @param file The file to copy. + * @param out The output stream to write to. + * @param status The download status. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void copyFileToStream(File file, OutputStream out, TransferStatus status, LongRange range) throws IOException { + LOG.info("Downloading '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer()); + + final int bufferSize = 16 * 1024; // 16 Kbit + InputStream in = new BufferedInputStream(new FileInputStream(file), bufferSize); + + try { + byte[] buf = new byte[bufferSize]; + long bitrateLimit = 0; + long lastLimitCheck = 0; + + while (true) { + long before = System.currentTimeMillis(); + int n = in.read(buf); + if (n == -1) { + break; + } + out.write(buf, 0, n); + + // Don't sleep if outside range. + if (range != null && !range.containsLong(status.getBytesSkipped() + status.getBytesTransfered())) { + status.addBytesSkipped(n); + continue; + } + + status.addBytesTransfered(n); + long after = System.currentTimeMillis(); + + // Calculate bitrate limit every 5 seconds. + if (after - lastLimitCheck > 5000) { + bitrateLimit = 1024L * settingsService.getDownloadBitrateLimit() / + Math.max(1, statusService.getAllDownloadStatuses().size()); + lastLimitCheck = after; + } + + // Sleep for a while to throttle bitrate. + if (bitrateLimit != 0) { + long sleepTime = 8L * 1000 * bufferSize / bitrateLimit - (after - before); + if (sleepTime > 0L) { + try { + Thread.sleep(sleepTime); + } catch (Exception x) { + LOG.warn("Failed to sleep.", x); + } + } + } + } + } finally { + out.flush(); + IOUtils.closeQuietly(in); + } + } + + /** + * Writes a file or a directory structure to a zip output stream. File entries in the zip file are relative + * to the given root. + * + * @param out The zip output stream. + * @param root The root of the directory structure. Used to create path information in the zip file. + * @param file The file or directory to zip. + * @param status The download status. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void zip(ZipOutputStream out, File root, File file, TransferStatus status, LongRange range) throws IOException { + + // Exclude all hidden files starting with a "." + if (file.getName().startsWith(".")) { + return; + } + + String zipName = file.getCanonicalPath().substring(root.getCanonicalPath().length() + 1); + + if (file.isFile()) { + status.setFile(file); + + ZipEntry zipEntry = new ZipEntry(zipName); + zipEntry.setSize(file.length()); + zipEntry.setCompressedSize(file.length()); + zipEntry.setCrc(computeCrc(file)); + + out.putNextEntry(zipEntry); + copyFileToStream(file, out, status, range); + out.closeEntry(); + + } else { + ZipEntry zipEntry = new ZipEntry(zipName + '/'); + zipEntry.setSize(0); + zipEntry.setCompressedSize(0); + zipEntry.setCrc(0); + + out.putNextEntry(zipEntry); + out.closeEntry(); + + File[] children = FileUtil.listFiles(file); + for (File child : children) { + zip(out, root, child, status, range); + } + } + } + + /** + * Computes the CRC checksum for the given file. + * + * @param file The file to compute checksum for. + * @return A CRC32 checksum. + * @throws IOException If an I/O error occurs. + */ + private long computeCrc(File file) throws IOException { + CRC32 crc = new CRC32(); + InputStream in = new FileInputStream(file); + + try { + + byte[] buf = new byte[8192]; + int n = in.read(buf); + while (n != -1) { + crc.update(buf, 0, n); + n = in.read(buf); + } + + } finally { + in.close(); + } + + return crc.getValue(); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java new file mode 100644 index 00000000..91492222 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java @@ -0,0 +1,194 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.service.metadata.MetaData; +import net.sourceforge.subsonic.service.metadata.MetaDataParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; + +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Controller for the page used to edit MP3 tags. + * + * @author Sindre Mehus + */ +public class EditTagsController extends ParameterizableViewController { + + private MetaDataParserFactory metaDataParserFactory; + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile dir = mediaFileService.getMediaFile(id); + List<MediaFile> files = mediaFileService.getChildrenOf(dir, true, false, true); + + Map<String, Object> map = new HashMap<String, Object>(); + if (!files.isEmpty()) { + map.put("defaultArtist", files.get(0).getArtist()); + map.put("defaultAlbum", files.get(0).getAlbumName()); + map.put("defaultYear", files.get(0).getYear()); + map.put("defaultGenre", files.get(0).getGenre()); + } + map.put("allGenres", JaudiotaggerParser.getID3V1Genres()); + + List<Song> songs = new ArrayList<Song>(); + for (int i = 0; i < files.size(); i++) { + songs.add(createSong(files.get(i), i)); + } + map.put("id", id); + map.put("songs", songs); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private Song createSong(MediaFile file, int index) { + MetaDataParser parser = metaDataParserFactory.getParser(file.getFile()); + MetaData metaData = parser.getRawMetaData(file.getFile()); + + Song song = new Song(); + song.setId(file.getId()); + song.setFileName(FilenameUtils.getBaseName(file.getPath())); + song.setTrack(metaData.getTrackNumber()); + song.setSuggestedTrack(index + 1); + song.setTitle(metaData.getTitle()); + song.setSuggestedTitle(parser.guessTitle(file.getFile())); + song.setArtist(metaData.getArtist()); + song.setAlbum(metaData.getAlbumName()); + song.setYear(metaData.getYear()); + song.setGenre(metaData.getGenre()); + return song; + } + + public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { + this.metaDataParserFactory = metaDataParserFactory; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + /** + * Contains information about a single song. + */ + public static class Song { + private int id; + private String fileName; + private Integer suggestedTrack; + private Integer track; + private String suggestedTitle; + private String title; + private String artist; + private String album; + private Integer year; + private String genre; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Integer getSuggestedTrack() { + return suggestedTrack; + } + + public void setSuggestedTrack(Integer suggestedTrack) { + this.suggestedTrack = suggestedTrack; + } + + public Integer getTrack() { + return track; + } + + public void setTrack(Integer track) { + this.track = track; + } + + public String getSuggestedTitle() { + return suggestedTitle; + } + + public void setSuggestedTitle(String suggestedTitle) { + this.suggestedTitle = suggestedTitle; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getAlbum() { + return album; + } + + public void setAlbum(String album) { + this.album = album; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java new file mode 100644 index 00000000..d8d28f93 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java @@ -0,0 +1,179 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.ShareDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.RandomStringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the page used to play shared music (Twitter, Facebook etc). + * + * @author Sindre Mehus + */ +public class ExternalPlayerController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(ExternalPlayerController.class); + private static final String GUEST_USERNAME = "guest"; + + private SettingsService settingsService; + private SecurityService securityService; + private PlayerService playerService; + private ShareDao shareDao; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + String pathInfo = request.getPathInfo(); + + if (pathInfo == null || !pathInfo.startsWith("/")) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + Share share = shareDao.getShareByName(pathInfo.substring(1)); + if (share == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + if (share.getExpires() != null && share.getExpires().before(new Date())) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + share.setLastVisited(new Date()); + share.setVisitCount(share.getVisitCount() + 1); + shareDao.updateShare(share); + + List<MediaFile> songs = getSongs(share); + List<File> coverArts = getCoverArts(songs); + + map.put("share", share); + map.put("songs", songs); + map.put("coverArts", coverArts); + + if (!coverArts.isEmpty()) { + map.put("coverArt", coverArts.get(0)); + } + map.put("redirectFrom", settingsService.getUrlRedirectFrom()); + map.put("player", getPlayer(request).getId()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List<MediaFile> getSongs(Share share) throws IOException { + List<MediaFile> result = new ArrayList<MediaFile>(); + + for (String path : shareDao.getSharedFiles(share.getId())) { + try { + MediaFile file = mediaFileService.getMediaFile(path); + if (file.getFile().exists()) { + if (file.isDirectory()) { + result.addAll(mediaFileService.getChildrenOf(file, true, false, true)); + } else { + result.add(file); + } + } + } catch (Exception x) { + LOG.warn("Couldn't read file " + path); + } + } + return result; + } + + private List<File> getCoverArts(List<MediaFile> songs) throws IOException { + List<File> result = new ArrayList<File>(); + for (MediaFile song : songs) { + result.add(mediaFileService.getCoverArt(song)); + } + return result; + } + + + private Player getPlayer(HttpServletRequest request) { + + // Create guest user if necessary. + User user = securityService.getUserByName(GUEST_USERNAME); + if (user == null) { + user = new User(GUEST_USERNAME, RandomStringUtils.randomAlphanumeric(30), null); + user.setStreamRole(true); + securityService.createUser(user); + } + + // Look for existing player. + List<Player> players = playerService.getPlayersForUserAndClientId(GUEST_USERNAME, null); + if (!players.isEmpty()) { + return players.get(0); + } + + // Create player if necessary. + Player player = new Player(); + player.setIpAddress(request.getRemoteAddr()); + player.setUsername(GUEST_USERNAME); + playerService.createPlayer(player); + + return player; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setShareDao(ShareDao shareDao) { + this.shareDao = shareDao; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java new file mode 100644 index 00000000..e7b19b04 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java @@ -0,0 +1,114 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.GeneralSettingsCommand; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Locale; + +/** + * Controller for the page used to administrate general settings. + * + * @author Sindre Mehus + */ +public class GeneralSettingsController extends SimpleFormController { + + private SettingsService settingsService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + GeneralSettingsCommand command = new GeneralSettingsCommand(); + command.setCoverArtFileTypes(settingsService.getCoverArtFileTypes()); + command.setIgnoredArticles(settingsService.getIgnoredArticles()); + command.setShortcuts(settingsService.getShortcuts()); + command.setIndex(settingsService.getIndexString()); + command.setMusicFileTypes(settingsService.getMusicFileTypes()); + command.setVideoFileTypes(settingsService.getVideoFileTypes()); + command.setSortAlbumsByYear(settingsService.isSortAlbumsByYear()); + command.setGettingStartedEnabled(settingsService.isGettingStartedEnabled()); + command.setWelcomeTitle(settingsService.getWelcomeTitle()); + command.setWelcomeSubtitle(settingsService.getWelcomeSubtitle()); + command.setWelcomeMessage(settingsService.getWelcomeMessage()); + command.setLoginMessage(settingsService.getLoginMessage()); + + Theme[] themes = settingsService.getAvailableThemes(); + command.setThemes(themes); + String currentThemeId = settingsService.getThemeId(); + for (int i = 0; i < themes.length; i++) { + if (currentThemeId.equals(themes[i].getId())) { + command.setThemeIndex(String.valueOf(i)); + break; + } + } + + Locale currentLocale = settingsService.getLocale(); + Locale[] locales = settingsService.getAvailableLocales(); + String[] localeStrings = new String[locales.length]; + for (int i = 0; i < locales.length; i++) { + localeStrings[i] = locales[i].getDisplayName(locales[i]); + + if (currentLocale.equals(locales[i])) { + command.setLocaleIndex(String.valueOf(i)); + } + } + command.setLocales(localeStrings); + + return command; + + } + + protected void doSubmitAction(Object comm) throws Exception { + GeneralSettingsCommand command = (GeneralSettingsCommand) comm; + + int themeIndex = Integer.parseInt(command.getThemeIndex()); + Theme theme = settingsService.getAvailableThemes()[themeIndex]; + + int localeIndex = Integer.parseInt(command.getLocaleIndex()); + Locale locale = settingsService.getAvailableLocales()[localeIndex]; + + command.setReloadNeeded(!settingsService.getIndexString().equals(command.getIndex()) || + !settingsService.getIgnoredArticles().equals(command.getIgnoredArticles()) || + !settingsService.getShortcuts().equals(command.getShortcuts()) || + !settingsService.getThemeId().equals(theme.getId()) || + !settingsService.getLocale().equals(locale)); + + settingsService.setIndexString(command.getIndex()); + settingsService.setIgnoredArticles(command.getIgnoredArticles()); + settingsService.setShortcuts(command.getShortcuts()); + settingsService.setMusicFileTypes(command.getMusicFileTypes()); + settingsService.setVideoFileTypes(command.getVideoFileTypes()); + settingsService.setCoverArtFileTypes(command.getCoverArtFileTypes()); + settingsService.setSortAlbumsByYear(command.isSortAlbumsByYear()); + settingsService.setGettingStartedEnabled(command.isGettingStartedEnabled()); + settingsService.setWelcomeTitle(command.getWelcomeTitle()); + settingsService.setWelcomeSubtitle(command.getWelcomeSubtitle()); + settingsService.setWelcomeMessage(command.getWelcomeMessage()); + settingsService.setLoginMessage(command.getLoginMessage()); + settingsService.setThemeId(theme.getId()); + settingsService.setLocale(locale); + settingsService.save(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java new file mode 100644 index 00000000..4e0b0945 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java @@ -0,0 +1,80 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.service.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Controller for the help page. + * + * @author Sindre Mehus + */ +public class HelpController extends ParameterizableViewController { + + private VersionService versionService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + if (versionService.isNewFinalVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestFinalVersion()); + } else if (versionService.isNewBetaVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestBetaVersion()); + } + + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + + String serverInfo = request.getSession().getServletContext().getServerInfo() + + ", java " + System.getProperty("java.version") + + ", " + System.getProperty("os.name"); + + map.put("brand", settingsService.getBrand()); + map.put("localVersion", versionService.getLocalVersion()); + map.put("buildDate", versionService.getLocalBuildDate()); + map.put("buildNumber", versionService.getLocalBuildNumber()); + map.put("serverInfo", serverInfo); + map.put("usedMemory", totalMemory - freeMemory); + map.put("totalMemory", totalMemory); + map.put("logEntries", Logger.getLatestLogEntries()); + map.put("logFile", Logger.getLogFile()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java new file mode 100644 index 00000000..49c95926 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java @@ -0,0 +1,340 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the home page. + * + * @author Sindre Mehus + */ +public class HomeController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(HomeController.class); + + private static final int DEFAULT_LIST_SIZE = 10; + private static final int MAX_LIST_SIZE = 500; + private static final int DEFAULT_LIST_OFFSET = 0; + private static final int MAX_LIST_OFFSET = 5000; + + private SettingsService settingsService; + private MediaScannerService mediaScannerService; + private RatingService ratingService; + private SecurityService securityService; + private MediaFileService mediaFileService; + private SearchService searchService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + if (user.isAdminRole() && settingsService.isGettingStartedEnabled()) { + return new ModelAndView(new RedirectView("gettingStarted.view")); + } + + int listSize = DEFAULT_LIST_SIZE; + int listOffset = DEFAULT_LIST_OFFSET; + if (request.getParameter("listSize") != null) { + listSize = Math.max(0, Math.min(Integer.parseInt(request.getParameter("listSize")), MAX_LIST_SIZE)); + } + if (request.getParameter("listOffset") != null) { + listOffset = Math.max(0, Math.min(Integer.parseInt(request.getParameter("listOffset")), MAX_LIST_OFFSET)); + } + + String listType = request.getParameter("listType"); + if (listType == null) { + listType = "random"; + } + + List<Album> albums; + if ("highest".equals(listType)) { + albums = getHighestRated(listOffset, listSize); + } else if ("frequent".equals(listType)) { + albums = getMostFrequent(listOffset, listSize); + } else if ("recent".equals(listType)) { + albums = getMostRecent(listOffset, listSize); + } else if ("newest".equals(listType)) { + albums = getNewest(listOffset, listSize); + } else if ("starred".equals(listType)) { + albums = getStarred(listOffset, listSize, user.getUsername()); + } else if ("random".equals(listType)) { + albums = getRandom(listSize); + } else if ("alphabetical".equals(listType)) { + albums = getAlphabetical(listOffset, listSize, true); + } else { + albums = Collections.emptyList(); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("albums", albums); + map.put("welcomeTitle", settingsService.getWelcomeTitle()); + map.put("welcomeSubtitle", settingsService.getWelcomeSubtitle()); + map.put("welcomeMessage", settingsService.getWelcomeMessage()); + map.put("isIndexBeingCreated", mediaScannerService.isScanning()); + map.put("listType", listType); + map.put("listSize", listSize); + map.put("listOffset", listOffset); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + List<Album> getHighestRated(int offset, int count) { + List<Album> result = new ArrayList<Album>(); + for (MediaFile mediaFile : ratingService.getHighestRated(offset, count)) { + Album album = createAlbum(mediaFile); + if (album != null) { + album.setRating((int) Math.round(ratingService.getAverageRating(mediaFile) * 10.0D)); + result.add(album); + } + } + return result; + } + + List<Album> getMostFrequent(int offset, int count) { + List<Album> result = new ArrayList<Album>(); + for (MediaFile mediaFile : mediaFileService.getMostFrequentlyPlayedAlbums(offset, count)) { + Album album = createAlbum(mediaFile); + if (album != null) { + album.setPlayCount(mediaFile.getPlayCount()); + result.add(album); + } + } + return result; + } + + List<Album> getMostRecent(int offset, int count) { + List<Album> result = new ArrayList<Album>(); + for (MediaFile mediaFile : mediaFileService.getMostRecentlyPlayedAlbums(offset, count)) { + Album album = createAlbum(mediaFile); + if (album != null) { + album.setLastPlayed(mediaFile.getLastPlayed()); + result.add(album); + } + } + return result; + } + + List<Album> getNewest(int offset, int count) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : mediaFileService.getNewestAlbums(offset, count)) { + Album album = createAlbum(file); + if (album != null) { + Date created = file.getCreated(); + if (created == null) { + created = file.getChanged(); + } + album.setCreated(created); + result.add(album); + } + } + return result; + } + + List<Album> getStarred(int offset, int count, String username) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : mediaFileService.getStarredAlbums(offset, count, username)) { + Album album = createAlbum(file); + if (album != null) { + result.add(album); + } + } + return result; + } + + List<Album> getRandom(int count) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : searchService.getRandomAlbums(count)) { + Album album = createAlbum(file); + if (album != null) { + result.add(album); + } + } + return result; + } + + List<Album> getAlphabetical(int offset, int count, boolean byArtist) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : mediaFileService.getAlphabetialAlbums(offset, count, byArtist)) { + Album album = createAlbum(file); + if (album != null) { + result.add(album); + } + } + return result; + } + + private Album createAlbum(MediaFile file) { + Album album = new Album(); + album.setId(file.getId()); + album.setPath(file.getPath()); + try { + resolveArtistAndAlbumTitle(album, file); + resolveCoverArt(album, file); + } catch (Exception x) { + LOG.warn("Failed to create albumTitle list entry for " + file.getPath(), x); + return null; + } + return album; + } + + private void resolveArtistAndAlbumTitle(Album album, MediaFile file) throws IOException { + album.setArtist(file.getArtist()); + album.setAlbumTitle(file.getAlbumName()); + } + + private void resolveCoverArt(Album album, MediaFile file) { + album.setCoverArtPath(file.getCoverArtPath()); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + /** + * Contains info for a single album. + */ + @Deprecated + public static class Album { + private String path; + private String coverArtPath; + private String artist; + private String albumTitle; + private Date created; + private Date lastPlayed; + private Integer playCount; + private Integer rating; + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getCoverArtPath() { + return coverArtPath; + } + + public void setCoverArtPath(String coverArtPath) { + this.coverArtPath = coverArtPath; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getAlbumTitle() { + return albumTitle; + } + + public void setAlbumTitle(String albumTitle) { + this.albumTitle = albumTitle; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastPlayed() { + return lastPlayed; + } + + public void setLastPlayed(Date lastPlayed) { + this.lastPlayed = lastPlayed; + } + + public Integer getPlayCount() { + return playCount; + } + + public void setPlayCount(Integer playCount) { + this.playCount = playCount; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java new file mode 100644 index 00000000..55e9b200 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java @@ -0,0 +1,93 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * @author Sindre Mehus + */ +public class ImportPlaylistController extends ParameterizableViewController { + + private static final long MAX_PLAYLIST_SIZE_MB = 5L; + + private SecurityService securityService; + private PlaylistService playlistService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + try { + if (ServletFileUpload.isMultipartContent(request)) { + + FileItemFactory factory = new DiskFileItemFactory(); + ServletFileUpload upload = new ServletFileUpload(factory); + List<?> items = upload.parseRequest(request); + for (Object o : items) { + FileItem item = (FileItem) o; + + if ("file".equals(item.getFieldName()) && !StringUtils.isBlank(item.getName())) { + if (item.getSize() > MAX_PLAYLIST_SIZE_MB * 1024L * 1024L) { + throw new Exception("The playlist file is too large. Max file size is " + MAX_PLAYLIST_SIZE_MB + " MB."); + } + String playlistName = FilenameUtils.getBaseName(item.getName()); + String fileName = FilenameUtils.getName(item.getName()); + String format = StringUtils.lowerCase(FilenameUtils.getExtension(item.getName())); + String username = securityService.getCurrentUsername(request); + Playlist playlist = playlistService.importPlaylist(username, playlistName, fileName, format, item.getInputStream()); + map.put("playlist", playlist); + } + } + } + } catch (Exception e) { + map.put("error", e.getMessage()); + } + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java new file mode 100644 index 00000000..5ee7b799 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java @@ -0,0 +1,116 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Date; + +/** + * Controller for the page used to administrate the set of internet radio/tv stations. + * + * @author Sindre Mehus + */ +public class InternetRadioSettingsController extends ParameterizableViewController { + + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + if (isFormSubmission(request)) { + String error = handleParameters(request); + map.put("error", error); + if (error == null) { + map.put("reload", true); + } + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("internetRadios", settingsService.getAllInternetRadios(true)); + + result.addObject("model", map); + return result; + } + + /** + * Determine if the given request represents a form submission. + * + * @param request current HTTP request + * @return if the request represents a form submission + */ + private boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + private String handleParameters(HttpServletRequest request) { + List<InternetRadio> radios = settingsService.getAllInternetRadios(true); + for (InternetRadio radio : radios) { + Integer id = radio.getId(); + String streamUrl = getParameter(request, "streamUrl", id); + String homepageUrl = getParameter(request, "homepageUrl", id); + String name = getParameter(request, "name", id); + boolean enabled = getParameter(request, "enabled", id) != null; + boolean delete = getParameter(request, "delete", id) != null; + + if (delete) { + settingsService.deleteInternetRadio(id); + } else { + if (name == null) { + return "internetradiosettings.noname"; + } + if (streamUrl == null) { + return "internetradiosettings.nourl"; + } + settingsService.updateInternetRadio(new InternetRadio(id, name, streamUrl, homepageUrl, enabled, new Date())); + } + } + + String name = StringUtils.trimToNull(request.getParameter("name")); + String streamUrl = StringUtils.trimToNull(request.getParameter("streamUrl")); + String homepageUrl = StringUtils.trimToNull(request.getParameter("homepageUrl")); + boolean enabled = StringUtils.trimToNull(request.getParameter("enabled")) != null; + + if (name != null && streamUrl != null) { + settingsService.createInternetRadio(new InternetRadio(name, streamUrl, homepageUrl, enabled, new Date())); + } + + return null; + } + + private String getParameter(HttpServletRequest request, String name, Integer id) { + return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]")); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java new file mode 100644 index 00000000..d273f0b9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java @@ -0,0 +1,270 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.service.PlaylistService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.LastModified; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.support.RequestContextUtils; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.MusicIndexService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller for the left index frame. + * + * @author Sindre Mehus + */ +public class LeftController extends ParameterizableViewController implements LastModified { + + private static final Logger LOG = Logger.getLogger(LeftController.class); + + // Update this time if you want to force a refresh in clients. + private static final Calendar LAST_COMPATIBILITY_TIME = Calendar.getInstance(); + static { + LAST_COMPATIBILITY_TIME.set(2012, Calendar.MARCH, 6, 0, 0, 0); + LAST_COMPATIBILITY_TIME.set(Calendar.MILLISECOND, 0); + } + + private MediaScannerService mediaScannerService; + private SettingsService settingsService; + private SecurityService securityService; + private MediaFileService mediaFileService; + private MusicIndexService musicIndexService; + private PlayerService playerService; + private PlaylistService playlistService; + + public long getLastModified(HttpServletRequest request) { + saveSelectedMusicFolder(request); + + if (mediaScannerService.isScanning()) { + return -1L; + } + + long lastModified = LAST_COMPATIBILITY_TIME.getTimeInMillis(); + String username = securityService.getCurrentUsername(request); + + // When was settings last changed? + lastModified = Math.max(lastModified, settingsService.getSettingsChanged()); + + // When was music folder(s) on disk last changed? + List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders(); + MusicFolder selectedMusicFolder = getSelectedMusicFolder(request); + if (selectedMusicFolder != null) { + File file = selectedMusicFolder.getPath(); + lastModified = Math.max(lastModified, FileUtil.lastModified(file)); + } else { + for (MusicFolder musicFolder : allMusicFolders) { + File file = musicFolder.getPath(); + lastModified = Math.max(lastModified, FileUtil.lastModified(file)); + } + } + + // When was music folder table last changed? + for (MusicFolder musicFolder : allMusicFolders) { + lastModified = Math.max(lastModified, musicFolder.getChanged().getTime()); + } + + // When was internet radio table last changed? + for (InternetRadio internetRadio : settingsService.getAllInternetRadios()) { + lastModified = Math.max(lastModified, internetRadio.getChanged().getTime()); + } + + // When was user settings last changed? + UserSettings userSettings = settingsService.getUserSettings(username); + lastModified = Math.max(lastModified, userSettings.getChanged().getTime()); + + return lastModified; + } + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + saveSelectedMusicFolder(request); + Map<String, Object> map = new HashMap<String, Object>(); + + MediaLibraryStatistics statistics = mediaScannerService.getStatistics(); + Locale locale = RequestContextUtils.getLocale(request); + + String username = securityService.getCurrentUsername(request); + List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders(); + MusicFolder selectedMusicFolder = getSelectedMusicFolder(request); + List<MusicFolder> musicFoldersToUse = selectedMusicFolder == null ? allMusicFolders : Arrays.asList(selectedMusicFolder); + String[] shortcuts = settingsService.getShortcutsAsArray(); + UserSettings userSettings = settingsService.getUserSettings(username); + + MusicFolderContent musicFolderContent = getMusicFolderContent(musicFoldersToUse); + + map.put("player", playerService.getPlayer(request, response)); + map.put("scanning", mediaScannerService.isScanning()); + map.put("musicFolders", allMusicFolders); + map.put("selectedMusicFolder", selectedMusicFolder); + map.put("radios", settingsService.getAllInternetRadios()); + map.put("shortcuts", getShortcuts(musicFoldersToUse, shortcuts)); + map.put("captionCutoff", userSettings.getMainVisibility().getCaptionCutoff()); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("organizeByFolderStructure", settingsService.isOrganizeByFolderStructure()); + + if (statistics != null) { + map.put("statistics", statistics); + long bytes = statistics.getTotalLengthInBytes(); + long hours = statistics.getTotalDurationInSeconds() / 3600L; + map.put("hours", hours); + map.put("bytes", StringUtil.formatBytes(bytes, locale)); + } + + map.put("indexedArtists", musicFolderContent.getIndexedArtists()); + map.put("singleSongs", musicFolderContent.getSingleSongs()); + map.put("indexes", musicFolderContent.getIndexedArtists().keySet()); + map.put("user", securityService.getCurrentUser(request)); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private void saveSelectedMusicFolder(HttpServletRequest request) { + if (request.getParameter("musicFolderId") == null) { + return; + } + int musicFolderId = Integer.parseInt(request.getParameter("musicFolderId")); + + // Note: UserSettings.setChanged() is intentionally not called. This would break browser caching + // of the left frame. + UserSettings settings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + settings.setSelectedMusicFolderId(musicFolderId); + settingsService.updateUserSettings(settings); + } + + /** + * Returns the selected music folder, or <code>null</code> if all music folders should be displayed. + */ + private MusicFolder getSelectedMusicFolder(HttpServletRequest request) { + UserSettings settings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + int musicFolderId = settings.getSelectedMusicFolderId(); + + return settingsService.getMusicFolderById(musicFolderId); + } + + protected List<MediaFile> getSingleSongs(List<MusicFolder> folders) throws IOException { + List<MediaFile> result = new ArrayList<MediaFile>(); + for (MusicFolder folder : folders) { + MediaFile parent = mediaFileService.getMediaFile(folder.getPath(), true); + result.addAll(mediaFileService.getChildrenOf(parent, true, false, true, true)); + } + return result; + } + + public List<MediaFile> getShortcuts(List<MusicFolder> musicFoldersToUse, String[] shortcuts) { + List<MediaFile> result = new ArrayList<MediaFile>(); + + for (String shortcut : shortcuts) { + for (MusicFolder musicFolder : musicFoldersToUse) { + File file = new File(musicFolder.getPath(), shortcut); + if (FileUtil.exists(file)) { + result.add(mediaFileService.getMediaFile(file, true)); + } + } + } + + return result; + } + + public MusicFolderContent getMusicFolderContent(List<MusicFolder> musicFoldersToUse) throws Exception { + SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists = musicIndexService.getIndexedArtists(musicFoldersToUse); + List<MediaFile> singleSongs = getSingleSongs(musicFoldersToUse); + return new MusicFolderContent(indexedArtists, singleSongs); + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public static class MusicFolderContent { + + private final SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists; + private final List<MediaFile> singleSongs; + + public MusicFolderContent(SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists, List<MediaFile> singleSongs) { + this.indexedArtists = indexedArtists; + this.singleSongs = singleSongs; + } + + public SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> getIndexedArtists() { + return indexedArtists; + } + + public List<MediaFile> getSingleSongs() { + return singleSongs; + } + + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java new file mode 100644 index 00000000..d47ad233 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java @@ -0,0 +1,46 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; +import java.util.HashMap; + +/** + * Controller for the lyrics popup. + * + * @author Sindre Mehus + */ +public class LyricsController extends ParameterizableViewController { + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + map.put("artist", request.getParameter("artist")); + map.put("song", request.getParameter("song")); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java new file mode 100644 index 00000000..bbd7a478 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java @@ -0,0 +1,128 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +/** + * Controller which produces the M3U playlist. + * + * @author Sindre Mehus + */ +public class M3UController implements Controller { + + private PlayerService playerService; + private SettingsService settingsService; + private TranscodingService transcodingService; + + private static final Logger LOG = Logger.getLogger(M3UController.class); + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + response.setContentType("audio/x-mpegurl"); + response.setCharacterEncoding(StringUtil.ENCODING_UTF8); + + Player player = playerService.getPlayer(request, response); + + String url = request.getRequestURL().toString(); + url = url.replaceFirst("play.m3u.*", "stream?"); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + url = StringUtil.rewriteUrl(url, referer); + } + + // Change protocol and port, if specified. (To make it work with players that don't support SSL.) + int streamPort = settingsService.getStreamPort(); + if (streamPort != 0) { + url = StringUtil.toHttpUrl(url, streamPort); + LOG.info("Using non-SSL port " + streamPort + " in m3u playlist."); + } + + if (player.isExternalWithPlaylist()) { + createClientSidePlaylist(response.getWriter(), player, url); + } else { + createServerSidePlaylist(response.getWriter(), player, url); + } + return null; + } + + private void createClientSidePlaylist(PrintWriter out, Player player, String url) throws Exception { + out.println("#EXTM3U"); + List<MediaFile> result; + synchronized (player.getPlayQueue()) { + result = player.getPlayQueue().getFiles(); + } + for (MediaFile mediaFile : result) { + Integer duration = mediaFile.getDurationSeconds(); + if (duration == null) { + duration = -1; + } + out.println("#EXTINF:" + duration + "," + mediaFile.getArtist() + " - " + mediaFile.getTitle()); + out.println(url + "player=" + player.getId() + "&id=" +mediaFile.getId() + "&suffix=." + transcodingService.getSuffix(player, mediaFile, null)); + } + } + + private void createServerSidePlaylist(PrintWriter out, Player player, String url) throws IOException { + + url += "player=" + player.getId(); + + // Get suffix of current file, e.g., ".mp3". + String suffix = getSuffix(player); + if (suffix != null) { + url += "&suffix=." + suffix; + } + + out.println("#EXTM3U"); + out.println("#EXTINF:-1,Subsonic"); + out.println(url); + } + + private String getSuffix(Player player) { + PlayQueue playQueue = player.getPlayQueue(); + return playQueue.isEmpty() ? null : transcodingService.getSuffix(player, playQueue.getFile(0), null); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java new file mode 100644 index 00000000..1d9e0a61 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java @@ -0,0 +1,297 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.AdService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Controller for the main page. + * + * @author Sindre Mehus + */ +public class MainController extends ParameterizableViewController { + + private SecurityService securityService; + private PlayerService playerService; + private SettingsService settingsService; + private RatingService ratingService; + private MediaFileService mediaFileService; + private AdService adService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + Player player = playerService.getPlayer(request, response); + List<MediaFile> mediaFiles = getMediaFiles(request); + + if (mediaFiles.isEmpty()) { + return new ModelAndView(new RedirectView("notFound.view")); + } + + MediaFile dir = mediaFiles.get(0); + if (dir.isFile()) { + dir = mediaFileService.getParentOf(dir); + } + + // Redirect if root directory. + if (mediaFileService.isRoot(dir)) { + return new ModelAndView(new RedirectView("home.view?")); + } + + List<MediaFile> children = mediaFiles.size() == 1 ? mediaFileService.getChildrenOf(dir, true, true, true) : getMultiFolderChildren(mediaFiles); + String username = securityService.getCurrentUsername(request); + UserSettings userSettings = settingsService.getUserSettings(username); + + mediaFileService.populateStarredDate(dir, username); + mediaFileService.populateStarredDate(children, username); + + map.put("dir", dir); + map.put("ancestors", getAncestors(dir)); + map.put("children", children); + map.put("artist", guessArtist(children)); + map.put("album", guessAlbum(children)); + map.put("player", player); + map.put("user", securityService.getCurrentUser(request)); + map.put("multipleArtists", isMultipleArtists(children)); + map.put("visibility", userSettings.getMainVisibility()); + map.put("showAlbumYear", settingsService.isSortAlbumsByYear()); + map.put("updateNowPlaying", request.getParameter("updateNowPlaying") != null); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("brand", settingsService.getBrand()); + if (!settingsService.isLicenseValid()) { + map.put("ad", adService.getAd()); + } + + try { + MediaFile parent = mediaFileService.getParentOf(dir); + map.put("parent", parent); + map.put("navigateUpAllowed", !mediaFileService.isRoot(parent)); + } catch (SecurityException x) { + // Happens if Podcast directory is outside music folder. + } + + Integer userRating = ratingService.getRatingForUser(username, dir); + Double averageRating = ratingService.getAverageRating(dir); + + if (userRating == null) { + userRating = 0; + } + + if (averageRating == null) { + averageRating = 0.0D; + } + + map.put("userRating", 10 * userRating); + map.put("averageRating", Math.round(10.0D * averageRating)); + map.put("starred", mediaFileService.getMediaFileStarredDate(dir.getId(), username) != null); + + CoverArtScheme scheme = player.getCoverArtScheme(); + if (scheme != CoverArtScheme.OFF) { + List<MediaFile> coverArts = getCoverArts(dir, children); + int size = coverArts.size() > 1 ? scheme.getSize() : scheme.getSize() * 2; + map.put("coverArts", coverArts); + map.put("coverArtSize", size); + if (coverArts.isEmpty() && dir.isAlbum()) { + map.put("showGenericCoverArt", true); + } + } + + setPreviousAndNextAlbums(dir, map); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List<MediaFile> getMediaFiles(HttpServletRequest request) { + List<MediaFile> mediaFiles = new ArrayList<MediaFile>(); + for (String path : ServletRequestUtils.getStringParameters(request, "path")) { + MediaFile mediaFile = mediaFileService.getMediaFile(path); + if (mediaFile != null) { + mediaFiles.add(mediaFile); + } + } + for (int id : ServletRequestUtils.getIntParameters(request, "id")) { + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile != null) { + mediaFiles.add(mediaFile); + } + } + return mediaFiles; + } + + private String guessArtist(List<MediaFile> children) { + for (MediaFile child : children) { + if (child.isFile() && child.getArtist() != null) { + return child.getArtist(); + } + } + return null; + } + + private String guessAlbum(List<MediaFile> children) { + for (MediaFile child : children) { + if (child.isFile() && child.getArtist() != null) { + return child.getAlbumName(); + } + } + return null; + } + + private List<MediaFile> getCoverArts(MediaFile dir, List<MediaFile> children) throws IOException { + int limit = settingsService.getCoverArtLimit(); + if (limit == 0) { + limit = Integer.MAX_VALUE; + } + + List<MediaFile> coverArts = new ArrayList<MediaFile>(); + if (dir.isAlbum() && dir.getCoverArtPath() != null) { + coverArts.add(dir); + } else { + for (MediaFile child : children) { + if (child.isAlbum()) { + if (child.getCoverArtPath() != null) { + coverArts.add(child); + } + if (coverArts.size() > limit) { + break; + } + } + } + } + return coverArts; + } + + private List<MediaFile> getMultiFolderChildren(List<MediaFile> mediaFiles) throws IOException { + List<MediaFile> result = new ArrayList<MediaFile>(); + for (MediaFile mediaFile : mediaFiles) { + if (mediaFile.isFile()) { + mediaFile = mediaFileService.getParentOf(mediaFile); + } + result.addAll(mediaFileService.getChildrenOf(mediaFile, true, true, true)); + } + return result; + } + + private List<MediaFile> getAncestors(MediaFile dir) throws IOException { + LinkedList<MediaFile> result = new LinkedList<MediaFile>(); + + try { + MediaFile parent = mediaFileService.getParentOf(dir); + while (parent != null && !mediaFileService.isRoot(parent)) { + result.addFirst(parent); + parent = mediaFileService.getParentOf(parent); + } + } catch (SecurityException x) { + // Happens if Podcast directory is outside music folder. + } + return result; + } + + private void setPreviousAndNextAlbums(MediaFile dir, Map<String, Object> map) throws IOException { + MediaFile parent = mediaFileService.getParentOf(dir); + + if (dir.isAlbum() && !mediaFileService.isRoot(parent)) { + List<MediaFile> sieblings = mediaFileService.getChildrenOf(parent, false, true, true); + + int index = sieblings.indexOf(dir); + if (index > 0) { + map.put("previousAlbum", sieblings.get(index - 1)); + } + if (index < sieblings.size() - 1) { + map.put("nextAlbum", sieblings.get(index + 1)); + } + } + } + + private boolean isMultipleArtists(List<MediaFile> children) { + // Collect unique artist names. + Set<String> artists = new HashSet<String>(); + for (MediaFile child : children) { + if (child.getArtist() != null) { + artists.add(child.getArtist().toLowerCase()); + } + } + + // If zero or one artist, it is definitely not multiple artists. + if (artists.size() < 2) { + return false; + } + + // Fuzzily compare artist names, allowing for some differences in spelling, whitespace etc. + List<String> artistList = new ArrayList<String>(artists); + for (String artist : artistList) { + if (StringUtils.getLevenshteinDistance(artist, artistList.get(0)) > 3) { + return true; + } + } + return false; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setAdService(AdService adService) { + this.adService = adService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java new file mode 100644 index 00000000..f29cb346 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java @@ -0,0 +1,89 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Calendar; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the "more" page. + * + * @author Sindre Mehus + */ +public class MoreController extends ParameterizableViewController { + + private SettingsService settingsService; + private SecurityService securityService; + private PlayerService playerService; + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + String uploadDirectory = null; + List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(); + if (musicFolders.size() > 0) { + uploadDirectory = new File(musicFolders.get(0).getPath(), "Incoming").getPath(); + } + + Player player = playerService.getPlayer(request, response); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + map.put("user", securityService.getCurrentUser(request)); + map.put("uploadDirectory", uploadDirectory); + map.put("genres", mediaFileService.getGenres()); + map.put("currentYear", Calendar.getInstance().get(Calendar.YEAR)); + map.put("musicFolders", settingsService.getAllMusicFolders()); + map.put("clientSidePlaylist", player.isExternalWithPlaylist() || player.isWeb()); + map.put("brand", settingsService.getBrand()); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java new file mode 100644 index 00000000..1d781565 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java @@ -0,0 +1,244 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpConnectionParams; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Multi-controller used for simple pages. + * + * @author Sindre Mehus + */ +public class MultiController extends MultiActionController { + + private static final Logger LOG = Logger.getLogger(MultiController.class); + + private SecurityService securityService; + private SettingsService settingsService; + private PlaylistService playlistService; + + public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception { + + // Auto-login if "user" and "password" parameters are given. + String username = request.getParameter("user"); + String password = request.getParameter("password"); + if (username != null && password != null) { + username = StringUtil.urlEncode(username); + password = StringUtil.urlEncode(password); + return new ModelAndView(new RedirectView("j_acegi_security_check?j_username=" + username + + "&j_password=" + password + "&_acegi_security_remember_me=checked")); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("logout", request.getParameter("logout") != null); + map.put("error", request.getParameter("error") != null); + map.put("brand", settingsService.getBrand()); + map.put("loginMessage", settingsService.getLoginMessage()); + + User admin = securityService.getUserByName(User.USERNAME_ADMIN); + if (User.USERNAME_ADMIN.equals(admin.getPassword())) { + map.put("insecure", true); + } + + return new ModelAndView("login", "model", map); + } + + public ModelAndView recover(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + String usernameOrEmail = StringUtils.trimToNull(request.getParameter("usernameOrEmail")); + + if (usernameOrEmail != null) { + User user = getUserByUsernameOrEmail(usernameOrEmail); + if (user == null) { + map.put("error", "recover.error.usernotfound"); + } else if (user.getEmail() == null) { + map.put("error", "recover.error.noemail"); + } else { + String password = RandomStringUtils.randomAlphanumeric(8); + if (emailPassword(password, user.getUsername(), user.getEmail())) { + map.put("sentTo", user.getEmail()); + user.setLdapAuthenticated(false); + user.setPassword(password); + securityService.updateUser(user); + } else { + map.put("error", "recover.error.sendfailed"); + } + } + } + + return new ModelAndView("recover", "model", map); + } + + private boolean emailPassword(String password, String username, String email) { + HttpClient client = new DefaultHttpClient(); + try { + HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000); + HttpConnectionParams.setSoTimeout(client.getParams(), 10000); + HttpPost method = new HttpPost("http://subsonic.org/backend/sendMail.view"); + + List<NameValuePair> params = new ArrayList<NameValuePair>(); + params.add(new BasicNameValuePair("from", "noreply@subsonic.org")); + params.add(new BasicNameValuePair("to", email)); + params.add(new BasicNameValuePair("subject", "Subsonic Password")); + params.add(new BasicNameValuePair("text", + "Hi there!\n\n" + + "You have requested to reset your Subsonic password. Please find your new login details below.\n\n" + + "Username: " + username + "\n" + + "Password: " + password + "\n\n" + + "--\n" + + "The Subsonic Team\n" + + "subsonic.org")); + method.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8)); + client.execute(method); + return true; + } catch (Exception x) { + LOG.warn("Failed to send email.", x); + return false; + } finally { + client.getConnectionManager().shutdown(); + } + } + + private User getUserByUsernameOrEmail(String usernameOrEmail) { + if (usernameOrEmail != null) { + User user = securityService.getUserByName(usernameOrEmail); + if (user != null) { + return user; + } + return securityService.getUserByEmail(usernameOrEmail); + } + return null; + } + + public ModelAndView accessDenied(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView("accessDenied"); + } + + public ModelAndView notFound(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView("notFound"); + } + + public ModelAndView gettingStarted(HttpServletRequest request, HttpServletResponse response) { + updatePortAndContextPath(request); + + if (request.getParameter("hide") != null) { + settingsService.setGettingStartedEnabled(false); + settingsService.save(); + return new ModelAndView(new RedirectView("home.view")); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("runningAsRoot", "root".equals(System.getProperty("user.name"))); + return new ModelAndView("gettingStarted", "model", map); + } + + public ModelAndView index(HttpServletRequest request, HttpServletResponse response) { + updatePortAndContextPath(request); + UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("showRight", userSettings.isShowNowPlayingEnabled() || userSettings.isShowChatEnabled()); + map.put("brand", settingsService.getBrand()); + return new ModelAndView("index", "model", map); + } + + public ModelAndView exportPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + Playlist playlist = playlistService.getPlaylist(id); + if (!playlistService.isReadAllowed(playlist, securityService.getCurrentUsername(request))) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + + } + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + StringUtil.fileSystemSafe(playlist.getName()) + ".m3u8\""); + + playlistService.exportPlaylist(id, response.getOutputStream()); + return null; + } + + private void updatePortAndContextPath(HttpServletRequest request) { + + int port = Integer.parseInt(System.getProperty("subsonic.port", String.valueOf(request.getLocalPort()))); + int httpsPort = Integer.parseInt(System.getProperty("subsonic.httpsPort", "0")); + + String contextPath = request.getContextPath().replace("/", ""); + + if (settingsService.getPort() != port) { + settingsService.setPort(port); + settingsService.save(); + } + if (settingsService.getHttpsPort() != httpsPort) { + settingsService.setHttpsPort(httpsPort); + settingsService.save(); + } + if (!ObjectUtils.equals(settingsService.getUrlRedirectContextPath(), contextPath)) { + settingsService.setUrlRedirectContextPath(contextPath); + settingsService.save(); + } + } + + public ModelAndView test(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView("test"); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java new file mode 100644 index 00000000..8c002342 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java @@ -0,0 +1,130 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.MusicFolderSettingsCommand; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; + +/** + * Controller for the page used to administrate the set of music folders. + * + * @author Sindre Mehus + */ +public class MusicFolderSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private MediaScannerService mediaScannerService; + private ArtistDao artistDao; + private AlbumDao albumDao; + private MediaFileDao mediaFolderDao; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + MusicFolderSettingsCommand command = new MusicFolderSettingsCommand(); + + if (request.getParameter("scanNow") != null) { + mediaScannerService.scanLibrary(); + } + if (request.getParameter("expunge") != null) { + expunge(); + } + + command.setInterval(String.valueOf(settingsService.getIndexCreationInterval())); + command.setHour(String.valueOf(settingsService.getIndexCreationHour())); + command.setFastCache(settingsService.isFastCacheEnabled()); + command.setOrganizeByFolderStructure(settingsService.isOrganizeByFolderStructure()); + command.setScanning(mediaScannerService.isScanning()); + command.setMusicFolders(wrap(settingsService.getAllMusicFolders(true, true))); + command.setNewMusicFolder(new MusicFolderSettingsCommand.MusicFolderInfo()); + command.setReload(request.getParameter("reload") != null || request.getParameter("scanNow") != null); + return command; + } + + private void expunge() { + artistDao.expunge(); + albumDao.expunge(); + mediaFolderDao.expunge(); + } + + private List<MusicFolderSettingsCommand.MusicFolderInfo> wrap(List<MusicFolder> musicFolders) { + ArrayList<MusicFolderSettingsCommand.MusicFolderInfo> result = new ArrayList<MusicFolderSettingsCommand.MusicFolderInfo>(); + for (MusicFolder musicFolder : musicFolders) { + result.add(new MusicFolderSettingsCommand.MusicFolderInfo(musicFolder)); + } + return result; + } + + @Override + protected ModelAndView onSubmit(Object comm) throws Exception { + MusicFolderSettingsCommand command = (MusicFolderSettingsCommand) comm; + + for (MusicFolderSettingsCommand.MusicFolderInfo musicFolderInfo : command.getMusicFolders()) { + if (musicFolderInfo.isDelete()) { + settingsService.deleteMusicFolder(musicFolderInfo.getId()); + } else { + settingsService.updateMusicFolder(musicFolderInfo.toMusicFolder()); + } + } + + MusicFolder newMusicFolder = command.getNewMusicFolder().toMusicFolder(); + if (newMusicFolder != null) { + settingsService.createMusicFolder(newMusicFolder); + } + + settingsService.setIndexCreationInterval(Integer.parseInt(command.getInterval())); + settingsService.setIndexCreationHour(Integer.parseInt(command.getHour())); + settingsService.setFastCacheEnabled(command.isFastCache()); + settingsService.setOrganizeByFolderStructure(command.isOrganizeByFolderStructure()); + settingsService.save(); + + mediaScannerService.schedule(); + return new ModelAndView(new RedirectView(getSuccessView() + ".view?reload")); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public void setMediaFolderDao(MediaFileDao mediaFolderDao) { + this.mediaFolderDao = mediaFolderDao; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java new file mode 100644 index 00000000..3807eb71 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java @@ -0,0 +1,89 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.Date; +import java.util.Random; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.NetworkSettingsCommand; +import net.sourceforge.subsonic.service.NetworkService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the page used to change the network settings. + * + * @author Sindre Mehus + */ +public class NetworkSettingsController extends SimpleFormController { + + private static final long TRIAL_DAYS = 30L; + + private SettingsService settingsService; + private NetworkService networkService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + NetworkSettingsCommand command = new NetworkSettingsCommand(); + command.setPortForwardingEnabled(settingsService.isPortForwardingEnabled()); + command.setUrlRedirectionEnabled(settingsService.isUrlRedirectionEnabled()); + command.setUrlRedirectFrom(settingsService.getUrlRedirectFrom()); + command.setPort(settingsService.getPort()); + + Date trialExpires = settingsService.getUrlRedirectTrialExpires(); + command.setTrialExpires(trialExpires); + command.setTrialExpired(trialExpires != null && trialExpires.before(new Date())); + command.setTrial(trialExpires != null && !settingsService.isLicenseValid()); + + return command; + } + + protected void doSubmitAction(Object cmd) throws Exception { + NetworkSettingsCommand command = (NetworkSettingsCommand) cmd; + + settingsService.setPortForwardingEnabled(command.isPortForwardingEnabled()); + settingsService.setUrlRedirectionEnabled(command.isUrlRedirectionEnabled()); + settingsService.setUrlRedirectFrom(StringUtils.lowerCase(command.getUrlRedirectFrom())); + + if (!settingsService.isLicenseValid() && settingsService.getUrlRedirectTrialExpires() == null) { + Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L); + settingsService.setUrlRedirectTrialExpires(expiryDate); + } + + if (settingsService.getServerId() == null) { + Random rand = new Random(System.currentTimeMillis()); + settingsService.setServerId(String.valueOf(Math.abs(rand.nextLong()))); + } + + settingsService.save(); + networkService.initPortForwarding(); + networkService.initUrlRedirection(true); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setNetworkService(NetworkService networkService) { + this.networkService = networkService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java new file mode 100644 index 00000000..79fe7c77 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java @@ -0,0 +1,79 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * Controller for showing what's currently playing. + * + * @author Sindre Mehus + */ +public class NowPlayingController extends AbstractController { + + private PlayerService playerService; + private StatusService statusService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Player player = playerService.getPlayer(request, response); + List<TransferStatus> statuses = statusService.getStreamStatusesForPlayer(player); + + MediaFile current = statuses.isEmpty() ? null : mediaFileService.getMediaFile(statuses.get(0).getFile()); + MediaFile dir = current == null ? null : mediaFileService.getParentOf(current); + + String url; + if (dir != null && !mediaFileService.isRoot(dir)) { + url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + + StringUtil.utf8HexEncode(dir.getPath()) + "&updateNowPlaying=true"; + } else { + url = "home.view"; + } + + return new ModelAndView(new RedirectView(url)); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java new file mode 100644 index 00000000..8dd8d875 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java @@ -0,0 +1,58 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.command.*; +import net.sourceforge.subsonic.domain.*; + +import javax.servlet.http.*; + +/** + * Controller for the page used to change password. + * + * @author Sindre Mehus + */ +public class PasswordSettingsController extends SimpleFormController { + + private SecurityService securityService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PasswordSettingsCommand command = new PasswordSettingsCommand(); + User user = securityService.getCurrentUser(request); + command.setUsername(user.getUsername()); + command.setLdapAuthenticated(user.isLdapAuthenticated()); + return command; + } + + protected void doSubmitAction(Object comm) throws Exception { + PasswordSettingsCommand command = (PasswordSettingsCommand) comm; + User user = securityService.getUserByName(command.getUsername()); + user.setPassword(command.getPassword()); + securityService.updateUser(user); + + command.setPassword(null); + command.setConfirmPassword(null); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java new file mode 100644 index 00000000..3bc3f7a5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java @@ -0,0 +1,164 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.*; +import org.apache.commons.lang.StringUtils; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.command.*; +import net.sourceforge.subsonic.domain.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Controller for the page used to administrate per-user settings. + * + * @author Sindre Mehus + */ +public class PersonalSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PersonalSettingsCommand command = new PersonalSettingsCommand(); + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + + command.setUser(user); + command.setLocaleIndex("-1"); + command.setThemeIndex("-1"); + command.setAvatars(settingsService.getAllSystemAvatars()); + command.setCustomAvatar(settingsService.getCustomAvatar(user.getUsername())); + command.setAvatarId(getAvatarId(userSettings)); + command.setPartyModeEnabled(userSettings.isPartyModeEnabled()); + command.setShowNowPlayingEnabled(userSettings.isShowNowPlayingEnabled()); + command.setShowChatEnabled(userSettings.isShowChatEnabled()); + command.setNowPlayingAllowed(userSettings.isNowPlayingAllowed()); + command.setMainVisibility(userSettings.getMainVisibility()); + command.setPlaylistVisibility(userSettings.getPlaylistVisibility()); + command.setFinalVersionNotificationEnabled(userSettings.isFinalVersionNotificationEnabled()); + command.setBetaVersionNotificationEnabled(userSettings.isBetaVersionNotificationEnabled()); + command.setLastFmEnabled(userSettings.isLastFmEnabled()); + command.setLastFmUsername(userSettings.getLastFmUsername()); + command.setLastFmPassword(userSettings.getLastFmPassword()); + + Locale currentLocale = userSettings.getLocale(); + Locale[] locales = settingsService.getAvailableLocales(); + String[] localeStrings = new String[locales.length]; + for (int i = 0; i < locales.length; i++) { + localeStrings[i] = locales[i].getDisplayName(locales[i]); + if (locales[i].equals(currentLocale)) { + command.setLocaleIndex(String.valueOf(i)); + } + } + command.setLocales(localeStrings); + + String currentThemeId = userSettings.getThemeId(); + Theme[] themes = settingsService.getAvailableThemes(); + command.setThemes(themes); + for (int i = 0; i < themes.length; i++) { + if (themes[i].getId().equals(currentThemeId)) { + command.setThemeIndex(String.valueOf(i)); + break; + } + } + + return command; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + PersonalSettingsCommand command = (PersonalSettingsCommand) comm; + + int localeIndex = Integer.parseInt(command.getLocaleIndex()); + Locale locale = null; + if (localeIndex != -1) { + locale = settingsService.getAvailableLocales()[localeIndex]; + } + + int themeIndex = Integer.parseInt(command.getThemeIndex()); + String themeId = null; + if (themeIndex != -1) { + themeId = settingsService.getAvailableThemes()[themeIndex].getId(); + } + + String username = command.getUser().getUsername(); + UserSettings settings = settingsService.getUserSettings(username); + + settings.setLocale(locale); + settings.setThemeId(themeId); + settings.setPartyModeEnabled(command.isPartyModeEnabled()); + settings.setShowNowPlayingEnabled(command.isShowNowPlayingEnabled()); + settings.setShowChatEnabled(command.isShowChatEnabled()); + settings.setNowPlayingAllowed(command.isNowPlayingAllowed()); + settings.setMainVisibility(command.getMainVisibility()); + settings.setPlaylistVisibility(command.getPlaylistVisibility()); + settings.setFinalVersionNotificationEnabled(command.isFinalVersionNotificationEnabled()); + settings.setBetaVersionNotificationEnabled(command.isBetaVersionNotificationEnabled()); + settings.setLastFmEnabled(command.isLastFmEnabled()); + settings.setLastFmUsername(command.getLastFmUsername()); + settings.setSystemAvatarId(getSystemAvatarId(command)); + settings.setAvatarScheme(getAvatarScheme(command)); + + if (StringUtils.isNotBlank(command.getLastFmPassword())) { + settings.setLastFmPassword(command.getLastFmPassword()); + } + + settings.setChanged(new Date()); + settingsService.updateUserSettings(settings); + + command.setReloadNeeded(true); + } + + private int getAvatarId(UserSettings userSettings) { + AvatarScheme avatarScheme = userSettings.getAvatarScheme(); + return avatarScheme == AvatarScheme.SYSTEM ? userSettings.getSystemAvatarId() : avatarScheme.getCode(); + } + + private AvatarScheme getAvatarScheme(PersonalSettingsCommand command) { + if (command.getAvatarId() == AvatarScheme.NONE.getCode()) { + return AvatarScheme.NONE; + } + if (command.getAvatarId() == AvatarScheme.CUSTOM.getCode()) { + return AvatarScheme.CUSTOM; + } + return AvatarScheme.SYSTEM; + } + + private Integer getSystemAvatarId(PersonalSettingsCommand command) { + int avatarId = command.getAvatarId(); + if (avatarId == AvatarScheme.NONE.getCode() || + avatarId == AvatarScheme.CUSTOM.getCode()) { + return null; + } + return avatarId; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java new file mode 100644 index 00000000..0074dda1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java @@ -0,0 +1,77 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the playlist frame. + * + * @author Sindre Mehus + */ +public class PlayQueueController extends ParameterizableViewController { + + private PlayerService playerService; + private SecurityService securityService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + Player player = playerService.getPlayer(request, response); + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("user", user); + map.put("player", player); + map.put("players", playerService.getPlayersForUserAndClientId(user.getUsername(), null)); + map.put("visibility", userSettings.getPlaylistVisibility()); + map.put("partyMode", userSettings.isPartyModeEnabled()); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java new file mode 100644 index 00000000..813d94a5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java @@ -0,0 +1,150 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.PlayerSettingsCommand; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.TranscodingService; + +/** + * Controller for the player settings page. + * + * @author Sindre Mehus + */ +public class PlayerSettingsController extends SimpleFormController { + + private PlayerService playerService; + private SecurityService securityService; + private TranscodingService transcodingService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + + handleRequestParameters(request); + List<Player> players = getPlayers(request); + + User user = securityService.getCurrentUser(request); + PlayerSettingsCommand command = new PlayerSettingsCommand(); + Player player = null; + String playerId = request.getParameter("id"); + if (playerId != null) { + player = playerService.getPlayerById(playerId); + } else if (!players.isEmpty()) { + player = players.get(0); + } + + if (player != null) { + command.setPlayerId(player.getId()); + command.setName(player.getName()); + command.setDescription(player.toString()); + command.setType(player.getType()); + command.setLastSeen(player.getLastSeen()); + command.setDynamicIp(player.isDynamicIp()); + command.setAutoControlEnabled(player.isAutoControlEnabled()); + command.setCoverArtSchemeName(player.getCoverArtScheme().name()); + command.setTranscodeSchemeName(player.getTranscodeScheme().name()); + command.setTechnologyName(player.getTechnology().name()); + command.setAllTranscodings(transcodingService.getAllTranscodings()); + List<Transcoding> activeTranscodings = transcodingService.getTranscodingsForPlayer(player); + int[] activeTranscodingIds = new int[activeTranscodings.size()]; + for (int i = 0; i < activeTranscodings.size(); i++) { + activeTranscodingIds[i] = activeTranscodings.get(i).getId(); + } + command.setActiveTranscodingIds(activeTranscodingIds); + } + + command.setTranscodingSupported(transcodingService.isDownsamplingSupported(null)); + command.setTranscodeDirectory(transcodingService.getTranscodeDirectory().getPath()); + command.setCoverArtSchemes(CoverArtScheme.values()); + command.setTranscodeSchemes(TranscodeScheme.values()); + command.setTechnologies(PlayerTechnology.values()); + command.setPlayers(players.toArray(new Player[players.size()])); + command.setAdmin(user.isAdminRole()); + + return command; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + PlayerSettingsCommand command = (PlayerSettingsCommand) comm; + Player player = playerService.getPlayerById(command.getPlayerId()); + + player.setAutoControlEnabled(command.isAutoControlEnabled()); + player.setCoverArtScheme(CoverArtScheme.valueOf(command.getCoverArtSchemeName())); + player.setDynamicIp(command.isDynamicIp()); + player.setName(StringUtils.trimToNull(command.getName())); + player.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName())); + player.setTechnology(PlayerTechnology.valueOf(command.getTechnologyName())); + + playerService.updatePlayer(player); + transcodingService.setTranscodingsForPlayer(player, command.getActiveTranscodingIds()); + + command.setReloadNeeded(true); + } + + private List<Player> getPlayers(HttpServletRequest request) { + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + List<Player> players = playerService.getAllPlayers(); + List<Player> authorizedPlayers = new ArrayList<Player>(); + + for (Player player : players) { + // Only display authorized players. + if (user.isAdminRole() || username.equals(player.getUsername())) { + authorizedPlayers.add(player); + } + } + return authorizedPlayers; + } + + private void handleRequestParameters(HttpServletRequest request) { + if (request.getParameter("delete") != null) { + playerService.removePlayerById(request.getParameter("delete")); + } else if (request.getParameter("clone") != null) { + playerService.clonePlayer(request.getParameter("clone")); + } + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java new file mode 100644 index 00000000..6b24a3c5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java @@ -0,0 +1,82 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * Controller for the main page. + * + * @author Sindre Mehus + */ +public class PlaylistController extends ParameterizableViewController { + + private SecurityService securityService; + private PlaylistService playlistService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + return new ModelAndView(new RedirectView("notFound.view")); + } + + map.put("playlist", playlist); + map.put("user", user); + map.put("editAllowed", username.equals(playlist.getUsername()) || securityService.isAdmin(username)); + map.put("partyMode", userSettings.isPartyModeEnabled()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java new file mode 100644 index 00000000..dbc6854b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java @@ -0,0 +1,152 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Controller for the page used to generate the Podcast XML file. + * + * @author Sindre Mehus + */ +public class PodcastController extends ParameterizableViewController { + + private static final DateFormat RSS_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); + private PlaylistService playlistService; + private SettingsService settingsService; + private SecurityService securityService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String url = request.getRequestURL().toString(); + String username = securityService.getCurrentUsername(request); + List<Playlist> playlists = playlistService.getReadablePlaylistsForUser(username); + List<Podcast> podcasts = new ArrayList<Podcast>(); + + for (Playlist playlist : playlists) { + + List<MediaFile> songs = playlistService.getFilesInPlaylist(playlist.getId()); + if (songs.isEmpty()) { + continue; + } + long length = 0L; + for (MediaFile song : songs) { + length += song.getFileSize(); + } + String publishDate = RSS_DATE_FORMAT.format(playlist.getCreated()); + + // Resolve content type. + String suffix = songs.get(0).getFormat(); + String type = StringUtil.getMimeType(suffix); + + String enclosureUrl = url.replaceFirst("/podcast.*", "/stream?playlist=" + playlist.getId() + "&suffix=." + suffix); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + url = StringUtil.rewriteUrl(url, referer); + } + + // Change protocol and port, if specified. (To make it work with players that don't support SSL.) + int streamPort = settingsService.getStreamPort(); + if (streamPort != 0) { + enclosureUrl = StringUtil.toHttpUrl(enclosureUrl, streamPort); + } + + podcasts.add(new Podcast(playlist.getName(), publishDate, enclosureUrl, length, type)); + } + + Map<String, Object> map = new HashMap<String, Object>(); + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("url", url); + map.put("podcasts", podcasts); + + result.addObject("model", map); + return result; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + /** + * Contains information about a single Podcast. + */ + public static class Podcast { + private String name; + private String publishDate; + private String enclosureUrl; + private long length; + private String type; + + public Podcast(String name, String publishDate, String enclosureUrl, long length, String type) { + this.name = name; + this.publishDate = publishDate; + this.enclosureUrl = enclosureUrl; + this.length = length; + this.type = type; + } + + public String getName() { + return name; + } + + public String getPublishDate() { + return publishDate; + } + + public String getEnclosureUrl() { + return enclosureUrl; + } + + public long getLength() { + return length; + } + + public String getType() { + return type; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java new file mode 100644 index 00000000..c955e884 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java @@ -0,0 +1,102 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.PodcastStatus; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.List; + +/** + * Controller for the "Podcast receiver" page. + * + * @author Sindre Mehus + */ +public class PodcastReceiverAdminController extends AbstractController { + + private PodcastService podcastService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + handleParameters(request); + return new ModelAndView(new RedirectView("podcastReceiver.view?expandedChannels=" + request.getParameter("expandedChannels"))); + } + + private void handleParameters(HttpServletRequest request) { + if (request.getParameter("add") != null) { + String url = request.getParameter("add"); + podcastService.createChannel(url); + } + if (request.getParameter("downloadChannel") != null || + request.getParameter("downloadEpisode") != null) { + download(StringUtil.parseInts(request.getParameter("downloadChannel")), + StringUtil.parseInts(request.getParameter("downloadEpisode"))); + } + if (request.getParameter("deleteChannel") != null) { + for (int channelId : StringUtil.parseInts(request.getParameter("deleteChannel"))) { + podcastService.deleteChannel(channelId); + } + } + if (request.getParameter("deleteEpisode") != null) { + for (int episodeId : StringUtil.parseInts(request.getParameter("deleteEpisode"))) { + podcastService.deleteEpisode(episodeId, true); + } + } + if (request.getParameter("refresh") != null) { + podcastService.refreshAllChannels(true); + } + } + + private void download(int[] channelIds, int[] episodeIds) { + SortedSet<Integer> uniqueEpisodeIds = new TreeSet<Integer>(); + for (int episodeId : episodeIds) { + uniqueEpisodeIds.add(episodeId); + } + for (int channelId : channelIds) { + List<PodcastEpisode> episodes = podcastService.getEpisodes(channelId, false); + for (PodcastEpisode episode : episodes) { + uniqueEpisodeIds.add(episode.getId()); + } + } + + for (Integer episodeId : uniqueEpisodeIds) { + PodcastEpisode episode = podcastService.getEpisode(episodeId, false); + if (episode != null && episode.getUrl() != null && + (episode.getStatus() == PodcastStatus.NEW || + episode.getStatus() == PodcastStatus.ERROR || + episode.getStatus() == PodcastStatus.SKIPPED)) { + + podcastService.downloadEpisode(episode); + } + } + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java new file mode 100644 index 00000000..93640c22 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java @@ -0,0 +1,85 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller for the "Podcast receiver" page. + * + * @author Sindre Mehus + */ +public class PodcastReceiverController extends ParameterizableViewController { + + private PodcastService podcastService; + private SecurityService securityService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + + Map<PodcastChannel, List<PodcastEpisode>> channels = new LinkedHashMap<PodcastChannel, List<PodcastEpisode>>(); + for (PodcastChannel channel : podcastService.getAllChannels()) { + channels.put(channel, podcastService.getEpisodes(channel.getId(), false)); + } + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + + map.put("user", user); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("channels", channels); + map.put("expandedChannels", StringUtil.parseInts(request.getParameter("expandedChannels"))); + return result; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java new file mode 100644 index 00000000..b6389616 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java @@ -0,0 +1,67 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.SimpleFormController; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.command.PodcastSettingsCommand; + +import javax.servlet.http.HttpServletRequest; + +/** + * Controller for the page used to administrate the Podcast receiver. + * + * @author Sindre Mehus + */ +public class PodcastSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private PodcastService podcastService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PodcastSettingsCommand command = new PodcastSettingsCommand(); + + command.setInterval(String.valueOf(settingsService.getPodcastUpdateInterval())); + command.setEpisodeRetentionCount(String.valueOf(settingsService.getPodcastEpisodeRetentionCount())); + command.setEpisodeDownloadCount(String.valueOf(settingsService.getPodcastEpisodeDownloadCount())); + command.setFolder(settingsService.getPodcastFolder()); + return command; + } + + protected void doSubmitAction(Object comm) throws Exception { + PodcastSettingsCommand command = (PodcastSettingsCommand) comm; + + settingsService.setPodcastUpdateInterval(Integer.parseInt(command.getInterval())); + settingsService.setPodcastEpisodeRetentionCount(Integer.parseInt(command.getEpisodeRetentionCount())); + settingsService.setPodcastEpisodeDownloadCount(Integer.parseInt(command.getEpisodeDownloadCount())); + settingsService.setPodcastFolder(command.getFolder()); + settingsService.save(); + + podcastService.schedule(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java new file mode 100644 index 00000000..9535e059 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java @@ -0,0 +1,68 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.InputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.HttpConnectionParams; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +/** + * A proxy for external HTTP requests. + * + * @author Sindre Mehus + */ +public class ProxyController implements Controller { + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String url = ServletRequestUtils.getRequiredStringParameter(request, "url"); + + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000); + HttpConnectionParams.setSoTimeout(client.getParams(), 15000); + HttpGet method = new HttpGet(url); + + InputStream in = null; + try { + HttpResponse resp = client.execute(method); + int statusCode = resp.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + response.sendError(statusCode); + } else { + in = resp.getEntity().getContent(); + IOUtils.copy(in, response.getOutputStream()); + } + } finally { + IOUtils.closeQuietly(in); + client.getConnectionManager().shutdown(); + } + return null; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java new file mode 100644 index 00000000..2d4fa73c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java @@ -0,0 +1,1983 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.ajax.PlayQueueService; +import net.sourceforge.subsonic.domain.Playlist; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.ajax.ChatService; +import net.sourceforge.subsonic.ajax.LyricsInfo; +import net.sourceforge.subsonic.ajax.LyricsService; +import net.sourceforge.subsonic.command.UserSettingsCommand; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.JukeboxService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.ShareService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.XMLBuilder; + +import static net.sourceforge.subsonic.security.RESTRequestParameterProcessingFilter.decrypt; +import static net.sourceforge.subsonic.util.XMLBuilder.Attribute; +import static net.sourceforge.subsonic.util.XMLBuilder.AttributeSet; + +/** + * Multi-controller used for the REST API. + * <p/> + * For documentation, please refer to api.jsp. + * + * @author Sindre Mehus + */ +public class RESTController extends MultiActionController { + + private static final Logger LOG = Logger.getLogger(RESTController.class); + + private SettingsService settingsService; + private SecurityService securityService; + private PlayerService playerService; + private MediaFileService mediaFileService; + private TranscodingService transcodingService; + private DownloadController downloadController; + private CoverArtController coverArtController; + private AvatarController avatarController; + private UserSettingsController userSettingsController; + private LeftController leftController; + private HomeController homeController; + private StatusService statusService; + private StreamController streamController; + private ShareService shareService; + private PlaylistService playlistService; + private ChatService chatService; + private LyricsService lyricsService; + private PlayQueueService playQueueService; + private JukeboxService jukeboxService; + private AudioScrobblerService audioScrobblerService; + private PodcastService podcastService; + private RatingService ratingService; + private SearchService searchService; + private MediaFileDao mediaFileDao; + private ArtistDao artistDao; + private AlbumDao albumDao; + + public void ping(HttpServletRequest request, HttpServletResponse response) throws Exception { + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } + + public void getLicense(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + String email = settingsService.getLicenseEmail(); + String key = settingsService.getLicenseCode(); + Date date = settingsService.getLicenseDate(); + boolean valid = settingsService.isLicenseValid(); + + AttributeSet attributes = new AttributeSet(); + attributes.add("valid", valid); + if (valid) { + attributes.add("email", email); + attributes.add("key", key); + attributes.add("date", StringUtil.toISO8601(date)); + } + + builder.add("license", attributes, true); + builder.endAll(); + response.getWriter().print(builder); + } + + public void getMusicFolders(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("musicFolders", false); + + for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { + AttributeSet attributes = new AttributeSet(); + attributes.add("id", musicFolder.getId()); + attributes.add("name", musicFolder.getName()); + builder.add("musicFolder", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getIndexes(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + long ifModifiedSince = ServletRequestUtils.getLongParameter(request, "ifModifiedSince", 0L); + long lastModified = leftController.getLastModified(request); + + if (lastModified <= ifModifiedSince) { + builder.endAll(); + response.getWriter().print(builder); + return; + } + + builder.add("indexes", "lastModified", lastModified, false); + + List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(); + Integer musicFolderId = ServletRequestUtils.getIntParameter(request, "musicFolderId"); + if (musicFolderId != null) { + for (MusicFolder musicFolder : musicFolders) { + if (musicFolderId.equals(musicFolder.getId())) { + musicFolders = Arrays.asList(musicFolder); + break; + } + } + } + + List<MediaFile> shortcuts = leftController.getShortcuts(musicFolders, settingsService.getShortcutsAsArray()); + for (MediaFile shortcut : shortcuts) { + builder.add("shortcut", true, + new Attribute("name", shortcut.getName()), + new Attribute("id", shortcut.getId())); + } + + SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists = leftController.getMusicFolderContent(musicFolders).getIndexedArtists(); + + for (Map.Entry<MusicIndex, SortedSet<MusicIndex.Artist>> entry : indexedArtists.entrySet()) { + builder.add("index", "name", entry.getKey().getIndex(), false); + + for (MusicIndex.Artist artist : entry.getValue()) { + for (MediaFile mediaFile : artist.getMediaFiles()) { + if (mediaFile.isDirectory()) { + builder.add("artist", true, + new Attribute("name", artist.getName()), + new Attribute("id", mediaFile.getId())); + } + } + } + builder.end(); + } + + // Add children + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + List<MediaFile> singleSongs = leftController.getSingleSongs(musicFolders); + + for (MediaFile singleSong : singleSongs) { + builder.add("child", createAttributesForMediaFile(player, singleSong, username), true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getArtists(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + String username = securityService.getCurrentUsername(request); + + builder.add("artists", false); + + List<Artist> artists = artistDao.getAlphabetialArtists(0, Integer.MAX_VALUE); + for (Artist artist : artists) { + AttributeSet attributes = createAttributesForArtist(artist, username); + builder.add("artist", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + private AttributeSet createAttributesForArtist(Artist artist, String username) { + AttributeSet attributes = new AttributeSet(); + attributes.add("id", artist.getId()); + attributes.add("name", artist.getName()); + if (artist.getCoverArtPath() != null) { + attributes.add("coverArt", CoverArtController.ARTIST_COVERART_PREFIX + artist.getId()); + } + attributes.add("albumCount", artist.getAlbumCount()); + attributes.add("starred", StringUtil.toISO8601(artistDao.getArtistStarredDate(artist.getId(), username))); + return attributes; + } + + public void getArtist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + String username = securityService.getCurrentUsername(request); + Artist artist; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + artist = artistDao.getArtist(id); + if (artist == null) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Artist not found."); + return; + } + + builder.add("artist", createAttributesForArtist(artist, username), false); + for (Album album : albumDao.getAlbumsForArtist(artist.getName())) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + private AttributeSet createAttributesForAlbum(Album album, String username) { + AttributeSet attributes; + attributes = new AttributeSet(); + attributes.add("id", album.getId()); + attributes.add("name", album.getName()); + attributes.add("artist", album.getArtist()); + if (album.getArtist() != null) { + Artist artist = artistDao.getArtist(album.getArtist()); + if (artist != null) { + attributes.add("artistId", artist.getId()); + } + } + if (album.getCoverArtPath() != null) { + attributes.add("coverArt", CoverArtController.ALBUM_COVERART_PREFIX + album.getId()); + } + attributes.add("songCount", album.getSongCount()); + attributes.add("duration", album.getDurationSeconds()); + attributes.add("created", StringUtil.toISO8601(album.getCreated())); + attributes.add("starred", StringUtil.toISO8601(albumDao.getAlbumStarredDate(album.getId(), username))); + + return attributes; + } + + private AttributeSet createAttributesForPlaylist(Playlist playlist) { + AttributeSet attributes; + attributes = new AttributeSet(); + attributes.add("id", playlist.getId()); + attributes.add("name", playlist.getName()); + attributes.add("comment", playlist.getComment()); + attributes.add("owner", playlist.getUsername()); + attributes.add("public", playlist.isPublic()); + attributes.add("songCount", playlist.getFileCount()); + attributes.add("duration", playlist.getDurationSeconds()); + attributes.add("created", StringUtil.toISO8601(playlist.getCreated())); + return attributes; + } + + public void getAlbum(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + Album album; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + album = albumDao.getAlbum(id); + if (album == null) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Album not found."); + return; + } + + builder.add("album", createAttributesForAlbum(album, username), false); + for (MediaFile mediaFile : mediaFileDao.getSongsForAlbum(album.getArtist(), album.getName())) { + builder.add("song", createAttributesForMediaFile(player, mediaFile, username) , true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getSong(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + MediaFile song; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + song = mediaFileDao.getMediaFile(id); + if (song == null || song.isDirectory()) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Song not found."); + return; + } + + builder.add("song", createAttributesForMediaFile(player, song, username), true); + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getMusicDirectory(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + MediaFile dir; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + dir = mediaFileService.getMediaFile(id); + if (dir == null) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Directory not found"); + return; + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("directory", false, + new Attribute("id", dir.getId()), + new Attribute("name", dir.getName())); + + for (MediaFile child : mediaFileService.getChildrenOf(dir, true, true, true)) { + AttributeSet attributes = createAttributesForMediaFile(player, child, username); + builder.add("child", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + @Deprecated + public void search(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + String any = request.getParameter("any"); + String artist = request.getParameter("artist"); + String album = request.getParameter("album"); + String title = request.getParameter("title"); + + StringBuilder query = new StringBuilder(); + if (any != null) { + query.append(any).append(" "); + } + if (artist != null) { + query.append(artist).append(" "); + } + if (album != null) { + query.append(album).append(" "); + } + if (title != null) { + query.append(title); + } + + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(query.toString().trim()); + criteria.setCount(ServletRequestUtils.getIntParameter(request, "count", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "offset", 0)); + + SearchResult result = searchService.search(criteria, SearchService.IndexType.SONG); + builder.add("searchResult", false, + new Attribute("offset", result.getOffset()), + new Attribute("totalHits", result.getTotalHits())); + + for (MediaFile mediaFile : result.getMediaFiles()) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("match", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void search2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + builder.add("searchResult2", false); + + String query = request.getParameter("query"); + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(StringUtils.trimToEmpty(query)); + criteria.setCount(ServletRequestUtils.getIntParameter(request, "artistCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "artistOffset", 0)); + SearchResult artists = searchService.search(criteria, SearchService.IndexType.ARTIST); + for (MediaFile mediaFile : artists.getMediaFiles()) { + builder.add("artist", true, + new Attribute("name", mediaFile.getName()), + new Attribute("id", mediaFile.getId())); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "albumCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "albumOffset", 0)); + SearchResult albums = searchService.search(criteria, SearchService.IndexType.ALBUM); + for (MediaFile mediaFile : albums.getMediaFiles()) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("album", attributes, true); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "songCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "songOffset", 0)); + SearchResult songs = searchService.search(criteria, SearchService.IndexType.SONG); + for (MediaFile mediaFile : songs.getMediaFiles()) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("song", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void search3(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + builder.add("searchResult3", false); + + String query = request.getParameter("query"); + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(StringUtils.trimToEmpty(query)); + criteria.setCount(ServletRequestUtils.getIntParameter(request, "artistCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "artistOffset", 0)); + SearchResult searchResult = searchService.search(criteria, SearchService.IndexType.ARTIST_ID3); + for (Artist artist : searchResult.getArtists()) { + builder.add("artist", createAttributesForArtist(artist, username), true); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "albumCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "albumOffset", 0)); + searchResult = searchService.search(criteria, SearchService.IndexType.ALBUM_ID3); + for (Album album : searchResult.getAlbums()) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "songCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "songOffset", 0)); + searchResult = searchService.search(criteria, SearchService.IndexType.SONG); + for (MediaFile song : searchResult.getMediaFiles()) { + builder.add("song", createAttributesForMediaFile(player, song, username), true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getPlaylists(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + User user = securityService.getCurrentUser(request); + String authenticatedUsername = user.getUsername(); + String requestedUsername = request.getParameter("username"); + + if (requestedUsername == null) { + requestedUsername = authenticatedUsername; + } else if (!user.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, authenticatedUsername + " is not authorized to get playlists for " + requestedUsername); + return; + } + + builder.add("playlists", false); + + for (Playlist playlist : playlistService.getReadablePlaylistsForUser(requestedUsername)) { + List<String> sharedUsers = playlistService.getPlaylistUsers(playlist.getId()); + builder.add("playlist", createAttributesForPlaylist(playlist), sharedUsers.isEmpty()); + if (!sharedUsers.isEmpty()) { + for (String username : sharedUsers) { + builder.add("allowedUser", (Iterable<Attribute>) null, username, true); + } + builder.end(); + } + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); + return; + } + if (!playlistService.isReadAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); + return; + } + builder.add("playlist", createAttributesForPlaylist(playlist), false); + for (String allowedUser : playlistService.getPlaylistUsers(playlist.getId())) { + builder.add("allowedUser", (Iterable<Attribute>) null, allowedUser, true); + } + for (MediaFile mediaFile : playlistService.getFilesInPlaylist(id)) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void jukeboxControl(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + + User user = securityService.getCurrentUser(request); + if (!user.isJukeboxRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to use jukebox."); + return; + } + + try { + boolean returnPlaylist = false; + String action = ServletRequestUtils.getRequiredStringParameter(request, "action"); + if ("start".equals(action)) { + playQueueService.doStart(request, response); + } else if ("stop".equals(action)) { + playQueueService.doStop(request, response); + } else if ("skip".equals(action)) { + int index = ServletRequestUtils.getRequiredIntParameter(request, "index"); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + playQueueService.doSkip(request, response, index, offset); + } else if ("add".equals(action)) { + int[] ids = ServletRequestUtils.getIntParameters(request, "id"); + playQueueService.doAdd(request, response, ids); + } else if ("set".equals(action)) { + int[] ids = ServletRequestUtils.getIntParameters(request, "id"); + playQueueService.doSet(request, response, ids); + } else if ("clear".equals(action)) { + playQueueService.doClear(request, response); + } else if ("remove".equals(action)) { + int index = ServletRequestUtils.getRequiredIntParameter(request, "index"); + playQueueService.doRemove(request, response, index); + } else if ("shuffle".equals(action)) { + playQueueService.doShuffle(request, response); + } else if ("setGain".equals(action)) { + float gain = ServletRequestUtils.getRequiredFloatParameter(request, "gain"); + jukeboxService.setGain(gain); + } else if ("get".equals(action)) { + returnPlaylist = true; + } else if ("status".equals(action)) { + // No action necessary. + } else { + throw new Exception("Unknown jukebox action: '" + action + "'."); + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + Player jukeboxPlayer = jukeboxService.getPlayer(); + boolean controlsJukebox = jukeboxPlayer != null && jukeboxPlayer.getId().equals(player.getId()); + PlayQueue playQueue = player.getPlayQueue(); + + List<Attribute> attrs = new ArrayList<Attribute>(Arrays.asList( + new Attribute("currentIndex", controlsJukebox && !playQueue.isEmpty() ? playQueue.getIndex() : -1), + new Attribute("playing", controlsJukebox && !playQueue.isEmpty() && playQueue.getStatus() == PlayQueue.Status.PLAYING), + new Attribute("gain", jukeboxService.getGain()), + new Attribute("position", controlsJukebox && !playQueue.isEmpty() ? jukeboxService.getPosition() : 0))); + + if (returnPlaylist) { + builder.add("jukeboxPlaylist", attrs, false); + List<MediaFile> result; + synchronized (playQueue) { + result = playQueue.getFiles(); + } + for (MediaFile mediaFile : result) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + } else { + builder.add("jukeboxStatus", attrs, false); + } + + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void createPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + try { + + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlistId"); + String name = request.getParameter("name"); + if (playlistId == null && name == null) { + error(request, response, ErrorCode.MISSING_PARAMETER, "Playlist ID or name must be specified."); + return; + } + + Playlist playlist; + if (playlistId != null) { + playlist = playlistService.getPlaylist(playlistId); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + playlistId); + return; + } + if (!playlistService.isWriteAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + playlistId); + return; + } + } else { + playlist = new Playlist(); + playlist.setName(name); + playlist.setCreated(new Date()); + playlist.setChanged(new Date()); + playlist.setPublic(false); + playlist.setUsername(username); + playlistService.createPlaylist(playlist); + } + + List<MediaFile> songs = new ArrayList<MediaFile>(); + for (int id : ServletRequestUtils.getIntParameters(request, "songId")) { + MediaFile song = mediaFileService.getMediaFile(id); + if (song != null) { + songs.add(song); + } + } + playlistService.setFilesInPlaylist(playlist.getId(), songs); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void updatePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "playlistId"); + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); + return; + } + if (!playlistService.isWriteAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); + return; + } + + String name = request.getParameter("name"); + if (name != null) { + playlist.setName(name); + } + String comment = request.getParameter("comment"); + if (comment != null) { + playlist.setComment(comment); + } + Boolean isPublic = ServletRequestUtils.getBooleanParameter(request, "public"); + if (isPublic != null) { + playlist.setPublic(isPublic); + } + playlistService.updatePlaylist(playlist); + + // TODO: Add later +// for (String usernameToAdd : ServletRequestUtils.getStringParameters(request, "usernameToAdd")) { +// if (securityService.getUserByName(usernameToAdd) != null) { +// playlistService.addPlaylistUser(id, usernameToAdd); +// } +// } +// for (String usernameToRemove : ServletRequestUtils.getStringParameters(request, "usernameToRemove")) { +// if (securityService.getUserByName(usernameToRemove) != null) { +// playlistService.deletePlaylistUser(id, usernameToRemove); +// } +// } + List<MediaFile> songs = playlistService.getFilesInPlaylist(id); + boolean songsChanged = false; + + SortedSet<Integer> tmp = new TreeSet<Integer>(); + for (int songIndexToRemove : ServletRequestUtils.getIntParameters(request, "songIndexToRemove")) { + tmp.add(songIndexToRemove); + } + List<Integer> songIndexesToRemove = new ArrayList<Integer>(tmp); + Collections.reverse(songIndexesToRemove); + for (Integer songIndexToRemove : songIndexesToRemove) { + songs.remove(songIndexToRemove.intValue()); + songsChanged = true; + } + for (int songToAdd : ServletRequestUtils.getIntParameters(request, "songIdToAdd")) { + MediaFile song = mediaFileService.getMediaFile(songToAdd); + if (song != null) { + songs.add(song); + songsChanged = true; + } + } + if (songsChanged) { + playlistService.setFilesInPlaylist(id, songs); + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void deletePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); + return; + } + if (!playlistService.isWriteAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); + return; + } + playlistService.deletePlaylist(id); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getAlbumList(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("albumList", false); + + try { + int size = ServletRequestUtils.getIntParameter(request, "size", 10); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + size = Math.max(0, Math.min(size, 500)); + String type = ServletRequestUtils.getRequiredStringParameter(request, "type"); + + List<HomeController.Album> albums; + if ("highest".equals(type)) { + albums = homeController.getHighestRated(offset, size); + } else if ("frequent".equals(type)) { + albums = homeController.getMostFrequent(offset, size); + } else if ("recent".equals(type)) { + albums = homeController.getMostRecent(offset, size); + } else if ("newest".equals(type)) { + albums = homeController.getNewest(offset, size); + } else if ("starred".equals(type)) { + albums = homeController.getStarred(offset, size, username); + } else if ("alphabeticalByArtist".equals(type)) { + albums = homeController.getAlphabetical(offset, size, true); + } else if ("alphabeticalByName".equals(type)) { + albums = homeController.getAlphabetical(offset, size, false); + } else if ("random".equals(type)) { + albums = homeController.getRandom(size); + } else { + throw new Exception("Invalid list type: " + type); + } + + for (HomeController.Album album : albums) { + MediaFile mediaFile = mediaFileService.getMediaFile(album.getPath()); + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("album", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getAlbumList2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("albumList2", false); + + try { + int size = ServletRequestUtils.getIntParameter(request, "size", 10); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + size = Math.max(0, Math.min(size, 500)); + String type = ServletRequestUtils.getRequiredStringParameter(request, "type"); + String username = securityService.getCurrentUsername(request); + + List<Album> albums; + if ("frequent".equals(type)) { + albums = albumDao.getMostFrequentlyPlayedAlbums(offset, size); + } else if ("recent".equals(type)) { + albums = albumDao.getMostRecentlyPlayedAlbums(offset, size); + } else if ("newest".equals(type)) { + albums = albumDao.getNewestAlbums(offset, size); + } else if ("alphabeticalByArtist".equals(type)) { + albums = albumDao.getAlphabetialAlbums(offset, size, true); + } else if ("alphabeticalByName".equals(type)) { + albums = albumDao.getAlphabetialAlbums(offset, size, false); + } else if ("starred".equals(type)) { + albums = albumDao.getStarredAlbums(offset, size, securityService.getCurrentUser(request).getUsername()); + } else if ("random".equals(type)) { + albums = searchService.getRandomAlbumsId3(size); + } else { + throw new Exception("Invalid list type: " + type); + } + for (Album album : albums) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getRandomSongs(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("randomSongs", false); + + try { + int size = ServletRequestUtils.getIntParameter(request, "size", 10); + size = Math.max(0, Math.min(size, 500)); + String genre = ServletRequestUtils.getStringParameter(request, "genre"); + Integer fromYear = ServletRequestUtils.getIntParameter(request, "fromYear"); + Integer toYear = ServletRequestUtils.getIntParameter(request, "toYear"); + Integer musicFolderId = ServletRequestUtils.getIntParameter(request, "musicFolderId"); + RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolderId); + + for (MediaFile mediaFile : searchService.getRandomSongs(criteria)) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("song", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getVideos(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("videos", false); + try { + int size = ServletRequestUtils.getIntParameter(request, "size", Integer.MAX_VALUE); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + + for (MediaFile mediaFile : mediaFileDao.getVideos(size, offset)) { + builder.add("video", createAttributesForMediaFile(player, mediaFile, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getNowPlaying(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("nowPlaying", false); + + for (TransferStatus status : statusService.getAllStreamStatuses()) { + + Player player = status.getPlayer(); + File file = status.getFile(); + if (player != null && player.getUsername() != null && file != null) { + + String username = player.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + if (!userSettings.isNowPlayingAllowed()) { + continue; + } + + MediaFile mediaFile = mediaFileService.getMediaFile(file); + + long minutesAgo = status.getMillisSinceLastUpdate() / 1000L / 60L; + if (minutesAgo < 60) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + attributes.add("username", username); + attributes.add("playerId", player.getId()); + attributes.add("playerName", player.getName()); + attributes.add("minutesAgo", minutesAgo); + builder.add("entry", attributes, true); + } + } + } + + builder.endAll(); + response.getWriter().print(builder); + } + + private AttributeSet createAttributesForMediaFile(Player player, MediaFile mediaFile, String username) { + MediaFile parent = mediaFileService.getParentOf(mediaFile); + AttributeSet attributes = new AttributeSet(); + attributes.add("id", mediaFile.getId()); + try { + if (!mediaFileService.isRoot(parent)) { + attributes.add("parent", parent.getId()); + } + } catch (SecurityException x) { + // Ignored. + } + attributes.add("title", mediaFile.getName()); + attributes.add("album", mediaFile.getAlbumName()); + attributes.add("artist", mediaFile.getArtist()); + attributes.add("isDir", mediaFile.isDirectory()); + attributes.add("coverArt", findCoverArt(mediaFile, parent)); + attributes.add("created", StringUtil.toISO8601(mediaFile.getCreated())); + attributes.add("starred", StringUtil.toISO8601(mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username))); + attributes.add("userRating", ratingService.getRatingForUser(username, mediaFile)); + attributes.add("averageRating", ratingService.getAverageRating(mediaFile)); + + if (mediaFile.isFile()) { + attributes.add("duration", mediaFile.getDurationSeconds()); + attributes.add("bitRate", mediaFile.getBitRate()); + attributes.add("track", mediaFile.getTrackNumber()); + attributes.add("discNumber", mediaFile.getDiscNumber()); + attributes.add("year", mediaFile.getYear()); + attributes.add("genre", mediaFile.getGenre()); + attributes.add("size", mediaFile.getFileSize()); + String suffix = mediaFile.getFormat(); + attributes.add("suffix", suffix); + attributes.add("contentType", StringUtil.getMimeType(suffix)); + attributes.add("isVideo", mediaFile.isVideo()); + attributes.add("path", getRelativePath(mediaFile)); + + if (mediaFile.getArtist() != null && mediaFile.getAlbumName() != null) { + Album album = albumDao.getAlbum(mediaFile.getAlbumArtist(), mediaFile.getAlbumName()); + if (album != null) { + attributes.add("albumId", album.getId()); + } + } + if (mediaFile.getArtist() != null) { + Artist artist = artistDao.getArtist(mediaFile.getArtist()); + if (artist != null) { + attributes.add("artistId", artist.getId()); + } + } + switch (mediaFile.getMediaType()) { + case MUSIC: + attributes.add("type", "music"); + break; + case PODCAST: + attributes.add("type", "podcast"); + break; + case AUDIOBOOK: + attributes.add("type", "audiobook"); + break; + default: + break; + } + + if (transcodingService.isTranscodingRequired(mediaFile, player)) { + String transcodedSuffix = transcodingService.getSuffix(player, mediaFile, null); + attributes.add("transcodedSuffix", transcodedSuffix); + attributes.add("transcodedContentType", StringUtil.getMimeType(transcodedSuffix)); + } + } + return attributes; + } + + private Integer findCoverArt(MediaFile mediaFile, MediaFile parent) { + MediaFile dir = mediaFile.isDirectory() ? mediaFile : parent; + if (dir != null && dir.getCoverArtPath() != null) { + return dir.getId(); + } + return null; + } + + private String getRelativePath(MediaFile musicFile) { + + String filePath = musicFile.getPath(); + + // Convert slashes. + filePath = filePath.replace('\\', '/'); + + String filePathLower = filePath.toLowerCase(); + + List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(false, true); + for (MusicFolder musicFolder : musicFolders) { + String folderPath = musicFolder.getPath().getPath(); + folderPath = folderPath.replace('\\', '/'); + String folderPathLower = folderPath.toLowerCase(); + + if (filePathLower.startsWith(folderPathLower)) { + String relativePath = filePath.substring(folderPath.length()); + return relativePath.startsWith("/") ? relativePath.substring(1) : relativePath; + } + } + + return null; + } + + public ModelAndView download(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isDownloadRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to download files."); + return null; + } + + long ifModifiedSince = request.getDateHeader("If-Modified-Since"); + long lastModified = downloadController.getLastModified(request); + + if (ifModifiedSince != -1 && lastModified != -1 && lastModified <= ifModifiedSince) { + response.sendError(HttpServletResponse.SC_NOT_MODIFIED); + return null; + } + + if (lastModified != -1) { + response.setDateHeader("Last-Modified", lastModified); + } + + return downloadController.handleRequest(request, response); + } + + public ModelAndView stream(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isStreamRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to play files."); + return null; + } + + streamController.handleRequest(request, response); + return null; + } + + public void scrobble(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + Player player = playerService.getPlayer(request, response); + + if (!settingsService.getUserSettings(player.getUsername()).isLastFmEnabled()) { + error(request, response, ErrorCode.GENERIC, "Scrobbling is not enabled for " + player.getUsername() + "."); + return; + } + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + if (file == null) { + error(request, response, ErrorCode.NOT_FOUND, "File not found: " + id); + return; + } + boolean submission = ServletRequestUtils.getBooleanParameter(request, "submission", true); + audioScrobblerService.register(file, player.getUsername(), submission); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + return; + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void star(HttpServletRequest request, HttpServletResponse response) throws Exception { + starOrUnstar(request, response, true); + } + + public void unstar(HttpServletRequest request, HttpServletResponse response) throws Exception { + starOrUnstar(request, response, false); + } + + private void starOrUnstar(HttpServletRequest request, HttpServletResponse response, boolean star) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + try { + String username = securityService.getCurrentUser(request).getUsername(); + for (int id : ServletRequestUtils.getIntParameters(request, "id")) { + MediaFile mediaFile = mediaFileDao.getMediaFile(id); + if (mediaFile == null) { + error(request, response, ErrorCode.NOT_FOUND, "Media file not found: " + id); + return; + } + if (star) { + mediaFileDao.starMediaFile(id, username); + } else { + mediaFileDao.unstarMediaFile(id, username); + } + } + for (int albumId : ServletRequestUtils.getIntParameters(request, "albumId")) { + Album album = albumDao.getAlbum(albumId); + if (album == null) { + error(request, response, ErrorCode.NOT_FOUND, "Album not found: " + albumId); + return; + } + if (star) { + albumDao.starAlbum(albumId, username); + } else { + albumDao.unstarAlbum(albumId, username); + } + } + for (int artistId : ServletRequestUtils.getIntParameters(request, "artistId")) { + Artist artist = artistDao.getArtist(artistId); + if (artist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Artist not found: " + artistId); + return; + } + if (star) { + artistDao.starArtist(artistId, username); + } else { + artistDao.unstarArtist(artistId, username); + } + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + return; + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getStarred(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("starred", false); + for (MediaFile artist : mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username)) { + builder.add("artist", true, + new Attribute("name", artist.getName()), + new Attribute("id", artist.getId())); + } + for (MediaFile album : mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username)) { + builder.add("album", createAttributesForMediaFile(player, album, username), true); + } + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username)) { + builder.add("song", createAttributesForMediaFile(player, song, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getStarred2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("starred2", false); + for (Artist artist : artistDao.getStarredArtists(0, Integer.MAX_VALUE, username)) { + builder.add("artist", createAttributesForArtist(artist, username), true); + } + for (Album album : albumDao.getStarredAlbums(0, Integer.MAX_VALUE, username)) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username)) { + builder.add("song", createAttributesForMediaFile(player, song, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getPodcasts(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("podcasts", false); + + for (PodcastChannel channel : podcastService.getAllChannels()) { + AttributeSet channelAttrs = new AttributeSet(); + channelAttrs.add("id", channel.getId()); + channelAttrs.add("url", channel.getUrl()); + channelAttrs.add("status", channel.getStatus().toString().toLowerCase()); + channelAttrs.add("title", channel.getTitle()); + channelAttrs.add("description", channel.getDescription()); + channelAttrs.add("errorMessage", channel.getErrorMessage()); + builder.add("channel", channelAttrs, false); + + List<PodcastEpisode> episodes = podcastService.getEpisodes(channel.getId(), false); + for (PodcastEpisode episode : episodes) { + AttributeSet episodeAttrs = new AttributeSet(); + + String path = episode.getPath(); + if (path != null) { + MediaFile mediaFile = mediaFileService.getMediaFile(path); + episodeAttrs.addAll(createAttributesForMediaFile(player, mediaFile, username)); + episodeAttrs.add("streamId", mediaFile.getId()); + } + + episodeAttrs.add("id", episode.getId()); // Overwrites the previous "id" attribute. + episodeAttrs.add("status", episode.getStatus().toString().toLowerCase()); + episodeAttrs.add("title", episode.getTitle()); + episodeAttrs.add("description", episode.getDescription()); + episodeAttrs.add("publishDate", episode.getPublishDate()); + + builder.add("episode", episodeAttrs, true); + } + + builder.end(); // <channel> + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getShares(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + User user = securityService.getCurrentUser(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + builder.add("shares", false); + for (Share share : shareService.getSharesForUser(user)) { + builder.add("share", createAttributesForShare(share), false); + + for (MediaFile mediaFile : shareService.getSharedFiles(share.getId())) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + + builder.end(); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void createShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + User user = securityService.getCurrentUser(request); + if (!user.isShareRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to share media."); + return; + } + + if (!settingsService.isUrlRedirectionEnabled()) { + error(request, response, ErrorCode.GENERIC, "Sharing is only supported for *.subsonic.org domain names."); + return; + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + + try { + + List<MediaFile> files = new ArrayList<MediaFile>(); + for (int id : ServletRequestUtils.getRequiredIntParameters(request, "id")) { + files.add(mediaFileService.getMediaFile(id)); + } + + // TODO: Update api.jsp + + Share share = shareService.createShare(request, files); + share.setDescription(request.getParameter("description")); + long expires = ServletRequestUtils.getLongParameter(request, "expires", 0L); + if (expires != 0) { + share.setExpires(new Date(expires)); + } + shareService.updateShare(share); + + builder.add("shares", false); + builder.add("share", createAttributesForShare(share), false); + + for (MediaFile mediaFile : shareService.getSharedFiles(share.getId())) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void deleteShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + Share share = shareService.getShareById(id); + if (share == null) { + error(request, response, ErrorCode.NOT_FOUND, "Shared media not found."); + return; + } + if (!user.isAdminRole() && !share.getUsername().equals(user.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Not authorized to delete shared media."); + return; + } + + shareService.deleteShare(id); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void updateShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + Share share = shareService.getShareById(id); + if (share == null) { + error(request, response, ErrorCode.NOT_FOUND, "Shared media not found."); + return; + } + if (!user.isAdminRole() && !share.getUsername().equals(user.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Not authorized to modify shared media."); + return; + } + + share.setDescription(request.getParameter("description")); + String expiresString = request.getParameter("expires"); + if (expiresString != null) { + long expires = Long.parseLong(expiresString); + share.setExpires(expires == 0L ? null : new Date(expires)); + } + shareService.updateShare(share); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + private List<Attribute> createAttributesForShare(Share share) { + List<Attribute> attributes = new ArrayList<Attribute>(); + + attributes.add(new Attribute("id", share.getId())); + attributes.add(new Attribute("url", shareService.getShareUrl(share))); + attributes.add(new Attribute("username", share.getUsername())); + attributes.add(new Attribute("created", StringUtil.toISO8601(share.getCreated()))); + attributes.add(new Attribute("visitCount", share.getVisitCount())); + attributes.add(new Attribute("description", share.getDescription())); + attributes.add(new Attribute("expires", StringUtil.toISO8601(share.getExpires()))); + attributes.add(new Attribute("lastVisited", StringUtil.toISO8601(share.getLastVisited()))); + + return attributes; + } + + public ModelAndView videoPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + Map<String, Object> map = new HashMap<String, Object>(); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + + int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0); + timeOffset = Math.max(0, timeOffset); + Integer duration = file.getDurationSeconds(); + if (duration != null) { + map.put("skipOffsets", VideoPlayerController.createSkipOffsets(duration)); + timeOffset = Math.min(duration, timeOffset); + duration -= timeOffset; + } + + map.put("id", request.getParameter("id")); + map.put("u", request.getParameter("u")); + map.put("p", request.getParameter("p")); + map.put("c", request.getParameter("c")); + map.put("v", request.getParameter("v")); + map.put("video", file); + map.put("maxBitRate", ServletRequestUtils.getIntParameter(request, "maxBitRate", VideoPlayerController.DEFAULT_BIT_RATE)); + map.put("duration", duration); + map.put("timeOffset", timeOffset); + map.put("bitRates", VideoPlayerController.BIT_RATES); + map.put("autoplay", ServletRequestUtils.getBooleanParameter(request, "autoplay", true)); + + ModelAndView result = new ModelAndView("rest/videoPlayer"); + result.addObject("model", map); + return result; + } + + public ModelAndView getCoverArt(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + return coverArtController.handleRequest(request, response); + } + + public ModelAndView getAvatar(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + return avatarController.handleRequest(request, response); + } + + public void changePassword(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + try { + + String username = ServletRequestUtils.getRequiredStringParameter(request, "username"); + String password = decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password")); + + User authUser = securityService.getCurrentUser(request); + if (!authUser.isAdminRole() && !username.equals(authUser.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, authUser.getUsername() + " is not authorized to change password for " + username); + return; + } + + User user = securityService.getUserByName(username); + user.setPassword(password); + securityService.updateUser(user); + + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + String username; + try { + username = ServletRequestUtils.getRequiredStringParameter(request, "username"); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + return; + } + + User currentUser = securityService.getCurrentUser(request); + if (!username.equals(currentUser.getUsername()) && !currentUser.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, currentUser.getUsername() + " is not authorized to get details for other users."); + return; + } + + User requestedUser = securityService.getUserByName(username); + if (requestedUser == null) { + error(request, response, ErrorCode.NOT_FOUND, "No such user: " + username); + return; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + + XMLBuilder builder = createXMLBuilder(request, response, true); + List<Attribute> attributes = Arrays.asList( + new Attribute("username", requestedUser.getUsername()), + new Attribute("email", requestedUser.getEmail()), + new Attribute("scrobblingEnabled", userSettings.isLastFmEnabled()), + new Attribute("adminRole", requestedUser.isAdminRole()), + new Attribute("settingsRole", requestedUser.isSettingsRole()), + new Attribute("downloadRole", requestedUser.isDownloadRole()), + new Attribute("uploadRole", requestedUser.isUploadRole()), + new Attribute("playlistRole", true), // Since 1.8.0 + new Attribute("coverArtRole", requestedUser.isCoverArtRole()), + new Attribute("commentRole", requestedUser.isCommentRole()), + new Attribute("podcastRole", requestedUser.isPodcastRole()), + new Attribute("streamRole", requestedUser.isStreamRole()), + new Attribute("jukeboxRole", requestedUser.isJukeboxRole()), + new Attribute("shareRole", requestedUser.isShareRole()) + ); + + builder.add("user", attributes, true); + builder.endAll(); + response.getWriter().print(builder); + } + + public void createUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to create new users."); + return; + } + + try { + UserSettingsCommand command = new UserSettingsCommand(); + command.setUsername(ServletRequestUtils.getRequiredStringParameter(request, "username")); + command.setPassword(decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password"))); + command.setEmail(ServletRequestUtils.getRequiredStringParameter(request, "email")); + command.setLdapAuthenticated(ServletRequestUtils.getBooleanParameter(request, "ldapAuthenticated", false)); + command.setAdminRole(ServletRequestUtils.getBooleanParameter(request, "adminRole", false)); + command.setCommentRole(ServletRequestUtils.getBooleanParameter(request, "commentRole", false)); + command.setCoverArtRole(ServletRequestUtils.getBooleanParameter(request, "coverArtRole", false)); + command.setDownloadRole(ServletRequestUtils.getBooleanParameter(request, "downloadRole", false)); + command.setStreamRole(ServletRequestUtils.getBooleanParameter(request, "streamRole", true)); + command.setUploadRole(ServletRequestUtils.getBooleanParameter(request, "uploadRole", false)); + command.setJukeboxRole(ServletRequestUtils.getBooleanParameter(request, "jukeboxRole", false)); + command.setPodcastRole(ServletRequestUtils.getBooleanParameter(request, "podcastRole", false)); + command.setSettingsRole(ServletRequestUtils.getBooleanParameter(request, "settingsRole", true)); + command.setTranscodeSchemeName(ServletRequestUtils.getStringParameter(request, "transcodeScheme", TranscodeScheme.OFF.name())); + command.setShareRole(ServletRequestUtils.getBooleanParameter(request, "shareRole", false)); + + userSettingsController.createUser(command); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void deleteUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to delete users."); + return; + } + + try { + String username = ServletRequestUtils.getRequiredStringParameter(request, "username"); + securityService.deleteUser(username); + + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getChatMessages(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + long since = ServletRequestUtils.getLongParameter(request, "since", 0L); + + builder.add("chatMessages", false); + + for (ChatService.Message message : chatService.getMessages(0L).getMessages()) { + long time = message.getDate().getTime(); + if (time > since) { + builder.add("chatMessage", true, new Attribute("username", message.getUsername()), + new Attribute("time", time), new Attribute("message", message.getContent())); + } + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void addChatMessage(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + try { + chatService.doAddMessage(ServletRequestUtils.getRequiredStringParameter(request, "message"), request); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } + } + + public void getLyrics(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String artist = request.getParameter("artist"); + String title = request.getParameter("title"); + LyricsInfo lyrics = lyricsService.getLyrics(artist, title); + + XMLBuilder builder = createXMLBuilder(request, response, true); + AttributeSet attributes = new AttributeSet(); + attributes.add("artist", lyrics.getArtist()); + attributes.add("title", lyrics.getTitle()); + builder.add("lyrics", attributes, lyrics.getLyrics(), true); + + builder.endAll(); + response.getWriter().print(builder); + } + + public void setRating(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + try { + Integer rating = ServletRequestUtils.getRequiredIntParameter(request, "rating"); + if (rating == 0) { + rating = null; + } + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile == null) { + error(request, response, ErrorCode.NOT_FOUND, "File not found: " + id); + return; + } + + String username = securityService.getCurrentUsername(request); + ratingService.setRatingForUser(username, mediaFile, rating); + + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } + } + + private HttpServletRequest wrapRequest(HttpServletRequest request) { + return wrapRequest(request, false); + } + + private HttpServletRequest wrapRequest(final HttpServletRequest request, boolean jukebox) { + final String playerId = createPlayerIfNecessary(request, jukebox); + return new HttpServletRequestWrapper(request) { + @Override + public String getParameter(String name) { + // Returns the correct player to be used in PlayerService.getPlayer() + if ("player".equals(name)) { + return playerId; + } + + // Support old style ID parameters. + if ("id".equals(name)) { + return mapId(request.getParameter("id")); + } + + return super.getParameter(name); + } + }; + } + + private String mapId(String id) { + if (id == null || id.startsWith(CoverArtController.ALBUM_COVERART_PREFIX) || + id.startsWith(CoverArtController.ARTIST_COVERART_PREFIX) || StringUtils.isNumeric(id)) { + return id; + } + + try { + String path = StringUtil.utf8HexDecode(id); + MediaFile mediaFile = mediaFileService.getMediaFile(path); + return String.valueOf(mediaFile.getId()); + } catch (Exception x) { + return id; + } + } + + private String getErrorMessage(Exception x) { + if (x.getMessage() != null) { + return x.getMessage(); + } + return x.getClass().getSimpleName(); + } + + private void error(HttpServletRequest request, HttpServletResponse response, ErrorCode code, String message) throws IOException { + XMLBuilder builder = createXMLBuilder(request, response, false); + builder.add("error", true, + new XMLBuilder.Attribute("code", code.getCode()), + new XMLBuilder.Attribute("message", message)); + builder.end(); + response.getWriter().print(builder); + } + + private XMLBuilder createXMLBuilder(HttpServletRequest request, HttpServletResponse response, boolean ok) throws IOException { + String format = ServletRequestUtils.getStringParameter(request, "f", "xml"); + boolean json = "json".equals(format); + boolean jsonp = "jsonp".equals(format); + XMLBuilder builder; + + response.setCharacterEncoding(StringUtil.ENCODING_UTF8); + + if (json) { + builder = XMLBuilder.createJSONBuilder(); + response.setContentType("application/json"); + } else if (jsonp) { + builder = XMLBuilder.createJSONPBuilder(request.getParameter("callback")); + response.setContentType("text/javascript"); + } else { + builder = XMLBuilder.createXMLBuilder(); + response.setContentType("text/xml"); + } + + builder.preamble(StringUtil.ENCODING_UTF8); + builder.add("subsonic-response", false, + new Attribute("xmlns", "http://subsonic.org/restapi"), + new Attribute("status", ok ? "ok" : "failed"), + new Attribute("version", StringUtil.getRESTProtocolVersion())); + return builder; + } + + private String createPlayerIfNecessary(HttpServletRequest request, boolean jukebox) { + String username = request.getRemoteUser(); + String clientId = request.getParameter("c"); + if (jukebox) { + clientId += "-jukebox"; + } + + List<Player> players = playerService.getPlayersForUserAndClientId(username, clientId); + + // If not found, create it. + if (players.isEmpty()) { + Player player = new Player(); + player.setIpAddress(request.getRemoteAddr()); + player.setUsername(username); + player.setClientId(clientId); + player.setName(clientId); + player.setTechnology(jukebox ? PlayerTechnology.JUKEBOX : PlayerTechnology.EXTERNAL_WITH_PLAYLIST); + playerService.createPlayer(player); + players = playerService.getPlayersForUserAndClientId(username, clientId); + } + + // Return the player ID. + return !players.isEmpty() ? players.get(0).getId() : null; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setDownloadController(DownloadController downloadController) { + this.downloadController = downloadController; + } + + public void setCoverArtController(CoverArtController coverArtController) { + this.coverArtController = coverArtController; + } + + public void setUserSettingsController(UserSettingsController userSettingsController) { + this.userSettingsController = userSettingsController; + } + + public void setLeftController(LeftController leftController) { + this.leftController = leftController; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setStreamController(StreamController streamController) { + this.streamController = streamController; + } + + public void setChatService(ChatService chatService) { + this.chatService = chatService; + } + + public void setHomeController(HomeController homeController) { + this.homeController = homeController; + } + + public void setLyricsService(LyricsService lyricsService) { + this.lyricsService = lyricsService; + } + + public void setPlayQueueService(PlayQueueService playQueueService) { + this.playQueueService = playQueueService; + } + + public void setJukeboxService(JukeboxService jukeboxService) { + this.jukeboxService = jukeboxService; + } + + public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) { + this.audioScrobblerService = audioScrobblerService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setAvatarController(AvatarController avatarController) { + this.avatarController = avatarController; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public static enum ErrorCode { + + GENERIC(0, "A generic error."), + MISSING_PARAMETER(10, "Required parameter is missing."), + PROTOCOL_MISMATCH_CLIENT_TOO_OLD(20, "Incompatible Subsonic REST protocol version. Client must upgrade."), + PROTOCOL_MISMATCH_SERVER_TOO_OLD(30, "Incompatible Subsonic REST protocol version. Server must upgrade."), + NOT_AUTHENTICATED(40, "Wrong username or password."), + NOT_AUTHORIZED(50, "User is not authorized for the given operation."), + NOT_LICENSED(60, "The trial period for the Subsonic server is over. Please donate to get a license key. Visit subsonic.org for details."), + NOT_FOUND(70, "Requested data was not found."); + + private final int code; + private final String message; + + ErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java new file mode 100644 index 00000000..a3738684 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java @@ -0,0 +1,101 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SearchService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the creating a random play queue. + * + * @author Sindre Mehus + */ +public class RandomPlayQueueController extends ParameterizableViewController { + + private PlayerService playerService; + private List<ReloadFrame> reloadFrames; + private SearchService searchService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int size = ServletRequestUtils.getRequiredIntParameter(request, "size"); + String genre = request.getParameter("genre"); + if (StringUtils.equalsIgnoreCase("any", genre)) { + genre = null; + } + + Integer fromYear = null; + Integer toYear = null; + + String year = request.getParameter("year"); + if (!StringUtils.equalsIgnoreCase("any", year)) { + String[] tmp = StringUtils.split(year); + fromYear = Integer.parseInt(tmp[0]); + toYear = Integer.parseInt(tmp[1]); + } + + Integer musicFolderId = ServletRequestUtils.getRequiredIntParameter(request, "musicFolderId"); + if (musicFolderId == -1) { + musicFolderId = null; + } + + Player player = playerService.getPlayer(request, response); + PlayQueue playQueue = player.getPlayQueue(); + + RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolderId); + playQueue.addFiles(false, searchService.getRandomSongs(criteria)); + + if (request.getParameter("autoRandom") != null) { + playQueue.setRandomSearchCriteria(criteria); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("reloadFrames", reloadFrames); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setReloadFrames(List<ReloadFrame> reloadFrames) { + this.reloadFrames = reloadFrames; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java new file mode 100644 index 00000000..093b7fa1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java @@ -0,0 +1,52 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +/** + * Used in subsonic-servlet.xml to specify frame reloading. + * + * @author Sindre Mehus + */ +public class ReloadFrame { + private String frame; + private String view; + + public ReloadFrame() {} + + public ReloadFrame(String frame, String view) { + this.frame = frame; + this.view = view; + } + + public String getFrame() { + return frame; + } + + public void setFrame(String frame) { + this.frame = frame; + } + + public String getView() { + return view; + } + + public void setView(String view) { + this.view = view; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java new file mode 100644 index 00000000..405c2dc7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java @@ -0,0 +1,66 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.dao.UserDao; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * Controller for the right frame. + * + * @author Sindre Mehus + */ +public class RightController extends ParameterizableViewController { + + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + ModelAndView result = super.handleRequestInternal(request, response); + + UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + map.put("showNowPlaying", userSettings.isShowNowPlayingEnabled()); + map.put("showChat", userSettings.isShowChatEnabled()); + map.put("user", securityService.getCurrentUser(request)); + + result.addObject("model", map); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java new file mode 100644 index 00000000..387ec7db --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java @@ -0,0 +1,106 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.SearchCommand; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.SearchService; + +/** + * Controller for the search page. + * + * @author Sindre Mehus + */ +public class SearchController extends SimpleFormController { + + private static final int MATCH_COUNT = 25; + + private SecurityService securityService; + private SettingsService settingsService; + private PlayerService playerService; + private SearchService searchService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + return new SearchCommand(); + } + + @Override + protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors) + throws Exception { + SearchCommand command = (SearchCommand) com; + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + command.setUser(user); + command.setPartyModeEnabled(userSettings.isPartyModeEnabled()); + + String any = StringUtils.trimToNull(command.getQuery()); + + if (any != null) { + + SearchCriteria criteria = new SearchCriteria(); + criteria.setCount(MATCH_COUNT); + criteria.setQuery(any); + + SearchResult artists = searchService.search(criteria, SearchService.IndexType.ARTIST); + command.setArtists(artists.getMediaFiles()); + + SearchResult albums = searchService.search(criteria, SearchService.IndexType.ALBUM); + command.setAlbums(albums.getMediaFiles()); + + SearchResult songs = searchService.search(criteria, SearchService.IndexType.SONG); + command.setSongs(songs.getMediaFiles()); + + command.setPlayer(playerService.getPlayer(request, response)); + } + + return new ModelAndView(getSuccessView(), errors.getModel()); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java new file mode 100644 index 00000000..8b3ebca7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java @@ -0,0 +1,58 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.view.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; + +/** + * Controller for updating music file metadata. + * + * @author Sindre Mehus + */ +public class SetMusicFileInfoController extends AbstractController { + + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + String path = request.getParameter("path"); + String action = request.getParameter("action"); + + MediaFile mediaFile = mediaFileService.getMediaFile(path); + + if ("comment".equals(action)) { + mediaFile.setComment(StringUtil.toHtml(request.getParameter("comment"))); + mediaFileService.updateMediaFile(mediaFile); + } + + String url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + StringUtil.utf8HexEncode(path); + return new ModelAndView(new RedirectView(url)); + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java new file mode 100644 index 00000000..aaeaa4a4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java @@ -0,0 +1,69 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import org.springframework.web.bind.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; +import org.springframework.web.servlet.view.*; + +import javax.servlet.http.*; + +/** + * Controller for updating music file ratings. + * + * @author Sindre Mehus + */ +public class SetRatingController extends AbstractController { + + private RatingService ratingService; + private SecurityService securityService; + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + String path = request.getParameter("path"); + Integer rating = ServletRequestUtils.getIntParameter(request, "rating"); + if (rating == 0) { + rating = null; + } + + MediaFile mediaFile = mediaFileService.getMediaFile(path); + String username = securityService.getCurrentUsername(request); + ratingService.setRatingForUser(username, mediaFile, rating); + + String url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + StringUtil.utf8HexEncode(path); + return new ModelAndView(new RedirectView(url)); + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java new file mode 100644 index 00000000..ed0c21c5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java @@ -0,0 +1,52 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.view.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; + +/** + * Controller for the main settings page. + * + * @author Sindre Mehus + */ +public class SettingsController extends AbstractController { + + private SecurityService securityService; + + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + + // Redirect to music folder settings if admin. + String view = user.isAdminRole() ? "musicFolderSettings.view" : "personalSettings.view"; + + return new ModelAndView(new RedirectView(view)); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java new file mode 100644 index 00000000..de2ea764 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java @@ -0,0 +1,123 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.*; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Share; + +/** + * Controller for sharing music on Twitter, Facebook etc. + * + * @author Sindre Mehus + */ +public class ShareManagementController extends MultiActionController { + + private MediaFileService mediaFileService; + private SettingsService settingsService; + private ShareService shareService; + private PlayerService playerService; + private SecurityService securityService; + + public ModelAndView createShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + + List<MediaFile> files = getMediaFiles(request); + MediaFile dir = null; + if (!files.isEmpty()) { + dir = files.get(0); + if (!dir.isAlbum()) { + dir = mediaFileService.getParentOf(dir); + } + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("urlRedirectionEnabled", settingsService.isUrlRedirectionEnabled()); + map.put("dir", dir); + map.put("user", securityService.getCurrentUser(request)); + Share share = shareService.createShare(request, files); + map.put("playUrl", shareService.getShareUrl(share)); + + return new ModelAndView("createShare", "model", map); + } + + private List<MediaFile> getMediaFiles(HttpServletRequest request) throws IOException { + String dir = request.getParameter("dir"); + String playerId = request.getParameter("player"); + + List<MediaFile> result = new ArrayList<MediaFile>(); + + if (dir != null) { + MediaFile album = mediaFileService.getMediaFile(dir); + int[] indexes = ServletRequestUtils.getIntParameters(request, "i"); + if (indexes.length == 0) { + return Arrays.asList(album); + } + List<MediaFile> children = mediaFileService.getChildrenOf(album, true, true, true); + for (int index : indexes) { + result.add(children.get(index)); + } + } else if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + PlayQueue playQueue = player.getPlayQueue(); + List<MediaFile> result1; + synchronized (playQueue) { + result1 = playQueue.getFiles(); + } + result = result1; + } + + return result; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java new file mode 100644 index 00000000..2b8a958a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java @@ -0,0 +1,161 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.ShareService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the page used to administrate the set of shared media. + * + * @author Sindre Mehus + */ +public class ShareSettingsController extends ParameterizableViewController { + + private ShareService shareService; + private SecurityService securityService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + if (isFormSubmission(request)) { + String error = handleParameters(request); + map.put("error", error); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("shareBaseUrl", shareService.getShareBaseUrl()); + map.put("shareInfos", getShareInfos(request)); + map.put("user", securityService.getCurrentUser(request)); + + result.addObject("model", map); + return result; + } + + /** + * Determine if the given request represents a form submission. + * + * @param request current HTTP request + * @return if the request represents a form submission + */ + private boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + private String handleParameters(HttpServletRequest request) { + User user = securityService.getCurrentUser(request); + for (Share share : shareService.getSharesForUser(user)) { + int id = share.getId(); + + String description = getParameter(request, "description", id); + boolean delete = getParameter(request, "delete", id) != null; + String expireIn = getParameter(request, "expireIn", id); + + if (delete) { + shareService.deleteShare(id); + } else { + if (expireIn != null) { + share.setExpires(parseExpireIn(expireIn)); + } + share.setDescription(description); + shareService.updateShare(share); + } + } + + return null; + } + + private List<ShareInfo> getShareInfos(HttpServletRequest request) { + List<ShareInfo> result = new ArrayList<ShareInfo>(); + User user = securityService.getCurrentUser(request); + for (Share share : shareService.getSharesForUser(user)) { + List<MediaFile> files = shareService.getSharedFiles(share.getId()); + if (!files.isEmpty()) { + MediaFile file = files.get(0); + result.add(new ShareInfo(share, file.isDirectory() ? file : mediaFileService.getParentOf(file))); + } + } + return result; + } + + + private String getParameter(HttpServletRequest request, String name, int id) { + return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]")); + } + + private Date parseExpireIn(String expireIn) { + int days = Integer.parseInt(expireIn); + if (days == 0) { + return null; + } + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_YEAR, days); + return calendar.getTime(); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public static class ShareInfo { + private final Share share; + private final MediaFile dir; + + public ShareInfo(Share share, MediaFile dir) { + this.share = share; + this.dir = dir; + } + + public Share getShare() { + return share; + } + + public MediaFile getDir() { + return dir; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java new file mode 100644 index 00000000..2da8b5ad --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java @@ -0,0 +1,96 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for showing a user's starred items. + * + * @author Sindre Mehus + */ +public class StarredController extends ParameterizableViewController { + + private PlayerService playerService; + private MediaFileDao mediaFileDao; + private SecurityService securityService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + + List<MediaFile> artists = mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username); + List<MediaFile> albums = mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username); + List<MediaFile> songs = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username); + mediaFileService.populateStarredDate(artists, username); + mediaFileService.populateStarredDate(albums, username); + mediaFileService.populateStarredDate(songs, username); + + map.put("user", user); + map.put("partyModeEnabled", userSettings.isPartyModeEnabled()); + map.put("player", playerService.getPlayer(request, response)); + map.put("artists", artists); + map.put("albums", albums); + map.put("songs", songs); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java new file mode 100644 index 00000000..878b8ae8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java @@ -0,0 +1,149 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import org.jfree.chart.*; +import org.jfree.chart.axis.*; +import org.jfree.chart.plot.*; +import org.jfree.chart.renderer.xy.*; +import org.jfree.data.*; +import org.jfree.data.time.*; +import org.springframework.web.servlet.*; + +import javax.servlet.http.*; +import java.awt.*; +import java.util.*; +import java.util.List; + +/** + * Controller for generating a chart showing bitrate vs time. + * + * @author Sindre Mehus + */ +public class StatusChartController extends AbstractChartController { + + private StatusService statusService; + + public static final int IMAGE_WIDTH = 350; + public static final int IMAGE_HEIGHT = 150; + + public synchronized ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String type = request.getParameter("type"); + int index = Integer.parseInt(request.getParameter("index")); + + List<TransferStatus> statuses = Collections.emptyList(); + if ("stream".equals(type)) { + statuses = statusService.getAllStreamStatuses(); + } else if ("download".equals(type)) { + statuses = statusService.getAllDownloadStatuses(); + } else if ("upload".equals(type)) { + statuses = statusService.getAllUploadStatuses(); + } + + if (index < 0 || index >= statuses.size()) { + return null; + } + TransferStatus status = statuses.get(index); + + TimeSeries series = new TimeSeries("Kbps", Millisecond.class); + TransferStatus.SampleHistory history = status.getHistory(); + long to = System.currentTimeMillis(); + long from = to - status.getHistoryLengthMillis(); + Range range = new DateRange(from, to); + + if (!history.isEmpty()) { + + TransferStatus.Sample previous = history.get(0); + + for (int i = 1; i < history.size(); i++) { + TransferStatus.Sample sample = history.get(i); + + long elapsedTimeMilis = sample.getTimestamp() - previous.getTimestamp(); + long bytesStreamed = Math.max(0L, sample.getBytesTransfered() - previous.getBytesTransfered()); + + double kbps = (8.0 * bytesStreamed / 1024.0) / (elapsedTimeMilis / 1000.0); + series.addOrUpdate(new Millisecond(new Date(sample.getTimestamp())), kbps); + + previous = sample; + } + } + + // Compute moving average. + series = MovingAverage.createMovingAverage(series, "Kbps", 20000, 5000); + + // Find min and max values. + double min = 100; + double max = 250; + for (Object obj : series.getItems()) { + TimeSeriesDataItem item = (TimeSeriesDataItem) obj; + double value = item.getValue().doubleValue(); + if (item.getPeriod().getFirstMillisecond() > from) { + min = Math.min(min, value); + max = Math.max(max, value); + } + } + + // Add 10% to max value. + max *= 1.1D; + + // Subtract 10% from min value. + min *= 0.9D; + + TimeSeriesCollection dataset = new TimeSeriesCollection(); + dataset.addSeries(series); + JFreeChart chart = ChartFactory.createTimeSeriesChart(null, null, null, dataset, false, false, false); + XYPlot plot = (XYPlot) chart.getPlot(); + + plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); + Paint background = new GradientPaint(0, 0, Color.lightGray, 0, IMAGE_HEIGHT, Color.white); + plot.setBackgroundPaint(background); + + XYItemRenderer renderer = plot.getRendererForDataset(dataset); + renderer.setSeriesPaint(0, Color.blue.darker()); + renderer.setSeriesStroke(0, new BasicStroke(2f)); + + // Set theme-specific colors. + Color bgColor = getBackground(request); + Color fgColor = getForeground(request); + + chart.setBackgroundPaint(bgColor); + + ValueAxis domainAxis = plot.getDomainAxis(); + domainAxis.setRange(range); + domainAxis.setTickLabelPaint(fgColor); + domainAxis.setTickMarkPaint(fgColor); + domainAxis.setAxisLinePaint(fgColor); + + ValueAxis rangeAxis = plot.getRangeAxis(); + rangeAxis.setRange(new Range(min, max)); + rangeAxis.setTickLabelPaint(fgColor); + rangeAxis.setTickMarkPaint(fgColor); + rangeAxis.setAxisLinePaint(fgColor); + + ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, IMAGE_WIDTH, IMAGE_HEIGHT); + + return null; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java new file mode 100644 index 00000000..964e7810 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java @@ -0,0 +1,141 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.support.RequestContextUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Controller for the status page. + * + * @author Sindre Mehus + */ +public class StatusController extends ParameterizableViewController { + + private StatusService statusService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + List<TransferStatus> streamStatuses = statusService.getAllStreamStatuses(); + List<TransferStatus> downloadStatuses = statusService.getAllDownloadStatuses(); + List<TransferStatus> uploadStatuses = statusService.getAllUploadStatuses(); + + Locale locale = RequestContextUtils.getLocale(request); + List<TransferStatusHolder> transferStatuses = new ArrayList<TransferStatusHolder>(); + + for (int i = 0; i < streamStatuses.size(); i++) { + long minutesAgo = streamStatuses.get(i).getMillisSinceLastUpdate() / 1000L / 60L; + if (minutesAgo < 60L) { + transferStatuses.add(new TransferStatusHolder(streamStatuses.get(i), true, false, false, i, locale)); + } + } + for (int i = 0; i < downloadStatuses.size(); i++) { + transferStatuses.add(new TransferStatusHolder(downloadStatuses.get(i), false, true, false, i, locale)); + } + for (int i = 0; i < uploadStatuses.size(); i++) { + transferStatuses.add(new TransferStatusHolder(uploadStatuses.get(i), false, false, true, i, locale)); + } + + map.put("transferStatuses", transferStatuses); + map.put("chartWidth", StatusChartController.IMAGE_WIDTH); + map.put("chartHeight", StatusChartController.IMAGE_HEIGHT); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public static class TransferStatusHolder { + private TransferStatus transferStatus; + private boolean isStream; + private boolean isDownload; + private boolean isUpload; + private int index; + private Locale locale; + + public TransferStatusHolder(TransferStatus transferStatus, boolean isStream, boolean isDownload, boolean isUpload, + int index, Locale locale) { + this.transferStatus = transferStatus; + this.isStream = isStream; + this.isDownload = isDownload; + this.isUpload = isUpload; + this.index = index; + this.locale = locale; + } + + public boolean isStream() { + return isStream; + } + + public boolean isDownload() { + return isDownload; + } + + public boolean isUpload() { + return isUpload; + } + + public int getIndex() { + return index; + } + + public Player getPlayer() { + return transferStatus.getPlayer(); + } + + public String getPlayerType() { + Player player = transferStatus.getPlayer(); + return player == null ? null : player.getType(); + } + + public String getUsername() { + Player player = transferStatus.getPlayer(); + return player == null ? null : player.getUsername(); + } + + public String getPath() { + return FileUtil.getShortPath(transferStatus.getFile()); + } + + public String getBytes() { + return StringUtil.formatBytes(transferStatus.getBytesTransfered(), locale); + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java new file mode 100644 index 00000000..a40f5da4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java @@ -0,0 +1,419 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Dimension; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SearchService; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.math.LongRange; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.VideoTranscodingSettings; +import net.sourceforge.subsonic.io.PlayQueueInputStream; +import net.sourceforge.subsonic.io.RangeOutputStream; +import net.sourceforge.subsonic.io.ShoutCastOutputStream; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +/** + * A controller which streams the content of a {@link net.sourceforge.subsonic.domain.PlayQueue} to a remote + * {@link Player}. + * + * @author Sindre Mehus + */ +public class StreamController implements Controller { + + private static final Logger LOG = Logger.getLogger(StreamController.class); + + private StatusService statusService; + private PlayerService playerService; + private PlaylistService playlistService; + private SecurityService securityService; + private SettingsService settingsService; + private TranscodingService transcodingService; + private AudioScrobblerService audioScrobblerService; + private MediaFileService mediaFileService; + private SearchService searchService; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + TransferStatus status = null; + PlayQueueInputStream in = null; + Player player = playerService.getPlayer(request, response, false, true); + User user = securityService.getUserByName(player.getUsername()); + + try { + + if (!user.isStreamRole()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername()); + return null; + } + + // If "playlist" request parameter is set, this is a Podcast request. In that case, create a separate + // play queue (in order to support multiple parallel Podcast streams). + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + boolean isPodcast = playlistId != null; + if (isPodcast) { + PlayQueue playQueue = new PlayQueue(); + playQueue.addFiles(false, playlistService.getFilesInPlaylist(playlistId)); + player.setPlayQueue(playQueue); + Util.setContentLength(response, playQueue.length()); + LOG.info("Incoming Podcast request for playlist " + playlistId); + } + + String contentType = StringUtil.getMimeType(request.getParameter("suffix")); + response.setContentType(contentType); + + String preferredTargetFormat = request.getParameter("format"); + Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate"); + if (Integer.valueOf(0).equals(maxBitRate)) { + maxBitRate = null; + } + + VideoTranscodingSettings videoTranscodingSettings = null; + + // Is this a request for a single file (typically from the embedded Flash player)? + // In that case, create a separate playlist (in order to support multiple parallel streams). + // Also, enable partial download (HTTP byte range). + MediaFile file = getSingleFile(request); + boolean isSingleFile = file != null; + LongRange range = null; + + if (isSingleFile) { + PlayQueue playQueue = new PlayQueue(); + playQueue.addFiles(true, file); + player.setPlayQueue(playQueue); + + if (!file.isVideo()) { + response.setIntHeader("ETag", file.getId()); + response.setHeader("Accept-Ranges", "bytes"); + } + + TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, videoTranscodingSettings); + long fileLength = getFileLength(parameters); + boolean isConversion = parameters.isDownsample() || parameters.isTranscode(); + boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request, "estimateContentLength", false); + + range = getRange(request, file); + if (range != null) { + LOG.info("Got range: " + range); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + Util.setContentLength(response, fileLength - range.getMinimumLong()); + long firstBytePos = range.getMinimumLong(); + long lastBytePos = fileLength - 1; + response.setHeader("Content-Range", "bytes " + firstBytePos + "-" + lastBytePos + "/" + fileLength); + } else if (!isConversion || estimateContentLength) { + Util.setContentLength(response, fileLength); + } + + String transcodedSuffix = transcodingService.getSuffix(player, file, preferredTargetFormat); + response.setContentType(StringUtil.getMimeType(transcodedSuffix)); + + if (file.isVideo()) { + videoTranscodingSettings = createVideoTranscodingSettings(file, request); + } + } + + if (request.getMethod().equals("HEAD")) { + return null; + } + + // Terminate any other streams to this player. + if (!isPodcast && !isSingleFile) { + for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) { + if (streamStatus.isActive()) { + streamStatus.terminate(); + } + } + } + + status = statusService.createStreamStatus(player); + + in = new PlayQueueInputStream(player, status, maxBitRate, preferredTargetFormat, videoTranscodingSettings, transcodingService, + audioScrobblerService, mediaFileService, searchService); + OutputStream out = RangeOutputStream.wrap(response.getOutputStream(), range); + + // Enabled SHOUTcast, if requested. + boolean isShoutCastRequested = "1".equals(request.getHeader("icy-metadata")); + if (isShoutCastRequested && !isSingleFile) { + response.setHeader("icy-metaint", "" + ShoutCastOutputStream.META_DATA_INTERVAL); + response.setHeader("icy-notice1", "This stream is served using Subsonic"); + response.setHeader("icy-notice2", "Subsonic - Free media streamer - subsonic.org"); + response.setHeader("icy-name", "Subsonic"); + response.setHeader("icy-genre", "Mixed"); + response.setHeader("icy-url", "http://subsonic.org/"); + out = new ShoutCastOutputStream(out, player.getPlayQueue(), settingsService); + } + + final int BUFFER_SIZE = 2048; + byte[] buf = new byte[BUFFER_SIZE]; + + while (true) { + + // Check if stream has been terminated. + if (status.terminated()) { + return null; + } + + if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) { + if (isPodcast || isSingleFile) { + break; + } else { + sendDummy(buf, out); + } + } else { + + int n = in.read(buf); + if (n == -1) { + if (isPodcast || isSingleFile) { + break; + } else { + sendDummy(buf, out); + } + } else { + out.write(buf, 0, n); + } + } + } + + } finally { + if (status != null) { + securityService.updateUserByteCounts(user, status.getBytesTransfered(), 0L, 0L); + statusService.removeStreamStatus(status); + } + IOUtils.closeQuietly(in); + } + return null; + } + + private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException { + String path = request.getParameter("path"); + if (path != null) { + return mediaFileService.getMediaFile(path); + } + Integer id = ServletRequestUtils.getIntParameter(request, "id"); + if (id != null) { + return mediaFileService.getMediaFile(id); + } + return null; + } + + private long getFileLength(TranscodingService.Parameters parameters) { + MediaFile file = parameters.getMediaFile(); + + if (!parameters.isDownsample() && !parameters.isTranscode()) { + return file.getFileSize(); + } + Integer duration = file.getDurationSeconds(); + Integer maxBitRate = parameters.getMaxBitRate(); + + if (duration == null) { + LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size."); + return file.getFileSize(); + } + + if (maxBitRate == null) { + LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size."); + return file.getFileSize(); + } + + return duration * maxBitRate * 1000L / 8L; + } + + private LongRange getRange(HttpServletRequest request, MediaFile file) { + + // First, look for "Range" HTTP header. + LongRange range = StringUtil.parseRange(request.getHeader("Range")); + if (range != null) { + return range; + } + + // Second, look for "offsetSeconds" request parameter. + String offsetSeconds = request.getParameter("offsetSeconds"); + range = parseAndConvertOffsetSeconds(offsetSeconds, file); + if (range != null) { + return range; + } + + return null; + } + + private LongRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) { + if (offsetSeconds == null) { + return null; + } + + try { + Integer duration = file.getDurationSeconds(); + Long fileSize = file.getFileSize(); + if (duration == null || fileSize == null) { + return null; + } + float offset = Float.parseFloat(offsetSeconds); + + // Convert from time offset to byte offset. + long byteOffset = (long) (fileSize * (offset / duration)); + return new LongRange(byteOffset, Long.MAX_VALUE); + + } catch (Exception x) { + LOG.error("Failed to parse and convert time offset: " + offsetSeconds, x); + return null; + } + } + + private VideoTranscodingSettings createVideoTranscodingSettings(MediaFile file, HttpServletRequest request) throws ServletRequestBindingException { + Integer existingWidth = file.getWidth(); + Integer existingHeight = file.getHeight(); + Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate"); + int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0); + + Dimension dim = getRequestedVideoSize(request.getParameter("size")); + if (dim == null) { + dim = getSuitableVideoSize(existingWidth, existingHeight, maxBitRate); + } + + return new VideoTranscodingSettings(dim.width, dim.height, timeOffset); + } + + protected Dimension getRequestedVideoSize(String sizeSpec) { + if (sizeSpec == null) { + return null; + } + + Pattern pattern = Pattern.compile("^(\\d+)x(\\d+)$"); + Matcher matcher = pattern.matcher(sizeSpec); + if (matcher.find()) { + int w = Integer.parseInt(matcher.group(1)); + int h = Integer.parseInt(matcher.group(2)); + if (w >= 0 && h >= 0 && w <= 2000 && h <= 2000) { + return new Dimension(w, h); + } + } + return null; + } + + protected Dimension getSuitableVideoSize(Integer existingWidth, Integer existingHeight, Integer maxBitRate) { + if (maxBitRate == null) { + return new Dimension(320, 240); + } + + int w, h; + if (maxBitRate <= 600) { + w = 320; h = 240; + } else if (maxBitRate <= 1000) { + w = 480; h = 360; + } else { + w = 640; h = 480; + } + + if (existingWidth == null || existingHeight == null) { + return new Dimension(w, h); + } + + if (existingWidth < w || existingHeight < h) { + return new Dimension(even(existingWidth), even(existingHeight)); + } + + double aspectRate = existingWidth.doubleValue() / existingHeight.doubleValue(); + w = (int) Math.round(h * aspectRate); + + return new Dimension(even(w), even(h)); + } + + // Make sure width and height are multiples of two, as some versions of ffmpeg require it. + private int even(int size) { + return size + (size % 2); + } + + /** + * Feed the other end with some dummy data to keep it from reconnecting. + */ + private void sendDummy(byte[] buf, OutputStream out) throws IOException { + try { + Thread.sleep(2000); + } catch (InterruptedException x) { + LOG.warn("Interrupted in sleep.", x); + } + Arrays.fill(buf, (byte) 0xFF); + out.write(buf); + out.flush(); + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) { + this.audioScrobblerService = audioScrobblerService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java new file mode 100644 index 00000000..800aef0e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java @@ -0,0 +1,84 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.VersionService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the top frame. + * + * @author Sindre Mehus + */ +public class TopController extends ParameterizableViewController { + + private SettingsService settingsService; + private VersionService versionService; + private SecurityService securityService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders(); + User user = securityService.getCurrentUser(request); + + map.put("user", user); + map.put("musicFoldersExist", !allMusicFolders.isEmpty()); + map.put("brand", settingsService.getBrand()); + map.put("licensed", settingsService.isLicenseValid()); + + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + if (userSettings.isFinalVersionNotificationEnabled() && versionService.isNewFinalVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestFinalVersion()); + + } else if (userSettings.isBetaVersionNotificationEnabled() && versionService.isNewBetaVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestBetaVersion()); + } + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java new file mode 100644 index 00000000..8bd87408 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java @@ -0,0 +1,139 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * Controller for the page used to administrate the set of transcoding configurations. + * + * @author Sindre Mehus + */ +public class TranscodingSettingsController extends ParameterizableViewController { + + private TranscodingService transcodingService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + if (isFormSubmission(request)) { + handleParameters(request, map); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("transcodings", transcodingService.getAllTranscodings()); + map.put("transcodeDirectory", transcodingService.getTranscodeDirectory()); + map.put("brand", settingsService.getBrand()); + + result.addObject("model", map); + return result; + } + + /** + * Determine if the given request represents a form submission. + * + * @param request current HTTP request + * @return if the request represents a form submission + */ + private boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + private void handleParameters(HttpServletRequest request, Map<String, Object> map) { + + for (Transcoding transcoding : transcodingService.getAllTranscodings()) { + Integer id = transcoding.getId(); + String name = getParameter(request, "name", id); + String sourceFormats = getParameter(request, "sourceFormats", id); + String targetFormat = getParameter(request, "targetFormat", id); + String step1 = getParameter(request, "step1", id); + String step2 = getParameter(request, "step2", id); + boolean delete = getParameter(request, "delete", id) != null; + + if (delete) { + transcodingService.deleteTranscoding(id); + } else if (name == null) { + map.put("error", "transcodingsettings.noname"); + } else if (sourceFormats == null) { + map.put("error", "transcodingsettings.nosourceformat"); + } else if (targetFormat == null) { + map.put("error", "transcodingsettings.notargetformat"); + } else if (step1 == null) { + map.put("error", "transcodingsettings.nostep1"); + } else { + transcoding.setName(name); + transcoding.setSourceFormats(sourceFormats); + transcoding.setTargetFormat(targetFormat); + transcoding.setStep1(step1); + transcoding.setStep2(step2); + transcodingService.updateTranscoding(transcoding); + } + } + + String name = StringUtils.trimToNull(request.getParameter("name")); + String sourceFormats = StringUtils.trimToNull(request.getParameter("sourceFormats")); + String targetFormat = StringUtils.trimToNull(request.getParameter("targetFormat")); + String step1 = StringUtils.trimToNull(request.getParameter("step1")); + String step2 = StringUtils.trimToNull(request.getParameter("step2")); + boolean defaultActive = request.getParameter("defaultActive") != null; + + if (name != null || sourceFormats != null || targetFormat != null || step1 != null || step2 != null) { + Transcoding transcoding = new Transcoding(null, name, sourceFormats, targetFormat, step1, step2, null, defaultActive); + if (name == null) { + map.put("error", "transcodingsettings.noname"); + } else if (sourceFormats == null) { + map.put("error", "transcodingsettings.nosourceformat"); + } else if (targetFormat == null) { + map.put("error", "transcodingsettings.notargetformat"); + } else if (step1 == null) { + map.put("error", "transcodingsettings.nostep1"); + } else { + transcodingService.createTranscoding(transcoding); + } + if (map.containsKey("error")) { + map.put("newTranscoding", transcoding); + } + } + } + + private String getParameter(HttpServletRequest request, String name, Integer id) { + return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]")); + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java new file mode 100644 index 00000000..de7bf8dd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java @@ -0,0 +1,260 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.upload.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import org.apache.commons.fileupload.*; +import org.apache.commons.fileupload.servlet.*; +import org.apache.commons.io.*; +import org.apache.tools.zip.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; +import java.io.*; +import java.util.*; + +/** + * Controller which receives uploaded files. + * + * @author Sindre Mehus + */ +public class UploadController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(UploadController.class); + + private SecurityService securityService; + private PlayerService playerService; + private StatusService statusService; + private SettingsService settingsService; + public static final String UPLOAD_STATUS = "uploadStatus"; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + List<File> uploadedFiles = new ArrayList<File>(); + List<File> unzippedFiles = new ArrayList<File>(); + TransferStatus status = null; + + try { + + status = statusService.createUploadStatus(playerService.getPlayer(request, response, false, false)); + status.setBytesTotal(request.getContentLength()); + + request.getSession().setAttribute(UPLOAD_STATUS, status); + + // Check that we have a file upload request + if (!ServletFileUpload.isMultipartContent(request)) { + throw new Exception("Illegal request."); + } + + File dir = null; + boolean unzip = false; + + UploadListener listener = new UploadListenerImpl(status); + + FileItemFactory factory = new MonitoredDiskFileItemFactory(listener); + ServletFileUpload upload = new ServletFileUpload(factory); + + List<?> items = upload.parseRequest(request); + + // First, look for "dir" and "unzip" parameters. + for (Object o : items) { + FileItem item = (FileItem) o; + + if (item.isFormField() && "dir".equals(item.getFieldName())) { + dir = new File(item.getString()); + } else if (item.isFormField() && "unzip".equals(item.getFieldName())) { + unzip = true; + } + } + + if (dir == null) { + throw new Exception("Missing 'dir' parameter."); + } + + // Look for file items. + for (Object o : items) { + FileItem item = (FileItem) o; + + if (!item.isFormField()) { + String fileName = item.getName(); + if (fileName.trim().length() > 0) { + + File targetFile = new File(dir, new File(fileName).getName()); + + if (!securityService.isUploadAllowed(targetFile)) { + throw new Exception("Permission denied: " + StringUtil.toHtml(targetFile.getPath())); + } + + if (!dir.exists()) { + dir.mkdirs(); + } + + item.write(targetFile); + uploadedFiles.add(targetFile); + LOG.info("Uploaded " + targetFile); + + if (unzip && targetFile.getName().toLowerCase().endsWith(".zip")) { + unzip(targetFile, unzippedFiles); + } + } + } + } + + } catch (Exception x) { + LOG.warn("Uploading failed.", x); + map.put("exception", x); + } finally { + if (status != null) { + statusService.removeUploadStatus(status); + request.getSession().removeAttribute(UPLOAD_STATUS); + User user = securityService.getCurrentUser(request); + securityService.updateUserByteCounts(user, 0L, 0L, status.getBytesTransfered()); + } + } + + map.put("uploadedFiles", uploadedFiles); + map.put("unzippedFiles", unzippedFiles); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private void unzip(File file, List<File> unzippedFiles) throws Exception { + LOG.info("Unzipping " + file); + + ZipFile zipFile = new ZipFile(file); + + try { + + Enumeration<?> entries = zipFile.getEntries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = (ZipEntry) entries.nextElement(); + File entryFile = new File(file.getParentFile(), entry.getName()); + + if (!entry.isDirectory()) { + + if (!securityService.isUploadAllowed(entryFile)) { + throw new Exception("Permission denied: " + StringUtil.toHtml(entryFile.getPath())); + } + + entryFile.getParentFile().mkdirs(); + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = zipFile.getInputStream(entry); + outputStream = new FileOutputStream(entryFile); + + byte[] buf = new byte[8192]; + while (true) { + int n = inputStream.read(buf); + if (n == -1) { + break; + } + outputStream.write(buf, 0, n); + } + + LOG.info("Unzipped " + entryFile); + unzippedFiles.add(entryFile); + } finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + zipFile.close(); + file.delete(); + + } finally { + zipFile.close(); + } + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + /** + * Receives callbacks as the file upload progresses. + */ + private class UploadListenerImpl implements UploadListener { + private TransferStatus status; + private long start; + + private UploadListenerImpl(TransferStatus status) { + this.status = status; + start = System.currentTimeMillis(); + } + + public void start(String fileName) { + status.setFile(new File(fileName)); + } + + public void bytesRead(long bytesRead) { + + // Throttle bitrate. + + long byteCount = status.getBytesTransfered() + bytesRead; + long bitCount = byteCount * 8L; + + float elapsedMillis = Math.max(1, System.currentTimeMillis() - start); + float elapsedSeconds = elapsedMillis / 1000.0F; + long maxBitsPerSecond = getBitrateLimit(); + + status.setBytesTransfered(byteCount); + + if (maxBitsPerSecond > 0) { + float sleepMillis = 1000.0F * (bitCount / maxBitsPerSecond - elapsedSeconds); + if (sleepMillis > 0) { + try { + Thread.sleep((long) sleepMillis); + } catch (InterruptedException x) { + LOG.warn("Failed to sleep.", x); + } + } + } + } + + private long getBitrateLimit() { + return 1024L * settingsService.getUploadBitrateLimit() / Math.max(1, statusService.getAllUploadStatuses().size()); + } + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java new file mode 100644 index 00000000..0428eff8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java @@ -0,0 +1,145 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Paint; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartUtilities; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.chart.axis.CategoryLabelPositions; +import org.jfree.chart.axis.LogarithmicAxis; +import org.jfree.chart.plot.CategoryPlot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.category.BarRenderer; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.springframework.web.servlet.ModelAndView; + +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * Controller for generating a chart showing bitrate vs time. + * + * @author Sindre Mehus + */ +public class UserChartController extends AbstractChartController { + + private SecurityService securityService; + + public static final int IMAGE_WIDTH = 400; + public static final int IMAGE_MIN_HEIGHT = 200; + private static final long BYTES_PER_MB = 1024L * 1024L; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String type = request.getParameter("type"); + CategoryDataset dataset = createDataset(type); + JFreeChart chart = createChart(dataset, request); + + int imageHeight = Math.max(IMAGE_MIN_HEIGHT, 15 * dataset.getColumnCount()); + + ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, IMAGE_WIDTH, imageHeight); + return null; + } + + private CategoryDataset createDataset(String type) { + DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + List<User> users = securityService.getAllUsers(); + for (User user : users) { + double value; + if ("stream".equals(type)) { + value = user.getBytesStreamed(); + } else if ("download".equals(type)) { + value = user.getBytesDownloaded(); + } else if ("upload".equals(type)) { + value = user.getBytesUploaded(); + } else if ("total".equals(type)) { + value = user.getBytesStreamed() + user.getBytesDownloaded() + user.getBytesUploaded(); + } else { + throw new RuntimeException("Illegal chart type: " + type); + } + + value /= BYTES_PER_MB; + dataset.addValue(value, "Series", user.getUsername()); + } + + return dataset; + } + + private JFreeChart createChart(CategoryDataset dataset, HttpServletRequest request) { + JFreeChart chart = ChartFactory.createBarChart(null, null, null, dataset, PlotOrientation.HORIZONTAL, false, false, false); + + CategoryPlot plot = chart.getCategoryPlot(); + Paint background = new GradientPaint(0, 0, Color.lightGray, 0, IMAGE_MIN_HEIGHT, Color.white); + plot.setBackgroundPaint(background); + plot.setDomainGridlinePaint(Color.white); + plot.setDomainGridlinesVisible(true); + plot.setRangeGridlinePaint(Color.white); + plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT); + + LogarithmicAxis rangeAxis = new LogarithmicAxis(null); + rangeAxis.setStrictValuesFlag(false); + rangeAxis.setAllowNegativesFlag(true); + plot.setRangeAxis(rangeAxis); + + // Disable bar outlines. + BarRenderer renderer = (BarRenderer) plot.getRenderer(); + renderer.setDrawBarOutline(false); + + // Set up gradient paint for series. + GradientPaint gp0 = new GradientPaint( + 0.0f, 0.0f, Color.blue, + 0.0f, 0.0f, new Color(0, 0, 64) + ); + renderer.setSeriesPaint(0, gp0); + + // Rotate labels. + CategoryAxis domainAxis = plot.getDomainAxis(); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.createUpRotationLabelPositions(Math.PI / 6.0)); + + // Set theme-specific colors. + Color bgColor = getBackground(request); + Color fgColor = getForeground(request); + + chart.setBackgroundPaint(bgColor); + + domainAxis.setTickLabelPaint(fgColor); + domainAxis.setTickMarkPaint(fgColor); + domainAxis.setAxisLinePaint(fgColor); + + rangeAxis.setTickLabelPaint(fgColor); + rangeAxis.setTickMarkPaint(fgColor); + rangeAxis.setAxisLinePaint(fgColor); + + return chart; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java new file mode 100644 index 00000000..58848840 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java @@ -0,0 +1,159 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.List; +import java.util.Date; + +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.command.*; +import org.springframework.web.servlet.mvc.*; +import org.springframework.web.bind.*; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.*; + +/** + * Controller for the page used to administrate users. + * + * @author Sindre Mehus + */ +public class UserSettingsController extends SimpleFormController { + + private SecurityService securityService; + private SettingsService settingsService; + private TranscodingService transcodingService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + UserSettingsCommand command = new UserSettingsCommand(); + + User user = getUser(request); + if (user != null) { + command.setUser(user); + command.setEmail(user.getEmail()); + command.setAdmin(User.USERNAME_ADMIN.equals(user.getUsername())); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + command.setTranscodeSchemeName(userSettings.getTranscodeScheme().name()); + + } else { + command.setNew(true); + command.setStreamRole(true); + command.setSettingsRole(true); + } + + command.setUsers(securityService.getAllUsers()); + command.setTranscodingSupported(transcodingService.isDownsamplingSupported(null)); + command.setTranscodeDirectory(transcodingService.getTranscodeDirectory().getPath()); + command.setTranscodeSchemes(TranscodeScheme.values()); + command.setLdapEnabled(settingsService.isLdapEnabled()); + + return command; + } + + private User getUser(HttpServletRequest request) throws ServletRequestBindingException { + Integer userIndex = ServletRequestUtils.getIntParameter(request, "userIndex"); + if (userIndex != null) { + List<User> allUsers = securityService.getAllUsers(); + if (userIndex >= 0 && userIndex < allUsers.size()) { + return allUsers.get(userIndex); + } + } + return null; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + UserSettingsCommand command = (UserSettingsCommand) comm; + + if (command.isDelete()) { + deleteUser(command); + } else if (command.isNew()) { + createUser(command); + } else { + updateUser(command); + } + resetCommand(command); + } + + private void deleteUser(UserSettingsCommand command) { + securityService.deleteUser(command.getUsername()); + } + + public void createUser(UserSettingsCommand command) { + User user = new User(command.getUsername(), command.getPassword(), StringUtils.trimToNull(command.getEmail())); + user.setLdapAuthenticated(command.isLdapAuthenticated()); + securityService.createUser(user); + updateUser(command); + } + + private void updateUser(UserSettingsCommand command) { + User user = securityService.getUserByName(command.getUsername()); + user.setEmail(StringUtils.trimToNull(command.getEmail())); + user.setLdapAuthenticated(command.isLdapAuthenticated()); + user.setAdminRole(command.isAdminRole()); + user.setDownloadRole(command.isDownloadRole()); + user.setUploadRole(command.isUploadRole()); + user.setCoverArtRole(command.isCoverArtRole()); + user.setCommentRole(command.isCommentRole()); + user.setPodcastRole(command.isPodcastRole()); + user.setStreamRole(command.isStreamRole()); + user.setJukeboxRole(command.isJukeboxRole()); + user.setSettingsRole(command.isSettingsRole()); + user.setShareRole(command.isShareRole()); + + if (command.isPasswordChange()) { + user.setPassword(command.getPassword()); + } + + securityService.updateUser(user); + + UserSettings userSettings = settingsService.getUserSettings(command.getUsername()); + userSettings.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName())); + userSettings.setChanged(new Date()); + settingsService.updateUserSettings(userSettings); + } + + private void resetCommand(UserSettingsCommand command) { + command.setUser(null); + command.setUsers(securityService.getAllUsers()); + command.setDelete(false); + command.setPasswordChange(false); + command.setNew(true); + command.setStreamRole(true); + command.setSettingsRole(true); + command.setPassword(null); + command.setConfirmPassword(null); + command.setEmail(null); + command.setTranscodeSchemeName(null); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java new file mode 100644 index 00000000..1d7686eb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java @@ -0,0 +1,110 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Controller for the page used to play videos. + * + * @author Sindre Mehus + */ +public class VideoPlayerController extends ParameterizableViewController { + + public static final int DEFAULT_BIT_RATE = 1000; + public static final int[] BIT_RATES = {200, 300, 400, 500, 700, 1000, 1200, 1500, 2000, 3000, 5000}; + private static final long TRIAL_DAYS = 30L; + + private MediaFileService mediaFileService; + private SettingsService settingsService; + private PlayerService playerService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + + int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0); + timeOffset = Math.max(0, timeOffset); + Integer duration = file.getDurationSeconds(); + if (duration != null) { + map.put("skipOffsets", createSkipOffsets(duration)); + timeOffset = Math.min(duration, timeOffset); + duration -= timeOffset; + } + + map.put("video", file); + map.put("player", playerService.getPlayer(request, response).getId()); + map.put("maxBitRate", ServletRequestUtils.getIntParameter(request, "maxBitRate", DEFAULT_BIT_RATE)); + map.put("popout", ServletRequestUtils.getBooleanParameter(request, "popout", false)); + map.put("duration", duration); + map.put("timeOffset", timeOffset); + map.put("bitRates", BIT_RATES); + + if (!settingsService.isLicenseValid() && settingsService.getVideoTrialExpires() == null) { + Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L); + settingsService.setVideoTrialExpires(expiryDate); + settingsService.save(); + } + Date trialExpires = settingsService.getVideoTrialExpires(); + map.put("trialExpires", trialExpires); + map.put("trialExpired", trialExpires != null && trialExpires.before(new Date())); + map.put("trial", trialExpires != null && !settingsService.isLicenseValid()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public static Map<String, Integer> createSkipOffsets(int durationSeconds) { + LinkedHashMap<String, Integer> result = new LinkedHashMap<String, Integer>(); + for (int i = 0; i < durationSeconds; i += 60) { + result.put(StringUtil.formatDuration(i), i); + } + return result; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java new file mode 100644 index 00000000..02509687 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java @@ -0,0 +1,247 @@ +/* + 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 <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MusicIndexService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Multi-controller used for wap pages. + * + * @author Sindre Mehus + */ +public class WapController extends MultiActionController { + + private SettingsService settingsService; + private PlayerService playerService; + private PlaylistService playlistService; + private SecurityService securityService; + private MusicIndexService musicIndexService; + private MediaFileService mediaFileService; + private SearchService searchService; + + public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception { + return wap(request, response); + } + + public ModelAndView wap(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + List<MusicFolder> folders = settingsService.getAllMusicFolders(); + + if (folders.isEmpty()) { + map.put("noMusic", true); + } else { + + SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> allArtists = musicIndexService.getIndexedArtists(folders); + + // If an index is given as parameter, only show music files for this index. + String index = request.getParameter("index"); + if (index != null) { + SortedSet<MusicIndex.Artist> artists = allArtists.get(new MusicIndex(index)); + if (artists == null) { + map.put("noMusic", true); + } else { + map.put("artists", artists); + } + } + + // Otherwise, list all indexes. + else { + map.put("indexes", allArtists.keySet()); + } + } + + return new ModelAndView("wap/index", "model", map); + } + + public ModelAndView browse(HttpServletRequest request, HttpServletResponse response) throws Exception { + String path = request.getParameter("path"); + MediaFile parent = mediaFileService.getMediaFile(path); + + // Create array of file(s) to display. + List<MediaFile> children; + if (parent.isDirectory()) { + children = mediaFileService.getChildrenOf(parent, true, true, true); + } else { + children = new ArrayList<MediaFile>(); + children.add(parent); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("parent", parent); + map.put("children", children); + map.put("user", securityService.getCurrentUser(request)); + + return new ModelAndView("wap/browse", "model", map); + } + + public ModelAndView playlist(HttpServletRequest request, HttpServletResponse response) throws Exception { + // Create array of players to control. If the "player" attribute is set for this session, + // only the player with this ID is controlled. Otherwise, all players are controlled. + List<Player> players = playerService.getAllPlayers(); + + String playerId = (String) request.getSession().getAttribute("player"); + if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + if (player != null) { + players = Arrays.asList(player); + } + } + + Map<String, Object> map = new HashMap<String, Object>(); + + for (Player player : players) { + PlayQueue playQueue = player.getPlayQueue(); + map.put("playlist", playQueue); + + if (request.getParameter("play") != null) { + MediaFile file = mediaFileService.getMediaFile(request.getParameter("play")); + playQueue.addFiles(false, file); + } else if (request.getParameter("add") != null) { + MediaFile file = mediaFileService.getMediaFile(request.getParameter("add")); + playQueue.addFiles(true, file); + } else if (request.getParameter("skip") != null) { + playQueue.setIndex(Integer.parseInt(request.getParameter("skip"))); + } else if (request.getParameter("clear") != null) { + playQueue.clear(); + } else if (request.getParameter("load") != null) { + List<MediaFile> songs = playlistService.getFilesInPlaylist(ServletRequestUtils.getIntParameter(request, "id")); + playQueue.addFiles(false, songs); + } else if (request.getParameter("random") != null) { + List<MediaFile> randomFiles = searchService.getRandomSongs(new RandomSearchCriteria(20, null, null, null, null)); + playQueue.addFiles(false, randomFiles); + } + } + + map.put("players", players); + return new ModelAndView("wap/playlist", "model", map); + } + + public ModelAndView loadPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("playlists", playlistService.getReadablePlaylistsForUser(securityService.getCurrentUsername(request))); + return new ModelAndView("wap/loadPlaylist", "model", map); + } + + public ModelAndView search(HttpServletRequest request, HttpServletResponse response) throws Exception { + return new ModelAndView("wap/search"); + } + + public ModelAndView searchResult(HttpServletRequest request, HttpServletResponse response) throws Exception { + String query = request.getParameter("query"); + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("hits", search(query)); + + return new ModelAndView("wap/searchResult", "model", map); + } + + public ModelAndView settings(HttpServletRequest request, HttpServletResponse response) throws Exception { + String playerId = (String) request.getSession().getAttribute("player"); + + List<Player> allPlayers = playerService.getAllPlayers(); + User user = securityService.getCurrentUser(request); + List<Player> players = new ArrayList<Player>(); + Map<String, Object> map = new HashMap<String, Object>(); + + for (Player player : allPlayers) { + // Only display authorized players. + if (user.isAdminRole() || user.getUsername().equals(player.getUsername())) { + players.add(player); + } + + } + map.put("playerId", playerId); + map.put("players", players); + return new ModelAndView("wap/settings", "model", map); + } + + public ModelAndView selectPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception { + request.getSession().setAttribute("player", request.getParameter("playerId")); + return settings(request, response); + } + + private List<MediaFile> search(String query) throws IOException { + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(query); + criteria.setOffset(0); + criteria.setCount(50); + + SearchResult result = searchService.search(criteria, SearchService.IndexType.SONG); + return result.getMediaFiles(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} |