aboutsummaryrefslogtreecommitdiff
path: root/subsonic-main/src/main/java/net/sourceforge/subsonic/controller
diff options
context:
space:
mode:
authorScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
committerScott Jackson <daneren2005@gmail.com>2012-07-02 21:24:02 -0700
commita1a18f77a50804e0127dfa4b0f5240c49c541184 (patch)
tree19a38880afe505beddb5590379a8134d7730a277 /subsonic-main/src/main/java/net/sourceforge/subsonic/controller
parentb61d787706979e7e20f4c3c4f93c1f129d92273f (diff)
downloaddsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.gz
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.bz2
dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.zip
Initial Commit
Diffstat (limited to 'subsonic-main/src/main/java/net/sourceforge/subsonic/controller')
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java60
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java91
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java38
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java82
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java141
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java72
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java294
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java66
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java74
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java453
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java194
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java179
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java114
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java80
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java340
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java93
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java116
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java270
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java46
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java128
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java297
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java89
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java244
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java130
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java89
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java79
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java58
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java164
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java77
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java150
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java82
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java152
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java102
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java85
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java67
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java68
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java1983
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java101
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java66
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java106
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java58
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java69
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java52
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java123
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java161
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java96
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java149
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java141
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java419
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java84
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java139
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java260
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java145
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java159
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java110
-rw-r--r--subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java247
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() + "&amp;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;
+ }
+}