diff options
author | Scott Jackson <daneren2005@gmail.com> | 2012-07-02 21:24:02 -0700 |
---|---|---|
committer | Scott Jackson <daneren2005@gmail.com> | 2012-07-02 21:24:02 -0700 |
commit | a1a18f77a50804e0127dfa4b0f5240c49c541184 (patch) | |
tree | 19a38880afe505beddb5590379a8134d7730a277 /subsonic-main/src/main/java | |
parent | b61d787706979e7e20f4c3c4f93c1f129d92273f (diff) | |
download | dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.gz dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.tar.bz2 dsub-a1a18f77a50804e0127dfa4b0f5240c49c541184.zip |
Initial Commit
Diffstat (limited to 'subsonic-main/src/main/java')
234 files changed, 40162 insertions, 0 deletions
diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java new file mode 100644 index 00000000..0e595458 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.java @@ -0,0 +1,231 @@ +/* + 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; + +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import org.apache.commons.lang.exception.*; + +import java.io.*; +import java.text.*; +import java.util.*; + +/** + * Logger implementation which logs to SUBSONIC_HOME/subsonic.log. + * <br/> + * Note: Third party logging libraries (such as log4j and Commons logging) are intentionally not + * used. These libraries causes a lot of headache when deploying to some application servers + * (for instance Jetty and JBoss). + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2005/05/09 19:58:26 $ + */ +public class Logger { + + private String category; + + private static List<Entry> entries = Collections.synchronizedList(new BoundedList<Entry>(50)); + private static PrintWriter writer; + + /** + * Creates a logger for the given class. + * @param clazz The class. + * @return A logger for the class. + */ + public static Logger getLogger(Class clazz) { + return new Logger(clazz.getName()); + } + + /** + * Creates a logger for the given namee. + * @param name The name. + * @return A logger for the name. + */ + public static Logger getLogger(String name) { + return new Logger(name); + } + + /** + * Returns the last few log entries. + * @return The last few log entries. + */ + public static Entry[] getLatestLogEntries() { + return entries.toArray(new Entry[0]); + } + + private Logger(String name) { + int lastDot = name.lastIndexOf('.'); + if (lastDot == -1) { + category = name; + } else { + category = name.substring(lastDot + 1); + } + } + + /** + * Logs a debug message. + * @param message The log message. + */ + public void debug(Object message) { + debug(message, null); + } + + /** + * Logs a debug message. + * @param message The message. + * @param error The optional exception. + */ + public void debug(Object message, Throwable error) { + add(Level.DEBUG, message, error); + } + + /** + * Logs an info message. + * @param message The message. + */ + public void info(Object message) { + info(message, null); + } + + /** + * Logs an info message. + * @param message The message. + * @param error The optional exception. + */ + public void info(Object message, Throwable error) { + add(Level.INFO, message, error); + } + + /** + * Logs a warning message. + * @param message The message. + */ + public void warn(Object message) { + warn(message, null); + } + + /** + * Logs a warning message. + * @param message The message. + * @param error The optional exception. + */ + public void warn(Object message, Throwable error) { + add(Level.WARN, message, error); + } + + /** + * Logs an error message. + * @param message The message. + */ + public void error(Object message) { + error(message, null); + } + + /** + * Logs an error message. + * @param message The message. + * @param error The optional exception. + */ + public void error(Object message, Throwable error) { + add(Level.ERROR, message, error); + } + + private void add(Level level, Object message, Throwable error) { + Entry entry = new Entry(category, level, message, error); + try { + getPrintWriter().println(entry); + } catch (IOException x) { + System.err.println("Failed to write to subsonic.log."); + x.printStackTrace(); + } + entries.add(entry); + } + + private static synchronized PrintWriter getPrintWriter() throws IOException { + if (writer == null) { + writer = new PrintWriter(new FileWriter(getLogFile(), false), true); + } + return writer; + } + + public static File getLogFile() { + File subsonicHome = SettingsService.getSubsonicHome(); + return new File(subsonicHome, "subsonic.log"); + } + + /** + * Log level. + */ + public enum Level { + DEBUG, INFO, WARN, ERROR + } + + /** + * Log entry. + */ + public static class Entry { + private String category; + private Date date; + private Level level; + private Object message; + private Throwable error; + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS"); + + public Entry(String category, Level level, Object message, Throwable error) { + this.date = new Date(); + this.category = category; + this.level = level; + this.message = message; + this.error = error; + } + + public String getCategory() { + return category; + } + + public Date getDate() { + return date; + } + + public Level getLevel() { + return level; + } + + public Object getMessage() { + return message; + } + + public Throwable getError() { + return error; + } + + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append('[').append(DATE_FORMAT.format(date)).append("] "); + buf.append(level).append(' '); + buf.append(category).append(" - "); + buf.append(message); + + if (error != null) { + buf.append('\n').append(ExceptionUtils.getFullStackTrace(error)); + } + return buf.toString(); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java new file mode 100644 index 00000000..8905c8a6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ChatService.java @@ -0,0 +1,163 @@ +/* + 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.ajax; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.util.BoundedList; +import org.apache.commons.lang.StringUtils; +import org.directwebremoting.WebContext; +import org.directwebremoting.WebContextFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * Provides AJAX-enabled services for the chatting. + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class ChatService { + + private static final Logger LOG = Logger.getLogger(ChatService.class); + private static final String CACHE_KEY = "1"; + private static final int MAX_MESSAGES = 10; + private static final long TTL_MILLIS = 3L * 24L * 60L * 60L * 1000L; // 3 days. + + private final LinkedList<Message> messages = new BoundedList<Message>(MAX_MESSAGES); + private SecurityService securityService; + + private long revision = System.identityHashCode(this); + + /** + * Invoked by Spring. + */ + public void init() { + // Delete old messages every hour. + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + Runnable runnable = new Runnable() { + public void run() { + removeOldMessages(); + } + }; + executor.scheduleWithFixedDelay(runnable, 0L, 3600L, TimeUnit.SECONDS); + } + + private synchronized void removeOldMessages() { + long now = System.currentTimeMillis(); + for (Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) { + Message message = iterator.next(); + if (now - message.getDate().getTime() > TTL_MILLIS) { + iterator.remove(); + revision++; + } + } + } + + public synchronized void addMessage(String message) { + WebContext webContext = WebContextFactory.get(); + doAddMessage(message, webContext.getHttpServletRequest()); + } + + public synchronized void doAddMessage(String message, HttpServletRequest request) { + + String user = securityService.getCurrentUsername(request); + message = StringUtils.trimToNull(message); + if (message != null && user != null) { + messages.addFirst(new Message(message, user, new Date())); + revision++; + } + } + + public synchronized void clearMessages() { + messages.clear(); + revision++; + } + + /** + * Returns all messages, but only if the given revision is different from the + * current revision. + */ + public synchronized Messages getMessages(long revision) { + if (this.revision != revision) { + return new Messages(new ArrayList<Message>(messages), this.revision); + } + return null; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public static class Messages implements Serializable { + + private static final long serialVersionUID = -752602719879818165L; + private final List<Message> messages; + private final long revision; + + public Messages(List<Message> messages, long revision) { + this.messages = messages; + this.revision = revision; + } + + public List<Message> getMessages() { + return messages; + } + + public long getRevision() { + return revision; + } + } + + public static class Message implements Serializable { + + private static final long serialVersionUID = -1907101191518133712L; + private final String content; + private final String username; + private final Date date; + + public Message(String content, String username, Date date) { + this.content = content; + this.username = username; + this.date = date; + } + + public String getContent() { + return content; + } + + public String getUsername() { + return username; + } + + public Date getDate() { + return date; + } + + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java new file mode 100644 index 00000000..c9160f26 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtInfo.java @@ -0,0 +1,43 @@ +/* + 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.ajax; + +/** + * Contains info about cover art images for an album. + * + * @author Sindre Mehus + */ +public class CoverArtInfo { + + private final String imagePreviewUrl; + private final String imageDownloadUrl; + + public CoverArtInfo(String imagePreviewUrl, String imageDownloadUrl) { + this.imagePreviewUrl = imagePreviewUrl; + this.imageDownloadUrl = imageDownloadUrl; + } + + public String getImagePreviewUrl() { + return imagePreviewUrl; + } + + public String getImageDownloadUrl() { + return imageDownloadUrl; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.java new file mode 100644 index 00000000..1c3642b6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.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.ajax; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +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 net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Provides AJAX-enabled services for changing cover art images. + * <p/> + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class CoverArtService { + + private static final Logger LOG = Logger.getLogger(CoverArtService.class); + + private SecurityService securityService; + private MediaFileService mediaFileService; + + /** + * Downloads and saves the cover art at the given URL. + * + * @param albumId ID of the album in question. + * @param url The image URL. + * @return The error string if something goes wrong, <code>null</code> otherwise. + */ + public String setCoverArtImage(int albumId, String url) { + try { + MediaFile mediaFile = mediaFileService.getMediaFile(albumId); + saveCoverArt(mediaFile.getPath(), url); + return null; + } catch (Exception x) { + LOG.warn("Failed to save cover art for album " + albumId, x); + return x.toString(); + } + } + + private void saveCoverArt(String path, String url) throws Exception { + InputStream input = null; + HttpClient client = new DefaultHttpClient(); + + try { + HttpConnectionParams.setConnectionTimeout(client.getParams(), 20 * 1000); // 20 seconds + HttpConnectionParams.setSoTimeout(client.getParams(), 20 * 1000); // 20 seconds + HttpGet method = new HttpGet(url); + + HttpResponse response = client.execute(method); + input = response.getEntity().getContent(); + + // Attempt to resolve proper suffix. + String suffix = "jpg"; + if (url.toLowerCase().endsWith(".gif")) { + suffix = "gif"; + } else if (url.toLowerCase().endsWith(".png")) { + suffix = "png"; + } + + // Check permissions. + File newCoverFile = new File(path, "cover." + suffix); + if (!securityService.isWriteAllowed(newCoverFile)) { + throw new Exception("Permission denied: " + StringUtil.toHtml(newCoverFile.getPath())); + } + + // If file exists, create a backup. + backup(newCoverFile, new File(path, "cover.backup." + suffix)); + + // Write file. + IOUtils.copy(input, new FileOutputStream(newCoverFile)); + + MediaFile mediaFile = mediaFileService.getMediaFile(path); + + // Rename existing cover file if new cover file is not the preferred. + try { + File coverFile = mediaFileService.getCoverArt(mediaFile); + if (coverFile != null) { + if (!newCoverFile.equals(coverFile)) { + coverFile.renameTo(new File(coverFile.getCanonicalPath() + ".old")); + LOG.info("Renamed old image file " + coverFile); + } + } + } catch (Exception x) { + LOG.warn("Failed to rename existing cover file.", x); + } + + mediaFileService.refreshMediaFile(mediaFile); + + } finally { + IOUtils.closeQuietly(input); + client.getConnectionManager().shutdown(); + } + } + + private void backup(File newCoverFile, File backup) { + if (newCoverFile.exists()) { + if (backup.exists()) { + backup.delete(); + } + if (newCoverFile.renameTo(backup)) { + LOG.info("Backed up old image file to " + backup); + } else { + LOG.warn("Failed to create image file backup " + backup); + } + } + } + + 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/ajax/LyricsInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.java new file mode 100644 index 00000000..b84ffe1f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.java @@ -0,0 +1,53 @@ +/* + 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.ajax; + +/** + * Contains lyrics info for a song. + * + * @author Sindre Mehus + */ +public class LyricsInfo { + + private final String lyrics; + private final String artist; + private final String title; + + public LyricsInfo() { + this(null, null, null); + } + + public LyricsInfo(String lyrics, String artist, String title) { + this.lyrics = lyrics; + this.artist = artist; + this.title = title; + } + + public String getLyrics() { + return lyrics; + } + + public String getArtist() { + return artist; + } + + public String getTitle() { + return title; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java new file mode 100644 index 00000000..45c039f7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.java @@ -0,0 +1,105 @@ +/* + 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.ajax; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.util.StringUtil; + +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.params.HttpConnectionParams; +import org.jdom.Document; +import org.jdom.Element; +import org.jdom.Namespace; +import org.jdom.input.SAXBuilder; + +import java.io.IOException; +import java.io.StringReader; + +/** + * Provides AJAX-enabled services for retrieving song lyrics from chartlyrics.com. + * <p/> + * See http://www.chartlyrics.com/api.aspx for details. + * <p/> + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class LyricsService { + + private static final Logger LOG = Logger.getLogger(LyricsService.class); + + /** + * Returns lyrics for the given song and artist. + * + * @param artist The artist. + * @param song The song. + * @return The lyrics, never <code>null</code> . + */ + public LyricsInfo getLyrics(String artist, String song) { + LyricsInfo lyrics = new LyricsInfo(); + try { + + artist = StringUtil.urlEncode(artist); + song = StringUtil.urlEncode(song); + + String url = "http://api.chartlyrics.com/apiv1.asmx/SearchLyricDirect?artist=" + artist + "&song=" + song; + String xml = executeGetRequest(url); + + lyrics = parseSearchResult(xml); + + } catch (Exception x) { + LOG.warn("Failed to get lyrics for song '" + song + "'.", x); + } + return lyrics; + } + + + private LyricsInfo parseSearchResult(String xml) throws Exception { + SAXBuilder builder = new SAXBuilder(); + Document document = builder.build(new StringReader(xml)); + + Element root = document.getRootElement(); + Namespace ns = root.getNamespace(); + + String lyric = root.getChildText("Lyric", ns); + String song = root.getChildText("LyricSong", ns); + String artist = root.getChildText("LyricArtist", ns); + + return new LyricsInfo(lyric, artist, song); + } + + private String executeGetRequest(String url) throws IOException { + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000); + HttpConnectionParams.setSoTimeout(client.getParams(), 15000); + HttpGet method = new HttpGet(url); + try { + + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + return client.execute(method, responseHandler); + + } finally { + client.getConnectionManager().shutdown(); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java new file mode 100644 index 00000000..0c83e30f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java @@ -0,0 +1,51 @@ +/* + 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.ajax; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.service.NetworkService; + +/** + * Provides miscellaneous AJAX-enabled services. + * <p/> + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class MultiService { + + private static final Logger LOG = Logger.getLogger(MultiService.class); + private NetworkService networkService; + + /** + * Returns status for port forwarding and URL redirection. + */ + public NetworkStatus getNetworkStatus() { + NetworkService.Status portForwardingStatus = networkService.getPortForwardingStatus(); + NetworkService.Status urlRedirectionStatus = networkService.getURLRedirecionStatus(); + return new NetworkStatus(portForwardingStatus.getText(), + portForwardingStatus.getDate(), + urlRedirectionStatus.getText(), + urlRedirectionStatus.getDate()); + } + + 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/ajax/NetworkStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NetworkStatus.java new file mode 100644 index 00000000..8634af26 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NetworkStatus.java @@ -0,0 +1,55 @@ +/* + 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.ajax; + +import java.util.Date; + +/** + * @author Sindre Mehus + */ +public class NetworkStatus { + private final String portForwardingStatusText; + private final Date portForwardingStatusDate; + private final String urlRedirectionStatusText; + private final Date urlRedirectionStatusDate; + + public NetworkStatus(String portForwardingStatusText, Date portForwardingStatusDate, + String urlRedirectionStatusText, Date urlRedirectionStatusDate) { + this.portForwardingStatusText = portForwardingStatusText; + this.portForwardingStatusDate = portForwardingStatusDate; + this.urlRedirectionStatusText = urlRedirectionStatusText; + this.urlRedirectionStatusDate = urlRedirectionStatusDate; + } + + public String getPortForwardingStatusText() { + return portForwardingStatusText; + } + + public Date getPortForwardingStatusDate() { + return portForwardingStatusDate; + } + + public String getUrlRedirectionStatusText() { + return urlRedirectionStatusText; + } + + public Date getUrlRedirectionStatusDate() { + return urlRedirectionStatusDate; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java new file mode 100644 index 00000000..520cfcab --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingInfo.java @@ -0,0 +1,98 @@ +/* + 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.ajax; + +/** + * Details about what a user is currently listening to. + * + * @author Sindre Mehus + */ +public class NowPlayingInfo { + + private final String username; + private final String artist; + private final String title; + private final String tooltip; + private final String streamUrl; + private final String albumUrl; + private final String lyricsUrl; + private final String coverArtUrl; + private final String coverArtZoomUrl; + private final String avatarUrl; + private final int minutesAgo; + + public NowPlayingInfo(String user, String artist, String title, String tooltip, String streamUrl, String albumUrl, + String lyricsUrl, String coverArtUrl, String coverArtZoomUrl, String avatarUrl, int minutesAgo) { + this.username = user; + this.artist = artist; + this.title = title; + this.tooltip = tooltip; + this.streamUrl = streamUrl; + this.albumUrl = albumUrl; + this.lyricsUrl = lyricsUrl; + this.coverArtUrl = coverArtUrl; + this.coverArtZoomUrl = coverArtZoomUrl; + this.avatarUrl = avatarUrl; + this.minutesAgo = minutesAgo; + } + + public String getUsername() { + return username; + } + + public String getArtist() { + return artist; + } + + public String getTitle() { + return title; + } + + public String getTooltip() { + return tooltip; + } + + public String getStreamUrl() { + return streamUrl; + } + + public String getAlbumUrl() { + return albumUrl; + } + + public String getLyricsUrl() { + return lyricsUrl; + } + + public String getCoverArtUrl() { + return coverArtUrl; + } + + public String getCoverArtZoomUrl() { + return coverArtZoomUrl; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public int getMinutesAgo() { + return minutesAgo; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java new file mode 100644 index 00000000..ef7922b4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java @@ -0,0 +1,172 @@ +/* + 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.ajax; + +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.lang.StringUtils; +import org.directwebremoting.WebContext; +import org.directwebremoting.WebContextFactory; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides AJAX-enabled services for retrieving the currently playing file and directory. + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class NowPlayingService { + + private PlayerService playerService; + private StatusService statusService; + private SettingsService settingsService; + private MediaScannerService mediaScannerService; + private MediaFileService mediaFileService; + + /** + * Returns details about what the current player is playing. + * + * @return Details about what the current player is playing, or <code>null</code> if not playing anything. + */ + public NowPlayingInfo getNowPlayingForCurrentPlayer() throws Exception { + WebContext webContext = WebContextFactory.get(); + Player player = playerService.getPlayer(webContext.getHttpServletRequest(), webContext.getHttpServletResponse()); + List<TransferStatus> statuses = statusService.getStreamStatusesForPlayer(player); + List<NowPlayingInfo> result = convert(statuses); + + return result.isEmpty() ? null : result.get(0); + } + + /** + * Returns details about what all users are currently playing. + * + * @return Details about what all users are currently playing. + */ + public List<NowPlayingInfo> getNowPlaying() throws Exception { + return convert(statusService.getAllStreamStatuses()); + } + + /** + * Returns media folder scanning status. + */ + public ScanInfo getScanningStatus() { + return new ScanInfo(mediaScannerService.isScanning(), mediaScannerService.getScanCount()); + } + + private List<NowPlayingInfo> convert(List<TransferStatus> statuses) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String url = request.getRequestURL().toString(); + List<NowPlayingInfo> result = new ArrayList<NowPlayingInfo>(); + for (TransferStatus status : statuses) { + + 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); + File coverArt = mediaFileService.getCoverArt(mediaFile); + + String artist = mediaFile.getArtist(); + String title = mediaFile.getTitle(); + String streamUrl = url.replaceFirst("/dwr/.*", "/stream?player=" + player.getId() + "&id=" + mediaFile.getId()); + String albumUrl = url.replaceFirst("/dwr/.*", "/main.view?id=" + mediaFile.getId()); + String lyricsUrl = url.replaceFirst("/dwr/.*", "/lyrics.view?artistUtf8Hex=" + StringUtil.utf8HexEncode(artist) + + "&songUtf8Hex=" + StringUtil.utf8HexEncode(title)); + String coverArtUrl = coverArt == null ? null : url.replaceFirst("/dwr/.*", "/coverArt.view?size=48&id=" + mediaFile.getId()); + String coverArtZoomUrl = coverArt == null ? null : url.replaceFirst("/dwr/.*", "/coverArt.view?id=" + mediaFile.getId()); + + String avatarUrl = null; + if (userSettings.getAvatarScheme() == AvatarScheme.SYSTEM) { + avatarUrl = url.replaceFirst("/dwr/.*", "/avatar.view?id=" + userSettings.getSystemAvatarId()); + } else if (userSettings.getAvatarScheme() == AvatarScheme.CUSTOM && settingsService.getCustomAvatar(username) != null) { + avatarUrl = url.replaceFirst("/dwr/.*", "/avatar.view?username=" + username); + } + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + streamUrl = StringUtil.rewriteUrl(streamUrl, referer); + albumUrl = StringUtil.rewriteUrl(albumUrl, referer); + lyricsUrl = StringUtil.rewriteUrl(lyricsUrl, referer); + coverArtUrl = StringUtil.rewriteUrl(coverArtUrl, referer); + coverArtZoomUrl = StringUtil.rewriteUrl(coverArtZoomUrl, referer); + avatarUrl = StringUtil.rewriteUrl(avatarUrl, referer); + } + + String tooltip = StringUtil.toHtml(artist) + " – " + StringUtil.toHtml(title); + + if (StringUtils.isNotBlank(player.getName())) { + username += "@" + player.getName(); + } + artist = StringUtil.toHtml(StringUtils.abbreviate(artist, 25)); + title = StringUtil.toHtml(StringUtils.abbreviate(title, 25)); + username = StringUtil.toHtml(StringUtils.abbreviate(username, 25)); + + long minutesAgo = status.getMillisSinceLastUpdate() / 1000L / 60L; + if (minutesAgo < 60) { + result.add(new NowPlayingInfo(username, artist, title, tooltip, streamUrl, albumUrl, lyricsUrl, + coverArtUrl, coverArtZoomUrl, avatarUrl, (int) minutesAgo)); + } + } + } + + return result; + + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java new file mode 100644 index 00000000..e95ec1c8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java @@ -0,0 +1,174 @@ +/* + 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.ajax; + +import java.util.List; + +/** + * The playlist of a player. + * + * @author Sindre Mehus + */ +public class PlayQueueInfo { + + private final List<Entry> entries; + private final int index; + private final boolean stopEnabled; + private final boolean repeatEnabled; + private final boolean sendM3U; + private final float gain; + + public PlayQueueInfo(List<Entry> entries, int index, boolean stopEnabled, boolean repeatEnabled, boolean sendM3U, float gain) { + this.entries = entries; + this.index = index; + this.stopEnabled = stopEnabled; + this.repeatEnabled = repeatEnabled; + this.sendM3U = sendM3U; + this.gain = gain; + } + + public List<Entry> getEntries() { + return entries; + } + + public int getIndex() { + return index; + } + + public boolean isStopEnabled() { + return stopEnabled; + } + + public boolean isSendM3U() { + return sendM3U; + } + + public boolean isRepeatEnabled() { + return repeatEnabled; + } + + public float getGain() { + return gain; + } + + public static class Entry { + private final int id; + private final Integer trackNumber; + private final String title; + private final String artist; + private final String album; + private final String genre; + private final Integer year; + private final String bitRate; + private final Integer duration; + private final String durationAsString; + private final String format; + private final String contentType; + private final String fileSize; + private final boolean starred; + private final String albumUrl; + private final String streamUrl; + + public Entry(int id, Integer trackNumber, String title, String artist, String album, String genre, Integer year, + String bitRate, Integer duration, String durationAsString, String format, String contentType, String fileSize, + boolean starred, String albumUrl, String streamUrl) { + this.id = id; + this.trackNumber = trackNumber; + this.title = title; + this.artist = artist; + this.album = album; + this.genre = genre; + this.year = year; + this.bitRate = bitRate; + this.duration = duration; + this.durationAsString = durationAsString; + this.format = format; + this.contentType = contentType; + this.fileSize = fileSize; + this.starred = starred; + this.albumUrl = albumUrl; + this.streamUrl = streamUrl; + } + + public int getId() { + return id; + } + + public Integer getTrackNumber() { + return trackNumber; + } + + public String getTitle() { + return title; + } + + public String getArtist() { + return artist; + } + + public String getAlbum() { + return album; + } + + public String getGenre() { + return genre; + } + + public Integer getYear() { + return year; + } + + public String getBitRate() { + return bitRate; + } + + public String getDurationAsString() { + return durationAsString; + } + + public Integer getDuration() { + return duration; + } + + public String getFormat() { + return format; + } + + public String getContentType() { + return contentType; + } + + public String getFileSize() { + return fileSize; + } + + public boolean isStarred() { + return starred; + } + + public String getAlbumUrl() { + return albumUrl; + } + + public String getStreamUrl() { + return streamUrl; + } + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java new file mode 100644 index 00000000..94f78aba --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java @@ -0,0 +1,455 @@ +/* + 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.ajax; + +import java.io.IOException; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.service.PlaylistService; +import org.directwebremoting.WebContextFactory; +import org.springframework.web.servlet.support.RequestContextUtils; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Provides AJAX-enabled services for manipulating the play queue of a player. + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class PlayQueueService { + + private PlayerService playerService; + private JukeboxService jukeboxService; + private TranscodingService transcodingService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + private SecurityService securityService; + private MediaFileDao mediaFileDao; + private net.sourceforge.subsonic.service.PlaylistService playlistService; + + /** + * Returns the play queue for the player of the current user. + * + * @return The play queue. + */ + public PlayQueueInfo getPlayQueue() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + return convert(request, player, false); + } + + public PlayQueueInfo start() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doStart(request, response); + } + + public PlayQueueInfo doStart(HttpServletRequest request, HttpServletResponse response) throws Exception { + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().setStatus(PlayQueue.Status.PLAYING); + return convert(request, player, true); + } + + public PlayQueueInfo stop() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doStop(request, response); + } + + public PlayQueueInfo doStop(HttpServletRequest request, HttpServletResponse response) throws Exception { + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().setStatus(PlayQueue.Status.STOPPED); + return convert(request, player, true); + } + + public PlayQueueInfo skip(int index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doSkip(request, response, index, 0); + } + + public PlayQueueInfo doSkip(HttpServletRequest request, HttpServletResponse response, int index, int offset) throws Exception { + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().setIndex(index); + boolean serverSidePlaylist = !player.isExternalWithPlaylist(); + return convert(request, player, serverSidePlaylist, offset); + } + + public PlayQueueInfo play(int id) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + Player player = getCurrentPlayer(request, response); + MediaFile file = mediaFileService.getMediaFile(id); + List<MediaFile> files = mediaFileService.getDescendantsOf(file, true); + return doPlay(request, player, files); + } + + public PlayQueueInfo playPlaylist(int id) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + List<MediaFile> files = playlistService.getFilesInPlaylist(id); + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files); + } + + private PlayQueueInfo doPlay(HttpServletRequest request, Player player, List<MediaFile> files) throws Exception { + if (player.isWeb()) { + removeVideoFiles(files); + } + player.getPlayQueue().addFiles(false, files); + player.getPlayQueue().setRandomSearchCriteria(null); + return convert(request, player, true); + } + + public PlayQueueInfo playRandom(int id, int count) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + MediaFile file = mediaFileService.getMediaFile(id); + List<MediaFile> randomFiles = getRandomChildren(file, count); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().addFiles(false, randomFiles); + player.getPlayQueue().setRandomSearchCriteria(null); + return convert(request, player, true); + } + + public PlayQueueInfo add(int id) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doAdd(request, response, new int[]{id}); + } + + public PlayQueueInfo doAdd(HttpServletRequest request, HttpServletResponse response, int[] ids) throws Exception { + Player player = getCurrentPlayer(request, response); + List<MediaFile> files = new ArrayList<MediaFile>(ids.length); + for (int id : ids) { + MediaFile ancestor = mediaFileService.getMediaFile(id); + files.addAll(mediaFileService.getDescendantsOf(ancestor, true)); + } + if (player.isWeb()) { + removeVideoFiles(files); + } + player.getPlayQueue().addFiles(true, files); + player.getPlayQueue().setRandomSearchCriteria(null); + return convert(request, player, false); + } + + public PlayQueueInfo doSet(HttpServletRequest request, HttpServletResponse response, int[] ids) throws Exception { + Player player = getCurrentPlayer(request, response); + PlayQueue playQueue = player.getPlayQueue(); + MediaFile currentFile = playQueue.getCurrentFile(); + PlayQueue.Status status = playQueue.getStatus(); + + playQueue.clear(); + PlayQueueInfo result = doAdd(request, response, ids); + + int index = currentFile == null ? -1 : playQueue.getFiles().indexOf(currentFile); + playQueue.setIndex(index); + playQueue.setStatus(status); + return result; + } + + public PlayQueueInfo clear() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doClear(request, response); + } + + public PlayQueueInfo doClear(HttpServletRequest request, HttpServletResponse response) throws Exception { + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().clear(); + boolean serverSidePlaylist = !player.isExternalWithPlaylist(); + return convert(request, player, serverSidePlaylist); + } + + public PlayQueueInfo shuffle() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doShuffle(request, response); + } + + public PlayQueueInfo doShuffle(HttpServletRequest request, HttpServletResponse response) throws Exception { + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().shuffle(); + return convert(request, player, false); + } + + public PlayQueueInfo remove(int index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doRemove(request, response, index); + } + + public PlayQueueInfo toggleStar(int index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + + MediaFile file = player.getPlayQueue().getFile(index); + String username = securityService.getCurrentUsername(request); + boolean starred = mediaFileDao.getMediaFileStarredDate(file.getId(), username) != null; + if (starred) { + mediaFileDao.unstarMediaFile(file.getId(), username); + } else { + mediaFileDao.starMediaFile(file.getId(), username); + } + return convert(request, player, false); + } + + public PlayQueueInfo doRemove(HttpServletRequest request, HttpServletResponse response, int index) throws Exception { + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().removeFileAt(index); + return convert(request, player, false); + } + + public PlayQueueInfo removeMany(int[] indexes) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + for (int i = indexes.length - 1; i >= 0; i--) { + player.getPlayQueue().removeFileAt(indexes[i]); + } + return convert(request, player, false); + } + + public PlayQueueInfo up(int index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().moveUp(index); + return convert(request, player, false); + } + + public PlayQueueInfo down(int index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().moveDown(index); + return convert(request, player, false); + } + + public PlayQueueInfo toggleRepeat() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().setRepeatEnabled(!player.getPlayQueue().isRepeatEnabled()); + return convert(request, player, false); + } + + public PlayQueueInfo undo() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().undo(); + boolean serverSidePlaylist = !player.isExternalWithPlaylist(); + return convert(request, player, serverSidePlaylist); + } + + public PlayQueueInfo sortByTrack() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().sort(PlayQueue.SortOrder.TRACK); + return convert(request, player, false); + } + + public PlayQueueInfo sortByArtist() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().sort(PlayQueue.SortOrder.ARTIST); + return convert(request, player, false); + } + + public PlayQueueInfo sortByAlbum() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().sort(PlayQueue.SortOrder.ALBUM); + return convert(request, player, false); + } + + public void setGain(float gain) { + jukeboxService.setGain(gain); + } + + + public String savePlaylist() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + Locale locale = settingsService.getLocale(); + DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, locale); + + Date now = new Date(); + Playlist playlist = new Playlist(); + playlist.setUsername(securityService.getCurrentUsername(request)); + playlist.setCreated(now); + playlist.setChanged(now); + playlist.setPublic(false); + playlist.setName(dateFormat.format(now)); + + playlistService.createPlaylist(playlist); + playlistService.setFilesInPlaylist(playlist.getId(), player.getPlayQueue().getFiles()); + return playlist.getName(); + } + + private List<MediaFile> getRandomChildren(MediaFile file, int count) throws IOException { + List<MediaFile> children = mediaFileService.getDescendantsOf(file, false); + removeVideoFiles(children); + + if (children.isEmpty()) { + return children; + } + Collections.shuffle(children); + return children.subList(0, Math.min(count, children.size())); + } + + private void removeVideoFiles(List<MediaFile> files) { + Iterator<MediaFile> iterator = files.iterator(); + while (iterator.hasNext()) { + MediaFile file = iterator.next(); + if (file.isVideo()) { + iterator.remove(); + } + } + } + + private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean sendM3U) throws Exception { + return convert(request, player, sendM3U, 0); + } + + private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean sendM3U, int offset) throws Exception { + String url = request.getRequestURL().toString(); + + if (sendM3U && player.isJukebox()) { + jukeboxService.updateJukebox(player, offset); + } + boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr()); + + boolean m3uSupported = player.isExternal() || player.isExternalWithPlaylist(); + sendM3U = player.isAutoControlEnabled() && m3uSupported && isCurrentPlayer && sendM3U; + Locale locale = RequestContextUtils.getLocale(request); + + List<PlayQueueInfo.Entry> entries = new ArrayList<PlayQueueInfo.Entry>(); + PlayQueue playQueue = player.getPlayQueue(); + for (MediaFile file : playQueue.getFiles()) { + String albumUrl = url.replaceFirst("/dwr/.*", "/main.view?id=" + file.getId()); + String streamUrl = url.replaceFirst("/dwr/.*", "/stream?player=" + player.getId() + "&id=" + file.getId()); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + albumUrl = StringUtil.rewriteUrl(albumUrl, referer); + streamUrl = StringUtil.rewriteUrl(streamUrl, referer); + } + + String format = formatFormat(player, file); + String username = securityService.getCurrentUsername(request); + boolean starred = mediaFileService.getMediaFileStarredDate(file.getId(), username) != null; + entries.add(new PlayQueueInfo.Entry(file.getId(), file.getTrackNumber(), file.getTitle(), file.getArtist(), + file.getAlbumName(), file.getGenre(), file.getYear(), formatBitRate(file), + file.getDurationSeconds(), file.getDurationString(), format, formatContentType(format), + formatFileSize(file.getFileSize(), locale), starred, albumUrl, streamUrl)); + } + boolean isStopEnabled = playQueue.getStatus() == PlayQueue.Status.PLAYING && !player.isExternalWithPlaylist(); + float gain = jukeboxService.getGain(); + return new PlayQueueInfo(entries, playQueue.getIndex(), isStopEnabled, playQueue.isRepeatEnabled(), sendM3U, gain); + } + + private String formatFileSize(Long fileSize, Locale locale) { + if (fileSize == null) { + return null; + } + return StringUtil.formatBytes(fileSize, locale); + } + + private String formatFormat(Player player, MediaFile file) { + return transcodingService.getSuffix(player, file, null); + } + + private String formatContentType(String format) { + return StringUtil.getMimeType(format); + } + + private String formatBitRate(MediaFile mediaFile) { + if (mediaFile.getBitRate() == null) { + return null; + } + if (mediaFile.isVariableBitRate()) { + return mediaFile.getBitRate() + " Kbps vbr"; + } + return mediaFile.getBitRate() + " Kbps"; + } + + private Player getCurrentPlayer(HttpServletRequest request, HttpServletResponse response) { + return playerService.getPlayer(request, response); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setJukeboxService(JukeboxService jukeboxService) { + this.jukeboxService = jukeboxService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + 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/ajax/PlaylistInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.java new file mode 100644 index 00000000..3fcbfb14 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.java @@ -0,0 +1,90 @@ +/* + 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.ajax; + +import java.util.List; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; + +/** + * The playlist of a player. + * + * @author Sindre Mehus + */ +public class PlaylistInfo { + + private final Playlist playlist; + private final List<Entry> entries; + + public PlaylistInfo(Playlist playlist, List<Entry> entries) { + this.playlist = playlist; + this.entries = entries; + } + + public Playlist getPlaylist() { + return playlist; + } + + public List<Entry> getEntries() { + return entries; + } + + public static class Entry { + private final int id; + private final String title; + private final String artist; + private final String album; + private final String durationAsString; + private final boolean starred; + + public Entry(int id, String title, String artist, String album, String durationAsString, boolean starred) { + this.id = id; + this.title = title; + this.artist = artist; + this.album = album; + this.durationAsString = durationAsString; + this.starred = starred; + } + + public int getId() { + return id; + } + + public String getTitle() { + return title; + } + + public String getArtist() { + return artist; + } + + public String getAlbum() { + return album; + } + + public String getDurationAsString() { + return durationAsString; + } + + public boolean isStarred() { + return starred; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java new file mode 100644 index 00000000..d3bf854f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java @@ -0,0 +1,187 @@ +/* + 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.ajax; + +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.service.SettingsService; +import org.directwebremoting.WebContextFactory; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * Provides AJAX-enabled services for manipulating playlists. + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class PlaylistService { + + private MediaFileService mediaFileService; + private SecurityService securityService; + private net.sourceforge.subsonic.service.PlaylistService playlistService; + private MediaFileDao mediaFileDao; + private SettingsService settingsService; + + public List<Playlist> getReadablePlaylists() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + return playlistService.getReadablePlaylistsForUser(username); + } + + public List<Playlist> getWritablePlaylists() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + return playlistService.getWritablePlaylistsForUser(username); + } + + public PlaylistInfo getPlaylist(int id) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + + Playlist playlist = playlistService.getPlaylist(id); + List<MediaFile> files = playlistService.getFilesInPlaylist(id); + + String username = securityService.getCurrentUsername(request); + mediaFileService.populateStarredDate(files, username); + return new PlaylistInfo(playlist, createEntries(files)); + } + + public List<Playlist> createEmptyPlaylist() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + Locale locale = settingsService.getLocale(); + DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, locale); + + Date now = new Date(); + Playlist playlist = new Playlist(); + playlist.setUsername(securityService.getCurrentUsername(request)); + playlist.setCreated(now); + playlist.setChanged(now); + playlist.setPublic(false); + playlist.setName(dateFormat.format(now)); + + playlistService.createPlaylist(playlist); + return getReadablePlaylists(); + } + + public void appendToPlaylist(int playlistId, List<Integer> mediaFileIds) { + List<MediaFile> files = playlistService.getFilesInPlaylist(playlistId); + for (Integer mediaFileId : mediaFileIds) { + MediaFile file = mediaFileService.getMediaFile(mediaFileId); + if (file != null) { + files.add(file); + } + } + playlistService.setFilesInPlaylist(playlistId, files); + } + + private List<PlaylistInfo.Entry> createEntries(List<MediaFile> files) { + List<PlaylistInfo.Entry> result = new ArrayList<PlaylistInfo.Entry>(); + for (MediaFile file : files) { + result.add(new PlaylistInfo.Entry(file.getId(), file.getTitle(), file.getArtist(), file.getAlbumName(), + file.getDurationString(), file.getStarredDate() != null)); + } + + return result; + } + + public PlaylistInfo toggleStar(int id, int index) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + List<MediaFile> files = playlistService.getFilesInPlaylist(id); + MediaFile file = files.get(index); + + boolean starred = mediaFileDao.getMediaFileStarredDate(file.getId(), username) != null; + if (starred) { + mediaFileDao.unstarMediaFile(file.getId(), username); + } else { + mediaFileDao.starMediaFile(file.getId(), username); + } + return getPlaylist(id); + } + + public PlaylistInfo remove(int id, int index) { + List<MediaFile> files = playlistService.getFilesInPlaylist(id); + files.remove(index); + playlistService.setFilesInPlaylist(id, files); + return getPlaylist(id); + } + + public PlaylistInfo up(int id, int index) { + List<MediaFile> files = playlistService.getFilesInPlaylist(id); + if (index > 0) { + MediaFile file = files.remove(index); + files.add(index - 1, file); + playlistService.setFilesInPlaylist(id, files); + } + return getPlaylist(id); + } + + public PlaylistInfo down(int id, int index) { + List<MediaFile> files = playlistService.getFilesInPlaylist(id); + if (index < files.size() - 1) { + MediaFile file = files.remove(index); + files.add(index + 1, file); + playlistService.setFilesInPlaylist(id, files); + } + return getPlaylist(id); + } + + public void deletePlaylist(int id) { + playlistService.deletePlaylist(id); + } + + public PlaylistInfo updatePlaylist(int id, String name, String comment, boolean isPublic) { + Playlist playlist = playlistService.getPlaylist(id); + playlist.setName(name); + playlist.setComment(comment); + playlist.setPublic(isPublic); + playlistService.updatePlaylist(playlist); + return getPlaylist(id); + } + + public void setPlaylistService(net.sourceforge.subsonic.service.PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + 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/ajax/ScanInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ScanInfo.java new file mode 100644 index 00000000..d984069e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ScanInfo.java @@ -0,0 +1,43 @@ +/* + 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.ajax; + +/** + * Media folder scanning status. + * + * @author Sindre Mehus + */ +public class ScanInfo { + + private final boolean scanning; + private final int count; + + public ScanInfo(boolean scanning, int count) { + this.scanning = scanning; + this.count = count; + } + + public boolean isScanning() { + return scanning; + } + + public int getCount() { + return count; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java new file mode 100644 index 00000000..15ba359b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.java @@ -0,0 +1,64 @@ +/* + 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.ajax; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.SecurityService; +import org.directwebremoting.WebContext; +import org.directwebremoting.WebContextFactory; + +/** + * Provides AJAX-enabled services for starring. + * <p/> + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class StarService { + + private static final Logger LOG = Logger.getLogger(StarService.class); + + private SecurityService securityService; + private MediaFileDao mediaFileDao; + + public void star(int id) { + mediaFileDao.starMediaFile(id, getUser()); + } + + public void unstar(int id) { + mediaFileDao.unstarMediaFile(id, getUser()); + } + + private String getUser() { + WebContext webContext = WebContextFactory.get(); + User user = securityService.getCurrentUser(webContext.getHttpServletRequest()); + return user.getUsername(); + } + + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.java new file mode 100644 index 00000000..f7373b4e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.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.ajax; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.metadata.MetaData; +import net.sourceforge.subsonic.service.metadata.MetaDataParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.StringUtils; + +/** + * Provides AJAX-enabled services for editing tags in music files. + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class TagService { + + private static final Logger LOG = Logger.getLogger(TagService.class); + + private MetaDataParserFactory metaDataParserFactory; + private MediaFileService mediaFileService; + + /** + * Updated tags for a given music file. + * + * @param id The ID of the music file. + * @param track The track number. + * @param artist The artist name. + * @param album The album name. + * @param title The song title. + * @param year The release year. + * @param genre The musical genre. + * @return "UPDATED" if the new tags were updated, "SKIPPED" if no update was necessary. + * Otherwise the error message is returned. + */ + public String setTags(int id, String track, String artist, String album, String title, String year, String genre) { + + track = StringUtils.trimToNull(track); + artist = StringUtils.trimToNull(artist); + album = StringUtils.trimToNull(album); + title = StringUtils.trimToNull(title); + year = StringUtils.trimToNull(year); + genre = StringUtils.trimToNull(genre); + + Integer trackNumber = null; + if (track != null) { + try { + trackNumber = new Integer(track); + } catch (NumberFormatException x) { + LOG.warn("Illegal track number: " + track, x); + } + } + + Integer yearNumber = null; + if (year != null) { + try { + yearNumber = new Integer(year); + } catch (NumberFormatException x) { + LOG.warn("Illegal year: " + year, x); + } + } + + try { + + MediaFile file = mediaFileService.getMediaFile(id); + MetaDataParser parser = metaDataParserFactory.getParser(file.getFile()); + + if (!parser.isEditingSupported()) { + return "Tag editing of " + FilenameUtils.getExtension(file.getPath()) + " files is not supported."; + } + + MetaData existingMetaData = parser.getRawMetaData(file.getFile()); + + if (StringUtils.equals(artist, existingMetaData.getArtist()) && + StringUtils.equals(album, existingMetaData.getAlbumName()) && + StringUtils.equals(title, existingMetaData.getTitle()) && + ObjectUtils.equals(yearNumber, existingMetaData.getYear()) && + StringUtils.equals(genre, existingMetaData.getGenre()) && + ObjectUtils.equals(trackNumber, existingMetaData.getTrackNumber())) { + return "SKIPPED"; + } + + MetaData newMetaData = new MetaData(); + newMetaData.setArtist(artist); + newMetaData.setAlbumName(album); + newMetaData.setTitle(title); + newMetaData.setYear(yearNumber); + newMetaData.setGenre(genre); + newMetaData.setTrackNumber(trackNumber); + parser.setMetaData(file, newMetaData); + mediaFileService.refreshMediaFile(file); + return "UPDATED"; + + } catch (Exception x) { + LOG.warn("Failed to update tags for " + id, x); + return x.getMessage(); + } + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { + this.metaDataParserFactory = metaDataParserFactory; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java new file mode 100644 index 00000000..19309348 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TransferService.java @@ -0,0 +1,49 @@ +/* + 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.ajax; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.controller.*; +import org.directwebremoting.*; + +import javax.servlet.http.*; + +/** + * Provides AJAX-enabled services for retrieving the status of ongoing transfers. + * This class is used by the DWR framework (http://getahead.ltd.uk/dwr/). + * + * @author Sindre Mehus + */ +public class TransferService { + + /** + * Returns info about any ongoing upload within the current session. + * @return Info about ongoing upload. + */ + public UploadInfo getUploadInfo() { + + HttpSession session = WebContextFactory.get().getSession(); + TransferStatus status = (TransferStatus) session.getAttribute(UploadController.UPLOAD_STATUS); + + if (status != null) { + return new UploadInfo(status.getBytesTransfered(), status.getBytesTotal()); + } + return new UploadInfo(0L, 0L); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.java new file mode 100644 index 00000000..47f9de99 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/UploadInfo.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.ajax; + +/** + * Contains status for a file upload. + * + * @author Sindre Mehus + */ +public class UploadInfo { + + private long bytesUploaded; + private long bytesTotal; + + public UploadInfo(long bytesUploaded, long bytesTotal) { + this.bytesUploaded = bytesUploaded; + this.bytesTotal = bytesTotal; + } + + /** + * Returns the number of bytes uploaded. + * @return The number of bytes uploaded. + */ + public long getBytesUploaded() { + return bytesUploaded; + } + + /** + * Returns the total number of bytes. + * @return The total number of bytes. + */ + public long getBytesTotal() { + return bytesTotal; + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.java new file mode 100644 index 00000000..00f656b1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/cache/CacheFactory.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.cache; + + +import java.io.File; + +import org.springframework.beans.factory.InitializingBean; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.config.Configuration; +import net.sf.ehcache.config.ConfigurationFactory; +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Initializes Ehcache and creates caches. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class CacheFactory implements InitializingBean { + + private static final Logger LOG = Logger.getLogger(CacheFactory.class); + private CacheManager cacheManager; + + public void afterPropertiesSet() throws Exception { + Configuration configuration = ConfigurationFactory.parseConfiguration(); + + // Override configuration to make sure cache is stored in Subsonic home dir. + File cacheDir = new File(SettingsService.getSubsonicHome(), "cache"); + configuration.getDiskStoreConfiguration().setPath(cacheDir.getPath()); + + cacheManager = CacheManager.create(configuration); + } + + public Ehcache getCache(String name) { + return cacheManager.getCache(name); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java new file mode 100644 index 00000000..6c87df51 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java @@ -0,0 +1,146 @@ +/* + 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.command; + +import net.sourceforge.subsonic.controller.AdvancedSettingsController; + +/** + * Command used in {@link AdvancedSettingsController}. + * + * @author Sindre Mehus + */ +public class AdvancedSettingsCommand { + private String downsampleCommand; + private String coverArtLimit; + private String downloadLimit; + private String uploadLimit; + private String streamPort; + private boolean ldapEnabled; + private String ldapUrl; + private String ldapSearchFilter; + private String ldapManagerDn; + private String ldapManagerPassword; + private boolean ldapAutoShadowing; + private String brand; + private boolean isReloadNeeded; + + public String getDownsampleCommand() { + return downsampleCommand; + } + + public void setDownsampleCommand(String downsampleCommand) { + this.downsampleCommand = downsampleCommand; + } + + public String getCoverArtLimit() { + return coverArtLimit; + } + + public void setCoverArtLimit(String coverArtLimit) { + this.coverArtLimit = coverArtLimit; + } + + public String getDownloadLimit() { + return downloadLimit; + } + + public void setDownloadLimit(String downloadLimit) { + this.downloadLimit = downloadLimit; + } + + public String getUploadLimit() { + return uploadLimit; + } + + public String getStreamPort() { + return streamPort; + } + + public void setStreamPort(String streamPort) { + this.streamPort = streamPort; + } + + public void setUploadLimit(String uploadLimit) { + this.uploadLimit = uploadLimit; + } + + public boolean isLdapEnabled() { + return ldapEnabled; + } + + public void setLdapEnabled(boolean ldapEnabled) { + this.ldapEnabled = ldapEnabled; + } + + public String getLdapUrl() { + return ldapUrl; + } + + public void setLdapUrl(String ldapUrl) { + this.ldapUrl = ldapUrl; + } + + public String getLdapSearchFilter() { + return ldapSearchFilter; + } + + public void setLdapSearchFilter(String ldapSearchFilter) { + this.ldapSearchFilter = ldapSearchFilter; + } + + public String getLdapManagerDn() { + return ldapManagerDn; + } + + public void setLdapManagerDn(String ldapManagerDn) { + this.ldapManagerDn = ldapManagerDn; + } + + public String getLdapManagerPassword() { + return ldapManagerPassword; + } + + public void setLdapManagerPassword(String ldapManagerPassword) { + this.ldapManagerPassword = ldapManagerPassword; + } + + public boolean isLdapAutoShadowing() { + return ldapAutoShadowing; + } + + public void setLdapAutoShadowing(boolean ldapAutoShadowing) { + this.ldapAutoShadowing = ldapAutoShadowing; + } + + public void setBrand(String brand) { + this.brand = brand; + } + + public String getBrand() { + return brand; + } + + public void setReloadNeeded(boolean reloadNeeded) { + isReloadNeeded = reloadNeeded; + } + + public boolean isReloadNeeded() { + return isReloadNeeded; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java new file mode 100644 index 00000000..04af0ff6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/DonateCommand.java @@ -0,0 +1,88 @@ +/* + 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.command; + +import net.sourceforge.subsonic.controller.DonateController; + +import java.util.Date; + +import org.apache.commons.lang.StringUtils; + +/** + * Command used in {@link DonateController}. + * + * @author Sindre Mehus + */ +public class DonateCommand { + + private String path; + private String emailAddress; + private String license; + private Date licenseDate; + private boolean licenseValid; + private String brand; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = StringUtils.trim(emailAddress); + } + + public String getLicense() { + return license; + } + + public void setLicense(String license) { + this.license = StringUtils.trim(license); + } + + public Date getLicenseDate() { + return licenseDate; + } + + public void setLicenseDate(Date licenseDate) { + this.licenseDate = licenseDate; + } + + public boolean isLicenseValid() { + return licenseValid; + } + + public void setLicenseValid(boolean licenseValid) { + this.licenseValid = licenseValid; + } + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java new file mode 100644 index 00000000..bb1fc5ff --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/EnumHolder.java @@ -0,0 +1,42 @@ +/* + 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.command; + +/** + * Holds the name and description of an enum value. + * + * @author Sindre Mehus + */ +public class EnumHolder { + private String name; + private String description; + + public EnumHolder(String name, String description) { + this.name = name; + this.description = description; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java new file mode 100644 index 00000000..2322a3bd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java @@ -0,0 +1,184 @@ +/* + 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.command; + +import net.sourceforge.subsonic.controller.GeneralSettingsController; +import net.sourceforge.subsonic.domain.Theme; + +/** + * Command used in {@link GeneralSettingsController}. + * + * @author Sindre Mehus + */ +public class GeneralSettingsCommand { + + private String musicFileTypes; + private String videoFileTypes; + private String coverArtFileTypes; + private String index; + private String ignoredArticles; + private String shortcuts; + private boolean sortAlbumsByYear; + private boolean gettingStartedEnabled; + private String welcomeTitle; + private String welcomeSubtitle; + private String welcomeMessage; + private String loginMessage; + private String localeIndex; + private String[] locales; + private String themeIndex; + private Theme[] themes; + private boolean isReloadNeeded; + + public String getMusicFileTypes() { + return musicFileTypes; + } + + public void setMusicFileTypes(String musicFileTypes) { + this.musicFileTypes = musicFileTypes; + } + + public String getVideoFileTypes() { + return videoFileTypes; + } + + public void setVideoFileTypes(String videoFileTypes) { + this.videoFileTypes = videoFileTypes; + } + + public String getCoverArtFileTypes() { + return coverArtFileTypes; + } + + public void setCoverArtFileTypes(String coverArtFileTypes) { + this.coverArtFileTypes = coverArtFileTypes; + } + + public String getIndex() { + return index; + } + + public void setIndex(String index) { + this.index = index; + } + + public String getIgnoredArticles() { + return ignoredArticles; + } + + public void setIgnoredArticles(String ignoredArticles) { + this.ignoredArticles = ignoredArticles; + } + + public String getShortcuts() { + return shortcuts; + } + + public void setShortcuts(String shortcuts) { + this.shortcuts = shortcuts; + } + + public String getWelcomeTitle() { + return welcomeTitle; + } + + public void setWelcomeTitle(String welcomeTitle) { + this.welcomeTitle = welcomeTitle; + } + + public String getWelcomeSubtitle() { + return welcomeSubtitle; + } + + public void setWelcomeSubtitle(String welcomeSubtitle) { + this.welcomeSubtitle = welcomeSubtitle; + } + + public String getWelcomeMessage() { + return welcomeMessage; + } + + public void setWelcomeMessage(String welcomeMessage) { + this.welcomeMessage = welcomeMessage; + } + + public String getLoginMessage() { + return loginMessage; + } + + public void setLoginMessage(String loginMessage) { + this.loginMessage = loginMessage; + } + + public String getLocaleIndex() { + return localeIndex; + } + + public void setLocaleIndex(String localeIndex) { + this.localeIndex = localeIndex; + } + + public String[] getLocales() { + return locales; + } + + public void setLocales(String[] locales) { + this.locales = locales; + } + + public String getThemeIndex() { + return themeIndex; + } + + public void setThemeIndex(String themeIndex) { + this.themeIndex = themeIndex; + } + + public Theme[] getThemes() { + return themes; + } + + public void setThemes(Theme[] themes) { + this.themes = themes; + } + + public boolean isReloadNeeded() { + return isReloadNeeded; + } + + public void setReloadNeeded(boolean reloadNeeded) { + isReloadNeeded = reloadNeeded; + } + + public boolean isSortAlbumsByYear() { + return sortAlbumsByYear; + } + + public void setSortAlbumsByYear(boolean sortAlbumsByYear) { + this.sortAlbumsByYear = sortAlbumsByYear; + } + + public boolean isGettingStartedEnabled() { + return gettingStartedEnabled; + } + + public void setGettingStartedEnabled(boolean gettingStartedEnabled) { + this.gettingStartedEnabled = gettingStartedEnabled; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java new file mode 100644 index 00000000..8fcfa72c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/MusicFolderSettingsCommand.java @@ -0,0 +1,187 @@ +/* + 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.command; + +import java.io.File; +import java.util.Date; +import java.util.List; + +import net.sourceforge.subsonic.controller.MusicFolderSettingsController; +import net.sourceforge.subsonic.domain.MusicFolder; +import org.apache.commons.lang.StringUtils; + +/** + * Command used in {@link MusicFolderSettingsController}. + * + * @author Sindre Mehus + */ +public class MusicFolderSettingsCommand { + + private String interval; + private String hour; + private boolean scanning; + private boolean fastCache; + private boolean organizeByFolderStructure; + private List<MusicFolderInfo> musicFolders; + private MusicFolderInfo newMusicFolder; + private boolean reload; + + public String getInterval() { + return interval; + } + + public void setInterval(String interval) { + this.interval = interval; + } + + public String getHour() { + return hour; + } + + public void setHour(String hour) { + this.hour = hour; + } + + public boolean isScanning() { + return scanning; + } + + public void setScanning(boolean scanning) { + this.scanning = scanning; + } + + public boolean isFastCache() { + return fastCache; + } + + public List<MusicFolderInfo> getMusicFolders() { + return musicFolders; + } + + public void setMusicFolders(List<MusicFolderInfo> musicFolders) { + this.musicFolders = musicFolders; + } + + public void setFastCache(boolean fastCache) { + this.fastCache = fastCache; + } + + public MusicFolderInfo getNewMusicFolder() { + return newMusicFolder; + } + + public void setNewMusicFolder(MusicFolderInfo newMusicFolder) { + this.newMusicFolder = newMusicFolder; + } + + public void setReload(boolean reload) { + this.reload = reload; + } + + public boolean isReload() { + return reload; + } + + public boolean isOrganizeByFolderStructure() { + return organizeByFolderStructure; + } + + public void setOrganizeByFolderStructure(boolean organizeByFolderStructure) { + this.organizeByFolderStructure = organizeByFolderStructure; + } + + public static class MusicFolderInfo { + + private Integer id; + private String path; + private String name; + private boolean enabled; + private boolean delete; + private boolean existing; + + public MusicFolderInfo(MusicFolder musicFolder) { + id = musicFolder.getId(); + path = musicFolder.getPath().getPath(); + name = musicFolder.getName(); + enabled = musicFolder.isEnabled(); + existing = musicFolder.getPath().exists() && musicFolder.getPath().isDirectory(); + } + + public MusicFolderInfo() { + enabled = true; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isDelete() { + return delete; + } + + public void setDelete(boolean delete) { + this.delete = delete; + } + + public MusicFolder toMusicFolder() { + String path = StringUtils.trimToNull(this.path); + if (path == null) { + return null; + } + File file = new File(path); + String name = StringUtils.trimToNull(this.name); + if (name == null) { + name = file.getName(); + } + return new MusicFolder(id, new File(path), name, enabled, new Date()); + } + + public boolean isExisting() { + return existing; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java new file mode 100644 index 00000000..d0ae2b07 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java @@ -0,0 +1,92 @@ +/* + 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.command; + +import java.util.Date; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class NetworkSettingsCommand { + + private boolean portForwardingEnabled; + private boolean urlRedirectionEnabled; + private String urlRedirectFrom; + private int port; + private boolean trial; + private Date trialExpires; + private boolean trialExpired; + + public void setPortForwardingEnabled(boolean portForwardingEnabled) { + this.portForwardingEnabled = portForwardingEnabled; + } + + public boolean isPortForwardingEnabled() { + return portForwardingEnabled; + } + + public boolean isUrlRedirectionEnabled() { + return urlRedirectionEnabled; + } + + public void setUrlRedirectionEnabled(boolean urlRedirectionEnabled) { + this.urlRedirectionEnabled = urlRedirectionEnabled; + } + + public String getUrlRedirectFrom() { + return urlRedirectFrom; + } + + public void setUrlRedirectFrom(String urlRedirectFrom) { + this.urlRedirectFrom = urlRedirectFrom; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public void setTrial(boolean trial) { + this.trial = trial; + } + + public boolean isTrial() { + return trial; + } + + public void setTrialExpires(Date trialExpires) { + this.trialExpires = trialExpires; + } + + public Date getTrialExpires() { + return trialExpires; + } + + public void setTrialExpired(boolean trialExpired) { + this.trialExpired = trialExpired; + } + + public boolean isTrialExpired() { + return trialExpired; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java new file mode 100644 index 00000000..b9e5383c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.java @@ -0,0 +1,65 @@ +/* + 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.command; + +import net.sourceforge.subsonic.controller.*; + +/** + * Command used in {@link PasswordSettingsController}. + * + * @author Sindre Mehus + */ +public class PasswordSettingsCommand { + private String username; + private String password; + private String confirmPassword; + private boolean ldapAuthenticated; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public boolean isLdapAuthenticated() { + return ldapAuthenticated; + } + + public void setLdapAuthenticated(boolean ldapAuthenticated) { + this.ldapAuthenticated = ldapAuthenticated; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java new file mode 100644 index 00000000..680d06e9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.java @@ -0,0 +1,215 @@ +/* + 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.command; + +import net.sourceforge.subsonic.controller.PersonalSettingsController; +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; + +import java.util.List; + +/** + * Command used in {@link PersonalSettingsController}. + * + * @author Sindre Mehus + */ +public class PersonalSettingsCommand { + private User user; + private String localeIndex; + private String[] locales; + private String themeIndex; + private Theme[] themes; + private int avatarId; + private List<Avatar> avatars; + private Avatar customAvatar; + private UserSettings.Visibility mainVisibility; + private UserSettings.Visibility playlistVisibility; + private boolean partyModeEnabled; + private boolean showNowPlayingEnabled; + private boolean showChatEnabled; + private boolean nowPlayingAllowed; + private boolean finalVersionNotificationEnabled; + private boolean betaVersionNotificationEnabled; + private boolean lastFmEnabled; + private String lastFmUsername; + private String lastFmPassword; + private boolean isReloadNeeded; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getLocaleIndex() { + return localeIndex; + } + + public void setLocaleIndex(String localeIndex) { + this.localeIndex = localeIndex; + } + + public String[] getLocales() { + return locales; + } + + public void setLocales(String[] locales) { + this.locales = locales; + } + + public String getThemeIndex() { + return themeIndex; + } + + public void setThemeIndex(String themeIndex) { + this.themeIndex = themeIndex; + } + + public Theme[] getThemes() { + return themes; + } + + public void setThemes(Theme[] themes) { + this.themes = themes; + } + + public int getAvatarId() { + return avatarId; + } + + public void setAvatarId(int avatarId) { + this.avatarId = avatarId; + } + + public List<Avatar> getAvatars() { + return avatars; + } + + public void setAvatars(List<Avatar> avatars) { + this.avatars = avatars; + } + + public Avatar getCustomAvatar() { + return customAvatar; + } + + public void setCustomAvatar(Avatar customAvatar) { + this.customAvatar = customAvatar; + } + + public UserSettings.Visibility getMainVisibility() { + return mainVisibility; + } + + public void setMainVisibility(UserSettings.Visibility mainVisibility) { + this.mainVisibility = mainVisibility; + } + + public UserSettings.Visibility getPlaylistVisibility() { + return playlistVisibility; + } + + public void setPlaylistVisibility(UserSettings.Visibility playlistVisibility) { + this.playlistVisibility = playlistVisibility; + } + + public boolean isPartyModeEnabled() { + return partyModeEnabled; + } + + public void setPartyModeEnabled(boolean partyModeEnabled) { + this.partyModeEnabled = partyModeEnabled; + } + + public boolean isShowNowPlayingEnabled() { + return showNowPlayingEnabled; + } + + public void setShowNowPlayingEnabled(boolean showNowPlayingEnabled) { + this.showNowPlayingEnabled = showNowPlayingEnabled; + } + + public boolean isShowChatEnabled() { + return showChatEnabled; + } + + public void setShowChatEnabled(boolean showChatEnabled) { + this.showChatEnabled = showChatEnabled; + } + + public boolean isNowPlayingAllowed() { + return nowPlayingAllowed; + } + + public void setNowPlayingAllowed(boolean nowPlayingAllowed) { + this.nowPlayingAllowed = nowPlayingAllowed; + } + + public boolean isFinalVersionNotificationEnabled() { + return finalVersionNotificationEnabled; + } + + public void setFinalVersionNotificationEnabled(boolean finalVersionNotificationEnabled) { + this.finalVersionNotificationEnabled = finalVersionNotificationEnabled; + } + + public boolean isBetaVersionNotificationEnabled() { + return betaVersionNotificationEnabled; + } + + public void setBetaVersionNotificationEnabled(boolean betaVersionNotificationEnabled) { + this.betaVersionNotificationEnabled = betaVersionNotificationEnabled; + } + + public boolean isLastFmEnabled() { + return lastFmEnabled; + } + + public void setLastFmEnabled(boolean lastFmEnabled) { + this.lastFmEnabled = lastFmEnabled; + } + + public String getLastFmUsername() { + return lastFmUsername; + } + + public void setLastFmUsername(String lastFmUsername) { + this.lastFmUsername = lastFmUsername; + } + + public String getLastFmPassword() { + return lastFmPassword; + } + + public void setLastFmPassword(String lastFmPassword) { + this.lastFmPassword = lastFmPassword; + } + + public boolean isReloadNeeded() { + return isReloadNeeded; + } + + public void setReloadNeeded(boolean reloadNeeded) { + isReloadNeeded = reloadNeeded; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java new file mode 100644 index 00000000..8331260d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java @@ -0,0 +1,250 @@ +/* + 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.command; + +import java.util.Date; +import java.util.List; + +import net.sourceforge.subsonic.controller.PlayerSettingsController; +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; + +/** + * Command used in {@link PlayerSettingsController}. + * + * @author Sindre Mehus + */ +public class PlayerSettingsCommand { + private String playerId; + private String name; + private String description; + private String type; + private Date lastSeen; + private boolean isDynamicIp; + private boolean isAutoControlEnabled; + private String coverArtSchemeName; + private String technologyName; + private String transcodeSchemeName; + private boolean transcodingSupported; + private String transcodeDirectory; + private List<Transcoding> allTranscodings; + private int[] activeTranscodingIds; + private EnumHolder[] technologyHolders; + private EnumHolder[] transcodeSchemeHolders; + private EnumHolder[] coverArtSchemeHolders; + private Player[] players; + private boolean isAdmin; + private boolean isReloadNeeded; + + public String getPlayerId() { + return playerId; + } + + public void setPlayerId(String playerId) { + this.playerId = playerId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Date getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Date lastSeen) { + this.lastSeen = lastSeen; + } + + public boolean isDynamicIp() { + return isDynamicIp; + } + + public void setDynamicIp(boolean dynamicIp) { + isDynamicIp = dynamicIp; + } + + public boolean isAutoControlEnabled() { + return isAutoControlEnabled; + } + + public void setAutoControlEnabled(boolean autoControlEnabled) { + isAutoControlEnabled = autoControlEnabled; + } + + public String getCoverArtSchemeName() { + return coverArtSchemeName; + } + + public void setCoverArtSchemeName(String coverArtSchemeName) { + this.coverArtSchemeName = coverArtSchemeName; + } + + public String getTranscodeSchemeName() { + return transcodeSchemeName; + } + + public void setTranscodeSchemeName(String transcodeSchemeName) { + this.transcodeSchemeName = transcodeSchemeName; + } + + public boolean isTranscodingSupported() { + return transcodingSupported; + } + + public void setTranscodingSupported(boolean transcodingSupported) { + this.transcodingSupported = transcodingSupported; + } + + public String getTranscodeDirectory() { + return transcodeDirectory; + } + + public void setTranscodeDirectory(String transcodeDirectory) { + this.transcodeDirectory = transcodeDirectory; + } + + public List<Transcoding> getAllTranscodings() { + return allTranscodings; + } + + public void setAllTranscodings(List<Transcoding> allTranscodings) { + this.allTranscodings = allTranscodings; + } + + public int[] getActiveTranscodingIds() { + return activeTranscodingIds; + } + + public void setActiveTranscodingIds(int[] activeTranscodingIds) { + this.activeTranscodingIds = activeTranscodingIds; + } + + public EnumHolder[] getTechnologyHolders() { + return technologyHolders; + } + + public void setTechnologies(PlayerTechnology[] technologies) { + technologyHolders = new EnumHolder[technologies.length]; + for (int i = 0; i < technologies.length; i++) { + PlayerTechnology technology = technologies[i]; + technologyHolders[i] = new EnumHolder(technology.name(), technology.toString()); + } + } + + public EnumHolder[] getTranscodeSchemeHolders() { + return transcodeSchemeHolders; + } + + public void setTranscodeSchemes(TranscodeScheme[] transcodeSchemes) { + transcodeSchemeHolders = new EnumHolder[transcodeSchemes.length]; + for (int i = 0; i < transcodeSchemes.length; i++) { + TranscodeScheme scheme = transcodeSchemes[i]; + transcodeSchemeHolders[i] = new EnumHolder(scheme.name(), scheme.toString()); + } + } + + public EnumHolder[] getCoverArtSchemeHolders() { + return coverArtSchemeHolders; + } + + public void setCoverArtSchemes(CoverArtScheme[] coverArtSchemes) { + coverArtSchemeHolders = new EnumHolder[coverArtSchemes.length]; + for (int i = 0; i < coverArtSchemes.length; i++) { + CoverArtScheme scheme = coverArtSchemes[i]; + coverArtSchemeHolders[i] = new EnumHolder(scheme.name(), scheme.toString()); + } + } + + public String getTechnologyName() { + return technologyName; + } + + public void setTechnologyName(String technologyName) { + this.technologyName = technologyName; + } + + public Player[] getPlayers() { + return players; + } + + public void setPlayers(Player[] players) { + this.players = players; + } + + public boolean isAdmin() { + return isAdmin; + } + + public void setAdmin(boolean admin) { + isAdmin = admin; + } + + public boolean isReloadNeeded() { + return isReloadNeeded; + } + + public void setReloadNeeded(boolean reloadNeeded) { + isReloadNeeded = reloadNeeded; + } + + /** + * Holds the transcoding and whether it is active for the given player. + */ + public static class TranscodingHolder { + private Transcoding transcoding; + private boolean isActive; + + public TranscodingHolder(Transcoding transcoding, boolean isActive) { + this.transcoding = transcoding; + this.isActive = isActive; + } + + public Transcoding getTranscoding() { + return transcoding; + } + + public boolean isActive() { + return isActive; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.java new file mode 100644 index 00000000..e6917ff4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.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.command; + +import net.sourceforge.subsonic.controller.PodcastSettingsController; + +/** + * Command used in {@link PodcastSettingsController}. + * + * @author Sindre Mehus + */ +public class PodcastSettingsCommand { + + private String interval; + private String folder; + private String episodeRetentionCount; + private String episodeDownloadCount; + + public String getInterval() { + return interval; + } + + public void setInterval(String interval) { + this.interval = interval; + } + + public String getFolder() { + return folder; + } + + public void setFolder(String folder) { + this.folder = folder; + } + + public String getEpisodeRetentionCount() { + return episodeRetentionCount; + } + + public void setEpisodeRetentionCount(String episodeRetentionCount) { + this.episodeRetentionCount = episodeRetentionCount; + } + + public String getEpisodeDownloadCount() { + return episodeDownloadCount; + } + + public void setEpisodeDownloadCount(String episodeDownloadCount) { + this.episodeDownloadCount = episodeDownloadCount; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java new file mode 100644 index 00000000..0dacfbd4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/SearchCommand.java @@ -0,0 +1,135 @@ +/* + 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.command; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.controller.*; + +import java.util.*; + +/** + * Command used in {@link SearchController}. + * + * @author Sindre Mehus + */ +public class SearchCommand { + + private String query; + private List<MediaFile> artists; + private List<MediaFile> albums; + private List<MediaFile> songs; + private boolean isIndexBeingCreated; + private User user; + private boolean partyModeEnabled; + private Player player; + + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public boolean isIndexBeingCreated() { + return isIndexBeingCreated; + } + + public void setIndexBeingCreated(boolean indexBeingCreated) { + isIndexBeingCreated = indexBeingCreated; + } + + public List<MediaFile> getArtists() { + return artists; + } + + public void setArtists(List<MediaFile> artists) { + this.artists = artists; + } + + public List<MediaFile> getAlbums() { + return albums; + } + + public void setAlbums(List<MediaFile> albums) { + this.albums = albums; + } + + public List<MediaFile> getSongs() { + return songs; + } + + public void setSongs(List<MediaFile> songs) { + this.songs = songs; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public boolean isPartyModeEnabled() { + return partyModeEnabled; + } + + public void setPartyModeEnabled(boolean partyModeEnabled) { + this.partyModeEnabled = partyModeEnabled; + } + + public Player getPlayer() { + return player; + } + + public void setPlayer(Player player) { + this.player = player; + } + + public static class Match { + private MediaFile mediaFile; + private String title; + private String album; + private String artist; + + public Match(MediaFile mediaFile, String title, String album, String artist) { + this.mediaFile = mediaFile; + this.title = title; + this.album = album; + this.artist = artist; + } + + public MediaFile getMediaFile() { + return mediaFile; + } + + public String getTitle() { + return title; + } + + public String getAlbum() { + return album; + } + + public String getArtist() { + return artist; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java new file mode 100644 index 00000000..ce185f7b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java @@ -0,0 +1,278 @@ +/* + 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.command; + +import java.util.List; + +import net.sourceforge.subsonic.controller.*; +import net.sourceforge.subsonic.domain.*; + +/** + * Command used in {@link UserSettingsController}. + * + * @author Sindre Mehus + */ +public class UserSettingsCommand { + private String username; + private boolean isAdminRole; + private boolean isDownloadRole; + private boolean isUploadRole; + private boolean isCoverArtRole; + private boolean isCommentRole; + private boolean isPodcastRole; + private boolean isStreamRole; + private boolean isJukeboxRole; + private boolean isSettingsRole; + private boolean isShareRole; + + private List<User> users; + private boolean isAdmin; + private boolean isPasswordChange; + private boolean isNew; + private boolean isDelete; + private String password; + private String confirmPassword; + private String email; + private boolean isLdapAuthenticated; + private boolean isLdapEnabled; + + private String transcodeSchemeName; + private EnumHolder[] transcodeSchemeHolders; + private boolean transcodingSupported; + private String transcodeDirectory; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public boolean isAdminRole() { + return isAdminRole; + } + + public void setAdminRole(boolean adminRole) { + isAdminRole = adminRole; + } + + public boolean isDownloadRole() { + return isDownloadRole; + } + + public void setDownloadRole(boolean downloadRole) { + isDownloadRole = downloadRole; + } + + public boolean isUploadRole() { + return isUploadRole; + } + + public void setUploadRole(boolean uploadRole) { + isUploadRole = uploadRole; + } + + public boolean isCoverArtRole() { + return isCoverArtRole; + } + + public void setCoverArtRole(boolean coverArtRole) { + isCoverArtRole = coverArtRole; + } + + public boolean isCommentRole() { + return isCommentRole; + } + + public void setCommentRole(boolean commentRole) { + isCommentRole = commentRole; + } + + public boolean isPodcastRole() { + return isPodcastRole; + } + + public void setPodcastRole(boolean podcastRole) { + isPodcastRole = podcastRole; + } + + public boolean isStreamRole() { + return isStreamRole; + } + + public void setStreamRole(boolean streamRole) { + isStreamRole = streamRole; + } + + public boolean isJukeboxRole() { + return isJukeboxRole; + } + + public void setJukeboxRole(boolean jukeboxRole) { + isJukeboxRole = jukeboxRole; + } + + public boolean isSettingsRole() { + return isSettingsRole; + } + + public void setSettingsRole(boolean settingsRole) { + isSettingsRole = settingsRole; + } + + public boolean isShareRole() { + return isShareRole; + } + + public void setShareRole(boolean shareRole) { + isShareRole = shareRole; + } + + public List<User> getUsers() { + return users; + } + + public void setUsers(List<User> users) { + this.users = users; + } + + public boolean isAdmin() { + return isAdmin; + } + + public void setAdmin(boolean admin) { + isAdmin = admin; + } + + public boolean isPasswordChange() { + return isPasswordChange; + } + + public void setPasswordChange(boolean passwordChange) { + isPasswordChange = passwordChange; + } + + public boolean isNew() { + return isNew; + } + + public void setNew(boolean isNew) { + this.isNew = isNew; + } + + public boolean isDelete() { + return isDelete; + } + + public void setDelete(boolean delete) { + isDelete = delete; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isLdapAuthenticated() { + return isLdapAuthenticated; + } + + public void setLdapAuthenticated(boolean ldapAuthenticated) { + isLdapAuthenticated = ldapAuthenticated; + } + + public boolean isLdapEnabled() { + return isLdapEnabled; + } + + public void setLdapEnabled(boolean ldapEnabled) { + isLdapEnabled = ldapEnabled; + } + + public String getTranscodeSchemeName() { + return transcodeSchemeName; + } + + public void setTranscodeSchemeName(String transcodeSchemeName) { + this.transcodeSchemeName = transcodeSchemeName; + } + + public EnumHolder[] getTranscodeSchemeHolders() { + return transcodeSchemeHolders; + } + + public void setTranscodeSchemes(TranscodeScheme[] transcodeSchemes) { + transcodeSchemeHolders = new EnumHolder[transcodeSchemes.length]; + for (int i = 0; i < transcodeSchemes.length; i++) { + TranscodeScheme scheme = transcodeSchemes[i]; + transcodeSchemeHolders[i] = new EnumHolder(scheme.name(), scheme.toString()); + } + } + + public boolean isTranscodingSupported() { + return transcodingSupported; + } + + public void setTranscodingSupported(boolean transcodingSupported) { + this.transcodingSupported = transcodingSupported; + } + + public String getTranscodeDirectory() { + return transcodeDirectory; + } + + public void setTranscodeDirectory(String transcodeDirectory) { + this.transcodeDirectory = transcodeDirectory; + } + + public void setUser(User user) { + username = user == null ? null : user.getUsername(); + isAdminRole = user != null && user.isAdminRole(); + isDownloadRole = user != null && user.isDownloadRole(); + isUploadRole = user != null && user.isUploadRole(); + isCoverArtRole = user != null && user.isCoverArtRole(); + isCommentRole = user != null && user.isCommentRole(); + isPodcastRole = user != null && user.isPodcastRole(); + isStreamRole = user != null && user.isStreamRole(); + isJukeboxRole = user != null && user.isJukeboxRole(); + isSettingsRole = user != null && user.isSettingsRole(); + isShareRole = user != null && user.isShareRole(); + isLdapAuthenticated = user != null && user.isLdapAuthenticated(); + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java new file mode 100644 index 00000000..f163f82d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AbstractChartController.java @@ -0,0 +1,60 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.support.*; +import org.springframework.web.servlet.mvc.*; +import org.springframework.ui.context.*; + +import javax.servlet.http.*; +import java.awt.*; +import java.util.*; + +/** + * Abstract super class for controllers which generate charts. + * + * @author Sindre Mehus + */ +public abstract class AbstractChartController implements Controller { + + /** + * Returns the chart background color for the current theme. + * @param request The servlet request. + * @return The chart background color. + */ + protected Color getBackground(HttpServletRequest request) { + return getColor("backgroundColor", request); + } + + /** + * Returns the chart foreground color for the current theme. + * @param request The servlet request. + * @return The chart foreground color. + */ + protected Color getForeground(HttpServletRequest request) { + return getColor("textColor", request); + } + + private Color getColor(String code, HttpServletRequest request) { + Theme theme = RequestContextUtils.getTheme(request); + Locale locale = RequestContextUtils.getLocale(request); + String colorHex = theme.getMessageSource().getMessage(code, new Object[0], locale); + return new Color(Integer.parseInt(colorHex, 16)); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java new file mode 100644 index 00000000..0b43f4eb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.java @@ -0,0 +1,91 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.AdvancedSettingsCommand; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.mvc.SimpleFormController; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.HttpServletRequest; + +/** + * Controller for the page used to administrate advanced settings. + * + * @author Sindre Mehus + */ +public class AdvancedSettingsController extends SimpleFormController { + + private SettingsService settingsService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + AdvancedSettingsCommand command = new AdvancedSettingsCommand(); + command.setCoverArtLimit(String.valueOf(settingsService.getCoverArtLimit())); + command.setDownsampleCommand(settingsService.getDownsamplingCommand()); + command.setDownloadLimit(String.valueOf(settingsService.getDownloadBitrateLimit())); + command.setUploadLimit(String.valueOf(settingsService.getUploadBitrateLimit())); + command.setStreamPort(String.valueOf(settingsService.getStreamPort())); + command.setLdapEnabled(settingsService.isLdapEnabled()); + command.setLdapUrl(settingsService.getLdapUrl()); + command.setLdapSearchFilter(settingsService.getLdapSearchFilter()); + command.setLdapManagerDn(settingsService.getLdapManagerDn()); + command.setLdapAutoShadowing(settingsService.isLdapAutoShadowing()); + command.setBrand(settingsService.getBrand()); + + return command; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + AdvancedSettingsCommand command = (AdvancedSettingsCommand) comm; + + command.setReloadNeeded(false); + settingsService.setDownsamplingCommand(command.getDownsampleCommand()); + + try { + settingsService.setCoverArtLimit(Integer.parseInt(command.getCoverArtLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setDownloadBitrateLimit(Long.parseLong(command.getDownloadLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setUploadBitrateLimit(Long.parseLong(command.getUploadLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setStreamPort(Integer.parseInt(command.getStreamPort())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + + settingsService.setLdapEnabled(command.isLdapEnabled()); + settingsService.setLdapUrl(command.getLdapUrl()); + settingsService.setLdapSearchFilter(command.getLdapSearchFilter()); + settingsService.setLdapManagerDn(command.getLdapManagerDn()); + settingsService.setLdapAutoShadowing(command.isLdapAutoShadowing()); + + if (StringUtils.isNotEmpty(command.getLdapManagerPassword())) { + settingsService.setLdapManagerPassword(command.getLdapManagerPassword()); + } + + settingsService.save(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java new file mode 100644 index 00000000..8b34f383 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AllmusicController.java @@ -0,0 +1,38 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; + +/** + * Controller for the page which forwards to allmusic.com. + * + * @author Sindre Mehus + */ +public class AllmusicController extends ParameterizableViewController { + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("album", request.getParameter("album")); + return result; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java new file mode 100644 index 00000000..100fcedb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.java @@ -0,0 +1,82 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Controller which produces avatar images. + * + * @author Sindre Mehus + */ +public class AvatarController implements Controller, LastModified { + + private SettingsService settingsService; + + public long getLastModified(HttpServletRequest request) { + Avatar avatar = getAvatar(request); + return avatar == null ? -1L : avatar.getCreatedDate().getTime(); + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + Avatar avatar = getAvatar(request); + + if (avatar == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + // TODO: specify caching filter. + + response.setContentType(avatar.getMimeType()); + response.getOutputStream().write(avatar.getData()); + return null; + } + + private Avatar getAvatar(HttpServletRequest request) { + String id = request.getParameter("id"); + if (id != null) { + return settingsService.getSystemAvatar(Integer.parseInt(id)); + } + + String username = request.getParameter("username"); + if (username == null) { + return null; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + if (userSettings.getAvatarScheme() == AvatarScheme.SYSTEM) { + return settingsService.getSystemAvatar(userSettings.getSystemAvatarId()); + } + return settingsService.getCustomAvatar(username); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java new file mode 100644 index 00000000..a22cd9a9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarUploadController.java @@ -0,0 +1,141 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller which receives uploaded avatar images. + * + * @author Sindre Mehus + */ +public class AvatarUploadController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(AvatarUploadController.class); + private static final int MAX_AVATAR_SIZE = 64; + + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String username = securityService.getCurrentUsername(request); + + // Check that we have a file upload request. + if (!ServletFileUpload.isMultipartContent(request)) { + throw new Exception("Illegal request."); + } + + Map<String, Object> map = new HashMap<String, Object>(); + FileItemFactory factory = new DiskFileItemFactory(); + ServletFileUpload upload = new ServletFileUpload(factory); + List<?> items = upload.parseRequest(request); + + // Look for file items. + for (Object o : items) { + FileItem item = (FileItem) o; + + if (!item.isFormField()) { + String fileName = item.getName(); + byte[] data = item.get(); + + if (StringUtils.isNotBlank(fileName) && data.length > 0) { + createAvatar(fileName, data, username, map); + } else { + map.put("error", new Exception("Missing file.")); + LOG.warn("Failed to upload personal image. No file specified."); + } + break; + } + } + + map.put("username", username); + map.put("avatar", settingsService.getCustomAvatar(username)); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private void createAvatar(String fileName, byte[] data, String username, Map<String, Object> map) throws IOException { + + BufferedImage image; + try { + image = ImageIO.read(new ByteArrayInputStream(data)); + if (image == null) { + throw new Exception("Failed to decode incoming image: " + fileName + " (" + data.length + " bytes)."); + } + int width = image.getWidth(); + int height = image.getHeight(); + String mimeType = StringUtil.getMimeType(FilenameUtils.getExtension(fileName)); + + // Scale down image if necessary. + if (width > MAX_AVATAR_SIZE || height > MAX_AVATAR_SIZE) { + double scaleFactor = (double) MAX_AVATAR_SIZE / (double) Math.max(width, height); + height = (int) (height * scaleFactor); + width = (int) (width * scaleFactor); + image = CoverArtController.scale(image, width, height); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + ImageIO.write(image, "jpeg", out); + data = out.toByteArray(); + mimeType = StringUtil.getMimeType("jpeg"); + map.put("resized", true); + } + Avatar avatar = new Avatar(0, fileName, new Date(), mimeType, width, height, data); + settingsService.setCustomAvatar(avatar, username); + LOG.info("Created avatar '" + fileName + "' (" + data.length + " bytes) for user " + username); + + } catch (Exception x) { + LOG.warn("Failed to upload personal image: " + x, x); + map.put("error", x); + } + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java new file mode 100644 index 00000000..94c88656 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ChangeCoverArtController.java @@ -0,0 +1,72 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; + +/** + * Controller for changing cover art. + * + * @author Sindre Mehus + */ +public class ChangeCoverArtController extends ParameterizableViewController { + + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + String artist = request.getParameter("artist"); + String album = request.getParameter("album"); + MediaFile dir = mediaFileService.getMediaFile(id); + + if (artist == null) { + artist = dir.getArtist(); + } + if (album == null) { + album = dir.getAlbumName(); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("id", id); + map.put("artist", artist); + map.put("album", album); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + + return result; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java new file mode 100644 index 00000000..a5093024 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java @@ -0,0 +1,294 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Controller which produces cover art images. + * + * @author Sindre Mehus + */ +public class CoverArtController implements Controller, LastModified { + + public static final String ALBUM_COVERART_PREFIX = "al-"; + public static final String ARTIST_COVERART_PREFIX = "ar-"; + + private static final Logger LOG = Logger.getLogger(CoverArtController.class); + + private SecurityService securityService; + private MediaFileService mediaFileService; + private ArtistDao artistDao; + private AlbumDao albumDao; + + public long getLastModified(HttpServletRequest request) { + try { + File file = getImageFile(request); + if (file == null) { + return 0; // Request for the default image. + } + if (!FileUtil.exists(file)) { + return -1; + } + + return FileUtil.lastModified(file); + } catch (Exception e) { + return -1; + } + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + File file = getImageFile(request); + + if (file != null && !FileUtil.exists(file)) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + // Check access. + if (file != null && !securityService.isReadAllowed(file)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + } + + // Send default image if no path is given. (No need to cache it, since it will be cached in browser.) + Integer size = ServletRequestUtils.getIntParameter(request, "size"); + if (file == null) { + sendDefault(size, response); + return null; + } + + // Optimize if no scaling is required. + if (size == null) { + sendUnscaled(file, response); + return null; + } + + // Send cached image, creating it if necessary. + try { + File cachedImage = getCachedImage(file, size); + sendImage(cachedImage, response); + } catch (IOException e) { + sendDefault(size, response); + } + + return null; + } + + private File getImageFile(HttpServletRequest request) { + String id = request.getParameter("id"); + if (id != null) { + if (id.startsWith(ALBUM_COVERART_PREFIX)) { + return getAlbumImage(Integer.valueOf(id.replace(ALBUM_COVERART_PREFIX, ""))); + } + if (id.startsWith(ARTIST_COVERART_PREFIX)) { + return getArtistImage(Integer.valueOf(id.replace(ARTIST_COVERART_PREFIX, ""))); + } + return getMediaFileImage(Integer.valueOf(id)); + } + + String path = StringUtils.trimToNull(request.getParameter("path")); + return path != null ? new File(path) : null; + } + + private File getArtistImage(int id) { + Artist artist = artistDao.getArtist(id); + return artist == null || artist.getCoverArtPath() == null ? null : new File(artist.getCoverArtPath()); + } + + private File getAlbumImage(int id) { + Album album = albumDao.getAlbum(id); + return album == null || album.getCoverArtPath() == null ? null : new File(album.getCoverArtPath()); + } + + private File getMediaFileImage(int id) { + MediaFile mediaFile = mediaFileService.getMediaFile(id); + return mediaFile == null ? null : mediaFileService.getCoverArt(mediaFile); + } + + private void sendImage(File file, HttpServletResponse response) throws IOException { + InputStream in = new FileInputStream(file); + try { + IOUtils.copy(in, response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private void sendDefault(Integer size, HttpServletResponse response) throws IOException { + InputStream in = null; + try { + in = getClass().getResourceAsStream("default_cover.jpg"); + BufferedImage image = ImageIO.read(in); + if (size != null) { + image = scale(image, size, size); + } + ImageIO.write(image, "jpeg", response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private void sendUnscaled(File file, HttpServletResponse response) throws IOException { + InputStream in = null; + try { + in = getImageInputStream(file); + IOUtils.copy(in, response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private File getCachedImage(File file, int size) throws IOException { + String md5 = DigestUtils.md5Hex(file.getPath()); + File cachedImage = new File(getImageCacheDirectory(size), md5 + ".jpeg"); + + // Is cache missing or obsolete? + if (!cachedImage.exists() || FileUtil.lastModified(file) > cachedImage.lastModified()) { + InputStream in = null; + OutputStream out = null; + try { + in = getImageInputStream(file); + out = new FileOutputStream(cachedImage); + BufferedImage image = ImageIO.read(in); + if (image == null) { + throw new Exception("Unable to decode image."); + } + + image = scale(image, size, size); + ImageIO.write(image, "jpeg", out); + + } catch (Throwable x) { + // Delete corrupt (probably empty) thumbnail cache. + LOG.warn("Failed to create thumbnail for " + file, x); + IOUtils.closeQuietly(out); + cachedImage.delete(); + throw new IOException("Failed to create thumbnail for " + file + ". " + x.getMessage()); + + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + } + } + return cachedImage; + } + + /** + * Returns an input stream to the image in the given file. If the file is an audio file, + * the embedded album art is returned. + */ + private InputStream getImageInputStream(File file) throws IOException { + JaudiotaggerParser parser = new JaudiotaggerParser(); + if (parser.isApplicable(file)) { + MediaFile mediaFile = mediaFileService.getMediaFile(file); + return new ByteArrayInputStream(parser.getImageData(mediaFile)); + } else { + return new FileInputStream(file); + } + } + + private synchronized File getImageCacheDirectory(int size) { + File dir = new File(SettingsService.getSubsonicHome(), "thumbs"); + dir = new File(dir, String.valueOf(size)); + if (!dir.exists()) { + if (dir.mkdirs()) { + LOG.info("Created thumbnail cache " + dir); + } else { + LOG.error("Failed to create thumbnail cache " + dir); + } + } + + return dir; + } + + public static BufferedImage scale(BufferedImage image, int width, int height) { + int w = image.getWidth(); + int h = image.getHeight(); + BufferedImage thumb = image; + + // For optimal results, use step by step bilinear resampling - halfing the size at each step. + do { + w /= 2; + h /= 2; + if (w < width) { + w = width; + } + if (h < height) { + h = height; + } + + BufferedImage temp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Graphics2D g2 = temp.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(thumb, 0, 0, temp.getWidth(), temp.getHeight(), null); + g2.dispose(); + + thumb = temp; + } while (w != width); + + return thumb; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java new file mode 100644 index 00000000..17d06497 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DBController.java @@ -0,0 +1,66 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.dao.DaoHelper; +import org.apache.commons.lang.exception.ExceptionUtils; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.ColumnMapRowMapper; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the DB admin page. + * + * @author Sindre Mehus + */ +public class DBController extends ParameterizableViewController { + + private DaoHelper daoHelper; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + String query = request.getParameter("query"); + if (query != null) { + map.put("query", query); + + try { + List<?> result = daoHelper.getJdbcTemplate().query(query, new ColumnMapRowMapper()); + map.put("result", result); + } catch (DataAccessException x) { + map.put("error", ExceptionUtils.getRootCause(x).getMessage()); + } + } + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setDaoHelper(DaoHelper daoHelper) { + this.daoHelper = daoHelper; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java new file mode 100644 index 00000000..144d3327 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DonateController.java @@ -0,0 +1,74 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.DonateCommand; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; + +/** + * Controller for the donation page. + * + * @author Sindre Mehus + */ +public class DonateController extends SimpleFormController { + + private SettingsService settingsService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + DonateCommand command = new DonateCommand(); + command.setPath(request.getParameter("path")); + + command.setEmailAddress(settingsService.getLicenseEmail()); + command.setLicenseDate(settingsService.getLicenseDate()); + command.setLicenseValid(settingsService.isLicenseValid()); + command.setLicense(settingsService.getLicenseCode()); + command.setBrand(settingsService.getBrand()); + + return command; + } + + protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors) + throws Exception { + DonateCommand command = (DonateCommand) com; + Date now = new Date(); + + settingsService.setLicenseCode(command.getLicense()); + settingsService.setLicenseEmail(command.getEmailAddress()); + settingsService.setLicenseDate(now); + settingsService.save(); + settingsService.validateLicenseAsync(); + + // Reflect changes in view. The validator has already validated the license. + command.setLicenseValid(true); + command.setLicenseDate(now); + + return new ModelAndView(getSuccessView(), errors.getModel()); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java new file mode 100644 index 00000000..0125d3bb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java @@ -0,0 +1,453 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.io.RangeOutputStream; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.math.LongRange; +import org.apache.tools.zip.ZipEntry; +import org.apache.tools.zip.ZipOutputStream; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.zip.CRC32; + +/** + * A controller used for downloading files to a remote client. If the requested path refers to a file, the + * given file is downloaded. If the requested path refers to a directory, the entire directory (including + * sub-directories) are downloaded as an uncompressed zip-file. + * + * @author Sindre Mehus + */ +public class DownloadController implements Controller, LastModified { + + private static final Logger LOG = Logger.getLogger(DownloadController.class); + + private PlayerService playerService; + private StatusService statusService; + private SecurityService securityService; + private PlaylistService playlistService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + + public long getLastModified(HttpServletRequest request) { + try { + MediaFile mediaFile = getSingleFile(request); + if (mediaFile == null || mediaFile.isDirectory() || mediaFile.getChanged() == null) { + return -1; + } + return mediaFile.getChanged().getTime(); + } catch (ServletRequestBindingException e) { + return -1; + } + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + TransferStatus status = null; + try { + + status = statusService.createDownloadStatus(playerService.getPlayer(request, response, false, false)); + + MediaFile mediaFile = getSingleFile(request); + String dir = request.getParameter("dir"); + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + String playerId = request.getParameter("player"); + int[] indexes = ServletRequestUtils.getIntParameters(request, "i"); + + if (mediaFile != null) { + response.setIntHeader("ETag", mediaFile.getId()); + response.setHeader("Accept-Ranges", "bytes"); + } + + LongRange range = StringUtil.parseRange(request.getHeader("Range")); + if (range != null) { + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + LOG.info("Got range: " + range); + } + + if (mediaFile != null) { + File file = mediaFile.getFile(); + if (!securityService.isReadAllowed(file)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + } + + if (file.isFile()) { + downloadFile(response, status, file, range); + } else { + downloadDirectory(response, status, file, range); + } + } else if (dir != null) { + File file = new File(dir); + if (!securityService.isReadAllowed(file)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + } + downloadFiles(response, status, file, indexes); + + } else if (playlistId != null) { + List<MediaFile> songs = playlistService.getFilesInPlaylist(playlistId); + downloadFiles(response, status, songs, null, range); + + } else if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + PlayQueue playQueue = player.getPlayQueue(); + playQueue.setName("Playlist"); + downloadFiles(response, status, playQueue.getFiles(), indexes.length == 0 ? null : indexes, range); + } + + + } finally { + if (status != null) { + statusService.removeDownloadStatus(status); + User user = securityService.getCurrentUser(request); + securityService.updateUserByteCounts(user, 0L, status.getBytesTransfered(), 0L); + } + } + + return null; + } + + private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException { + String path = request.getParameter("path"); + if (path != null) { + return mediaFileService.getMediaFile(path); + } + Integer id = ServletRequestUtils.getIntParameter(request, "id"); + if (id != null) { + return mediaFileService.getMediaFile(id); + } + return null; + } + + /** + * Downloads a single file. + * + * @param response The HTTP response. + * @param status The download status. + * @param file The file to download. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void downloadFile(HttpServletResponse response, TransferStatus status, File file, LongRange range) throws IOException { + LOG.info("Starting to download '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer()); + status.setFile(file); + + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + '\"'); + if (range == null) { + Util.setContentLength(response, file.length()); + } + + copyFileToStream(file, RangeOutputStream.wrap(response.getOutputStream(), range), status, range); + LOG.info("Downloaded '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer()); + } + + /** + * Downloads a collection of files within a directory. + * + * @param response The HTTP response. + * @param status The download status. + * @param dir The directory. + * @param indexes Only download files with these indexes within the directory. + * @throws IOException If an I/O error occurs. + */ + private void downloadFiles(HttpServletResponse response, TransferStatus status, File dir, int[] indexes) throws IOException { + String zipFileName = dir.getName() + ".zip"; + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + status.setFile(dir); + + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + "\""); + + ZipOutputStream out = new ZipOutputStream(response.getOutputStream()); + out.setMethod(ZipOutputStream.STORED); // No compression. + + List<MediaFile> allChildren = mediaFileService.getChildrenOf(dir, true, true, true); + List<MediaFile> mediaFiles = new ArrayList<MediaFile>(); + for (int index : indexes) { + mediaFiles.add(allChildren.get(index)); + } + + for (MediaFile mediaFile : mediaFiles) { + zip(out, mediaFile.getParentFile(), mediaFile.getFile(), status, null); + } + + out.close(); + LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer()); + } + + /** + * Downloads all files in a directory (including sub-directories). The files are packed together in an + * uncompressed zip-file. + * + * @param response The HTTP response. + * @param status The download status. + * @param file The file to download. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void downloadDirectory(HttpServletResponse response, TransferStatus status, File file, LongRange range) throws IOException { + String zipFileName = file.getName() + ".zip"; + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + '"'); + + ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range)); + out.setMethod(ZipOutputStream.STORED); // No compression. + + zip(out, file.getParentFile(), file, status, range); + out.close(); + LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer()); + } + + /** + * Downloads the given files. The files are packed together in an + * uncompressed zip-file. + * + * @param response The HTTP response. + * @param status The download status. + * @param files The files to download. + * @param indexes Only download songs at these indexes. May be <code>null</code>. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void downloadFiles(HttpServletResponse response, TransferStatus status, List<MediaFile> files, int[] indexes, LongRange range) throws IOException { + if (indexes != null && indexes.length == 1) { + downloadFile(response, status, files.get(indexes[0]).getFile(), range); + return; + } + + String zipFileName = "download.zip"; + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + zipFileName + '"'); + + ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range)); + out.setMethod(ZipOutputStream.STORED); // No compression. + + List<MediaFile> filesToDownload = new ArrayList<MediaFile>(); + if (indexes == null) { + filesToDownload.addAll(files); + } else { + for (int index : indexes) { + try { + filesToDownload.add(files.get(index)); + } catch (IndexOutOfBoundsException x) { /* Ignored */} + } + } + + for (MediaFile mediaFile : filesToDownload) { + zip(out, mediaFile.getParentFile(), mediaFile.getFile(), status, range); + } + + out.close(); + LOG.info("Downloaded '" + zipFileName + "' to " + status.getPlayer()); + } + + /** + * Utility method for writing the content of a given file to a given output stream. + * + * @param file The file to copy. + * @param out The output stream to write to. + * @param status The download status. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void copyFileToStream(File file, OutputStream out, TransferStatus status, LongRange range) throws IOException { + LOG.info("Downloading '" + FileUtil.getShortPath(file) + "' to " + status.getPlayer()); + + final int bufferSize = 16 * 1024; // 16 Kbit + InputStream in = new BufferedInputStream(new FileInputStream(file), bufferSize); + + try { + byte[] buf = new byte[bufferSize]; + long bitrateLimit = 0; + long lastLimitCheck = 0; + + while (true) { + long before = System.currentTimeMillis(); + int n = in.read(buf); + if (n == -1) { + break; + } + out.write(buf, 0, n); + + // Don't sleep if outside range. + if (range != null && !range.containsLong(status.getBytesSkipped() + status.getBytesTransfered())) { + status.addBytesSkipped(n); + continue; + } + + status.addBytesTransfered(n); + long after = System.currentTimeMillis(); + + // Calculate bitrate limit every 5 seconds. + if (after - lastLimitCheck > 5000) { + bitrateLimit = 1024L * settingsService.getDownloadBitrateLimit() / + Math.max(1, statusService.getAllDownloadStatuses().size()); + lastLimitCheck = after; + } + + // Sleep for a while to throttle bitrate. + if (bitrateLimit != 0) { + long sleepTime = 8L * 1000 * bufferSize / bitrateLimit - (after - before); + if (sleepTime > 0L) { + try { + Thread.sleep(sleepTime); + } catch (Exception x) { + LOG.warn("Failed to sleep.", x); + } + } + } + } + } finally { + out.flush(); + IOUtils.closeQuietly(in); + } + } + + /** + * Writes a file or a directory structure to a zip output stream. File entries in the zip file are relative + * to the given root. + * + * @param out The zip output stream. + * @param root The root of the directory structure. Used to create path information in the zip file. + * @param file The file or directory to zip. + * @param status The download status. + * @param range The byte range, may be <code>null</code>. + * @throws IOException If an I/O error occurs. + */ + private void zip(ZipOutputStream out, File root, File file, TransferStatus status, LongRange range) throws IOException { + + // Exclude all hidden files starting with a "." + if (file.getName().startsWith(".")) { + return; + } + + String zipName = file.getCanonicalPath().substring(root.getCanonicalPath().length() + 1); + + if (file.isFile()) { + status.setFile(file); + + ZipEntry zipEntry = new ZipEntry(zipName); + zipEntry.setSize(file.length()); + zipEntry.setCompressedSize(file.length()); + zipEntry.setCrc(computeCrc(file)); + + out.putNextEntry(zipEntry); + copyFileToStream(file, out, status, range); + out.closeEntry(); + + } else { + ZipEntry zipEntry = new ZipEntry(zipName + '/'); + zipEntry.setSize(0); + zipEntry.setCompressedSize(0); + zipEntry.setCrc(0); + + out.putNextEntry(zipEntry); + out.closeEntry(); + + File[] children = FileUtil.listFiles(file); + for (File child : children) { + zip(out, root, child, status, range); + } + } + } + + /** + * Computes the CRC checksum for the given file. + * + * @param file The file to compute checksum for. + * @return A CRC32 checksum. + * @throws IOException If an I/O error occurs. + */ + private long computeCrc(File file) throws IOException { + CRC32 crc = new CRC32(); + InputStream in = new FileInputStream(file); + + try { + + byte[] buf = new byte[8192]; + int n = in.read(buf); + while (n != -1) { + crc.update(buf, 0, n); + n = in.read(buf); + } + + } finally { + in.close(); + } + + return crc.getValue(); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java new file mode 100644 index 00000000..91492222 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java @@ -0,0 +1,194 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.service.metadata.MetaData; +import net.sourceforge.subsonic.service.metadata.MetaDataParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; + +import org.apache.commons.io.FilenameUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Controller for the page used to edit MP3 tags. + * + * @author Sindre Mehus + */ +public class EditTagsController extends ParameterizableViewController { + + private MetaDataParserFactory metaDataParserFactory; + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile dir = mediaFileService.getMediaFile(id); + List<MediaFile> files = mediaFileService.getChildrenOf(dir, true, false, true); + + Map<String, Object> map = new HashMap<String, Object>(); + if (!files.isEmpty()) { + map.put("defaultArtist", files.get(0).getArtist()); + map.put("defaultAlbum", files.get(0).getAlbumName()); + map.put("defaultYear", files.get(0).getYear()); + map.put("defaultGenre", files.get(0).getGenre()); + } + map.put("allGenres", JaudiotaggerParser.getID3V1Genres()); + + List<Song> songs = new ArrayList<Song>(); + for (int i = 0; i < files.size(); i++) { + songs.add(createSong(files.get(i), i)); + } + map.put("id", id); + map.put("songs", songs); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private Song createSong(MediaFile file, int index) { + MetaDataParser parser = metaDataParserFactory.getParser(file.getFile()); + MetaData metaData = parser.getRawMetaData(file.getFile()); + + Song song = new Song(); + song.setId(file.getId()); + song.setFileName(FilenameUtils.getBaseName(file.getPath())); + song.setTrack(metaData.getTrackNumber()); + song.setSuggestedTrack(index + 1); + song.setTitle(metaData.getTitle()); + song.setSuggestedTitle(parser.guessTitle(file.getFile())); + song.setArtist(metaData.getArtist()); + song.setAlbum(metaData.getAlbumName()); + song.setYear(metaData.getYear()); + song.setGenre(metaData.getGenre()); + return song; + } + + public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { + this.metaDataParserFactory = metaDataParserFactory; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + /** + * Contains information about a single song. + */ + public static class Song { + private int id; + private String fileName; + private Integer suggestedTrack; + private Integer track; + private String suggestedTitle; + private String title; + private String artist; + private String album; + private Integer year; + private String genre; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Integer getSuggestedTrack() { + return suggestedTrack; + } + + public void setSuggestedTrack(Integer suggestedTrack) { + this.suggestedTrack = suggestedTrack; + } + + public Integer getTrack() { + return track; + } + + public void setTrack(Integer track) { + this.track = track; + } + + public String getSuggestedTitle() { + return suggestedTitle; + } + + public void setSuggestedTitle(String suggestedTitle) { + this.suggestedTitle = suggestedTitle; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getAlbum() { + return album; + } + + public void setAlbum(String album) { + this.album = album; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java new file mode 100644 index 00000000..d8d28f93 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java @@ -0,0 +1,179 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.ShareDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.RandomStringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the page used to play shared music (Twitter, Facebook etc). + * + * @author Sindre Mehus + */ +public class ExternalPlayerController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(ExternalPlayerController.class); + private static final String GUEST_USERNAME = "guest"; + + private SettingsService settingsService; + private SecurityService securityService; + private PlayerService playerService; + private ShareDao shareDao; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + String pathInfo = request.getPathInfo(); + + if (pathInfo == null || !pathInfo.startsWith("/")) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + Share share = shareDao.getShareByName(pathInfo.substring(1)); + if (share == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + if (share.getExpires() != null && share.getExpires().before(new Date())) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + share.setLastVisited(new Date()); + share.setVisitCount(share.getVisitCount() + 1); + shareDao.updateShare(share); + + List<MediaFile> songs = getSongs(share); + List<File> coverArts = getCoverArts(songs); + + map.put("share", share); + map.put("songs", songs); + map.put("coverArts", coverArts); + + if (!coverArts.isEmpty()) { + map.put("coverArt", coverArts.get(0)); + } + map.put("redirectFrom", settingsService.getUrlRedirectFrom()); + map.put("player", getPlayer(request).getId()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List<MediaFile> getSongs(Share share) throws IOException { + List<MediaFile> result = new ArrayList<MediaFile>(); + + for (String path : shareDao.getSharedFiles(share.getId())) { + try { + MediaFile file = mediaFileService.getMediaFile(path); + if (file.getFile().exists()) { + if (file.isDirectory()) { + result.addAll(mediaFileService.getChildrenOf(file, true, false, true)); + } else { + result.add(file); + } + } + } catch (Exception x) { + LOG.warn("Couldn't read file " + path); + } + } + return result; + } + + private List<File> getCoverArts(List<MediaFile> songs) throws IOException { + List<File> result = new ArrayList<File>(); + for (MediaFile song : songs) { + result.add(mediaFileService.getCoverArt(song)); + } + return result; + } + + + private Player getPlayer(HttpServletRequest request) { + + // Create guest user if necessary. + User user = securityService.getUserByName(GUEST_USERNAME); + if (user == null) { + user = new User(GUEST_USERNAME, RandomStringUtils.randomAlphanumeric(30), null); + user.setStreamRole(true); + securityService.createUser(user); + } + + // Look for existing player. + List<Player> players = playerService.getPlayersForUserAndClientId(GUEST_USERNAME, null); + if (!players.isEmpty()) { + return players.get(0); + } + + // Create player if necessary. + Player player = new Player(); + player.setIpAddress(request.getRemoteAddr()); + player.setUsername(GUEST_USERNAME); + playerService.createPlayer(player); + + return player; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setShareDao(ShareDao shareDao) { + this.shareDao = shareDao; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java new file mode 100644 index 00000000..e7b19b04 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java @@ -0,0 +1,114 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.GeneralSettingsCommand; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import javax.servlet.http.HttpServletRequest; +import java.util.Locale; + +/** + * Controller for the page used to administrate general settings. + * + * @author Sindre Mehus + */ +public class GeneralSettingsController extends SimpleFormController { + + private SettingsService settingsService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + GeneralSettingsCommand command = new GeneralSettingsCommand(); + command.setCoverArtFileTypes(settingsService.getCoverArtFileTypes()); + command.setIgnoredArticles(settingsService.getIgnoredArticles()); + command.setShortcuts(settingsService.getShortcuts()); + command.setIndex(settingsService.getIndexString()); + command.setMusicFileTypes(settingsService.getMusicFileTypes()); + command.setVideoFileTypes(settingsService.getVideoFileTypes()); + command.setSortAlbumsByYear(settingsService.isSortAlbumsByYear()); + command.setGettingStartedEnabled(settingsService.isGettingStartedEnabled()); + command.setWelcomeTitle(settingsService.getWelcomeTitle()); + command.setWelcomeSubtitle(settingsService.getWelcomeSubtitle()); + command.setWelcomeMessage(settingsService.getWelcomeMessage()); + command.setLoginMessage(settingsService.getLoginMessage()); + + Theme[] themes = settingsService.getAvailableThemes(); + command.setThemes(themes); + String currentThemeId = settingsService.getThemeId(); + for (int i = 0; i < themes.length; i++) { + if (currentThemeId.equals(themes[i].getId())) { + command.setThemeIndex(String.valueOf(i)); + break; + } + } + + Locale currentLocale = settingsService.getLocale(); + Locale[] locales = settingsService.getAvailableLocales(); + String[] localeStrings = new String[locales.length]; + for (int i = 0; i < locales.length; i++) { + localeStrings[i] = locales[i].getDisplayName(locales[i]); + + if (currentLocale.equals(locales[i])) { + command.setLocaleIndex(String.valueOf(i)); + } + } + command.setLocales(localeStrings); + + return command; + + } + + protected void doSubmitAction(Object comm) throws Exception { + GeneralSettingsCommand command = (GeneralSettingsCommand) comm; + + int themeIndex = Integer.parseInt(command.getThemeIndex()); + Theme theme = settingsService.getAvailableThemes()[themeIndex]; + + int localeIndex = Integer.parseInt(command.getLocaleIndex()); + Locale locale = settingsService.getAvailableLocales()[localeIndex]; + + command.setReloadNeeded(!settingsService.getIndexString().equals(command.getIndex()) || + !settingsService.getIgnoredArticles().equals(command.getIgnoredArticles()) || + !settingsService.getShortcuts().equals(command.getShortcuts()) || + !settingsService.getThemeId().equals(theme.getId()) || + !settingsService.getLocale().equals(locale)); + + settingsService.setIndexString(command.getIndex()); + settingsService.setIgnoredArticles(command.getIgnoredArticles()); + settingsService.setShortcuts(command.getShortcuts()); + settingsService.setMusicFileTypes(command.getMusicFileTypes()); + settingsService.setVideoFileTypes(command.getVideoFileTypes()); + settingsService.setCoverArtFileTypes(command.getCoverArtFileTypes()); + settingsService.setSortAlbumsByYear(command.isSortAlbumsByYear()); + settingsService.setGettingStartedEnabled(command.isGettingStartedEnabled()); + settingsService.setWelcomeTitle(command.getWelcomeTitle()); + settingsService.setWelcomeSubtitle(command.getWelcomeSubtitle()); + settingsService.setWelcomeMessage(command.getWelcomeMessage()); + settingsService.setLoginMessage(command.getLoginMessage()); + settingsService.setThemeId(theme.getId()); + settingsService.setLocale(locale); + settingsService.save(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java new file mode 100644 index 00000000..4e0b0945 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.java @@ -0,0 +1,80 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.service.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Controller for the help page. + * + * @author Sindre Mehus + */ +public class HelpController extends ParameterizableViewController { + + private VersionService versionService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + if (versionService.isNewFinalVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestFinalVersion()); + } else if (versionService.isNewBetaVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestBetaVersion()); + } + + long totalMemory = Runtime.getRuntime().totalMemory(); + long freeMemory = Runtime.getRuntime().freeMemory(); + + String serverInfo = request.getSession().getServletContext().getServerInfo() + + ", java " + System.getProperty("java.version") + + ", " + System.getProperty("os.name"); + + map.put("brand", settingsService.getBrand()); + map.put("localVersion", versionService.getLocalVersion()); + map.put("buildDate", versionService.getLocalBuildDate()); + map.put("buildNumber", versionService.getLocalBuildNumber()); + map.put("serverInfo", serverInfo); + map.put("usedMemory", totalMemory - freeMemory); + map.put("totalMemory", totalMemory); + map.put("logEntries", Logger.getLatestLogEntries()); + map.put("logFile", Logger.getLogFile()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java new file mode 100644 index 00000000..49c95926 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java @@ -0,0 +1,340 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the home page. + * + * @author Sindre Mehus + */ +public class HomeController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(HomeController.class); + + private static final int DEFAULT_LIST_SIZE = 10; + private static final int MAX_LIST_SIZE = 500; + private static final int DEFAULT_LIST_OFFSET = 0; + private static final int MAX_LIST_OFFSET = 5000; + + private SettingsService settingsService; + private MediaScannerService mediaScannerService; + private RatingService ratingService; + private SecurityService securityService; + private MediaFileService mediaFileService; + private SearchService searchService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + if (user.isAdminRole() && settingsService.isGettingStartedEnabled()) { + return new ModelAndView(new RedirectView("gettingStarted.view")); + } + + int listSize = DEFAULT_LIST_SIZE; + int listOffset = DEFAULT_LIST_OFFSET; + if (request.getParameter("listSize") != null) { + listSize = Math.max(0, Math.min(Integer.parseInt(request.getParameter("listSize")), MAX_LIST_SIZE)); + } + if (request.getParameter("listOffset") != null) { + listOffset = Math.max(0, Math.min(Integer.parseInt(request.getParameter("listOffset")), MAX_LIST_OFFSET)); + } + + String listType = request.getParameter("listType"); + if (listType == null) { + listType = "random"; + } + + List<Album> albums; + if ("highest".equals(listType)) { + albums = getHighestRated(listOffset, listSize); + } else if ("frequent".equals(listType)) { + albums = getMostFrequent(listOffset, listSize); + } else if ("recent".equals(listType)) { + albums = getMostRecent(listOffset, listSize); + } else if ("newest".equals(listType)) { + albums = getNewest(listOffset, listSize); + } else if ("starred".equals(listType)) { + albums = getStarred(listOffset, listSize, user.getUsername()); + } else if ("random".equals(listType)) { + albums = getRandom(listSize); + } else if ("alphabetical".equals(listType)) { + albums = getAlphabetical(listOffset, listSize, true); + } else { + albums = Collections.emptyList(); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("albums", albums); + map.put("welcomeTitle", settingsService.getWelcomeTitle()); + map.put("welcomeSubtitle", settingsService.getWelcomeSubtitle()); + map.put("welcomeMessage", settingsService.getWelcomeMessage()); + map.put("isIndexBeingCreated", mediaScannerService.isScanning()); + map.put("listType", listType); + map.put("listSize", listSize); + map.put("listOffset", listOffset); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + List<Album> getHighestRated(int offset, int count) { + List<Album> result = new ArrayList<Album>(); + for (MediaFile mediaFile : ratingService.getHighestRated(offset, count)) { + Album album = createAlbum(mediaFile); + if (album != null) { + album.setRating((int) Math.round(ratingService.getAverageRating(mediaFile) * 10.0D)); + result.add(album); + } + } + return result; + } + + List<Album> getMostFrequent(int offset, int count) { + List<Album> result = new ArrayList<Album>(); + for (MediaFile mediaFile : mediaFileService.getMostFrequentlyPlayedAlbums(offset, count)) { + Album album = createAlbum(mediaFile); + if (album != null) { + album.setPlayCount(mediaFile.getPlayCount()); + result.add(album); + } + } + return result; + } + + List<Album> getMostRecent(int offset, int count) { + List<Album> result = new ArrayList<Album>(); + for (MediaFile mediaFile : mediaFileService.getMostRecentlyPlayedAlbums(offset, count)) { + Album album = createAlbum(mediaFile); + if (album != null) { + album.setLastPlayed(mediaFile.getLastPlayed()); + result.add(album); + } + } + return result; + } + + List<Album> getNewest(int offset, int count) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : mediaFileService.getNewestAlbums(offset, count)) { + Album album = createAlbum(file); + if (album != null) { + Date created = file.getCreated(); + if (created == null) { + created = file.getChanged(); + } + album.setCreated(created); + result.add(album); + } + } + return result; + } + + List<Album> getStarred(int offset, int count, String username) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : mediaFileService.getStarredAlbums(offset, count, username)) { + Album album = createAlbum(file); + if (album != null) { + result.add(album); + } + } + return result; + } + + List<Album> getRandom(int count) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : searchService.getRandomAlbums(count)) { + Album album = createAlbum(file); + if (album != null) { + result.add(album); + } + } + return result; + } + + List<Album> getAlphabetical(int offset, int count, boolean byArtist) throws IOException { + List<Album> result = new ArrayList<Album>(); + for (MediaFile file : mediaFileService.getAlphabetialAlbums(offset, count, byArtist)) { + Album album = createAlbum(file); + if (album != null) { + result.add(album); + } + } + return result; + } + + private Album createAlbum(MediaFile file) { + Album album = new Album(); + album.setId(file.getId()); + album.setPath(file.getPath()); + try { + resolveArtistAndAlbumTitle(album, file); + resolveCoverArt(album, file); + } catch (Exception x) { + LOG.warn("Failed to create albumTitle list entry for " + file.getPath(), x); + return null; + } + return album; + } + + private void resolveArtistAndAlbumTitle(Album album, MediaFile file) throws IOException { + album.setArtist(file.getArtist()); + album.setAlbumTitle(file.getAlbumName()); + } + + private void resolveCoverArt(Album album, MediaFile file) { + album.setCoverArtPath(file.getCoverArtPath()); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + /** + * Contains info for a single album. + */ + @Deprecated + public static class Album { + private String path; + private String coverArtPath; + private String artist; + private String albumTitle; + private Date created; + private Date lastPlayed; + private Integer playCount; + private Integer rating; + private int id; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getCoverArtPath() { + return coverArtPath; + } + + public void setCoverArtPath(String coverArtPath) { + this.coverArtPath = coverArtPath; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getAlbumTitle() { + return albumTitle; + } + + public void setAlbumTitle(String albumTitle) { + this.albumTitle = albumTitle; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastPlayed() { + return lastPlayed; + } + + public void setLastPlayed(Date lastPlayed) { + this.lastPlayed = lastPlayed; + } + + public Integer getPlayCount() { + return playCount; + } + + public void setPlayCount(Integer playCount) { + this.playCount = playCount; + } + + public Integer getRating() { + return rating; + } + + public void setRating(Integer rating) { + this.rating = rating; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java new file mode 100644 index 00000000..55e9b200 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ImportPlaylistController.java @@ -0,0 +1,93 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * @author Sindre Mehus + */ +public class ImportPlaylistController extends ParameterizableViewController { + + private static final long MAX_PLAYLIST_SIZE_MB = 5L; + + private SecurityService securityService; + private PlaylistService playlistService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + try { + if (ServletFileUpload.isMultipartContent(request)) { + + FileItemFactory factory = new DiskFileItemFactory(); + ServletFileUpload upload = new ServletFileUpload(factory); + List<?> items = upload.parseRequest(request); + for (Object o : items) { + FileItem item = (FileItem) o; + + if ("file".equals(item.getFieldName()) && !StringUtils.isBlank(item.getName())) { + if (item.getSize() > MAX_PLAYLIST_SIZE_MB * 1024L * 1024L) { + throw new Exception("The playlist file is too large. Max file size is " + MAX_PLAYLIST_SIZE_MB + " MB."); + } + String playlistName = FilenameUtils.getBaseName(item.getName()); + String fileName = FilenameUtils.getName(item.getName()); + String format = StringUtils.lowerCase(FilenameUtils.getExtension(item.getName())); + String username = securityService.getCurrentUsername(request); + Playlist playlist = playlistService.importPlaylist(username, playlistName, fileName, format, item.getInputStream()); + map.put("playlist", playlist); + } + } + } + } catch (Exception e) { + map.put("error", e.getMessage()); + } + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java new file mode 100644 index 00000000..5ee7b799 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java @@ -0,0 +1,116 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Date; + +/** + * Controller for the page used to administrate the set of internet radio/tv stations. + * + * @author Sindre Mehus + */ +public class InternetRadioSettingsController extends ParameterizableViewController { + + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + if (isFormSubmission(request)) { + String error = handleParameters(request); + map.put("error", error); + if (error == null) { + map.put("reload", true); + } + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("internetRadios", settingsService.getAllInternetRadios(true)); + + result.addObject("model", map); + return result; + } + + /** + * Determine if the given request represents a form submission. + * + * @param request current HTTP request + * @return if the request represents a form submission + */ + private boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + private String handleParameters(HttpServletRequest request) { + List<InternetRadio> radios = settingsService.getAllInternetRadios(true); + for (InternetRadio radio : radios) { + Integer id = radio.getId(); + String streamUrl = getParameter(request, "streamUrl", id); + String homepageUrl = getParameter(request, "homepageUrl", id); + String name = getParameter(request, "name", id); + boolean enabled = getParameter(request, "enabled", id) != null; + boolean delete = getParameter(request, "delete", id) != null; + + if (delete) { + settingsService.deleteInternetRadio(id); + } else { + if (name == null) { + return "internetradiosettings.noname"; + } + if (streamUrl == null) { + return "internetradiosettings.nourl"; + } + settingsService.updateInternetRadio(new InternetRadio(id, name, streamUrl, homepageUrl, enabled, new Date())); + } + } + + String name = StringUtils.trimToNull(request.getParameter("name")); + String streamUrl = StringUtils.trimToNull(request.getParameter("streamUrl")); + String homepageUrl = StringUtils.trimToNull(request.getParameter("homepageUrl")); + boolean enabled = StringUtils.trimToNull(request.getParameter("enabled")) != null; + + if (name != null && streamUrl != null) { + settingsService.createInternetRadio(new InternetRadio(name, streamUrl, homepageUrl, enabled, new Date())); + } + + return null; + } + + private String getParameter(HttpServletRequest request, String name, Integer id) { + return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]")); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java new file mode 100644 index 00000000..d273f0b9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java @@ -0,0 +1,270 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.service.PlaylistService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.LastModified; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.support.RequestContextUtils; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.MusicIndexService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller for the left index frame. + * + * @author Sindre Mehus + */ +public class LeftController extends ParameterizableViewController implements LastModified { + + private static final Logger LOG = Logger.getLogger(LeftController.class); + + // Update this time if you want to force a refresh in clients. + private static final Calendar LAST_COMPATIBILITY_TIME = Calendar.getInstance(); + static { + LAST_COMPATIBILITY_TIME.set(2012, Calendar.MARCH, 6, 0, 0, 0); + LAST_COMPATIBILITY_TIME.set(Calendar.MILLISECOND, 0); + } + + private MediaScannerService mediaScannerService; + private SettingsService settingsService; + private SecurityService securityService; + private MediaFileService mediaFileService; + private MusicIndexService musicIndexService; + private PlayerService playerService; + private PlaylistService playlistService; + + public long getLastModified(HttpServletRequest request) { + saveSelectedMusicFolder(request); + + if (mediaScannerService.isScanning()) { + return -1L; + } + + long lastModified = LAST_COMPATIBILITY_TIME.getTimeInMillis(); + String username = securityService.getCurrentUsername(request); + + // When was settings last changed? + lastModified = Math.max(lastModified, settingsService.getSettingsChanged()); + + // When was music folder(s) on disk last changed? + List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders(); + MusicFolder selectedMusicFolder = getSelectedMusicFolder(request); + if (selectedMusicFolder != null) { + File file = selectedMusicFolder.getPath(); + lastModified = Math.max(lastModified, FileUtil.lastModified(file)); + } else { + for (MusicFolder musicFolder : allMusicFolders) { + File file = musicFolder.getPath(); + lastModified = Math.max(lastModified, FileUtil.lastModified(file)); + } + } + + // When was music folder table last changed? + for (MusicFolder musicFolder : allMusicFolders) { + lastModified = Math.max(lastModified, musicFolder.getChanged().getTime()); + } + + // When was internet radio table last changed? + for (InternetRadio internetRadio : settingsService.getAllInternetRadios()) { + lastModified = Math.max(lastModified, internetRadio.getChanged().getTime()); + } + + // When was user settings last changed? + UserSettings userSettings = settingsService.getUserSettings(username); + lastModified = Math.max(lastModified, userSettings.getChanged().getTime()); + + return lastModified; + } + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + saveSelectedMusicFolder(request); + Map<String, Object> map = new HashMap<String, Object>(); + + MediaLibraryStatistics statistics = mediaScannerService.getStatistics(); + Locale locale = RequestContextUtils.getLocale(request); + + String username = securityService.getCurrentUsername(request); + List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders(); + MusicFolder selectedMusicFolder = getSelectedMusicFolder(request); + List<MusicFolder> musicFoldersToUse = selectedMusicFolder == null ? allMusicFolders : Arrays.asList(selectedMusicFolder); + String[] shortcuts = settingsService.getShortcutsAsArray(); + UserSettings userSettings = settingsService.getUserSettings(username); + + MusicFolderContent musicFolderContent = getMusicFolderContent(musicFoldersToUse); + + map.put("player", playerService.getPlayer(request, response)); + map.put("scanning", mediaScannerService.isScanning()); + map.put("musicFolders", allMusicFolders); + map.put("selectedMusicFolder", selectedMusicFolder); + map.put("radios", settingsService.getAllInternetRadios()); + map.put("shortcuts", getShortcuts(musicFoldersToUse, shortcuts)); + map.put("captionCutoff", userSettings.getMainVisibility().getCaptionCutoff()); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("organizeByFolderStructure", settingsService.isOrganizeByFolderStructure()); + + if (statistics != null) { + map.put("statistics", statistics); + long bytes = statistics.getTotalLengthInBytes(); + long hours = statistics.getTotalDurationInSeconds() / 3600L; + map.put("hours", hours); + map.put("bytes", StringUtil.formatBytes(bytes, locale)); + } + + map.put("indexedArtists", musicFolderContent.getIndexedArtists()); + map.put("singleSongs", musicFolderContent.getSingleSongs()); + map.put("indexes", musicFolderContent.getIndexedArtists().keySet()); + map.put("user", securityService.getCurrentUser(request)); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private void saveSelectedMusicFolder(HttpServletRequest request) { + if (request.getParameter("musicFolderId") == null) { + return; + } + int musicFolderId = Integer.parseInt(request.getParameter("musicFolderId")); + + // Note: UserSettings.setChanged() is intentionally not called. This would break browser caching + // of the left frame. + UserSettings settings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + settings.setSelectedMusicFolderId(musicFolderId); + settingsService.updateUserSettings(settings); + } + + /** + * Returns the selected music folder, or <code>null</code> if all music folders should be displayed. + */ + private MusicFolder getSelectedMusicFolder(HttpServletRequest request) { + UserSettings settings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + int musicFolderId = settings.getSelectedMusicFolderId(); + + return settingsService.getMusicFolderById(musicFolderId); + } + + protected List<MediaFile> getSingleSongs(List<MusicFolder> folders) throws IOException { + List<MediaFile> result = new ArrayList<MediaFile>(); + for (MusicFolder folder : folders) { + MediaFile parent = mediaFileService.getMediaFile(folder.getPath(), true); + result.addAll(mediaFileService.getChildrenOf(parent, true, false, true, true)); + } + return result; + } + + public List<MediaFile> getShortcuts(List<MusicFolder> musicFoldersToUse, String[] shortcuts) { + List<MediaFile> result = new ArrayList<MediaFile>(); + + for (String shortcut : shortcuts) { + for (MusicFolder musicFolder : musicFoldersToUse) { + File file = new File(musicFolder.getPath(), shortcut); + if (FileUtil.exists(file)) { + result.add(mediaFileService.getMediaFile(file, true)); + } + } + } + + return result; + } + + public MusicFolderContent getMusicFolderContent(List<MusicFolder> musicFoldersToUse) throws Exception { + SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists = musicIndexService.getIndexedArtists(musicFoldersToUse); + List<MediaFile> singleSongs = getSingleSongs(musicFoldersToUse); + return new MusicFolderContent(indexedArtists, singleSongs); + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public static class MusicFolderContent { + + private final SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists; + private final List<MediaFile> singleSongs; + + public MusicFolderContent(SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists, List<MediaFile> singleSongs) { + this.indexedArtists = indexedArtists; + this.singleSongs = singleSongs; + } + + public SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> getIndexedArtists() { + return indexedArtists; + } + + public List<MediaFile> getSingleSongs() { + return singleSongs; + } + + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java new file mode 100644 index 00000000..d47ad233 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LyricsController.java @@ -0,0 +1,46 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.ModelAndView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Map; +import java.util.HashMap; + +/** + * Controller for the lyrics popup. + * + * @author Sindre Mehus + */ +public class LyricsController extends ParameterizableViewController { + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + map.put("artist", request.getParameter("artist")); + map.put("song", request.getParameter("song")); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java new file mode 100644 index 00000000..bbd7a478 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java @@ -0,0 +1,128 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +/** + * Controller which produces the M3U playlist. + * + * @author Sindre Mehus + */ +public class M3UController implements Controller { + + private PlayerService playerService; + private SettingsService settingsService; + private TranscodingService transcodingService; + + private static final Logger LOG = Logger.getLogger(M3UController.class); + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + response.setContentType("audio/x-mpegurl"); + response.setCharacterEncoding(StringUtil.ENCODING_UTF8); + + Player player = playerService.getPlayer(request, response); + + String url = request.getRequestURL().toString(); + url = url.replaceFirst("play.m3u.*", "stream?"); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + url = StringUtil.rewriteUrl(url, referer); + } + + // Change protocol and port, if specified. (To make it work with players that don't support SSL.) + int streamPort = settingsService.getStreamPort(); + if (streamPort != 0) { + url = StringUtil.toHttpUrl(url, streamPort); + LOG.info("Using non-SSL port " + streamPort + " in m3u playlist."); + } + + if (player.isExternalWithPlaylist()) { + createClientSidePlaylist(response.getWriter(), player, url); + } else { + createServerSidePlaylist(response.getWriter(), player, url); + } + return null; + } + + private void createClientSidePlaylist(PrintWriter out, Player player, String url) throws Exception { + out.println("#EXTM3U"); + List<MediaFile> result; + synchronized (player.getPlayQueue()) { + result = player.getPlayQueue().getFiles(); + } + for (MediaFile mediaFile : result) { + Integer duration = mediaFile.getDurationSeconds(); + if (duration == null) { + duration = -1; + } + out.println("#EXTINF:" + duration + "," + mediaFile.getArtist() + " - " + mediaFile.getTitle()); + out.println(url + "player=" + player.getId() + "&id=" +mediaFile.getId() + "&suffix=." + transcodingService.getSuffix(player, mediaFile, null)); + } + } + + private void createServerSidePlaylist(PrintWriter out, Player player, String url) throws IOException { + + url += "player=" + player.getId(); + + // Get suffix of current file, e.g., ".mp3". + String suffix = getSuffix(player); + if (suffix != null) { + url += "&suffix=." + suffix; + } + + out.println("#EXTM3U"); + out.println("#EXTINF:-1,Subsonic"); + out.println(url); + } + + private String getSuffix(Player player) { + PlayQueue playQueue = player.getPlayQueue(); + return playQueue.isEmpty() ? null : transcodingService.getSuffix(player, playQueue.getFile(0), null); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java new file mode 100644 index 00000000..1d9e0a61 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java @@ -0,0 +1,297 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.AdService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Controller for the main page. + * + * @author Sindre Mehus + */ +public class MainController extends ParameterizableViewController { + + private SecurityService securityService; + private PlayerService playerService; + private SettingsService settingsService; + private RatingService ratingService; + private MediaFileService mediaFileService; + private AdService adService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + Player player = playerService.getPlayer(request, response); + List<MediaFile> mediaFiles = getMediaFiles(request); + + if (mediaFiles.isEmpty()) { + return new ModelAndView(new RedirectView("notFound.view")); + } + + MediaFile dir = mediaFiles.get(0); + if (dir.isFile()) { + dir = mediaFileService.getParentOf(dir); + } + + // Redirect if root directory. + if (mediaFileService.isRoot(dir)) { + return new ModelAndView(new RedirectView("home.view?")); + } + + List<MediaFile> children = mediaFiles.size() == 1 ? mediaFileService.getChildrenOf(dir, true, true, true) : getMultiFolderChildren(mediaFiles); + String username = securityService.getCurrentUsername(request); + UserSettings userSettings = settingsService.getUserSettings(username); + + mediaFileService.populateStarredDate(dir, username); + mediaFileService.populateStarredDate(children, username); + + map.put("dir", dir); + map.put("ancestors", getAncestors(dir)); + map.put("children", children); + map.put("artist", guessArtist(children)); + map.put("album", guessAlbum(children)); + map.put("player", player); + map.put("user", securityService.getCurrentUser(request)); + map.put("multipleArtists", isMultipleArtists(children)); + map.put("visibility", userSettings.getMainVisibility()); + map.put("showAlbumYear", settingsService.isSortAlbumsByYear()); + map.put("updateNowPlaying", request.getParameter("updateNowPlaying") != null); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("brand", settingsService.getBrand()); + if (!settingsService.isLicenseValid()) { + map.put("ad", adService.getAd()); + } + + try { + MediaFile parent = mediaFileService.getParentOf(dir); + map.put("parent", parent); + map.put("navigateUpAllowed", !mediaFileService.isRoot(parent)); + } catch (SecurityException x) { + // Happens if Podcast directory is outside music folder. + } + + Integer userRating = ratingService.getRatingForUser(username, dir); + Double averageRating = ratingService.getAverageRating(dir); + + if (userRating == null) { + userRating = 0; + } + + if (averageRating == null) { + averageRating = 0.0D; + } + + map.put("userRating", 10 * userRating); + map.put("averageRating", Math.round(10.0D * averageRating)); + map.put("starred", mediaFileService.getMediaFileStarredDate(dir.getId(), username) != null); + + CoverArtScheme scheme = player.getCoverArtScheme(); + if (scheme != CoverArtScheme.OFF) { + List<MediaFile> coverArts = getCoverArts(dir, children); + int size = coverArts.size() > 1 ? scheme.getSize() : scheme.getSize() * 2; + map.put("coverArts", coverArts); + map.put("coverArtSize", size); + if (coverArts.isEmpty() && dir.isAlbum()) { + map.put("showGenericCoverArt", true); + } + } + + setPreviousAndNextAlbums(dir, map); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List<MediaFile> getMediaFiles(HttpServletRequest request) { + List<MediaFile> mediaFiles = new ArrayList<MediaFile>(); + for (String path : ServletRequestUtils.getStringParameters(request, "path")) { + MediaFile mediaFile = mediaFileService.getMediaFile(path); + if (mediaFile != null) { + mediaFiles.add(mediaFile); + } + } + for (int id : ServletRequestUtils.getIntParameters(request, "id")) { + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile != null) { + mediaFiles.add(mediaFile); + } + } + return mediaFiles; + } + + private String guessArtist(List<MediaFile> children) { + for (MediaFile child : children) { + if (child.isFile() && child.getArtist() != null) { + return child.getArtist(); + } + } + return null; + } + + private String guessAlbum(List<MediaFile> children) { + for (MediaFile child : children) { + if (child.isFile() && child.getArtist() != null) { + return child.getAlbumName(); + } + } + return null; + } + + private List<MediaFile> getCoverArts(MediaFile dir, List<MediaFile> children) throws IOException { + int limit = settingsService.getCoverArtLimit(); + if (limit == 0) { + limit = Integer.MAX_VALUE; + } + + List<MediaFile> coverArts = new ArrayList<MediaFile>(); + if (dir.isAlbum() && dir.getCoverArtPath() != null) { + coverArts.add(dir); + } else { + for (MediaFile child : children) { + if (child.isAlbum()) { + if (child.getCoverArtPath() != null) { + coverArts.add(child); + } + if (coverArts.size() > limit) { + break; + } + } + } + } + return coverArts; + } + + private List<MediaFile> getMultiFolderChildren(List<MediaFile> mediaFiles) throws IOException { + List<MediaFile> result = new ArrayList<MediaFile>(); + for (MediaFile mediaFile : mediaFiles) { + if (mediaFile.isFile()) { + mediaFile = mediaFileService.getParentOf(mediaFile); + } + result.addAll(mediaFileService.getChildrenOf(mediaFile, true, true, true)); + } + return result; + } + + private List<MediaFile> getAncestors(MediaFile dir) throws IOException { + LinkedList<MediaFile> result = new LinkedList<MediaFile>(); + + try { + MediaFile parent = mediaFileService.getParentOf(dir); + while (parent != null && !mediaFileService.isRoot(parent)) { + result.addFirst(parent); + parent = mediaFileService.getParentOf(parent); + } + } catch (SecurityException x) { + // Happens if Podcast directory is outside music folder. + } + return result; + } + + private void setPreviousAndNextAlbums(MediaFile dir, Map<String, Object> map) throws IOException { + MediaFile parent = mediaFileService.getParentOf(dir); + + if (dir.isAlbum() && !mediaFileService.isRoot(parent)) { + List<MediaFile> sieblings = mediaFileService.getChildrenOf(parent, false, true, true); + + int index = sieblings.indexOf(dir); + if (index > 0) { + map.put("previousAlbum", sieblings.get(index - 1)); + } + if (index < sieblings.size() - 1) { + map.put("nextAlbum", sieblings.get(index + 1)); + } + } + } + + private boolean isMultipleArtists(List<MediaFile> children) { + // Collect unique artist names. + Set<String> artists = new HashSet<String>(); + for (MediaFile child : children) { + if (child.getArtist() != null) { + artists.add(child.getArtist().toLowerCase()); + } + } + + // If zero or one artist, it is definitely not multiple artists. + if (artists.size() < 2) { + return false; + } + + // Fuzzily compare artist names, allowing for some differences in spelling, whitespace etc. + List<String> artistList = new ArrayList<String>(artists); + for (String artist : artistList) { + if (StringUtils.getLevenshteinDistance(artist, artistList.get(0)) > 3) { + return true; + } + } + return false; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setAdService(AdService adService) { + this.adService = adService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java new file mode 100644 index 00000000..f29cb346 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.java @@ -0,0 +1,89 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Calendar; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the "more" page. + * + * @author Sindre Mehus + */ +public class MoreController extends ParameterizableViewController { + + private SettingsService settingsService; + private SecurityService securityService; + private PlayerService playerService; + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + String uploadDirectory = null; + List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(); + if (musicFolders.size() > 0) { + uploadDirectory = new File(musicFolders.get(0).getPath(), "Incoming").getPath(); + } + + Player player = playerService.getPlayer(request, response); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + map.put("user", securityService.getCurrentUser(request)); + map.put("uploadDirectory", uploadDirectory); + map.put("genres", mediaFileService.getGenres()); + map.put("currentYear", Calendar.getInstance().get(Calendar.YEAR)); + map.put("musicFolders", settingsService.getAllMusicFolders()); + map.put("clientSidePlaylist", player.isExternalWithPlaylist() || player.isWeb()); + map.put("brand", settingsService.getBrand()); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java new file mode 100644 index 00000000..1d781565 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java @@ -0,0 +1,244 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpConnectionParams; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Multi-controller used for simple pages. + * + * @author Sindre Mehus + */ +public class MultiController extends MultiActionController { + + private static final Logger LOG = Logger.getLogger(MultiController.class); + + private SecurityService securityService; + private SettingsService settingsService; + private PlaylistService playlistService; + + public ModelAndView login(HttpServletRequest request, HttpServletResponse response) throws Exception { + + // Auto-login if "user" and "password" parameters are given. + String username = request.getParameter("user"); + String password = request.getParameter("password"); + if (username != null && password != null) { + username = StringUtil.urlEncode(username); + password = StringUtil.urlEncode(password); + return new ModelAndView(new RedirectView("j_acegi_security_check?j_username=" + username + + "&j_password=" + password + "&_acegi_security_remember_me=checked")); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("logout", request.getParameter("logout") != null); + map.put("error", request.getParameter("error") != null); + map.put("brand", settingsService.getBrand()); + map.put("loginMessage", settingsService.getLoginMessage()); + + User admin = securityService.getUserByName(User.USERNAME_ADMIN); + if (User.USERNAME_ADMIN.equals(admin.getPassword())) { + map.put("insecure", true); + } + + return new ModelAndView("login", "model", map); + } + + public ModelAndView recover(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + String usernameOrEmail = StringUtils.trimToNull(request.getParameter("usernameOrEmail")); + + if (usernameOrEmail != null) { + User user = getUserByUsernameOrEmail(usernameOrEmail); + if (user == null) { + map.put("error", "recover.error.usernotfound"); + } else if (user.getEmail() == null) { + map.put("error", "recover.error.noemail"); + } else { + String password = RandomStringUtils.randomAlphanumeric(8); + if (emailPassword(password, user.getUsername(), user.getEmail())) { + map.put("sentTo", user.getEmail()); + user.setLdapAuthenticated(false); + user.setPassword(password); + securityService.updateUser(user); + } else { + map.put("error", "recover.error.sendfailed"); + } + } + } + + return new ModelAndView("recover", "model", map); + } + + private boolean emailPassword(String password, String username, String email) { + HttpClient client = new DefaultHttpClient(); + try { + HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000); + HttpConnectionParams.setSoTimeout(client.getParams(), 10000); + HttpPost method = new HttpPost("http://subsonic.org/backend/sendMail.view"); + + List<NameValuePair> params = new ArrayList<NameValuePair>(); + params.add(new BasicNameValuePair("from", "noreply@subsonic.org")); + params.add(new BasicNameValuePair("to", email)); + params.add(new BasicNameValuePair("subject", "Subsonic Password")); + params.add(new BasicNameValuePair("text", + "Hi there!\n\n" + + "You have requested to reset your Subsonic password. Please find your new login details below.\n\n" + + "Username: " + username + "\n" + + "Password: " + password + "\n\n" + + "--\n" + + "The Subsonic Team\n" + + "subsonic.org")); + method.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8)); + client.execute(method); + return true; + } catch (Exception x) { + LOG.warn("Failed to send email.", x); + return false; + } finally { + client.getConnectionManager().shutdown(); + } + } + + private User getUserByUsernameOrEmail(String usernameOrEmail) { + if (usernameOrEmail != null) { + User user = securityService.getUserByName(usernameOrEmail); + if (user != null) { + return user; + } + return securityService.getUserByEmail(usernameOrEmail); + } + return null; + } + + public ModelAndView accessDenied(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView("accessDenied"); + } + + public ModelAndView notFound(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView("notFound"); + } + + public ModelAndView gettingStarted(HttpServletRequest request, HttpServletResponse response) { + updatePortAndContextPath(request); + + if (request.getParameter("hide") != null) { + settingsService.setGettingStartedEnabled(false); + settingsService.save(); + return new ModelAndView(new RedirectView("home.view")); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("runningAsRoot", "root".equals(System.getProperty("user.name"))); + return new ModelAndView("gettingStarted", "model", map); + } + + public ModelAndView index(HttpServletRequest request, HttpServletResponse response) { + updatePortAndContextPath(request); + UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("showRight", userSettings.isShowNowPlayingEnabled() || userSettings.isShowChatEnabled()); + map.put("brand", settingsService.getBrand()); + return new ModelAndView("index", "model", map); + } + + public ModelAndView exportPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + Playlist playlist = playlistService.getPlaylist(id); + if (!playlistService.isReadAllowed(playlist, securityService.getCurrentUsername(request))) { + response.sendError(HttpServletResponse.SC_FORBIDDEN); + return null; + + } + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + StringUtil.fileSystemSafe(playlist.getName()) + ".m3u8\""); + + playlistService.exportPlaylist(id, response.getOutputStream()); + return null; + } + + private void updatePortAndContextPath(HttpServletRequest request) { + + int port = Integer.parseInt(System.getProperty("subsonic.port", String.valueOf(request.getLocalPort()))); + int httpsPort = Integer.parseInt(System.getProperty("subsonic.httpsPort", "0")); + + String contextPath = request.getContextPath().replace("/", ""); + + if (settingsService.getPort() != port) { + settingsService.setPort(port); + settingsService.save(); + } + if (settingsService.getHttpsPort() != httpsPort) { + settingsService.setHttpsPort(httpsPort); + settingsService.save(); + } + if (!ObjectUtils.equals(settingsService.getUrlRedirectContextPath(), contextPath)) { + settingsService.setUrlRedirectContextPath(contextPath); + settingsService.save(); + } + } + + public ModelAndView test(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView("test"); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java new file mode 100644 index 00000000..8c002342 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.java @@ -0,0 +1,130 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.MusicFolderSettingsCommand; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.List; + +/** + * Controller for the page used to administrate the set of music folders. + * + * @author Sindre Mehus + */ +public class MusicFolderSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private MediaScannerService mediaScannerService; + private ArtistDao artistDao; + private AlbumDao albumDao; + private MediaFileDao mediaFolderDao; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + MusicFolderSettingsCommand command = new MusicFolderSettingsCommand(); + + if (request.getParameter("scanNow") != null) { + mediaScannerService.scanLibrary(); + } + if (request.getParameter("expunge") != null) { + expunge(); + } + + command.setInterval(String.valueOf(settingsService.getIndexCreationInterval())); + command.setHour(String.valueOf(settingsService.getIndexCreationHour())); + command.setFastCache(settingsService.isFastCacheEnabled()); + command.setOrganizeByFolderStructure(settingsService.isOrganizeByFolderStructure()); + command.setScanning(mediaScannerService.isScanning()); + command.setMusicFolders(wrap(settingsService.getAllMusicFolders(true, true))); + command.setNewMusicFolder(new MusicFolderSettingsCommand.MusicFolderInfo()); + command.setReload(request.getParameter("reload") != null || request.getParameter("scanNow") != null); + return command; + } + + private void expunge() { + artistDao.expunge(); + albumDao.expunge(); + mediaFolderDao.expunge(); + } + + private List<MusicFolderSettingsCommand.MusicFolderInfo> wrap(List<MusicFolder> musicFolders) { + ArrayList<MusicFolderSettingsCommand.MusicFolderInfo> result = new ArrayList<MusicFolderSettingsCommand.MusicFolderInfo>(); + for (MusicFolder musicFolder : musicFolders) { + result.add(new MusicFolderSettingsCommand.MusicFolderInfo(musicFolder)); + } + return result; + } + + @Override + protected ModelAndView onSubmit(Object comm) throws Exception { + MusicFolderSettingsCommand command = (MusicFolderSettingsCommand) comm; + + for (MusicFolderSettingsCommand.MusicFolderInfo musicFolderInfo : command.getMusicFolders()) { + if (musicFolderInfo.isDelete()) { + settingsService.deleteMusicFolder(musicFolderInfo.getId()); + } else { + settingsService.updateMusicFolder(musicFolderInfo.toMusicFolder()); + } + } + + MusicFolder newMusicFolder = command.getNewMusicFolder().toMusicFolder(); + if (newMusicFolder != null) { + settingsService.createMusicFolder(newMusicFolder); + } + + settingsService.setIndexCreationInterval(Integer.parseInt(command.getInterval())); + settingsService.setIndexCreationHour(Integer.parseInt(command.getHour())); + settingsService.setFastCacheEnabled(command.isFastCache()); + settingsService.setOrganizeByFolderStructure(command.isOrganizeByFolderStructure()); + settingsService.save(); + + mediaScannerService.schedule(); + return new ModelAndView(new RedirectView(getSuccessView() + ".view?reload")); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaScannerService(MediaScannerService mediaScannerService) { + this.mediaScannerService = mediaScannerService; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public void setMediaFolderDao(MediaFileDao mediaFolderDao) { + this.mediaFolderDao = mediaFolderDao; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java new file mode 100644 index 00000000..3807eb71 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java @@ -0,0 +1,89 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.Date; +import java.util.Random; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.NetworkSettingsCommand; +import net.sourceforge.subsonic.service.NetworkService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the page used to change the network settings. + * + * @author Sindre Mehus + */ +public class NetworkSettingsController extends SimpleFormController { + + private static final long TRIAL_DAYS = 30L; + + private SettingsService settingsService; + private NetworkService networkService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + NetworkSettingsCommand command = new NetworkSettingsCommand(); + command.setPortForwardingEnabled(settingsService.isPortForwardingEnabled()); + command.setUrlRedirectionEnabled(settingsService.isUrlRedirectionEnabled()); + command.setUrlRedirectFrom(settingsService.getUrlRedirectFrom()); + command.setPort(settingsService.getPort()); + + Date trialExpires = settingsService.getUrlRedirectTrialExpires(); + command.setTrialExpires(trialExpires); + command.setTrialExpired(trialExpires != null && trialExpires.before(new Date())); + command.setTrial(trialExpires != null && !settingsService.isLicenseValid()); + + return command; + } + + protected void doSubmitAction(Object cmd) throws Exception { + NetworkSettingsCommand command = (NetworkSettingsCommand) cmd; + + settingsService.setPortForwardingEnabled(command.isPortForwardingEnabled()); + settingsService.setUrlRedirectionEnabled(command.isUrlRedirectionEnabled()); + settingsService.setUrlRedirectFrom(StringUtils.lowerCase(command.getUrlRedirectFrom())); + + if (!settingsService.isLicenseValid() && settingsService.getUrlRedirectTrialExpires() == null) { + Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L); + settingsService.setUrlRedirectTrialExpires(expiryDate); + } + + if (settingsService.getServerId() == null) { + Random rand = new Random(System.currentTimeMillis()); + settingsService.setServerId(String.valueOf(Math.abs(rand.nextLong()))); + } + + settingsService.save(); + networkService.initPortForwarding(); + networkService.initUrlRedirection(true); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setNetworkService(NetworkService networkService) { + this.networkService = networkService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java new file mode 100644 index 00000000..79fe7c77 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java @@ -0,0 +1,79 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +/** + * Controller for showing what's currently playing. + * + * @author Sindre Mehus + */ +public class NowPlayingController extends AbstractController { + + private PlayerService playerService; + private StatusService statusService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Player player = playerService.getPlayer(request, response); + List<TransferStatus> statuses = statusService.getStreamStatusesForPlayer(player); + + MediaFile current = statuses.isEmpty() ? null : mediaFileService.getMediaFile(statuses.get(0).getFile()); + MediaFile dir = current == null ? null : mediaFileService.getParentOf(current); + + String url; + if (dir != null && !mediaFileService.isRoot(dir)) { + url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + + StringUtil.utf8HexEncode(dir.getPath()) + "&updateNowPlaying=true"; + } else { + url = "home.view"; + } + + return new ModelAndView(new RedirectView(url)); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java new file mode 100644 index 00000000..8dd8d875 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.java @@ -0,0 +1,58 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.command.*; +import net.sourceforge.subsonic.domain.*; + +import javax.servlet.http.*; + +/** + * Controller for the page used to change password. + * + * @author Sindre Mehus + */ +public class PasswordSettingsController extends SimpleFormController { + + private SecurityService securityService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PasswordSettingsCommand command = new PasswordSettingsCommand(); + User user = securityService.getCurrentUser(request); + command.setUsername(user.getUsername()); + command.setLdapAuthenticated(user.isLdapAuthenticated()); + return command; + } + + protected void doSubmitAction(Object comm) throws Exception { + PasswordSettingsCommand command = (PasswordSettingsCommand) comm; + User user = securityService.getUserByName(command.getUsername()); + user.setPassword(command.getPassword()); + securityService.updateUser(user); + + command.setPassword(null); + command.setConfirmPassword(null); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java new file mode 100644 index 00000000..3bc3f7a5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java @@ -0,0 +1,164 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.*; +import org.apache.commons.lang.StringUtils; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.command.*; +import net.sourceforge.subsonic.domain.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Controller for the page used to administrate per-user settings. + * + * @author Sindre Mehus + */ +public class PersonalSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PersonalSettingsCommand command = new PersonalSettingsCommand(); + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + + command.setUser(user); + command.setLocaleIndex("-1"); + command.setThemeIndex("-1"); + command.setAvatars(settingsService.getAllSystemAvatars()); + command.setCustomAvatar(settingsService.getCustomAvatar(user.getUsername())); + command.setAvatarId(getAvatarId(userSettings)); + command.setPartyModeEnabled(userSettings.isPartyModeEnabled()); + command.setShowNowPlayingEnabled(userSettings.isShowNowPlayingEnabled()); + command.setShowChatEnabled(userSettings.isShowChatEnabled()); + command.setNowPlayingAllowed(userSettings.isNowPlayingAllowed()); + command.setMainVisibility(userSettings.getMainVisibility()); + command.setPlaylistVisibility(userSettings.getPlaylistVisibility()); + command.setFinalVersionNotificationEnabled(userSettings.isFinalVersionNotificationEnabled()); + command.setBetaVersionNotificationEnabled(userSettings.isBetaVersionNotificationEnabled()); + command.setLastFmEnabled(userSettings.isLastFmEnabled()); + command.setLastFmUsername(userSettings.getLastFmUsername()); + command.setLastFmPassword(userSettings.getLastFmPassword()); + + Locale currentLocale = userSettings.getLocale(); + Locale[] locales = settingsService.getAvailableLocales(); + String[] localeStrings = new String[locales.length]; + for (int i = 0; i < locales.length; i++) { + localeStrings[i] = locales[i].getDisplayName(locales[i]); + if (locales[i].equals(currentLocale)) { + command.setLocaleIndex(String.valueOf(i)); + } + } + command.setLocales(localeStrings); + + String currentThemeId = userSettings.getThemeId(); + Theme[] themes = settingsService.getAvailableThemes(); + command.setThemes(themes); + for (int i = 0; i < themes.length; i++) { + if (themes[i].getId().equals(currentThemeId)) { + command.setThemeIndex(String.valueOf(i)); + break; + } + } + + return command; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + PersonalSettingsCommand command = (PersonalSettingsCommand) comm; + + int localeIndex = Integer.parseInt(command.getLocaleIndex()); + Locale locale = null; + if (localeIndex != -1) { + locale = settingsService.getAvailableLocales()[localeIndex]; + } + + int themeIndex = Integer.parseInt(command.getThemeIndex()); + String themeId = null; + if (themeIndex != -1) { + themeId = settingsService.getAvailableThemes()[themeIndex].getId(); + } + + String username = command.getUser().getUsername(); + UserSettings settings = settingsService.getUserSettings(username); + + settings.setLocale(locale); + settings.setThemeId(themeId); + settings.setPartyModeEnabled(command.isPartyModeEnabled()); + settings.setShowNowPlayingEnabled(command.isShowNowPlayingEnabled()); + settings.setShowChatEnabled(command.isShowChatEnabled()); + settings.setNowPlayingAllowed(command.isNowPlayingAllowed()); + settings.setMainVisibility(command.getMainVisibility()); + settings.setPlaylistVisibility(command.getPlaylistVisibility()); + settings.setFinalVersionNotificationEnabled(command.isFinalVersionNotificationEnabled()); + settings.setBetaVersionNotificationEnabled(command.isBetaVersionNotificationEnabled()); + settings.setLastFmEnabled(command.isLastFmEnabled()); + settings.setLastFmUsername(command.getLastFmUsername()); + settings.setSystemAvatarId(getSystemAvatarId(command)); + settings.setAvatarScheme(getAvatarScheme(command)); + + if (StringUtils.isNotBlank(command.getLastFmPassword())) { + settings.setLastFmPassword(command.getLastFmPassword()); + } + + settings.setChanged(new Date()); + settingsService.updateUserSettings(settings); + + command.setReloadNeeded(true); + } + + private int getAvatarId(UserSettings userSettings) { + AvatarScheme avatarScheme = userSettings.getAvatarScheme(); + return avatarScheme == AvatarScheme.SYSTEM ? userSettings.getSystemAvatarId() : avatarScheme.getCode(); + } + + private AvatarScheme getAvatarScheme(PersonalSettingsCommand command) { + if (command.getAvatarId() == AvatarScheme.NONE.getCode()) { + return AvatarScheme.NONE; + } + if (command.getAvatarId() == AvatarScheme.CUSTOM.getCode()) { + return AvatarScheme.CUSTOM; + } + return AvatarScheme.SYSTEM; + } + + private Integer getSystemAvatarId(PersonalSettingsCommand command) { + int avatarId = command.getAvatarId(); + if (avatarId == AvatarScheme.NONE.getCode() || + avatarId == AvatarScheme.CUSTOM.getCode()) { + return null; + } + return avatarId; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java new file mode 100644 index 00000000..0074dda1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.java @@ -0,0 +1,77 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the playlist frame. + * + * @author Sindre Mehus + */ +public class PlayQueueController extends ParameterizableViewController { + + private PlayerService playerService; + private SecurityService securityService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + Player player = playerService.getPlayer(request, response); + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("user", user); + map.put("player", player); + map.put("players", playerService.getPlayersForUserAndClientId(user.getUsername(), null)); + map.put("visibility", userSettings.getPlaylistVisibility()); + map.put("partyMode", userSettings.isPartyModeEnabled()); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java new file mode 100644 index 00000000..813d94a5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.java @@ -0,0 +1,150 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.PlayerSettingsCommand; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.TranscodingService; + +/** + * Controller for the player settings page. + * + * @author Sindre Mehus + */ +public class PlayerSettingsController extends SimpleFormController { + + private PlayerService playerService; + private SecurityService securityService; + private TranscodingService transcodingService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + + handleRequestParameters(request); + List<Player> players = getPlayers(request); + + User user = securityService.getCurrentUser(request); + PlayerSettingsCommand command = new PlayerSettingsCommand(); + Player player = null; + String playerId = request.getParameter("id"); + if (playerId != null) { + player = playerService.getPlayerById(playerId); + } else if (!players.isEmpty()) { + player = players.get(0); + } + + if (player != null) { + command.setPlayerId(player.getId()); + command.setName(player.getName()); + command.setDescription(player.toString()); + command.setType(player.getType()); + command.setLastSeen(player.getLastSeen()); + command.setDynamicIp(player.isDynamicIp()); + command.setAutoControlEnabled(player.isAutoControlEnabled()); + command.setCoverArtSchemeName(player.getCoverArtScheme().name()); + command.setTranscodeSchemeName(player.getTranscodeScheme().name()); + command.setTechnologyName(player.getTechnology().name()); + command.setAllTranscodings(transcodingService.getAllTranscodings()); + List<Transcoding> activeTranscodings = transcodingService.getTranscodingsForPlayer(player); + int[] activeTranscodingIds = new int[activeTranscodings.size()]; + for (int i = 0; i < activeTranscodings.size(); i++) { + activeTranscodingIds[i] = activeTranscodings.get(i).getId(); + } + command.setActiveTranscodingIds(activeTranscodingIds); + } + + command.setTranscodingSupported(transcodingService.isDownsamplingSupported(null)); + command.setTranscodeDirectory(transcodingService.getTranscodeDirectory().getPath()); + command.setCoverArtSchemes(CoverArtScheme.values()); + command.setTranscodeSchemes(TranscodeScheme.values()); + command.setTechnologies(PlayerTechnology.values()); + command.setPlayers(players.toArray(new Player[players.size()])); + command.setAdmin(user.isAdminRole()); + + return command; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + PlayerSettingsCommand command = (PlayerSettingsCommand) comm; + Player player = playerService.getPlayerById(command.getPlayerId()); + + player.setAutoControlEnabled(command.isAutoControlEnabled()); + player.setCoverArtScheme(CoverArtScheme.valueOf(command.getCoverArtSchemeName())); + player.setDynamicIp(command.isDynamicIp()); + player.setName(StringUtils.trimToNull(command.getName())); + player.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName())); + player.setTechnology(PlayerTechnology.valueOf(command.getTechnologyName())); + + playerService.updatePlayer(player); + transcodingService.setTranscodingsForPlayer(player, command.getActiveTranscodingIds()); + + command.setReloadNeeded(true); + } + + private List<Player> getPlayers(HttpServletRequest request) { + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + List<Player> players = playerService.getAllPlayers(); + List<Player> authorizedPlayers = new ArrayList<Player>(); + + for (Player player : players) { + // Only display authorized players. + if (user.isAdminRole() || username.equals(player.getUsername())) { + authorizedPlayers.add(player); + } + } + return authorizedPlayers; + } + + private void handleRequestParameters(HttpServletRequest request) { + if (request.getParameter("delete") != null) { + playerService.removePlayerById(request.getParameter("delete")); + } else if (request.getParameter("clone") != null) { + playerService.clonePlayer(request.getParameter("clone")); + } + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java new file mode 100644 index 00000000..6b24a3c5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.java @@ -0,0 +1,82 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * Controller for the main page. + * + * @author Sindre Mehus + */ +public class PlaylistController extends ParameterizableViewController { + + private SecurityService securityService; + private PlaylistService playlistService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + return new ModelAndView(new RedirectView("notFound.view")); + } + + map.put("playlist", playlist); + map.put("user", user); + map.put("editAllowed", username.equals(playlist.getUsername()) || securityService.isAdmin(username)); + map.put("partyMode", userSettings.isPartyModeEnabled()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java new file mode 100644 index 00000000..dbc6854b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java @@ -0,0 +1,152 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Controller for the page used to generate the Podcast XML file. + * + * @author Sindre Mehus + */ +public class PodcastController extends ParameterizableViewController { + + private static final DateFormat RSS_DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); + private PlaylistService playlistService; + private SettingsService settingsService; + private SecurityService securityService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + String url = request.getRequestURL().toString(); + String username = securityService.getCurrentUsername(request); + List<Playlist> playlists = playlistService.getReadablePlaylistsForUser(username); + List<Podcast> podcasts = new ArrayList<Podcast>(); + + for (Playlist playlist : playlists) { + + List<MediaFile> songs = playlistService.getFilesInPlaylist(playlist.getId()); + if (songs.isEmpty()) { + continue; + } + long length = 0L; + for (MediaFile song : songs) { + length += song.getFileSize(); + } + String publishDate = RSS_DATE_FORMAT.format(playlist.getCreated()); + + // Resolve content type. + String suffix = songs.get(0).getFormat(); + String type = StringUtil.getMimeType(suffix); + + String enclosureUrl = url.replaceFirst("/podcast.*", "/stream?playlist=" + playlist.getId() + "&suffix=." + suffix); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + url = StringUtil.rewriteUrl(url, referer); + } + + // Change protocol and port, if specified. (To make it work with players that don't support SSL.) + int streamPort = settingsService.getStreamPort(); + if (streamPort != 0) { + enclosureUrl = StringUtil.toHttpUrl(enclosureUrl, streamPort); + } + + podcasts.add(new Podcast(playlist.getName(), publishDate, enclosureUrl, length, type)); + } + + Map<String, Object> map = new HashMap<String, Object>(); + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("url", url); + map.put("podcasts", podcasts); + + result.addObject("model", map); + return result; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + /** + * Contains information about a single Podcast. + */ + public static class Podcast { + private String name; + private String publishDate; + private String enclosureUrl; + private long length; + private String type; + + public Podcast(String name, String publishDate, String enclosureUrl, long length, String type) { + this.name = name; + this.publishDate = publishDate; + this.enclosureUrl = enclosureUrl; + this.length = length; + this.type = type; + } + + public String getName() { + return name; + } + + public String getPublishDate() { + return publishDate; + } + + public String getEnclosureUrl() { + return enclosureUrl; + } + + public long getLength() { + return length; + } + + public String getType() { + return type; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java new file mode 100644 index 00000000..c955e884 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.java @@ -0,0 +1,102 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.PodcastStatus; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.List; + +/** + * Controller for the "Podcast receiver" page. + * + * @author Sindre Mehus + */ +public class PodcastReceiverAdminController extends AbstractController { + + private PodcastService podcastService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + handleParameters(request); + return new ModelAndView(new RedirectView("podcastReceiver.view?expandedChannels=" + request.getParameter("expandedChannels"))); + } + + private void handleParameters(HttpServletRequest request) { + if (request.getParameter("add") != null) { + String url = request.getParameter("add"); + podcastService.createChannel(url); + } + if (request.getParameter("downloadChannel") != null || + request.getParameter("downloadEpisode") != null) { + download(StringUtil.parseInts(request.getParameter("downloadChannel")), + StringUtil.parseInts(request.getParameter("downloadEpisode"))); + } + if (request.getParameter("deleteChannel") != null) { + for (int channelId : StringUtil.parseInts(request.getParameter("deleteChannel"))) { + podcastService.deleteChannel(channelId); + } + } + if (request.getParameter("deleteEpisode") != null) { + for (int episodeId : StringUtil.parseInts(request.getParameter("deleteEpisode"))) { + podcastService.deleteEpisode(episodeId, true); + } + } + if (request.getParameter("refresh") != null) { + podcastService.refreshAllChannels(true); + } + } + + private void download(int[] channelIds, int[] episodeIds) { + SortedSet<Integer> uniqueEpisodeIds = new TreeSet<Integer>(); + for (int episodeId : episodeIds) { + uniqueEpisodeIds.add(episodeId); + } + for (int channelId : channelIds) { + List<PodcastEpisode> episodes = podcastService.getEpisodes(channelId, false); + for (PodcastEpisode episode : episodes) { + uniqueEpisodeIds.add(episode.getId()); + } + } + + for (Integer episodeId : uniqueEpisodeIds) { + PodcastEpisode episode = podcastService.getEpisode(episodeId, false); + if (episode != null && episode.getUrl() != null && + (episode.getStatus() == PodcastStatus.NEW || + episode.getStatus() == PodcastStatus.ERROR || + episode.getStatus() == PodcastStatus.SKIPPED)) { + + podcastService.downloadEpisode(episode); + } + } + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java new file mode 100644 index 00000000..93640c22 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverController.java @@ -0,0 +1,85 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller for the "Podcast receiver" page. + * + * @author Sindre Mehus + */ +public class PodcastReceiverController extends ParameterizableViewController { + + private PodcastService podcastService; + private SecurityService securityService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + + Map<PodcastChannel, List<PodcastEpisode>> channels = new LinkedHashMap<PodcastChannel, List<PodcastEpisode>>(); + for (PodcastChannel channel : podcastService.getAllChannels()) { + channels.put(channel, podcastService.getEpisodes(channel.getId(), false)); + } + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + + map.put("user", user); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("channels", channels); + map.put("expandedChannels", StringUtil.parseInts(request.getParameter("expandedChannels"))); + return result; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java new file mode 100644 index 00000000..b6389616 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java @@ -0,0 +1,67 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import org.springframework.web.servlet.mvc.SimpleFormController; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.command.PodcastSettingsCommand; + +import javax.servlet.http.HttpServletRequest; + +/** + * Controller for the page used to administrate the Podcast receiver. + * + * @author Sindre Mehus + */ +public class PodcastSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private PodcastService podcastService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PodcastSettingsCommand command = new PodcastSettingsCommand(); + + command.setInterval(String.valueOf(settingsService.getPodcastUpdateInterval())); + command.setEpisodeRetentionCount(String.valueOf(settingsService.getPodcastEpisodeRetentionCount())); + command.setEpisodeDownloadCount(String.valueOf(settingsService.getPodcastEpisodeDownloadCount())); + command.setFolder(settingsService.getPodcastFolder()); + return command; + } + + protected void doSubmitAction(Object comm) throws Exception { + PodcastSettingsCommand command = (PodcastSettingsCommand) comm; + + settingsService.setPodcastUpdateInterval(Integer.parseInt(command.getInterval())); + settingsService.setPodcastEpisodeRetentionCount(Integer.parseInt(command.getEpisodeRetentionCount())); + settingsService.setPodcastEpisodeDownloadCount(Integer.parseInt(command.getEpisodeDownloadCount())); + settingsService.setPodcastFolder(command.getFolder()); + settingsService.save(); + + podcastService.schedule(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java new file mode 100644 index 00000000..9535e059 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ProxyController.java @@ -0,0 +1,68 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.InputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.HttpConnectionParams; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +/** + * A proxy for external HTTP requests. + * + * @author Sindre Mehus + */ +public class ProxyController implements Controller { + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String url = ServletRequestUtils.getRequiredStringParameter(request, "url"); + + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000); + HttpConnectionParams.setSoTimeout(client.getParams(), 15000); + HttpGet method = new HttpGet(url); + + InputStream in = null; + try { + HttpResponse resp = client.execute(method); + int statusCode = resp.getStatusLine().getStatusCode(); + if (statusCode != HttpStatus.SC_OK) { + response.sendError(statusCode); + } else { + in = resp.getEntity().getContent(); + IOUtils.copy(in, response.getOutputStream()); + } + } finally { + IOUtils.closeQuietly(in); + client.getConnectionManager().shutdown(); + } + return null; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java new file mode 100644 index 00000000..2d4fa73c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java @@ -0,0 +1,1983 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.ajax.PlayQueueService; +import net.sourceforge.subsonic.domain.Playlist; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.ajax.ChatService; +import net.sourceforge.subsonic.ajax.LyricsInfo; +import net.sourceforge.subsonic.ajax.LyricsService; +import net.sourceforge.subsonic.command.UserSettingsCommand; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.JukeboxService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.ShareService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.XMLBuilder; + +import static net.sourceforge.subsonic.security.RESTRequestParameterProcessingFilter.decrypt; +import static net.sourceforge.subsonic.util.XMLBuilder.Attribute; +import static net.sourceforge.subsonic.util.XMLBuilder.AttributeSet; + +/** + * Multi-controller used for the REST API. + * <p/> + * For documentation, please refer to api.jsp. + * + * @author Sindre Mehus + */ +public class RESTController extends MultiActionController { + + private static final Logger LOG = Logger.getLogger(RESTController.class); + + private SettingsService settingsService; + private SecurityService securityService; + private PlayerService playerService; + private MediaFileService mediaFileService; + private TranscodingService transcodingService; + private DownloadController downloadController; + private CoverArtController coverArtController; + private AvatarController avatarController; + private UserSettingsController userSettingsController; + private LeftController leftController; + private HomeController homeController; + private StatusService statusService; + private StreamController streamController; + private ShareService shareService; + private PlaylistService playlistService; + private ChatService chatService; + private LyricsService lyricsService; + private PlayQueueService playQueueService; + private JukeboxService jukeboxService; + private AudioScrobblerService audioScrobblerService; + private PodcastService podcastService; + private RatingService ratingService; + private SearchService searchService; + private MediaFileDao mediaFileDao; + private ArtistDao artistDao; + private AlbumDao albumDao; + + public void ping(HttpServletRequest request, HttpServletResponse response) throws Exception { + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } + + public void getLicense(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + String email = settingsService.getLicenseEmail(); + String key = settingsService.getLicenseCode(); + Date date = settingsService.getLicenseDate(); + boolean valid = settingsService.isLicenseValid(); + + AttributeSet attributes = new AttributeSet(); + attributes.add("valid", valid); + if (valid) { + attributes.add("email", email); + attributes.add("key", key); + attributes.add("date", StringUtil.toISO8601(date)); + } + + builder.add("license", attributes, true); + builder.endAll(); + response.getWriter().print(builder); + } + + public void getMusicFolders(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("musicFolders", false); + + for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { + AttributeSet attributes = new AttributeSet(); + attributes.add("id", musicFolder.getId()); + attributes.add("name", musicFolder.getName()); + builder.add("musicFolder", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getIndexes(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + long ifModifiedSince = ServletRequestUtils.getLongParameter(request, "ifModifiedSince", 0L); + long lastModified = leftController.getLastModified(request); + + if (lastModified <= ifModifiedSince) { + builder.endAll(); + response.getWriter().print(builder); + return; + } + + builder.add("indexes", "lastModified", lastModified, false); + + List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(); + Integer musicFolderId = ServletRequestUtils.getIntParameter(request, "musicFolderId"); + if (musicFolderId != null) { + for (MusicFolder musicFolder : musicFolders) { + if (musicFolderId.equals(musicFolder.getId())) { + musicFolders = Arrays.asList(musicFolder); + break; + } + } + } + + List<MediaFile> shortcuts = leftController.getShortcuts(musicFolders, settingsService.getShortcutsAsArray()); + for (MediaFile shortcut : shortcuts) { + builder.add("shortcut", true, + new Attribute("name", shortcut.getName()), + new Attribute("id", shortcut.getId())); + } + + SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> indexedArtists = leftController.getMusicFolderContent(musicFolders).getIndexedArtists(); + + for (Map.Entry<MusicIndex, SortedSet<MusicIndex.Artist>> entry : indexedArtists.entrySet()) { + builder.add("index", "name", entry.getKey().getIndex(), false); + + for (MusicIndex.Artist artist : entry.getValue()) { + for (MediaFile mediaFile : artist.getMediaFiles()) { + if (mediaFile.isDirectory()) { + builder.add("artist", true, + new Attribute("name", artist.getName()), + new Attribute("id", mediaFile.getId())); + } + } + } + builder.end(); + } + + // Add children + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + List<MediaFile> singleSongs = leftController.getSingleSongs(musicFolders); + + for (MediaFile singleSong : singleSongs) { + builder.add("child", createAttributesForMediaFile(player, singleSong, username), true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getArtists(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + String username = securityService.getCurrentUsername(request); + + builder.add("artists", false); + + List<Artist> artists = artistDao.getAlphabetialArtists(0, Integer.MAX_VALUE); + for (Artist artist : artists) { + AttributeSet attributes = createAttributesForArtist(artist, username); + builder.add("artist", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + private AttributeSet createAttributesForArtist(Artist artist, String username) { + AttributeSet attributes = new AttributeSet(); + attributes.add("id", artist.getId()); + attributes.add("name", artist.getName()); + if (artist.getCoverArtPath() != null) { + attributes.add("coverArt", CoverArtController.ARTIST_COVERART_PREFIX + artist.getId()); + } + attributes.add("albumCount", artist.getAlbumCount()); + attributes.add("starred", StringUtil.toISO8601(artistDao.getArtistStarredDate(artist.getId(), username))); + return attributes; + } + + public void getArtist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + String username = securityService.getCurrentUsername(request); + Artist artist; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + artist = artistDao.getArtist(id); + if (artist == null) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Artist not found."); + return; + } + + builder.add("artist", createAttributesForArtist(artist, username), false); + for (Album album : albumDao.getAlbumsForArtist(artist.getName())) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + private AttributeSet createAttributesForAlbum(Album album, String username) { + AttributeSet attributes; + attributes = new AttributeSet(); + attributes.add("id", album.getId()); + attributes.add("name", album.getName()); + attributes.add("artist", album.getArtist()); + if (album.getArtist() != null) { + Artist artist = artistDao.getArtist(album.getArtist()); + if (artist != null) { + attributes.add("artistId", artist.getId()); + } + } + if (album.getCoverArtPath() != null) { + attributes.add("coverArt", CoverArtController.ALBUM_COVERART_PREFIX + album.getId()); + } + attributes.add("songCount", album.getSongCount()); + attributes.add("duration", album.getDurationSeconds()); + attributes.add("created", StringUtil.toISO8601(album.getCreated())); + attributes.add("starred", StringUtil.toISO8601(albumDao.getAlbumStarredDate(album.getId(), username))); + + return attributes; + } + + private AttributeSet createAttributesForPlaylist(Playlist playlist) { + AttributeSet attributes; + attributes = new AttributeSet(); + attributes.add("id", playlist.getId()); + attributes.add("name", playlist.getName()); + attributes.add("comment", playlist.getComment()); + attributes.add("owner", playlist.getUsername()); + attributes.add("public", playlist.isPublic()); + attributes.add("songCount", playlist.getFileCount()); + attributes.add("duration", playlist.getDurationSeconds()); + attributes.add("created", StringUtil.toISO8601(playlist.getCreated())); + return attributes; + } + + public void getAlbum(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + Album album; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + album = albumDao.getAlbum(id); + if (album == null) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Album not found."); + return; + } + + builder.add("album", createAttributesForAlbum(album, username), false); + for (MediaFile mediaFile : mediaFileDao.getSongsForAlbum(album.getArtist(), album.getName())) { + builder.add("song", createAttributesForMediaFile(player, mediaFile, username) , true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getSong(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + MediaFile song; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + song = mediaFileDao.getMediaFile(id); + if (song == null || song.isDirectory()) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Song not found."); + return; + } + + builder.add("song", createAttributesForMediaFile(player, song, username), true); + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getMusicDirectory(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + MediaFile dir; + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + dir = mediaFileService.getMediaFile(id); + if (dir == null) { + throw new Exception(); + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.NOT_FOUND, "Directory not found"); + return; + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("directory", false, + new Attribute("id", dir.getId()), + new Attribute("name", dir.getName())); + + for (MediaFile child : mediaFileService.getChildrenOf(dir, true, true, true)) { + AttributeSet attributes = createAttributesForMediaFile(player, child, username); + builder.add("child", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + @Deprecated + public void search(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + String any = request.getParameter("any"); + String artist = request.getParameter("artist"); + String album = request.getParameter("album"); + String title = request.getParameter("title"); + + StringBuilder query = new StringBuilder(); + if (any != null) { + query.append(any).append(" "); + } + if (artist != null) { + query.append(artist).append(" "); + } + if (album != null) { + query.append(album).append(" "); + } + if (title != null) { + query.append(title); + } + + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(query.toString().trim()); + criteria.setCount(ServletRequestUtils.getIntParameter(request, "count", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "offset", 0)); + + SearchResult result = searchService.search(criteria, SearchService.IndexType.SONG); + builder.add("searchResult", false, + new Attribute("offset", result.getOffset()), + new Attribute("totalHits", result.getTotalHits())); + + for (MediaFile mediaFile : result.getMediaFiles()) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("match", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void search2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + builder.add("searchResult2", false); + + String query = request.getParameter("query"); + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(StringUtils.trimToEmpty(query)); + criteria.setCount(ServletRequestUtils.getIntParameter(request, "artistCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "artistOffset", 0)); + SearchResult artists = searchService.search(criteria, SearchService.IndexType.ARTIST); + for (MediaFile mediaFile : artists.getMediaFiles()) { + builder.add("artist", true, + new Attribute("name", mediaFile.getName()), + new Attribute("id", mediaFile.getId())); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "albumCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "albumOffset", 0)); + SearchResult albums = searchService.search(criteria, SearchService.IndexType.ALBUM); + for (MediaFile mediaFile : albums.getMediaFiles()) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("album", attributes, true); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "songCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "songOffset", 0)); + SearchResult songs = searchService.search(criteria, SearchService.IndexType.SONG); + for (MediaFile mediaFile : songs.getMediaFiles()) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("song", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void search3(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + builder.add("searchResult3", false); + + String query = request.getParameter("query"); + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(StringUtils.trimToEmpty(query)); + criteria.setCount(ServletRequestUtils.getIntParameter(request, "artistCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "artistOffset", 0)); + SearchResult searchResult = searchService.search(criteria, SearchService.IndexType.ARTIST_ID3); + for (Artist artist : searchResult.getArtists()) { + builder.add("artist", createAttributesForArtist(artist, username), true); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "albumCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "albumOffset", 0)); + searchResult = searchService.search(criteria, SearchService.IndexType.ALBUM_ID3); + for (Album album : searchResult.getAlbums()) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + + criteria.setCount(ServletRequestUtils.getIntParameter(request, "songCount", 20)); + criteria.setOffset(ServletRequestUtils.getIntParameter(request, "songOffset", 0)); + searchResult = searchService.search(criteria, SearchService.IndexType.SONG); + for (MediaFile song : searchResult.getMediaFiles()) { + builder.add("song", createAttributesForMediaFile(player, song, username), true); + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getPlaylists(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + User user = securityService.getCurrentUser(request); + String authenticatedUsername = user.getUsername(); + String requestedUsername = request.getParameter("username"); + + if (requestedUsername == null) { + requestedUsername = authenticatedUsername; + } else if (!user.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, authenticatedUsername + " is not authorized to get playlists for " + requestedUsername); + return; + } + + builder.add("playlists", false); + + for (Playlist playlist : playlistService.getReadablePlaylistsForUser(requestedUsername)) { + List<String> sharedUsers = playlistService.getPlaylistUsers(playlist.getId()); + builder.add("playlist", createAttributesForPlaylist(playlist), sharedUsers.isEmpty()); + if (!sharedUsers.isEmpty()) { + for (String username : sharedUsers) { + builder.add("allowedUser", (Iterable<Attribute>) null, username, true); + } + builder.end(); + } + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); + return; + } + if (!playlistService.isReadAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); + return; + } + builder.add("playlist", createAttributesForPlaylist(playlist), false); + for (String allowedUser : playlistService.getPlaylistUsers(playlist.getId())) { + builder.add("allowedUser", (Iterable<Attribute>) null, allowedUser, true); + } + for (MediaFile mediaFile : playlistService.getFilesInPlaylist(id)) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void jukeboxControl(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + + User user = securityService.getCurrentUser(request); + if (!user.isJukeboxRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to use jukebox."); + return; + } + + try { + boolean returnPlaylist = false; + String action = ServletRequestUtils.getRequiredStringParameter(request, "action"); + if ("start".equals(action)) { + playQueueService.doStart(request, response); + } else if ("stop".equals(action)) { + playQueueService.doStop(request, response); + } else if ("skip".equals(action)) { + int index = ServletRequestUtils.getRequiredIntParameter(request, "index"); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + playQueueService.doSkip(request, response, index, offset); + } else if ("add".equals(action)) { + int[] ids = ServletRequestUtils.getIntParameters(request, "id"); + playQueueService.doAdd(request, response, ids); + } else if ("set".equals(action)) { + int[] ids = ServletRequestUtils.getIntParameters(request, "id"); + playQueueService.doSet(request, response, ids); + } else if ("clear".equals(action)) { + playQueueService.doClear(request, response); + } else if ("remove".equals(action)) { + int index = ServletRequestUtils.getRequiredIntParameter(request, "index"); + playQueueService.doRemove(request, response, index); + } else if ("shuffle".equals(action)) { + playQueueService.doShuffle(request, response); + } else if ("setGain".equals(action)) { + float gain = ServletRequestUtils.getRequiredFloatParameter(request, "gain"); + jukeboxService.setGain(gain); + } else if ("get".equals(action)) { + returnPlaylist = true; + } else if ("status".equals(action)) { + // No action necessary. + } else { + throw new Exception("Unknown jukebox action: '" + action + "'."); + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + Player jukeboxPlayer = jukeboxService.getPlayer(); + boolean controlsJukebox = jukeboxPlayer != null && jukeboxPlayer.getId().equals(player.getId()); + PlayQueue playQueue = player.getPlayQueue(); + + List<Attribute> attrs = new ArrayList<Attribute>(Arrays.asList( + new Attribute("currentIndex", controlsJukebox && !playQueue.isEmpty() ? playQueue.getIndex() : -1), + new Attribute("playing", controlsJukebox && !playQueue.isEmpty() && playQueue.getStatus() == PlayQueue.Status.PLAYING), + new Attribute("gain", jukeboxService.getGain()), + new Attribute("position", controlsJukebox && !playQueue.isEmpty() ? jukeboxService.getPosition() : 0))); + + if (returnPlaylist) { + builder.add("jukeboxPlaylist", attrs, false); + List<MediaFile> result; + synchronized (playQueue) { + result = playQueue.getFiles(); + } + for (MediaFile mediaFile : result) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + } else { + builder.add("jukeboxStatus", attrs, false); + } + + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void createPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + try { + + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlistId"); + String name = request.getParameter("name"); + if (playlistId == null && name == null) { + error(request, response, ErrorCode.MISSING_PARAMETER, "Playlist ID or name must be specified."); + return; + } + + Playlist playlist; + if (playlistId != null) { + playlist = playlistService.getPlaylist(playlistId); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + playlistId); + return; + } + if (!playlistService.isWriteAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + playlistId); + return; + } + } else { + playlist = new Playlist(); + playlist.setName(name); + playlist.setCreated(new Date()); + playlist.setChanged(new Date()); + playlist.setPublic(false); + playlist.setUsername(username); + playlistService.createPlaylist(playlist); + } + + List<MediaFile> songs = new ArrayList<MediaFile>(); + for (int id : ServletRequestUtils.getIntParameters(request, "songId")) { + MediaFile song = mediaFileService.getMediaFile(id); + if (song != null) { + songs.add(song); + } + } + playlistService.setFilesInPlaylist(playlist.getId(), songs); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void updatePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "playlistId"); + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); + return; + } + if (!playlistService.isWriteAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); + return; + } + + String name = request.getParameter("name"); + if (name != null) { + playlist.setName(name); + } + String comment = request.getParameter("comment"); + if (comment != null) { + playlist.setComment(comment); + } + Boolean isPublic = ServletRequestUtils.getBooleanParameter(request, "public"); + if (isPublic != null) { + playlist.setPublic(isPublic); + } + playlistService.updatePlaylist(playlist); + + // TODO: Add later +// for (String usernameToAdd : ServletRequestUtils.getStringParameters(request, "usernameToAdd")) { +// if (securityService.getUserByName(usernameToAdd) != null) { +// playlistService.addPlaylistUser(id, usernameToAdd); +// } +// } +// for (String usernameToRemove : ServletRequestUtils.getStringParameters(request, "usernameToRemove")) { +// if (securityService.getUserByName(usernameToRemove) != null) { +// playlistService.deletePlaylistUser(id, usernameToRemove); +// } +// } + List<MediaFile> songs = playlistService.getFilesInPlaylist(id); + boolean songsChanged = false; + + SortedSet<Integer> tmp = new TreeSet<Integer>(); + for (int songIndexToRemove : ServletRequestUtils.getIntParameters(request, "songIndexToRemove")) { + tmp.add(songIndexToRemove); + } + List<Integer> songIndexesToRemove = new ArrayList<Integer>(tmp); + Collections.reverse(songIndexesToRemove); + for (Integer songIndexToRemove : songIndexesToRemove) { + songs.remove(songIndexToRemove.intValue()); + songsChanged = true; + } + for (int songToAdd : ServletRequestUtils.getIntParameters(request, "songIdToAdd")) { + MediaFile song = mediaFileService.getMediaFile(songToAdd); + if (song != null) { + songs.add(song); + songsChanged = true; + } + } + if (songsChanged) { + playlistService.setFilesInPlaylist(id, songs); + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void deletePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + Playlist playlist = playlistService.getPlaylist(id); + if (playlist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Playlist not found: " + id); + return; + } + if (!playlistService.isWriteAllowed(playlist, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Permission denied for playlist " + id); + return; + } + playlistService.deletePlaylist(id); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getAlbumList(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("albumList", false); + + try { + int size = ServletRequestUtils.getIntParameter(request, "size", 10); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + size = Math.max(0, Math.min(size, 500)); + String type = ServletRequestUtils.getRequiredStringParameter(request, "type"); + + List<HomeController.Album> albums; + if ("highest".equals(type)) { + albums = homeController.getHighestRated(offset, size); + } else if ("frequent".equals(type)) { + albums = homeController.getMostFrequent(offset, size); + } else if ("recent".equals(type)) { + albums = homeController.getMostRecent(offset, size); + } else if ("newest".equals(type)) { + albums = homeController.getNewest(offset, size); + } else if ("starred".equals(type)) { + albums = homeController.getStarred(offset, size, username); + } else if ("alphabeticalByArtist".equals(type)) { + albums = homeController.getAlphabetical(offset, size, true); + } else if ("alphabeticalByName".equals(type)) { + albums = homeController.getAlphabetical(offset, size, false); + } else if ("random".equals(type)) { + albums = homeController.getRandom(size); + } else { + throw new Exception("Invalid list type: " + type); + } + + for (HomeController.Album album : albums) { + MediaFile mediaFile = mediaFileService.getMediaFile(album.getPath()); + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("album", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getAlbumList2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("albumList2", false); + + try { + int size = ServletRequestUtils.getIntParameter(request, "size", 10); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + size = Math.max(0, Math.min(size, 500)); + String type = ServletRequestUtils.getRequiredStringParameter(request, "type"); + String username = securityService.getCurrentUsername(request); + + List<Album> albums; + if ("frequent".equals(type)) { + albums = albumDao.getMostFrequentlyPlayedAlbums(offset, size); + } else if ("recent".equals(type)) { + albums = albumDao.getMostRecentlyPlayedAlbums(offset, size); + } else if ("newest".equals(type)) { + albums = albumDao.getNewestAlbums(offset, size); + } else if ("alphabeticalByArtist".equals(type)) { + albums = albumDao.getAlphabetialAlbums(offset, size, true); + } else if ("alphabeticalByName".equals(type)) { + albums = albumDao.getAlphabetialAlbums(offset, size, false); + } else if ("starred".equals(type)) { + albums = albumDao.getStarredAlbums(offset, size, securityService.getCurrentUser(request).getUsername()); + } else if ("random".equals(type)) { + albums = searchService.getRandomAlbumsId3(size); + } else { + throw new Exception("Invalid list type: " + type); + } + for (Album album : albums) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getRandomSongs(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("randomSongs", false); + + try { + int size = ServletRequestUtils.getIntParameter(request, "size", 10); + size = Math.max(0, Math.min(size, 500)); + String genre = ServletRequestUtils.getStringParameter(request, "genre"); + Integer fromYear = ServletRequestUtils.getIntParameter(request, "fromYear"); + Integer toYear = ServletRequestUtils.getIntParameter(request, "toYear"); + Integer musicFolderId = ServletRequestUtils.getIntParameter(request, "musicFolderId"); + RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolderId); + + for (MediaFile mediaFile : searchService.getRandomSongs(criteria)) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("song", attributes, true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getVideos(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("videos", false); + try { + int size = ServletRequestUtils.getIntParameter(request, "size", Integer.MAX_VALUE); + int offset = ServletRequestUtils.getIntParameter(request, "offset", 0); + + for (MediaFile mediaFile : mediaFileDao.getVideos(size, offset)) { + builder.add("video", createAttributesForMediaFile(player, mediaFile, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getNowPlaying(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("nowPlaying", false); + + for (TransferStatus status : statusService.getAllStreamStatuses()) { + + Player player = status.getPlayer(); + File file = status.getFile(); + if (player != null && player.getUsername() != null && file != null) { + + String username = player.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + if (!userSettings.isNowPlayingAllowed()) { + continue; + } + + MediaFile mediaFile = mediaFileService.getMediaFile(file); + + long minutesAgo = status.getMillisSinceLastUpdate() / 1000L / 60L; + if (minutesAgo < 60) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + attributes.add("username", username); + attributes.add("playerId", player.getId()); + attributes.add("playerName", player.getName()); + attributes.add("minutesAgo", minutesAgo); + builder.add("entry", attributes, true); + } + } + } + + builder.endAll(); + response.getWriter().print(builder); + } + + private AttributeSet createAttributesForMediaFile(Player player, MediaFile mediaFile, String username) { + MediaFile parent = mediaFileService.getParentOf(mediaFile); + AttributeSet attributes = new AttributeSet(); + attributes.add("id", mediaFile.getId()); + try { + if (!mediaFileService.isRoot(parent)) { + attributes.add("parent", parent.getId()); + } + } catch (SecurityException x) { + // Ignored. + } + attributes.add("title", mediaFile.getName()); + attributes.add("album", mediaFile.getAlbumName()); + attributes.add("artist", mediaFile.getArtist()); + attributes.add("isDir", mediaFile.isDirectory()); + attributes.add("coverArt", findCoverArt(mediaFile, parent)); + attributes.add("created", StringUtil.toISO8601(mediaFile.getCreated())); + attributes.add("starred", StringUtil.toISO8601(mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username))); + attributes.add("userRating", ratingService.getRatingForUser(username, mediaFile)); + attributes.add("averageRating", ratingService.getAverageRating(mediaFile)); + + if (mediaFile.isFile()) { + attributes.add("duration", mediaFile.getDurationSeconds()); + attributes.add("bitRate", mediaFile.getBitRate()); + attributes.add("track", mediaFile.getTrackNumber()); + attributes.add("discNumber", mediaFile.getDiscNumber()); + attributes.add("year", mediaFile.getYear()); + attributes.add("genre", mediaFile.getGenre()); + attributes.add("size", mediaFile.getFileSize()); + String suffix = mediaFile.getFormat(); + attributes.add("suffix", suffix); + attributes.add("contentType", StringUtil.getMimeType(suffix)); + attributes.add("isVideo", mediaFile.isVideo()); + attributes.add("path", getRelativePath(mediaFile)); + + if (mediaFile.getArtist() != null && mediaFile.getAlbumName() != null) { + Album album = albumDao.getAlbum(mediaFile.getAlbumArtist(), mediaFile.getAlbumName()); + if (album != null) { + attributes.add("albumId", album.getId()); + } + } + if (mediaFile.getArtist() != null) { + Artist artist = artistDao.getArtist(mediaFile.getArtist()); + if (artist != null) { + attributes.add("artistId", artist.getId()); + } + } + switch (mediaFile.getMediaType()) { + case MUSIC: + attributes.add("type", "music"); + break; + case PODCAST: + attributes.add("type", "podcast"); + break; + case AUDIOBOOK: + attributes.add("type", "audiobook"); + break; + default: + break; + } + + if (transcodingService.isTranscodingRequired(mediaFile, player)) { + String transcodedSuffix = transcodingService.getSuffix(player, mediaFile, null); + attributes.add("transcodedSuffix", transcodedSuffix); + attributes.add("transcodedContentType", StringUtil.getMimeType(transcodedSuffix)); + } + } + return attributes; + } + + private Integer findCoverArt(MediaFile mediaFile, MediaFile parent) { + MediaFile dir = mediaFile.isDirectory() ? mediaFile : parent; + if (dir != null && dir.getCoverArtPath() != null) { + return dir.getId(); + } + return null; + } + + private String getRelativePath(MediaFile musicFile) { + + String filePath = musicFile.getPath(); + + // Convert slashes. + filePath = filePath.replace('\\', '/'); + + String filePathLower = filePath.toLowerCase(); + + List<MusicFolder> musicFolders = settingsService.getAllMusicFolders(false, true); + for (MusicFolder musicFolder : musicFolders) { + String folderPath = musicFolder.getPath().getPath(); + folderPath = folderPath.replace('\\', '/'); + String folderPathLower = folderPath.toLowerCase(); + + if (filePathLower.startsWith(folderPathLower)) { + String relativePath = filePath.substring(folderPath.length()); + return relativePath.startsWith("/") ? relativePath.substring(1) : relativePath; + } + } + + return null; + } + + public ModelAndView download(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isDownloadRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to download files."); + return null; + } + + long ifModifiedSince = request.getDateHeader("If-Modified-Since"); + long lastModified = downloadController.getLastModified(request); + + if (ifModifiedSince != -1 && lastModified != -1 && lastModified <= ifModifiedSince) { + response.sendError(HttpServletResponse.SC_NOT_MODIFIED); + return null; + } + + if (lastModified != -1) { + response.setDateHeader("Last-Modified", lastModified); + } + + return downloadController.handleRequest(request, response); + } + + public ModelAndView stream(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isStreamRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to play files."); + return null; + } + + streamController.handleRequest(request, response); + return null; + } + + public void scrobble(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + Player player = playerService.getPlayer(request, response); + + if (!settingsService.getUserSettings(player.getUsername()).isLastFmEnabled()) { + error(request, response, ErrorCode.GENERIC, "Scrobbling is not enabled for " + player.getUsername() + "."); + return; + } + + try { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + if (file == null) { + error(request, response, ErrorCode.NOT_FOUND, "File not found: " + id); + return; + } + boolean submission = ServletRequestUtils.getBooleanParameter(request, "submission", true); + audioScrobblerService.register(file, player.getUsername(), submission); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + return; + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void star(HttpServletRequest request, HttpServletResponse response) throws Exception { + starOrUnstar(request, response, true); + } + + public void unstar(HttpServletRequest request, HttpServletResponse response) throws Exception { + starOrUnstar(request, response, false); + } + + private void starOrUnstar(HttpServletRequest request, HttpServletResponse response, boolean star) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + try { + String username = securityService.getCurrentUser(request).getUsername(); + for (int id : ServletRequestUtils.getIntParameters(request, "id")) { + MediaFile mediaFile = mediaFileDao.getMediaFile(id); + if (mediaFile == null) { + error(request, response, ErrorCode.NOT_FOUND, "Media file not found: " + id); + return; + } + if (star) { + mediaFileDao.starMediaFile(id, username); + } else { + mediaFileDao.unstarMediaFile(id, username); + } + } + for (int albumId : ServletRequestUtils.getIntParameters(request, "albumId")) { + Album album = albumDao.getAlbum(albumId); + if (album == null) { + error(request, response, ErrorCode.NOT_FOUND, "Album not found: " + albumId); + return; + } + if (star) { + albumDao.starAlbum(albumId, username); + } else { + albumDao.unstarAlbum(albumId, username); + } + } + for (int artistId : ServletRequestUtils.getIntParameters(request, "artistId")) { + Artist artist = artistDao.getArtist(artistId); + if (artist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Artist not found: " + artistId); + return; + } + if (star) { + artistDao.starArtist(artistId, username); + } else { + artistDao.unstarArtist(artistId, username); + } + } + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + return; + } + + builder.endAll(); + response.getWriter().print(builder); + } + + public void getStarred(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("starred", false); + for (MediaFile artist : mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username)) { + builder.add("artist", true, + new Attribute("name", artist.getName()), + new Attribute("id", artist.getId())); + } + for (MediaFile album : mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username)) { + builder.add("album", createAttributesForMediaFile(player, album, username), true); + } + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username)) { + builder.add("song", createAttributesForMediaFile(player, song, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getStarred2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("starred2", false); + for (Artist artist : artistDao.getStarredArtists(0, Integer.MAX_VALUE, username)) { + builder.add("artist", createAttributesForArtist(artist, username), true); + } + for (Album album : albumDao.getStarredAlbums(0, Integer.MAX_VALUE, username)) { + builder.add("album", createAttributesForAlbum(album, username), true); + } + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username)) { + builder.add("song", createAttributesForMediaFile(player, song, username), true); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getPodcasts(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + XMLBuilder builder = createXMLBuilder(request, response, true); + builder.add("podcasts", false); + + for (PodcastChannel channel : podcastService.getAllChannels()) { + AttributeSet channelAttrs = new AttributeSet(); + channelAttrs.add("id", channel.getId()); + channelAttrs.add("url", channel.getUrl()); + channelAttrs.add("status", channel.getStatus().toString().toLowerCase()); + channelAttrs.add("title", channel.getTitle()); + channelAttrs.add("description", channel.getDescription()); + channelAttrs.add("errorMessage", channel.getErrorMessage()); + builder.add("channel", channelAttrs, false); + + List<PodcastEpisode> episodes = podcastService.getEpisodes(channel.getId(), false); + for (PodcastEpisode episode : episodes) { + AttributeSet episodeAttrs = new AttributeSet(); + + String path = episode.getPath(); + if (path != null) { + MediaFile mediaFile = mediaFileService.getMediaFile(path); + episodeAttrs.addAll(createAttributesForMediaFile(player, mediaFile, username)); + episodeAttrs.add("streamId", mediaFile.getId()); + } + + episodeAttrs.add("id", episode.getId()); // Overwrites the previous "id" attribute. + episodeAttrs.add("status", episode.getStatus().toString().toLowerCase()); + episodeAttrs.add("title", episode.getTitle()); + episodeAttrs.add("description", episode.getDescription()); + episodeAttrs.add("publishDate", episode.getPublishDate()); + + builder.add("episode", episodeAttrs, true); + } + + builder.end(); // <channel> + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void getShares(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + User user = securityService.getCurrentUser(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + builder.add("shares", false); + for (Share share : shareService.getSharesForUser(user)) { + builder.add("share", createAttributesForShare(share), false); + + for (MediaFile mediaFile : shareService.getSharedFiles(share.getId())) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + + builder.end(); + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void createShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + User user = securityService.getCurrentUser(request); + if (!user.isShareRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to share media."); + return; + } + + if (!settingsService.isUrlRedirectionEnabled()) { + error(request, response, ErrorCode.GENERIC, "Sharing is only supported for *.subsonic.org domain names."); + return; + } + + XMLBuilder builder = createXMLBuilder(request, response, true); + + try { + + List<MediaFile> files = new ArrayList<MediaFile>(); + for (int id : ServletRequestUtils.getRequiredIntParameters(request, "id")) { + files.add(mediaFileService.getMediaFile(id)); + } + + // TODO: Update api.jsp + + Share share = shareService.createShare(request, files); + share.setDescription(request.getParameter("description")); + long expires = ServletRequestUtils.getLongParameter(request, "expires", 0L); + if (expires != 0) { + share.setExpires(new Date(expires)); + } + shareService.updateShare(share); + + builder.add("shares", false); + builder.add("share", createAttributesForShare(share), false); + + for (MediaFile mediaFile : shareService.getSharedFiles(share.getId())) { + AttributeSet attributes = createAttributesForMediaFile(player, mediaFile, username); + builder.add("entry", attributes, true); + } + + builder.endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void deleteShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + Share share = shareService.getShareById(id); + if (share == null) { + error(request, response, ErrorCode.NOT_FOUND, "Shared media not found."); + return; + } + if (!user.isAdminRole() && !share.getUsername().equals(user.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Not authorized to delete shared media."); + return; + } + + shareService.deleteShare(id); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void updateShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + try { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + Share share = shareService.getShareById(id); + if (share == null) { + error(request, response, ErrorCode.NOT_FOUND, "Shared media not found."); + return; + } + if (!user.isAdminRole() && !share.getUsername().equals(user.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Not authorized to modify shared media."); + return; + } + + share.setDescription(request.getParameter("description")); + String expiresString = request.getParameter("expires"); + if (expiresString != null) { + long expires = Long.parseLong(expiresString); + share.setExpires(expires == 0L ? null : new Date(expires)); + } + shareService.updateShare(share); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + private List<Attribute> createAttributesForShare(Share share) { + List<Attribute> attributes = new ArrayList<Attribute>(); + + attributes.add(new Attribute("id", share.getId())); + attributes.add(new Attribute("url", shareService.getShareUrl(share))); + attributes.add(new Attribute("username", share.getUsername())); + attributes.add(new Attribute("created", StringUtil.toISO8601(share.getCreated()))); + attributes.add(new Attribute("visitCount", share.getVisitCount())); + attributes.add(new Attribute("description", share.getDescription())); + attributes.add(new Attribute("expires", StringUtil.toISO8601(share.getExpires()))); + attributes.add(new Attribute("lastVisited", StringUtil.toISO8601(share.getLastVisited()))); + + return attributes; + } + + public ModelAndView videoPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + Map<String, Object> map = new HashMap<String, Object>(); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + + int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0); + timeOffset = Math.max(0, timeOffset); + Integer duration = file.getDurationSeconds(); + if (duration != null) { + map.put("skipOffsets", VideoPlayerController.createSkipOffsets(duration)); + timeOffset = Math.min(duration, timeOffset); + duration -= timeOffset; + } + + map.put("id", request.getParameter("id")); + map.put("u", request.getParameter("u")); + map.put("p", request.getParameter("p")); + map.put("c", request.getParameter("c")); + map.put("v", request.getParameter("v")); + map.put("video", file); + map.put("maxBitRate", ServletRequestUtils.getIntParameter(request, "maxBitRate", VideoPlayerController.DEFAULT_BIT_RATE)); + map.put("duration", duration); + map.put("timeOffset", timeOffset); + map.put("bitRates", VideoPlayerController.BIT_RATES); + map.put("autoplay", ServletRequestUtils.getBooleanParameter(request, "autoplay", true)); + + ModelAndView result = new ModelAndView("rest/videoPlayer"); + result.addObject("model", map); + return result; + } + + public ModelAndView getCoverArt(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + return coverArtController.handleRequest(request, response); + } + + public ModelAndView getAvatar(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + return avatarController.handleRequest(request, response); + } + + public void changePassword(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + try { + + String username = ServletRequestUtils.getRequiredStringParameter(request, "username"); + String password = decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password")); + + User authUser = securityService.getCurrentUser(request); + if (!authUser.isAdminRole() && !username.equals(authUser.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, authUser.getUsername() + " is not authorized to change password for " + username); + return; + } + + User user = securityService.getUserByName(username); + user.setPassword(password); + securityService.updateUser(user); + + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + String username; + try { + username = ServletRequestUtils.getRequiredStringParameter(request, "username"); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + return; + } + + User currentUser = securityService.getCurrentUser(request); + if (!username.equals(currentUser.getUsername()) && !currentUser.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, currentUser.getUsername() + " is not authorized to get details for other users."); + return; + } + + User requestedUser = securityService.getUserByName(username); + if (requestedUser == null) { + error(request, response, ErrorCode.NOT_FOUND, "No such user: " + username); + return; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + + XMLBuilder builder = createXMLBuilder(request, response, true); + List<Attribute> attributes = Arrays.asList( + new Attribute("username", requestedUser.getUsername()), + new Attribute("email", requestedUser.getEmail()), + new Attribute("scrobblingEnabled", userSettings.isLastFmEnabled()), + new Attribute("adminRole", requestedUser.isAdminRole()), + new Attribute("settingsRole", requestedUser.isSettingsRole()), + new Attribute("downloadRole", requestedUser.isDownloadRole()), + new Attribute("uploadRole", requestedUser.isUploadRole()), + new Attribute("playlistRole", true), // Since 1.8.0 + new Attribute("coverArtRole", requestedUser.isCoverArtRole()), + new Attribute("commentRole", requestedUser.isCommentRole()), + new Attribute("podcastRole", requestedUser.isPodcastRole()), + new Attribute("streamRole", requestedUser.isStreamRole()), + new Attribute("jukeboxRole", requestedUser.isJukeboxRole()), + new Attribute("shareRole", requestedUser.isShareRole()) + ); + + builder.add("user", attributes, true); + builder.endAll(); + response.getWriter().print(builder); + } + + public void createUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to create new users."); + return; + } + + try { + UserSettingsCommand command = new UserSettingsCommand(); + command.setUsername(ServletRequestUtils.getRequiredStringParameter(request, "username")); + command.setPassword(decrypt(ServletRequestUtils.getRequiredStringParameter(request, "password"))); + command.setEmail(ServletRequestUtils.getRequiredStringParameter(request, "email")); + command.setLdapAuthenticated(ServletRequestUtils.getBooleanParameter(request, "ldapAuthenticated", false)); + command.setAdminRole(ServletRequestUtils.getBooleanParameter(request, "adminRole", false)); + command.setCommentRole(ServletRequestUtils.getBooleanParameter(request, "commentRole", false)); + command.setCoverArtRole(ServletRequestUtils.getBooleanParameter(request, "coverArtRole", false)); + command.setDownloadRole(ServletRequestUtils.getBooleanParameter(request, "downloadRole", false)); + command.setStreamRole(ServletRequestUtils.getBooleanParameter(request, "streamRole", true)); + command.setUploadRole(ServletRequestUtils.getBooleanParameter(request, "uploadRole", false)); + command.setJukeboxRole(ServletRequestUtils.getBooleanParameter(request, "jukeboxRole", false)); + command.setPodcastRole(ServletRequestUtils.getBooleanParameter(request, "podcastRole", false)); + command.setSettingsRole(ServletRequestUtils.getBooleanParameter(request, "settingsRole", true)); + command.setTranscodeSchemeName(ServletRequestUtils.getStringParameter(request, "transcodeScheme", TranscodeScheme.OFF.name())); + command.setShareRole(ServletRequestUtils.getBooleanParameter(request, "shareRole", false)); + + userSettingsController.createUser(command); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void deleteUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to delete users."); + return; + } + + try { + String username = ServletRequestUtils.getRequiredStringParameter(request, "username"); + securityService.deleteUser(username); + + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } catch (Exception x) { + LOG.warn("Error in REST API.", x); + error(request, response, ErrorCode.GENERIC, getErrorMessage(x)); + } + } + + public void getChatMessages(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + XMLBuilder builder = createXMLBuilder(request, response, true); + + long since = ServletRequestUtils.getLongParameter(request, "since", 0L); + + builder.add("chatMessages", false); + + for (ChatService.Message message : chatService.getMessages(0L).getMessages()) { + long time = message.getDate().getTime(); + if (time > since) { + builder.add("chatMessage", true, new Attribute("username", message.getUsername()), + new Attribute("time", time), new Attribute("message", message.getContent())); + } + } + builder.endAll(); + response.getWriter().print(builder); + } + + public void addChatMessage(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + try { + chatService.doAddMessage(ServletRequestUtils.getRequiredStringParameter(request, "message"), request); + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } + } + + public void getLyrics(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String artist = request.getParameter("artist"); + String title = request.getParameter("title"); + LyricsInfo lyrics = lyricsService.getLyrics(artist, title); + + XMLBuilder builder = createXMLBuilder(request, response, true); + AttributeSet attributes = new AttributeSet(); + attributes.add("artist", lyrics.getArtist()); + attributes.add("title", lyrics.getTitle()); + builder.add("lyrics", attributes, lyrics.getLyrics(), true); + + builder.endAll(); + response.getWriter().print(builder); + } + + public void setRating(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + try { + Integer rating = ServletRequestUtils.getRequiredIntParameter(request, "rating"); + if (rating == 0) { + rating = null; + } + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile == null) { + error(request, response, ErrorCode.NOT_FOUND, "File not found: " + id); + return; + } + + String username = securityService.getCurrentUsername(request); + ratingService.setRatingForUser(username, mediaFile, rating); + + XMLBuilder builder = createXMLBuilder(request, response, true).endAll(); + response.getWriter().print(builder); + } catch (ServletRequestBindingException x) { + error(request, response, ErrorCode.MISSING_PARAMETER, getErrorMessage(x)); + } + } + + private HttpServletRequest wrapRequest(HttpServletRequest request) { + return wrapRequest(request, false); + } + + private HttpServletRequest wrapRequest(final HttpServletRequest request, boolean jukebox) { + final String playerId = createPlayerIfNecessary(request, jukebox); + return new HttpServletRequestWrapper(request) { + @Override + public String getParameter(String name) { + // Returns the correct player to be used in PlayerService.getPlayer() + if ("player".equals(name)) { + return playerId; + } + + // Support old style ID parameters. + if ("id".equals(name)) { + return mapId(request.getParameter("id")); + } + + return super.getParameter(name); + } + }; + } + + private String mapId(String id) { + if (id == null || id.startsWith(CoverArtController.ALBUM_COVERART_PREFIX) || + id.startsWith(CoverArtController.ARTIST_COVERART_PREFIX) || StringUtils.isNumeric(id)) { + return id; + } + + try { + String path = StringUtil.utf8HexDecode(id); + MediaFile mediaFile = mediaFileService.getMediaFile(path); + return String.valueOf(mediaFile.getId()); + } catch (Exception x) { + return id; + } + } + + private String getErrorMessage(Exception x) { + if (x.getMessage() != null) { + return x.getMessage(); + } + return x.getClass().getSimpleName(); + } + + private void error(HttpServletRequest request, HttpServletResponse response, ErrorCode code, String message) throws IOException { + XMLBuilder builder = createXMLBuilder(request, response, false); + builder.add("error", true, + new XMLBuilder.Attribute("code", code.getCode()), + new XMLBuilder.Attribute("message", message)); + builder.end(); + response.getWriter().print(builder); + } + + private XMLBuilder createXMLBuilder(HttpServletRequest request, HttpServletResponse response, boolean ok) throws IOException { + String format = ServletRequestUtils.getStringParameter(request, "f", "xml"); + boolean json = "json".equals(format); + boolean jsonp = "jsonp".equals(format); + XMLBuilder builder; + + response.setCharacterEncoding(StringUtil.ENCODING_UTF8); + + if (json) { + builder = XMLBuilder.createJSONBuilder(); + response.setContentType("application/json"); + } else if (jsonp) { + builder = XMLBuilder.createJSONPBuilder(request.getParameter("callback")); + response.setContentType("text/javascript"); + } else { + builder = XMLBuilder.createXMLBuilder(); + response.setContentType("text/xml"); + } + + builder.preamble(StringUtil.ENCODING_UTF8); + builder.add("subsonic-response", false, + new Attribute("xmlns", "http://subsonic.org/restapi"), + new Attribute("status", ok ? "ok" : "failed"), + new Attribute("version", StringUtil.getRESTProtocolVersion())); + return builder; + } + + private String createPlayerIfNecessary(HttpServletRequest request, boolean jukebox) { + String username = request.getRemoteUser(); + String clientId = request.getParameter("c"); + if (jukebox) { + clientId += "-jukebox"; + } + + List<Player> players = playerService.getPlayersForUserAndClientId(username, clientId); + + // If not found, create it. + if (players.isEmpty()) { + Player player = new Player(); + player.setIpAddress(request.getRemoteAddr()); + player.setUsername(username); + player.setClientId(clientId); + player.setName(clientId); + player.setTechnology(jukebox ? PlayerTechnology.JUKEBOX : PlayerTechnology.EXTERNAL_WITH_PLAYLIST); + playerService.createPlayer(player); + players = playerService.getPlayersForUserAndClientId(username, clientId); + } + + // Return the player ID. + return !players.isEmpty() ? players.get(0).getId() : null; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setDownloadController(DownloadController downloadController) { + this.downloadController = downloadController; + } + + public void setCoverArtController(CoverArtController coverArtController) { + this.coverArtController = coverArtController; + } + + public void setUserSettingsController(UserSettingsController userSettingsController) { + this.userSettingsController = userSettingsController; + } + + public void setLeftController(LeftController leftController) { + this.leftController = leftController; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setStreamController(StreamController streamController) { + this.streamController = streamController; + } + + public void setChatService(ChatService chatService) { + this.chatService = chatService; + } + + public void setHomeController(HomeController homeController) { + this.homeController = homeController; + } + + public void setLyricsService(LyricsService lyricsService) { + this.lyricsService = lyricsService; + } + + public void setPlayQueueService(PlayQueueService playQueueService) { + this.playQueueService = playQueueService; + } + + public void setJukeboxService(JukeboxService jukeboxService) { + this.jukeboxService = jukeboxService; + } + + public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) { + this.audioScrobblerService = audioScrobblerService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setAvatarController(AvatarController avatarController) { + this.avatarController = avatarController; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public static enum ErrorCode { + + GENERIC(0, "A generic error."), + MISSING_PARAMETER(10, "Required parameter is missing."), + PROTOCOL_MISMATCH_CLIENT_TOO_OLD(20, "Incompatible Subsonic REST protocol version. Client must upgrade."), + PROTOCOL_MISMATCH_SERVER_TOO_OLD(30, "Incompatible Subsonic REST protocol version. Server must upgrade."), + NOT_AUTHENTICATED(40, "Wrong username or password."), + NOT_AUTHORIZED(50, "User is not authorized for the given operation."), + NOT_LICENSED(60, "The trial period for the Subsonic server is over. Please donate to get a license key. Visit subsonic.org for details."), + NOT_FOUND(70, "Requested data was not found."); + + private final int code; + private final String message; + + ErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java new file mode 100644 index 00000000..a3738684 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.java @@ -0,0 +1,101 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SearchService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the creating a random play queue. + * + * @author Sindre Mehus + */ +public class RandomPlayQueueController extends ParameterizableViewController { + + private PlayerService playerService; + private List<ReloadFrame> reloadFrames; + private SearchService searchService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + int size = ServletRequestUtils.getRequiredIntParameter(request, "size"); + String genre = request.getParameter("genre"); + if (StringUtils.equalsIgnoreCase("any", genre)) { + genre = null; + } + + Integer fromYear = null; + Integer toYear = null; + + String year = request.getParameter("year"); + if (!StringUtils.equalsIgnoreCase("any", year)) { + String[] tmp = StringUtils.split(year); + fromYear = Integer.parseInt(tmp[0]); + toYear = Integer.parseInt(tmp[1]); + } + + Integer musicFolderId = ServletRequestUtils.getRequiredIntParameter(request, "musicFolderId"); + if (musicFolderId == -1) { + musicFolderId = null; + } + + Player player = playerService.getPlayer(request, response); + PlayQueue playQueue = player.getPlayQueue(); + + RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolderId); + playQueue.addFiles(false, searchService.getRandomSongs(criteria)); + + if (request.getParameter("autoRandom") != null) { + playQueue.setRandomSearchCriteria(criteria); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("reloadFrames", reloadFrames); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setReloadFrames(List<ReloadFrame> reloadFrames) { + this.reloadFrames = reloadFrames; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java new file mode 100644 index 00000000..093b7fa1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ReloadFrame.java @@ -0,0 +1,52 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +/** + * Used in subsonic-servlet.xml to specify frame reloading. + * + * @author Sindre Mehus + */ +public class ReloadFrame { + private String frame; + private String view; + + public ReloadFrame() {} + + public ReloadFrame(String frame, String view) { + this.frame = frame; + this.view = view; + } + + public String getFrame() { + return frame; + } + + public void setFrame(String frame) { + this.frame = frame; + } + + public String getView() { + return view; + } + + public void setView(String view) { + this.view = view; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java new file mode 100644 index 00000000..405c2dc7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.java @@ -0,0 +1,66 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.dao.UserDao; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * Controller for the right frame. + * + * @author Sindre Mehus + */ +public class RightController extends ParameterizableViewController { + + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + ModelAndView result = super.handleRequestInternal(request, response); + + UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + map.put("showNowPlaying", userSettings.isShowNowPlayingEnabled()); + map.put("showChat", userSettings.isShowChatEnabled()); + map.put("user", securityService.getCurrentUser(request)); + + result.addObject("model", map); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java new file mode 100644 index 00000000..387ec7db --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.java @@ -0,0 +1,106 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.SearchCommand; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.SearchService; + +/** + * Controller for the search page. + * + * @author Sindre Mehus + */ +public class SearchController extends SimpleFormController { + + private static final int MATCH_COUNT = 25; + + private SecurityService securityService; + private SettingsService settingsService; + private PlayerService playerService; + private SearchService searchService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + return new SearchCommand(); + } + + @Override + protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors) + throws Exception { + SearchCommand command = (SearchCommand) com; + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + command.setUser(user); + command.setPartyModeEnabled(userSettings.isPartyModeEnabled()); + + String any = StringUtils.trimToNull(command.getQuery()); + + if (any != null) { + + SearchCriteria criteria = new SearchCriteria(); + criteria.setCount(MATCH_COUNT); + criteria.setQuery(any); + + SearchResult artists = searchService.search(criteria, SearchService.IndexType.ARTIST); + command.setArtists(artists.getMediaFiles()); + + SearchResult albums = searchService.search(criteria, SearchService.IndexType.ALBUM); + command.setAlbums(albums.getMediaFiles()); + + SearchResult songs = searchService.search(criteria, SearchService.IndexType.SONG); + command.setSongs(songs.getMediaFiles()); + + command.setPlayer(playerService.getPlayer(request, response)); + } + + return new ModelAndView(getSuccessView(), errors.getModel()); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java new file mode 100644 index 00000000..8b3ebca7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.java @@ -0,0 +1,58 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.view.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; + +/** + * Controller for updating music file metadata. + * + * @author Sindre Mehus + */ +public class SetMusicFileInfoController extends AbstractController { + + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + String path = request.getParameter("path"); + String action = request.getParameter("action"); + + MediaFile mediaFile = mediaFileService.getMediaFile(path); + + if ("comment".equals(action)) { + mediaFile.setComment(StringUtil.toHtml(request.getParameter("comment"))); + mediaFileService.updateMediaFile(mediaFile); + } + + String url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + StringUtil.utf8HexEncode(path); + return new ModelAndView(new RedirectView(url)); + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java new file mode 100644 index 00000000..aaeaa4a4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.java @@ -0,0 +1,69 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import org.springframework.web.bind.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; +import org.springframework.web.servlet.view.*; + +import javax.servlet.http.*; + +/** + * Controller for updating music file ratings. + * + * @author Sindre Mehus + */ +public class SetRatingController extends AbstractController { + + private RatingService ratingService; + private SecurityService securityService; + private MediaFileService mediaFileService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + String path = request.getParameter("path"); + Integer rating = ServletRequestUtils.getIntParameter(request, "rating"); + if (rating == 0) { + rating = null; + } + + MediaFile mediaFile = mediaFileService.getMediaFile(path); + String username = securityService.getCurrentUsername(request); + ratingService.setRatingForUser(username, mediaFile, rating); + + String url = "main.view?path" + ParameterDecodingFilter.PARAM_SUFFIX + "=" + StringUtil.utf8HexEncode(path); + return new ModelAndView(new RedirectView(url)); + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java new file mode 100644 index 00000000..ed0c21c5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SettingsController.java @@ -0,0 +1,52 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.view.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; + +/** + * Controller for the main settings page. + * + * @author Sindre Mehus + */ +public class SettingsController extends AbstractController { + + private SecurityService securityService; + + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + + // Redirect to music folder settings if admin. + String view = user.isAdminRole() ? "musicFolderSettings.view" : "personalSettings.view"; + + return new ModelAndView(new RedirectView(view)); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java new file mode 100644 index 00000000..de2ea764 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.java @@ -0,0 +1,123 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.*; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Share; + +/** + * Controller for sharing music on Twitter, Facebook etc. + * + * @author Sindre Mehus + */ +public class ShareManagementController extends MultiActionController { + + private MediaFileService mediaFileService; + private SettingsService settingsService; + private ShareService shareService; + private PlayerService playerService; + private SecurityService securityService; + + public ModelAndView createShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + + List<MediaFile> files = getMediaFiles(request); + MediaFile dir = null; + if (!files.isEmpty()) { + dir = files.get(0); + if (!dir.isAlbum()) { + dir = mediaFileService.getParentOf(dir); + } + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("urlRedirectionEnabled", settingsService.isUrlRedirectionEnabled()); + map.put("dir", dir); + map.put("user", securityService.getCurrentUser(request)); + Share share = shareService.createShare(request, files); + map.put("playUrl", shareService.getShareUrl(share)); + + return new ModelAndView("createShare", "model", map); + } + + private List<MediaFile> getMediaFiles(HttpServletRequest request) throws IOException { + String dir = request.getParameter("dir"); + String playerId = request.getParameter("player"); + + List<MediaFile> result = new ArrayList<MediaFile>(); + + if (dir != null) { + MediaFile album = mediaFileService.getMediaFile(dir); + int[] indexes = ServletRequestUtils.getIntParameters(request, "i"); + if (indexes.length == 0) { + return Arrays.asList(album); + } + List<MediaFile> children = mediaFileService.getChildrenOf(album, true, true, true); + for (int index : indexes) { + result.add(children.get(index)); + } + } else if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + PlayQueue playQueue = player.getPlayQueue(); + List<MediaFile> result1; + synchronized (playQueue) { + result1 = playQueue.getFiles(); + } + result = result1; + } + + return result; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java new file mode 100644 index 00000000..2b8a958a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java @@ -0,0 +1,161 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.ShareService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the page used to administrate the set of shared media. + * + * @author Sindre Mehus + */ +public class ShareSettingsController extends ParameterizableViewController { + + private ShareService shareService; + private SecurityService securityService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + if (isFormSubmission(request)) { + String error = handleParameters(request); + map.put("error", error); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("shareBaseUrl", shareService.getShareBaseUrl()); + map.put("shareInfos", getShareInfos(request)); + map.put("user", securityService.getCurrentUser(request)); + + result.addObject("model", map); + return result; + } + + /** + * Determine if the given request represents a form submission. + * + * @param request current HTTP request + * @return if the request represents a form submission + */ + private boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + private String handleParameters(HttpServletRequest request) { + User user = securityService.getCurrentUser(request); + for (Share share : shareService.getSharesForUser(user)) { + int id = share.getId(); + + String description = getParameter(request, "description", id); + boolean delete = getParameter(request, "delete", id) != null; + String expireIn = getParameter(request, "expireIn", id); + + if (delete) { + shareService.deleteShare(id); + } else { + if (expireIn != null) { + share.setExpires(parseExpireIn(expireIn)); + } + share.setDescription(description); + shareService.updateShare(share); + } + } + + return null; + } + + private List<ShareInfo> getShareInfos(HttpServletRequest request) { + List<ShareInfo> result = new ArrayList<ShareInfo>(); + User user = securityService.getCurrentUser(request); + for (Share share : shareService.getSharesForUser(user)) { + List<MediaFile> files = shareService.getSharedFiles(share.getId()); + if (!files.isEmpty()) { + MediaFile file = files.get(0); + result.add(new ShareInfo(share, file.isDirectory() ? file : mediaFileService.getParentOf(file))); + } + } + return result; + } + + + private String getParameter(HttpServletRequest request, String name, int id) { + return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]")); + } + + private Date parseExpireIn(String expireIn) { + int days = Integer.parseInt(expireIn); + if (days == 0) { + return null; + } + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_YEAR, days); + return calendar.getTime(); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public static class ShareInfo { + private final Share share; + private final MediaFile dir; + + public ShareInfo(Share share, MediaFile dir) { + this.share = share; + this.dir = dir; + } + + public Share getShare() { + return share; + } + + public MediaFile getDir() { + return dir; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java new file mode 100644 index 00000000..2da8b5ad --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java @@ -0,0 +1,96 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for showing a user's starred items. + * + * @author Sindre Mehus + */ +public class StarredController extends ParameterizableViewController { + + private PlayerService playerService; + private MediaFileDao mediaFileDao; + private SecurityService securityService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + + List<MediaFile> artists = mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username); + List<MediaFile> albums = mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username); + List<MediaFile> songs = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username); + mediaFileService.populateStarredDate(artists, username); + mediaFileService.populateStarredDate(albums, username); + mediaFileService.populateStarredDate(songs, username); + + map.put("user", user); + map.put("partyModeEnabled", userSettings.isPartyModeEnabled()); + map.put("player", playerService.getPlayer(request, response)); + map.put("artists", artists); + map.put("albums", albums); + map.put("songs", songs); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java new file mode 100644 index 00000000..878b8ae8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusChartController.java @@ -0,0 +1,149 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.service.*; +import org.jfree.chart.*; +import org.jfree.chart.axis.*; +import org.jfree.chart.plot.*; +import org.jfree.chart.renderer.xy.*; +import org.jfree.data.*; +import org.jfree.data.time.*; +import org.springframework.web.servlet.*; + +import javax.servlet.http.*; +import java.awt.*; +import java.util.*; +import java.util.List; + +/** + * Controller for generating a chart showing bitrate vs time. + * + * @author Sindre Mehus + */ +public class StatusChartController extends AbstractChartController { + + private StatusService statusService; + + public static final int IMAGE_WIDTH = 350; + public static final int IMAGE_HEIGHT = 150; + + public synchronized ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String type = request.getParameter("type"); + int index = Integer.parseInt(request.getParameter("index")); + + List<TransferStatus> statuses = Collections.emptyList(); + if ("stream".equals(type)) { + statuses = statusService.getAllStreamStatuses(); + } else if ("download".equals(type)) { + statuses = statusService.getAllDownloadStatuses(); + } else if ("upload".equals(type)) { + statuses = statusService.getAllUploadStatuses(); + } + + if (index < 0 || index >= statuses.size()) { + return null; + } + TransferStatus status = statuses.get(index); + + TimeSeries series = new TimeSeries("Kbps", Millisecond.class); + TransferStatus.SampleHistory history = status.getHistory(); + long to = System.currentTimeMillis(); + long from = to - status.getHistoryLengthMillis(); + Range range = new DateRange(from, to); + + if (!history.isEmpty()) { + + TransferStatus.Sample previous = history.get(0); + + for (int i = 1; i < history.size(); i++) { + TransferStatus.Sample sample = history.get(i); + + long elapsedTimeMilis = sample.getTimestamp() - previous.getTimestamp(); + long bytesStreamed = Math.max(0L, sample.getBytesTransfered() - previous.getBytesTransfered()); + + double kbps = (8.0 * bytesStreamed / 1024.0) / (elapsedTimeMilis / 1000.0); + series.addOrUpdate(new Millisecond(new Date(sample.getTimestamp())), kbps); + + previous = sample; + } + } + + // Compute moving average. + series = MovingAverage.createMovingAverage(series, "Kbps", 20000, 5000); + + // Find min and max values. + double min = 100; + double max = 250; + for (Object obj : series.getItems()) { + TimeSeriesDataItem item = (TimeSeriesDataItem) obj; + double value = item.getValue().doubleValue(); + if (item.getPeriod().getFirstMillisecond() > from) { + min = Math.min(min, value); + max = Math.max(max, value); + } + } + + // Add 10% to max value. + max *= 1.1D; + + // Subtract 10% from min value. + min *= 0.9D; + + TimeSeriesCollection dataset = new TimeSeriesCollection(); + dataset.addSeries(series); + JFreeChart chart = ChartFactory.createTimeSeriesChart(null, null, null, dataset, false, false, false); + XYPlot plot = (XYPlot) chart.getPlot(); + + plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_RIGHT); + Paint background = new GradientPaint(0, 0, Color.lightGray, 0, IMAGE_HEIGHT, Color.white); + plot.setBackgroundPaint(background); + + XYItemRenderer renderer = plot.getRendererForDataset(dataset); + renderer.setSeriesPaint(0, Color.blue.darker()); + renderer.setSeriesStroke(0, new BasicStroke(2f)); + + // Set theme-specific colors. + Color bgColor = getBackground(request); + Color fgColor = getForeground(request); + + chart.setBackgroundPaint(bgColor); + + ValueAxis domainAxis = plot.getDomainAxis(); + domainAxis.setRange(range); + domainAxis.setTickLabelPaint(fgColor); + domainAxis.setTickMarkPaint(fgColor); + domainAxis.setAxisLinePaint(fgColor); + + ValueAxis rangeAxis = plot.getRangeAxis(); + rangeAxis.setRange(new Range(min, max)); + rangeAxis.setTickLabelPaint(fgColor); + rangeAxis.setTickMarkPaint(fgColor); + rangeAxis.setAxisLinePaint(fgColor); + + ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, IMAGE_WIDTH, IMAGE_HEIGHT); + + return null; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java new file mode 100644 index 00000000..964e7810 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StatusController.java @@ -0,0 +1,141 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; +import org.springframework.web.servlet.support.RequestContextUtils; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Controller for the status page. + * + * @author Sindre Mehus + */ +public class StatusController extends ParameterizableViewController { + + private StatusService statusService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + List<TransferStatus> streamStatuses = statusService.getAllStreamStatuses(); + List<TransferStatus> downloadStatuses = statusService.getAllDownloadStatuses(); + List<TransferStatus> uploadStatuses = statusService.getAllUploadStatuses(); + + Locale locale = RequestContextUtils.getLocale(request); + List<TransferStatusHolder> transferStatuses = new ArrayList<TransferStatusHolder>(); + + for (int i = 0; i < streamStatuses.size(); i++) { + long minutesAgo = streamStatuses.get(i).getMillisSinceLastUpdate() / 1000L / 60L; + if (minutesAgo < 60L) { + transferStatuses.add(new TransferStatusHolder(streamStatuses.get(i), true, false, false, i, locale)); + } + } + for (int i = 0; i < downloadStatuses.size(); i++) { + transferStatuses.add(new TransferStatusHolder(downloadStatuses.get(i), false, true, false, i, locale)); + } + for (int i = 0; i < uploadStatuses.size(); i++) { + transferStatuses.add(new TransferStatusHolder(uploadStatuses.get(i), false, false, true, i, locale)); + } + + map.put("transferStatuses", transferStatuses); + map.put("chartWidth", StatusChartController.IMAGE_WIDTH); + map.put("chartHeight", StatusChartController.IMAGE_HEIGHT); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public static class TransferStatusHolder { + private TransferStatus transferStatus; + private boolean isStream; + private boolean isDownload; + private boolean isUpload; + private int index; + private Locale locale; + + public TransferStatusHolder(TransferStatus transferStatus, boolean isStream, boolean isDownload, boolean isUpload, + int index, Locale locale) { + this.transferStatus = transferStatus; + this.isStream = isStream; + this.isDownload = isDownload; + this.isUpload = isUpload; + this.index = index; + this.locale = locale; + } + + public boolean isStream() { + return isStream; + } + + public boolean isDownload() { + return isDownload; + } + + public boolean isUpload() { + return isUpload; + } + + public int getIndex() { + return index; + } + + public Player getPlayer() { + return transferStatus.getPlayer(); + } + + public String getPlayerType() { + Player player = transferStatus.getPlayer(); + return player == null ? null : player.getType(); + } + + public String getUsername() { + Player player = transferStatus.getPlayer(); + return player == null ? null : player.getUsername(); + } + + public String getPath() { + return FileUtil.getShortPath(transferStatus.getFile()); + } + + public String getBytes() { + return StringUtil.formatBytes(transferStatus.getBytesTransfered(), locale); + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java new file mode 100644 index 00000000..a40f5da4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java @@ -0,0 +1,419 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Dimension; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SearchService; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.math.LongRange; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.VideoTranscodingSettings; +import net.sourceforge.subsonic.io.PlayQueueInputStream; +import net.sourceforge.subsonic.io.RangeOutputStream; +import net.sourceforge.subsonic.io.ShoutCastOutputStream; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.StatusService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +/** + * A controller which streams the content of a {@link net.sourceforge.subsonic.domain.PlayQueue} to a remote + * {@link Player}. + * + * @author Sindre Mehus + */ +public class StreamController implements Controller { + + private static final Logger LOG = Logger.getLogger(StreamController.class); + + private StatusService statusService; + private PlayerService playerService; + private PlaylistService playlistService; + private SecurityService securityService; + private SettingsService settingsService; + private TranscodingService transcodingService; + private AudioScrobblerService audioScrobblerService; + private MediaFileService mediaFileService; + private SearchService searchService; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + TransferStatus status = null; + PlayQueueInputStream in = null; + Player player = playerService.getPlayer(request, response, false, true); + User user = securityService.getUserByName(player.getUsername()); + + try { + + if (!user.isStreamRole()) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, "Streaming is forbidden for user " + user.getUsername()); + return null; + } + + // If "playlist" request parameter is set, this is a Podcast request. In that case, create a separate + // play queue (in order to support multiple parallel Podcast streams). + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + boolean isPodcast = playlistId != null; + if (isPodcast) { + PlayQueue playQueue = new PlayQueue(); + playQueue.addFiles(false, playlistService.getFilesInPlaylist(playlistId)); + player.setPlayQueue(playQueue); + Util.setContentLength(response, playQueue.length()); + LOG.info("Incoming Podcast request for playlist " + playlistId); + } + + String contentType = StringUtil.getMimeType(request.getParameter("suffix")); + response.setContentType(contentType); + + String preferredTargetFormat = request.getParameter("format"); + Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate"); + if (Integer.valueOf(0).equals(maxBitRate)) { + maxBitRate = null; + } + + VideoTranscodingSettings videoTranscodingSettings = null; + + // Is this a request for a single file (typically from the embedded Flash player)? + // In that case, create a separate playlist (in order to support multiple parallel streams). + // Also, enable partial download (HTTP byte range). + MediaFile file = getSingleFile(request); + boolean isSingleFile = file != null; + LongRange range = null; + + if (isSingleFile) { + PlayQueue playQueue = new PlayQueue(); + playQueue.addFiles(true, file); + player.setPlayQueue(playQueue); + + if (!file.isVideo()) { + response.setIntHeader("ETag", file.getId()); + response.setHeader("Accept-Ranges", "bytes"); + } + + TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, videoTranscodingSettings); + long fileLength = getFileLength(parameters); + boolean isConversion = parameters.isDownsample() || parameters.isTranscode(); + boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request, "estimateContentLength", false); + + range = getRange(request, file); + if (range != null) { + LOG.info("Got range: " + range); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + Util.setContentLength(response, fileLength - range.getMinimumLong()); + long firstBytePos = range.getMinimumLong(); + long lastBytePos = fileLength - 1; + response.setHeader("Content-Range", "bytes " + firstBytePos + "-" + lastBytePos + "/" + fileLength); + } else if (!isConversion || estimateContentLength) { + Util.setContentLength(response, fileLength); + } + + String transcodedSuffix = transcodingService.getSuffix(player, file, preferredTargetFormat); + response.setContentType(StringUtil.getMimeType(transcodedSuffix)); + + if (file.isVideo()) { + videoTranscodingSettings = createVideoTranscodingSettings(file, request); + } + } + + if (request.getMethod().equals("HEAD")) { + return null; + } + + // Terminate any other streams to this player. + if (!isPodcast && !isSingleFile) { + for (TransferStatus streamStatus : statusService.getStreamStatusesForPlayer(player)) { + if (streamStatus.isActive()) { + streamStatus.terminate(); + } + } + } + + status = statusService.createStreamStatus(player); + + in = new PlayQueueInputStream(player, status, maxBitRate, preferredTargetFormat, videoTranscodingSettings, transcodingService, + audioScrobblerService, mediaFileService, searchService); + OutputStream out = RangeOutputStream.wrap(response.getOutputStream(), range); + + // Enabled SHOUTcast, if requested. + boolean isShoutCastRequested = "1".equals(request.getHeader("icy-metadata")); + if (isShoutCastRequested && !isSingleFile) { + response.setHeader("icy-metaint", "" + ShoutCastOutputStream.META_DATA_INTERVAL); + response.setHeader("icy-notice1", "This stream is served using Subsonic"); + response.setHeader("icy-notice2", "Subsonic - Free media streamer - subsonic.org"); + response.setHeader("icy-name", "Subsonic"); + response.setHeader("icy-genre", "Mixed"); + response.setHeader("icy-url", "http://subsonic.org/"); + out = new ShoutCastOutputStream(out, player.getPlayQueue(), settingsService); + } + + final int BUFFER_SIZE = 2048; + byte[] buf = new byte[BUFFER_SIZE]; + + while (true) { + + // Check if stream has been terminated. + if (status.terminated()) { + return null; + } + + if (player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) { + if (isPodcast || isSingleFile) { + break; + } else { + sendDummy(buf, out); + } + } else { + + int n = in.read(buf); + if (n == -1) { + if (isPodcast || isSingleFile) { + break; + } else { + sendDummy(buf, out); + } + } else { + out.write(buf, 0, n); + } + } + } + + } finally { + if (status != null) { + securityService.updateUserByteCounts(user, status.getBytesTransfered(), 0L, 0L); + statusService.removeStreamStatus(status); + } + IOUtils.closeQuietly(in); + } + return null; + } + + private MediaFile getSingleFile(HttpServletRequest request) throws ServletRequestBindingException { + String path = request.getParameter("path"); + if (path != null) { + return mediaFileService.getMediaFile(path); + } + Integer id = ServletRequestUtils.getIntParameter(request, "id"); + if (id != null) { + return mediaFileService.getMediaFile(id); + } + return null; + } + + private long getFileLength(TranscodingService.Parameters parameters) { + MediaFile file = parameters.getMediaFile(); + + if (!parameters.isDownsample() && !parameters.isTranscode()) { + return file.getFileSize(); + } + Integer duration = file.getDurationSeconds(); + Integer maxBitRate = parameters.getMaxBitRate(); + + if (duration == null) { + LOG.warn("Unknown duration for " + file + ". Unable to estimate transcoded size."); + return file.getFileSize(); + } + + if (maxBitRate == null) { + LOG.error("Unknown bit rate for " + file + ". Unable to estimate transcoded size."); + return file.getFileSize(); + } + + return duration * maxBitRate * 1000L / 8L; + } + + private LongRange getRange(HttpServletRequest request, MediaFile file) { + + // First, look for "Range" HTTP header. + LongRange range = StringUtil.parseRange(request.getHeader("Range")); + if (range != null) { + return range; + } + + // Second, look for "offsetSeconds" request parameter. + String offsetSeconds = request.getParameter("offsetSeconds"); + range = parseAndConvertOffsetSeconds(offsetSeconds, file); + if (range != null) { + return range; + } + + return null; + } + + private LongRange parseAndConvertOffsetSeconds(String offsetSeconds, MediaFile file) { + if (offsetSeconds == null) { + return null; + } + + try { + Integer duration = file.getDurationSeconds(); + Long fileSize = file.getFileSize(); + if (duration == null || fileSize == null) { + return null; + } + float offset = Float.parseFloat(offsetSeconds); + + // Convert from time offset to byte offset. + long byteOffset = (long) (fileSize * (offset / duration)); + return new LongRange(byteOffset, Long.MAX_VALUE); + + } catch (Exception x) { + LOG.error("Failed to parse and convert time offset: " + offsetSeconds, x); + return null; + } + } + + private VideoTranscodingSettings createVideoTranscodingSettings(MediaFile file, HttpServletRequest request) throws ServletRequestBindingException { + Integer existingWidth = file.getWidth(); + Integer existingHeight = file.getHeight(); + Integer maxBitRate = ServletRequestUtils.getIntParameter(request, "maxBitRate"); + int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0); + + Dimension dim = getRequestedVideoSize(request.getParameter("size")); + if (dim == null) { + dim = getSuitableVideoSize(existingWidth, existingHeight, maxBitRate); + } + + return new VideoTranscodingSettings(dim.width, dim.height, timeOffset); + } + + protected Dimension getRequestedVideoSize(String sizeSpec) { + if (sizeSpec == null) { + return null; + } + + Pattern pattern = Pattern.compile("^(\\d+)x(\\d+)$"); + Matcher matcher = pattern.matcher(sizeSpec); + if (matcher.find()) { + int w = Integer.parseInt(matcher.group(1)); + int h = Integer.parseInt(matcher.group(2)); + if (w >= 0 && h >= 0 && w <= 2000 && h <= 2000) { + return new Dimension(w, h); + } + } + return null; + } + + protected Dimension getSuitableVideoSize(Integer existingWidth, Integer existingHeight, Integer maxBitRate) { + if (maxBitRate == null) { + return new Dimension(320, 240); + } + + int w, h; + if (maxBitRate <= 600) { + w = 320; h = 240; + } else if (maxBitRate <= 1000) { + w = 480; h = 360; + } else { + w = 640; h = 480; + } + + if (existingWidth == null || existingHeight == null) { + return new Dimension(w, h); + } + + if (existingWidth < w || existingHeight < h) { + return new Dimension(even(existingWidth), even(existingHeight)); + } + + double aspectRate = existingWidth.doubleValue() / existingHeight.doubleValue(); + w = (int) Math.round(h * aspectRate); + + return new Dimension(even(w), even(h)); + } + + // Make sure width and height are multiples of two, as some versions of ffmpeg require it. + private int even(int size) { + return size + (size % 2); + } + + /** + * Feed the other end with some dummy data to keep it from reconnecting. + */ + private void sendDummy(byte[] buf, OutputStream out) throws IOException { + try { + Thread.sleep(2000); + } catch (InterruptedException x) { + LOG.warn("Interrupted in sleep.", x); + } + Arrays.fill(buf, (byte) 0xFF); + out.write(buf); + out.flush(); + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) { + this.audioScrobblerService = audioScrobblerService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java new file mode 100644 index 00000000..800aef0e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.java @@ -0,0 +1,84 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.VersionService; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Controller for the top frame. + * + * @author Sindre Mehus + */ +public class TopController extends ParameterizableViewController { + + private SettingsService settingsService; + private VersionService versionService; + private SecurityService securityService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + + List<MusicFolder> allMusicFolders = settingsService.getAllMusicFolders(); + User user = securityService.getCurrentUser(request); + + map.put("user", user); + map.put("musicFoldersExist", !allMusicFolders.isEmpty()); + map.put("brand", settingsService.getBrand()); + map.put("licensed", settingsService.isLicenseValid()); + + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + if (userSettings.isFinalVersionNotificationEnabled() && versionService.isNewFinalVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestFinalVersion()); + + } else if (userSettings.isBetaVersionNotificationEnabled() && versionService.isNewBetaVersionAvailable()) { + map.put("newVersionAvailable", true); + map.put("latestVersion", versionService.getLatestBetaVersion()); + } + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java new file mode 100644 index 00000000..8bd87408 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java @@ -0,0 +1,139 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +/** + * Controller for the page used to administrate the set of transcoding configurations. + * + * @author Sindre Mehus + */ +public class TranscodingSettingsController extends ParameterizableViewController { + + private TranscodingService transcodingService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + + if (isFormSubmission(request)) { + handleParameters(request, map); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("transcodings", transcodingService.getAllTranscodings()); + map.put("transcodeDirectory", transcodingService.getTranscodeDirectory()); + map.put("brand", settingsService.getBrand()); + + result.addObject("model", map); + return result; + } + + /** + * Determine if the given request represents a form submission. + * + * @param request current HTTP request + * @return if the request represents a form submission + */ + private boolean isFormSubmission(HttpServletRequest request) { + return "POST".equals(request.getMethod()); + } + + private void handleParameters(HttpServletRequest request, Map<String, Object> map) { + + for (Transcoding transcoding : transcodingService.getAllTranscodings()) { + Integer id = transcoding.getId(); + String name = getParameter(request, "name", id); + String sourceFormats = getParameter(request, "sourceFormats", id); + String targetFormat = getParameter(request, "targetFormat", id); + String step1 = getParameter(request, "step1", id); + String step2 = getParameter(request, "step2", id); + boolean delete = getParameter(request, "delete", id) != null; + + if (delete) { + transcodingService.deleteTranscoding(id); + } else if (name == null) { + map.put("error", "transcodingsettings.noname"); + } else if (sourceFormats == null) { + map.put("error", "transcodingsettings.nosourceformat"); + } else if (targetFormat == null) { + map.put("error", "transcodingsettings.notargetformat"); + } else if (step1 == null) { + map.put("error", "transcodingsettings.nostep1"); + } else { + transcoding.setName(name); + transcoding.setSourceFormats(sourceFormats); + transcoding.setTargetFormat(targetFormat); + transcoding.setStep1(step1); + transcoding.setStep2(step2); + transcodingService.updateTranscoding(transcoding); + } + } + + String name = StringUtils.trimToNull(request.getParameter("name")); + String sourceFormats = StringUtils.trimToNull(request.getParameter("sourceFormats")); + String targetFormat = StringUtils.trimToNull(request.getParameter("targetFormat")); + String step1 = StringUtils.trimToNull(request.getParameter("step1")); + String step2 = StringUtils.trimToNull(request.getParameter("step2")); + boolean defaultActive = request.getParameter("defaultActive") != null; + + if (name != null || sourceFormats != null || targetFormat != null || step1 != null || step2 != null) { + Transcoding transcoding = new Transcoding(null, name, sourceFormats, targetFormat, step1, step2, null, defaultActive); + if (name == null) { + map.put("error", "transcodingsettings.noname"); + } else if (sourceFormats == null) { + map.put("error", "transcodingsettings.nosourceformat"); + } else if (targetFormat == null) { + map.put("error", "transcodingsettings.notargetformat"); + } else if (step1 == null) { + map.put("error", "transcodingsettings.nostep1"); + } else { + transcodingService.createTranscoding(transcoding); + } + if (map.containsKey("error")) { + map.put("newTranscoding", transcoding); + } + } + } + + private String getParameter(HttpServletRequest request, String name, Integer id) { + return StringUtils.trimToNull(request.getParameter(name + "[" + id + "]")); + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java new file mode 100644 index 00000000..de7bf8dd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UploadController.java @@ -0,0 +1,260 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.upload.*; +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.util.*; +import org.apache.commons.fileupload.*; +import org.apache.commons.fileupload.servlet.*; +import org.apache.commons.io.*; +import org.apache.tools.zip.*; +import org.springframework.web.servlet.*; +import org.springframework.web.servlet.mvc.*; + +import javax.servlet.http.*; +import java.io.*; +import java.util.*; + +/** + * Controller which receives uploaded files. + * + * @author Sindre Mehus + */ +public class UploadController extends ParameterizableViewController { + + private static final Logger LOG = Logger.getLogger(UploadController.class); + + private SecurityService securityService; + private PlayerService playerService; + private StatusService statusService; + private SettingsService settingsService; + public static final String UPLOAD_STATUS = "uploadStatus"; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + List<File> uploadedFiles = new ArrayList<File>(); + List<File> unzippedFiles = new ArrayList<File>(); + TransferStatus status = null; + + try { + + status = statusService.createUploadStatus(playerService.getPlayer(request, response, false, false)); + status.setBytesTotal(request.getContentLength()); + + request.getSession().setAttribute(UPLOAD_STATUS, status); + + // Check that we have a file upload request + if (!ServletFileUpload.isMultipartContent(request)) { + throw new Exception("Illegal request."); + } + + File dir = null; + boolean unzip = false; + + UploadListener listener = new UploadListenerImpl(status); + + FileItemFactory factory = new MonitoredDiskFileItemFactory(listener); + ServletFileUpload upload = new ServletFileUpload(factory); + + List<?> items = upload.parseRequest(request); + + // First, look for "dir" and "unzip" parameters. + for (Object o : items) { + FileItem item = (FileItem) o; + + if (item.isFormField() && "dir".equals(item.getFieldName())) { + dir = new File(item.getString()); + } else if (item.isFormField() && "unzip".equals(item.getFieldName())) { + unzip = true; + } + } + + if (dir == null) { + throw new Exception("Missing 'dir' parameter."); + } + + // Look for file items. + for (Object o : items) { + FileItem item = (FileItem) o; + + if (!item.isFormField()) { + String fileName = item.getName(); + if (fileName.trim().length() > 0) { + + File targetFile = new File(dir, new File(fileName).getName()); + + if (!securityService.isUploadAllowed(targetFile)) { + throw new Exception("Permission denied: " + StringUtil.toHtml(targetFile.getPath())); + } + + if (!dir.exists()) { + dir.mkdirs(); + } + + item.write(targetFile); + uploadedFiles.add(targetFile); + LOG.info("Uploaded " + targetFile); + + if (unzip && targetFile.getName().toLowerCase().endsWith(".zip")) { + unzip(targetFile, unzippedFiles); + } + } + } + } + + } catch (Exception x) { + LOG.warn("Uploading failed.", x); + map.put("exception", x); + } finally { + if (status != null) { + statusService.removeUploadStatus(status); + request.getSession().removeAttribute(UPLOAD_STATUS); + User user = securityService.getCurrentUser(request); + securityService.updateUserByteCounts(user, 0L, 0L, status.getBytesTransfered()); + } + } + + map.put("uploadedFiles", uploadedFiles); + map.put("unzippedFiles", unzippedFiles); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private void unzip(File file, List<File> unzippedFiles) throws Exception { + LOG.info("Unzipping " + file); + + ZipFile zipFile = new ZipFile(file); + + try { + + Enumeration<?> entries = zipFile.getEntries(); + + while (entries.hasMoreElements()) { + ZipEntry entry = (ZipEntry) entries.nextElement(); + File entryFile = new File(file.getParentFile(), entry.getName()); + + if (!entry.isDirectory()) { + + if (!securityService.isUploadAllowed(entryFile)) { + throw new Exception("Permission denied: " + StringUtil.toHtml(entryFile.getPath())); + } + + entryFile.getParentFile().mkdirs(); + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = zipFile.getInputStream(entry); + outputStream = new FileOutputStream(entryFile); + + byte[] buf = new byte[8192]; + while (true) { + int n = inputStream.read(buf); + if (n == -1) { + break; + } + outputStream.write(buf, 0, n); + } + + LOG.info("Unzipped " + entryFile); + unzippedFiles.add(entryFile); + } finally { + IOUtils.closeQuietly(inputStream); + IOUtils.closeQuietly(outputStream); + } + } + } + + zipFile.close(); + file.delete(); + + } finally { + zipFile.close(); + } + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + /** + * Receives callbacks as the file upload progresses. + */ + private class UploadListenerImpl implements UploadListener { + private TransferStatus status; + private long start; + + private UploadListenerImpl(TransferStatus status) { + this.status = status; + start = System.currentTimeMillis(); + } + + public void start(String fileName) { + status.setFile(new File(fileName)); + } + + public void bytesRead(long bytesRead) { + + // Throttle bitrate. + + long byteCount = status.getBytesTransfered() + bytesRead; + long bitCount = byteCount * 8L; + + float elapsedMillis = Math.max(1, System.currentTimeMillis() - start); + float elapsedSeconds = elapsedMillis / 1000.0F; + long maxBitsPerSecond = getBitrateLimit(); + + status.setBytesTransfered(byteCount); + + if (maxBitsPerSecond > 0) { + float sleepMillis = 1000.0F * (bitCount / maxBitsPerSecond - elapsedSeconds); + if (sleepMillis > 0) { + try { + Thread.sleep((long) sleepMillis); + } catch (InterruptedException x) { + LOG.warn("Failed to sleep.", x); + } + } + } + } + + private long getBitrateLimit() { + return 1024L * settingsService.getUploadBitrateLimit() / Math.max(1, statusService.getAllUploadStatuses().size()); + } + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java new file mode 100644 index 00000000..0428eff8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserChartController.java @@ -0,0 +1,145 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Color; +import java.awt.GradientPaint; +import java.awt.Paint; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.jfree.chart.ChartFactory; +import org.jfree.chart.ChartUtilities; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.AxisLocation; +import org.jfree.chart.axis.CategoryAxis; +import org.jfree.chart.axis.CategoryLabelPositions; +import org.jfree.chart.axis.LogarithmicAxis; +import org.jfree.chart.plot.CategoryPlot; +import org.jfree.chart.plot.PlotOrientation; +import org.jfree.chart.renderer.category.BarRenderer; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.springframework.web.servlet.ModelAndView; + +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * Controller for generating a chart showing bitrate vs time. + * + * @author Sindre Mehus + */ +public class UserChartController extends AbstractChartController { + + private SecurityService securityService; + + public static final int IMAGE_WIDTH = 400; + public static final int IMAGE_MIN_HEIGHT = 200; + private static final long BYTES_PER_MB = 1024L * 1024L; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + String type = request.getParameter("type"); + CategoryDataset dataset = createDataset(type); + JFreeChart chart = createChart(dataset, request); + + int imageHeight = Math.max(IMAGE_MIN_HEIGHT, 15 * dataset.getColumnCount()); + + ChartUtilities.writeChartAsPNG(response.getOutputStream(), chart, IMAGE_WIDTH, imageHeight); + return null; + } + + private CategoryDataset createDataset(String type) { + DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + List<User> users = securityService.getAllUsers(); + for (User user : users) { + double value; + if ("stream".equals(type)) { + value = user.getBytesStreamed(); + } else if ("download".equals(type)) { + value = user.getBytesDownloaded(); + } else if ("upload".equals(type)) { + value = user.getBytesUploaded(); + } else if ("total".equals(type)) { + value = user.getBytesStreamed() + user.getBytesDownloaded() + user.getBytesUploaded(); + } else { + throw new RuntimeException("Illegal chart type: " + type); + } + + value /= BYTES_PER_MB; + dataset.addValue(value, "Series", user.getUsername()); + } + + return dataset; + } + + private JFreeChart createChart(CategoryDataset dataset, HttpServletRequest request) { + JFreeChart chart = ChartFactory.createBarChart(null, null, null, dataset, PlotOrientation.HORIZONTAL, false, false, false); + + CategoryPlot plot = chart.getCategoryPlot(); + Paint background = new GradientPaint(0, 0, Color.lightGray, 0, IMAGE_MIN_HEIGHT, Color.white); + plot.setBackgroundPaint(background); + plot.setDomainGridlinePaint(Color.white); + plot.setDomainGridlinesVisible(true); + plot.setRangeGridlinePaint(Color.white); + plot.setRangeAxisLocation(AxisLocation.BOTTOM_OR_LEFT); + + LogarithmicAxis rangeAxis = new LogarithmicAxis(null); + rangeAxis.setStrictValuesFlag(false); + rangeAxis.setAllowNegativesFlag(true); + plot.setRangeAxis(rangeAxis); + + // Disable bar outlines. + BarRenderer renderer = (BarRenderer) plot.getRenderer(); + renderer.setDrawBarOutline(false); + + // Set up gradient paint for series. + GradientPaint gp0 = new GradientPaint( + 0.0f, 0.0f, Color.blue, + 0.0f, 0.0f, new Color(0, 0, 64) + ); + renderer.setSeriesPaint(0, gp0); + + // Rotate labels. + CategoryAxis domainAxis = plot.getDomainAxis(); + domainAxis.setCategoryLabelPositions(CategoryLabelPositions.createUpRotationLabelPositions(Math.PI / 6.0)); + + // Set theme-specific colors. + Color bgColor = getBackground(request); + Color fgColor = getForeground(request); + + chart.setBackgroundPaint(bgColor); + + domainAxis.setTickLabelPaint(fgColor); + domainAxis.setTickMarkPaint(fgColor); + domainAxis.setAxisLinePaint(fgColor); + + rangeAxis.setTickLabelPaint(fgColor); + rangeAxis.setTickMarkPaint(fgColor); + rangeAxis.setAxisLinePaint(fgColor); + + return chart; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java new file mode 100644 index 00000000..58848840 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java @@ -0,0 +1,159 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.List; +import java.util.Date; + +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.command.*; +import org.springframework.web.servlet.mvc.*; +import org.springframework.web.bind.*; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.*; + +/** + * Controller for the page used to administrate users. + * + * @author Sindre Mehus + */ +public class UserSettingsController extends SimpleFormController { + + private SecurityService securityService; + private SettingsService settingsService; + private TranscodingService transcodingService; + + @Override + protected Object formBackingObject(HttpServletRequest request) throws Exception { + UserSettingsCommand command = new UserSettingsCommand(); + + User user = getUser(request); + if (user != null) { + command.setUser(user); + command.setEmail(user.getEmail()); + command.setAdmin(User.USERNAME_ADMIN.equals(user.getUsername())); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + command.setTranscodeSchemeName(userSettings.getTranscodeScheme().name()); + + } else { + command.setNew(true); + command.setStreamRole(true); + command.setSettingsRole(true); + } + + command.setUsers(securityService.getAllUsers()); + command.setTranscodingSupported(transcodingService.isDownsamplingSupported(null)); + command.setTranscodeDirectory(transcodingService.getTranscodeDirectory().getPath()); + command.setTranscodeSchemes(TranscodeScheme.values()); + command.setLdapEnabled(settingsService.isLdapEnabled()); + + return command; + } + + private User getUser(HttpServletRequest request) throws ServletRequestBindingException { + Integer userIndex = ServletRequestUtils.getIntParameter(request, "userIndex"); + if (userIndex != null) { + List<User> allUsers = securityService.getAllUsers(); + if (userIndex >= 0 && userIndex < allUsers.size()) { + return allUsers.get(userIndex); + } + } + return null; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + UserSettingsCommand command = (UserSettingsCommand) comm; + + if (command.isDelete()) { + deleteUser(command); + } else if (command.isNew()) { + createUser(command); + } else { + updateUser(command); + } + resetCommand(command); + } + + private void deleteUser(UserSettingsCommand command) { + securityService.deleteUser(command.getUsername()); + } + + public void createUser(UserSettingsCommand command) { + User user = new User(command.getUsername(), command.getPassword(), StringUtils.trimToNull(command.getEmail())); + user.setLdapAuthenticated(command.isLdapAuthenticated()); + securityService.createUser(user); + updateUser(command); + } + + private void updateUser(UserSettingsCommand command) { + User user = securityService.getUserByName(command.getUsername()); + user.setEmail(StringUtils.trimToNull(command.getEmail())); + user.setLdapAuthenticated(command.isLdapAuthenticated()); + user.setAdminRole(command.isAdminRole()); + user.setDownloadRole(command.isDownloadRole()); + user.setUploadRole(command.isUploadRole()); + user.setCoverArtRole(command.isCoverArtRole()); + user.setCommentRole(command.isCommentRole()); + user.setPodcastRole(command.isPodcastRole()); + user.setStreamRole(command.isStreamRole()); + user.setJukeboxRole(command.isJukeboxRole()); + user.setSettingsRole(command.isSettingsRole()); + user.setShareRole(command.isShareRole()); + + if (command.isPasswordChange()) { + user.setPassword(command.getPassword()); + } + + securityService.updateUser(user); + + UserSettings userSettings = settingsService.getUserSettings(command.getUsername()); + userSettings.setTranscodeScheme(TranscodeScheme.valueOf(command.getTranscodeSchemeName())); + userSettings.setChanged(new Date()); + settingsService.updateUserSettings(userSettings); + } + + private void resetCommand(UserSettingsCommand command) { + command.setUser(null); + command.setUsers(securityService.getAllUsers()); + command.setDelete(false); + command.setPasswordChange(false); + command.setNew(true); + command.setStreamRole(true); + command.setSettingsRole(true); + command.setPassword(null); + command.setConfirmPassword(null); + command.setEmail(null); + command.setTranscodeSchemeName(null); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java new file mode 100644 index 00000000..1d7686eb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java @@ -0,0 +1,110 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Controller for the page used to play videos. + * + * @author Sindre Mehus + */ +public class VideoPlayerController extends ParameterizableViewController { + + public static final int DEFAULT_BIT_RATE = 1000; + public static final int[] BIT_RATES = {200, 300, 400, 500, 700, 1000, 1200, 1500, 2000, 3000, 5000}; + private static final long TRIAL_DAYS = 30L; + + private MediaFileService mediaFileService; + private SettingsService settingsService; + private PlayerService playerService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map<String, Object> map = new HashMap<String, Object>(); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + + int timeOffset = ServletRequestUtils.getIntParameter(request, "timeOffset", 0); + timeOffset = Math.max(0, timeOffset); + Integer duration = file.getDurationSeconds(); + if (duration != null) { + map.put("skipOffsets", createSkipOffsets(duration)); + timeOffset = Math.min(duration, timeOffset); + duration -= timeOffset; + } + + map.put("video", file); + map.put("player", playerService.getPlayer(request, response).getId()); + map.put("maxBitRate", ServletRequestUtils.getIntParameter(request, "maxBitRate", DEFAULT_BIT_RATE)); + map.put("popout", ServletRequestUtils.getBooleanParameter(request, "popout", false)); + map.put("duration", duration); + map.put("timeOffset", timeOffset); + map.put("bitRates", BIT_RATES); + + if (!settingsService.isLicenseValid() && settingsService.getVideoTrialExpires() == null) { + Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L); + settingsService.setVideoTrialExpires(expiryDate); + settingsService.save(); + } + Date trialExpires = settingsService.getVideoTrialExpires(); + map.put("trialExpires", trialExpires); + map.put("trialExpired", trialExpires != null && trialExpires.before(new Date())); + map.put("trial", trialExpires != null && !settingsService.isLicenseValid()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public static Map<String, Integer> createSkipOffsets(int durationSeconds) { + LinkedHashMap<String, Integer> result = new LinkedHashMap<String, Integer>(); + for (int i = 0; i < durationSeconds; i += 60) { + result.put(StringUtil.formatDuration(i), i); + } + return result; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java new file mode 100644 index 00000000..02509687 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java @@ -0,0 +1,247 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.SortedSet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.MusicIndexService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Multi-controller used for wap pages. + * + * @author Sindre Mehus + */ +public class WapController extends MultiActionController { + + private SettingsService settingsService; + private PlayerService playerService; + private PlaylistService playlistService; + private SecurityService securityService; + private MusicIndexService musicIndexService; + private MediaFileService mediaFileService; + private SearchService searchService; + + public ModelAndView index(HttpServletRequest request, HttpServletResponse response) throws Exception { + return wap(request, response); + } + + public ModelAndView wap(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + List<MusicFolder> folders = settingsService.getAllMusicFolders(); + + if (folders.isEmpty()) { + map.put("noMusic", true); + } else { + + SortedMap<MusicIndex, SortedSet<MusicIndex.Artist>> allArtists = musicIndexService.getIndexedArtists(folders); + + // If an index is given as parameter, only show music files for this index. + String index = request.getParameter("index"); + if (index != null) { + SortedSet<MusicIndex.Artist> artists = allArtists.get(new MusicIndex(index)); + if (artists == null) { + map.put("noMusic", true); + } else { + map.put("artists", artists); + } + } + + // Otherwise, list all indexes. + else { + map.put("indexes", allArtists.keySet()); + } + } + + return new ModelAndView("wap/index", "model", map); + } + + public ModelAndView browse(HttpServletRequest request, HttpServletResponse response) throws Exception { + String path = request.getParameter("path"); + MediaFile parent = mediaFileService.getMediaFile(path); + + // Create array of file(s) to display. + List<MediaFile> children; + if (parent.isDirectory()) { + children = mediaFileService.getChildrenOf(parent, true, true, true); + } else { + children = new ArrayList<MediaFile>(); + children.add(parent); + } + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("parent", parent); + map.put("children", children); + map.put("user", securityService.getCurrentUser(request)); + + return new ModelAndView("wap/browse", "model", map); + } + + public ModelAndView playlist(HttpServletRequest request, HttpServletResponse response) throws Exception { + // Create array of players to control. If the "player" attribute is set for this session, + // only the player with this ID is controlled. Otherwise, all players are controlled. + List<Player> players = playerService.getAllPlayers(); + + String playerId = (String) request.getSession().getAttribute("player"); + if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + if (player != null) { + players = Arrays.asList(player); + } + } + + Map<String, Object> map = new HashMap<String, Object>(); + + for (Player player : players) { + PlayQueue playQueue = player.getPlayQueue(); + map.put("playlist", playQueue); + + if (request.getParameter("play") != null) { + MediaFile file = mediaFileService.getMediaFile(request.getParameter("play")); + playQueue.addFiles(false, file); + } else if (request.getParameter("add") != null) { + MediaFile file = mediaFileService.getMediaFile(request.getParameter("add")); + playQueue.addFiles(true, file); + } else if (request.getParameter("skip") != null) { + playQueue.setIndex(Integer.parseInt(request.getParameter("skip"))); + } else if (request.getParameter("clear") != null) { + playQueue.clear(); + } else if (request.getParameter("load") != null) { + List<MediaFile> songs = playlistService.getFilesInPlaylist(ServletRequestUtils.getIntParameter(request, "id")); + playQueue.addFiles(false, songs); + } else if (request.getParameter("random") != null) { + List<MediaFile> randomFiles = searchService.getRandomSongs(new RandomSearchCriteria(20, null, null, null, null)); + playQueue.addFiles(false, randomFiles); + } + } + + map.put("players", players); + return new ModelAndView("wap/playlist", "model", map); + } + + public ModelAndView loadPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map<String, Object> map = new HashMap<String, Object>(); + map.put("playlists", playlistService.getReadablePlaylistsForUser(securityService.getCurrentUsername(request))); + return new ModelAndView("wap/loadPlaylist", "model", map); + } + + public ModelAndView search(HttpServletRequest request, HttpServletResponse response) throws Exception { + return new ModelAndView("wap/search"); + } + + public ModelAndView searchResult(HttpServletRequest request, HttpServletResponse response) throws Exception { + String query = request.getParameter("query"); + + Map<String, Object> map = new HashMap<String, Object>(); + map.put("hits", search(query)); + + return new ModelAndView("wap/searchResult", "model", map); + } + + public ModelAndView settings(HttpServletRequest request, HttpServletResponse response) throws Exception { + String playerId = (String) request.getSession().getAttribute("player"); + + List<Player> allPlayers = playerService.getAllPlayers(); + User user = securityService.getCurrentUser(request); + List<Player> players = new ArrayList<Player>(); + Map<String, Object> map = new HashMap<String, Object>(); + + for (Player player : allPlayers) { + // Only display authorized players. + if (user.isAdminRole() || user.getUsername().equals(player.getUsername())) { + players.add(player); + } + + } + map.put("playerId", playerId); + map.put("players", players); + return new ModelAndView("wap/settings", "model", map); + } + + public ModelAndView selectPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception { + request.getSession().setAttribute("player", request.getParameter("playerId")); + return settings(request, response); + } + + private List<MediaFile> search(String query) throws IOException { + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(query); + criteria.setOffset(0); + criteria.setCount(50); + + SearchResult result = searchService.search(criteria, SearchService.IndexType.SONG); + return result.getMediaFiles(); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java new file mode 100644 index 00000000..de17f4d4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java @@ -0,0 +1,127 @@ +/* + 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.dao; + +import java.util.Date; +import java.util.List; + +import org.springframework.jdbc.core.*; + +import net.sourceforge.subsonic.Logger; + +/** + * Abstract superclass for all DAO's. + * + * @author Sindre Mehus + */ +public class AbstractDao { + private static final Logger LOG = Logger.getLogger(AbstractDao.class); + + private DaoHelper daoHelper; + + /** + * Returns a JDBC template for performing database operations. + * @return A JDBC template. + */ + public JdbcTemplate getJdbcTemplate() { + return daoHelper.getJdbcTemplate(); + } + + protected String questionMarks(String columns) { + int count = columns.split(", ").length; + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < count; i++) { + builder.append('?'); + if (i < count - 1) { + builder.append(", "); + } + } + return builder.toString(); + } + + protected String prefix(String columns, String prefix) { + StringBuilder builder = new StringBuilder(); + for (String s : columns.split(", ")) { + builder.append(prefix).append(".").append(s).append(","); + } + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + return builder.toString(); + } + + protected int update(String sql, Object... args) { + long t = System.nanoTime(); + int result = getJdbcTemplate().update(sql, args); + log(sql, t); + return result; + } + + private void log(String sql, long startTimeNano) { +// long micros = (System.nanoTime() - startTimeNano) / 1000L; +// LOG.debug(micros + " " + sql); + } + + protected <T> List<T> query(String sql, RowMapper rowMapper, Object... args) { + long t = System.nanoTime(); + List<T> result = getJdbcTemplate().query(sql, args, rowMapper); + log(sql, t); + return result; + } + + protected List<String> queryForStrings(String sql, Object... args) { + long t = System.nanoTime(); + List<String> result = getJdbcTemplate().queryForList(sql, args, String.class); + log(sql, t); + return result; + } + + protected Integer queryForInt(String sql, Integer defaultValue, Object... args) { + long t = System.nanoTime(); + List<Integer> list = getJdbcTemplate().queryForList(sql, args, Integer.class); + Integer result = list.isEmpty() ? defaultValue : list.get(0) == null ? defaultValue : list.get(0); + log(sql, t); + return result; + } + + protected Date queryForDate(String sql, Date defaultValue, Object... args) { + long t = System.nanoTime(); + List<Date> list = getJdbcTemplate().queryForList(sql, args, Date.class); + Date result = list.isEmpty() ? defaultValue : list.get(0) == null ? defaultValue : list.get(0); + log(sql, t); + return result; + } + + protected Long queryForLong(String sql, Long defaultValue, Object... args) { + long t = System.nanoTime(); + List<Long> list = getJdbcTemplate().queryForList(sql, args, Long.class); + Long result = list.isEmpty() ? defaultValue : list.get(0) == null ? defaultValue : list.get(0); + log(sql, t); + return result; + } + + protected <T> T queryOne(String sql, RowMapper rowMapper, Object... args) { + List<T> list = query(sql, rowMapper, args); + return list.isEmpty() ? null : list.get(0); + } + + public void setDaoHelper(DaoHelper daoHelper) { + this.daoHelper = daoHelper; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java new file mode 100644 index 00000000..603f6dad --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java @@ -0,0 +1,243 @@ +/* + 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.dao; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.MediaFile; +import org.apache.commons.lang.ObjectUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +/** + * Provides database services for albums. + * + * @author Sindre Mehus + */ +public class AlbumDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(AlbumDao.class); + private static final String COLUMNS = "id, path, name, artist, song_count, duration_seconds, cover_art_path, " + + "play_count, last_played, comment, created, last_scanned, present"; + + private final RowMapper rowMapper = new AlbumMapper(); + + /** + * Returns the album with the given artist and album name. + * + * @param artistName The artist name. + * @param albumName The album name. + * @return The album or null. + */ + public Album getAlbum(String artistName, String albumName) { + return queryOne("select " + COLUMNS + " from album where artist=? and name=?", rowMapper, artistName, albumName); + } + + /** + * Returns the album that the given file (most likely) is part of. + * + * @param file The media file. + * @return The album or null. + */ + public Album getAlbumForFile(MediaFile file) { + + // First, get all albums with the correct album name (irrespective of artist). + List<Album> candidates = query("select " + COLUMNS + " from album where name=?", rowMapper, file.getAlbumName()); + if (candidates.isEmpty()) { + return null; + } + + // Look for album with the correct artist. + for (Album candidate : candidates) { + if (ObjectUtils.equals(candidate.getArtist(), file.getArtist())) { + return candidate; + } + } + + // Look for album with the same path as the file. + for (Album candidate : candidates) { + if (ObjectUtils.equals(candidate.getPath(), file.getParentPath())) { + return candidate; + } + } + + // No appropriate album found. + return null; + } + + public Album getAlbum(int id) { + return queryOne("select " + COLUMNS + " from album where id=?", rowMapper, id); + } + + public List<Album> getAlbumsForArtist(String artist) { + return query("select " + COLUMNS + " from album where artist=? and present order by name", rowMapper, artist); + } + + /** + * Creates or updates an album. + * + * @param album The album to create/update. + */ + public synchronized void createOrUpdateAlbum(Album album) { + String sql = "update album set " + + "song_count=?," + + "duration_seconds=?," + + "cover_art_path=?," + + "play_count=?," + + "last_played=?," + + "comment=?," + + "created=?," + + "last_scanned=?," + + "present=? " + + "where artist=? and name=?"; + + int n = update(sql, album.getSongCount(), album.getDurationSeconds(), album.getCoverArtPath(), album.getPlayCount(), album.getLastPlayed(), + album.getComment(), album.getCreated(), album.getLastScanned(), album.isPresent(), album.getArtist(), album.getName()); + + if (n == 0) { + + update("insert into album (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null, album.getPath(), album.getName(), album.getArtist(), + album.getSongCount(), album.getDurationSeconds(), album.getCoverArtPath(), album.getPlayCount(), album.getLastPlayed(), + album.getComment(), album.getCreated(), album.getLastScanned(), album.isPresent()); + } + + int id = queryForInt("select id from album where artist=? and name=?", null, album.getArtist(), album.getName()); + album.setId(id); + } + + /** + * Returns albums in alphabetical order. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param byArtist Whether to sort by artist name + * @return Albums in alphabetical order. + */ + public List<Album> getAlphabetialAlbums(int offset, int count, boolean byArtist) { + String orderBy = byArtist ? "artist, name" : "name"; + return query("select " + COLUMNS + " from album where present order by " + orderBy + " limit ? offset ?", rowMapper, count, offset); + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most frequently played albums. + */ + public List<Album> getMostFrequentlyPlayedAlbums(int offset, int count) { + return query("select " + COLUMNS + " from album where play_count > 0 and present " + + "order by play_count desc limit ? offset ?", rowMapper, count, offset); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently played albums. + */ + public List<Album> getMostRecentlyPlayedAlbums(int offset, int count) { + return query("select " + COLUMNS + " from album where last_played is not null and present " + + "order by last_played desc limit ? offset ?", rowMapper, count, offset); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently added albums. + */ + public List<Album> getNewestAlbums(int offset, int count) { + return query("select " + COLUMNS + " from album where present order by created desc limit ? offset ?", + rowMapper, count, offset); + } + + /** + * Returns the most recently starred albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param username Returns albums starred by this user. + * @return The most recently starred albums for this user. + */ + public List<Album> getStarredAlbums(int offset, int count, String username) { + return query("select " + prefix(COLUMNS, "album") + " from album, starred_album where album.id = starred_album.album_id and " + + "album.present and starred_album.username=? order by starred_album.created desc limit ? offset ?", + rowMapper, username, count, offset); + } + + public void markNonPresent(Date lastScanned) { + int minId = queryForInt("select id from album where true limit 1", 0); + int maxId = queryForInt("select max(id) from album", 0); + + final int batchSize = 1000; + for (int id = minId; id <= maxId; id += batchSize) { + update("update album set present=false where id between ? and ? and last_scanned != ? and present", id, id + batchSize, lastScanned); + } + } + + public void expunge() { + int minId = queryForInt("select id from album where true limit 1", 0); + int maxId = queryForInt("select max(id) from album", 0); + + final int batchSize = 1000; + for (int id = minId; id <= maxId; id += batchSize) { + update("delete from album where id between ? and ? and not present", id, id + batchSize); + } + } + + public void starAlbum(int albumId, String username) { + unstarAlbum(albumId, username); + update("insert into starred_album(album_id, username, created) values (?,?,?)", albumId, username, new Date()); + } + + public void unstarAlbum(int albumId, String username) { + update("delete from starred_album where album_id=? and username=?", albumId, username); + } + + public Date getAlbumStarredDate(int albumId, String username) { + return queryForDate("select created from starred_album where album_id=? and username=?", null, albumId, username); + } + + private static class AlbumMapper implements ParameterizedRowMapper<Album> { + public Album mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Album( + rs.getInt(1), + rs.getString(2), + rs.getString(3), + rs.getString(4), + rs.getInt(5), + rs.getInt(6), + rs.getString(7), + rs.getInt(8), + rs.getTimestamp(9), + rs.getString(10), + rs.getTimestamp(11), + rs.getTimestamp(12), + rs.getBoolean(13)); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java new file mode 100644 index 00000000..41d57c33 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.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.dao; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Artist; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +/** + * Provides database services for artists. + * + * @author Sindre Mehus + */ +public class ArtistDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(ArtistDao.class); + private static final String COLUMNS = "id, name, cover_art_path, album_count, last_scanned, present"; + + private final RowMapper rowMapper = new ArtistMapper(); + + /** + * Returns the artist with the given name. + * + * @param artistName The artist name. + * @return The artist or null. + */ + public Artist getArtist(String artistName) { + return queryOne("select " + COLUMNS + " from artist where name=?", rowMapper, artistName); + } + + /** + * Returns the artist with the given ID. + * + * @param id The artist ID. + * @return The artist or null. + */ + public Artist getArtist(int id) { + return queryOne("select " + COLUMNS + " from artist where id=?", rowMapper, id); + } + + /** + * Creates or updates an artist. + * + * @param artist The artist to create/update. + */ + public synchronized void createOrUpdateArtist(Artist artist) { + String sql = "update artist set " + + "cover_art_path=?," + + "album_count=?," + + "last_scanned=?," + + "present=? " + + "where name=?"; + + int n = update(sql, artist.getCoverArtPath(), artist.getAlbumCount(), artist.getLastScanned(), artist.isPresent(), artist.getName()); + + if (n == 0) { + + update("insert into artist (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null, + artist.getName(), artist.getCoverArtPath(), artist.getAlbumCount(), artist.getLastScanned(), artist.isPresent()); + } + + int id = queryForInt("select id from artist where name=?", null, artist.getName()); + artist.setId(id); + } + + /** + * Returns artists in alphabetical order. + * + * @param offset Number of artists to skip. + * @param count Maximum number of artists to return. + * @return Artists in alphabetical order. + */ + public List<Artist> getAlphabetialArtists(int offset, int count) { + return query("select " + COLUMNS + " from artist where present order by name limit ? offset ?", rowMapper, count, offset); + } + + /** + * Returns the most recently starred artists. + * + * @param offset Number of artists to skip. + * @param count Maximum number of artists to return. + * @param username Returns artists starred by this user. + * @return The most recently starred artists for this user. + */ + public List<Artist> getStarredArtists(int offset, int count, String username) { + return query("select " + prefix(COLUMNS, "artist") + " from artist, starred_artist where artist.id = starred_artist.artist_id and " + + "artist.present and starred_artist.username=? order by starred_artist.created desc limit ? offset ?", + rowMapper, username, count, offset); + } + + public void markPresent(String artistName, Date lastScanned) { + update("update artist set present=?, last_scanned=? where name=?", true, lastScanned, artistName); + } + + public void markNonPresent(Date lastScanned) { + int minId = queryForInt("select id from artist where true limit 1", 0); + int maxId = queryForInt("select max(id) from artist", 0); + + final int batchSize = 1000; + for (int id = minId; id <= maxId; id += batchSize) { + update("update artist set present=false where id between ? and ? and last_scanned != ? and present", id, id + batchSize, lastScanned); + } + } + + public void expunge() { + int minId = queryForInt("select id from artist where true limit 1", 0); + int maxId = queryForInt("select max(id) from artist", 0); + + final int batchSize = 1000; + for (int id = minId; id <= maxId; id += batchSize) { + update("delete from artist where id between ? and ? and not present", id, id + batchSize); + } + } + + public void starArtist(int artistId, String username) { + unstarArtist(artistId, username); + update("insert into starred_artist(artist_id, username, created) values (?,?,?)", artistId, username, new Date()); + } + + public void unstarArtist(int artistId, String username) { + update("delete from starred_artist where artist_id=? and username=?", artistId, username); + } + + public Date getArtistStarredDate(int artistId, String username) { + return queryForDate("select created from starred_artist where artist_id=? and username=?", null, artistId, username); + } + + private static class ArtistMapper implements ParameterizedRowMapper<Artist> { + public Artist mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Artist( + rs.getInt(1), + rs.getString(2), + rs.getString(3), + rs.getInt(4), + rs.getTimestamp(5), + rs.getBoolean(6)); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java new file mode 100644 index 00000000..abdc118d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AvatarDao.java @@ -0,0 +1,94 @@ +/* + 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.dao; + +import net.sourceforge.subsonic.domain.Avatar; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +/** + * Provides database services for avatars. + * + * @author Sindre Mehus + */ +public class AvatarDao extends AbstractDao { + + private static final String COLUMNS = "id, name, created_date, mime_type, width, height, data"; + private final AvatarRowMapper rowMapper = new AvatarRowMapper(); + + /** + * Returns all system avatars. + * + * @return All system avatars. + */ + public List<Avatar> getAllSystemAvatars() { + String sql = "select " + COLUMNS + " from system_avatar"; + return query(sql, rowMapper); + } + + /** + * Returns the system avatar with the given ID. + * + * @param id The system avatar ID. + * @return The avatar or <code>null</code> if not found. + */ + public Avatar getSystemAvatar(int id) { + String sql = "select " + COLUMNS + " from system_avatar where id=" + id; + return queryOne(sql, rowMapper); + } + + /** + * Returns the custom avatar for the given user. + * + * @param username The username. + * @return The avatar or <code>null</code> if not found. + */ + public Avatar getCustomAvatar(String username) { + String sql = "select " + COLUMNS + " from custom_avatar where username=?"; + return queryOne(sql, rowMapper, username); + } + + /** + * Sets the custom avatar for the given user. + * + * @param avatar The avatar, or <code>null</code> to remove the avatar. + * @param username The username. + */ + public void setCustomAvatar(Avatar avatar, String username) { + String sql = "delete from custom_avatar where username=?"; + update(sql, username); + + if (avatar != null) { + update("insert into custom_avatar(" + COLUMNS + ", username) values(" + questionMarks(COLUMNS) + ", ?)", + null, avatar.getName(), avatar.getCreatedDate(), avatar.getMimeType(), + avatar.getWidth(), avatar.getHeight(), avatar.getData(), username); + } + } + + private static class AvatarRowMapper implements ParameterizedRowMapper<Avatar> { + public Avatar mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Avatar(rs.getInt(1), rs.getString(2), rs.getTimestamp(3), rs.getString(4), + rs.getInt(5), rs.getInt(6), rs.getBytes(7)); + } + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java new file mode 100644 index 00000000..802a5b3d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.java @@ -0,0 +1,117 @@ +/* + 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.dao; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; +import net.sourceforge.subsonic.dao.schema.Schema25; +import net.sourceforge.subsonic.dao.schema.Schema26; +import net.sourceforge.subsonic.dao.schema.Schema27; +import net.sourceforge.subsonic.dao.schema.Schema28; +import net.sourceforge.subsonic.dao.schema.Schema29; +import net.sourceforge.subsonic.dao.schema.Schema30; +import net.sourceforge.subsonic.dao.schema.Schema31; +import net.sourceforge.subsonic.dao.schema.Schema32; +import net.sourceforge.subsonic.dao.schema.Schema33; +import net.sourceforge.subsonic.dao.schema.Schema34; +import net.sourceforge.subsonic.dao.schema.Schema35; +import net.sourceforge.subsonic.dao.schema.Schema36; +import net.sourceforge.subsonic.dao.schema.Schema37; +import net.sourceforge.subsonic.dao.schema.Schema38; +import net.sourceforge.subsonic.dao.schema.Schema40; +import net.sourceforge.subsonic.dao.schema.Schema43; +import net.sourceforge.subsonic.dao.schema.Schema45; +import net.sourceforge.subsonic.dao.schema.Schema46; +import net.sourceforge.subsonic.dao.schema.Schema47; +import net.sourceforge.subsonic.service.SettingsService; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import javax.sql.DataSource; +import java.io.File; + +/** + * DAO helper class which creates the data source, and updates the database schema. + * + * @author Sindre Mehus + */ +public class DaoHelper { + + private static final Logger LOG = Logger.getLogger(DaoHelper.class); + + private Schema[] schemas = {new Schema25(), new Schema26(), new Schema27(), new Schema28(), new Schema29(), + new Schema30(), new Schema31(), new Schema32(), new Schema33(), new Schema34(), + new Schema35(), new Schema36(), new Schema37(), new Schema38(), new Schema40(), + new Schema43(), new Schema45(), new Schema46(), new Schema47()}; + private DataSource dataSource; + private static boolean shutdownHookAdded; + + public DaoHelper() { + dataSource = createDataSource(); + checkDatabase(); + addShutdownHook(); + } + + private void addShutdownHook() { + if (shutdownHookAdded) { + return; + } + shutdownHookAdded = true; + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.err.println("Shutting down database."); + getJdbcTemplate().execute("shutdown"); + System.err.println("Done."); + } + }); + } + + /** + * Returns a JDBC template for performing database operations. + * + * @return A JDBC template. + */ + public JdbcTemplate getJdbcTemplate() { + return new JdbcTemplate(dataSource); + } + + private DataSource createDataSource() { + File subsonicHome = SettingsService.getSubsonicHome(); + DriverManagerDataSource ds = new DriverManagerDataSource(); + ds.setDriverClassName("org.hsqldb.jdbcDriver"); + ds.setUrl("jdbc:hsqldb:file:" + subsonicHome.getPath() + "/db/subsonic"); + ds.setUsername("sa"); + ds.setPassword(""); + + return ds; + } + + private void checkDatabase() { + LOG.info("Checking database schema."); + try { + for (Schema schema : schemas) { + schema.execute(getJdbcTemplate()); + } + LOG.info("Done checking database schema."); + } catch (Exception x) { + LOG.error("Failed to initialize database.", x); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.java new file mode 100644 index 00000000..c3c20a74 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/InternetRadioDao.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.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.InternetRadio; + +/** + * Provides database services for internet radio. + * + * @author Sindre Mehus + */ +public class InternetRadioDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(InternetRadioDao.class); + private static final String COLUMNS = "id, name, stream_url, homepage_url, enabled, changed"; + private final InternetRadioRowMapper rowMapper = new InternetRadioRowMapper(); + + /** + * Returns all internet radio stations. + * + * @return Possibly empty list of all internet radio stations. + */ + public List<InternetRadio> getAllInternetRadios() { + String sql = "select " + COLUMNS + " from internet_radio"; + return query(sql, rowMapper); + } + + /** + * Creates a new internet radio station. + * + * @param radio The internet radio station to create. + */ + public void createInternetRadio(InternetRadio radio) { + String sql = "insert into internet_radio (" + COLUMNS + ") values (null, ?, ?, ?, ?, ?)"; + update(sql, radio.getName(), radio.getStreamUrl(), radio.getHomepageUrl(), radio.isEnabled(), radio.getChanged()); + LOG.info("Created internet radio station " + radio.getName()); + } + + /** + * Deletes the internet radio station with the given ID. + * + * @param id The internet radio station ID. + */ + public void deleteInternetRadio(Integer id) { + String sql = "delete from internet_radio where id=?"; + update(sql, id); + LOG.info("Deleted internet radio station with ID " + id); + } + + /** + * Updates the given internet radio station. + * + * @param radio The internet radio station to update. + */ + public void updateInternetRadio(InternetRadio radio) { + String sql = "update internet_radio set name=?, stream_url=?, homepage_url=?, enabled=?, changed=? where id=?"; + update(sql, radio.getName(), radio.getStreamUrl(), radio.getHomepageUrl(), radio.isEnabled(), radio.getChanged(), radio.getId()); + } + + private static class InternetRadioRowMapper implements ParameterizedRowMapper<InternetRadio> { + public InternetRadio mapRow(ResultSet rs, int rowNum) throws SQLException { + return new InternetRadio(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getBoolean(5), rs.getTimestamp(6)); + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java new file mode 100644 index 00000000..e75bc7a6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java @@ -0,0 +1,374 @@ +/* + 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.dao; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Date; +import java.util.List; + +import static net.sourceforge.subsonic.domain.MediaFile.MediaType; +import static net.sourceforge.subsonic.domain.MediaFile.MediaType.*; + +/** + * Provides database services for media files. + * + * @author Sindre Mehus + */ +public class MediaFileDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(MediaFileDao.class); + private static final String COLUMNS = "id, path, folder, type, format, title, album, artist, album_artist, disc_number, " + + "track_number, year, genre, bit_rate, variable_bit_rate, duration_seconds, file_size, width, height, cover_art_path, " + + "parent_path, play_count, last_played, comment, created, changed, last_scanned, children_last_updated, present, version"; + + private static final int VERSION = 1; + + private final RowMapper rowMapper = new MediaFileMapper(); + private final RowMapper musicFileInfoRowMapper = new MusicFileInfoMapper(); + + /** + * Returns the media file for the given path. + * + * @param path The path. + * @return The media file or null. + */ + public MediaFile getMediaFile(String path) { + return queryOne("select " + COLUMNS + " from media_file where path=?", rowMapper, path); + } + + /** + * Returns the media file for the given ID. + * + * @param id The ID. + * @return The media file or null. + */ + public MediaFile getMediaFile(int id) { + return queryOne("select " + COLUMNS + " from media_file where id=?", rowMapper, id); + } + + /** + * Returns the media file that are direct children of the given path. + * + * @param path The path. + * @return The list of children. + */ + public List<MediaFile> getChildrenOf(String path) { + return query("select " + COLUMNS + " from media_file where parent_path=? and present", rowMapper, path); + } + + public List<MediaFile> getFilesInPlaylist(int playlistId) { + return query("select " + prefix(COLUMNS, "media_file") + " from media_file, playlist_file where " + + "media_file.id = playlist_file.media_file_id and " + + "playlist_file.playlist_id = ? and " + + "media_file.present order by playlist_file.id", rowMapper, playlistId); + } + + public List<MediaFile> getSongsForAlbum(String artist, String album) { + return query("select " + COLUMNS + " from media_file where album_artist=? and album=? and present and type in (?,?,?) order by track_number", rowMapper, + artist, album, MUSIC.name(), AUDIOBOOK.name(), PODCAST.name()); + } + + public List<MediaFile> getVideos(int size, int offset) { + return query("select " + COLUMNS + " from media_file where type=? and present order by title limit ? offset ?", rowMapper, + VIDEO.name(), size, offset); + } + + /** + * Creates or updates a media file. + * + * @param file The media file to create/update. + */ + public synchronized void createOrUpdateMediaFile(MediaFile file) { + String sql = "update media_file set " + + "folder=?," + + "type=?," + + "format=?," + + "title=?," + + "album=?," + + "artist=?," + + "album_artist=?," + + "disc_number=?," + + "track_number=?," + + "year=?," + + "genre=?," + + "bit_rate=?," + + "variable_bit_rate=?," + + "duration_seconds=?," + + "file_size=?," + + "width=?," + + "height=?," + + "cover_art_path=?," + + "parent_path=?," + + "play_count=?," + + "last_played=?," + + "comment=?," + + "changed=?," + + "last_scanned=?," + + "children_last_updated=?," + + "present=?, " + + "version=? " + + "where path=?"; + + int n = update(sql, + file.getFolder(), file.getMediaType().name(), file.getFormat(), file.getTitle(), file.getAlbumName(), file.getArtist(), + file.getAlbumArtist(), file.getDiscNumber(), file.getTrackNumber(), file.getYear(), file.getGenre(), file.getBitRate(), + file.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(), + file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(), + file.getChanged(), file.getLastScanned(), file.getChildrenLastUpdated(), file.isPresent(), VERSION, file.getPath()); + + if (n == 0) { + + // Copy values from obsolete table music_file_info. + MediaFile musicFileInfo = getMusicFileInfo(file.getPath()); + if (musicFileInfo != null) { + file.setComment(musicFileInfo.getComment()); + file.setLastPlayed(musicFileInfo.getLastPlayed()); + file.setPlayCount(musicFileInfo.getPlayCount()); + } + + update("insert into media_file (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null, + file.getPath(), file.getFolder(), file.getMediaType().name(), file.getFormat(), file.getTitle(), file.getAlbumName(), file.getArtist(), + file.getAlbumArtist(), file.getDiscNumber(), file.getTrackNumber(), file.getYear(), file.getGenre(), file.getBitRate(), + file.isVariableBitRate(), file.getDurationSeconds(), file.getFileSize(), file.getWidth(), file.getHeight(), + file.getCoverArtPath(), file.getParentPath(), file.getPlayCount(), file.getLastPlayed(), file.getComment(), + file.getCreated(), file.getChanged(), file.getLastScanned(), + file.getChildrenLastUpdated(), file.isPresent(), VERSION); + } + + int id = queryForInt("select id from media_file where path=?", null, file.getPath()); + file.setId(id); + } + + private MediaFile getMusicFileInfo(String path) { + return queryOne("select play_count, last_played, comment from music_file_info where path=?", musicFileInfoRowMapper, path); + } + + @Deprecated + public List<String> getArtists() { + return queryForStrings("select distinct artist from media_file where artist is not null and present order by artist"); + } + + public void deleteMediaFile(String path) { + update("update media_file set present=false, children_last_updated=? where path=?", new Date(0L), path); + } + + public List<String> getGenres() { + return queryForStrings("select distinct genre from media_file where genre is not null and present order by genre"); + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most frequently played albums. + */ + public List<MediaFile> getMostFrequentlyPlayedAlbums(int offset, int count) { + return query("select " + COLUMNS + " from media_file where type=? and play_count > 0 and present " + + "order by play_count desc limit ? offset ?", rowMapper, ALBUM.name(), count, offset); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently played albums. + */ + public List<MediaFile> getMostRecentlyPlayedAlbums(int offset, int count) { + return query("select " + COLUMNS + " from media_file where type=? and last_played is not null and present " + + "order by last_played desc limit ? offset ?", rowMapper, ALBUM.name(), count, offset); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently added albums. + */ + public List<MediaFile> getNewestAlbums(int offset, int count) { + return query("select " + COLUMNS + " from media_file where type=? and present order by created desc limit ? offset ?", + rowMapper, ALBUM.name(), count, offset); + } + + /** + * Returns albums in alphabetical order. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param byArtist Whether to sort by artist name + * @return Albums in alphabetical order. + */ + public List<MediaFile> getAlphabetialAlbums(int offset, int count, boolean byArtist) { + String orderBy = byArtist ? "artist, album" : "album"; + return query("select " + COLUMNS + " from media_file where type=? and artist != '' and present order by " + orderBy + " limit ? offset ?", + rowMapper, ALBUM.name(), count, offset); + } + + /** + * Returns the most recently starred albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param username Returns albums starred by this user. + * @return The most recently starred albums for this user. + */ + public List<MediaFile> getStarredAlbums(int offset, int count, String username) { + return query("select " + prefix(COLUMNS, "media_file") + " from media_file, starred_media_file where media_file.id = starred_media_file.media_file_id and " + + "media_file.present and media_file.type=? and starred_media_file.username=? order by starred_media_file.created desc limit ? offset ?", + rowMapper, ALBUM.name(), username, count, offset); + } + + /** + * Returns the most recently starred directories. + * + * @param offset Number of directories to skip. + * @param count Maximum number of directories to return. + * @param username Returns directories starred by this user. + * @return The most recently starred directories for this user. + */ + public List<MediaFile> getStarredDirectories(int offset, int count, String username) { + return query("select " + prefix(COLUMNS, "media_file") + " from media_file, starred_media_file where media_file.id = starred_media_file.media_file_id and " + + "media_file.present and media_file.type=? and starred_media_file.username=? order by starred_media_file.created desc limit ? offset ?", + rowMapper, DIRECTORY.name(), username, count, offset); + } + + /** + * Returns the most recently starred files. + * + * @param offset Number of files to skip. + * @param count Maximum number of files to return. + * @param username Returns files starred by this user. + * @return The most recently starred files for this user. + */ + public List<MediaFile> getStarredFiles(int offset, int count, String username) { + return query("select " + prefix(COLUMNS, "media_file") + " from media_file, starred_media_file where media_file.id = starred_media_file.media_file_id and " + + "media_file.present and media_file.type in (?,?,?,?) and starred_media_file.username=? order by starred_media_file.created desc limit ? offset ?", + rowMapper, MUSIC.name(), PODCAST.name(), AUDIOBOOK.name(), VIDEO.name(), username, count, offset); + } + + public void starMediaFile(int id, String username) { + unstarMediaFile(id, username); + update("insert into starred_media_file(media_file_id, username, created) values (?,?,?)", id, username, new Date()); + } + + public void unstarMediaFile(int id, String username) { + update("delete from starred_media_file where media_file_id=? and username=?", id, username); + } + + public Date getMediaFileStarredDate(int id, String username) { + return queryForDate("select created from starred_media_file where media_file_id=? and username=?", null, id, username); + } + + /** + * Returns media library statistics, including the number of artists, albums and songs. + * + * @return Media library statistics. + */ + public MediaLibraryStatistics getStatistics() { + int artistCount = queryForInt("select count(1) from artist where present", 0); + int albumCount = queryForInt("select count(1) from album where present", 0); + int songCount = queryForInt("select count(1) from media_file where type in (?, ?, ?, ?) and present", 0, VIDEO.name(), MUSIC.name(), AUDIOBOOK.name(), PODCAST.name()); + long totalLengthInBytes = queryForLong("select sum(file_size) from media_file where present", 0L); + long totalDurationInSeconds = queryForLong("select sum(duration_seconds) from media_file where present", 0L); + + return new MediaLibraryStatistics(artistCount, albumCount, songCount, totalLengthInBytes, totalDurationInSeconds); + } + + public void markPresent(String path, Date lastScanned) { + update("update media_file set present=?, last_scanned=? where path=?", true, lastScanned, path); + } + + public void markNonPresent(Date lastScanned) { + int minId = queryForInt("select id from media_file where true limit 1", 0); + int maxId = queryForInt("select max(id) from media_file", 0); + + final int batchSize = 1000; + Date childrenLastUpdated = new Date(0L); // Used to force a children rescan if file is later resurrected. + for (int id = minId; id <= maxId; id += batchSize) { + update("update media_file set present=false, children_last_updated=? where id between ? and ? and last_scanned != ? and present", + childrenLastUpdated, id, id + batchSize, lastScanned); + } + } + + public void expunge() { + int minId = queryForInt("select id from media_file where true limit 1", 0); + int maxId = queryForInt("select max(id) from media_file", 0); + + final int batchSize = 1000; + for (int id = minId; id <= maxId; id += batchSize) { + update("delete from media_file where id between ? and ? and not present", id, id + batchSize); + } + update("checkpoint"); + } + + private static class MediaFileMapper implements ParameterizedRowMapper<MediaFile> { + public MediaFile mapRow(ResultSet rs, int rowNum) throws SQLException { + return new MediaFile( + rs.getInt(1), + rs.getString(2), + rs.getString(3), + MediaType.valueOf(rs.getString(4)), + rs.getString(5), + rs.getString(6), + rs.getString(7), + rs.getString(8), + rs.getString(9), + rs.getInt(10) == 0 ? null : rs.getInt(10), + rs.getInt(11) == 0 ? null : rs.getInt(11), + rs.getInt(12) == 0 ? null : rs.getInt(12), + rs.getString(13), + rs.getInt(14) == 0 ? null : rs.getInt(14), + rs.getBoolean(15), + rs.getInt(16) == 0 ? null : rs.getInt(16), + rs.getLong(17) == 0 ? null : rs.getLong(17), + rs.getInt(18) == 0 ? null : rs.getInt(18), + rs.getInt(19) == 0 ? null : rs.getInt(19), + rs.getString(20), + rs.getString(21), + rs.getInt(22), + rs.getTimestamp(23), + rs.getString(24), + rs.getTimestamp(25), + rs.getTimestamp(26), + rs.getTimestamp(27), + rs.getTimestamp(28), + rs.getBoolean(29)); + } + } + + private static class MusicFileInfoMapper implements ParameterizedRowMapper<MediaFile> { + public MediaFile mapRow(ResultSet rs, int rowNum) throws SQLException { + MediaFile file = new MediaFile(); + file.setPlayCount(rs.getInt(1)); + file.setLastPlayed(rs.getTimestamp(2)); + file.setComment(rs.getString(3)); + return file; + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.java new file mode 100644 index 00000000..a5205d71 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.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.dao; + +import java.io.File; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MusicFolder; + +/** + * Provides database services for music folders. + * + * @author Sindre Mehus + */ +public class MusicFolderDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(MusicFolderDao.class); + private static final String COLUMNS = "id, path, name, enabled, changed"; + private final MusicFolderRowMapper rowMapper = new MusicFolderRowMapper(); + + /** + * Returns all music folders. + * + * @return Possibly empty list of all music folders. + */ + public List<MusicFolder> getAllMusicFolders() { + String sql = "select " + COLUMNS + " from music_folder"; + return query(sql, rowMapper); + } + + /** + * Creates a new music folder. + * + * @param musicFolder The music folder to create. + */ + public void createMusicFolder(MusicFolder musicFolder) { + String sql = "insert into music_folder (" + COLUMNS + ") values (null, ?, ?, ?, ?)"; + update(sql, musicFolder.getPath(), musicFolder.getName(), musicFolder.isEnabled(), musicFolder.getChanged()); + LOG.info("Created music folder " + musicFolder.getPath()); + } + + /** + * Deletes the music folder with the given ID. + * + * @param id The music folder ID. + */ + public void deleteMusicFolder(Integer id) { + String sql = "delete from music_folder where id=?"; + update(sql, id); + LOG.info("Deleted music folder with ID " + id); + } + + /** + * Updates the given music folder. + * + * @param musicFolder The music folder to update. + */ + public void updateMusicFolder(MusicFolder musicFolder) { + String sql = "update music_folder set path=?, name=?, enabled=?, changed=? where id=?"; + update(sql, musicFolder.getPath().getPath(), musicFolder.getName(), + musicFolder.isEnabled(), musicFolder.getChanged(), musicFolder.getId()); + } + + private static class MusicFolderRowMapper implements ParameterizedRowMapper<MusicFolder> { + public MusicFolder mapRow(ResultSet rs, int rowNum) throws SQLException { + return new MusicFolder(rs.getInt(1), new File(rs.getString(2)), rs.getString(3), rs.getBoolean(4), rs.getTimestamp(5)); + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java new file mode 100644 index 00000000..f129fa37 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.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.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Calendar; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.TranscodeScheme; + +/** + * Provides player-related database services. + * + * @author Sindre Mehus + */ +public class PlayerDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(PlayerDao.class); + private static final String COLUMNS = "id, name, type, username, ip_address, auto_control_enabled, " + + "last_seen, cover_art_scheme, transcode_scheme, dynamic_ip, technology, client_id"; + + private PlayerRowMapper rowMapper = new PlayerRowMapper(); + private Map<String, PlayQueue> playlists = Collections.synchronizedMap(new HashMap<String, PlayQueue>()); + + /** + * Returns all players. + * + * @return Possibly empty list of all users. + */ + public List<Player> getAllPlayers() { + String sql = "select " + COLUMNS + " from player"; + return query(sql, rowMapper); + } + + /** + * Returns all players owned by the given username and client ID. + * + * @param username The name of the user. + * @param clientId The third-party client ID (used if this player is managed over the + * Subsonic REST API). May be <code>null</code>. + * @return All relevant players. + */ + public List<Player> getPlayersForUserAndClientId(String username, String clientId) { + if (clientId != null) { + String sql = "select " + COLUMNS + " from player where username=? and client_id=?"; + return query(sql, rowMapper, username, clientId); + } else { + String sql = "select " + COLUMNS + " from player where username=? and client_id is null"; + return query(sql, rowMapper, username); + } + } + + /** + * Returns the player with the given ID. + * + * @param id The unique player ID. + * @return The player with the given ID, or <code>null</code> if no such player exists. + */ + public Player getPlayerById(String id) { + String sql = "select " + COLUMNS + " from player where id=?"; + return queryOne(sql, rowMapper, id); + } + + /** + * Creates a new player. + * + * @param player The player to create. + */ + public synchronized void createPlayer(Player player) { + int id = getJdbcTemplate().queryForInt("select max(id) from player") + 1; + player.setId(String.valueOf(id)); + String sql = "insert into player (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")"; + update(sql, player.getId(), player.getName(), player.getType(), player.getUsername(), + player.getIpAddress(), player.isAutoControlEnabled(), + player.getLastSeen(), player.getCoverArtScheme().name(), + player.getTranscodeScheme().name(), player.isDynamicIp(), + player.getTechnology().name(), player.getClientId()); + addPlaylist(player); + + LOG.info("Created player " + id + '.'); + } + + /** + * Deletes the player with the given ID. + * + * @param id The player ID. + */ + public void deletePlayer(String id) { + String sql = "delete from player where id=?"; + update(sql, id); + playlists.remove(id); + } + + + /** + * Delete players that haven't been used for the given number of days, and which is not given a name + * or is used by a REST client. + * + * @param days Number of days. + */ + public void deleteOldPlayers(int days) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DATE, -days); + String sql = "delete from player where name is null and client_id is null and (last_seen is null or last_seen < ?)"; + int n = update(sql, cal.getTime()); + if (n > 0) { + LOG.info("Deleted " + n + " player(s) that haven't been used after " + cal.getTime()); + } + } + + /** + * Updates the given player. + * + * @param player The player to update. + */ + public void updatePlayer(Player player) { + String sql = "update player set " + + "name = ?," + + "type = ?," + + "username = ?," + + "ip_address = ?," + + "auto_control_enabled = ?," + + "last_seen = ?," + + "cover_art_scheme = ?," + + "transcode_scheme = ?, " + + "dynamic_ip = ?, " + + "technology = ?, " + + "client_id = ? " + + "where id = ?"; + update(sql, player.getName(), player.getType(), player.getUsername(), + player.getIpAddress(), player.isAutoControlEnabled(), + player.getLastSeen(), player.getCoverArtScheme().name(), + player.getTranscodeScheme().name(), player.isDynamicIp(), + player.getTechnology(), player.getClientId(), player.getId()); + } + + private void addPlaylist(Player player) { + PlayQueue playQueue = playlists.get(player.getId()); + if (playQueue == null) { + playQueue = new PlayQueue(); + playlists.put(player.getId(), playQueue); + } + player.setPlayQueue(playQueue); + } + + private class PlayerRowMapper implements ParameterizedRowMapper<Player> { + public Player mapRow(ResultSet rs, int rowNum) throws SQLException { + Player player = new Player(); + int col = 1; + player.setId(rs.getString(col++)); + player.setName(rs.getString(col++)); + player.setType(rs.getString(col++)); + player.setUsername(rs.getString(col++)); + player.setIpAddress(rs.getString(col++)); + player.setAutoControlEnabled(rs.getBoolean(col++)); + player.setLastSeen(rs.getTimestamp(col++)); + player.setCoverArtScheme(CoverArtScheme.valueOf(rs.getString(col++))); + player.setTranscodeScheme(TranscodeScheme.valueOf(rs.getString(col++))); + player.setDynamicIp(rs.getBoolean(col++)); + player.setTechnology(PlayerTechnology.valueOf(rs.getString(col++))); + player.setClientId(rs.getString(col++)); + + addPlaylist(player); + return player; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java new file mode 100644 index 00000000..54cbaded --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlaylistDao.java @@ -0,0 +1,142 @@ +/* + 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.dao; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Provides database services for playlists. + * + * @author Sindre Mehus + */ +public class PlaylistDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(PlaylistDao.class); + private static final String COLUMNS = "id, username, is_public, name, comment, file_count, duration_seconds, " + + "created, changed, imported_from"; + private final RowMapper rowMapper = new PlaylistMapper(); + + public List<Playlist> getReadablePlaylistsForUser(String username) { + + List<Playlist> result1 = getWritablePlaylistsForUser(username); + List<Playlist> result2 = query("select " + COLUMNS + " from playlist where is_public", rowMapper); + List<Playlist> result3 = query("select " + prefix(COLUMNS, "playlist") + " from playlist, playlist_user where " + + "playlist.id = playlist_user.playlist_id and " + + "playlist.username != ? and " + + "playlist_user.username = ?", rowMapper, username, username); + + // Put in sorted map to avoid duplicates. + SortedMap<Integer, Playlist> map = new TreeMap<Integer, Playlist>(); + for (Playlist playlist : result1) { + map.put(playlist.getId(), playlist); + } + for (Playlist playlist : result2) { + map.put(playlist.getId(), playlist); + } + for (Playlist playlist : result3) { + map.put(playlist.getId(), playlist); + } + return new ArrayList<Playlist>(map.values()); + } + + public List<Playlist> getWritablePlaylistsForUser(String username) { + return query("select " + COLUMNS + " from playlist where username=?", rowMapper, username); + } + + public Playlist getPlaylist(int id) { + return queryOne("select " + COLUMNS + " from playlist where id=?", rowMapper, id); + } + + public List<Playlist> getAllPlaylists() { + return query("select " + COLUMNS + " from playlist", rowMapper); + } + + public synchronized void createPlaylist(Playlist playlist) { + update("insert into playlist(" + COLUMNS + ") values(" + questionMarks(COLUMNS) + ")", + null, playlist.getUsername(), playlist.isPublic(), playlist.getName(), playlist.getComment(), + 0, 0, playlist.getCreated(), playlist.getChanged(), playlist.getImportedFrom()); + + int id = queryForInt("select max(id) from playlist", 0); + playlist.setId(id); + } + + public void setFilesInPlaylist(int id, List<MediaFile> files) { + update("delete from playlist_file where playlist_id=?", id); + int duration = 0; + for (MediaFile file : files) { + update("insert into playlist_file (playlist_id, media_file_id) values (?, ?)", id, file.getId()); + if (file.getDurationSeconds() != null) { + duration += file.getDurationSeconds(); + } + } + update("update playlist set file_count=?, duration_seconds=?, changed=? where id=?", files.size(), duration, new Date(), id); + } + + public List<String> getPlaylistUsers(int playlistId) { + return queryForStrings("select username from playlist_user where playlist_id=?", playlistId); + } + + public void addPlaylistUser(int playlistId, String username) { + if (!getPlaylistUsers(playlistId).contains(username)) { + update("insert into playlist_user(playlist_id,username) values (?,?)", playlistId, username); + } + } + + public void deletePlaylistUser(int playlistId, String username) { + update("delete from playlist_user where playlist_id=? and username=?", playlistId, username); + } + + public synchronized void deletePlaylist(int id) { + update("delete from playlist where id=?", id); + } + + public void updatePlaylist(Playlist playlist) { + update("update playlist set username=?, is_public=?, name=?, comment=?, changed=?, imported_from=? where id=?", + playlist.getUsername(), playlist.isPublic(), playlist.getName(), playlist.getComment(), + new Date(), playlist.getImportedFrom(), playlist.getId()); + } + + private static class PlaylistMapper implements ParameterizedRowMapper<Playlist> { + public Playlist mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Playlist( + rs.getInt(1), + rs.getString(2), + rs.getBoolean(3), + rs.getString(4), + rs.getString(5), + rs.getInt(6), + rs.getInt(7), + rs.getTimestamp(8), + rs.getTimestamp(9), + rs.getString(10)); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java new file mode 100644 index 00000000..3f274ec6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java @@ -0,0 +1,165 @@ +/* + 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.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.PodcastStatus; + +/** + * Provides database services for Podcast channels and episodes. + * + * @author Sindre Mehus + */ +public class PodcastDao extends AbstractDao { + + private static final String CHANNEL_COLUMNS = "id, url, title, description, status, error_message"; + private static final String EPISODE_COLUMNS = "id, channel_id, url, path, title, description, publish_date, " + + "duration, bytes_total, bytes_downloaded, status, error_message"; + + private PodcastChannelRowMapper channelRowMapper = new PodcastChannelRowMapper(); + private PodcastEpisodeRowMapper episodeRowMapper = new PodcastEpisodeRowMapper(); + + /** + * Creates a new Podcast channel. + * + * @param channel The Podcast channel to create. + * @return The ID of the newly created channel. + */ + public synchronized int createChannel(PodcastChannel channel) { + String sql = "insert into podcast_channel (" + CHANNEL_COLUMNS + ") values (" + questionMarks(CHANNEL_COLUMNS) + ")"; + update(sql, null, channel.getUrl(), channel.getTitle(), channel.getDescription(), + channel.getStatus().name(), channel.getErrorMessage()); + + return getJdbcTemplate().queryForInt("select max(id) from podcast_channel"); + } + + /** + * Returns all Podcast channels. + * + * @return Possibly empty list of all Podcast channels. + */ + public List<PodcastChannel> getAllChannels() { + String sql = "select " + CHANNEL_COLUMNS + " from podcast_channel"; + return query(sql, channelRowMapper); + } + + /** + * Updates the given Podcast channel. + * + * @param channel The Podcast channel to update. + */ + public void updateChannel(PodcastChannel channel) { + String sql = "update podcast_channel set url=?, title=?, description=?, status=?, error_message=? where id=?"; + update(sql, channel.getUrl(), channel.getTitle(), channel.getDescription(), + channel.getStatus().name(), channel.getErrorMessage(), channel.getId()); + } + + /** + * Deletes the Podcast channel with the given ID. + * + * @param id The Podcast channel ID. + */ + public void deleteChannel(int id) { + String sql = "delete from podcast_channel where id=?"; + update(sql, id); + } + + /** + * Creates a new Podcast episode. + * + * @param episode The Podcast episode to create. + */ + public void createEpisode(PodcastEpisode episode) { + String sql = "insert into podcast_episode (" + EPISODE_COLUMNS + ") values (" + questionMarks(EPISODE_COLUMNS) + ")"; + update(sql, null, episode.getChannelId(), episode.getUrl(), episode.getPath(), + episode.getTitle(), episode.getDescription(), episode.getPublishDate(), + episode.getDuration(), episode.getBytesTotal(), episode.getBytesDownloaded(), + episode.getStatus().name(), episode.getErrorMessage()); + } + + /** + * Returns all Podcast episodes for a given channel. + * + * @return Possibly empty list of all Podcast episodes for the given channel, sorted in + * reverse chronological order (newest episode first). + */ + public List<PodcastEpisode> getEpisodes(int channelId) { + String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where channel_id=? order by publish_date desc"; + return query(sql, episodeRowMapper, channelId); + } + + /** + * Returns the Podcast episode with the given ID. + * + * @param episodeId The Podcast episode ID. + * @return The episode or <code>null</code> if not found. + */ + public PodcastEpisode getEpisode(int episodeId) { + String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where id=?"; + return queryOne(sql, episodeRowMapper, episodeId); + } + + /** + * Updates the given Podcast episode. + * + * @param episode The Podcast episode to update. + * @return The number of episodes updated (zero or one). + */ + public int updateEpisode(PodcastEpisode episode) { + String sql = "update podcast_episode set url=?, path=?, title=?, description=?, publish_date=?, duration=?, " + + "bytes_total=?, bytes_downloaded=?, status=?, error_message=? where id=?"; + return update(sql, episode.getUrl(), episode.getPath(), episode.getTitle(), + episode.getDescription(), episode.getPublishDate(), episode.getDuration(), + episode.getBytesTotal(), episode.getBytesDownloaded(), episode.getStatus().name(), + episode.getErrorMessage(), episode.getId()); + } + + /** + * Deletes the Podcast episode with the given ID. + * + * @param id The Podcast episode ID. + */ + public void deleteEpisode(int id) { + String sql = "delete from podcast_episode where id=?"; + update(sql, id); + } + + private static class PodcastChannelRowMapper implements RowMapper { + public Object mapRow(ResultSet rs, int rowNum) throws SQLException { + return new PodcastChannel(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), + PodcastStatus.valueOf(rs.getString(5)), rs.getString(6)); + } + } + + private static class PodcastEpisodeRowMapper implements ParameterizedRowMapper<PodcastEpisode> { + public PodcastEpisode mapRow(ResultSet rs, int rowNum) throws SQLException { + return new PodcastEpisode(rs.getInt(1), rs.getInt(2), rs.getString(3), rs.getString(4), rs.getString(5), + rs.getString(6), rs.getTimestamp(7), rs.getString(8), (Long) rs.getObject(9), + (Long) rs.getObject(10), PodcastStatus.valueOf(rs.getString(11)), rs.getString(12)); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java new file mode 100644 index 00000000..221fe889 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.java @@ -0,0 +1,99 @@ +/* + 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.dao; + +import net.sourceforge.subsonic.domain.MediaFile; +import org.springframework.dao.EmptyResultDataAccessException; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides database services for ratings. + * + * @author Sindre Mehus + */ +public class RatingDao extends AbstractDao { + + /** + * Returns paths for the highest rated music files. + * + * @param offset Number of files to skip. + * @param count Maximum number of files to return. + * @return Paths for the highest rated music files. + */ + public List<String> getHighestRated(int offset, int count) { + if (count < 1) { + return new ArrayList<String>(); + } + + String sql = "select user_rating.path from user_rating, media_file " + + "where user_rating.path=media_file.path and media_file.present " + + "group by path " + + "order by avg(rating) desc limit " + count + " offset " + offset; + return queryForStrings(sql); + } + + /** + * Sets the rating for a media file and a given user. + * + * @param username The user name. + * @param mediaFile The media file. + * @param rating The rating between 1 and 5, or <code>null</code> to remove the rating. + */ + public void setRatingForUser(String username, MediaFile mediaFile, Integer rating) { + if (rating != null && (rating < 1 || rating > 5)) { + return; + } + + update("delete from user_rating where username=? and path=?", username, mediaFile.getPath()); + if (rating != null) { + update("insert into user_rating values(?, ?, ?)", username, mediaFile.getPath(), rating); + } + } + + /** + * Returns the average rating for the given media file. + * + * @param mediaFile The media file. + * @return The average rating, or <code>null</code> if no ratings are set. + */ + public Double getAverageRating(MediaFile mediaFile) { + try { + return (Double) getJdbcTemplate().queryForObject("select avg(rating) from user_rating where path=?", new Object[]{mediaFile.getPath()}, Double.class); + } catch (EmptyResultDataAccessException x) { + return null; + } + } + + /** + * Returns the rating for the given user and media file. + * + * @param username The user name. + * @param mediaFile The media file. + * @return The rating, or <code>null</code> if no rating is set. + */ + public Integer getRatingForUser(String username, MediaFile mediaFile) { + try { + return getJdbcTemplate().queryForInt("select rating from user_rating where username=? and path=?", new Object[]{username, mediaFile.getPath()}); + } catch (EmptyResultDataAccessException x) { + return null; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java new file mode 100644 index 00000000..17d4cd73 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java @@ -0,0 +1,131 @@ +/* + 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.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.domain.Share; + +/** + * Provides database services for shared media. + * + * @author Sindre Mehus + */ +public class ShareDao extends AbstractDao { + + private static final String COLUMNS = "id, name, description, username, created, expires, last_visited, visit_count"; + + private ShareRowMapper shareRowMapper = new ShareRowMapper(); + private ShareFileRowMapper shareFileRowMapper = new ShareFileRowMapper(); + + /** + * Creates a new share. + * + * @param share The share to create. The ID of the share will be set by this method. + */ + public synchronized void createShare(Share share) { + String sql = "insert into share (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")"; + update(sql, null, share.getName(), share.getDescription(), share.getUsername(), share.getCreated(), + share.getExpires(), share.getLastVisited(), share.getVisitCount()); + + int id = getJdbcTemplate().queryForInt("select max(id) from share"); + share.setId(id); + } + + /** + * Returns all shares. + * + * @return Possibly empty list of all shares. + */ + public List<Share> getAllShares() { + String sql = "select " + COLUMNS + " from share"; + return query(sql, shareRowMapper); + } + + public Share getShareByName(String shareName) { + String sql = "select " + COLUMNS + " from share where name=?"; + return queryOne(sql, shareRowMapper, shareName); + } + + public Share getShareById(int id) { + String sql = "select " + COLUMNS + " from share where id=?"; + return queryOne(sql, shareRowMapper, id); + } + + /** + * Updates the given share. + * + * @param share The share to update. + */ + public void updateShare(Share share) { + String sql = "update share set name=?, description=?, username=?, created=?, expires=?, last_visited=?, visit_count=? where id=?"; + update(sql, share.getName(), share.getDescription(), share.getUsername(), share.getCreated(), share.getExpires(), + share.getLastVisited(), share.getVisitCount(), share.getId()); + } + + /** + * Creates shared files. + * + * @param shareId The share ID. + * @param paths Paths of the files to share. + */ + public void createSharedFiles(int shareId, String... paths) { + String sql = "insert into share_file (share_id, path) values (?, ?)"; + for (String path : paths) { + update(sql, shareId, path); + } + } + + /** + * Returns files for a share. + * + * @param shareId The ID of the share. + * @return The paths of the shared files. + */ + public List<String> getSharedFiles(int shareId) { + return query("select path from share_file where share_id=?", shareFileRowMapper, shareId); + } + + /** + * Deletes the share with the given ID. + * + * @param id The ID of the share to delete. + */ + public void deleteShare(Integer id) { + update("delete from share where id=?", id); + } + + private static class ShareRowMapper implements ParameterizedRowMapper<Share> { + public Share mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Share(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getTimestamp(5), + rs.getTimestamp(6), rs.getTimestamp(7), rs.getInt(8)); + } + } + + private static class ShareFileRowMapper implements ParameterizedRowMapper<String> { + public String mapRow(ResultSet rs, int rowNum) throws SQLException { + return rs.getString(1); + } + + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.java new file mode 100644 index 00000000..22b8ae20 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/TranscodingDao.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.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Transcoding; + +/** + * Provides database services for transcoding configurations. + * + * @author Sindre Mehus + */ +public class TranscodingDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(TranscodingDao.class); + private static final String COLUMNS = "id, name, source_formats, target_format, step1, step2, step3, default_active"; + private TranscodingRowMapper rowMapper = new TranscodingRowMapper(); + + /** + * Returns all transcodings. + * + * @return Possibly empty list of all transcodings. + */ + public List<Transcoding> getAllTranscodings() { + String sql = "select " + COLUMNS + " from transcoding2"; + return query(sql, rowMapper); + } + + /** + * Returns all active transcodings for the given player. + * + * @param playerId The player ID. + * @return All active transcodings for the player. + */ + public List<Transcoding> getTranscodingsForPlayer(String playerId) { + String sql = "select " + COLUMNS + " from transcoding2, player_transcoding2 " + + "where player_transcoding2.player_id = ? " + + "and player_transcoding2.transcoding_id = transcoding2.id"; + return query(sql, rowMapper, playerId); + } + + /** + * Sets the list of active transcodings for the given player. + * + * @param playerId The player ID. + * @param transcodingIds ID's of the active transcodings. + */ + public void setTranscodingsForPlayer(String playerId, int[] transcodingIds) { + update("delete from player_transcoding2 where player_id = ?", playerId); + String sql = "insert into player_transcoding2(player_id, transcoding_id) values (?, ?)"; + for (int transcodingId : transcodingIds) { + update(sql, playerId, transcodingId); + } + } + + /** + * Creates a new transcoding. + * + * @param transcoding The transcoding to create. + */ + public synchronized void createTranscoding(Transcoding transcoding) { + int id = getJdbcTemplate().queryForInt("select max(id) + 1 from transcoding2"); + transcoding.setId(id); + String sql = "insert into transcoding2 (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")"; + update(sql, transcoding.getId(), transcoding.getName(), transcoding.getSourceFormats(), + transcoding.getTargetFormat(), transcoding.getStep1(), + transcoding.getStep2(), transcoding.getStep3(), transcoding.isDefaultActive()); + LOG.info("Created transcoding " + transcoding.getName()); + } + + /** + * Deletes the transcoding with the given ID. + * + * @param id The transcoding ID. + */ + public void deleteTranscoding(Integer id) { + String sql = "delete from transcoding2 where id=?"; + update(sql, id); + LOG.info("Deleted transcoding with ID " + id); + } + + /** + * Updates the given transcoding. + * + * @param transcoding The transcoding to update. + */ + public void updateTranscoding(Transcoding transcoding) { + String sql = "update transcoding2 set name=?, source_formats=?, target_format=?, " + + "step1=?, step2=?, step3=?, default_active=? where id=?"; + update(sql, transcoding.getName(), transcoding.getSourceFormats(), + transcoding.getTargetFormat(), transcoding.getStep1(), transcoding.getStep2(), + transcoding.getStep3(), transcoding.isDefaultActive(), transcoding.getId()); + } + + private static class TranscodingRowMapper implements ParameterizedRowMapper<Transcoding> { + public Transcoding mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Transcoding(rs.getInt(1), rs.getString(2), rs.getString(3), rs.getString(4), rs.getString(5), + rs.getString(6), rs.getString(7), rs.getBoolean(8)); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java new file mode 100644 index 00000000..e7807765 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java @@ -0,0 +1,352 @@ +/* + 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.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Provides user-related database services. + * + * @author Sindre Mehus + */ +public class UserDao extends AbstractDao { + + private static final Logger LOG = Logger.getLogger(UserDao.class); + private static final String USER_COLUMNS = "username, password, email, ldap_authenticated, bytes_streamed, bytes_downloaded, bytes_uploaded"; + private static final String USER_SETTINGS_COLUMNS = "username, locale, theme_id, final_version_notification, beta_version_notification, " + + "main_caption_cutoff, main_track_number, main_artist, main_album, main_genre, " + + "main_year, main_bit_rate, main_duration, main_format, main_file_size, " + + "playlist_caption_cutoff, playlist_track_number, playlist_artist, playlist_album, playlist_genre, " + + "playlist_year, playlist_bit_rate, playlist_duration, playlist_format, playlist_file_size, " + + "last_fm_enabled, last_fm_username, last_fm_password, transcode_scheme, show_now_playing, selected_music_folder_id, " + + "party_mode_enabled, now_playing_allowed, avatar_scheme, system_avatar_id, changed, show_chat"; + + private static final Integer ROLE_ID_ADMIN = 1; + private static final Integer ROLE_ID_DOWNLOAD = 2; + private static final Integer ROLE_ID_UPLOAD = 3; + private static final Integer ROLE_ID_PLAYLIST = 4; + private static final Integer ROLE_ID_COVER_ART = 5; + private static final Integer ROLE_ID_COMMENT = 6; + private static final Integer ROLE_ID_PODCAST = 7; + private static final Integer ROLE_ID_STREAM = 8; + private static final Integer ROLE_ID_SETTINGS = 9; + private static final Integer ROLE_ID_JUKEBOX = 10; + private static final Integer ROLE_ID_SHARE = 11; + + private UserRowMapper userRowMapper = new UserRowMapper(); + private UserSettingsRowMapper userSettingsRowMapper = new UserSettingsRowMapper(); + + /** + * Returns the user with the given username. + * + * @param username The username used when logging in. + * @return The user, or <code>null</code> if not found. + */ + public User getUserByName(String username) { + String sql = "select " + USER_COLUMNS + " from user where username=?"; + return queryOne(sql, userRowMapper, username); + } + + /** + * Returns the user with the given email address. + * + * @param email The email address. + * @return The user, or <code>null</code> if not found. + */ + public User getUserByEmail(String email) { + String sql = "select " + USER_COLUMNS + " from user where email=?"; + return queryOne(sql, userRowMapper, email); + } + + /** + * Returns all users. + * + * @return Possibly empty array of all users. + */ + public List<User> getAllUsers() { + String sql = "select " + USER_COLUMNS + " from user"; + return query(sql, userRowMapper); + } + + /** + * Creates a new user. + * + * @param user The user to create. + */ + public void createUser(User user) { + String sql = "insert into user (" + USER_COLUMNS + ") values (" + questionMarks(USER_COLUMNS) + ')'; + update(sql, user.getUsername(), encrypt(user.getPassword()), user.getEmail(), user.isLdapAuthenticated(), + user.getBytesStreamed(), user.getBytesDownloaded(), user.getBytesUploaded()); + writeRoles(user); + } + + /** + * Deletes the user with the given username. + * + * @param username The username. + */ + public void deleteUser(String username) { + if (User.USERNAME_ADMIN.equals(username)) { + throw new IllegalArgumentException("Can't delete admin user."); + } + + String sql = "delete from user_role where username=?"; + update(sql, username); + + sql = "delete from user where username=?"; + update(sql, username); + } + + /** + * Updates the given user. + * + * @param user The user to update. + */ + public void updateUser(User user) { + String sql = "update user set password=?, email=?, ldap_authenticated=?, bytes_streamed=?, bytes_downloaded=?, bytes_uploaded=? " + + "where username=?"; + getJdbcTemplate().update(sql, new Object[]{encrypt(user.getPassword()), user.getEmail(), user.isLdapAuthenticated(), + user.getBytesStreamed(), user.getBytesDownloaded(), user.getBytesUploaded(), + user.getUsername()}); + writeRoles(user); + } + + /** + * Returns the name of the roles for the given user. + * + * @param username The user name. + * @return Roles the user is granted. + */ + public String[] getRolesForUser(String username) { + String sql = "select r.name from role r, user_role ur " + + "where ur.username=? and ur.role_id=r.id"; + List<?> roles = getJdbcTemplate().queryForList(sql, new Object[]{username}, String.class); + String[] result = new String[roles.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = (String) roles.get(i); + } + return result; + } + + /** + * Returns settings for the given user. + * + * @param username The username. + * @return User-specific settings, or <code>null</code> if no such settings exist. + */ + public UserSettings getUserSettings(String username) { + String sql = "select " + USER_SETTINGS_COLUMNS + " from user_settings where username=?"; + return queryOne(sql, userSettingsRowMapper, username); + } + + /** + * Updates settings for the given username, creating it if necessary. + * + * @param settings The user-specific settings. + */ + public void updateUserSettings(UserSettings settings) { + getJdbcTemplate().update("delete from user_settings where username=?", new Object[]{settings.getUsername()}); + + String sql = "insert into user_settings (" + USER_SETTINGS_COLUMNS + ") values (" + questionMarks(USER_SETTINGS_COLUMNS) + ')'; + String locale = settings.getLocale() == null ? null : settings.getLocale().toString(); + UserSettings.Visibility main = settings.getMainVisibility(); + UserSettings.Visibility playlist = settings.getPlaylistVisibility(); + getJdbcTemplate().update(sql, new Object[]{settings.getUsername(), locale, settings.getThemeId(), + settings.isFinalVersionNotificationEnabled(), settings.isBetaVersionNotificationEnabled(), + main.getCaptionCutoff(), main.isTrackNumberVisible(), main.isArtistVisible(), main.isAlbumVisible(), + main.isGenreVisible(), main.isYearVisible(), main.isBitRateVisible(), main.isDurationVisible(), + main.isFormatVisible(), main.isFileSizeVisible(), + playlist.getCaptionCutoff(), playlist.isTrackNumberVisible(), playlist.isArtistVisible(), playlist.isAlbumVisible(), + playlist.isGenreVisible(), playlist.isYearVisible(), playlist.isBitRateVisible(), playlist.isDurationVisible(), + playlist.isFormatVisible(), playlist.isFileSizeVisible(), + settings.isLastFmEnabled(), settings.getLastFmUsername(), encrypt(settings.getLastFmPassword()), + settings.getTranscodeScheme().name(), settings.isShowNowPlayingEnabled(), + settings.getSelectedMusicFolderId(), settings.isPartyModeEnabled(), settings.isNowPlayingAllowed(), + settings.getAvatarScheme().name(), settings.getSystemAvatarId(), settings.getChanged(), settings.isShowChatEnabled()}); + } + + private static String encrypt(String s) { + if (s == null) { + return null; + } + try { + return "enc:" + StringUtil.utf8HexEncode(s); + } catch (Exception e) { + return s; + } + } + + private static String decrypt(String s) { + if (s == null) { + return null; + } + if (!s.startsWith("enc:")) { + return s; + } + try { + return StringUtil.utf8HexDecode(s.substring(4)); + } catch (Exception e) { + return s; + } + } + + private void readRoles(User user) { + synchronized (user.getUsername().intern()) { + String sql = "select role_id from user_role where username=?"; + List<?> roles = getJdbcTemplate().queryForList(sql, new Object[]{user.getUsername()}, Integer.class); + for (Object role : roles) { + if (ROLE_ID_ADMIN.equals(role)) { + user.setAdminRole(true); + } else if (ROLE_ID_DOWNLOAD.equals(role)) { + user.setDownloadRole(true); + } else if (ROLE_ID_UPLOAD.equals(role)) { + user.setUploadRole(true); + } else if (ROLE_ID_PLAYLIST.equals(role)) { + user.setPlaylistRole(true); + } else if (ROLE_ID_COVER_ART.equals(role)) { + user.setCoverArtRole(true); + } else if (ROLE_ID_COMMENT.equals(role)) { + user.setCommentRole(true); + } else if (ROLE_ID_PODCAST.equals(role)) { + user.setPodcastRole(true); + } else if (ROLE_ID_STREAM.equals(role)) { + user.setStreamRole(true); + } else if (ROLE_ID_SETTINGS.equals(role)) { + user.setSettingsRole(true); + } else if (ROLE_ID_JUKEBOX.equals(role)) { + user.setJukeboxRole(true); + } else if (ROLE_ID_SHARE.equals(role)) { + user.setShareRole(true); + } else { + LOG.warn("Unknown role: '" + role + '\''); + } + } + } + } + + private void writeRoles(User user) { + synchronized (user.getUsername().intern()) { + String sql = "delete from user_role where username=?"; + getJdbcTemplate().update(sql, new Object[]{user.getUsername()}); + sql = "insert into user_role (username, role_id) values(?, ?)"; + if (user.isAdminRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_ADMIN}); + } + if (user.isDownloadRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_DOWNLOAD}); + } + if (user.isUploadRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_UPLOAD}); + } + if (user.isPlaylistRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_PLAYLIST}); + } + if (user.isCoverArtRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_COVER_ART}); + } + if (user.isCommentRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_COMMENT}); + } + if (user.isPodcastRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_PODCAST}); + } + if (user.isStreamRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_STREAM}); + } + if (user.isJukeboxRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_JUKEBOX}); + } + if (user.isSettingsRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_SETTINGS}); + } + if (user.isShareRole()) { + getJdbcTemplate().update(sql, new Object[]{user.getUsername(), ROLE_ID_SHARE}); + } + } + } + + private class UserRowMapper implements ParameterizedRowMapper<User> { + public User mapRow(ResultSet rs, int rowNum) throws SQLException { + User user = new User(rs.getString(1), decrypt(rs.getString(2)), rs.getString(3), rs.getBoolean(4), + rs.getLong(5), rs.getLong(6), rs.getLong(7)); + readRoles(user); + return user; + } + } + + private static class UserSettingsRowMapper implements ParameterizedRowMapper<UserSettings> { + public UserSettings mapRow(ResultSet rs, int rowNum) throws SQLException { + int col = 1; + UserSettings settings = new UserSettings(rs.getString(col++)); + settings.setLocale(StringUtil.parseLocale(rs.getString(col++))); + settings.setThemeId(rs.getString(col++)); + settings.setFinalVersionNotificationEnabled(rs.getBoolean(col++)); + settings.setBetaVersionNotificationEnabled(rs.getBoolean(col++)); + + settings.getMainVisibility().setCaptionCutoff(rs.getInt(col++)); + settings.getMainVisibility().setTrackNumberVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setArtistVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setAlbumVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setGenreVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setYearVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setBitRateVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setDurationVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setFormatVisible(rs.getBoolean(col++)); + settings.getMainVisibility().setFileSizeVisible(rs.getBoolean(col++)); + + settings.getPlaylistVisibility().setCaptionCutoff(rs.getInt(col++)); + settings.getPlaylistVisibility().setTrackNumberVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setArtistVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setAlbumVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setGenreVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setYearVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setBitRateVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setDurationVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setFormatVisible(rs.getBoolean(col++)); + settings.getPlaylistVisibility().setFileSizeVisible(rs.getBoolean(col++)); + + settings.setLastFmEnabled(rs.getBoolean(col++)); + settings.setLastFmUsername(rs.getString(col++)); + settings.setLastFmPassword(decrypt(rs.getString(col++))); + + settings.setTranscodeScheme(TranscodeScheme.valueOf(rs.getString(col++))); + settings.setShowNowPlayingEnabled(rs.getBoolean(col++)); + settings.setSelectedMusicFolderId(rs.getInt(col++)); + settings.setPartyModeEnabled(rs.getBoolean(col++)); + settings.setNowPlayingAllowed(rs.getBoolean(col++)); + settings.setAvatarScheme(AvatarScheme.valueOf(rs.getString(col++))); + settings.setSystemAvatarId((Integer) rs.getObject(col++)); + settings.setChanged(rs.getTimestamp(col++)); + settings.setShowChatEnabled(rs.getBoolean(col++)); + + return settings; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.java new file mode 100644 index 00000000..674f85ca --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.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.dao.schema; + +import org.springframework.jdbc.core.*; + +/** + * Used for creating and evolving the database schema. + * + * @author Sindre Mehus + */ +public abstract class Schema { + + /** + * Executes this schema. + * @param template The JDBC template to use. + */ + public abstract void execute(JdbcTemplate template); + + /** + * Returns whether the given table exists. + * @param template The JDBC template to use. + * @param table The table in question. + * @return Whether the table exists. + */ + protected boolean tableExists(JdbcTemplate template, String table) { + try { + template.execute("select 1 from " + table); + } catch (Exception x) { + return false; + } + return true; + } + + /** + * Returns whether the given column in the given table exists. + * @param template The JDBC template to use. + * @param column The column in question. + * @param table The table in question. + * @return Whether the column exists. + */ + protected boolean columnExists(JdbcTemplate template, String column, String table) { + try { + template.execute("select " + column + " from " + table + " where 1 = 0"); + } catch (Exception x) { + return false; + } + return true; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java new file mode 100644 index 00000000..33cc2525 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema25.java @@ -0,0 +1,81 @@ +/* + 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.dao.schema; + +import org.springframework.jdbc.core.*; +import net.sourceforge.subsonic.*; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 2.5. + * + * @author Sindre Mehus + */ +public class Schema25 extends Schema{ + private static final Logger LOG = Logger.getLogger(Schema25.class); + + public void execute(JdbcTemplate template) { + if (!tableExists(template, "version")) { + LOG.info("Database table 'version' not found. Creating it."); + template.execute("create table version (version int not null)"); + template.execute("insert into version values (1)"); + LOG.info("Database table 'version' was created successfully."); + } + + if (!tableExists(template, "role")) { + LOG.info("Database table 'role' not found. Creating it."); + template.execute("create table role (" + + "id int not null," + + "name varchar not null," + + "primary key (id))"); + template.execute("insert into role values (1, 'admin')"); + template.execute("insert into role values (2, 'download')"); + template.execute("insert into role values (3, 'upload')"); + template.execute("insert into role values (4, 'playlist')"); + template.execute("insert into role values (5, 'coverart')"); + LOG.info("Database table 'role' was created successfully."); + } + + if (!tableExists(template, "user")) { + LOG.info("Database table 'user' not found. Creating it."); + template.execute("create table user (" + + "username varchar not null," + + "password varchar not null," + + "primary key (username))"); + template.execute("insert into user values ('admin', 'admin')"); + LOG.info("Database table 'user' was created successfully."); + } + + if (!tableExists(template, "user_role")) { + LOG.info("Database table 'user_role' not found. Creating it."); + template.execute("create table user_role (" + + "username varchar not null," + + "role_id int not null," + + "primary key (username, role_id)," + + "foreign key (username) references user(username)," + + "foreign key (role_id) references role(id))"); + template.execute("insert into user_role values ('admin', 1)"); + template.execute("insert into user_role values ('admin', 2)"); + template.execute("insert into user_role values ('admin', 3)"); + template.execute("insert into user_role values ('admin', 4)"); + template.execute("insert into user_role values ('admin', 5)"); + LOG.info("Database table 'user_role' was created successfully."); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.java new file mode 100644 index 00000000..6d60b29b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema26.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.dao.schema; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.util.Util; +import org.springframework.jdbc.core.*; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 2.6. + * + * @author Sindre Mehus + */ +public class Schema26 extends Schema{ + private static final Logger LOG = Logger.getLogger(Schema26.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 2") == 0) { + LOG.info("Updating database schema to version 2."); + template.execute("insert into version values (2)"); + } + + if (!tableExists(template, "music_folder")) { + LOG.info("Database table 'music_folder' not found. Creating it."); + template.execute("create table music_folder (" + + "id identity," + + "path varchar not null," + + "name varchar not null," + + "enabled boolean not null)"); + template.execute("insert into music_folder values (null, '" + Util.getDefaultMusicFolder() + "', 'Music', true)"); + LOG.info("Database table 'music_folder' was created successfully."); + } + + if (!tableExists(template, "music_file_info")) { + LOG.info("Database table 'music_file_info' not found. Creating it."); + template.execute("create cached table music_file_info (" + + "id identity," + + "path varchar not null," + + "rating int," + + "comment varchar," + + "play_count int," + + "last_played datetime)"); + template.execute("create index idx_music_file_info_path on music_file_info(path)"); + LOG.info("Database table 'music_file_info' was created successfully."); + } + + if (!tableExists(template, "internet_radio")) { + LOG.info("Database table 'internet_radio' not found. Creating it."); + template.execute("create table internet_radio (" + + "id identity," + + "name varchar not null," + + "stream_url varchar not null," + + "homepage_url varchar," + + "enabled boolean not null)"); + LOG.info("Database table 'internet_radio' was created successfully."); + } + + if (!tableExists(template, "player")) { + LOG.info("Database table 'player' not found. Creating it."); + template.execute("create table player (" + + "id int not null," + + "name varchar," + + "type varchar," + + "username varchar," + + "ip_address varchar," + + "auto_control_enabled boolean not null," + + "last_seen datetime," + + "cover_art_scheme varchar not null," + + "transcode_scheme varchar not null," + + "primary key (id))"); + LOG.info("Database table 'player' was created successfully."); + } + + // 'dynamic_ip' was added in 2.6.beta2 + if (!columnExists(template, "dynamic_ip", "player")) { + LOG.info("Database column 'player.dynamic_ip' not found. Creating it."); + template.execute("alter table player " + + "add dynamic_ip boolean default true not null"); + LOG.info("Database column 'player.dynamic_ip' was added successfully."); + } + + if (template.queryForInt("select count(*) from role where id = 6") == 0) { + LOG.info("Role 'comment' not found in database. Creating it."); + template.execute("insert into role values (6, 'comment')"); + template.execute("insert into user_role " + + "select distinct u.username, 6 from user u, user_role ur " + + "where u.username = ur.username and ur.role_id in (1, 5)"); + LOG.info("Role 'comment' was created successfully."); + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java new file mode 100644 index 00000000..4057622e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema27.java @@ -0,0 +1,54 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.*; +import org.springframework.jdbc.core.*; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 2.7. + * + * @author Sindre Mehus + */ +public class Schema27 extends Schema{ + private static final Logger LOG = Logger.getLogger(Schema27.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 3") == 0) { + LOG.info("Updating database schema to version 3."); + template.execute("insert into version values (3)"); + + LOG.info("Converting database column 'music_file_info.path' to varchar_ignorecase."); + template.execute("drop index idx_music_file_info_path"); + template.execute("alter table music_file_info alter column path varchar_ignorecase not null"); + template.execute("create index idx_music_file_info_path on music_file_info(path)"); + LOG.info("Database column 'music_file_info.path' was converted successfully."); + } + + if (!columnExists(template, "bytes_streamed", "user")) { + LOG.info("Database columns 'user.bytes_streamed/downloaded/uploaded' not found. Creating them."); + template.execute("alter table user add bytes_streamed bigint default 0 not null"); + template.execute("alter table user add bytes_downloaded bigint default 0 not null"); + template.execute("alter table user add bytes_uploaded bigint default 0 not null"); + LOG.info("Database columns 'user.bytes_streamed/downloaded/uploaded' were added successfully."); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.java new file mode 100644 index 00000000..dbee6730 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema28.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.dao.schema; + +import net.sourceforge.subsonic.*; +import org.springframework.jdbc.core.*; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 2.8. + * + * @author Sindre Mehus + */ +public class Schema28 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema28.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 4") == 0) { + LOG.info("Updating database schema to version 4."); + template.execute("insert into version values (4)"); + } + + if (!tableExists(template, "user_settings")) { + LOG.info("Database table 'user_settings' not found. Creating it."); + template.execute("create table user_settings (" + + "username varchar not null," + + "locale varchar," + + "theme_id varchar," + + "final_version_notification boolean default true not null," + + "beta_version_notification boolean default false not null," + + "main_caption_cutoff int default 35 not null," + + "main_track_number boolean default true not null," + + "main_artist boolean default true not null," + + "main_album boolean default false not null," + + "main_genre boolean default false not null," + + "main_year boolean default false not null," + + "main_bit_rate boolean default false not null," + + "main_duration boolean default true not null," + + "main_format boolean default false not null," + + "main_file_size boolean default false not null," + + "playlist_caption_cutoff int default 35 not null," + + "playlist_track_number boolean default false not null," + + "playlist_artist boolean default true not null," + + "playlist_album boolean default true not null," + + "playlist_genre boolean default false not null," + + "playlist_year boolean default true not null," + + "playlist_bit_rate boolean default false not null," + + "playlist_duration boolean default true not null," + + "playlist_format boolean default true not null," + + "playlist_file_size boolean default true not null," + + "primary key (username)," + + "foreign key (username) references user(username) on delete cascade)"); + LOG.info("Database table 'user_settings' was created successfully."); + } + + if (!tableExists(template, "transcoding")) { + LOG.info("Database table 'transcoding' not found. Creating it."); + template.execute("create table transcoding (" + + "id identity," + + "name varchar not null," + + "source_format varchar not null," + + "target_format varchar not null," + + "step1 varchar not null," + + "step2 varchar," + + "step3 varchar," + + "enabled boolean not null)"); + + template.execute("insert into transcoding values(null,'wav > mp3', 'wav', 'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'flac > mp3','flac','mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'ogg > mp3' ,'ogg' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'wma > mp3' ,'wma' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'m4a > mp3' ,'m4a' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,false)"); + template.execute("insert into transcoding values(null,'aac > mp3' ,'aac' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,false)"); + template.execute("insert into transcoding values(null,'ape > mp3' ,'ape' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'mpc > mp3' ,'mpc' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'mv > mp3' ,'mv' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + template.execute("insert into transcoding values(null,'shn > mp3' ,'shn' ,'mp3','ffmpeg -i %s -v 0 -f wav -','lame -b %b --tt %t --ta %a --tl %l -S --resample 44.1 - -',null,true)"); + + LOG.info("Database table 'transcoding' was created successfully."); + } + + if (!tableExists(template, "player_transcoding")) { + LOG.info("Database table 'player_transcoding' not found. Creating it."); + template.execute("create table player_transcoding (" + + "player_id int not null," + + "transcoding_id int not null," + + "primary key (player_id, transcoding_id)," + + "foreign key (player_id) references player(id) on delete cascade," + + "foreign key (transcoding_id) references transcoding(id) on delete cascade)"); + LOG.info("Database table 'player_transcoding' was created successfully."); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java new file mode 100644 index 00000000..dd4748d1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema29.java @@ -0,0 +1,55 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.*; +import org.springframework.jdbc.core.*; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 2.9. + * + * @author Sindre Mehus + */ +public class Schema29 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema29.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 5") == 0) { + LOG.info("Updating database schema to version 5."); + template.execute("insert into version values (5)"); + } + + if (!tableExists(template, "user_rating")) { + LOG.info("Database table 'user_rating' not found. Creating it."); + template.execute("create table user_rating (" + + "username varchar not null," + + "path varchar not null," + + "rating double not null," + + "primary key (username, path)," + + "foreign key (username) references user(username) on delete cascade)"); + LOG.info("Database table 'user_rating' was created successfully."); + + template.execute("insert into user_rating select 'admin', path, rating from music_file_info " + + "where rating is not null and rating > 0"); + LOG.info("Migrated data from 'music_file_info' to 'user_rating'."); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java new file mode 100644 index 00000000..cdea199b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema30.java @@ -0,0 +1,56 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import org.springframework.jdbc.core.*; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.0. + * + * @author Sindre Mehus + */ +public class Schema30 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema30.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 6") == 0) { + LOG.info("Updating database schema to version 6."); + template.execute("insert into version values (6)"); + } + + if (!columnExists(template, "last_fm_enabled", "user_settings")) { + LOG.info("Database columns 'user_settings.last_fm_*' not found. Creating them."); + template.execute("alter table user_settings add last_fm_enabled boolean default false not null"); + template.execute("alter table user_settings add last_fm_username varchar null"); + template.execute("alter table user_settings add last_fm_password varchar null"); + LOG.info("Database columns 'user_settings.last_fm_*' were added successfully."); + } + + if (!columnExists(template, "transcode_scheme", "user_settings")) { + LOG.info("Database column 'user_settings.transcode_scheme' not found. Creating it."); + template.execute("alter table user_settings add transcode_scheme varchar default '" + + TranscodeScheme.OFF.name() + "' not null"); + LOG.info("Database column 'user_settings.transcode_scheme' was added successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.java new file mode 100644 index 00000000..00fb0c87 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema31.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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.1. + * + * @author Sindre Mehus + */ +public class Schema31 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema31.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 7") == 0) { + LOG.info("Updating database schema to version 7."); + template.execute("insert into version values (7)"); + } + + if (!columnExists(template, "enabled", "music_file_info")) { + LOG.info("Database column 'music_file_info.enabled' not found. Creating it."); + template.execute("alter table music_file_info add enabled boolean default true not null"); + LOG.info("Database column 'music_file_info.enabled' was added successfully."); + } + + if (!columnExists(template, "default_active", "transcoding")) { + LOG.info("Database column 'transcoding.default_active' not found. Creating it."); + template.execute("alter table transcoding add default_active boolean default true not null"); + LOG.info("Database column 'transcoding.default_active' was added successfully."); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.java new file mode 100644 index 00000000..a1439bb0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema32.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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.2. + * + * @author Sindre Mehus + */ +public class Schema32 extends Schema { + private static final Logger LOG = Logger.getLogger(Schema32.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 8") == 0) { + LOG.info("Updating database schema to version 8."); + template.execute("insert into version values (8)"); + } + + if (!columnExists(template, "show_now_playing", "user_settings")) { + LOG.info("Database column 'user_settings.show_now_playing' not found. Creating it."); + template.execute("alter table user_settings add show_now_playing boolean default true not null"); + LOG.info("Database column 'user_settings.show_now_playing' was added successfully."); + } + + if (!columnExists(template, "selected_music_folder_id", "user_settings")) { + LOG.info("Database column 'user_settings.selected_music_folder_id' not found. Creating it."); + template.execute("alter table user_settings add selected_music_folder_id int default -1 not null"); + LOG.info("Database column 'user_settings.selected_music_folder_id' was added successfully."); + } + + if (!tableExists(template, "podcast_channel")) { + LOG.info("Database table 'podcast_channel' not found. Creating it."); + template.execute("create table podcast_channel (" + + "id identity," + + "url varchar not null," + + "title varchar," + + "description varchar," + + "status varchar not null," + + "error_message varchar)"); + LOG.info("Database table 'podcast_channel' was created successfully."); + } + + if (!tableExists(template, "podcast_episode")) { + LOG.info("Database table 'podcast_episode' not found. Creating it."); + template.execute("create table podcast_episode (" + + "id identity," + + "channel_id int not null," + + "url varchar not null," + + "path varchar," + + "title varchar," + + "description varchar," + + "publish_date datetime," + + "duration varchar," + + "bytes_total bigint," + + "bytes_downloaded bigint," + + "status varchar not null," + + "error_message varchar," + + "foreign key (channel_id) references podcast_channel(id) on delete cascade)"); + LOG.info("Database table 'podcast_episode' was created successfully."); + } + + if (template.queryForInt("select count(*) from role where id = 7") == 0) { + LOG.info("Role 'podcast' not found in database. Creating it."); + template.execute("insert into role values (7, 'podcast')"); + template.execute("insert into user_role " + + "select distinct u.username, 7 from user u, user_role ur " + + "where u.username = ur.username and ur.role_id = 1"); + LOG.info("Role 'podcast' was created successfully."); + } + + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java new file mode 100644 index 00000000..6f754306 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema33.java @@ -0,0 +1,47 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.3. + * + * @author Sindre Mehus + */ +public class Schema33 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema33.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 9") == 0) { + LOG.info("Updating database schema to version 9."); + template.execute("insert into version values (9)"); + } + + if (!columnExists(template, "client_side_playlist", "player")) { + LOG.info("Database column 'player.client_side_playlist' not found. Creating it."); + template.execute("alter table player add client_side_playlist boolean default false not null"); + LOG.info("Database column 'player.client_side_playlist' was added successfully."); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java new file mode 100644 index 00000000..daaf98ca --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema34.java @@ -0,0 +1,53 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.4. + * + * @author Sindre Mehus + */ +public class Schema34 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema34.class); + + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 10") == 0) { + LOG.info("Updating database schema to version 10."); + template.execute("insert into version values (10)"); + } + + if (!columnExists(template, "ldap_authenticated", "user")) { + LOG.info("Database column 'user.ldap_authenticated' not found. Creating it."); + template.execute("alter table user add ldap_authenticated boolean default false not null"); + LOG.info("Database column 'user.ldap_authenticated' was added successfully."); + } + + if (!columnExists(template, "party_mode_enabled", "user_settings")) { + LOG.info("Database column 'user_settings.party_mode_enabled' not found. Creating it."); + template.execute("alter table user_settings add party_mode_enabled boolean default false not null"); + LOG.info("Database column 'user_settings.party_mode_enabled' was added successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java new file mode 100644 index 00000000..56b5073d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema35.java @@ -0,0 +1,151 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.apache.commons.io.IOUtils; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.5. + * + * @author Sindre Mehus + */ +public class Schema35 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema35.class); + + private static final String[] AVATARS = { + "Formal", "Engineer", "Footballer", "Green-Boy", + + "Linux-Zealot", "Mac-Zealot", "Windows-Zealot", "Army-Officer", "Beatnik", + "All-Caps", "Clown", "Commie-Pinko", "Forum-Flirt", "Gamer", "Hopelessly-Addicted", + "Jekyll-And-Hyde", "Joker", "Lurker", "Moderator", "Newbie", "No-Dissent", + "Performer", "Push-My-Button", "Ray-Of-Sunshine", "Red-Hot-Chili-Peppers-1", + "Red-Hot-Chili-Peppers-2", "Red-Hot-Chili-Peppers-3", "Red-Hot-Chili-Peppers-4", + "Ringmaster", "Rumor-Junkie", "Sozzled-Surfer", "Statistician", "Tech-Support", + "The-Guru", "The-Referee", "Troll", "Uptight", + + "Fire-Guitar", "Drum", "Headphones", "Mic", "Turntable", "Vinyl", + + "Cool", "Laugh", "Study" + }; + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 11") == 0) { + LOG.info("Updating database schema to version 11."); + template.execute("insert into version values (11)"); + } + + if (!columnExists(template, "now_playing_allowed", "user_settings")) { + LOG.info("Database column 'user_settings.now_playing_allowed' not found. Creating it."); + template.execute("alter table user_settings add now_playing_allowed boolean default true not null"); + LOG.info("Database column 'user_settings.now_playing_allowed' was added successfully."); + } + + if (!columnExists(template, "web_player_default", "user_settings")) { + LOG.info("Database column 'user_settings.web_player_default' not found. Creating it."); + template.execute("alter table user_settings add web_player_default boolean default false not null"); + LOG.info("Database column 'user_settings.web_player_default' was added successfully."); + } + + if (template.queryForInt("select count(*) from role where id = 8") == 0) { + LOG.info("Role 'stream' not found in database. Creating it."); + template.execute("insert into role values (8, 'stream')"); + template.execute("insert into user_role select distinct u.username, 8 from user u"); + LOG.info("Role 'stream' was created successfully."); + } + + if (!tableExists(template, "system_avatar")) { + LOG.info("Database table 'system_avatar' not found. Creating it."); + template.execute("create table system_avatar (" + + "id identity," + + "name varchar," + + "created_date datetime not null," + + "mime_type varchar not null," + + "width int not null," + + "height int not null," + + "data binary not null)"); + LOG.info("Database table 'system_avatar' was created successfully."); + } + + for (String avatar : AVATARS) { + createAvatar(template, avatar); + } + + if (!tableExists(template, "custom_avatar")) { + LOG.info("Database table 'custom_avatar' not found. Creating it."); + template.execute("create table custom_avatar (" + + "id identity," + + "name varchar," + + "created_date datetime not null," + + "mime_type varchar not null," + + "width int not null," + + "height int not null," + + "data binary not null," + + "username varchar not null," + + "foreign key (username) references user(username) on delete cascade)"); + LOG.info("Database table 'custom_avatar' was created successfully."); + } + + if (!columnExists(template, "avatar_scheme", "user_settings")) { + LOG.info("Database column 'user_settings.avatar_scheme' not found. Creating it."); + template.execute("alter table user_settings add avatar_scheme varchar default 'NONE' not null"); + LOG.info("Database column 'user_settings.avatar_scheme' was added successfully."); + } + + if (!columnExists(template, "system_avatar_id", "user_settings")) { + LOG.info("Database column 'user_settings.system_avatar_id' not found. Creating it."); + template.execute("alter table user_settings add system_avatar_id int"); + template.execute("alter table user_settings add foreign key (system_avatar_id) references system_avatar(id)"); + LOG.info("Database column 'user_settings.system_avatar_id' was added successfully."); + } + + if (!columnExists(template, "jukebox", "player")) { + LOG.info("Database column 'player.jukebox' not found. Creating it."); + template.execute("alter table player add jukebox boolean default false not null"); + LOG.info("Database column 'player.jukebox' was added successfully."); + } + } + + private void createAvatar(JdbcTemplate template, String avatar) { + if (template.queryForInt("select count(*) from system_avatar where name = ?", new Object[]{avatar}) == 0) { + + InputStream in = null; + try { + in = getClass().getResourceAsStream(avatar + ".png"); + byte[] imageData = IOUtils.toByteArray(in); + template.update("insert into system_avatar values (null, ?, ?, ?, ?, ?, ?)", + new Object[]{avatar, new Date(), "image/png", 48, 48, imageData}); + LOG.info("Created avatar '" + avatar + "'."); + } catch (IOException x) { + LOG.error("Failed to create avatar '" + avatar + "'.", x); + } finally { + IOUtils.closeQuietly(in); + } + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java new file mode 100644 index 00000000..caed6cdb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema36.java @@ -0,0 +1,48 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implementes the database schema for Subsonic version 3.6. + * + * @author Sindre Mehus + */ +public class Schema36 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema36.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 12") == 0) { + LOG.info("Updating database schema to version 12."); + template.execute("insert into version values (12)"); + } + + if (!columnExists(template, "technology", "player")) { + LOG.info("Database column 'player.technology' not found. Creating it."); + template.execute("alter table player add technology varchar default 'WEB' not null"); + LOG.info("Database column 'player.technology' was added successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.java new file mode 100644 index 00000000..afb8fb6e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema37.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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 3.7. + * + * @author Sindre Mehus + */ +public class Schema37 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema37.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 13") == 0) { + LOG.info("Updating database schema to version 13."); + template.execute("insert into version values (13)"); + } + + if (template.queryForInt("select count(*) from role where id = 9") == 0) { + LOG.info("Role 'settings' not found in database. Creating it."); + template.execute("insert into role values (9, 'settings')"); + template.execute("insert into user_role select distinct u.username, 9 from user u"); + LOG.info("Role 'settings' was created successfully."); + } + + if (template.queryForInt("select count(*) from role where id = 10") == 0) { + LOG.info("Role 'jukebox' not found in database. Creating it."); + template.execute("insert into role values (10, 'jukebox')"); + template.execute("insert into user_role " + + "select distinct u.username, 10 from user u, user_role ur " + + "where u.username = ur.username and ur.role_id = 1"); + LOG.info("Role 'jukebox' was created successfully."); + } + + if (!columnExists(template, "changed", "music_folder")) { + LOG.info("Database column 'music_folder.changed' not found. Creating it."); + template.execute("alter table music_folder add changed datetime default 0 not null"); + LOG.info("Database column 'music_folder.changed' was added successfully."); + } + + if (!columnExists(template, "changed", "internet_radio")) { + LOG.info("Database column 'internet_radio.changed' not found. Creating it."); + template.execute("alter table internet_radio add changed datetime default 0 not null"); + LOG.info("Database column 'internet_radio.changed' was added successfully."); + } + + if (!columnExists(template, "changed", "user_settings")) { + LOG.info("Database column 'user_settings.changed' not found. Creating it."); + template.execute("alter table user_settings add changed datetime default 0 not null"); + LOG.info("Database column 'user_settings.changed' was added successfully."); + } + + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java new file mode 100644 index 00000000..fac49511 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema38.java @@ -0,0 +1,54 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 3.8. + * + * @author Sindre Mehus + */ +public class Schema38 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema38.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 14") == 0) { + LOG.info("Updating database schema to version 14."); + template.execute("insert into version values (14)"); + } + + if (!columnExists(template, "client_id", "player")) { + LOG.info("Database column 'player.client_id' not found. Creating it."); + template.execute("alter table player add client_id varchar"); + LOG.info("Database column 'player.client_id' was added successfully."); + } + + if (!columnExists(template, "show_chat", "user_settings")) { + LOG.info("Database column 'user_settings.show_chat' not found. Creating it."); + template.execute("alter table user_settings add show_chat boolean default true not null"); + LOG.info("Database column 'user_settings.show_chat' was added successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.java new file mode 100644 index 00000000..e01d1ef0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema40.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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 4.0. + * + * @author Sindre Mehus + */ +public class Schema40 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema40.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 15") == 0) { + LOG.info("Updating database schema to version 15."); + template.execute("insert into version values (15)"); + + // Reset stream byte count since they have been wrong in earlier releases. + template.execute("update user set bytes_streamed = 0"); + LOG.info("Reset stream byte count statistics."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java new file mode 100644 index 00000000..cba1572c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema43.java @@ -0,0 +1,65 @@ +/* + 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.dao.schema; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; + +import java.util.Arrays; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 4.3. + * + * @author Sindre Mehus + */ +public class Schema43 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema43.class); + + @Override + public void execute(JdbcTemplate template) { + + // version 16 was used for 4.3.beta1 + if (template.queryForInt("select count(*) from version where version = 16") == 0) { + LOG.info("Updating database schema to version 16."); + template.execute("insert into version values (16)"); + } + + if (template.queryForInt("select count(*) from version where version = 17") == 0) { + LOG.info("Updating database schema to version 17."); + template.execute("insert into version values (17)"); + + for (String format : Arrays.asList("avi", "mpg", "mpeg", "mp4", "m4v", "mkv", "mov", "wmv", "ogv")) { + template.update("delete from transcoding where source_format=? and target_format=?", new Object[] {format, "flv"}); + template.execute("insert into transcoding values(null,'" + format + " > flv' ,'" + format + "' ,'flv','ffmpeg -ss %o -i %s -async 1 -b %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f flv -',null,null,true,true)"); + template.execute("insert into player_transcoding select p.id as player_id, t.id as transaction_id from player p, transcoding t where t.name = '" + format + " > flv'"); + } + LOG.info("Created video transcoding configuration."); + } + + if (!columnExists(template, "email", "user")) { + LOG.info("Database column 'user.email' not found. Creating it."); + template.execute("alter table user add email varchar"); + LOG.info("Database column 'user.email' was added successfully."); + } + + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java new file mode 100644 index 00000000..d82f2a92 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema45.java @@ -0,0 +1,76 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 4.5. + * + * @author Sindre Mehus + */ +public class Schema45 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema45.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 18") == 0) { + LOG.info("Updating database schema to version 18."); + template.execute("insert into version values (18)"); + } + + if (template.queryForInt("select count(*) from role where id = 11") == 0) { + LOG.info("Role 'share' not found in database. Creating it."); + template.execute("insert into role values (11, 'share')"); + template.execute("insert into user_role " + + "select distinct u.username, 11 from user u, user_role ur " + + "where u.username = ur.username and ur.role_id = 1"); + LOG.info("Role 'share' was created successfully."); + } + + if (!tableExists(template, "share")) { + LOG.info("Table 'share' not found in database. Creating it."); + template.execute("create cached table share (" + + "id identity," + + "name varchar not null," + + "description varchar," + + "username varchar not null," + + "created datetime not null," + + "expires datetime," + + "last_visited datetime," + + "visit_count int default 0 not null," + + "unique (name)," + + "foreign key (username) references user(username) on delete cascade)"); + template.execute("create index idx_share_name on share(name)"); + + LOG.info("Table 'share' was created successfully."); + LOG.info("Table 'share_file' not found in database. Creating it."); + template.execute("create cached table share_file (" + + "id identity," + + "share_id int not null," + + "path varchar not null," + + "foreign key (share_id) references share(id) on delete cascade)"); + LOG.info("Table 'share_file' was created successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java new file mode 100644 index 00000000..c1fcf357 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema46.java @@ -0,0 +1,87 @@ +/* + 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.dao.schema; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 4.6. + * + * @author Sindre Mehus + */ +public class Schema46 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema46.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 19") == 0) { + LOG.info("Updating database schema to version 19."); + template.execute("insert into version values (19)"); + } + + if (!tableExists(template, "transcoding2")) { + LOG.info("Database table 'transcoding2' not found. Creating it."); + template.execute("create table transcoding2 (" + + "id identity," + + "name varchar not null," + + "source_formats varchar not null," + + "target_format varchar not null," + + "step1 varchar not null," + + "step2 varchar," + + "step3 varchar)"); + + template.execute("insert into transcoding2 values(null,'mp3 audio'," + + "'ogg oga aac m4a flac wav wma aif aiff ape mpc shn', 'mp3', " + + "'ffmpeg -i %s -ab %bk -v 0 -f mp3 -', null, null)"); + + template.execute("insert into transcoding2 values(null,'flv/h264 video', " + + "'avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts', 'flv', " + + "'ffmpeg -ss %o -i %s -async 1 -b %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f flv -vcodec libx264 -preset superfast -threads 0 -', null, null)"); + + LOG.info("Database table 'transcoding2' was created successfully."); + } + + if (!tableExists(template, "player_transcoding2")) { + LOG.info("Database table 'player_transcoding2' not found. Creating it."); + template.execute("create table player_transcoding2 (" + + "player_id int not null," + + "transcoding_id int not null," + + "primary key (player_id, transcoding_id)," + + "foreign key (player_id) references player(id) on delete cascade," + + "foreign key (transcoding_id) references transcoding2(id) on delete cascade)"); + + template.execute("insert into player_transcoding2(player_id, transcoding_id) " + + "select distinct p.id, t.id from player p, transcoding2 t"); + + LOG.info("Database table 'player_transcoding2' was created successfully."); + } + + if (!columnExists(template, "default_active", "transcoding2")) { + LOG.info("Database column 'transcoding2.default_active' not found. Creating it."); + template.execute("alter table transcoding2 add default_active boolean default true not null"); + LOG.info("Database column 'transcoding2.default_active' was added successfully."); + } + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java new file mode 100644 index 00000000..8b290b47 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema47.java @@ -0,0 +1,234 @@ +/* + 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.dao.schema; + +import net.sourceforge.subsonic.Logger; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 4.7. + * + * @author Sindre Mehus + */ +public class Schema47 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema47.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 20") == 0) { + LOG.info("Updating database schema to version 20."); + template.execute("insert into version values (20)"); + } + + if (!tableExists(template, "media_file")) { + LOG.info("Database table 'media_file' not found. Creating it."); + template.execute("create cached table media_file (" + + "id identity," + + "path varchar not null," + + "folder varchar," + + "type varchar not null," + + "format varchar," + + "title varchar," + + "album varchar," + + "artist varchar," + + "album_artist varchar," + + "disc_number int," + + "track_number int," + + "year int," + + "genre varchar," + + "bit_rate int," + + "variable_bit_rate boolean not null," + + "duration_seconds int," + + "file_size bigint," + + "width int," + + "height int," + + "cover_art_path varchar," + + "parent_path varchar," + + "play_count int not null," + + "last_played datetime," + + "comment varchar," + + "created datetime not null," + + "changed datetime not null," + + "last_scanned datetime not null," + + "children_last_updated datetime not null," + + "present boolean not null," + + "version int not null," + + "unique (path))"); + + template.execute("create index idx_media_file_path on media_file(path)"); + template.execute("create index idx_media_file_parent_path on media_file(parent_path)"); + template.execute("create index idx_media_file_type on media_file(type)"); + template.execute("create index idx_media_file_album on media_file(album)"); + template.execute("create index idx_media_file_artist on media_file(artist)"); + template.execute("create index idx_media_file_album_artist on media_file(album_artist)"); + template.execute("create index idx_media_file_present on media_file(present)"); + template.execute("create index idx_media_file_genre on media_file(genre)"); + template.execute("create index idx_media_file_play_count on media_file(play_count)"); + template.execute("create index idx_media_file_created on media_file(created)"); + template.execute("create index idx_media_file_last_played on media_file(last_played)"); + + LOG.info("Database table 'media_file' was created successfully."); + } + + if (!tableExists(template, "artist")) { + LOG.info("Database table 'artist' not found. Creating it."); + template.execute("create cached table artist (" + + "id identity," + + "name varchar not null," + + "cover_art_path varchar," + + "album_count int default 0 not null," + + "last_scanned datetime not null," + + "present boolean not null," + + "unique (name))"); + + template.execute("create index idx_artist_name on artist(name)"); + template.execute("create index idx_artist_present on artist(present)"); + + LOG.info("Database table 'artist' was created successfully."); + } + + if (!tableExists(template, "album")) { + LOG.info("Database table 'album' not found. Creating it."); + template.execute("create cached table album (" + + "id identity," + + "path varchar not null," + + "name varchar not null," + + "artist varchar not null," + + "song_count int default 0 not null," + + "duration_seconds int default 0 not null," + + "cover_art_path varchar," + + "play_count int default 0 not null," + + "last_played datetime," + + "comment varchar," + + "created datetime not null," + + "last_scanned datetime not null," + + "present boolean not null," + + "unique (artist, name))"); + + template.execute("create index idx_album_artist_name on album(artist, name)"); + template.execute("create index idx_album_play_count on album(play_count)"); + template.execute("create index idx_album_last_played on album(last_played)"); + template.execute("create index idx_album_present on album(present)"); + + LOG.info("Database table 'album' was created successfully."); + } + + if (!tableExists(template, "starred_media_file")) { + LOG.info("Database table 'starred_media_file' not found. Creating it."); + template.execute("create table starred_media_file (" + + "id identity," + + "media_file_id int not null," + + "username varchar not null," + + "created datetime not null," + + "foreign key (media_file_id) references media_file(id) on delete cascade,"+ + "foreign key (username) references user(username) on delete cascade," + + "unique (media_file_id, username))"); + + template.execute("create index idx_starred_media_file_media_file_id on starred_media_file(media_file_id)"); + template.execute("create index idx_starred_media_file_username on starred_media_file(username)"); + + LOG.info("Database table 'starred_media_file' was created successfully."); + } + + if (!tableExists(template, "starred_album")) { + LOG.info("Database table 'starred_album' not found. Creating it."); + template.execute("create table starred_album (" + + "id identity," + + "album_id int not null," + + "username varchar not null," + + "created datetime not null," + + "foreign key (album_id) references album(id) on delete cascade," + + "foreign key (username) references user(username) on delete cascade," + + "unique (album_id, username))"); + + template.execute("create index idx_starred_album_album_id on starred_album(album_id)"); + template.execute("create index idx_starred_album_username on starred_album(username)"); + + LOG.info("Database table 'starred_album' was created successfully."); + } + + if (!tableExists(template, "starred_artist")) { + LOG.info("Database table 'starred_artist' not found. Creating it."); + template.execute("create table starred_artist (" + + "id identity," + + "artist_id int not null," + + "username varchar not null," + + "created datetime not null," + + "foreign key (artist_id) references artist(id) on delete cascade,"+ + "foreign key (username) references user(username) on delete cascade," + + "unique (artist_id, username))"); + + template.execute("create index idx_starred_artist_artist_id on starred_artist(artist_id)"); + template.execute("create index idx_starred_artist_username on starred_artist(username)"); + + LOG.info("Database table 'starred_artist' was created successfully."); + } + + if (!tableExists(template, "playlist")) { + LOG.info("Database table 'playlist' not found. Creating it."); + template.execute("create table playlist (" + + "id identity," + + "username varchar not null," + + "is_public boolean not null," + + "name varchar not null," + + "comment varchar," + + "file_count int default 0 not null," + + "duration_seconds int default 0 not null," + + "created datetime not null," + + "changed datetime not null," + + "foreign key (username) references user(username) on delete cascade)"); + + LOG.info("Database table 'playlist' was created successfully."); + } + + if (!columnExists(template, "imported_from", "playlist")) { + LOG.info("Database column 'playlist.imported_from' not found. Creating it."); + template.execute("alter table playlist add imported_from varchar"); + LOG.info("Database column 'playlist.imported_from' was added successfully."); + } + + if (!tableExists(template, "playlist_file")) { + LOG.info("Database table 'playlist_file' not found. Creating it."); + template.execute("create cached table playlist_file (" + + "id identity," + + "playlist_id int not null," + + "media_file_id int not null," + + "foreign key (playlist_id) references playlist(id) on delete cascade," + + "foreign key (media_file_id) references media_file(id) on delete cascade)"); + + LOG.info("Database table 'playlist_file' was created successfully."); + } + + if (!tableExists(template, "playlist_user")) { + LOG.info("Database table 'playlist_user' not found. Creating it."); + template.execute("create table playlist_user (" + + "id identity," + + "playlist_id int not null," + + "username varchar not null," + + "unique(playlist_id, username)," + + "foreign key (playlist_id) references playlist(id) on delete cascade," + + "foreign key (username) references user(username) on delete cascade)"); + + LOG.info("Database table 'playlist_user' was created successfully."); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java new file mode 100644 index 00000000..23e8afaf --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java @@ -0,0 +1,166 @@ +/* + 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.domain; + +import java.util.Date; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class Album { + + private int id; + private String path; + private String name; + private String artist; + private int songCount; + private int durationSeconds; + private String coverArtPath; + private int playCount; + private Date lastPlayed; + private String comment; + private Date created; + private Date lastScanned; + private boolean present; + + public Album() { + } + + public Album(int id, String path, String name, String artist, int songCount, int durationSeconds, String coverArtPath, + int playCount, Date lastPlayed, String comment, Date created, Date lastScanned, boolean present) { + this.id = id; + this.path = path; + this.name = name; + this.artist = artist; + this.songCount = songCount; + this.durationSeconds = durationSeconds; + this.coverArtPath = coverArtPath; + this.playCount = playCount; + this.lastPlayed = lastPlayed; + this.comment = comment; + this.created = created; + this.lastScanned = lastScanned; + this.present = present; + } + + 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 getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public int getSongCount() { + return songCount; + } + + public void setSongCount(int songCount) { + this.songCount = songCount; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getCoverArtPath() { + return coverArtPath; + } + + public void setCoverArtPath(String coverArtPath) { + this.coverArtPath = coverArtPath; + } + + public int getPlayCount() { + return playCount; + } + + public void setPlayCount(int playCount) { + this.playCount = playCount; + } + + public Date getLastPlayed() { + return lastPlayed; + } + + public void setLastPlayed(Date lastPlayed) { + this.lastPlayed = lastPlayed; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getLastScanned() { + return lastScanned; + } + + public void setLastScanned(Date lastScanned) { + this.lastScanned = lastScanned; + } + + public boolean isPresent() { + return present; + } + + public void setPresent(boolean present) { + this.present = present; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java new file mode 100644 index 00000000..e6141f78 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.java @@ -0,0 +1,95 @@ +/* + 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.domain; + +import java.util.Date; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class Artist { + + private int id; + private String name; + private String coverArtPath; + private int albumCount; + private Date lastScanned; + private boolean present; + + public Artist() { + } + + public Artist(int id, String name, String coverArtPath, int albumCount, Date lastScanned, boolean present) { + this.id = id; + this.name = name; + this.coverArtPath = coverArtPath; + this.albumCount = albumCount; + this.lastScanned = lastScanned; + this.present = present; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCoverArtPath() { + return coverArtPath; + } + + public void setCoverArtPath(String coverArtPath) { + this.coverArtPath = coverArtPath; + } + + public int getAlbumCount() { + return albumCount; + } + + public void setAlbumCount(int albumCount) { + this.albumCount = albumCount; + } + + public Date getLastScanned() { + return lastScanned; + } + + public void setLastScanned(Date lastScanned) { + this.lastScanned = lastScanned; + } + + public boolean isPresent() { + return present; + } + + public void setPresent(boolean present) { + this.present = present; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java new file mode 100644 index 00000000..0089a8a3 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Avatar.java @@ -0,0 +1,75 @@ +/* + 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.domain; + +import java.util.Date; + +/** + * An icon representing a user. + * + * @author Sindre Mehus + */ +public class Avatar { + + private int id; + private String name; + private Date createdDate; + private String mimeType; + private int width; + private int height; + private byte[] data; + + public Avatar(int id, String name, Date createdDate, String mimeType, int width, int height, byte[] data) { + this.id = id; + this.name = name; + this.createdDate = createdDate; + this.mimeType = mimeType; + this.width = width; + this.height = height; + this.data = data; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public Date getCreatedDate() { + return createdDate; + } + + public String getMimeType() { + return mimeType; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public byte[] getData() { + return data; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.java new file mode 100644 index 00000000..024dcb24 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AvatarScheme.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.domain; + +/** + * Enumeration of avatar schemes. + * + * @author Sindre Mehus + */ +public enum AvatarScheme { + + /** + * No avatar should be displayed. + */ + NONE(-1), + + /** + * One of the system avatars should be displayed. + */ + SYSTEM(0), + + /** + * The custom avatar should be displayed. + */ + CUSTOM(-2); + + private final int code; + + AvatarScheme(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java new file mode 100644 index 00000000..bb52eff7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CacheElement.java @@ -0,0 +1,65 @@ +/* + 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.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class CacheElement { + + private final long id; + private final int type; + private final String key; + private final Object value; + private final long created; + + public CacheElement(int type, String key, Object value, long created) { + this.type = type; + this.key = key; + this.value = value; + this.created = created; + + id = createId(type, key); + } + + public static long createId(int type, String key) { + return ((long) type << 32) | Math.abs(key.hashCode()); + } + + public long getId() { + return id; + } + + public int getType() { + return type; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + public long getCreated() { + return created; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java new file mode 100644 index 00000000..91293e9f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.java @@ -0,0 +1,48 @@ +/* + 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.domain; + +/** + * Enumeration of cover art schemes. Each value contains a size, which indicates how big the + * scaled covert art images should be. + * + * @author Sindre Mehus + * @version $Revision: 1.3 $ $Date: 2005/06/15 18:10:40 $ + */ +public enum CoverArtScheme { + + OFF(0), + SMALL(70), + MEDIUM(100), + LARGE(150); + + private int size; + + CoverArtScheme(int size) { + this.size = size; + } + + /** + * Returns the covert art size for this scheme. + * @return the covert art size for this scheme. + */ + public int getSize() { + return size; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java new file mode 100644 index 00000000..ae0c1f67 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/InternetRadio.java @@ -0,0 +1,168 @@ +/* + 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.domain; + +import java.util.Date; + +/** + * Represents an internet radio station. + * + * @author Sindre Mehus + * @version $Revision: 1.2 $ $Date: 2005/12/25 13:48:46 $ + */ +public class InternetRadio { + + private Integer id; + private String name; + private String streamUrl; + private String homepageUrl; + private boolean isEnabled; + private Date changed; + + /** + * Creates a new internet radio station. + * + * @param id The system-generated ID. + * @param name The user-defined name. + * @param streamUrl The stream URL for the station. + * @param homepageUrl The home page URL for the station. + * @param isEnabled Whether the station is enabled. + * @param changed When the corresponding database entry was last changed. + */ + public InternetRadio(Integer id, String name, String streamUrl, String homepageUrl, boolean isEnabled, Date changed) { + this.id = id; + this.name = name; + this.streamUrl = streamUrl; + this.homepageUrl = homepageUrl; + this.isEnabled = isEnabled; + this.changed = changed; + } + + /** + * Creates a new internet radio station. + * + * @param name The user-defined name. + * @param streamUrl The URL for the station. + * @param homepageUrl The home page URL for the station. + * @param isEnabled Whether the station is enabled. + * @param changed When the corresponding database entry was last changed. + */ + public InternetRadio(String name, String streamUrl, String homepageUrl, boolean isEnabled, Date changed) { + this(null, name, streamUrl, homepageUrl, isEnabled, changed); + } + + /** + * Returns the system-generated ID. + * + * @return The system-generated ID. + */ + public Integer getId() { + return id; + } + + /** + * Returns the user-defined name. + * + * @return The user-defined name. + */ + public String getName() { + return name; + } + + /** + * Sets the user-defined name. + * + * @param name The user-defined name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the stream URL of the radio station. + * + * @return The stream URL of the radio station. + */ + public String getStreamUrl() { + return streamUrl; + } + + /** + * Sets the stream URL of the radio station. + * + * @param streamUrl The stream URL of the radio station. + */ + public void setStreamUrl(String streamUrl) { + this.streamUrl = streamUrl; + } + + /** + * Returns the homepage URL of the radio station. + * + * @return The homepage URL of the radio station. + */ + public String getHomepageUrl() { + return homepageUrl; + } + + /** + * Sets the home page URL of the radio station. + * + * @param homepageUrl The home page URL of the radio station. + */ + public void setHomepageUrl(String homepageUrl) { + this.homepageUrl = homepageUrl; + } + + /** + * Returns whether the radio station is enabled. + * + * @return Whether the radio station is enabled. + */ + public boolean isEnabled() { + return isEnabled; + } + + /** + * Sets whether the radio station is enabled. + * + * @param enabled Whether the radio station is enabled. + */ + public void setEnabled(boolean enabled) { + isEnabled = enabled; + } + + /** + * Returns when the corresponding database entry was last changed. + * + * @return When the corresponding database entry was last changed. + */ + public Date getChanged() { + return changed; + } + + /** + * Sets when the corresponding database entry was last changed. + * + * @param changed When the corresponding database entry was last changed. + */ + public void setChanged(Date changed) { + this.changed = changed; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java new file mode 100644 index 00000000..4f315028 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java @@ -0,0 +1,449 @@ +/* + 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.domain; + +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.util.Date; + +/** + * A media file (audio, video or directory) with an assortment of its meta data. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class MediaFile { + + private int id; + private String path; + private String folder; + private MediaType mediaType; + private String format; + private String title; + private String albumName; + private String artist; + private String albumArtist; + private Integer discNumber; + private Integer trackNumber; + private Integer year; + private String genre; + private Integer bitRate; + private boolean variableBitRate; + private Integer durationSeconds; + private Long fileSize; + private Integer width; + private Integer height; + private String coverArtPath; + private String parentPath; + private int playCount; + private Date lastPlayed; + private String comment; + private Date created; + private Date changed; + private Date lastScanned; + private Date starredDate; + private Date childrenLastUpdated; + private boolean present; + + public MediaFile(int id, String path, String folder, MediaType mediaType, String format, String title, + String albumName, String artist, String albumArtist, Integer discNumber, Integer trackNumber, Integer year, String genre, Integer bitRate, + boolean variableBitRate, Integer durationSeconds, Long fileSize, Integer width, Integer height, String coverArtPath, + String parentPath, int playCount, Date lastPlayed, String comment, Date created, Date changed, Date lastScanned, + Date childrenLastUpdated, boolean present) { + this.id = id; + this.path = path; + this.folder = folder; + this.mediaType = mediaType; + this.format = format; + this.title = title; + this.albumName = albumName; + this.artist = artist; + this.albumArtist = albumArtist; + this.discNumber = discNumber; + this.trackNumber = trackNumber; + this.year = year; + this.genre = genre; + this.bitRate = bitRate; + this.variableBitRate = variableBitRate; + this.durationSeconds = durationSeconds; + this.fileSize = fileSize; + this.width = width; + this.height = height; + this.coverArtPath = coverArtPath; + this.parentPath = parentPath; + this.playCount = playCount; + this.lastPlayed = lastPlayed; + this.comment = comment; + this.created = created; + this.changed = changed; + this.lastScanned = lastScanned; + this.childrenLastUpdated = childrenLastUpdated; + this.present = present; + } + + public MediaFile() { + } + + 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 getFolder() { + return folder; + } + + public void setFolder(String folder) { + this.folder = folder; + } + + public File getFile() { + // TODO: Optimize + return new File(path); + } + + public boolean exists() { + return FileUtil.exists(getFile()); + } + + public MediaType getMediaType() { + return mediaType; + } + + public void setMediaType(MediaType mediaType) { + this.mediaType = mediaType; + } + + public boolean isVideo() { + return mediaType == MediaType.VIDEO; + } + + public boolean isAudio() { + return mediaType == MediaType.MUSIC || mediaType == MediaType.AUDIOBOOK || mediaType == MediaType.PODCAST; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + public boolean isDirectory() { + return !isFile(); + } + + public boolean isFile() { + return mediaType != MediaType.DIRECTORY && mediaType != MediaType.ALBUM; + } + + public boolean isAlbum() { + return mediaType == MediaType.ALBUM; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAlbumName() { + return albumName; + } + + public void setAlbumName(String album) { + this.albumName = album; + } + + public String getArtist() { + return artist; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public String getAlbumArtist() { + return albumArtist; + } + + public void setAlbumArtist(String albumArtist) { + this.albumArtist = albumArtist; + } + + public String getName() { + if (isFile()) { + return title != null ? title : FilenameUtils.getBaseName(path); + } + + return FilenameUtils.getName(path); + } + + public Integer getDiscNumber() { + return discNumber; + } + + public void setDiscNumber(Integer discNumber) { + this.discNumber = discNumber; + } + + public Integer getTrackNumber() { + return trackNumber; + } + + public void setTrackNumber(Integer trackNumber) { + this.trackNumber = trackNumber; + } + + 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; + } + + public Integer getBitRate() { + return bitRate; + } + + public void setBitRate(Integer bitRate) { + this.bitRate = bitRate; + } + + public boolean isVariableBitRate() { + return variableBitRate; + } + + public void setVariableBitRate(boolean variableBitRate) { + this.variableBitRate = variableBitRate; + } + + public Integer getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(Integer durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getDurationString() { + if (durationSeconds == null) { + return null; + } + + StringBuilder result = new StringBuilder(8); + + int seconds = durationSeconds; + + int hours = seconds / 3600; + seconds -= hours * 3600; + + int minutes = seconds / 60; + seconds -= minutes * 60; + + if (hours > 0) { + result.append(hours).append(':'); + if (minutes < 10) { + result.append('0'); + } + } + + result.append(minutes).append(':'); + if (seconds < 10) { + result.append('0'); + } + result.append(seconds); + + return result.toString(); + } + + public Long getFileSize() { + return fileSize; + } + + public void setFileSize(Long fileSize) { + this.fileSize = fileSize; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } + + public String getCoverArtPath() { + return coverArtPath; + } + + public void setCoverArtPath(String coverArtPath) { + this.coverArtPath = coverArtPath; + } + + + public String getParentPath() { + return parentPath; + } + + public void setParentPath(String parentPath) { + this.parentPath = parentPath; + } + + public File getParentFile() { + return getFile().getParentFile(); + } + + public int getPlayCount() { + return playCount; + } + + public void setPlayCount(int playCount) { + this.playCount = playCount; + } + + public Date getLastPlayed() { + return lastPlayed; + } + + public void setLastPlayed(Date lastPlayed) { + this.lastPlayed = lastPlayed; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getChanged() { + return changed; + } + + public void setChanged(Date changed) { + this.changed = changed; + } + + public Date getLastScanned() { + return lastScanned; + } + + public void setLastScanned(Date lastScanned) { + this.lastScanned = lastScanned; + } + + public Date getStarredDate() { + return starredDate; + } + + public void setStarredDate(Date starredDate) { + this.starredDate = starredDate; + } + + /** + * Returns when the children was last updated in the database. + */ + public Date getChildrenLastUpdated() { + return childrenLastUpdated; + } + + public void setChildrenLastUpdated(Date childrenLastUpdated) { + this.childrenLastUpdated = childrenLastUpdated; + } + + public boolean isPresent() { + return present; + } + + public void setPresent(boolean present) { + this.present = present; + } + + @Override + public boolean equals(Object o) { + return o instanceof MediaFile && ((MediaFile) o).path.equals(path); + } + + @Override + public int hashCode() { + return path.hashCode(); + } + + public File getCoverArtFile() { + // TODO: Optimize + return coverArtPath == null ? null : new File(coverArtPath); + } + + @Override + public String toString() { + return getName(); + } + + public static enum MediaType { + MUSIC, + PODCAST, + AUDIOBOOK, + VIDEO, + DIRECTORY, + ALBUM + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java new file mode 100644 index 00000000..13b5bbbd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java @@ -0,0 +1,99 @@ +/* + 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.domain; + +import java.util.Comparator; + +import static net.sourceforge.subsonic.domain.MediaFile.MediaType.DIRECTORY; + +/** + * Comparator for sorting media files. + */ +public class MediaFileComparator implements Comparator<MediaFile> { + + private final boolean sortAlbumsByYear; + + public MediaFileComparator(boolean sortAlbumsByYear) { + this.sortAlbumsByYear = sortAlbumsByYear; + } + + public int compare(MediaFile a, MediaFile b) { + + // Directories before files. + if (a.isFile() && b.isDirectory()) { + return 1; + } + if (a.isDirectory() && b.isFile()) { + return -1; + } + + // Non-album directories before album directories. + if (a.isAlbum() && b.getMediaType() == DIRECTORY) { + return 1; + } + if (a.getMediaType() == DIRECTORY && b.isAlbum()) { + return -1; + } + + // Sort albums by year + if (sortAlbumsByYear && a.isAlbum() && b.isAlbum()) { + int i = nullSafeCompare(a.getYear(), b.getYear(), false); + if (i != 0) { + return i; + } + } + + if (a.isDirectory() && b.isDirectory()) { + return a.getName().compareToIgnoreCase(b.getName()); + } + + // Compare by disc and track numbers, if present. + Integer trackA = getSortableDiscAndTrackNumber(a); + Integer trackB = getSortableDiscAndTrackNumber(b); + int i = nullSafeCompare(trackA, trackB, false); + if (i != 0) { + return i; + } + + return a.getName().compareToIgnoreCase(b.getName()); + } + + private <T extends Comparable<T>> int nullSafeCompare(T a, T b, boolean nullIsSmaller) { + if (a == null && b == null) { + return 0; + } + if (a == null) { + return nullIsSmaller ? -1 : 1; + } + if (b == null) { + return nullIsSmaller ? 1 : -1; + } + return a.compareTo(b); + } + + private Integer getSortableDiscAndTrackNumber(MediaFile file) { + if (file.getTrackNumber() == null) { + return null; + } + + int discNumber = file.getDiscNumber() == null ? 1 : file.getDiscNumber(); + return discNumber * 1000 + file.getTrackNumber(); + } +} + diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java new file mode 100644 index 00000000..c8b0cdd9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.java @@ -0,0 +1,62 @@ +/* + 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.domain; + +/** + * Contains media libaray statistics, including the number of artists, albums and songs. + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2005/11/17 18:29:03 $ + */ +public class MediaLibraryStatistics { + + private int artistCount; + private int albumCount; + private int songCount; + private long totalLengthInBytes; + private long totalDurationInSeconds; + + public MediaLibraryStatistics(int artistCount, int albumCount, int songCount, long totalLengthInBytes, long totalDurationInSeconds) { + this.artistCount = artistCount; + this.albumCount = albumCount; + this.songCount = songCount; + this.totalLengthInBytes = totalLengthInBytes; + this.totalDurationInSeconds = totalDurationInSeconds; + } + + public int getArtistCount() { + return artistCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public int getSongCount() { + return songCount; + } + + public long getTotalLengthInBytes() { + return totalLengthInBytes; + } + + public long getTotalDurationInSeconds() { + return totalDurationInSeconds; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java new file mode 100644 index 00000000..613b0a7f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java @@ -0,0 +1,148 @@ +/* + 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.domain; + +import java.io.File; +import java.io.Serializable; +import java.util.Date; + +/** + * Represents a top level directory in which music or other media is stored. + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2005/11/27 14:32:05 $ + */ +public class MusicFolder implements Serializable { + + private Integer id; + private File path; + private String name; + private boolean isEnabled; + private Date changed; + + /** + * Creates a new music folder. + * + * @param id The system-generated ID. + * @param path The path of the music folder. + * @param name The user-defined name. + * @param enabled Whether the folder is enabled. + * @param changed When the corresponding database entry was last changed. + */ + public MusicFolder(Integer id, File path, String name, boolean enabled, Date changed) { + this.id = id; + this.path = path; + this.name = name; + isEnabled = enabled; + this.changed = changed; + } + + /** + * Creates a new music folder. + * + * @param path The path of the music folder. + * @param name The user-defined name. + * @param enabled Whether the folder is enabled. + * @param changed When the corresponding database entry was last changed. + */ + public MusicFolder(File path, String name, boolean enabled, Date changed) { + this(null, path, name, enabled, changed); + } + + /** + * Returns the system-generated ID. + * + * @return The system-generated ID. + */ + public Integer getId() { + return id; + } + + /** + * Returns the path of the music folder. + * + * @return The path of the music folder. + */ + public File getPath() { + return path; + } + + /** + * Sets the path of the music folder. + * + * @param path The path of the music folder. + */ + public void setPath(File path) { + this.path = path; + } + + /** + * Returns the user-defined name. + * + * @return The user-defined name. + */ + public String getName() { + return name; + } + + /** + * Sets the user-defined name. + * + * @param name The user-defined name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns whether the folder is enabled. + * + * @return Whether the folder is enabled. + */ + public boolean isEnabled() { + return isEnabled; + } + + /** + * Sets whether the folder is enabled. + * + * @param enabled Whether the folder is enabled. + */ + public void setEnabled(boolean enabled) { + isEnabled = enabled; + } + + /** + * Returns when the corresponding database entry was last changed. + * + * @return When the corresponding database entry was last changed. + */ + public Date getChanged() { + return changed; + } + + /** + * Sets when the corresponding database entry was last changed. + * + * @param changed When the corresponding database entry was last changed. + */ + public void setChanged(Date changed) { + this.changed = changed; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java new file mode 100644 index 00000000..5753fa4d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java @@ -0,0 +1,167 @@ +/* + 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.domain; + +import java.util.ArrayList; +import java.util.List; +import java.io.Serializable; + +/** + * A music index is a mapping from an index string to a list of prefixes. A complete index consists of a list of + * <code>MusicIndex</code> instances.<p/> + * <p/> + * For a normal alphabetical index, such a mapping would typically be <em>"A" -> ["A"]</em>. The index can also be used + * to group less frequently used letters, such as <em>"X-Å" -> ["X", "Y", "Z", "Æ", "Ø", "Å"]</em>, or to make multiple + * indexes for frequently used letters, such as <em>"SA" -> ["SA"]</em> and <em>"SO" -> ["SO"]</em><p/> + * <p/> + * Clicking on an index in the user interface will typically bring up a list of all music files that are categorized + * under that index. + * + * @author Sindre Mehus + */ +public class MusicIndex implements Serializable { + + public static final MusicIndex OTHER = new MusicIndex("#"); + + private final String index; + private final List<String> prefixes = new ArrayList<String>(); + + /** + * Creates a new index with the given index string. + * + * @param index The index string, e.g., "A" or "The". + */ + public MusicIndex(String index) { + this.index = index; + } + + /** + * Adds a prefix to this index. Music files that starts with this prefix will be categorized under this index entry. + * + * @param prefix The prefix. + */ + public void addPrefix(String prefix) { + prefixes.add(prefix); + } + + /** + * Returns the index name. + * + * @return The index name. + */ + public String getIndex() { + return index; + } + + /** + * Returns the list of prefixes. + * + * @return The list of prefixes. + */ + public List<String> getPrefixes() { + return prefixes; + } + + /** + * Returns whether this object is equal to another one. + * + * @param o Object to compare to. + * @return <code>true</code> if, and only if, the other object is a <code>MusicIndex</code> with the same + * index name as this one. + */ + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof MusicIndex)) { + return false; + } + + final MusicIndex musicIndex = (MusicIndex) o; + + if (index != null ? !index.equals(musicIndex.index) : musicIndex.index != null) { + return false; + } + + return true; + } + + /** + * Returns a hash code for this object. + * + * @return A hash code for this object. + */ + @Override + public int hashCode() { + return (index != null ? index.hashCode() : 0); + } + + /** + * An artist in an index. + */ + public static class Artist implements Comparable<Artist>, Serializable { + + private final String name; + private final String sortableName; + private final List<MediaFile> mediaFiles = new ArrayList<MediaFile>(); + + public Artist(String name, String sortableName) { + this.name = name; + this.sortableName = sortableName; + } + + public void addMediaFile(MediaFile mediaFile) { + mediaFiles.add(mediaFile); + } + + public String getName() { + return name; + } + + public String getSortableName() { + return sortableName; + } + + public List<MediaFile> getMediaFiles() { + return mediaFiles; + } + + public int compareTo(Artist artist) { + return sortableName.compareToIgnoreCase(artist.sortableName); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Artist artist = (Artist) o; + return sortableName.equalsIgnoreCase(artist.sortableName); + } + + @Override + public int hashCode() { + return sortableName.hashCode(); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java new file mode 100644 index 00000000..ef3821a1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/NATPMPRouter.java @@ -0,0 +1,61 @@ +/* + 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.domain; + +import com.hoodcomputing.natpmp.MapRequestMessage; +import com.hoodcomputing.natpmp.NatPmpDevice; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class NATPMPRouter implements Router { + + private final NatPmpDevice device; + + private NATPMPRouter(NatPmpDevice device) { + this.device = device; + } + + public static NATPMPRouter findRouter() { + try { + return new NATPMPRouter(new NatPmpDevice(false)); + } catch (Exception x) { + return null; + } + } + + public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception { + + // Use one week if lease duration is "forever". + if (leaseDuration == 0) { + leaseDuration = 7 * 24 * 3600; + } + + MapRequestMessage map = new MapRequestMessage(true, internalPort, externalPort, leaseDuration, null); + device.enqueueMessage(map); + device.waitUntilQueueEmpty(); + } + + public void deletePortMapping(int externalPort, int internalPort) throws Exception { + MapRequestMessage map = new MapRequestMessage(true, internalPort, externalPort, 0, null); + device.enqueueMessage(map); + device.waitUntilQueueEmpty(); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java new file mode 100644 index 00000000..97748f72 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java @@ -0,0 +1,417 @@ +/* + 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.domain; + +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * A playlist is a list of music files that are associated to a remote player. + * + * @author Sindre Mehus + */ +public class PlayQueue { + + private List<MediaFile> files = new ArrayList<MediaFile>(); + private boolean repeatEnabled; + private String name = "(unnamed)"; + private Status status = Status.PLAYING; + private RandomSearchCriteria randomSearchCriteria; + + /** + * The index of the current song, or -1 is the end of the playlist is reached. + * Note that both the index and the playlist size can be zero. + */ + private int index = 0; + + /** + * Used for undo functionality. + */ + private List<MediaFile> filesBackup = new ArrayList<MediaFile>(); + private int indexBackup = 0; + + /** + * Returns the user-defined name of the playlist. + * + * @return The name of the playlist, or <code>null</code> if no name has been assigned. + */ + public synchronized String getName() { + return name; + } + + /** + * Sets the user-defined name of the playlist. + * + * @param name The name of the playlist. + */ + public synchronized void setName(String name) { + this.name = name; + } + + /** + * Returns the current song in the playlist. + * + * @return The current song in the playlist, or <code>null</code> if no current song exists. + */ + public synchronized MediaFile getCurrentFile() { + if (index == -1 || index == 0 && size() == 0) { + setStatus(Status.STOPPED); + return null; + } else { + MediaFile file = files.get(index); + + // Remove file from playlist if it doesn't exist. + if (!file.exists()) { + files.remove(index); + index = Math.max(0, Math.min(index, size() - 1)); + return getCurrentFile(); + } + + return file; + } + } + + /** + * Returns all music files in the playlist. + * + * @return All music files in the playlist. + */ + public synchronized List<MediaFile> getFiles() { + return files; + } + + /** + * Returns the music file at the given index. + * + * @param index The index. + * @return The music file at the given index. + * @throws IndexOutOfBoundsException If the index is out of range. + */ + public synchronized MediaFile getFile(int index) { + return files.get(index); + } + + /** + * Skip to the next song in the playlist. + */ + public synchronized void next() { + index++; + + // Reached the end? + if (index >= size()) { + index = isRepeatEnabled() ? 0 : -1; + } + } + + /** + * Returns the number of songs in the playlists. + * + * @return The number of songs in the playlists. + */ + public synchronized int size() { + return files.size(); + } + + /** + * Returns whether the playlist is empty. + * + * @return Whether the playlist is empty. + */ + public synchronized boolean isEmpty() { + return files.isEmpty(); + } + + /** + * Returns the index of the current song. + * + * @return The index of the current song, or -1 if the end of the playlist is reached. + */ + public synchronized int getIndex() { + return index; + } + + /** + * Sets the index of the current song. + * + * @param index The index of the current song. + */ + public synchronized void setIndex(int index) { + makeBackup(); + this.index = Math.max(0, Math.min(index, size() - 1)); + setStatus(Status.PLAYING); + } + + /** + * Adds one or more music file to the playlist. + * + * @param append Whether existing songs in the playlist should be kept. + * @param mediaFiles The music files to add. + * @throws IOException If an I/O error occurs. + */ + public synchronized void addFiles(boolean append, Iterable<MediaFile> mediaFiles) throws IOException { + makeBackup(); + if (!append) { + index = 0; + files.clear(); + } + for (MediaFile mediaFile : mediaFiles) { + files.add(mediaFile); + } + setStatus(Status.PLAYING); + } + + /** + * Convenience method, equivalent to {@link #addFiles(boolean, Iterable)}. + */ + public synchronized void addFiles(boolean append, MediaFile... mediaFiles) throws IOException { + addFiles(append, Arrays.asList(mediaFiles)); + } + + /** + * Removes the music file at the given index. + * + * @param index The playlist index. + */ + public synchronized void removeFileAt(int index) { + makeBackup(); + index = Math.max(0, Math.min(index, size() - 1)); + if (this.index > index) { + this.index--; + } + files.remove(index); + + if (index != -1) { + this.index = Math.max(0, Math.min(this.index, size() - 1)); + } + } + + /** + * Clears the playlist. + */ + public synchronized void clear() { + makeBackup(); + files.clear(); + index = 0; + } + + /** + * Shuffles the playlist. + */ + public synchronized void shuffle() { + makeBackup(); + MediaFile currentFile = getCurrentFile(); + Collections.shuffle(files); + if (currentFile != null) { + index = files.indexOf(currentFile); + } + } + + /** + * Sorts the playlist according to the given sort order. + */ + public synchronized void sort(final SortOrder sortOrder) { + makeBackup(); + MediaFile currentFile = getCurrentFile(); + + Comparator<MediaFile> comparator = new Comparator<MediaFile>() { + public int compare(MediaFile a, MediaFile b) { + switch (sortOrder) { + case TRACK: + Integer trackA = a.getTrackNumber(); + Integer trackB = b.getTrackNumber(); + if (trackA == null) { + trackA = 0; + } + if (trackB == null) { + trackB = 0; + } + return trackA.compareTo(trackB); + + case ARTIST: + String artistA = StringUtils.trimToEmpty(a.getArtist()); + String artistB = StringUtils.trimToEmpty(b.getArtist()); + return artistA.compareTo(artistB); + + case ALBUM: + String albumA = StringUtils.trimToEmpty(a.getAlbumName()); + String albumB = StringUtils.trimToEmpty(b.getAlbumName()); + return albumA.compareTo(albumB); + default: + return 0; + } + } + }; + + Collections.sort(files, comparator); + if (currentFile != null) { + index = files.indexOf(currentFile); + } + } + + /** + * Moves the song at the given index one step up. + * + * @param index The playlist index. + */ + public synchronized void moveUp(int index) { + makeBackup(); + if (index <= 0 || index >= size()) { + return; + } + Collections.swap(files, index, index - 1); + + if (this.index == index) { + this.index--; + } else if (this.index == index - 1) { + this.index++; + } + } + + /** + * Moves the song at the given index one step down. + * + * @param index The playlist index. + */ + public synchronized void moveDown(int index) { + makeBackup(); + if (index < 0 || index >= size() - 1) { + return; + } + Collections.swap(files, index, index + 1); + + if (this.index == index) { + this.index++; + } else if (this.index == index + 1) { + this.index--; + } + } + + /** + * Returns whether the playlist is repeating. + * + * @return Whether the playlist is repeating. + */ + public synchronized boolean isRepeatEnabled() { + return repeatEnabled; + } + + /** + * Sets whether the playlist is repeating. + * + * @param repeatEnabled Whether the playlist is repeating. + */ + public synchronized void setRepeatEnabled(boolean repeatEnabled) { + this.repeatEnabled = repeatEnabled; + } + + /** + * Revert the last operation. + */ + public synchronized void undo() { + List<MediaFile> filesTmp = new ArrayList<MediaFile>(files); + int indexTmp = index; + + index = indexBackup; + files = filesBackup; + + indexBackup = indexTmp; + filesBackup = filesTmp; + } + + /** + * Returns the playlist status. + * + * @return The playlist status. + */ + public synchronized Status getStatus() { + return status; + } + + /** + * Sets the playlist status. + * + * @param status The playlist status. + */ + public synchronized void setStatus(Status status) { + this.status = status; + if (index == -1) { + index = Math.max(0, Math.min(index, size() - 1)); + } + } + + /** + * Returns the criteria used to generate this random playlist. + * + * @return The search criteria, or <code>null</code> if this is not a random playlist. + */ + public synchronized RandomSearchCriteria getRandomSearchCriteria() { + return randomSearchCriteria; + } + + /** + * Sets the criteria used to generate this random playlist. + * + * @param randomSearchCriteria The search criteria, or <code>null</code> if this is not a random playlist. + */ + public synchronized void setRandomSearchCriteria(RandomSearchCriteria randomSearchCriteria) { + this.randomSearchCriteria = randomSearchCriteria; + } + + /** + * Returns the total length in bytes. + * + * @return The total length in bytes. + */ + public synchronized long length() { + long length = 0; + for (MediaFile mediaFile : files) { + length += mediaFile.getFileSize(); + } + return length; + } + + private void makeBackup() { + filesBackup = new ArrayList<MediaFile>(files); + indexBackup = index; + } + + /** + * Playlist status. + */ + public enum Status { + PLAYING, + STOPPED + } + + /** + * Playlist sort order. + */ + public enum SortOrder { + TRACK, + ARTIST, + ALBUM + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java new file mode 100644 index 00000000..e1780936 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java @@ -0,0 +1,338 @@ +/* + 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.domain; + +import org.apache.commons.lang.StringUtils; + +import java.util.Date; + +/** + * Represens a remote player. A player has a unique ID, a user-defined name, a logged-on user, + * miscellaneous identifiers, and an associated playlist. + * + * @author Sindre Mehus + */ +public class Player { + + private String id; + private String name; + private PlayerTechnology technology = PlayerTechnology.WEB; + private String clientId; + private String type; + private String username; + private String ipAddress; + private boolean isDynamicIp = true; + private boolean isAutoControlEnabled = true; + private Date lastSeen; + private CoverArtScheme coverArtScheme = CoverArtScheme.MEDIUM; + private TranscodeScheme transcodeScheme = TranscodeScheme.OFF; + private PlayQueue playQueue; + + /** + * Returns the player ID. + * + * @return The player ID. + */ + public String getId() { + return id; + } + + /** + * Sets the player ID. + * + * @param id The player ID. + */ + public void setId(String id) { + this.id = id; + } + + /** + * Returns the user-defined player name. + * + * @return The user-defined player name. + */ + public String getName() { + return name; + } + + /** + * Sets the user-defined player name. + * + * @param name The user-defined player name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the player "technology", e.g., web, external or jukebox. + * + * @return The player technology. + */ + public PlayerTechnology getTechnology() { + return technology; + } + + /** + * Returns the third-party client ID (used if this player is managed over the + * Subsonic REST API). + * + * @return The client ID. + */ + public String getClientId() { + return clientId; + } + + /** + * Sets the third-party client ID (used if this player is managed over the + * Subsonic REST API). + * + * @param clientId The client ID. + */ + public void setClientId(String clientId) { + this.clientId = clientId; + } + + /** + * Sets the player "technology", e.g., web, external or jukebox. + * + * @param technology The player technology. + */ + public void setTechnology(PlayerTechnology technology) { + this.technology = technology; + } + + public boolean isJukebox() { + return technology == PlayerTechnology.JUKEBOX; + } + + public boolean isExternal() { + return technology == PlayerTechnology.EXTERNAL; + } + + public boolean isExternalWithPlaylist() { + return technology == PlayerTechnology.EXTERNAL_WITH_PLAYLIST; + } + + public boolean isWeb() { + return technology == PlayerTechnology.WEB; + } + + /** + * Returns the player type, e.g., WinAmp, iTunes. + * + * @return The player type. + */ + public String getType() { + return type; + } + + /** + * Sets the player type, e.g., WinAmp, iTunes. + * + * @param type The player type. + */ + public void setType(String type) { + this.type = type; + } + + /** + * Returns the logged-in user. + * + * @return The logged-in user. + */ + public String getUsername() { + return username; + } + + /** + * Sets the logged-in username. + * + * @param username The logged-in username. + */ + public void setUsername(String username) { + this.username = username; + } + + /** + * Returns whether the player is automatically started. + * + * @return Whether the player is automatically started. + */ + public boolean isAutoControlEnabled() { + return isAutoControlEnabled; + } + + /** + * Sets whether the player is automatically started. + * + * @param isAutoControlEnabled Whether the player is automatically started. + */ + public void setAutoControlEnabled(boolean isAutoControlEnabled) { + this.isAutoControlEnabled = isAutoControlEnabled; + } + + /** + * Returns the time when the player was last seen. + * + * @return The time when the player was last seen. + */ + public Date getLastSeen() { + return lastSeen; + } + + /** + * Sets the time when the player was last seen. + * + * @param lastSeen The time when the player was last seen. + */ + public void setLastSeen(Date lastSeen) { + this.lastSeen = lastSeen; + } + + /** + * Returns the cover art scheme. + * + * @return The cover art scheme. + */ + public CoverArtScheme getCoverArtScheme() { + return coverArtScheme; + } + + /** + * Sets the cover art scheme. + * + * @param coverArtScheme The cover art scheme. + */ + public void setCoverArtScheme(CoverArtScheme coverArtScheme) { + this.coverArtScheme = coverArtScheme; + } + + /** + * Returns the transcode scheme. + * + * @return The transcode scheme. + */ + public TranscodeScheme getTranscodeScheme() { + return transcodeScheme; + } + + /** + * Sets the transcode scheme. + * + * @param transcodeScheme The transcode scheme. + */ + public void setTranscodeScheme(TranscodeScheme transcodeScheme) { + this.transcodeScheme = transcodeScheme; + } + + /** + * Returns the IP address of the player. + * + * @return The IP address of the player. + */ + public String getIpAddress() { + return ipAddress; + } + + /** + * Sets the IP address of the player. + * + * @param ipAddress The IP address of the player. + */ + public void setIpAddress(String ipAddress) { + this.ipAddress = ipAddress; + } + + /** + * Returns whether this player has a dynamic IP address. + * + * @return Whether this player has a dynamic IP address. + */ + public boolean isDynamicIp() { + return isDynamicIp; + } + + /** + * Sets whether this player has a dynamic IP address. + * + * @param dynamicIp Whether this player has a dynamic IP address. + */ + public void setDynamicIp(boolean dynamicIp) { + isDynamicIp = dynamicIp; + } + + /** + * Returns the player's playlist. + * + * @return The player's playlist + */ + public PlayQueue getPlayQueue() { + return playQueue; + } + + /** + * Sets the player's playlist. + * + * @param playQueue The player's playlist. + */ + public void setPlayQueue(PlayQueue playQueue) { + this.playQueue = playQueue; + } + + /** + * Returns a long description of the player, e.g., <code>Player 3 [admin]</code> + * + * @return A long description of the player. + */ + public String getDescription() { + StringBuilder builder = new StringBuilder(); + if (name != null) { + builder.append(name); + } else { + builder.append("Player ").append(id); + } + + builder.append(" [").append(username).append(']'); + return builder.toString(); + } + + /** + * Returns a short description of the player, e.g., <code>Player 3</code> + * + * @return A short description of the player. + */ + public String getShortDescription() { + if (StringUtils.isNotBlank(name)) { + return name; + } + return "Player " + id; + } + + /** + * Returns a string representation of the player. + * + * @return A string representation of the player. + * @see #getDescription() + */ + @Override + public String toString() { + return getDescription(); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java new file mode 100644 index 00000000..5ba3ff71 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayerTechnology.java @@ -0,0 +1,49 @@ +/* + 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.domain; + +/** + * Enumeration of player technologies. + * + * @author Sindre Mehus + */ +public enum PlayerTechnology { + + /** + * Plays music directly in the web browser using the integrated Flash player. + */ + WEB, + + /** + * Plays music in an external player, such as WinAmp or Windows Media Player. + */ + EXTERNAL, + + /** + * Same as above, but the playlist is managed by the player, rather than the Subsonic server. + * In this mode, skipping within songs is possible. + */ + EXTERNAL_WITH_PLAYLIST, + + /** + * Plays music directly on the audio device of the Subsonic server. + */ + JUKEBOX + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.java new file mode 100644 index 00000000..80555ec7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Playlist.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.domain; + +import net.sourceforge.subsonic.util.StringUtil; + +import java.util.Date; + +/** + * @author Sindre Mehus + */ +public class Playlist { + + private int id; + private String username; + private boolean isPublic; + private String name; + private String comment; + private int fileCount; + private int durationSeconds; + private Date created; + private Date changed; + private String importedFrom; + + public Playlist() { + } + + public Playlist(int id, String username, boolean isPublic, String name, String comment, int fileCount, + int durationSeconds, Date created, Date changed, String importedFrom) { + this.id = id; + this.username = username; + this.isPublic = isPublic; + this.name = name; + this.comment = comment; + this.fileCount = fileCount; + this.durationSeconds = durationSeconds; + this.created = created; + this.changed = changed; + this.importedFrom = importedFrom; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public boolean isPublic() { + return isPublic; + } + + public void setPublic(boolean isPublic) { + this.isPublic = isPublic; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getComment() { + return comment; + } + + public void setComment(String comment) { + this.comment = comment; + } + + public int getFileCount() { + return fileCount; + } + + public void setFileCount(int fileCount) { + this.fileCount = fileCount; + } + + public int getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(int durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public String getDurationAsString() { + return StringUtil.formatDuration(durationSeconds); + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getChanged() { + return changed; + } + + public void setChanged(Date changed) { + this.changed = changed; + } + + public String getImportedFrom() { + return importedFrom; + } + + public void setImportedFrom(String importedFrom) { + this.importedFrom = importedFrom; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java new file mode 100644 index 00000000..1127a5cb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.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.domain; + +import net.sourceforge.subsonic.util.StringUtil; + +/** + * A Podcast channel. Each channel contain several episodes. + * + * @author Sindre Mehus + * @see PodcastEpisode + */ +public class PodcastChannel { + + private Integer id; + private String url; + private String title; + private String description; + private PodcastStatus status; + private String errorMessage; + + public PodcastChannel(Integer id, String url, String title, String description, + PodcastStatus status, String errorMessage) { + this.id = id; + this.url = url; + this.title = StringUtil.removeMarkup(title); + this.description = StringUtil.removeMarkup(description); + this.status = status; + this.errorMessage = errorMessage; + } + + public PodcastChannel(String url) { + this.url = url; + status = PodcastStatus.NEW; + } + + public Integer getId() { + return id; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PodcastStatus getStatus() { + return status; + } + + public void setStatus(PodcastStatus status) { + this.status = status; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java new file mode 100644 index 00000000..b5a835cb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastEpisode.java @@ -0,0 +1,172 @@ +/* + 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.domain; + +import java.util.Date; + +import net.sourceforge.subsonic.util.StringUtil; + +/** + * A Podcast episode belonging to a channel. + * + * @author Sindre Mehus + * @see PodcastChannel + */ +public class PodcastEpisode { + + private Integer id; + private Integer mediaFileId; + private Integer channelId; + private String url; + private String path; + private String title; + private String description; + private Date publishDate; + private String duration; + private Long bytesTotal; + private Long bytesDownloaded; + private PodcastStatus status; + private String errorMessage; + + public PodcastEpisode(Integer id, Integer channelId, String url, String path, String title, + String description, Date publishDate, String duration, Long length, Long bytesDownloaded, + PodcastStatus status, String errorMessage) { + this.id = id; + this.channelId = channelId; + this.url = url; + this.path = path; + this.title = StringUtil.removeMarkup(title); + this.description = StringUtil.removeMarkup(description); + this.publishDate = publishDate; + this.duration = duration; + this.bytesTotal = length; + this.bytesDownloaded = bytesDownloaded; + this.status = status; + this.errorMessage = errorMessage; + } + + public Integer getId() { + return id; + } + + public Integer getChannelId() { + return channelId; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public Date getPublishDate() { + return publishDate; + } + + public void setPublishDate(Date publishDate) { + this.publishDate = publishDate; + } + + public String getDuration() { + return duration; + } + + public void setDuration(String duration) { + this.duration = duration; + } + + public Long getBytesTotal() { + return bytesTotal; + } + + public void setBytesTotal(Long bytesTotal) { + this.bytesTotal = bytesTotal; + } + + public Long getBytesDownloaded() { + return bytesDownloaded; + } + + public Double getCompletionRate() { + if (bytesTotal == null || bytesTotal == 0) { + return null; + } + if (bytesDownloaded == null) { + return 0.0; + } + + double d = bytesDownloaded; + double t = bytesTotal; + return d / t; + } + + public void setBytesDownloaded(Long bytesDownloaded) { + this.bytesDownloaded = bytesDownloaded; + } + + public PodcastStatus getStatus() { + return status; + } + + public void setStatus(PodcastStatus status) { + this.status = status; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Integer getMediaFileId() { + return mediaFileId; + } + + public void setMediaFileId(Integer mediaFileId) { + this.mediaFileId = mediaFileId; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java new file mode 100644 index 00000000..57cad155 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastStatus.java @@ -0,0 +1,29 @@ +/* + 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.domain; + +/** + * Enumeration of statuses for {@link PodcastChannel} and + * {@link PodcastEpisode}. + * + * @author Sindre Mehus + */ +public enum PodcastStatus { + NEW, DOWNLOADING, COMPLETED, ERROR, DELETED, SKIPPED +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java new file mode 100644 index 00000000..d52cec39 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java @@ -0,0 +1,70 @@ +/* + 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.domain; + +/** + * Defines criteria used when generating random playlists. + * + * @author Sindre Mehus + * @see net.sourceforge.subsonic.service.SearchService#getRandomSongs + */ +public class RandomSearchCriteria { + private final int count; + private final String genre; + private final Integer fromYear; + private final Integer toYear; + private final Integer musicFolderId; + + /** + * Creates a new instance. + * + * @param count Maximum number of songs to return. + * @param genre Only return songs of the given genre. May be <code>null</code>. + * @param fromYear Only return songs released after (or in) this year. May be <code>null</code>. + * @param toYear Only return songs released before (or in) this year. May be <code>null</code>. + * @param musicFolderId Only return songs from this music folder. May be <code>null</code>. + */ + public RandomSearchCriteria(int count, String genre, Integer fromYear, Integer toYear, Integer musicFolderId) { + this.count = count; + this.genre = genre; + this.fromYear = fromYear; + this.toYear = toYear; + this.musicFolderId = musicFolderId; + } + + public int getCount() { + return count; + } + + public String getGenre() { + return genre; + } + + public Integer getFromYear() { + return fromYear; + } + + public Integer getToYear() { + return toYear; + } + + public Integer getMusicFolderId() { + return musicFolderId; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java new file mode 100644 index 00000000..ede9d19e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Router.java @@ -0,0 +1,43 @@ +/* + 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.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public interface Router { + + /** + * Adds a NAT entry on the UPNP device. + * + * @param externalPort The external port to open on the UPNP device an map on the internal client. + * @param internalPort The internal client port where data should be redirected. + * @param leaseDuration Seconds the lease duration in seconds, or 0 for an infinite time. + */ + void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception; + + /** + * Deletes a NAT entry on the UPNP device. + * + * @param externalPort The external port of the NAT entry to delete. + * @param internalPort The internal port of the NAT entry to delete. + */ + void deletePortMapping(int externalPort, int internalPort) throws Exception; +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java new file mode 100644 index 00000000..a639e665 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SBBIRouter.java @@ -0,0 +1,63 @@ +/* + 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.domain; + +import net.sbbi.upnp.impls.InternetGatewayDevice; + +import java.io.IOException; +import java.net.InetAddress; + +/** + * @author Sindre Mehus + */ +public class SBBIRouter implements Router { + + // The timeout in milliseconds for finding a router device. + private static final int DISCOVERY_TIMEOUT = 3000; + + private final InternetGatewayDevice device; + + private SBBIRouter(InternetGatewayDevice device) { + this.device = device; + } + + public static SBBIRouter findRouter() throws Exception { + InternetGatewayDevice[] devices; + try { + devices = InternetGatewayDevice.getDevices(DISCOVERY_TIMEOUT); + } catch (IOException e) { + throw new Exception("Could not find router", e); + } + + if (devices == null || devices.length == 0) { + return null; + } + + return new SBBIRouter(devices[0]); + } + + public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception { + String localIp = InetAddress.getLocalHost().getHostAddress(); + device.addPortMapping("Subsonic", null, internalPort, externalPort, localIp, leaseDuration, "TCP"); + } + + public void deletePortMapping(int externalPort, int internal) throws Exception { + device.deletePortMapping(null, externalPort, "TCP"); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java new file mode 100644 index 00000000..f06a6512 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.java @@ -0,0 +1,59 @@ +/* + 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.domain; + +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.SearchService; + +/** + * Defines criteria used when searching. + * + * @author Sindre Mehus + * @see SearchService#search + */ +public class SearchCriteria { + + private String query; + private int offset; + private int count; + + public void setQuery(String query) { + this.query = query; + } + + public String getQuery() { + return query; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getCount() { + return count; + } + + public void setCount(int count) { + this.count = count; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.java new file mode 100644 index 00000000..bf4b370a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchResult.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.domain; + +import java.util.ArrayList; +import java.util.List; + +import net.sourceforge.subsonic.service.MediaScannerService; +import net.sourceforge.subsonic.service.SearchService; + +/** + * The outcome of a search. + * + * @author Sindre Mehus + * @see SearchService#search + */ +public class SearchResult { + + private final List<MediaFile> mediaFiles = new ArrayList<MediaFile>(); + private final List<Artist> artists = new ArrayList<Artist>(); + private final List<Album> albums = new ArrayList<Album>(); + + private int offset; + private int totalHits; + + public List<MediaFile> getMediaFiles() { + return mediaFiles; + } + + public List<Artist> getArtists() { + return artists; + } + + public List<Album> getAlbums() { + return albums; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getTotalHits() { + return totalHits; + } + + public void setTotalHits(int totalHits) { + this.totalHits = totalHits; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java new file mode 100644 index 00000000..7494769b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java @@ -0,0 +1,100 @@ +package net.sourceforge.subsonic.domain; + +import java.util.Date; + +/** + * A collection of media files that is shared with someone, and accessible via a direct URL. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class Share { + + private int id; + private String name; + private String description; + private String username; + private Date created; + private Date expires; + private Date lastVisited; + private int visitCount; + + public Share() { + } + + public Share(int id, String name, String description, String username, Date created, + Date expires, Date lastVisited, int visitCount) { + this.id = id; + this.name = name; + this.description = description; + this.username = username; + this.created = created; + this.expires = expires; + this.lastVisited = lastVisited; + this.visitCount = visitCount; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getExpires() { + return expires; + } + + public void setExpires(Date expires) { + this.expires = expires; + } + + public Date getLastVisited() { + return lastVisited; + } + + public void setLastVisited(Date lastVisited) { + this.lastVisited = lastVisited; + } + + public int getVisitCount() { + return visitCount; + } + + public void setVisitCount(int visitCount) { + this.visitCount = visitCount; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java new file mode 100644 index 00000000..f8bd66bd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.java @@ -0,0 +1,42 @@ +/* + 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.domain; + +/** + * Contains the ID and name for a theme. + * + * @author Sindre Mehus + */ +public class Theme { + private String id; + private String name; + + public Theme(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java new file mode 100644 index 00000000..f45b452a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java @@ -0,0 +1,104 @@ +/* + 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.domain; + +/** + * Enumeration of transcoding schemes. Transcoding is the process of + * converting an audio stream to a lower bit rate. + * + * @author Sindre Mehus + */ +public enum TranscodeScheme { + + OFF(0), + MAX_32(32), + MAX_40(40), + MAX_48(48), + MAX_56(56), + MAX_64(64), + MAX_80(80), + MAX_96(96), + MAX_112(112), + MAX_128(128), + MAX_160(160), + MAX_192(192), + MAX_224(224), + MAX_256(256), + MAX_320(320); + + private int maxBitRate; + + TranscodeScheme(int maxBitRate) { + this.maxBitRate = maxBitRate; + } + + /** + * Returns the maximum bit rate for this transcoding scheme. + * + * @return The maximum bit rate for this transcoding scheme. + */ + public int getMaxBitRate() { + return maxBitRate; + } + + /** + * Returns the strictest transcode scheme (i.e., the scheme with the lowest max bitrate). + * + * @param other The other transcode scheme. May be <code>null</code>, in which case 'this' is returned. + * @return The strictest scheme. + */ + public TranscodeScheme strictest(TranscodeScheme other) { + if (other == null || other == TranscodeScheme.OFF) { + return this; + } + + if (this == TranscodeScheme.OFF) { + return other; + } + + return maxBitRate < other.maxBitRate ? this : other; + } + + /** + * Returns a human-readable string representation of this object. + * + * @return A human-readable string representation of this object. + */ + public String toString() { + if (this == OFF) { + return "No limit"; + } + return "" + getMaxBitRate() + " Kbps"; + } + + /** + * Returns the enum constant which corresponds to the given max bit rate. + * + * @param maxBitRate The max bit rate. + * @return The corresponding enum, or <code>null</code> if not found. + */ + public static TranscodeScheme valueOf(int maxBitRate) { + for (TranscodeScheme scheme : values()) { + if (scheme.getMaxBitRate() == maxBitRate) { + return scheme; + } + } + return null; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java new file mode 100644 index 00000000..57c8316f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Transcoding.java @@ -0,0 +1,221 @@ +/* + 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.domain; + +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Contains the configuration for a transcoding, i.e., a specification of how a given media format + * should be converted to another. + * <br/> + * A transcoding may contain up to three steps. Typically you need to convert in several steps, for + * instance from OGG to WAV to MP3. + * + * @author Sindre Mehus + */ +public class Transcoding { + + private Integer id; + private String name; + private String sourceFormats; + private String targetFormat; + private String step1; + private String step2; + private String step3; + private boolean defaultActive; + + /** + * Creates a new transcoding specification. + * + * @param id The system-generated ID. + * @param name The user-defined name. + * @param sourceFormats The source formats, e.g., "ogg wav aac". + * @param targetFormat The target format, e.g., "mp3". + * @param step1 The command to execute in step 1. + * @param step2 The command to execute in step 2. + * @param step3 The command to execute in step 3. + * @param defaultActive Whether the transcoding should be automatically activated for all players. + */ + public Transcoding(Integer id, String name, String sourceFormats, String targetFormat, String step1, + String step2, String step3, boolean defaultActive) { + this.id = id; + this.name = name; + this.sourceFormats = sourceFormats; + this.targetFormat = targetFormat; + this.step1 = step1; + this.step2 = step2; + this.step3 = step3; + this.defaultActive = defaultActive; + } + + /** + * Returns the system-generated ID. + * + * @return The system-generated ID. + */ + public Integer getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + /** + * Returns the user-defined name. + * + * @return The user-defined name. + */ + public String getName() { + return name; + } + + /** + * Sets the user-defined name. + * + * @param name The user-defined name. + */ + public void setName(String name) { + this.name = name; + } + + /** + * Returns the source format, e.g., "ogg wav aac". + * + * @return The source format, e.g., "ogg wav aac". + */ + public String getSourceFormats() { + return sourceFormats; + } + + public String[] getSourceFormatsAsArray() { + return StringUtil.split(sourceFormats); + } + + /** + * Sets the source formats, e.g., "ogg wav aac". + * + * @param sourceFormats The source formats, e.g., "ogg wav aac". + */ + public void setSourceFormats(String sourceFormats) { + this.sourceFormats = sourceFormats; + } + + /** + * Returns the target format, e.g., mp3. + * + * @return The target format, e.g., mp3. + */ + public String getTargetFormat() { + return targetFormat; + } + + /** + * Sets the target format, e.g., mp3. + * + * @param targetFormat The target format, e.g., mp3. + */ + public void setTargetFormat(String targetFormat) { + this.targetFormat = targetFormat; + } + + /** + * Returns the command to execute in step 1. + * + * @return The command to execute in step 1. + */ + public String getStep1() { + return step1; + } + + /** + * Sets the command to execute in step 1. + * + * @param step1 The command to execute in step 1. + */ + public void setStep1(String step1) { + this.step1 = step1; + } + + /** + * Returns the command to execute in step 2. + * + * @return The command to execute in step 2. + */ + public String getStep2() { + return step2; + } + + /** + * Sets the command to execute in step 2. + * + * @param step2 The command to execute in step 2. + */ + public void setStep2(String step2) { + this.step2 = step2; + } + + /** + * Returns the command to execute in step 3. + * + * @return The command to execute in step 3. + */ + public String getStep3() { + return step3; + } + + /** + * Sets the command to execute in step 3. + * + * @param step3 The command to execute in step 3. + */ + public void setStep3(String step3) { + this.step3 = step3; + } + + /** + * Returns whether the transcoding should be automatically activated for all players + */ + public boolean isDefaultActive() { + return defaultActive; + } + + /** + * Sets whether the transcoding should be automatically activated for all players + */ + public void setDefaultActive(boolean defaultActive) { + this.defaultActive = defaultActive; + } + + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Transcoding that = (Transcoding) o; + return !(id != null ? !id.equals(that.id) : that.id != null); + } + + public int hashCode() { + return (id != null ? id.hashCode() : 0); + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java new file mode 100644 index 00000000..06930ae3 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TransferStatus.java @@ -0,0 +1,303 @@ +/* + 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.domain; + +import java.io.File; + +import net.sourceforge.subsonic.util.BoundedList; + +/** + * Status for a single transfer (stream, download or upload). + * + * @author Sindre Mehus + */ +public class TransferStatus { + + private static final int HISTORY_LENGTH = 200; + private static final long SAMPLE_INTERVAL_MILLIS = 5000; + + private Player player; + private File file; + private long bytesTransfered; + private long bytesSkipped; + private long bytesTotal; + private final SampleHistory history = new SampleHistory(); + private boolean terminated; + private boolean active = true; + + /** + * Return the number of bytes transferred. + * + * @return The number of bytes transferred. + */ + public synchronized long getBytesTransfered() { + return bytesTransfered; + } + + /** + * Adds the given byte count to the total number of bytes transferred. + * + * @param byteCount The byte count. + */ + public synchronized void addBytesTransfered(long byteCount) { + setBytesTransfered(bytesTransfered + byteCount); + } + + /** + * Sets the number of bytes transferred. + * + * @param bytesTransfered The number of bytes transferred. + */ + public synchronized void setBytesTransfered(long bytesTransfered) { + this.bytesTransfered = bytesTransfered; + createSample(bytesTransfered, false); + } + + private void createSample(long bytesTransfered, boolean force) { + long now = System.currentTimeMillis(); + + if (history.isEmpty()) { + history.add(new Sample(bytesTransfered, now)); + } else { + Sample lastSample = history.getLast(); + if (force || now - lastSample.getTimestamp() > TransferStatus.SAMPLE_INTERVAL_MILLIS) { + history.add(new Sample(bytesTransfered, now)); + } + } + } + + /** + * Returns the number of milliseconds since the transfer status was last updated. + * + * @return Number of milliseconds, or <code>0</code> if never updated. + */ + public synchronized long getMillisSinceLastUpdate() { + if (history.isEmpty()) { + return 0L; + } + return System.currentTimeMillis() - history.getLast().timestamp; + } + + /** + * Returns the total number of bytes, or 0 if unknown. + * + * @return The total number of bytes, or 0 if unknown. + */ + public long getBytesTotal() { + return bytesTotal; + } + + /** + * Sets the total number of bytes, or 0 if unknown. + * + * @param bytesTotal The total number of bytes, or 0 if unknown. + */ + public void setBytesTotal(long bytesTotal) { + this.bytesTotal = bytesTotal; + } + + /** + * Returns the number of bytes that has been skipped (for instance when + * resuming downloads). + * + * @return The number of skipped bytes. + */ + public synchronized long getBytesSkipped() { + return bytesSkipped; + } + + /** + * Sets the number of bytes that has been skipped (for instance when + * resuming downloads). + * + * @param bytesSkipped The number of skipped bytes. + */ + public synchronized void setBytesSkipped(long bytesSkipped) { + this.bytesSkipped = bytesSkipped; + } + + + /** + * Adds the given byte count to the total number of bytes skipped. + * + * @param byteCount The byte count. + */ + public synchronized void addBytesSkipped(long byteCount) { + bytesSkipped += byteCount; + } + + /** + * Returns the file that is currently being transferred. + * + * @return The file that is currently being transferred. + */ + public synchronized File getFile() { + return file; + } + + /** + * Sets the file that is currently being transferred. + * + * @param file The file that is currently being transferred. + */ + public synchronized void setFile(File file) { + this.file = file; + } + + /** + * Returns the remote player for the stream. + * + * @return The remote player for the stream. + */ + public synchronized Player getPlayer() { + return player; + } + + /** + * Sets the remote player for the stream. + * + * @param player The remote player for the stream. + */ + public synchronized void setPlayer(Player player) { + this.player = player; + } + + /** + * Returns a history of samples for the stream + * + * @return A (copy of) the history list of samples. + */ + public synchronized SampleHistory getHistory() { + return new SampleHistory(history); + } + + /** + * Returns the history length in milliseconds. + * + * @return The history length in milliseconds. + */ + public long getHistoryLengthMillis() { + return TransferStatus.SAMPLE_INTERVAL_MILLIS * (TransferStatus.HISTORY_LENGTH - 1); + } + + /** + * Indicate that the stream should be terminated. + */ + public void terminate() { + terminated = true; + } + + /** + * Returns whether this stream has been terminated. + * Not that the <em>terminated status</em> is cleared by this method. + * + * @return Whether this stream has been terminated. + */ + public boolean terminated() { + boolean result = terminated; + terminated = false; + return result; + } + + /** + * Returns whether this transfer is active, i.e., if the connection is still established. + * + * @return Whether this transfer is active. + */ + public boolean isActive() { + return active; + } + + /** + * Sets whether this transfer is active, i.e., if the connection is still established. + * + * @param active Whether this transfer is active. + */ + public void setActive(boolean active) { + this.active = active; + + if (active) { + setBytesSkipped(0L); + setBytesTotal(0L); + setBytesTransfered(0L); + } else { + createSample(getBytesTransfered(), true); + } + } + + /** + * A sample containing a timestamp and the number of bytes transferred up to that point in time. + */ + public static class Sample { + private long bytesTransfered; + private long timestamp; + + /** + * Creates a new sample. + * + * @param bytesTransfered The total number of bytes transferred. + * @param timestamp A point in time, in milliseconds. + */ + public Sample(long bytesTransfered, long timestamp) { + this.bytesTransfered = bytesTransfered; + this.timestamp = timestamp; + } + + /** + * Returns the number of bytes transferred. + * + * @return The number of bytes transferred. + */ + public long getBytesTransfered() { + return bytesTransfered; + } + + /** + * Returns the timestamp of the sample. + * + * @return The timestamp in milliseconds. + */ + public long getTimestamp() { + return timestamp; + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("TransferStatus-").append(hashCode()).append(" [player: ").append(player.getId()).append(", file: "); + builder.append(file).append(", terminated: ").append(terminated).append(", active: ").append(active).append("]"); + return builder.toString(); + } + + /** + * Contains recent history of samples. + */ + public static class SampleHistory extends BoundedList<Sample> { + + public SampleHistory() { + super(HISTORY_LENGTH); + } + + public SampleHistory(SampleHistory other) { + super(HISTORY_LENGTH); + addAll(other); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java new file mode 100644 index 00000000..95e51004 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.java @@ -0,0 +1,245 @@ +/* + 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.domain; + +/** + * Represent a user. + * + * @author Sindre Mehus + */ +public class User { + + public static final String USERNAME_ADMIN = "admin"; + + private final String username; + private String password; + private String email; + private boolean ldapAuthenticated; + private long bytesStreamed; + private long bytesDownloaded; + private long bytesUploaded; + + private boolean isAdminRole; + private boolean isSettingsRole; + private boolean isDownloadRole; + private boolean isUploadRole; + private boolean isPlaylistRole; + private boolean isCoverArtRole; + private boolean isCommentRole; + private boolean isPodcastRole; + private boolean isStreamRole; + private boolean isJukeboxRole; + private boolean isShareRole; + + public User(String username, String password, String email, boolean ldapAuthenticated, + long bytesStreamed, long bytesDownloaded, long bytesUploaded) { + this.username = username; + this.password = password; + this.email = email; + this.ldapAuthenticated = ldapAuthenticated; + this.bytesStreamed = bytesStreamed; + this.bytesDownloaded = bytesDownloaded; + this.bytesUploaded = bytesUploaded; + } + + public User(String username, String password, String email) { + this(username, password, email, false, 0, 0, 0); + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public boolean isLdapAuthenticated() { + return ldapAuthenticated; + } + + public void setLdapAuthenticated(boolean ldapAuthenticated) { + this.ldapAuthenticated = ldapAuthenticated; + } + + public long getBytesStreamed() { + return bytesStreamed; + } + + public void setBytesStreamed(long bytesStreamed) { + this.bytesStreamed = bytesStreamed; + } + + public long getBytesDownloaded() { + return bytesDownloaded; + } + + public void setBytesDownloaded(long bytesDownloaded) { + this.bytesDownloaded = bytesDownloaded; + } + + public long getBytesUploaded() { + return bytesUploaded; + } + + public void setBytesUploaded(long bytesUploaded) { + this.bytesUploaded = bytesUploaded; + } + + public boolean isAdminRole() { + return isAdminRole; + } + + public void setAdminRole(boolean isAdminRole) { + this.isAdminRole = isAdminRole; + } + + public boolean isSettingsRole() { + return isSettingsRole; + } + + public void setSettingsRole(boolean isSettingsRole) { + this.isSettingsRole = isSettingsRole; + } + + public boolean isCommentRole() { + return isCommentRole; + } + + public void setCommentRole(boolean isCommentRole) { + this.isCommentRole = isCommentRole; + } + + public boolean isDownloadRole() { + return isDownloadRole; + } + + public void setDownloadRole(boolean isDownloadRole) { + this.isDownloadRole = isDownloadRole; + } + + public boolean isUploadRole() { + return isUploadRole; + } + + public void setUploadRole(boolean isUploadRole) { + this.isUploadRole = isUploadRole; + } + + public boolean isPlaylistRole() { + return isPlaylistRole; + } + + public void setPlaylistRole(boolean isPlaylistRole) { + this.isPlaylistRole = isPlaylistRole; + } + + public boolean isCoverArtRole() { + return isCoverArtRole; + } + + public void setCoverArtRole(boolean isCoverArtRole) { + this.isCoverArtRole = isCoverArtRole; + } + + public boolean isPodcastRole() { + return isPodcastRole; + } + + public void setPodcastRole(boolean isPodcastRole) { + this.isPodcastRole = isPodcastRole; + } + + public boolean isStreamRole() { + return isStreamRole; + } + + public void setStreamRole(boolean streamRole) { + isStreamRole = streamRole; + } + + public boolean isJukeboxRole() { + return isJukeboxRole; + } + + public void setJukeboxRole(boolean jukeboxRole) { + isJukeboxRole = jukeboxRole; + } + + public boolean isShareRole() { + return isShareRole; + } + + public void setShareRole(boolean shareRole) { + isShareRole = shareRole; + } + + @Override + public String toString() { + StringBuffer result = new StringBuffer(username); + + if (isAdminRole) { + result.append(" [admin]"); + } + if (isSettingsRole) { + result.append(" [settings]"); + } + if (isDownloadRole) { + result.append(" [download]"); + } + if (isUploadRole) { + result.append(" [upload]"); + } + if (isPlaylistRole) { + result.append(" [playlist]"); + } + if (isCoverArtRole) { + result.append(" [coverart]"); + } + if (isCommentRole) { + result.append(" [comment]"); + } + if (isPodcastRole) { + result.append(" [podcast]"); + } + if (isStreamRole) { + result.append(" [stream]"); + } + if (isJukeboxRole) { + result.append(" [jukebox]"); + } + if (isShareRole) { + result.append(" [share]"); + } + + return result.toString(); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java new file mode 100644 index 00000000..856591bc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java @@ -0,0 +1,328 @@ +/* + 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.domain; + +import java.util.*; + +/** + * Represent user-specific settings. + * + * @author Sindre Mehus + */ +public class UserSettings { + + private String username; + private Locale locale; + private String themeId; + private boolean showNowPlayingEnabled; + private boolean showChatEnabled; + private boolean finalVersionNotificationEnabled; + private boolean betaVersionNotificationEnabled; + private Visibility mainVisibility = new Visibility(); + private Visibility playlistVisibility = new Visibility(); + private boolean lastFmEnabled; + private String lastFmUsername; + private String lastFmPassword; + private TranscodeScheme transcodeScheme = TranscodeScheme.OFF; + private int selectedMusicFolderId = -1; + private boolean partyModeEnabled; + private boolean nowPlayingAllowed; + private AvatarScheme avatarScheme = AvatarScheme.NONE; + private Integer systemAvatarId; + private Date changed = new Date(); + + public UserSettings(String username) { + this.username = username; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Locale getLocale() { + return locale; + } + + public void setLocale(Locale locale) { + this.locale = locale; + } + + public String getThemeId() { + return themeId; + } + + public void setThemeId(String themeId) { + this.themeId = themeId; + } + + public boolean isShowNowPlayingEnabled() { + return showNowPlayingEnabled; + } + + public void setShowNowPlayingEnabled(boolean showNowPlayingEnabled) { + this.showNowPlayingEnabled = showNowPlayingEnabled; + } + + public boolean isShowChatEnabled() { + return showChatEnabled; + } + + public void setShowChatEnabled(boolean showChatEnabled) { + this.showChatEnabled = showChatEnabled; + } + + public boolean isFinalVersionNotificationEnabled() { + return finalVersionNotificationEnabled; + } + + public void setFinalVersionNotificationEnabled(boolean finalVersionNotificationEnabled) { + this.finalVersionNotificationEnabled = finalVersionNotificationEnabled; + } + + public boolean isBetaVersionNotificationEnabled() { + return betaVersionNotificationEnabled; + } + + public void setBetaVersionNotificationEnabled(boolean betaVersionNotificationEnabled) { + this.betaVersionNotificationEnabled = betaVersionNotificationEnabled; + } + + public Visibility getMainVisibility() { + return mainVisibility; + } + + public void setMainVisibility(Visibility mainVisibility) { + this.mainVisibility = mainVisibility; + } + + public Visibility getPlaylistVisibility() { + return playlistVisibility; + } + + public void setPlaylistVisibility(Visibility playlistVisibility) { + this.playlistVisibility = playlistVisibility; + } + + public boolean isLastFmEnabled() { + return lastFmEnabled; + } + + public void setLastFmEnabled(boolean lastFmEnabled) { + this.lastFmEnabled = lastFmEnabled; + } + + public String getLastFmUsername() { + return lastFmUsername; + } + + public void setLastFmUsername(String lastFmUsername) { + this.lastFmUsername = lastFmUsername; + } + + public String getLastFmPassword() { + return lastFmPassword; + } + + public void setLastFmPassword(String lastFmPassword) { + this.lastFmPassword = lastFmPassword; + } + + public TranscodeScheme getTranscodeScheme() { + return transcodeScheme; + } + + public void setTranscodeScheme(TranscodeScheme transcodeScheme) { + this.transcodeScheme = transcodeScheme; + } + + public int getSelectedMusicFolderId() { + return selectedMusicFolderId; + } + + public void setSelectedMusicFolderId(int selectedMusicFolderId) { + this.selectedMusicFolderId = selectedMusicFolderId; + } + + public boolean isPartyModeEnabled() { + return partyModeEnabled; + } + + public void setPartyModeEnabled(boolean partyModeEnabled) { + this.partyModeEnabled = partyModeEnabled; + } + + public boolean isNowPlayingAllowed() { + return nowPlayingAllowed; + } + + public void setNowPlayingAllowed(boolean nowPlayingAllowed) { + this.nowPlayingAllowed = nowPlayingAllowed; + } + + public AvatarScheme getAvatarScheme() { + return avatarScheme; + } + + public void setAvatarScheme(AvatarScheme avatarScheme) { + this.avatarScheme = avatarScheme; + } + + public Integer getSystemAvatarId() { + return systemAvatarId; + } + + public void setSystemAvatarId(Integer systemAvatarId) { + this.systemAvatarId = systemAvatarId; + } + + /** + * Returns when the corresponding database entry was last changed. + * + * @return When the corresponding database entry was last changed. + */ + public Date getChanged() { + return changed; + } + + /** + * Sets when the corresponding database entry was last changed. + * + * @param changed When the corresponding database entry was last changed. + */ + public void setChanged(Date changed) { + this.changed = changed; + } + + /** + * Configuration of what information to display about a song. + */ + public static class Visibility { + private int captionCutoff; + private boolean isTrackNumberVisible; + private boolean isArtistVisible; + private boolean isAlbumVisible; + private boolean isGenreVisible; + private boolean isYearVisible; + private boolean isBitRateVisible; + private boolean isDurationVisible; + private boolean isFormatVisible; + private boolean isFileSizeVisible; + + public Visibility() {} + + public Visibility(int captionCutoff, boolean trackNumberVisible, boolean artistVisible, boolean albumVisible, + boolean genreVisible, boolean yearVisible, boolean bitRateVisible, + boolean durationVisible, boolean formatVisible, boolean fileSizeVisible) { + this.captionCutoff = captionCutoff; + isTrackNumberVisible = trackNumberVisible; + isArtistVisible = artistVisible; + isAlbumVisible = albumVisible; + isGenreVisible = genreVisible; + isYearVisible = yearVisible; + isBitRateVisible = bitRateVisible; + isDurationVisible = durationVisible; + isFormatVisible = formatVisible; + isFileSizeVisible = fileSizeVisible; + } + + public int getCaptionCutoff() { + return captionCutoff; + } + + public void setCaptionCutoff(int captionCutoff) { + this.captionCutoff = captionCutoff; + } + + public boolean isTrackNumberVisible() { + return isTrackNumberVisible; + } + + public void setTrackNumberVisible(boolean trackNumberVisible) { + isTrackNumberVisible = trackNumberVisible; + } + + public boolean isArtistVisible() { + return isArtistVisible; + } + + public void setArtistVisible(boolean artistVisible) { + isArtistVisible = artistVisible; + } + + public boolean isAlbumVisible() { + return isAlbumVisible; + } + + public void setAlbumVisible(boolean albumVisible) { + isAlbumVisible = albumVisible; + } + + public boolean isGenreVisible() { + return isGenreVisible; + } + + public void setGenreVisible(boolean genreVisible) { + isGenreVisible = genreVisible; + } + + public boolean isYearVisible() { + return isYearVisible; + } + + public void setYearVisible(boolean yearVisible) { + isYearVisible = yearVisible; + } + + public boolean isBitRateVisible() { + return isBitRateVisible; + } + + public void setBitRateVisible(boolean bitRateVisible) { + isBitRateVisible = bitRateVisible; + } + + public boolean isDurationVisible() { + return isDurationVisible; + } + + public void setDurationVisible(boolean durationVisible) { + isDurationVisible = durationVisible; + } + + public boolean isFormatVisible() { + return isFormatVisible; + } + + public void setFormatVisible(boolean formatVisible) { + isFormatVisible = formatVisible; + } + + public boolean isFileSizeVisible() { + return isFileSizeVisible; + } + + public void setFileSizeVisible(boolean fileSizeVisible) { + isFileSizeVisible = fileSizeVisible; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.java new file mode 100644 index 00000000..c4d42a99 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.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.domain; + +/** + * Represents the version number of Subsonic. + * + * @author Sindre Mehus + * @version $Revision: 1.3 $ $Date: 2006/01/20 21:25:16 $ + */ +public class Version implements Comparable<Version> { + private int major; + private int minor; + private int beta; + private int bugfix; + + /** + * Creates a new version instance by parsing the given string. + * @param version A string of the format "1.27", "1.27.2" or "1.27.beta3". + */ + public Version(String version) { + String[] s = version.split("\\."); + major = Integer.valueOf(s[0]); + minor = Integer.valueOf(s[1]); + + if (s.length > 2) { + if (s[2].contains("beta")) { + beta = Integer.valueOf(s[2].replace("beta", "")); + } else { + bugfix = Integer.valueOf(s[2]); + } + } + } + + public int getMajor() { + return major; + } + + public int getMinor() { + return minor; + } + + /** + * Return whether this object is equal to another. + * @param o Object to compare to. + * @return Whether this object is equals to another. + */ + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final Version version = (Version) o; + + if (beta != version.beta) return false; + if (bugfix != version.bugfix) return false; + if (major != version.major) return false; + return minor == version.minor; + } + + /** + * Returns a hash code for this object. + * @return A hash code for this object. + */ + public int hashCode() { + int result; + result = major; + result = 29 * result + minor; + result = 29 * result + beta; + result = 29 * result + bugfix; + return result; + } + + /** + * Returns a string representation of the form "1.27", "1.27.2" or "1.27.beta3". + * @return A string representation of the form "1.27", "1.27.2" or "1.27.beta3". + */ + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(major).append('.').append(minor); + if (beta != 0) { + buf.append(".beta").append(beta); + } else if (bugfix != 0) { + buf.append('.').append(bugfix); + } + + return buf.toString(); + } + + /** + * Compares this object with the specified object for order. + * @param version The object to compare to. + * @return A negative integer, zero, or a positive integer as this object is less than, equal to, or + * greater than the specified object. + */ + public int compareTo(Version version) { + if (major < version.major) { + return -1; + } else if (major > version.major) { + return 1; + } + + if (minor < version.minor) { + return -1; + } else if (minor > version.minor) { + return 1; + } + + if (bugfix < version.bugfix) { + return -1; + } else if (bugfix > version.bugfix) { + return 1; + } + + int thisBeta = beta == 0 ? Integer.MAX_VALUE : beta; + int otherBeta = version.beta == 0 ? Integer.MAX_VALUE : version.beta; + + if (thisBeta < otherBeta) { + return -1; + } else if (thisBeta > otherBeta) { + return 1; + } + + return 0; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java new file mode 100644 index 00000000..18661ba4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.java @@ -0,0 +1,50 @@ +/* + 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.domain; + +/** + * Parameters used when transcoding videos. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class VideoTranscodingSettings { + + private final int width; + private final int height; + private final int timeOffset; + + public VideoTranscodingSettings(int width, int height, int timeOffset) { + this.width = width; + this.height = height; + this.timeOffset = timeOffset; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getTimeOffset() { + return timeOffset; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java new file mode 100644 index 00000000..e36701e8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/WeUPnPRouter.java @@ -0,0 +1,56 @@ +/* + 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.domain; + +import org.wetorrent.upnp.GatewayDevice; +import org.wetorrent.upnp.GatewayDiscover; + +import java.net.InetAddress; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class WeUPnPRouter implements Router { + private final GatewayDevice device; + + private WeUPnPRouter(GatewayDevice device) { + this.device = device; + } + + public static WeUPnPRouter findRouter() throws Exception { + GatewayDiscover discover = new GatewayDiscover(); + discover.discover(); + GatewayDevice device = discover.getValidGateway(); + if (device == null) { + return null; + } + + return new WeUPnPRouter(device); + } + + public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception { + String localIp = InetAddress.getLocalHost().getHostAddress(); + device.addPortMapping(externalPort, internalPort, localIp, "TCP", "Subsonic"); + } + + public void deletePortMapping(int externalPort, int internalPort) throws Exception { + device.deletePortMapping(externalPort, "TCP"); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java new file mode 100644 index 00000000..c93d0603 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java @@ -0,0 +1,107 @@ +/* + 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.filter; + +import net.sourceforge.subsonic.service.SettingsService; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.File; +import java.io.IOException; + +/** + * This filter is executed very early in the filter chain. It verifies that + * the Subsonic home directory (c:\subsonic or /var/subsonic) exists and + * is writable. If not, a proper error message is given to the user. + * <p/> + * (The Subsonic home directory is usually created automatically, but a common + * problem on Linux is that the Tomcat user does not have the necessary + * privileges). + * + * @author Sindre Mehus + */ +public class BootstrapVerificationFilter implements Filter { + + private boolean subsonicHomeVerified = false; + + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + + // Already verified? + if (subsonicHomeVerified) { + chain.doFilter(req, res); + return; + } + + File home = SettingsService.getSubsonicHome(); + if (!directoryExists(home)) { + error(res, "<p>The directory <b>" + home + "</b> does not exist. Please create it and make it writable, " + + "then restart the servlet container.</p>" + + "<p>(You can override the directory location by specifying -Dsubsonic.home=... when " + + "starting the servlet container.)</p>"); + + } else if (!directoryWritable(home)) { + error(res, "<p>The directory <b>" + home + "</b> is not writable. Please change file permissions, " + + "then restart the servlet container.</p>" + + "<p>(You can override the directory location by specifying -Dsubsonic.home=... when " + + "starting the servlet container.)</p>"); + + } else { + subsonicHomeVerified = true; + chain.doFilter(req, res); + } + } + + private boolean directoryExists(File dir) { + return dir.exists() && dir.isDirectory(); + } + + private boolean directoryWritable(File dir) { + try { + File tempFile = File.createTempFile("test", null, dir); + tempFile.delete(); + return true; + } catch (IOException x) { + return false; + } + } + + private void error(ServletResponse res, String error) throws IOException { + ServletOutputStream out = res.getOutputStream(); + out.println("<html>" + + "<head><title>Subsonic Error</title></head>" + + "<body>" + + "<h2>Subsonic Error</h2>" + + error + + "</body>" + + "</html>"); + } + + public void init(FilterConfig filterConfig) { + } + + public void destroy() { + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java new file mode 100644 index 00000000..52a98ad0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ParameterDecodingFilter.java @@ -0,0 +1,147 @@ +/* + 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.filter; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.util.StringUtil; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.IOException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Map; +import java.util.Vector; + +/** + * Servlet filter which decodes HTTP request parameters. If a parameter name ends with + * "Utf8Hex" ({@link #PARAM_SUFFIX}) , the corresponding parameter value is assumed to be the + * hexadecimal representation of the UTF-8 bytes of the value. + * <p/> + * Used to support request parameter values of any character encoding. + * + * @author Sindre Mehus + */ +public class ParameterDecodingFilter implements Filter { + + public static final String PARAM_SUFFIX = "Utf8Hex"; + private static final Logger LOG = Logger.getLogger(ParameterDecodingFilter.class); + + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + // Wrap request in decoder. + ServletRequest decodedRequest = new DecodingServletRequestWrapper((HttpServletRequest) request); + + // Pass the request/response on + chain.doFilter(decodedRequest, response); + } + + public void init(FilterConfig filterConfig) { + } + + public void destroy() { + } + + private static class DecodingServletRequestWrapper extends HttpServletRequestWrapper { + + public DecodingServletRequestWrapper(HttpServletRequest servletRequest) { + super(servletRequest); + } + + @Override + public String getParameter(String name) { + String[] values = getParameterValues(name); + if (values == null || values.length == 0) { + return null; + } + return values[0]; + } + + @Override + public Map getParameterMap() { + Map map = super.getParameterMap(); + Map<String, String[]> result = new HashMap<String, String[]>(); + + for (Object o : map.entrySet()) { + Map.Entry entry = (Map.Entry) o; + String name = (String) entry.getKey(); + String[] values = (String[]) entry.getValue(); + + if (name.endsWith(PARAM_SUFFIX)) { + result.put(name.replace(PARAM_SUFFIX, ""), decode(values)); + } else { + result.put(name, values); + } + } + return result; + } + + @Override + public Enumeration getParameterNames() { + Enumeration e = super.getParameterNames(); + Vector<String> v = new Vector<String>(); + while (e.hasMoreElements()) { + String name = (String) e.nextElement(); + if (name.endsWith(PARAM_SUFFIX)) { + name = name.replace(PARAM_SUFFIX, ""); + } + v.add(name); + } + + return v.elements(); + } + + @Override + public String[] getParameterValues(String name) { + String[] values = super.getParameterValues(name); + if (values != null) { + return values; + } + + values = super.getParameterValues(name + PARAM_SUFFIX); + if (values != null) { + return decode(values); + } + + return null; + } + + private String[] decode(String[] values) { + if (values == null) { + return null; + } + + String[] result = new String[values.length]; + for (int i = 0; i < values.length; i++) { + try { + result[i] = StringUtil.utf8HexDecode(values[i]); + } catch (Exception x) { + LOG.error("Failed to decode parameter value '" + values[i] + "'"); + result[i] = values[i]; + } + } + + return result; + } + + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java new file mode 100644 index 00000000..3b37e8d4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RequestEncodingFilter.java @@ -0,0 +1,54 @@ +/* + 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.filter; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; + +/** + * Configurable filter for setting the character encoding to use for the HTTP request. + * Typically used to set UTF-8 encoding when reading request parameters with non-Latin + * content. + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2006/03/01 16:58:08 $ + */ +public class RequestEncodingFilter implements Filter { + + private String encoding; + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) req; + request.setCharacterEncoding(encoding); + + // Pass the request/response on + chain.doFilter(req, res); + } + + public void init(FilterConfig filterConfig) { + encoding = filterConfig.getInitParameter("encoding"); + } + + public void destroy() { + encoding = null; + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java new file mode 100644 index 00000000..33f60f83 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/ResponseHeaderFilter.java @@ -0,0 +1,57 @@ +/* + 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.filter; + +import javax.servlet.*; +import javax.servlet.http.*; +import java.io.*; +import java.util.*; + +/** + * Configurable filter for setting HTTP response headers. Can be used, for instance, to + * set cache control directives for certain resources. + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2005/08/14 13:14:47 $ + */ +public class ResponseHeaderFilter implements Filter { + private FilterConfig filterConfig; + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) + throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) res; + + // Sets the provided HTTP response parameters + for (Enumeration e = filterConfig.getInitParameterNames(); e.hasMoreElements();) { + String headerName = (String) e.nextElement(); + response.addHeader(headerName, filterConfig.getInitParameter(headerName)); + } + + // pass the request/response on + chain.doFilter(req, response); + } + + public void init(FilterConfig filterConfig) { + this.filterConfig = filterConfig; + } + + public void destroy() { + this.filterConfig = null; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java new file mode 100644 index 00000000..231ad6e7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/i18n/SubsonicLocaleResolver.java @@ -0,0 +1,104 @@ +/* + 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.i18n; + +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.domain.*; +import org.springframework.web.servlet.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Locale resolver implementation which returns the locale selected in the settings. + * + * @author Sindre Mehus + */ +public class SubsonicLocaleResolver implements LocaleResolver { + + private SecurityService securityService; + private SettingsService settingsService; + private Set<Locale> locales; + + /** + * Resolve the current locale via the given request. + * + * @param request Request to be used for resolution. + * @return The current locale. + */ + public Locale resolveLocale(HttpServletRequest request) { + Locale locale = (Locale) request.getAttribute("subsonic.locale"); + if (locale != null) { + return locale; + } + + // Optimization: Cache locale in the request. + locale = doResolveLocale(request); + request.setAttribute("subsonic.locale", locale); + + return locale; + } + + private Locale doResolveLocale(HttpServletRequest request) { + Locale locale = null; + + // Look for user-specific locale. + String username = securityService.getCurrentUsername(request); + if (username != null) { + UserSettings userSettings = settingsService.getUserSettings(username); + if (userSettings != null) { + locale = userSettings.getLocale(); + } + } + + if (locale != null && localeExists(locale)) { + return locale; + } + + // Return system locale. + locale = settingsService.getLocale(); + return localeExists(locale) ? locale : Locale.ENGLISH; + } + + /** + * Returns whether the given locale exists. + * @param locale The locale. + * @return Whether the locale exists. + */ + private synchronized boolean localeExists(Locale locale) { + // Lazily create set of locales. + if (locales == null) { + locales = new HashSet<Locale>(Arrays.asList(settingsService.getAvailableLocales())); + } + + return locales.contains(locale); + } + + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + throw new UnsupportedOperationException("Cannot change locale - use a different locale resolution strategy"); + } + + 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/io/InputStreamReaderThread.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java new file mode 100644 index 00000000..1019f73a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/InputStreamReaderThread.java @@ -0,0 +1,63 @@ +/* + 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.io; + +import net.sourceforge.subsonic.*; +import org.apache.commons.io.*; + +import java.io.*; + +/** + * Utility class which reads everything from an input stream and optionally logs it. + * + * @see TranscodeInputStream + * @author Sindre Mehus + */ +public class InputStreamReaderThread extends Thread { + + private static final Logger LOG = Logger.getLogger(InputStreamReaderThread.class); + + private InputStream input; + private String name; + private boolean log; + + public InputStreamReaderThread(InputStream input, String name, boolean log) { + super(name + " InputStreamLogger"); + this.input = input; + this.name = name; + this.log = log; + } + + public void run() { + BufferedReader reader = null; + try { + reader = new BufferedReader(new InputStreamReader(input)); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + if (log) { + LOG.debug('(' + name + ") " + line); + } + } + } catch (IOException x) { + // Intentionally ignored. + } finally { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(input); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java new file mode 100644 index 00000000..3be7fdd9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java @@ -0,0 +1,154 @@ +/* + 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.io; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.VideoTranscodingSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.TranscodingService; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +/** + * Implementation of {@link InputStream} which reads from a {@link net.sourceforge.subsonic.domain.PlayQueue}. + * + * @author Sindre Mehus + */ +public class PlayQueueInputStream extends InputStream { + + private static final Logger LOG = Logger.getLogger(PlayQueueInputStream.class); + + private final Player player; + private final TransferStatus status; + private final Integer maxBitRate; + private final String preferredTargetFormat; + private final VideoTranscodingSettings videoTranscodingSettings; + private final TranscodingService transcodingService; + private final AudioScrobblerService audioScrobblerService; + private final MediaFileService mediaFileService; + private MediaFile currentFile; + private InputStream currentInputStream; + private SearchService searchService; + + public PlayQueueInputStream(Player player, TransferStatus status, Integer maxBitRate, String preferredTargetFormat, + VideoTranscodingSettings videoTranscodingSettings, TranscodingService transcodingService, + AudioScrobblerService audioScrobblerService, MediaFileService mediaFileService, SearchService searchService) { + this.player = player; + this.status = status; + this.maxBitRate = maxBitRate; + this.preferredTargetFormat = preferredTargetFormat; + this.videoTranscodingSettings = videoTranscodingSettings; + this.transcodingService = transcodingService; + this.audioScrobblerService = audioScrobblerService; + this.mediaFileService = mediaFileService; + this.searchService = searchService; + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + int n = read(b); + return n == -1 ? -1 : b[0]; + } + + @Override + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + prepare(); + if (currentInputStream == null || player.getPlayQueue().getStatus() == PlayQueue.Status.STOPPED) { + return -1; + } + + int n = currentInputStream.read(b, off, len); + + // If end of song reached, skip to next song and call read() again. + if (n == -1) { + player.getPlayQueue().next(); + close(); + return read(b, off, len); + } else { + status.addBytesTransfered(n); + } + return n; + } + + private void prepare() throws IOException { + PlayQueue playQueue = player.getPlayQueue(); + + // If playlist is in auto-random mode, populate it with new random songs. + if (playQueue.getIndex() == -1 && playQueue.getRandomSearchCriteria() != null) { + populateRandomPlaylist(playQueue); + } + + MediaFile result; + synchronized (playQueue) { + result = playQueue.getCurrentFile(); + } + MediaFile file = result; + if (file == null) { + close(); + } else if (!file.equals(currentFile)) { + close(); + LOG.info(player.getUsername() + " listening to \"" + FileUtil.getShortPath(file.getFile()) + "\""); + mediaFileService.incrementPlayCount(file); + if (player.getClientId() == null) { // Don't scrobble REST players. + audioScrobblerService.register(file, player.getUsername(), false); + } + + TranscodingService.Parameters parameters = transcodingService.getParameters(file, player, maxBitRate, preferredTargetFormat, videoTranscodingSettings); + currentInputStream = transcodingService.getTranscodedInputStream(parameters); + currentFile = file; + status.setFile(currentFile.getFile()); + } + } + + private void populateRandomPlaylist(PlayQueue playQueue) throws IOException { + List<MediaFile> files = searchService.getRandomSongs(playQueue.getRandomSearchCriteria()); + playQueue.addFiles(false, files); + LOG.info("Recreated random playlist with " + playQueue.size() + " songs."); + } + + @Override + public void close() throws IOException { + try { + if (currentInputStream != null) { + currentInputStream.close(); + } + } finally { + if (player.getClientId() == null) { // Don't scrobble REST players. + audioScrobblerService.register(currentFile, player.getUsername(), true); + } + currentInputStream = null; + currentFile = null; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.java new file mode 100644 index 00000000..25bc03d2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.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.io; + +import org.apache.commons.lang.math.Range; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + + +/** + * Special output stream for grabbing only part of a passed stream. + * + * @author Sindre Mehus (based on code found on http://www.koders.com/ + */ +public class RangeOutputStream extends FilterOutputStream { + + /** + * The starting index. + */ + private long start; + + /** + * The ending index. + */ + private long end; + + /** + * The current position. + */ + protected long pos; + + /** + * Wraps the given output stream in a RangeOutputStream, using the values + * in the given range, unless the range is <code>null</code> in which case + * the original OutputStream is returned. + * + * @param out The output stream to wrap in a RangeOutputStream. + * @param range The range, may be <code>null</code>. + * @return The possibly wrapped output stream. + */ + public static OutputStream wrap(OutputStream out, Range range) { + if (range == null) { + return out; + } + return new RangeOutputStream(out, range.getMinimumLong(), range.getMaximumLong()); + } + + /** + * Creates the stream with the passed start and end. + * + * @param out The stream to write to. + * @param start The starting position. + * @param end The ending position. + */ + public RangeOutputStream(OutputStream out, long start, long end) { + super(out); + this.start = start; + this.end = end; + pos = 0; + } + + /** + * Writes the byte IF it is within the range, otherwise it only + * increments the position. + * + * @param b The byte to write. + * @throws IOException Thrown if there was a problem writing to the stream. + */ + @Override + public void write(int b) throws IOException { + if ((pos >= start) && (pos <= end)) { + super.write(b); + } + pos++; + } + + /** + * Writes the bytes IF it is within the range, otherwise it only + * increments the position. + * + * @param b The bytes to write. + * @param off The offset to start at. + * @param len The length to write. + * @throws IOException Thrown if there was a problem writing to the stream. + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + boolean allowWrite = false; + long newPos = pos + off, newOff = off, newLen = len; + + // Check to see if we are in the range + if (newPos <= end) { + if (newPos >= start) { + // We are so check to make sure we don't leave it + if (newPos + newLen > end) { + newLen = end - newPos; + } + + // Enable writing + allowWrite = true; + } + + // We aren't yet in the range, but if see if the proposed write + // would place us there + else if (newPos + newLen >= start) { + // It would so, update the offset + newOff += start - newPos; + + // New offset means, a new position, so update that too + newPos = newOff + pos; + newLen = len + (pos - newPos); + + // Make sure we don't go past the range + if (newPos + newLen > end) { + newLen = end - newPos; + } + + // Enable writting + allowWrite = true; + } + } + + // If we have enabled writing, do the write! + if (allowWrite) { + out.write(b, (int) newOff, (int) newLen); + } + + // Move the cursor along + pos += off + len; + } +} + diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java new file mode 100644 index 00000000..9a8618c6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/ShoutCastOutputStream.java @@ -0,0 +1,205 @@ +/* + 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.io; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +/** + * Implements SHOUTcast support by decorating an existing output stream. + * <p/> + * Based on protocol description found on + * <em>http://www.smackfu.com/stuff/programming/shoutcast.html</em> + * + * @author Sindre Mehus + */ +public class ShoutCastOutputStream extends OutputStream { + + private static final Logger LOG = Logger.getLogger(ShoutCastOutputStream.class); + + /** + * Number of bytes between each SHOUTcast metadata block. + */ + public static final int META_DATA_INTERVAL = 20480; + + /** + * The underlying output stream to decorate. + */ + private OutputStream out; + + /** + * What to write in the SHOUTcast metadata is fetched from the playlist. + */ + private PlayQueue playQueue; + + /** + * Keeps track of the number of bytes written (excluding meta-data). Between 0 and {@link #META_DATA_INTERVAL}. + */ + private int byteCount; + + /** + * The last stream title sent. + */ + private String previousStreamTitle; + + private SettingsService settingsService; + + /** + * Creates a new SHOUTcast-decorated stream for the given output stream. + * + * @param out The output stream to decorate. + * @param playQueue Meta-data is fetched from this playlist. + */ + public ShoutCastOutputStream(OutputStream out, PlayQueue playQueue, SettingsService settingsService) { + this.out = out; + this.playQueue = playQueue; + this.settingsService = settingsService; + } + + /** + * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary. + */ + public void write(byte[] b, int off, int len) throws IOException { + + int bytesWritten = 0; + while (bytesWritten < len) { + + // 'n' is the number of bytes to write before the next potential meta-data block. + int n = Math.min(len - bytesWritten, ShoutCastOutputStream.META_DATA_INTERVAL - byteCount); + + out.write(b, off + bytesWritten, n); + bytesWritten += n; + byteCount += n; + + // Reached meta-data block? + if (byteCount % ShoutCastOutputStream.META_DATA_INTERVAL == 0) { + writeMetaData(); + byteCount = 0; + } + } + } + + /** + * Writes the given byte array to the underlying stream, adding SHOUTcast meta-data as necessary. + */ + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Writes the given byte to the underlying stream, adding SHOUTcast meta-data as necessary. + */ + public void write(int b) throws IOException { + byte[] buf = new byte[]{(byte) b}; + write(buf); + } + + /** + * Flushes the underlying stream. + */ + public void flush() throws IOException { + out.flush(); + } + + /** + * Closes the underlying stream. + */ + public void close() throws IOException { + out.close(); + } + + private void writeMetaData() throws IOException { + String streamTitle = StringUtils.trimToEmpty(settingsService.getWelcomeTitle()); + + MediaFile result; + synchronized (playQueue) { + result = playQueue.getCurrentFile(); + } + MediaFile mediaFile = result; + if (mediaFile != null) { + streamTitle = mediaFile.getArtist() + " - " + mediaFile.getTitle(); + } + + byte[] bytes; + + if (streamTitle.equals(previousStreamTitle)) { + bytes = new byte[0]; + } else { + try { + previousStreamTitle = streamTitle; + bytes = createStreamTitle(streamTitle); + } catch (UnsupportedEncodingException x) { + LOG.warn("Failed to create SHOUTcast meta-data. Ignoring.", x); + bytes = new byte[0]; + } + } + + // Length in groups of 16 bytes. + int length = bytes.length / 16; + if (bytes.length % 16 > 0) { + length++; + } + + // Write the length as a single byte. + out.write(length); + + // Write the message. + out.write(bytes); + + // Write padding zero bytes. + int padding = length * 16 - bytes.length; + for (int i = 0; i < padding; i++) { + out.write(0); + } + } + + private byte[] createStreamTitle(String title) throws UnsupportedEncodingException { + // Remove any quotes from the title. + title = title.replaceAll("'", ""); + + // Convert non-ascii characters to similar ascii characters. + for (char[] chars : ShoutCastOutputStream.CHAR_MAP) { + title = title.replace(chars[0], chars[1]); + } + + title = "StreamTitle='" + title + "';"; + return title.getBytes("US-ASCII"); + } + + /** + * Maps from miscellaneous accented characters to similar-looking ASCII characters. + */ + private static final char[][] CHAR_MAP = { + {'\u00C0', 'A'}, {'\u00C1', 'A'}, {'\u00C2', 'A'}, {'\u00C3', 'A'}, {'\u00C4', 'A'}, {'\u00C5', 'A'}, {'\u00C6', 'A'}, + {'\u00C8', 'E'}, {'\u00C9', 'E'}, {'\u00CA', 'E'}, {'\u00CB', 'E'}, {'\u00CC', 'I'}, {'\u00CD', 'I'}, {'\u00CE', 'I'}, + {'\u00CF', 'I'}, {'\u00D2', 'O'}, {'\u00D3', 'O'}, {'\u00D4', 'O'}, {'\u00D5', 'O'}, {'\u00D6', 'O'}, {'\u00D9', 'U'}, + {'\u00DA', 'U'}, {'\u00DB', 'U'}, {'\u00DC', 'U'}, {'\u00DF', 'B'}, {'\u00E0', 'a'}, {'\u00E1', 'a'}, {'\u00E2', 'a'}, + {'\u00E3', 'a'}, {'\u00E4', 'a'}, {'\u00E5', 'a'}, {'\u00E6', 'a'}, {'\u00E7', 'c'}, {'\u00E8', 'e'}, {'\u00E9', 'e'}, + {'\u00EA', 'e'}, {'\u00EB', 'e'}, {'\u00EC', 'i'}, {'\u00ED', 'i'}, {'\u00EE', 'i'}, {'\u00EF', 'i'}, {'\u00F1', 'n'}, + {'\u00F2', 'o'}, {'\u00F3', 'o'}, {'\u00F4', 'o'}, {'\u00F5', 'o'}, {'\u00F6', 'o'}, {'\u00F8', 'o'}, {'\u00F9', 'u'}, + {'\u00FA', 'u'}, {'\u00FB', 'u'}, {'\u00FC', 'u'}, {'\u2013', '-'} + }; +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java new file mode 100644 index 00000000..b7a5e31e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/TranscodeInputStream.java @@ -0,0 +1,124 @@ +/* + 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.io; + +import net.sourceforge.subsonic.*; + +import org.apache.commons.io.*; + +import java.io.*; + +/** + * Subclass of {@link InputStream} which provides on-the-fly transcoding. + * Instances of <code>TranscodeInputStream</code> can be chained together, for instance to convert + * from OGG to WAV to MP3. + * + * @author Sindre Mehus + */ +public class TranscodeInputStream extends InputStream { + + private static final Logger LOG = Logger.getLogger(TranscodeInputStream.class); + + private InputStream processInputStream; + private OutputStream processOutputStream; + private Process process; + private final File tmpFile; + + /** + * Creates a transcoded input stream by executing an external process. If <code>in</code> is not null, + * data from it is copied to the command. + * + * @param processBuilder Used to create the external process. + * @param in Data to feed to the process. May be {@code null}. + * @param tmpFile Temporary file to delete when this stream is closed. May be {@code null}. + * @throws IOException If an I/O error occurs. + */ + public TranscodeInputStream(ProcessBuilder processBuilder, final InputStream in, File tmpFile) throws IOException { + this.tmpFile = tmpFile; + + StringBuffer buf = new StringBuffer("Starting transcoder: "); + for (String s : processBuilder.command()) { + buf.append('[').append(s).append("] "); + } + LOG.debug(buf); + + process = processBuilder.start(); + processOutputStream = process.getOutputStream(); + processInputStream = process.getInputStream(); + + // Must read stderr from the process, otherwise it may block. + final String name = processBuilder.command().get(0); + new InputStreamReaderThread(process.getErrorStream(), name, true).start(); + + // Copy data in a separate thread + if (in != null) { + new Thread(name + " TranscodedInputStream copy thread") { + public void run() { + try { + IOUtils.copy(in, processOutputStream); + } catch (IOException x) { + // Intentionally ignored. Will happen if the remote player closes the stream. + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(processOutputStream); + } + } + }.start(); + } + } + + /** + * @see InputStream#read() + */ + public int read() throws IOException { + return processInputStream.read(); + } + + /** + * @see InputStream#read(byte[]) + */ + public int read(byte[] b) throws IOException { + return processInputStream.read(b); + } + + /** + * @see InputStream#read(byte[], int, int) + */ + public int read(byte[] b, int off, int len) throws IOException { + return processInputStream.read(b, off, len); + } + + /** + * @see InputStream#close() + */ + public void close() throws IOException { + IOUtils.closeQuietly(processInputStream); + IOUtils.closeQuietly(processOutputStream); + + if (process != null) { + process.destroy(); + } + + if (tmpFile != null) { + if (!tmpFile.delete()) { + LOG.warn("Failed to delete tmp file: " + tmpFile); + } + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java new file mode 100644 index 00000000..fee4ff2c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/SubsonicLdapBindAuthenticator.java @@ -0,0 +1,131 @@ +/* + 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.ldap; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.acegisecurity.BadCredentialsException; +import org.acegisecurity.ldap.DefaultInitialDirContextFactory; +import org.acegisecurity.ldap.search.FilterBasedLdapUserSearch; +import org.acegisecurity.providers.ldap.LdapAuthenticator; +import org.acegisecurity.providers.ldap.authenticator.BindAuthenticator; +import org.acegisecurity.userdetails.ldap.LdapUserDetails; +import org.apache.commons.lang.StringUtils; + +import java.util.HashMap; +import java.util.Map; + +/** + * LDAP authenticator which uses a delegate {@link BindAuthenticator}, and which + * supports dynamically changing LDAP provider URL and search filter. + * + * @author Sindre Mehus + */ +public class SubsonicLdapBindAuthenticator implements LdapAuthenticator { + + private static final Logger LOG = Logger.getLogger(SubsonicLdapBindAuthenticator.class); + + private SecurityService securityService; + private SettingsService settingsService; + + private long authenticatorTimestamp; + private BindAuthenticator delegateAuthenticator; + + public LdapUserDetails authenticate(String username, String password) { + + // LDAP authentication must be enabled on the system. + if (!settingsService.isLdapEnabled()) { + throw new BadCredentialsException("LDAP authentication disabled."); + } + + // User must be defined in Subsonic, unless auto-shadowing is enabled. + User user = securityService.getUserByName(username); + if (user == null && !settingsService.isLdapAutoShadowing()) { + throw new BadCredentialsException("User does not exist."); + } + + // LDAP authentication must be enabled for the given user. + if (user != null && !user.isLdapAuthenticated()) { + throw new BadCredentialsException("LDAP authentication disabled for user."); + } + + try { + createDelegate(); + LdapUserDetails details = delegateAuthenticator.authenticate(username, password); + if (details != null) { + LOG.info("User '" + username + "' successfully authenticated in LDAP. DN: " + details.getDn()); + + if (user == null) { + User newUser = new User(username, "", null, true, 0L, 0L, 0L); + newUser.setStreamRole(true); + newUser.setSettingsRole(true); + securityService.createUser(newUser); + LOG.info("Created local user '" + username + "' for DN " + details.getDn()); + } + } + + return details; + } catch (RuntimeException x) { + LOG.info("Failed to authenticate user '" + username + "' in LDAP.", x); + throw x; + } + } + + /** + * Creates the delegate {@link BindAuthenticator}. + */ + private synchronized void createDelegate() { + + // Only create it if necessary. + if (delegateAuthenticator == null || authenticatorTimestamp < settingsService.getSettingsChanged()) { + + DefaultInitialDirContextFactory contextFactory = new DefaultInitialDirContextFactory(settingsService.getLdapUrl()); + + String managerDn = settingsService.getLdapManagerDn(); + String managerPassword = settingsService.getLdapManagerPassword(); + if (StringUtils.isNotEmpty(managerDn) && StringUtils.isNotEmpty(managerPassword)) { + contextFactory.setManagerDn(managerDn); + contextFactory.setManagerPassword(managerPassword); + } + + Map<String, String> extraEnvVars = new HashMap<String, String>(); + extraEnvVars.put("java.naming.referral", "follow"); + contextFactory.setExtraEnvVars(extraEnvVars); + + FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch("", settingsService.getLdapSearchFilter(), contextFactory); + userSearch.setSearchSubtree(true); + userSearch.setDerefLinkFlag(true); + + delegateAuthenticator = new BindAuthenticator(contextFactory); + delegateAuthenticator.setUserSearch(userSearch); + + authenticatorTimestamp = settingsService.getSettingsChanged(); + } + } + + 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/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java new file mode 100644 index 00000000..a3b9359e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ldap/UserDetailsServiceBasedAuthoritiesPopulator.java @@ -0,0 +1,50 @@ +/* + 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.ldap; + +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.ldap.LdapDataAccessException; +import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator; +import org.acegisecurity.userdetails.UserDetailsService; +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.ldap.LdapUserDetails; + +/** + * An {@link LdapAuthoritiesPopulator} that retrieves the roles from the + * database using the {@link UserDetailsService} instead of retrieving the roles + * from LDAP. An instance of this class can be configured for the + * {@link org.acegisecurity.providers.ldap.LdapAuthenticationProvider} when + * authentication should be done using LDAP and authorization using the + * information stored in the database. + * + * @author Thomas M. Hofmann + */ +public class UserDetailsServiceBasedAuthoritiesPopulator implements LdapAuthoritiesPopulator { + + private UserDetailsService userDetailsService; + + public GrantedAuthority[] getGrantedAuthorities(LdapUserDetails userDetails) throws LdapDataAccessException { + UserDetails details = userDetailsService.loadUserByUsername(userDetails.getUsername()); + return details.getAuthorities(); + } + + public void setUserDetailsService(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java new file mode 100644 index 00000000..add44643 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java @@ -0,0 +1,246 @@ +/* + 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.security; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.domain.Version; +import net.sourceforge.subsonic.controller.RESTController; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.XMLBuilder; +import org.acegisecurity.Authentication; +import org.acegisecurity.AuthenticationException; +import org.acegisecurity.context.SecurityContextHolder; +import org.acegisecurity.providers.ProviderManager; +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +/** + * Performs authentication based on credentials being present in the HTTP request parameters. Also checks + * API versions and license information. + * <p/> + * The username should be set in parameter "u", and the password should be set in parameter "p". + * The REST protocol version should be set in parameter "v". + * + * The password can either be in plain text or be UTF-8 hexencoded preceded by "enc:". + * + * @author Sindre Mehus + */ +public class RESTRequestParameterProcessingFilter implements Filter { + + private static final Logger LOG = Logger.getLogger(RESTRequestParameterProcessingFilter.class); + private static final long TRIAL_DAYS = 35L; + + private ProviderManager authenticationManager; + private SettingsService settingsService; + + /** + * {@inheritDoc} + */ + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException("Can only process HttpServletRequest"); + } + if (!(response instanceof HttpServletResponse)) { + throw new ServletException("Can only process HttpServletResponse"); + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + String username = StringUtils.trimToNull(httpRequest.getParameter("u")); + String password = decrypt(StringUtils.trimToNull(httpRequest.getParameter("p"))); + String version = StringUtils.trimToNull(httpRequest.getParameter("v")); + String client = StringUtils.trimToNull(httpRequest.getParameter("c")); + + RESTController.ErrorCode errorCode = null; + + // The username and password parameters are not required if the user + // was previously authenticated, for example using Basic Auth. + Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication(); + boolean missingCredentials = previousAuth == null && (username == null || password == null); + if (missingCredentials || version == null || client == null) { + errorCode = RESTController.ErrorCode.MISSING_PARAMETER; + } + + if (errorCode == null) { + errorCode = checkAPIVersion(version); + } + + if (errorCode == null) { + errorCode = authenticate(username, password, previousAuth); + } + + if (errorCode == null) { + String restMethod = StringUtils.substringAfterLast(httpRequest.getRequestURI(), "/"); + errorCode = checkLicense(client, restMethod); + } + + if (errorCode == null) { + chain.doFilter(request, response); + } else { + SecurityContextHolder.getContext().setAuthentication(null); + sendErrorXml(httpRequest, httpResponse, errorCode); + } + } + + private RESTController.ErrorCode checkAPIVersion(String version) { + Version serverVersion = new Version(StringUtil.getRESTProtocolVersion()); + Version clientVersion = new Version(version); + + if (serverVersion.getMajor() > clientVersion.getMajor()) { + return RESTController.ErrorCode.PROTOCOL_MISMATCH_CLIENT_TOO_OLD; + } else if (serverVersion.getMajor() < clientVersion.getMajor()) { + return RESTController.ErrorCode.PROTOCOL_MISMATCH_SERVER_TOO_OLD; + } else if (serverVersion.getMinor() < clientVersion.getMinor()) { + return RESTController.ErrorCode.PROTOCOL_MISMATCH_SERVER_TOO_OLD; + } + return null; + } + + private RESTController.ErrorCode authenticate(String username, String password, Authentication previousAuth) { + + // Previously authenticated and username not overridden? + if (username == null && previousAuth != null) { + return null; + } + + // Ensure password is given. + if (password == null) { + return RESTController.ErrorCode.MISSING_PARAMETER; + } + + try { + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); + Authentication authResult = authenticationManager.authenticate(authRequest); + SecurityContextHolder.getContext().setAuthentication(authResult); +// LOG.info("Authentication succeeded for user " + username); + } catch (AuthenticationException x) { + LOG.info("Authentication failed for user " + username); + return RESTController.ErrorCode.NOT_AUTHENTICATED; + } + return null; + } + + private RESTController.ErrorCode checkLicense(String client, String restMethod) { + if (settingsService.isLicenseValid()) { + return null; + } + + if (settingsService.getRESTTrialExpires(client) == null) { + Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L); + settingsService.setRESTTrialExpires(client, expiryDate); + settingsService.save(); + LOG.info("REST access for client '" + client + "' will expire " + expiryDate); + } else if (settingsService.getRESTTrialExpires(client).before(new Date())) { + + // Exception: iPhone clients are allowed to call any method except stream.view and download.view. + List<String> iPhoneClients = Arrays.asList("iSub", "zsubsonic"); + List<String> restrictedMethods = Arrays.asList("stream.view", "download.view"); + if (iPhoneClients.contains(client) && !restrictedMethods.contains(restMethod)) { + return null; + } + + LOG.info("REST access for client '" + client + "' has expired."); + return RESTController.ErrorCode.NOT_LICENSED; + } + + return null; + } + + public static String decrypt(String s) { + if (s == null) { + return null; + } + if (!s.startsWith("enc:")) { + return s; + } + try { + return StringUtil.utf8HexDecode(s.substring(4)); + } catch (Exception e) { + return s; + } + } + + private void sendErrorXml(HttpServletRequest request, HttpServletResponse response, RESTController.ErrorCode errorCode) 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 XMLBuilder.Attribute("xmlns", "http://subsonic.org/restapi"), + new XMLBuilder.Attribute("status", "failed"), + new XMLBuilder.Attribute("version", StringUtil.getRESTProtocolVersion())); + + builder.add("error", true, + new XMLBuilder.Attribute("code", errorCode.getCode()), + new XMLBuilder.Attribute("message", errorCode.getMessage())); + builder.end(); + response.getWriter().print(builder); + } + + /** + * {@inheritDoc} + */ + public void init(FilterConfig filterConfig) throws ServletException { + } + + /** + * {@inheritDoc} + */ + public void destroy() { + } + + public void setAuthenticationManager(ProviderManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java new file mode 100644 index 00000000..9ae4d765 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java @@ -0,0 +1,71 @@ +/* + 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.service; + +/** + * Provides services for generating ads. + * + * @author Sindre Mehus + */ +public class AdService { + + private final String[] ads = { + + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=computers_accesories&banner=1CH7VNNWF908JYQPHX82&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=21&l=ur1&category=game_downloads&banner=13PTQH69Q2290VF8SR82&f=ifr' width='125' height='125' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='ad/omakasa.html' width='120' height='240' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=29&l=ur1&category=homeaudiohometheater&banner=0T4YJ6YBNCMJM9GGAK02&f=ifr' width='120' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=21&l=ur1&category=50mp3albums5each&banner=19QT8FZHDHFZDN87C482&f=ifr' width='125' height='125' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=21&l=ur1&category=computers_accesories&banner=0Q1FJ9TBD13SA09DSMR2&f=ifr' width='125' height='125' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=mp3&banner=0TBQHNYNA4B47J02NFG2&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<OBJECT classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab' id='Player_3d36fdd7-b2fa-4dfd-b517-c5efe035a14d' WIDTH='120px' HEIGHT='500px'> <PARAM NAME='movie' VALUE='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8010%2F3d36fdd7-b2fa-4dfd-b517-c5efe035a14d&Operation=GetDisplayTemplate'><PARAM NAME='quality' VALUE='high'><PARAM NAME='bgcolor' VALUE='#FFFFFF'><PARAM NAME='allowscriptaccess' VALUE='always'><embed src='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8010%2F3d36fdd7-b2fa-4dfd-b517-c5efe035a14d&Operation=GetDisplayTemplate' id='Player_3d36fdd7-b2fa-4dfd-b517-c5efe035a14d' quality='high' bgcolor='#ffffff' name='Player_3d36fdd7-b2fa-4dfd-b517-c5efe035a14d' allowscriptaccess='always' type='application/x-shockwave-flash' align='middle' height='500px' width='120px'/> </OBJECT> <NOSCRIPT><A HREF='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8010%2F3d36fdd7-b2fa-4dfd-b517-c5efe035a14d&Operation=NoScript'>Amazon.com Widgets</A></NOSCRIPT>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=kindle&banner=19NTJJCKSX6TY1C567G2&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='ad/omakasa.html' width='120' height='240' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=14&l=ur1&category=electronicsrot&f=ifr' width='160' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<OBJECT classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab' id='Player_3fde1609-804d-46de-8802-2a16321cf533' WIDTH='160px' HEIGHT='400px'> <PARAM NAME='movie' VALUE='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8009%2F3fde1609-804d-46de-8802-2a16321cf533&Operation=GetDisplayTemplate'><PARAM NAME='quality' VALUE='high'><PARAM NAME='bgcolor' VALUE='#FFFFFF'><PARAM NAME='allowscriptaccess' VALUE='always'><embed src='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8009%2F3fde1609-804d-46de-8802-2a16321cf533&Operation=GetDisplayTemplate' id='Player_3fde1609-804d-46de-8802-2a16321cf533' quality='high' bgcolor='#ffffff' name='Player_3fde1609-804d-46de-8802-2a16321cf533' allowscriptaccess='always' type='application/x-shockwave-flash' align='middle' height='400px' width='160px'/> </OBJECT> <NOSCRIPT><A HREF='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8009%2F3fde1609-804d-46de-8802-2a16321cf533&Operation=NoScript'>Amazon.com Widgets</A></NOSCRIPT>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=40&l=ur1&category=unboxdigital&banner=10NVPFMW8ACPNX4T4E82&f=ifr' width='120' height='60' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<OBJECT classid='clsid:D27CDB6E-AE6D-11cf-96B8-444553540000' codebase='http://fpdownload.macromedia.com/get/flashplayer/current/swflash.cab' id='Player_2e2141ca-ec13-4dc9-88f2-08be95e47e6d' WIDTH='160px' HEIGHT='300px'> <PARAM NAME='movie' VALUE='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8014%2F2e2141ca-ec13-4dc9-88f2-08be95e47e6d&Operation=GetDisplayTemplate'><PARAM NAME='quality' VALUE='high'><PARAM NAME='bgcolor' VALUE='#FFFFFF'><PARAM NAME='allowscriptaccess' VALUE='always'><embed src='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8014%2F2e2141ca-ec13-4dc9-88f2-08be95e47e6d&Operation=GetDisplayTemplate' id='Player_2e2141ca-ec13-4dc9-88f2-08be95e47e6d' quality='high' bgcolor='#ffffff' name='Player_2e2141ca-ec13-4dc9-88f2-08be95e47e6d' allowscriptaccess='always' type='application/x-shockwave-flash' align='middle' height='300px' width='160px'></embed></OBJECT> <NOSCRIPT><A HREF='http://ws.amazon.com/widgets/q?ServiceVersion=20070822&MarketPlace=US&ID=V20070822%2FUS%2Fsubsonic-20%2F8014%2F2e2141ca-ec13-4dc9-88f2-08be95e47e6d&Operation=NoScript'>Amazon.com Widgets</A></NOSCRIPT>", + "<iframe src='http://rcm.amazon.com/e/cm?t=subsonic-20&o=1&p=14&l=ur1&category=musicandentertainmentrot&f=ifr' width='160' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://www.subsonic.org/pages/subsonic-ad.jsp' width='180' height='400' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>", + "<iframe src='http://www.subsonic.org/pages/zazeen-ad.jsp' width='120' height='600' scrolling='no' border='0' marginwidth='0' style='border:none;' frameborder='0'></iframe>" + }; + private int adInterval; + private int pageCount; + private int adIndex; + + /** + * Returns an ad or <code>null</code> if no ad should be displayed. + */ + public String getAd() { + if (pageCount++ % adInterval == 0) { + + adIndex = (adIndex + 1) % ads.length; + return ads[adIndex]; + } + + return null; + } + + /** + * Set by Spring. + */ + public void setAdInterval(int adInterval) { + this.adInterval = adInterval; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java new file mode 100644 index 00000000..9ca402b8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.java @@ -0,0 +1,331 @@ +/* + 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.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.LinkedBlockingQueue; + +import net.sourceforge.subsonic.domain.MediaFile; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpConnectionParams; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Provides services for "audioscrobbling", which is the process of + * registering what songs are played at www.last.fm. + * <p/> + * See http://www.last.fm/api/submissions + * + * @author Sindre Mehus + */ +public class AudioScrobblerService { + + private static final Logger LOG = Logger.getLogger(AudioScrobblerService.class); + private static final int MAX_PENDING_REGISTRATION = 2000; + private static final long MIN_REGISTRATION_INTERVAL = 30000L; + + private RegistrationThread thread; + private final Map<String, Long> lastRegistrationTimes = new HashMap<String, Long>(); + private final LinkedBlockingQueue<RegistrationData> queue = new LinkedBlockingQueue<RegistrationData>(); + + private SettingsService settingsService; + + /** + * Registers the given media file at www.last.fm. This method returns immediately, the actual registration is done + * by a separate thread. + * + * @param mediaFile The media file to register. + * @param username The user which played the music file. + * @param submission Whether this is a submission or a now playing notification. + */ + public synchronized void register(MediaFile mediaFile, String username, boolean submission) { + + if (thread == null) { + thread = new RegistrationThread(); + thread.start(); + } + + if (queue.size() >= MAX_PENDING_REGISTRATION) { + LOG.warn("Last.fm scrobbler queue is full. Ignoring " + mediaFile); + return; + } + + RegistrationData registrationData = createRegistrationData(mediaFile, username, submission); + if (registrationData == null) { + return; + } + + try { + queue.put(registrationData); + } catch (InterruptedException x) { + LOG.warn("Interrupted while queuing Last.fm scrobble.", x); + } + } + + /** + * Returns registration details, or <code>null</code> if not eligible for registration. + */ + private RegistrationData createRegistrationData(MediaFile mediaFile, String username, boolean submission) { + + if (mediaFile == null || mediaFile.isVideo()) { + return null; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + if (!userSettings.isLastFmEnabled() || userSettings.getLastFmUsername() == null || userSettings.getLastFmPassword() == null) { + return null; + } + + long now = System.currentTimeMillis(); + + // Don't register submissions more often than every 30 seconds. + if (submission) { + Long lastRegistrationTime = lastRegistrationTimes.get(username); + if (lastRegistrationTime != null && now - lastRegistrationTime < MIN_REGISTRATION_INTERVAL) { + return null; + } + lastRegistrationTimes.put(username, now); + } + + RegistrationData reg = new RegistrationData(); + reg.username = userSettings.getLastFmUsername(); + reg.password = userSettings.getLastFmPassword(); + reg.artist = mediaFile.getArtist(); + reg.album = mediaFile.getAlbumName(); + reg.title = mediaFile.getTitle(); + reg.duration = mediaFile.getDurationSeconds() == null ? 0 : mediaFile.getDurationSeconds(); + reg.time = new Date(now); + reg.submission = submission; + + return reg; + } + + /** + * Scrobbles the given song data at last.fm, using the protocol defined at http://www.last.fm/api/submissions. + * + * @param registrationData Registration data for the song. + */ + private void scrobble(RegistrationData registrationData) throws Exception { + if (registrationData == null) { + return; + } + + String[] lines = authenticate(registrationData); + if (lines == null) { + return; + } + + String sessionId = lines[1]; + String nowPlayingUrl = lines[2]; + String submissionUrl = lines[3]; + + if (registrationData.submission) { + lines = registerSubmission(registrationData, sessionId, submissionUrl); + } else { + lines = registerNowPlaying(registrationData, sessionId, nowPlayingUrl); + } + + if (lines[0].startsWith("FAILED")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm: " + lines[0]); + } else if (lines[0].startsWith("BADSESSION")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Invalid session."); + } else if (lines[0].startsWith("OK")) { + LOG.debug("Successfully registered " + (registrationData.submission ? "submission" : "now playing") + + " for song '" + registrationData.title + "' for user " + registrationData.username + " at Last.fm."); + } + } + + /** + * Returns the following lines if authentication succeeds: + * <p/> + * Line 0: Always "OK" + * Line 1: Session ID, e.g., "17E61E13454CDD8B68E8D7DEEEDF6170" + * Line 2: URL to use for now playing, e.g., "http://post.audioscrobbler.com:80/np_1.2" + * Line 3: URL to use for submissions, e.g., "http://post2.audioscrobbler.com:80/protocol_1.2" + * <p/> + * If authentication fails, <code>null</code> is returned. + */ + private String[] authenticate(RegistrationData registrationData) throws Exception { + String clientId = "sub"; + String clientVersion = "0.1"; + long timestamp = System.currentTimeMillis() / 1000L; + String authToken = calculateAuthenticationToken(registrationData.password, timestamp); + String[] lines = executeGetRequest("http://post.audioscrobbler.com/?hs=true&p=1.2.1&c=" + clientId + "&v=" + + clientVersion + "&u=" + registrationData.username + "&t=" + timestamp + "&a=" + authToken); + + if (lines[0].startsWith("BANNED")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Client version is banned."); + return null; + } + + if (lines[0].startsWith("BADAUTH")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Wrong username or password."); + return null; + } + + if (lines[0].startsWith("BADTIME")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Bad timestamp, please check local clock."); + return null; + } + + if (lines[0].startsWith("FAILED")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm: " + lines[0]); + return null; + } + + if (!lines[0].startsWith("OK")) { + LOG.warn("Failed to scrobble song '" + registrationData.title + "' at Last.fm. Unknown response: " + lines[0]); + return null; + } + + return lines; + } + + private String[] registerSubmission(RegistrationData registrationData, String sessionId, String url) throws IOException { + Map<String, String> params = new HashMap<String, String>(); + params.put("s", sessionId); + params.put("a[0]", registrationData.artist); + params.put("t[0]", registrationData.title); + params.put("i[0]", String.valueOf(registrationData.time.getTime() / 1000L)); + params.put("o[0]", "P"); + params.put("r[0]", ""); + params.put("l[0]", String.valueOf(registrationData.duration)); + params.put("b[0]", registrationData.album); + params.put("n[0]", ""); + params.put("m[0]", ""); + return executePostRequest(url, params); + } + + private String[] registerNowPlaying(RegistrationData registrationData, String sessionId, String url) throws IOException { + Map<String, String> params = new HashMap<String, String>(); + params.put("s", sessionId); + params.put("a", registrationData.artist); + params.put("t", registrationData.title); + params.put("b", registrationData.album); + params.put("l", String.valueOf(registrationData.duration)); + params.put("n", ""); + params.put("m", ""); + return executePostRequest(url, params); + } + + private String calculateAuthenticationToken(String password, long timestamp) { + return DigestUtils.md5Hex(DigestUtils.md5Hex(password) + timestamp); + } + + private String[] executeGetRequest(String url) throws IOException { + return executeRequest(new HttpGet(url)); + } + + private String[] executePostRequest(String url, Map<String, String> parameters) throws IOException { + List<NameValuePair> params = new ArrayList<NameValuePair>(); + for (Map.Entry<String, String> entry : parameters.entrySet()) { + params.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); + } + + HttpPost request = new HttpPost(url); + request.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8)); + + return executeRequest(request); + } + + private String[] executeRequest(HttpUriRequest request) throws IOException { + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 15000); + HttpConnectionParams.setSoTimeout(client.getParams(), 15000); + + try { + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + String response = client.execute(request, responseHandler); + return response.split("\\n"); + + } finally { + client.getConnectionManager().shutdown(); + } + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + private class RegistrationThread extends Thread { + private RegistrationThread() { + super("AudioScrobbler Registration"); + } + + @Override + public void run() { + while (true) { + RegistrationData registrationData = null; + try { + registrationData = queue.take(); + scrobble(registrationData); + } catch (IOException x) { + handleNetworkError(registrationData, x); + } catch (Exception x) { + LOG.warn("Error in Last.fm registration.", x); + } + } + } + + private void handleNetworkError(RegistrationData registrationData, IOException x) { + try { + queue.put(registrationData); + LOG.info("Last.fm registration for " + registrationData.title + + " encountered network error. Will try again later. In queue: " + queue.size(), x); + } catch (InterruptedException e) { + LOG.error("Failed to reschedule Last.fm registration for " + registrationData.title, e); + } + try { + sleep(15L * 60L * 1000L); // Wait 15 minutes. + } catch (InterruptedException e) { + LOG.error("Failed to sleep after Last.fm registration failure for " + registrationData.title, e); + } + } + } + + private static class RegistrationData { + private String username; + private String password; + private String artist; + private String album; + private String title; + private int duration; + private Date time; + public boolean submission; + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java new file mode 100644 index 00000000..9f2eff22 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java @@ -0,0 +1,206 @@ +/* + 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.service; + +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.domain.Transcoding; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.VideoTranscodingSettings; +import net.sourceforge.subsonic.service.jukebox.AudioPlayer; +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.commons.io.IOUtils; + +import java.io.InputStream; + +import static net.sourceforge.subsonic.service.jukebox.AudioPlayer.State.EOM; + +/** + * Plays music on the local audio device. + * + * @author Sindre Mehus + */ +public class JukeboxService implements AudioPlayer.Listener { + + private static final Logger LOG = Logger.getLogger(JukeboxService.class); + + private AudioPlayer audioPlayer; + private TranscodingService transcodingService; + private AudioScrobblerService audioScrobblerService; + private StatusService statusService; + private SettingsService settingsService; + private SecurityService securityService; + + private Player player; + private TransferStatus status; + private MediaFile currentPlayingFile; + private float gain = 0.5f; + private int offset; + private MediaFileService mediaFileService; + + /** + * Updates the jukebox by starting or pausing playback on the local audio device. + * + * @param player The player in question. + * @param offset Start playing after this many seconds into the track. + */ + public synchronized void updateJukebox(Player player, int offset) throws Exception { + User user = securityService.getUserByName(player.getUsername()); + if (!user.isJukeboxRole()) { + LOG.warn(user.getUsername() + " is not authorized for jukebox playback."); + return; + } + + if (player.getPlayQueue().getStatus() == PlayQueue.Status.PLAYING) { + this.player = player; + MediaFile result; + synchronized (player.getPlayQueue()) { + result = player.getPlayQueue().getCurrentFile(); + } + play(result, offset); + } else { + if (audioPlayer != null) { + audioPlayer.pause(); + } + } + } + + private synchronized void play(MediaFile file, int offset) { + InputStream in = null; + try { + + // Resume if possible. + boolean sameFile = file != null && file.equals(currentPlayingFile); + boolean paused = audioPlayer != null && audioPlayer.getState() == AudioPlayer.State.PAUSED; + if (sameFile && paused && offset == 0) { + audioPlayer.play(); + } else { + this.offset = offset; + if (audioPlayer != null) { + audioPlayer.close(); + if (currentPlayingFile != null) { + onSongEnd(currentPlayingFile); + } + } + + if (file != null) { + TranscodingService.Parameters parameters = new TranscodingService.Parameters(file, new VideoTranscodingSettings(0, 0, offset)); + String command = settingsService.getJukeboxCommand(); + parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false)); + in = transcodingService.getTranscodedInputStream(parameters); + audioPlayer = new AudioPlayer(in, this); + audioPlayer.setGain(gain); + audioPlayer.play(); + onSongStart(file); + } + } + + currentPlayingFile = file; + + } catch (Exception x) { + LOG.error("Error in jukebox: " + x, x); + IOUtils.closeQuietly(in); + } + } + + public synchronized void stateChanged(AudioPlayer audioPlayer, AudioPlayer.State state) { + if (state == EOM) { + player.getPlayQueue().next(); + MediaFile result; + synchronized (player.getPlayQueue()) { + result = player.getPlayQueue().getCurrentFile(); + } + play(result, 0); + } + } + + public synchronized float getGain() { + return gain; + } + + public synchronized int getPosition() { + return audioPlayer == null ? 0 : offset + audioPlayer.getPosition(); + } + + /** + * Returns the player which currently uses the jukebox. + * + * @return The player, may be {@code null}. + */ + public Player getPlayer() { + return player; + } + + private void onSongStart(MediaFile file) { + LOG.info(player.getUsername() + " starting jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\""); + status = statusService.createStreamStatus(player); + status.setFile(file.getFile()); + status.addBytesTransfered(file.getFileSize()); + mediaFileService.incrementPlayCount(file); + scrobble(file, false); + } + + private void onSongEnd(MediaFile file) { + LOG.info(player.getUsername() + " stopping jukebox for \"" + FileUtil.getShortPath(file.getFile()) + "\""); + if (status != null) { + statusService.removeStreamStatus(status); + } + scrobble(file, true); + } + + private void scrobble(MediaFile file, boolean submission) { + if (player.getClientId() == null) { // Don't scrobble REST players. + audioScrobblerService.register(file, player.getUsername(), submission); + } + } + + public synchronized void setGain(float gain) { + this.gain = gain; + if (audioPlayer != null) { + audioPlayer.setGain(gain); + } + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setAudioScrobblerService(AudioScrobblerService audioScrobblerService) { + this.audioScrobblerService = audioScrobblerService; + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + 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/service/MediaFileService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java new file mode 100644 index 00000000..bc575714 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaFileService.java @@ -0,0 +1,614 @@ +/* + 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.service; + +import net.sf.ehcache.Ehcache; +import net.sf.ehcache.Element; +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaFileComparator; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; +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.util.FileUtil; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import static net.sourceforge.subsonic.domain.MediaFile.MediaType.*; + +/** + * Provides services for instantiating and caching media files and cover art. + * + * @author Sindre Mehus + */ +public class MediaFileService { + + private static final Logger LOG = Logger.getLogger(MediaFileService.class); + + private Ehcache mediaFileMemoryCache; + + private SecurityService securityService; + private SettingsService settingsService; + private MediaFileDao mediaFileDao; + private AlbumDao albumDao; + private MetaDataParserFactory metaDataParserFactory; + + /** + * Returns a media file instance for the given file. If possible, a cached value is returned. + * + * @param file A file on the local file system. + * @return A media file instance, or null if not found. + * @throws SecurityException If access is denied to the given file. + */ + public MediaFile getMediaFile(File file) { + return getMediaFile(file, settingsService.isFastCacheEnabled()); + } + + /** + * Returns a media file instance for the given file. If possible, a cached value is returned. + * + * @param file A file on the local file system. + * @return A media file instance, or null if not found. + * @throws SecurityException If access is denied to the given file. + */ + public MediaFile getMediaFile(File file, boolean useFastCache) { + + // Look in fast memory cache first. + Element element = mediaFileMemoryCache.get(file); + MediaFile result = element == null ? null : (MediaFile) element.getObjectValue(); + if (result != null) { + return result; + } + + if (!securityService.isReadAllowed(file)) { + throw new SecurityException("Access denied to file " + file); + } + + // Secondly, look in database. + result = mediaFileDao.getMediaFile(file.getPath()); + if (result != null) { + result = checkLastModified(result, useFastCache); + mediaFileMemoryCache.put(new Element(file, result)); + return result; + } + + if (!FileUtil.exists(file)) { + return null; + } + // Not found in database, must read from disk. + result = createMediaFile(file); + + // Put in cache and database. + mediaFileMemoryCache.put(new Element(file, result)); + mediaFileDao.createOrUpdateMediaFile(result); + + return result; + } + + private MediaFile checkLastModified(MediaFile mediaFile, boolean useFastCache) { + if (useFastCache || mediaFile.getChanged().getTime() >= FileUtil.lastModified(mediaFile.getFile())) { + return mediaFile; + } + mediaFile = createMediaFile(mediaFile.getFile()); + mediaFileDao.createOrUpdateMediaFile(mediaFile); + return mediaFile; + } + + /** + * Returns a media file instance for the given path name. If possible, a cached value is returned. + * + * @param pathName A path name for a file on the local file system. + * @return A media file instance. + * @throws SecurityException If access is denied to the given file. + */ + public MediaFile getMediaFile(String pathName) { + return getMediaFile(new File(pathName)); + } + + // TODO: Optimize with memory caching. + public MediaFile getMediaFile(int id) { + MediaFile mediaFile = mediaFileDao.getMediaFile(id); + if (mediaFile == null) { + return null; + } + + if (!securityService.isReadAllowed(mediaFile.getFile())) { + throw new SecurityException("Access denied to file " + mediaFile); + } + + return checkLastModified(mediaFile, settingsService.isFastCacheEnabled()); + } + + public MediaFile getParentOf(MediaFile mediaFile) { + if (mediaFile.getParentPath() == null) { + return null; + } + return getMediaFile(mediaFile.getParentPath()); + } + + public List<MediaFile> getChildrenOf(String parentPath, boolean includeFiles, boolean includeDirectories, boolean sort) { + return getChildrenOf(new File(parentPath), includeFiles, includeDirectories, sort); + } + + public List<MediaFile> getChildrenOf(File parent, boolean includeFiles, boolean includeDirectories, boolean sort) { + return getChildrenOf(getMediaFile(parent), includeFiles, includeDirectories, sort); + } + + /** + * Returns all media files that are children of a given media file. + * + * @param includeFiles Whether files should be included in the result. + * @param includeDirectories Whether directories should be included in the result. + * @param sort Whether to sort files in the same directory. + * @return All children media files. + */ + public List<MediaFile> getChildrenOf(MediaFile parent, boolean includeFiles, boolean includeDirectories, boolean sort) { + return getChildrenOf(parent, includeFiles, includeDirectories, sort, settingsService.isFastCacheEnabled()); + } + + /** + * Returns all media files that are children of a given media file. + * + * @param includeFiles Whether files should be included in the result. + * @param includeDirectories Whether directories should be included in the result. + * @param sort Whether to sort files in the same directory. + * @return All children media files. + */ + public List<MediaFile> getChildrenOf(MediaFile parent, boolean includeFiles, boolean includeDirectories, boolean sort, boolean useFastCache) { + + if (!parent.isDirectory()) { + return Collections.emptyList(); + } + + // Make sure children are stored and up-to-date in the database. + if (!useFastCache) { + updateChildren(parent); + } + + List<MediaFile> result = new ArrayList<MediaFile>(); + for (MediaFile child : mediaFileDao.getChildrenOf(parent.getPath())) { + child = checkLastModified(child, useFastCache); + if (child.isDirectory() && includeDirectories) { + result.add(child); + } + if (child.isFile() && includeFiles) { + result.add(child); + } + } + + if (sort) { + Comparator<MediaFile> comparator = new MediaFileComparator(settingsService.isSortAlbumsByYear()); + // Note: Intentionally not using Collections.sort() since it can be problematic on Java 7. + // http://www.oracle.com/technetwork/java/javase/compatibility-417013.html#jdk7 + Set<MediaFile> set = new TreeSet<MediaFile>(comparator); + set.addAll(result); + result = new ArrayList<MediaFile>(set); + } + + return result; + } + + /** + * Returns whether the given file is the root of a media folder. + * + * @see MusicFolder + */ + public boolean isRoot(MediaFile mediaFile) { + for (MusicFolder musicFolder : settingsService.getAllMusicFolders(false, true)) { + if (mediaFile.getPath().equals(musicFolder.getPath().getPath())) { + return true; + } + } + return false; + } + + /** + * Returns all genres in the music collection. + * + * @return Sorted list of genres. + */ + public List<String> getGenres() { + return mediaFileDao.getGenres(); + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most frequently played albums. + */ + public List<MediaFile> getMostFrequentlyPlayedAlbums(int offset, int count) { + return mediaFileDao.getMostFrequentlyPlayedAlbums(offset, count); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently played albums. + */ + public List<MediaFile> getMostRecentlyPlayedAlbums(int offset, int count) { + return mediaFileDao.getMostRecentlyPlayedAlbums(offset, count); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @return The most recently added albums. + */ + public List<MediaFile> getNewestAlbums(int offset, int count) { + return mediaFileDao.getNewestAlbums(offset, count); + } + + /** + * Returns the most recently starred albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param username Returns albums starred by this user. + * @return The most recently starred albums for this user. + */ + public List<MediaFile> getStarredAlbums(int offset, int count, String username) { + return mediaFileDao.getStarredAlbums(offset, count, username); + } + + /** + * Returns albums in alphabetial order. + * + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param byArtist Whether to sort by artist name + * @return Albums in alphabetical order. + */ + public List<MediaFile> getAlphabetialAlbums(int offset, int count, boolean byArtist) { + return mediaFileDao.getAlphabetialAlbums(offset, count, byArtist); + } + + public Date getMediaFileStarredDate(int id, String username) { + return mediaFileDao.getMediaFileStarredDate(id, username); + } + + public void populateStarredDate(List<MediaFile> mediaFiles, String username) { + for (MediaFile mediaFile : mediaFiles) { + populateStarredDate(mediaFile, username); + } + } + + public void populateStarredDate(MediaFile mediaFile, String username) { + Date starredDate = mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username); + mediaFile.setStarredDate(starredDate); + } + + private void updateChildren(MediaFile parent) { + + // Check timestamps. + if (parent.getChildrenLastUpdated().getTime() >= parent.getChanged().getTime()) { + return; + } + + List<MediaFile> storedChildren = mediaFileDao.getChildrenOf(parent.getPath()); + Map<String, MediaFile> storedChildrenMap = new HashMap<String, MediaFile>(); + for (MediaFile child : storedChildren) { + storedChildrenMap.put(child.getPath(), child); + } + + List<File> children = filterMediaFiles(FileUtil.listFiles(parent.getFile())); + for (File child : children) { + if (storedChildrenMap.remove(child.getPath()) == null) { + // Add children that are not already stored. + mediaFileDao.createOrUpdateMediaFile(createMediaFile(child)); + } + } + + // Delete children that no longer exist on disk. + for (String path : storedChildrenMap.keySet()) { + mediaFileDao.deleteMediaFile(path); + } + + // Update timestamp in parent. + parent.setChildrenLastUpdated(parent.getChanged()); + parent.setPresent(true); + mediaFileDao.createOrUpdateMediaFile(parent); + } + + private List<File> filterMediaFiles(File[] candidates) { + List<File> result = new ArrayList<File>(); + for (File candidate : candidates) { + String suffix = FilenameUtils.getExtension(candidate.getName()).toLowerCase(); + if (!isExcluded(candidate) && (FileUtil.isDirectory(candidate) || isAudioFile(suffix) || isVideoFile(suffix))) { + result.add(candidate); + } + } + return result; + } + + private boolean isAudioFile(String suffix) { + for (String s : settingsService.getMusicFileTypesAsArray()) { + if (suffix.equals(s.toLowerCase())) { + return true; + } + } + return false; + } + + private boolean isVideoFile(String suffix) { + for (String s : settingsService.getVideoFileTypesAsArray()) { + if (suffix.equals(s.toLowerCase())) { + return true; + } + } + return false; + } + + /** + * Returns whether the given file is excluded. + * + * @param file The child file in question. + * @return Whether the child file is excluded. + */ + private boolean isExcluded(File file) { + + // Exclude all hidden files starting with a "." or "@eaDir" (thumbnail dir created on Synology devices). + String name = file.getName(); + return name.startsWith(".") || name.startsWith("@eaDir") || name.equals("Thumbs.db"); + } + + private MediaFile createMediaFile(File file) { + MediaFile mediaFile = new MediaFile(); + Date lastModified = new Date(FileUtil.lastModified(file)); + mediaFile.setPath(file.getPath()); + mediaFile.setFolder(securityService.getRootFolderForFile(file)); + mediaFile.setParentPath(file.getParent()); + mediaFile.setChanged(lastModified); + mediaFile.setLastScanned(new Date()); + mediaFile.setPlayCount(0); + mediaFile.setChildrenLastUpdated(new Date(0)); + mediaFile.setCreated(lastModified); + mediaFile.setMediaType(DIRECTORY); + mediaFile.setPresent(true); + + if (file.isFile()) { + + MetaDataParser parser = metaDataParserFactory.getParser(file); + if (parser != null) { + MetaData metaData = parser.getMetaData(file); + mediaFile.setArtist(metaData.getArtist()); + mediaFile.setAlbumArtist(metaData.getArtist()); + mediaFile.setAlbumName(metaData.getAlbumName()); + mediaFile.setTitle(metaData.getTitle()); + mediaFile.setDiscNumber(metaData.getDiscNumber()); + mediaFile.setTrackNumber(metaData.getTrackNumber()); + mediaFile.setGenre(metaData.getGenre()); + mediaFile.setYear(metaData.getYear()); + mediaFile.setDurationSeconds(metaData.getDurationSeconds()); + mediaFile.setBitRate(metaData.getBitRate()); + mediaFile.setVariableBitRate(metaData.getVariableBitRate()); + mediaFile.setHeight(metaData.getHeight()); + mediaFile.setWidth(metaData.getWidth()); + } + String format = StringUtils.trimToNull(StringUtils.lowerCase(FilenameUtils.getExtension(mediaFile.getPath()))); + mediaFile.setFormat(format); + mediaFile.setFileSize(FileUtil.length(file)); + mediaFile.setMediaType(getMediaType(mediaFile)); + + } else { + + // Is this an album? + if (!isRoot(mediaFile)) { + File[] children = FileUtil.listFiles(file); + File firstChild = null; + for (File child : filterMediaFiles(children)) { + if (FileUtil.isFile(child)) { + firstChild = child; + break; + } + } + + if (firstChild != null) { + mediaFile.setMediaType(ALBUM); + + // Guess artist/album name and year. + MetaDataParser parser = metaDataParserFactory.getParser(firstChild); + if (parser != null) { + MetaData metaData = parser.getMetaData(firstChild); + mediaFile.setArtist(metaData.getArtist()); + mediaFile.setAlbumName(metaData.getAlbumName()); + mediaFile.setYear(metaData.getYear()); + } + + // Look for cover art. + try { + File coverArt = findCoverArt(children); + if (coverArt != null) { + mediaFile.setCoverArtPath(coverArt.getPath()); + } + } catch (IOException x) { + LOG.error("Failed to find cover art.", x); + } + + } else { + mediaFile.setArtist(file.getName()); + } + } + } + + return mediaFile; + } + + private MediaFile.MediaType getMediaType(MediaFile mediaFile) { + if (isVideoFile(mediaFile.getFormat())) { + return VIDEO; + } + String path = mediaFile.getPath().toLowerCase(); + String genre = StringUtils.trimToEmpty(mediaFile.getGenre()).toLowerCase(); + if (path.contains("podcast") || genre.contains("podcast")) { + return PODCAST; + } + if (path.contains("audiobook") || genre.contains("audiobook") || path.contains("audio book") || genre.contains("audio book")) { + return AUDIOBOOK; + } + return MUSIC; + } + + public void refreshMediaFile(MediaFile mediaFile) { + mediaFile = createMediaFile(mediaFile.getFile()); + mediaFileDao.createOrUpdateMediaFile(mediaFile); + mediaFileMemoryCache.remove(mediaFile.getFile()); + } + + /** + * Returns a cover art image for the given media file. + */ + public File getCoverArt(MediaFile mediaFile) { + if (mediaFile.getCoverArtFile() != null) { + return mediaFile.getCoverArtFile(); + } + MediaFile parent = getParentOf(mediaFile); + return parent == null ? null : parent.getCoverArtFile(); + } + + /** + * Finds a cover art image for the given directory, by looking for it on the disk. + */ + private File findCoverArt(File[] candidates) throws IOException { + for (String mask : settingsService.getCoverArtFileTypesAsArray()) { + for (File candidate : candidates) { + if (candidate.isFile() && candidate.getName().toUpperCase().endsWith(mask.toUpperCase()) && !candidate.getName().startsWith(".")) { + return candidate; + } + } + } + + // Look for embedded images in audiofiles. (Only check first audio file encountered). + JaudiotaggerParser parser = new JaudiotaggerParser(); + for (File candidate : candidates) { + if (parser.isApplicable(candidate)) { + if (parser.isImageAvailable(getMediaFile(candidate))) { + return candidate; + } else { + return null; + } + } + } + return null; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileMemoryCache(Ehcache mediaFileMemoryCache) { + this.mediaFileMemoryCache = mediaFileMemoryCache; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + /** + * Returns all media files that are children, grand-children etc of a given media file. + * Directories are not included in the result. + * + * @param sort Whether to sort files in the same directory. + * @return All descendant music files. + */ + public List<MediaFile> getDescendantsOf(MediaFile ancestor, boolean sort) { + + if (ancestor.isFile()) { + return Arrays.asList(ancestor); + } + + List<MediaFile> result = new ArrayList<MediaFile>(); + + for (MediaFile child : getChildrenOf(ancestor, true, true, sort)) { + if (child.isDirectory()) { + result.addAll(getDescendantsOf(child, sort)); + } else { + result.add(child); + } + } + return result; + } + + public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { + this.metaDataParserFactory = metaDataParserFactory; + } + + public void updateMediaFile(MediaFile mediaFile) { + mediaFileDao.createOrUpdateMediaFile(mediaFile); + } + + /** + * Increments the play count and last played date for the given media file and its + * directory and album. + */ + public void incrementPlayCount(MediaFile file) { + Date now = new Date(); + file.setLastPlayed(now); + file.setPlayCount(file.getPlayCount() + 1); + updateMediaFile(file); + + MediaFile parent = getParentOf(file); + if (!isRoot(parent)) { + parent.setLastPlayed(now); + parent.setPlayCount(parent.getPlayCount() + 1); + updateMediaFile(parent); + } + + Album album = albumDao.getAlbum(file.getAlbumArtist(), file.getAlbumName()); + if (album != null) { + album.setLastPlayed(now); + album.setPlayCount(album.getPlayCount() + 1); + albumDao.createOrUpdateAlbum(album); + } + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java new file mode 100644 index 00000000..84f2d31c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java @@ -0,0 +1,354 @@ +/* + 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.service; + +import java.io.File; +import java.util.Calendar; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + +import net.sourceforge.subsonic.Logger; +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.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.commons.lang.ObjectUtils; + +/** + * Provides services for scanning the music library. + * + * @author Sindre Mehus + */ +public class MediaScannerService { + + private static final int INDEX_VERSION = 15; + private static final Logger LOG = Logger.getLogger(MediaScannerService.class); + + private MediaLibraryStatistics statistics; + + private boolean scanning; + private Timer timer; + private SettingsService settingsService; + private SearchService searchService; + private MediaFileService mediaFileService; + private MediaFileDao mediaFileDao; + private ArtistDao artistDao; + private AlbumDao albumDao; + private int scanCount; + + public void init() { + deleteOldIndexFiles(); + statistics = mediaFileDao.getStatistics(); + schedule(); + } + + /** + * Schedule background execution of media library scanning. + */ + public synchronized void schedule() { + if (timer != null) { + timer.cancel(); + } + timer = new Timer(true); + + TimerTask task = new TimerTask() { + @Override + public void run() { + scanLibrary(); + } + }; + + long daysBetween = settingsService.getIndexCreationInterval(); + int hour = settingsService.getIndexCreationHour(); + + if (daysBetween == -1) { + LOG.info("Automatic media scanning disabled."); + return; + } + + Date now = new Date(); + Calendar cal = Calendar.getInstance(); + cal.setTime(now); + cal.set(Calendar.HOUR_OF_DAY, hour); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + + if (cal.getTime().before(now)) { + cal.add(Calendar.DATE, 1); + } + + Date firstTime = cal.getTime(); + long period = daysBetween * 24L * 3600L * 1000L; + timer.schedule(task, firstTime, period); + + LOG.info("Automatic media library scanning scheduled to run every " + daysBetween + " day(s), starting at " + firstTime); + + // In addition, create index immediately if it doesn't exist on disk. + if (settingsService.getLastScanned() == null) { + LOG.info("Media library never scanned. Doing it now."); + scanLibrary(); + } + } + + /** + * Returns whether the media library is currently being scanned. + */ + public synchronized boolean isScanning() { + return scanning; + } + + /** + * Returns the number of files scanned so far. + */ + public int getScanCount() { + return scanCount; + } + + /** + * Scans the media library. + * The scanning is done asynchronously, i.e., this method returns immediately. + */ + public synchronized void scanLibrary() { + if (isScanning()) { + return; + } + scanning = true; + + Thread thread = new Thread("MediaLibraryScanner") { + @Override + public void run() { + doScanLibrary(); + } + }; + + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + } + + private void doScanLibrary() { + LOG.info("Starting to scan media library."); + + try { + Date lastScanned = new Date(); + Map<String, Integer> albumCount = new HashMap<String, Integer>(); + scanCount = 0; + + searchService.startIndexing(); + + // Recurse through all files on disk. + for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { + MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false); + scanFile(root, musicFolder, lastScanned, albumCount); + } + mediaFileDao.markNonPresent(lastScanned); + artistDao.markNonPresent(lastScanned); + albumDao.markNonPresent(lastScanned); + + // Update statistics + statistics = mediaFileDao.getStatistics(); + + settingsService.setLastScanned(lastScanned); + settingsService.save(false); + LOG.info("Scanned media library with " + scanCount + " entries."); + + } catch (Throwable x) { + LOG.error("Failed to scan media library.", x); + } finally { + scanning = false; + searchService.stopIndexing(); + } + } + + private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, Map<String, Integer> albumCount) { + scanCount++; + if (scanCount % 250 == 0) { + LOG.info("Scanned media library with " + scanCount + " entries."); + } + + searchService.index(file); + + // Update the root folder if it has changed. + if (!musicFolder.getPath().getPath().equals(file.getFolder())) { + file.setFolder(musicFolder.getPath().getPath()); + mediaFileDao.createOrUpdateMediaFile(file); + } + + if (file.isDirectory()) { + for (MediaFile child : mediaFileService.getChildrenOf(file, true, false, false, false)) { + scanFile(child, musicFolder, lastScanned, albumCount); + } + for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) { + scanFile(child, musicFolder, lastScanned, albumCount); + } + } else { + updateAlbum(file, lastScanned, albumCount); + updateArtist(file, lastScanned, albumCount); + } + + mediaFileDao.markPresent(file.getPath(), lastScanned); + artistDao.markPresent(file.getArtist(), lastScanned); + } + + private void updateAlbum(MediaFile file, Date lastScanned, Map<String, Integer> albumCount) { + if (file.getAlbumName() == null || file.getArtist() == null || file.getParentPath() == null || !file.isAudio()) { + return; + } + + Album album = albumDao.getAlbumForFile(file); + if (album == null) { + album = new Album(); + album.setPath(file.getParentPath()); + album.setName(file.getAlbumName()); + album.setArtist(file.getArtist()); + album.setCreated(file.getChanged()); + } + if (album.getCoverArtPath() == null) { + MediaFile parent = mediaFileService.getParentOf(file); + if (parent != null) { + album.setCoverArtPath(parent.getCoverArtPath()); + } + } + boolean firstEncounter = !lastScanned.equals(album.getLastScanned()); + if (firstEncounter) { + album.setDurationSeconds(0); + album.setSongCount(0); + Integer n = albumCount.get(file.getArtist()); + albumCount.put(file.getArtist(), n == null ? 1 : n + 1); + } + if (file.getDurationSeconds() != null) { + album.setDurationSeconds(album.getDurationSeconds() + file.getDurationSeconds()); + } + if (file.isAudio()) { + album.setSongCount(album.getSongCount() + 1); + } + + album.setLastScanned(lastScanned); + album.setPresent(true); + albumDao.createOrUpdateAlbum(album); + if (firstEncounter) { + searchService.index(album); + } + + // Update the file's album artist, if necessary. + if (!ObjectUtils.equals(album.getArtist(), file.getAlbumArtist())) { + file.setAlbumArtist(album.getArtist()); + mediaFileDao.createOrUpdateMediaFile(file); + } + } + + private void updateArtist(MediaFile file, Date lastScanned, Map<String, Integer> albumCount) { + if (file.getArtist() == null || !file.isAudio()) { + return; + } + + Artist artist = artistDao.getArtist(file.getArtist()); + if (artist == null) { + artist = new Artist(); + artist.setName(file.getArtist()); + } + if (artist.getCoverArtPath() == null) { + MediaFile parent = mediaFileService.getParentOf(file); + if (parent != null) { + artist.setCoverArtPath(parent.getCoverArtPath()); + } + } + boolean firstEncounter = !lastScanned.equals(artist.getLastScanned()); + + Integer n = albumCount.get(artist.getName()); + artist.setAlbumCount(n == null ? 0 : n); + + artist.setLastScanned(lastScanned); + artist.setPresent(true); + artistDao.createOrUpdateArtist(artist); + + if (firstEncounter) { + searchService.index(artist); + } + } + + /** + * Returns media library statistics, including the number of artists, albums and songs. + * + * @return Media library statistics. + */ + public MediaLibraryStatistics getStatistics() { + return statistics; + } + + /** + * Deletes old versions of the index file. + */ + private void deleteOldIndexFiles() { + for (int i = 2; i < INDEX_VERSION; i++) { + File file = getIndexFile(i); + try { + if (FileUtil.exists(file)) { + if (file.delete()) { + LOG.info("Deleted old index file: " + file.getPath()); + } + } + } catch (Exception x) { + LOG.warn("Failed to delete old index file: " + file.getPath(), x); + } + } + } + + /** + * Returns the index file for the given index version. + * + * @param version The index version. + * @return The index file for the given index version. + */ + private File getIndexFile(int version) { + File home = SettingsService.getSubsonicHome(); + return new File(home, "subsonic" + version + ".index"); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + 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/service/MusicIndexService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java new file mode 100644 index 00000000..b6ee682e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java @@ -0,0 +1,250 @@ +/* + 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.service; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.SortedSet; +import java.util.StringTokenizer; +import java.util.TreeMap; +import java.util.TreeSet; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.MusicIndex.Artist; + +/** + * Provides services for grouping artists by index. + * + * @author Sindre Mehus + */ +public class MusicIndexService { + + private SettingsService settingsService; + private MediaFileService mediaFileService; + private MediaFileDao mediaFileDao; + + /** + * Returns a map from music indexes to sets of artists that are direct children of the given music folders. + * + * @param folders The music folders. + * @return A map from music indexes to sets of artists that are direct children of this music file. + * @throws IOException If an I/O error occurs. + */ + public SortedMap<MusicIndex, SortedSet<Artist>> getIndexedArtists(List<MusicFolder> folders) throws IOException { + + String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); + String[] shortcuts = settingsService.getShortcutsAsArray(); + final List<MusicIndex> indexes = createIndexesFromExpression(settingsService.getIndexString()); + + Comparator<MusicIndex> indexComparator = new MusicIndexComparator(indexes); + SortedSet<Artist> artists = createArtists(folders, ignoredArticles, shortcuts); + SortedMap<MusicIndex, SortedSet<Artist>> result = new TreeMap<MusicIndex, SortedSet<Artist>>(indexComparator); + + for (Artist artist : artists) { + MusicIndex index = getIndex(artist, indexes); + SortedSet<Artist> artistSet = result.get(index); + if (artistSet == null) { + artistSet = new TreeSet<Artist>(); + result.put(index, artistSet); + } + artistSet.add(artist); + } + + return result; + } + + /** + * Creates a new instance by parsing the given expression. The expression consists of an index name, followed by + * an optional list of one-character prefixes. For example:<p/> + * <p/> + * The expression <em>"A"</em> will create the index <em>"A" -> ["A"]</em><br/> + * The expression <em>"The"</em> will create the index <em>"The" -> ["The"]</em><br/> + * The expression <em>"A(AÅÆ)"</em> will create the index <em>"A" -> ["A", "Å", "Æ"]</em><br/> + * The expression <em>"X-Z(XYZ)"</em> will create the index <em>"X-Z" -> ["X", "Y", "Z"]</em> + * + * @param expr The expression to parse. + * @return A new instance. + */ + protected MusicIndex createIndexFromExpression(String expr) { + int separatorIndex = expr.indexOf('('); + if (separatorIndex == -1) { + + MusicIndex index = new MusicIndex(expr); + index.addPrefix(expr); + return index; + } + + MusicIndex index = new MusicIndex(expr.substring(0, separatorIndex)); + String prefixString = expr.substring(separatorIndex + 1, expr.length() - 1); + for (int i = 0; i < prefixString.length(); i++) { + index.addPrefix(prefixString.substring(i, i + 1)); + } + return index; + } + + /** + * Creates a list of music indexes by parsing the given expression. The expression is a space-separated list of + * sub-expressions, for which the rules described in {@link #createIndexFromExpression} apply. + * + * @param expr The expression to parse. + * @return A list of music indexes. + */ + protected List<MusicIndex> createIndexesFromExpression(String expr) { + List<MusicIndex> result = new ArrayList<MusicIndex>(); + + StringTokenizer tokenizer = new StringTokenizer(expr, " "); + while (tokenizer.hasMoreTokens()) { + MusicIndex index = createIndexFromExpression(tokenizer.nextToken()); + result.add(index); + } + + return result; + } + + private SortedSet<Artist> createArtists(List<MusicFolder> folders, String[] ignoredArticles, String[] shortcuts) throws IOException { + return settingsService.isOrganizeByFolderStructure() ? + createArtistsByFolderStructure(folders, ignoredArticles, shortcuts) : + createArtistsByTagStructure(folders, ignoredArticles, shortcuts); + } + + private SortedSet<Artist> createArtistsByFolderStructure(List<MusicFolder> folders, String[] ignoredArticles, String[] shortcuts) { + SortedMap<String, Artist> artistMap = new TreeMap<String, Artist>(); + Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts)); + + for (MusicFolder folder : folders) { + + MediaFile root = mediaFileService.getMediaFile(folder.getPath(), true); + List<MediaFile> children = mediaFileService.getChildrenOf(root, false, true, true, true); + for (MediaFile child : children) { + if (shortcutSet.contains(child.getName())) { + continue; + } + + String sortableName = createSortableName(child.getName(), ignoredArticles); + Artist artist = artistMap.get(sortableName); + if (artist == null) { + artist = new Artist(child.getName(), sortableName); + artistMap.put(sortableName, artist); + } + artist.addMediaFile(child); + } + } + + return new TreeSet<Artist>(artistMap.values()); + } + + private SortedSet<Artist> createArtistsByTagStructure(List<MusicFolder> folders, String[] ignoredArticles, String[] shortcuts) { + Set<String> shortcutSet = new HashSet<String>(Arrays.asList(shortcuts)); + SortedSet<Artist> artists = new TreeSet<Artist>(); + + // TODO: Filter by folder + for (String artistName : mediaFileDao.getArtists()) { + + if (shortcutSet.contains(artistName)) { + continue; + } + + String sortableName = createSortableName(artistName, ignoredArticles); + Artist artist = new Artist(artistName, sortableName); + artists.add(artist); + } + + return artists; + } + + private String createSortableName(String name, String[] ignoredArticles) { + String uppercaseName = name.toUpperCase(); + for (String article : ignoredArticles) { + if (uppercaseName.startsWith(article.toUpperCase() + " ")) { + return name.substring(article.length() + 1) + ", " + article; + } + } + return name; + } + + /** + * Returns the music index to which the given artist belongs. + * + * @param artist The artist in question. + * @param indexes List of available indexes. + * @return The music index to which this music file belongs, or {@link MusicIndex#OTHER} if no index applies. + */ + private MusicIndex getIndex(Artist artist, List<MusicIndex> indexes) { + String sortableName = artist.getSortableName().toUpperCase(); + for (MusicIndex index : indexes) { + for (String prefix : index.getPrefixes()) { + if (sortableName.startsWith(prefix.toUpperCase())) { + return index; + } + } + } + return MusicIndex.OTHER; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + private static class MusicIndexComparator implements Comparator<MusicIndex>, Serializable { + + private List<MusicIndex> indexes; + + public MusicIndexComparator(List<MusicIndex> indexes) { + this.indexes = indexes; + } + + public int compare(MusicIndex a, MusicIndex b) { + int indexA = indexes.indexOf(a); + int indexB = indexes.indexOf(b); + + if (indexA == -1) { + indexA = Integer.MAX_VALUE; + } + if (indexB == -1) { + indexB = Integer.MAX_VALUE; + } + + if (indexA < indexB) { + return -1; + } + if (indexA > indexB) { + return 1; + } + return 0; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java new file mode 100644 index 00000000..b54026a0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java @@ -0,0 +1,336 @@ +/* + 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.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.util.EntityUtils; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.NATPMPRouter; +import net.sourceforge.subsonic.domain.Router; +import net.sourceforge.subsonic.domain.SBBIRouter; +import net.sourceforge.subsonic.domain.WeUPnPRouter; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +/** + * Provides network-related services, including port forwarding on UPnP routers and + * URL redirection from http://xxxx.subsonic.org. + * + * @author Sindre Mehus + */ +public class NetworkService { + + private static final Logger LOG = Logger.getLogger(NetworkService.class); + private static final long PORT_FORWARDING_DELAY = 3600L; + private static final long URL_REDIRECTION_DELAY = 2 * 3600L; + + private static final String URL_REDIRECTION_REGISTER_URL = getBackendUrl() + "/backend/redirect/register.view"; + private static final String URL_REDIRECTION_UNREGISTER_URL = getBackendUrl() + "/backend/redirect/unregister.view"; + private static final String URL_REDIRECTION_TEST_URL = getBackendUrl() + "/backend/redirect/test.view"; + + private SettingsService settingsService; + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(4); + private final PortForwardingTask portForwardingTask = new PortForwardingTask(); + private final URLRedirectionTask urlRedirectionTask = new URLRedirectionTask(); + private Future<?> portForwardingFuture; + private Future<?> urlRedirectionFuture; + + private final Status portForwardingStatus = new Status(); + private final Status urlRedirectionStatus = new Status(); + private boolean testUrlRedirection; + + public void init() { + initPortForwarding(); + initUrlRedirection(false); + } + + /** + * Configures UPnP port forwarding. + */ + public synchronized void initPortForwarding() { + portForwardingStatus.setText("Idle"); + if (portForwardingFuture != null) { + portForwardingFuture.cancel(true); + } + portForwardingFuture = executor.scheduleWithFixedDelay(portForwardingTask, 0L, PORT_FORWARDING_DELAY, TimeUnit.SECONDS); + } + + /** + * Configures URL redirection. + * + * @param test Whether to test that the redirection works. + */ + public synchronized void initUrlRedirection(boolean test) { + urlRedirectionStatus.setText("Idle"); + if (urlRedirectionFuture != null) { + urlRedirectionFuture.cancel(true); + } + testUrlRedirection = test; + urlRedirectionFuture = executor.scheduleWithFixedDelay(urlRedirectionTask, 0L, URL_REDIRECTION_DELAY, TimeUnit.SECONDS); + } + + public Status getPortForwardingStatus() { + return portForwardingStatus; + } + + public Status getURLRedirecionStatus() { + return urlRedirectionStatus; + } + + public static String getBackendUrl() { + return "true".equals(System.getProperty("subsonic.test")) ? "http://localhost:8181" : "http://subsonic.org"; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + private class PortForwardingTask extends Task { + + @Override + protected void execute() { + + boolean enabled = settingsService.isPortForwardingEnabled(); + portForwardingStatus.setText("Looking for router..."); + Router router = findRouter(); + if (router == null) { + LOG.warn("No UPnP router found."); + portForwardingStatus.setText("No router found."); + } else { + + portForwardingStatus.setText("Router found."); + + int port = settingsService.getPort(); + int httpsPort = settingsService.getHttpsPort(); + + // Create new NAT entry. + if (enabled) { + try { + router.addPortMapping(port, port, 0); + String message = "Successfully forwarding port " + port; + + if (httpsPort != 0 && httpsPort != port) { + router.addPortMapping(httpsPort, httpsPort, 0); + message += " and port " + httpsPort; + } + message += "."; + + LOG.info(message); + portForwardingStatus.setText(message); + } catch (Throwable x) { + String message = "Failed to create port forwarding."; + LOG.warn(message, x); + portForwardingStatus.setText(message + " See log for details."); + } + } + + // Delete NAT entry. + else { + try { + router.deletePortMapping(port, port); + LOG.info("Deleted port mapping for port " + port); + if (httpsPort != 0 && httpsPort != port) { + router.deletePortMapping(httpsPort, httpsPort); + LOG.info("Deleted port mapping for port " + httpsPort); + } + } catch (Throwable x) { + LOG.warn("Failed to delete port mapping.", x); + } + portForwardingStatus.setText("Port forwarding disabled."); + } + } + + // Don't do it again if disabled. + if (!enabled && portForwardingFuture != null) { + portForwardingFuture.cancel(false); + } + } + + private Router findRouter() { + try { + Router router = SBBIRouter.findRouter(); + if (router != null) { + return router; + } + } catch (Throwable x) { + LOG.warn("Failed to find UPnP router using SBBI library.", x); + } + + try { + Router router = WeUPnPRouter.findRouter(); + if (router != null) { + return router; + } + } catch (Throwable x) { + LOG.warn("Failed to find UPnP router using WeUPnP library.", x); + } + + try { + Router router = NATPMPRouter.findRouter(); + if (router != null) { + return router; + } + } catch (Throwable x) { + LOG.warn("Failed to find NAT-PMP router.", x); + } + + return null; + } + } + + private class URLRedirectionTask extends Task { + + @Override + protected void execute() { + + boolean enable = settingsService.isUrlRedirectionEnabled(); + HttpPost request = new HttpPost(enable ? URL_REDIRECTION_REGISTER_URL : URL_REDIRECTION_UNREGISTER_URL); + + int port = settingsService.getPort(); + boolean trial = !settingsService.isLicenseValid(); + Date trialExpires = settingsService.getUrlRedirectTrialExpires(); + + List<NameValuePair> params = new ArrayList<NameValuePair>(); + params.add(new BasicNameValuePair("serverId", settingsService.getServerId())); + params.add(new BasicNameValuePair("redirectFrom", settingsService.getUrlRedirectFrom())); + params.add(new BasicNameValuePair("port", String.valueOf(port))); + params.add(new BasicNameValuePair("localIp", Util.getLocalIpAddress())); + params.add(new BasicNameValuePair("localPort", String.valueOf(port))); + params.add(new BasicNameValuePair("contextPath", settingsService.getUrlRedirectContextPath())); + params.add(new BasicNameValuePair("trial", String.valueOf(trial))); + if (trial && trialExpires != null) { + params.add(new BasicNameValuePair("trialExpires", String.valueOf(trialExpires.getTime()))); + } else { + params.add(new BasicNameValuePair("licenseHolder", settingsService.getLicenseEmail())); + } + + HttpClient client = new DefaultHttpClient(); + + try { + urlRedirectionStatus.setText(enable ? "Registering web address..." : "Unregistering web address..."); + request.setEntity(new UrlEncodedFormEntity(params, StringUtil.ENCODING_UTF8)); + + HttpResponse response = client.execute(request); + StatusLine status = response.getStatusLine(); + + switch (status.getStatusCode()) { + case HttpStatus.SC_BAD_REQUEST: + urlRedirectionStatus.setText(EntityUtils.toString(response.getEntity())); + break; + case HttpStatus.SC_OK: + urlRedirectionStatus.setText(enable ? "Successfully registered web address." : "Web address disabled."); + break; + default: + throw new IOException(status.getStatusCode() + " " + status.getReasonPhrase()); + } + + } catch (Throwable x) { + LOG.warn(enable ? "Failed to register web address." : "Failed to unregister web address.", x); + urlRedirectionStatus.setText(enable ? ("Failed to register web address. " + x.getMessage() + + " (" + x.getClass().getSimpleName() + ")") : "Web address disabled."); + } finally { + client.getConnectionManager().shutdown(); + } + + // Test redirection, but only once. + if (testUrlRedirection) { + testUrlRedirection = false; + testUrlRedirection(); + } + + // Don't do it again if disabled. + if (!enable && urlRedirectionFuture != null) { + urlRedirectionFuture.cancel(false); + } + } + + private void testUrlRedirection() { + + HttpGet request = new HttpGet(URL_REDIRECTION_TEST_URL + "?redirectFrom=" + settingsService.getUrlRedirectFrom()); + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000); + HttpConnectionParams.setSoTimeout(client.getParams(), 30000); + + try { + urlRedirectionStatus.setText("Testing web address " + settingsService.getUrlRedirectFrom() + ".subsonic.org. Please wait..."); + String response = client.execute(request, new BasicResponseHandler()); + urlRedirectionStatus.setText(response); + + } catch (Throwable x) { + LOG.warn("Failed to test web address.", x); + urlRedirectionStatus.setText("Failed to test web address. " + x.getMessage() + " (" + x.getClass().getSimpleName() + ")"); + } finally { + client.getConnectionManager().shutdown(); + } + } + } + + private abstract class Task implements Runnable { + public void run() { + String name = getClass().getSimpleName(); + try { + execute(); + } catch (Throwable x) { + LOG.error("Error executing " + name + ": " + x.getMessage(), x); + } + } + + protected abstract void execute(); + } + + public static class Status { + + private String text; + private Date date; + + public void setText(String text) { + this.text = text; + date = new Date(); + } + + public String getText() { + return text; + } + + public Date getDate() { + return date; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java new file mode 100644 index 00000000..0f24b2b8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java @@ -0,0 +1,317 @@ +/* + 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.service; + +import net.sourceforge.subsonic.dao.PlayerDao; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.lang.StringUtils; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * Provides services for maintaining the set of players. + * + * @author Sindre Mehus + * @see Player + */ +public class PlayerService { + + private static final String COOKIE_NAME = "player"; + private static final int COOKIE_EXPIRY = 365 * 24 * 3600; // One year + + private PlayerDao playerDao; + private StatusService statusService; + private SecurityService securityService; + private TranscodingService transcodingService; + + public void init() { + playerDao.deleteOldPlayers(60); + } + + /** + * Equivalent to <code>getPlayer(request, response, true)</code> . + */ + public Player getPlayer(HttpServletRequest request, HttpServletResponse response) { + return getPlayer(request, response, true, false); + } + + /** + * Returns the player associated with the given HTTP request. If no such player exists, a new + * one is created. + * + * @param request The HTTP request. + * @param response The HTTP response. + * @param remoteControlEnabled Whether this method should return a remote-controlled player. + * @param isStreamRequest Whether the HTTP request is a request for streaming data. + * @return The player associated with the given HTTP request. + */ + public synchronized Player getPlayer(HttpServletRequest request, HttpServletResponse response, + boolean remoteControlEnabled, boolean isStreamRequest) { + + // Find by 'player' request parameter. + Player player = getPlayerById(request.getParameter("player")); + + // Find in session context. + if (player == null && remoteControlEnabled) { + String playerId = (String) request.getSession().getAttribute("player"); + if (playerId != null) { + player = getPlayerById(playerId); + } + } + + // Find by cookie. + String username = securityService.getCurrentUsername(request); + if (player == null && remoteControlEnabled) { + player = getPlayerById(getPlayerIdFromCookie(request, username)); + } + + // Make sure we're not hijacking the player of another user. + if (player != null && player.getUsername() != null && username != null && !player.getUsername().equals(username)) { + player = null; + } + + // Look for player with same IP address and user name. + if (player == null) { + player = getPlayerByIpAddressAndUsername(request.getRemoteAddr(), username); + + // Don't use this player if it's used by REST API. + if (player != null && player.getClientId() != null) { + player = null; + } + } + + // If no player was found, create it. + if (player == null) { + player = new Player(); + createPlayer(player); +// LOG.debug("Created player " + player.getId() + " (remoteControlEnabled: " + remoteControlEnabled + +// ", isStreamRequest: " + isStreamRequest + ", username: " + username + +// ", ip: " + request.getRemoteAddr() + ")."); + } + + // Update player data. + boolean isUpdate = false; + if (username != null && player.getUsername() == null) { + player.setUsername(username); + isUpdate = true; + } + if (player.getIpAddress() == null || isStreamRequest || + (!isPlayerConnected(player) && player.isDynamicIp() && !request.getRemoteAddr().equals(player.getIpAddress()))) { + player.setIpAddress(request.getRemoteAddr()); + isUpdate = true; + } + String userAgent = request.getHeader("user-agent"); + if (isStreamRequest) { + player.setType(userAgent); + player.setLastSeen(new Date()); + isUpdate = true; + } + + if (isUpdate) { + updatePlayer(player); + } + + // Set cookie in response. + if (response != null) { + String cookieName = COOKIE_NAME + "-" + StringUtil.utf8HexEncode(username); + Cookie cookie = new Cookie(cookieName, player.getId()); + cookie.setMaxAge(COOKIE_EXPIRY); + String path = request.getContextPath(); + if (StringUtils.isEmpty(path)) { + path = "/"; + } + cookie.setPath(path); + response.addCookie(cookie); + } + + // Save player in session context. + if (remoteControlEnabled) { + request.getSession().setAttribute("player", player.getId()); + } + + return player; + } + + /** + * Updates the given player. + * + * @param player The player to update. + */ + public void updatePlayer(Player player) { + playerDao.updatePlayer(player); + } + + /** + * Returns the player with the given ID. + * + * @param id The unique player ID. + * @return The player with the given ID, or <code>null</code> if no such player exists. + */ + public Player getPlayerById(String id) { + return playerDao.getPlayerById(id); + } + + /** + * Returns whether the given player is connected. + * + * @param player The player in question. + * @return Whether the player is connected. + */ + private boolean isPlayerConnected(Player player) { + for (TransferStatus status : statusService.getStreamStatusesForPlayer(player)) { + if (status.isActive()) { + return true; + } + } + return false; + } + + /** + * Returns the player with the given IP address and username. If no username is given, only IP address is + * used as search criteria. + * + * @param ipAddress The IP address. + * @param username The remote user. + * @return The player with the given IP address, or <code>null</code> if no such player exists. + */ + private Player getPlayerByIpAddressAndUsername(final String ipAddress, final String username) { + if (ipAddress == null) { + return null; + } + for (Player player : getAllPlayers()) { + boolean ipMatches = ipAddress.equals(player.getIpAddress()); + boolean userMatches = username == null || username.equals(player.getUsername()); + if (ipMatches && userMatches) { + return player; + } + } + return null; + } + + /** + * Reads the player ID from the cookie in the HTTP request. + * + * @param request The HTTP request. + * @param username The name of the current user. + * @return The player ID embedded in the cookie, or <code>null</code> if cookie is not present. + */ + private String getPlayerIdFromCookie(HttpServletRequest request, String username) { + Cookie[] cookies = request.getCookies(); + if (cookies == null) { + return null; + } + String cookieName = COOKIE_NAME + "-" + StringUtil.utf8HexEncode(username); + for (Cookie cookie : cookies) { + if (cookieName.equals(cookie.getName())) { + return cookie.getValue(); + } + } + return null; + } + + /** + * Returns all players owned by the given username and client ID. + * + * @param username The name of the user. + * @param clientId The third-party client ID (used if this player is managed over the + * Subsonic REST API). May be <code>null</code>. + * @return All relevant players. + */ + public List<Player> getPlayersForUserAndClientId(String username, String clientId) { + return playerDao.getPlayersForUserAndClientId(username, clientId); + } + + /** + * Returns all currently registered players. + * + * @return All currently registered players. + */ + public List<Player> getAllPlayers() { + return playerDao.getAllPlayers(); + } + + /** + * Removes the player with the given ID. + * + * @param id The unique player ID. + */ + public synchronized void removePlayerById(String id) { + playerDao.deletePlayer(id); + } + + /** + * Creates and returns a clone of the given player. + * + * @param playerId The ID of the player to clone. + * @return The cloned player. + */ + public Player clonePlayer(String playerId) { + Player player = getPlayerById(playerId); + if (player.getName() != null) { + player.setName(player.getName() + " (copy)"); + } + + createPlayer(player); + return player; + } + + /** + * Creates the given player, and activates all transcodings. + * + * @param player The player to create. + */ + public void createPlayer(Player player) { + playerDao.createPlayer(player); + + List<Transcoding> transcodings = transcodingService.getAllTranscodings(); + List<Transcoding> defaultActiveTranscodings = new ArrayList<Transcoding>(); + for (Transcoding transcoding : transcodings) { + if (transcoding.isDefaultActive()) { + defaultActiveTranscodings.add(transcoding); + } + } + + transcodingService.setTranscodingsForPlayer(player, defaultActiveTranscodings); + } + + public void setStatusService(StatusService statusService) { + this.statusService = statusService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPlayerDao(PlayerDao playerDao) { + this.playerDao = playerDao; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java new file mode 100644 index 00000000..6208c3dc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java @@ -0,0 +1,426 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see <http://www.gnu.org/licenses/>. + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.util.Pair; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringEscapeUtils; +import org.jdom.Document; +import org.jdom.Element; +import org.jdom.JDOMException; +import org.jdom.Namespace; +import org.jdom.input.SAXBuilder; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.dao.PlaylistDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Provides services for loading and saving playlists to and from persistent storage. + * + * @author Sindre Mehus + * @see net.sourceforge.subsonic.domain.PlayQueue + */ +public class PlaylistService { + + private static final Logger LOG = Logger.getLogger(PlaylistService.class); + private MediaFileService mediaFileService; + private MediaFileDao mediaFileDao; + private PlaylistDao playlistDao; + private SecurityService securityService; + private SettingsService settingsService; + + public void init() { + try { + importPlaylists(); + } catch (Throwable x) { + LOG.warn("Failed to import playlists: " + x, x); + } + } + + public List<Playlist> getReadablePlaylistsForUser(String username) { + return playlistDao.getReadablePlaylistsForUser(username); + } + + public List<Playlist> getWritablePlaylistsForUser(String username) { + + // Admin users are allowed to modify all playlists that are visible to them. + if (securityService.isAdmin(username)) { + return getReadablePlaylistsForUser(username); + } + + return playlistDao.getWritablePlaylistsForUser(username); + } + + public Playlist getPlaylist(int id) { + return playlistDao.getPlaylist(id); + } + + public List<String> getPlaylistUsers(int playlistId) { + return playlistDao.getPlaylistUsers(playlistId); + } + + public List<MediaFile> getFilesInPlaylist(int id) { + return mediaFileDao.getFilesInPlaylist(id); + } + + public void setFilesInPlaylist(int id, List<MediaFile> files) { + playlistDao.setFilesInPlaylist(id, files); + } + + public void createPlaylist(Playlist playlist) { + playlistDao.createPlaylist(playlist); + } + + public void addPlaylistUser(int playlistId, String username) { + playlistDao.addPlaylistUser(playlistId, username); + } + + public void deletePlaylistUser(int playlistId, String username) { + playlistDao.deletePlaylistUser(playlistId, username); + } + + public boolean isReadAllowed(Playlist playlist, String username) { + if (username == null) { + return false; + } + if (username.equals(playlist.getUsername()) || playlist.isPublic()) { + return true; + } + return playlistDao.getPlaylistUsers(playlist.getId()).contains(username); + } + + public boolean isWriteAllowed(Playlist playlist, String username) { + return username != null && username.equals(playlist.getUsername()); + } + + public void deletePlaylist(int id) { + playlistDao.deletePlaylist(id); + } + + public void updatePlaylist(Playlist playlist) { + playlistDao.updatePlaylist(playlist); + } + + public Playlist importPlaylist(String username, String playlistName, String fileName, String format, InputStream inputStream) throws Exception { + PlaylistFormat playlistFormat = PlaylistFormat.getPlaylistFormat(format); + if (playlistFormat == null) { + throw new Exception("Unsupported playlist format: " + format); + } + + Pair<List<MediaFile>, List<String>> result = parseFiles(IOUtils.toByteArray(inputStream), playlistFormat); + if (result.getFirst().isEmpty() && !result.getSecond().isEmpty()) { + throw new Exception("No songs in the playlist were found."); + } + + for (String error : result.getSecond()) { + LOG.warn("File in playlist '" + fileName + "' not found: " + error); + } + + Date now = new Date(); + Playlist playlist = new Playlist(); + playlist.setUsername(username); + playlist.setCreated(now); + playlist.setChanged(now); + playlist.setPublic(true); + playlist.setName(playlistName); + playlist.setImportedFrom(fileName); + + createPlaylist(playlist); + setFilesInPlaylist(playlist.getId(), result.getFirst()); + + return playlist; + } + + private Pair<List<MediaFile>, List<String>> parseFiles(byte[] playlist, PlaylistFormat playlistFormat) throws IOException { + Pair<List<MediaFile>, List<String>> result = null; + + // Try with multiple encodings; use the one that finds the most files. + String[] encodings = {StringUtil.ENCODING_LATIN, StringUtil.ENCODING_UTF8, Charset.defaultCharset().name()}; + for (String encoding : encodings) { + Pair<List<MediaFile>, List<String>> files = parseFilesWithEncoding(playlist, playlistFormat, encoding); + if (result == null || result.getFirst().size() < files.getFirst().size()) { + result = files; + } + } + return result; + } + + private Pair<List<MediaFile>, List<String>> parseFilesWithEncoding(byte[] playlist, PlaylistFormat playlistFormat, String encoding) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(playlist), encoding)); + return playlistFormat.parse(reader, mediaFileService); + } + + public void exportPlaylist(int id, OutputStream out) throws Exception { + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StringUtil.ENCODING_UTF8)); + new M3UFormat().format(getFilesInPlaylist(id), writer); + } + + /** + * Implementation of M3U playlist format. + */ + private void importPlaylists() throws Exception { + String playlistFolderPath = settingsService.getPlaylistFolder(); + if (playlistFolderPath == null) { + return; + } + File playlistFolder = new File(playlistFolderPath); + if (!playlistFolder.exists()) { + return; + } + + List<Playlist> allPlaylists = playlistDao.getAllPlaylists(); + for (File file : playlistFolder.listFiles()) { + try { + importPlaylistIfNotExisting(file, allPlaylists); + } catch (Exception x) { + LOG.warn("Failed to auto-import playlist " + file + ". " + x.getMessage()); + } + } + } + + private void importPlaylistIfNotExisting(File file, List<Playlist> allPlaylists) throws Exception { + String format = FilenameUtils.getExtension(file.getPath()); + if (PlaylistFormat.getPlaylistFormat(format) == null) { + return; + } + + String fileName = file.getName(); + for (Playlist playlist : allPlaylists) { + if (fileName.equals(playlist.getImportedFrom())) { + return; // Already imported. + } + } + InputStream in = new FileInputStream(file); + try { + importPlaylist(User.USERNAME_ADMIN, FilenameUtils.getBaseName(fileName), fileName, format, in); + LOG.info("Auto-imported playlist " + file); + } finally { + IOUtils.closeQuietly(in); + } + } + + public void setPlaylistDao(PlaylistDao playlistDao) { + this.playlistDao = playlistDao; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + /** + * Abstract superclass for playlist formats. + */ + + private abstract static class PlaylistFormat { + public abstract Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException; + + public abstract void format(List<MediaFile> files, PrintWriter writer) throws IOException; + + public static PlaylistFormat getPlaylistFormat(String format) { + if (format == null) { + return null; + } + if (format.equalsIgnoreCase("m3u") || format.equalsIgnoreCase("m3u8")) { + return new M3UFormat(); + } + if (format.equalsIgnoreCase("pls")) { + return new PLSFormat(); + } + if (format.equalsIgnoreCase("xspf")) { + return new XSPFFormat(); + } + return null; + } + + protected MediaFile getMediaFile(MediaFileService mediaFileService, String path) { + try { + MediaFile file = mediaFileService.getMediaFile(path); + if (file != null && file.exists()) { + return file; + } + } catch (SecurityException x) { + // Ignored + } + return null; + } + } + + private static class M3UFormat extends PlaylistFormat { + public Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { + List<MediaFile> ok = new ArrayList<MediaFile>(); + List<String> error = new ArrayList<String>(); + String line = reader.readLine(); + while (line != null) { + if (!line.startsWith("#")) { + MediaFile file = getMediaFile(mediaFileService, line); + if (file != null) { + ok.add(file); + } else { + error.add(line); + } + } + line = reader.readLine(); + } + return new Pair<List<MediaFile>, List<String>>(ok, error); + } + + public void format(List<MediaFile> files, PrintWriter writer) throws IOException { + writer.println("#EXTM3U"); + for (MediaFile file : files) { + writer.println(file.getPath()); + } + if (writer.checkError()) { + throw new IOException("Error when writing playlist"); + } + } + } + + /** + * Implementation of PLS playlist format. + */ + private static class PLSFormat extends PlaylistFormat { + public Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { + List<MediaFile> ok = new ArrayList<MediaFile>(); + List<String> error = new ArrayList<String>(); + + Pattern pattern = Pattern.compile("^File\\d+=(.*)$"); + String line = reader.readLine(); + while (line != null) { + + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + String path = matcher.group(1); + MediaFile file = getMediaFile(mediaFileService, path); + if (file != null) { + ok.add(file); + } else { + error.add(path); + } + } + line = reader.readLine(); + } + return new Pair<List<MediaFile>, List<String>>(ok, error); + } + + public void format(List<MediaFile> files, PrintWriter writer) throws IOException { + writer.println("[playlist]"); + int counter = 0; + + for (MediaFile file : files) { + counter++; + writer.println("File" + counter + '=' + file.getPath()); + } + writer.println("NumberOfEntries=" + counter); + writer.println("Version=2"); + + if (writer.checkError()) { + throw new IOException("Error when writing playlist."); + } + } + } + + /** + * Implementation of XSPF (http://www.xspf.org/) playlist format. + */ + private static class XSPFFormat extends PlaylistFormat { + public Pair<List<MediaFile>, List<String>> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { + List<MediaFile> ok = new ArrayList<MediaFile>(); + List<String> error = new ArrayList<String>(); + + SAXBuilder builder = new SAXBuilder(); + Document document; + try { + document = builder.build(reader); + } catch (JDOMException x) { + LOG.warn("Failed to parse XSPF playlist.", x); + throw new IOException("Failed to parse XSPF playlist."); + } + + Element root = document.getRootElement(); + Namespace ns = root.getNamespace(); + Element trackList = root.getChild("trackList", ns); + List<?> tracks = trackList.getChildren("track", ns); + + for (Object obj : tracks) { + Element track = (Element) obj; + String location = track.getChildText("location", ns); + if (location != null && location.startsWith("file://")) { + location = location.replaceFirst("file://", ""); + MediaFile file = getMediaFile(mediaFileService, location); + if (file != null) { + ok.add(file); + } else { + error.add(location); + } + } + } + return new Pair<List<MediaFile>, List<String>>(ok, error); + } + + public void format(List<MediaFile> files, PrintWriter writer) throws IOException { + writer.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); + writer.println("<playlist version=\"1\" xmlns=\"http://xspf.org/ns/0/\">"); + writer.println(" <trackList>"); + + for (MediaFile file : files) { + writer.println(" <track><location>file://" + StringEscapeUtils.escapeXml(file.getPath()) + "</location></track>"); + } + writer.println(" </trackList>"); + writer.println("</playlist>"); + + if (writer.checkError()) { + throw new IOException("Error when writing playlist."); + } + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java new file mode 100644 index 00000000..09184df6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java @@ -0,0 +1,599 @@ +/* + 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.service; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.PodcastDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.PodcastStatus; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.HttpResponse; +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.jdom.Document; +import org.jdom.Element; +import org.jdom.Namespace; +import org.jdom.input.SAXBuilder; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * Provides services for Podcast reception. + * + * @author Sindre Mehus + */ +public class PodcastService { + + private static final Logger LOG = Logger.getLogger(PodcastService.class); + private static final DateFormat[] RSS_DATE_FORMATS = {new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US), + new SimpleDateFormat("dd MMM yyyy HH:mm:ss Z", Locale.US)}; + + private static final Namespace[] ITUNES_NAMESPACES = {Namespace.getNamespace("http://www.itunes.com/DTDs/Podcast-1.0.dtd"), + Namespace.getNamespace("http://www.itunes.com/dtds/podcast-1.0.dtd")}; + + private final ExecutorService refreshExecutor; + private final ExecutorService downloadExecutor; + private final ScheduledExecutorService scheduledExecutor; + private ScheduledFuture<?> scheduledRefresh; + private PodcastDao podcastDao; + private SettingsService settingsService; + private SecurityService securityService; + private MediaFileService mediaFileService; + + public PodcastService() { + ThreadFactory threadFactory = new ThreadFactory() { + public Thread newThread(Runnable r) { + Thread t = Executors.defaultThreadFactory().newThread(r); + t.setDaemon(true); + return t; + } + }; + refreshExecutor = Executors.newFixedThreadPool(5, threadFactory); + downloadExecutor = Executors.newFixedThreadPool(3, threadFactory); + scheduledExecutor = Executors.newSingleThreadScheduledExecutor(threadFactory); + } + + public synchronized void init() { + // Clean up partial downloads. + for (PodcastChannel channel : getAllChannels()) { + for (PodcastEpisode episode : getEpisodes(channel.getId(), false)) { + if (episode.getStatus() == PodcastStatus.DOWNLOADING) { + deleteEpisode(episode.getId(), false); + LOG.info("Deleted Podcast episode '" + episode.getTitle() + "' since download was interrupted."); + } + } + } + + schedule(); + } + + public synchronized void schedule() { + Runnable task = new Runnable() { + public void run() { + LOG.info("Starting scheduled Podcast refresh."); + refreshAllChannels(true); + LOG.info("Completed scheduled Podcast refresh."); + } + }; + + if (scheduledRefresh != null) { + scheduledRefresh.cancel(true); + } + + int hoursBetween = settingsService.getPodcastUpdateInterval(); + + if (hoursBetween == -1) { + LOG.info("Automatic Podcast update disabled."); + return; + } + + long periodMillis = hoursBetween * 60L * 60L * 1000L; + long initialDelayMillis = 5L * 60L * 1000L; + + scheduledRefresh = scheduledExecutor.scheduleAtFixedRate(task, initialDelayMillis, periodMillis, TimeUnit.MILLISECONDS); + Date firstTime = new Date(System.currentTimeMillis() + initialDelayMillis); + LOG.info("Automatic Podcast update scheduled to run every " + hoursBetween + " hour(s), starting at " + firstTime); + } + + /** + * Creates a new Podcast channel. + * + * @param url The URL of the Podcast channel. + */ + public void createChannel(String url) { + url = sanitizeUrl(url); + PodcastChannel channel = new PodcastChannel(url); + int channelId = podcastDao.createChannel(channel); + + refreshChannels(Arrays.asList(getChannel(channelId)), true); + } + + private String sanitizeUrl(String url) { + return url.replace(" ", "%20"); + } + + private PodcastChannel getChannel(int channelId) { + for (PodcastChannel channel : getAllChannels()) { + if (channelId == channel.getId()) { + return channel; + } + } + return null; + } + + /** + * Returns all Podcast channels. + * + * @return Possibly empty list of all Podcast channels. + */ + public List<PodcastChannel> getAllChannels() { + return podcastDao.getAllChannels(); + } + + /** + * Returns all Podcast episodes for a given channel. + * + * @param channelId The Podcast channel ID. + * @param includeDeleted Whether to include logically deleted episodes in the result. + * @return Possibly empty list of all Podcast episodes for the given channel, sorted in + * reverse chronological order (newest episode first). + */ + public List<PodcastEpisode> getEpisodes(int channelId, boolean includeDeleted) { + List<PodcastEpisode> all = podcastDao.getEpisodes(channelId); + addMediaFileIdToEpisodes(all); + if (includeDeleted) { + return all; + } + + List<PodcastEpisode> filtered = new ArrayList<PodcastEpisode>(); + for (PodcastEpisode episode : all) { + if (episode.getStatus() != PodcastStatus.DELETED) { + filtered.add(episode); + } + } + return filtered; + } + + public PodcastEpisode getEpisode(int episodeId, boolean includeDeleted) { + PodcastEpisode episode = podcastDao.getEpisode(episodeId); + if (episode == null) { + return null; + } + if (episode.getStatus() == PodcastStatus.DELETED && !includeDeleted) { + return null; + } + addMediaFileIdToEpisodes(Arrays.asList(episode)); + return episode; + } + + private void addMediaFileIdToEpisodes(List<PodcastEpisode> episodes) { + for (PodcastEpisode episode : episodes) { + if (episode.getPath() != null) { + MediaFile mediaFile = mediaFileService.getMediaFile(episode.getPath()); + if (mediaFile != null) { + episode.setMediaFileId(mediaFile.getId()); + } + } + } + } + + private PodcastEpisode getEpisode(int channelId, String url) { + if (url == null) { + return null; + } + + for (PodcastEpisode episode : getEpisodes(channelId, true)) { + if (url.equals(episode.getUrl())) { + return episode; + } + } + return null; + } + + public void refreshAllChannels(boolean downloadEpisodes) { + refreshChannels(getAllChannels(), downloadEpisodes); + } + + private void refreshChannels(final List<PodcastChannel> channels, final boolean downloadEpisodes) { + for (final PodcastChannel channel : channels) { + Runnable task = new Runnable() { + public void run() { + doRefreshChannel(channel, downloadEpisodes); + } + }; + refreshExecutor.submit(task); + } + } + + @SuppressWarnings({"unchecked"}) + private void doRefreshChannel(PodcastChannel channel, boolean downloadEpisodes) { + InputStream in = null; + HttpClient client = new DefaultHttpClient(); + + try { + channel.setStatus(PodcastStatus.DOWNLOADING); + channel.setErrorMessage(null); + podcastDao.updateChannel(channel); + + HttpConnectionParams.setConnectionTimeout(client.getParams(), 2 * 60 * 1000); // 2 minutes + HttpConnectionParams.setSoTimeout(client.getParams(), 10 * 60 * 1000); // 10 minutes + HttpGet method = new HttpGet(channel.getUrl()); + + HttpResponse response = client.execute(method); + in = response.getEntity().getContent(); + + Document document = new SAXBuilder().build(in); + Element channelElement = document.getRootElement().getChild("channel"); + + channel.setTitle(channelElement.getChildTextTrim("title")); + channel.setDescription(channelElement.getChildTextTrim("description")); + channel.setStatus(PodcastStatus.COMPLETED); + channel.setErrorMessage(null); + podcastDao.updateChannel(channel); + + refreshEpisodes(channel, channelElement.getChildren("item")); + + } catch (Exception x) { + LOG.warn("Failed to get/parse RSS file for Podcast channel " + channel.getUrl(), x); + channel.setStatus(PodcastStatus.ERROR); + channel.setErrorMessage(x.toString()); + podcastDao.updateChannel(channel); + } finally { + IOUtils.closeQuietly(in); + client.getConnectionManager().shutdown(); + } + + if (downloadEpisodes) { + for (final PodcastEpisode episode : getEpisodes(channel.getId(), false)) { + if (episode.getStatus() == PodcastStatus.NEW && episode.getUrl() != null) { + downloadEpisode(episode); + } + } + } + } + + public void downloadEpisode(final PodcastEpisode episode) { + Runnable task = new Runnable() { + public void run() { + doDownloadEpisode(episode); + } + }; + downloadExecutor.submit(task); + } + + private void refreshEpisodes(PodcastChannel channel, List<Element> episodeElements) { + + List<PodcastEpisode> episodes = new ArrayList<PodcastEpisode>(); + + for (Element episodeElement : episodeElements) { + + String title = episodeElement.getChildTextTrim("title"); + String duration = getITunesElement(episodeElement, "duration"); + String description = episodeElement.getChildTextTrim("description"); + if (StringUtils.isBlank(description)) { + description = getITunesElement(episodeElement, "summary"); + } + + Element enclosure = episodeElement.getChild("enclosure"); + if (enclosure == null) { + LOG.debug("No enclosure found for episode " + title); + continue; + } + + String url = enclosure.getAttributeValue("url"); + url = sanitizeUrl(url); + if (url == null) { + LOG.debug("No enclosure URL found for episode " + title); + continue; + } + + if (getEpisode(channel.getId(), url) == null) { + Long length = null; + try { + length = new Long(enclosure.getAttributeValue("length")); + } catch (Exception x) { + LOG.warn("Failed to parse enclosure length.", x); + } + + Date date = parseDate(episodeElement.getChildTextTrim("pubDate")); + PodcastEpisode episode = new PodcastEpisode(null, channel.getId(), url, null, title, description, date, + duration, length, 0L, PodcastStatus.NEW, null); + episodes.add(episode); + LOG.info("Created Podcast episode " + title); + } + } + + // Sort episode in reverse chronological order (newest first) + Collections.sort(episodes, new Comparator<PodcastEpisode>() { + public int compare(PodcastEpisode a, PodcastEpisode b) { + long timeA = a.getPublishDate() == null ? 0L : a.getPublishDate().getTime(); + long timeB = b.getPublishDate() == null ? 0L : b.getPublishDate().getTime(); + + if (timeA < timeB) { + return 1; + } + if (timeA > timeB) { + return -1; + } + return 0; + } + }); + + // Create episodes in database, skipping the proper number of episodes. + int downloadCount = settingsService.getPodcastEpisodeDownloadCount(); + if (downloadCount == -1) { + downloadCount = Integer.MAX_VALUE; + } + + for (int i = 0; i < episodes.size(); i++) { + PodcastEpisode episode = episodes.get(i); + if (i >= downloadCount) { + episode.setStatus(PodcastStatus.SKIPPED); + } + podcastDao.createEpisode(episode); + } + } + + private Date parseDate(String s) { + for (DateFormat dateFormat : RSS_DATE_FORMATS) { + try { + return dateFormat.parse(s); + } catch (Exception x) { + // Ignored. + } + } + LOG.warn("Failed to parse publish date: '" + s + "'."); + return null; + } + + private String getITunesElement(Element element, String childName) { + for (Namespace ns : ITUNES_NAMESPACES) { + String value = element.getChildTextTrim(childName, ns); + if (value != null) { + return value; + } + } + return null; + } + + private void doDownloadEpisode(PodcastEpisode episode) { + InputStream in = null; + OutputStream out = null; + + if (getEpisode(episode.getId(), false) == null) { + LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download."); + return; + } + + LOG.info("Starting to download Podcast from " + episode.getUrl()); + + HttpClient client = new DefaultHttpClient(); + try { + PodcastChannel channel = getChannel(episode.getChannelId()); + + HttpConnectionParams.setConnectionTimeout(client.getParams(), 2 * 60 * 1000); // 2 minutes + HttpConnectionParams.setSoTimeout(client.getParams(), 10 * 60 * 1000); // 10 minutes + HttpGet method = new HttpGet(episode.getUrl()); + + HttpResponse response = client.execute(method); + in = response.getEntity().getContent(); + + File file = getFile(channel, episode); + out = new FileOutputStream(file); + + episode.setStatus(PodcastStatus.DOWNLOADING); + episode.setBytesDownloaded(0L); + episode.setErrorMessage(null); + episode.setPath(file.getPath()); + podcastDao.updateEpisode(episode); + + byte[] buffer = new byte[4096]; + long bytesDownloaded = 0; + int n; + long nextLogCount = 30000L; + + while ((n = in.read(buffer)) != -1) { + out.write(buffer, 0, n); + bytesDownloaded += n; + + if (bytesDownloaded > nextLogCount) { + episode.setBytesDownloaded(bytesDownloaded); + nextLogCount += 30000L; + if (getEpisode(episode.getId(), false) == null) { + break; + } + podcastDao.updateEpisode(episode); + } + } + + if (getEpisode(episode.getId(), false) == null) { + LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download."); + IOUtils.closeQuietly(out); + file.delete(); + } else { + episode.setBytesDownloaded(bytesDownloaded); + podcastDao.updateEpisode(episode); + LOG.info("Downloaded " + bytesDownloaded + " bytes from Podcast " + episode.getUrl()); + IOUtils.closeQuietly(out); + episode.setStatus(PodcastStatus.COMPLETED); + podcastDao.updateEpisode(episode); + deleteObsoleteEpisodes(channel); + } + + } catch (Exception x) { + LOG.warn("Failed to download Podcast from " + episode.getUrl(), x); + episode.setStatus(PodcastStatus.ERROR); + episode.setErrorMessage(x.toString()); + podcastDao.updateEpisode(episode); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + client.getConnectionManager().shutdown(); + } + } + + private synchronized void deleteObsoleteEpisodes(PodcastChannel channel) { + int episodeCount = settingsService.getPodcastEpisodeRetentionCount(); + if (episodeCount == -1) { + return; + } + + List<PodcastEpisode> episodes = getEpisodes(channel.getId(), false); + + // Don't do anything if other episodes of the same channel is currently downloading. + for (PodcastEpisode episode : episodes) { + if (episode.getStatus() == PodcastStatus.DOWNLOADING) { + return; + } + } + + // Reverse array to get chronological order (oldest episodes first). + Collections.reverse(episodes); + + int episodesToDelete = Math.max(0, episodes.size() - episodeCount); + for (int i = 0; i < episodesToDelete; i++) { + deleteEpisode(episodes.get(i).getId(), true); + LOG.info("Deleted old Podcast episode " + episodes.get(i).getUrl()); + } + } + + private synchronized File getFile(PodcastChannel channel, PodcastEpisode episode) { + + File podcastDir = new File(settingsService.getPodcastFolder()); + File channelDir = new File(podcastDir, StringUtil.fileSystemSafe(channel.getTitle())); + + if (!channelDir.exists()) { + boolean ok = channelDir.mkdirs(); + if (!ok) { + throw new RuntimeException("Failed to create directory " + channelDir); + } + + MediaFile mediaFile = mediaFileService.getMediaFile(channelDir); + mediaFile.setComment(channel.getDescription()); + mediaFileService.updateMediaFile(mediaFile); + } + + String filename = StringUtil.getUrlFile(episode.getUrl()); + if (filename == null) { + filename = episode.getTitle(); + } + filename = StringUtil.fileSystemSafe(filename); + String extension = FilenameUtils.getExtension(filename); + filename = FilenameUtils.removeExtension(filename); + if (StringUtils.isBlank(extension)) { + extension = "mp3"; + } + + File file = new File(channelDir, filename + "." + extension); + for (int i = 0; file.exists(); i++) { + file = new File(channelDir, filename + i + "." + extension); + } + + if (!securityService.isWriteAllowed(file)) { + throw new SecurityException("Access denied to file " + file); + } + return file; + } + + /** + * Deletes the Podcast channel with the given ID. + * + * @param channelId The Podcast channel ID. + */ + public void deleteChannel(int channelId) { + // Delete all associated episodes (in case they have files that need to be deleted). + List<PodcastEpisode> episodes = getEpisodes(channelId, false); + for (PodcastEpisode episode : episodes) { + deleteEpisode(episode.getId(), false); + } + podcastDao.deleteChannel(channelId); + } + + /** + * Deletes the Podcast episode with the given ID. + * + * @param episodeId The Podcast episode ID. + * @param logicalDelete Whether to perform a logical delete by setting the + * episode status to {@link PodcastStatus#DELETED}. + */ + public void deleteEpisode(int episodeId, boolean logicalDelete) { + PodcastEpisode episode = podcastDao.getEpisode(episodeId); + if (episode == null) { + return; + } + + // Delete file. + if (episode.getPath() != null) { + File file = new File(episode.getPath()); + if (file.exists()) { + file.delete(); + // TODO: Delete directory if empty? + } + } + + if (logicalDelete) { + episode.setStatus(PodcastStatus.DELETED); + episode.setErrorMessage(null); + podcastDao.updateEpisode(episode); + } else { + podcastDao.deleteEpisode(episodeId); + } + } + + public void setPodcastDao(PodcastDao podcastDao) { + this.podcastDao = podcastDao; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + 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/service/RatingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.java new file mode 100644 index 00000000..6208faf2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.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.service; + +import net.sourceforge.subsonic.dao.*; +import net.sourceforge.subsonic.domain.*; +import net.sourceforge.subsonic.util.FileUtil; + +import java.util.*; +import java.io.File; + +/** + * Provides services for user ratings. + * + * @author Sindre Mehus + */ +public class RatingService { + + private RatingDao ratingDao; + private SecurityService securityService; + private MediaFileService mediaFileService; + + /** + * Returns the highest rated music files. + * + * @param offset Number of files to skip. + * @param count Maximum number of files to return. + * @return The highest rated music files. + */ + public List<MediaFile> getHighestRated(int offset, int count) { + List<String> highestRated = ratingDao.getHighestRated(offset, count); + List<MediaFile> result = new ArrayList<MediaFile>(); + for (String path : highestRated) { + File file = new File(path); + if (FileUtil.exists(file) && securityService.isReadAllowed(file)) { + result.add(mediaFileService.getMediaFile(path)); + } + } + return result; + } + + /** + * Sets the rating for a music file and a given user. + * + * @param username The user name. + * @param mediaFile The music file. + * @param rating The rating between 1 and 5, or <code>null</code> to remove the rating. + */ + public void setRatingForUser(String username, MediaFile mediaFile, Integer rating) { + ratingDao.setRatingForUser(username, mediaFile, rating); + } + + /** + * Returns the average rating for the given music file. + * + * @param mediaFile The music file. + * @return The average rating, or <code>null</code> if no ratings are set. + */ + public Double getAverageRating(MediaFile mediaFile) { + return ratingDao.getAverageRating(mediaFile); + } + + /** + * Returns the rating for the given user and music file. + * + * @param username The user name. + * @param mediaFile The music file. + * @return The rating, or <code>null</code> if no rating is set. + */ + public Integer getRatingForUser(String username, MediaFile mediaFile) { + return ratingDao.getRatingForUser(username, mediaFile); + } + + public void setRatingDao(RatingDao ratingDao) { + this.ratingDao = ratingDao; + } + + 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/service/SearchService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java new file mode 100644 index 00000000..2698bbd6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java @@ -0,0 +1,567 @@ +/* + 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.service; + +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.domain.MusicFolder; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.util.FileUtil; +import org.apache.lucene.analysis.ASCIIFoldingFilter; +import org.apache.lucene.analysis.Analyzer; +import org.apache.lucene.analysis.LowerCaseFilter; +import org.apache.lucene.analysis.StopFilter; +import org.apache.lucene.analysis.TokenStream; +import org.apache.lucene.analysis.standard.StandardAnalyzer; +import org.apache.lucene.analysis.standard.StandardFilter; +import org.apache.lucene.analysis.standard.StandardTokenizer; +import org.apache.lucene.document.Document; +import org.apache.lucene.document.Field; +import org.apache.lucene.document.NumericField; +import org.apache.lucene.index.IndexReader; +import org.apache.lucene.index.IndexWriter; +import org.apache.lucene.index.Term; +import org.apache.lucene.queryParser.MultiFieldQueryParser; +import org.apache.lucene.search.BooleanClause; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.IndexSearcher; +import org.apache.lucene.search.MatchAllDocsQuery; +import org.apache.lucene.search.NumericRangeQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.util.Version; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import static net.sourceforge.subsonic.service.SearchService.IndexType.*; +import static net.sourceforge.subsonic.service.SearchService.IndexType.SONG; + +/** + * Performs Lucene-based searching and indexing. + * + * @author Sindre Mehus + * @version $Id$ + * @see MediaScannerService + */ +public class SearchService { + + private static final Logger LOG = Logger.getLogger(SearchService.class); + + private static final String FIELD_ID = "id"; + private static final String FIELD_TITLE = "title"; + private static final String FIELD_ALBUM = "album"; + private static final String FIELD_ARTIST = "artist"; + private static final String FIELD_GENRE = "genre"; + private static final String FIELD_YEAR = "year"; + private static final String FIELD_MEDIA_TYPE = "mediaType"; + private static final String FIELD_FOLDER = "folder"; + + private static final Version LUCENE_VERSION = Version.LUCENE_30; + + private MediaFileService mediaFileService; + private SettingsService settingsService; + private ArtistDao artistDao; + private AlbumDao albumDao; + + private IndexWriter artistWriter; + private IndexWriter artistId3Writer; + private IndexWriter albumWriter; + private IndexWriter albumId3Writer; + private IndexWriter songWriter; + + public SearchService() { + removeLocks(); + } + + + public void startIndexing() { + try { + artistWriter = createIndexWriter(ARTIST); + artistId3Writer = createIndexWriter(ARTIST_ID3); + albumWriter = createIndexWriter(ALBUM); + albumId3Writer = createIndexWriter(ALBUM_ID3); + songWriter = createIndexWriter(SONG); + } catch (Exception x) { + LOG.error("Failed to create search index.", x); + } + } + + public void index(MediaFile mediaFile) { + try { + if (mediaFile.isFile()) { + songWriter.addDocument(SONG.createDocument(mediaFile)); + } else if (mediaFile.isAlbum()) { + albumWriter.addDocument(ALBUM.createDocument(mediaFile)); + } else { + artistWriter.addDocument(ARTIST.createDocument(mediaFile)); + } + } catch (Exception x) { + LOG.error("Failed to create search index for " + mediaFile, x); + } + } + + public void index(Artist artist) { + try { + artistId3Writer.addDocument(ARTIST_ID3.createDocument(artist)); + } catch (Exception x) { + LOG.error("Failed to create search index for " + artist, x); + } + } + + public void index(Album album) { + try { + albumId3Writer.addDocument(ALBUM_ID3.createDocument(album)); + } catch (Exception x) { + LOG.error("Failed to create search index for " + album, x); + } + } + + public void stopIndexing() { + try { + artistWriter.optimize(); + artistId3Writer.optimize(); + albumWriter.optimize(); + albumId3Writer.optimize(); + songWriter.optimize(); + } catch (Exception x) { + LOG.error("Failed to create search index.", x); + } finally { + FileUtil.closeQuietly(artistId3Writer); + FileUtil.closeQuietly(artistWriter); + FileUtil.closeQuietly(albumWriter); + FileUtil.closeQuietly(albumId3Writer); + FileUtil.closeQuietly(songWriter); + } + } + + public SearchResult search(SearchCriteria criteria, IndexType indexType) { + SearchResult result = new SearchResult(); + int offset = criteria.getOffset(); + int count = criteria.getCount(); + result.setOffset(offset); + + IndexReader reader = null; + try { + reader = createIndexReader(indexType); + Searcher searcher = new IndexSearcher(reader); + Analyzer analyzer = new SubsonicAnalyzer(); + + MultiFieldQueryParser queryParser = new MultiFieldQueryParser(LUCENE_VERSION, indexType.getFields(), analyzer, indexType.getBoosts()); + Query query = queryParser.parse(criteria.getQuery()); + + TopDocs topDocs = searcher.search(query, null, offset + count); + result.setTotalHits(topDocs.totalHits); + + int start = Math.min(offset, topDocs.totalHits); + int end = Math.min(start + count, topDocs.totalHits); + for (int i = start; i < end; i++) { + Document doc = searcher.doc(topDocs.scoreDocs[i].doc); + switch (indexType) { + case SONG: + case ARTIST: + case ALBUM: + MediaFile mediaFile = mediaFileService.getMediaFile(Integer.valueOf(doc.get(FIELD_ID))); + addIfNotNull(mediaFile, result.getMediaFiles()); + break; + case ARTIST_ID3: + Artist artist = artistDao.getArtist(Integer.valueOf(doc.get(FIELD_ID))); + addIfNotNull(artist, result.getArtists()); + break; + case ALBUM_ID3: + Album album = albumDao.getAlbum(Integer.valueOf(doc.get(FIELD_ID))); + addIfNotNull(album, result.getAlbums()); + break; + default: + break; + } + } + + } catch (Throwable x) { + LOG.error("Failed to execute Lucene search.", x); + } finally { + FileUtil.closeQuietly(reader); + } + return result; + } + + /** + * Returns a number of random songs. + * + * @param criteria Search criteria. + * @return List of random songs. + */ + public List<MediaFile> getRandomSongs(RandomSearchCriteria criteria) { + List<MediaFile> result = new ArrayList<MediaFile>(); + + String musicFolderPath = null; + if (criteria.getMusicFolderId() != null) { + MusicFolder musicFolder = settingsService.getMusicFolderById(criteria.getMusicFolderId()); + musicFolderPath = musicFolder.getPath().getPath(); + } + + IndexReader reader = null; + try { + reader = createIndexReader(SONG); + Searcher searcher = new IndexSearcher(reader); + + BooleanQuery query = new BooleanQuery(); + query.add(new TermQuery(new Term(FIELD_MEDIA_TYPE, MediaFile.MediaType.MUSIC.name().toLowerCase())), BooleanClause.Occur.MUST); + if (criteria.getGenre() != null) { + String genre = normalizeGenre(criteria.getGenre()); + query.add(new TermQuery(new Term(FIELD_GENRE, genre)), BooleanClause.Occur.MUST); + } + if (criteria.getFromYear() != null || criteria.getToYear() != null) { + NumericRangeQuery<Integer> rangeQuery = NumericRangeQuery.newIntRange(FIELD_YEAR, criteria.getFromYear(), criteria.getToYear(), true, true); + query.add(rangeQuery, BooleanClause.Occur.MUST); + } + if (musicFolderPath != null) { + query.add(new TermQuery(new Term(FIELD_FOLDER, musicFolderPath)), BooleanClause.Occur.MUST); + } + + TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); + Random random = new Random(System.currentTimeMillis()); + + for (int i = 0; i < Math.min(criteria.getCount(), topDocs.totalHits); i++) { + int index = random.nextInt(topDocs.totalHits); + Document doc = searcher.doc(topDocs.scoreDocs[index].doc); + int id = Integer.valueOf(doc.get(FIELD_ID)); + try { + result.add(mediaFileService.getMediaFile(id)); + } catch (Exception x) { + LOG.warn("Failed to get media file " + id); + } + } + + } catch (Throwable x) { + LOG.error("Failed to search or random songs.", x); + } finally { + FileUtil.closeQuietly(reader); + } + return result; + } + + private static String normalizeGenre(String genre) { + return genre.toLowerCase().replace(" ", ""); + } + + /** + * Returns a number of random albums. + * + * @param count Number of albums to return. + * @return List of random albums. + */ + public List<MediaFile> getRandomAlbums(int count) { + List<MediaFile> result = new ArrayList<MediaFile>(); + + IndexReader reader = null; + try { + reader = createIndexReader(ALBUM); + Searcher searcher = new IndexSearcher(reader); + + Query query = new MatchAllDocsQuery(); + TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); + Random random = new Random(System.currentTimeMillis()); + + for (int i = 0; i < Math.min(count, topDocs.totalHits); i++) { + int index = random.nextInt(topDocs.totalHits); + Document doc = searcher.doc(topDocs.scoreDocs[index].doc); + int id = Integer.valueOf(doc.get(FIELD_ID)); + try { + addIfNotNull(mediaFileService.getMediaFile(id), result); + } catch (Exception x) { + LOG.warn("Failed to get media file " + id, x); + } + } + + } catch (Throwable x) { + LOG.error("Failed to search for random albums.", x); + } finally { + FileUtil.closeQuietly(reader); + } + return result; + } + + /** + * Returns a number of random albums, using ID3 tag. + * + * @param count Number of albums to return. + * @return List of random albums. + */ + public List<Album> getRandomAlbumsId3(int count) { + List<Album> result = new ArrayList<Album>(); + + IndexReader reader = null; + try { + reader = createIndexReader(ALBUM_ID3); + Searcher searcher = new IndexSearcher(reader); + + Query query = new MatchAllDocsQuery(); + TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); + Random random = new Random(System.currentTimeMillis()); + + for (int i = 0; i < Math.min(count, topDocs.totalHits); i++) { + int index = random.nextInt(topDocs.totalHits); + Document doc = searcher.doc(topDocs.scoreDocs[index].doc); + int id = Integer.valueOf(doc.get(FIELD_ID)); + try { + addIfNotNull(albumDao.getAlbum(id), result); + } catch (Exception x) { + LOG.warn("Failed to get album file " + id, x); + } + } + + } catch (Throwable x) { + LOG.error("Failed to search for random albums.", x); + } finally { + FileUtil.closeQuietly(reader); + } + return result; + } + + private <T> void addIfNotNull(T value, List<T> list) { + if (value != null) { + list.add(value); + } + } + private IndexWriter createIndexWriter(IndexType indexType) throws IOException { + File dir = getIndexDirectory(indexType); + return new IndexWriter(FSDirectory.open(dir), new SubsonicAnalyzer(), true, new IndexWriter.MaxFieldLength(10)); + } + + private IndexReader createIndexReader(IndexType indexType) throws IOException { + File dir = getIndexDirectory(indexType); + return IndexReader.open(FSDirectory.open(dir), true); + } + + private File getIndexRootDirectory() { + return new File(SettingsService.getSubsonicHome(), "lucene2"); + } + + private File getIndexDirectory(IndexType indexType) { + return new File(getIndexRootDirectory(), indexType.toString().toLowerCase()); + } + + private void removeLocks() { + for (IndexType indexType : IndexType.values()) { + Directory dir = null; + try { + dir = FSDirectory.open(getIndexDirectory(indexType)); + if (IndexWriter.isLocked(dir)) { + IndexWriter.unlock(dir); + LOG.info("Removed Lucene lock file in " + dir); + } + } catch (Exception x) { + LOG.warn("Failed to remove Lucene lock file in " + dir, x); + } finally { + FileUtil.closeQuietly(dir); + } + } + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public static enum IndexType { + + SONG(new String[]{FIELD_TITLE, FIELD_ARTIST}, FIELD_TITLE) { + @Override + public Document createDocument(MediaFile mediaFile) { + Document doc = new Document(); + doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId())); + doc.add(new Field(FIELD_MEDIA_TYPE, mediaFile.getMediaType().name(), Field.Store.NO, Field.Index.ANALYZED_NO_NORMS)); + + if (mediaFile.getTitle() != null) { + doc.add(new Field(FIELD_TITLE, mediaFile.getTitle(), Field.Store.YES, Field.Index.ANALYZED)); + } + if (mediaFile.getArtist() != null) { + doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); + } + if (mediaFile.getGenre() != null) { + doc.add(new Field(FIELD_GENRE, normalizeGenre(mediaFile.getGenre()), Field.Store.NO, Field.Index.ANALYZED)); + } + if (mediaFile.getYear() != null) { + doc.add(new NumericField(FIELD_YEAR, Field.Store.NO, true).setIntValue(mediaFile.getYear())); + } + if (mediaFile.getFolder() != null) { + doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); + } + + return doc; + } + }, + + ALBUM(new String[]{FIELD_ALBUM, FIELD_ARTIST}, FIELD_ALBUM) { + @Override + public Document createDocument(MediaFile mediaFile) { + Document doc = new Document(); + doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId())); + + if (mediaFile.getArtist() != null) { + doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); + } + if (mediaFile.getAlbumName() != null) { + doc.add(new Field(FIELD_ALBUM, mediaFile.getAlbumName(), Field.Store.YES, Field.Index.ANALYZED)); + } + + return doc; + } + }, + + ALBUM_ID3(new String[]{FIELD_ALBUM, FIELD_ARTIST}, FIELD_ALBUM) { + @Override + public Document createDocument(Album album) { + Document doc = new Document(); + doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(album.getId())); + + if (album.getArtist() != null) { + doc.add(new Field(FIELD_ARTIST, album.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); + } + if (album.getName() != null) { + doc.add(new Field(FIELD_ALBUM, album.getName(), Field.Store.YES, Field.Index.ANALYZED)); + } + + return doc; + } + }, + + ARTIST(new String[]{FIELD_ARTIST}, null) { + @Override + public Document createDocument(MediaFile mediaFile) { + Document doc = new Document(); + doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(mediaFile.getId())); + + if (mediaFile.getArtist() != null) { + doc.add(new Field(FIELD_ARTIST, mediaFile.getArtist(), Field.Store.YES, Field.Index.ANALYZED)); + } + + return doc; + } + }, + + ARTIST_ID3(new String[]{FIELD_ARTIST}, null) { + @Override + public Document createDocument(Artist artist) { + Document doc = new Document(); + doc.add(new NumericField(FIELD_ID, Field.Store.YES, false).setIntValue(artist.getId())); + doc.add(new Field(FIELD_ARTIST, artist.getName(), Field.Store.YES, Field.Index.ANALYZED)); + + return doc; + } + }; + + private final String[] fields; + private final Map<String, Float> boosts; + + private IndexType(String[] fields, String boostedField) { + this.fields = fields; + boosts = new HashMap<String, Float>(); + if (boostedField != null) { + boosts.put(boostedField, 2.0F); + } + } + + public String[] getFields() { + return fields; + } + + protected Document createDocument(MediaFile mediaFile) { + throw new UnsupportedOperationException(); + } + + protected Document createDocument(Artist artist) { + throw new UnsupportedOperationException(); + } + + protected Document createDocument(Album album) { + throw new UnsupportedOperationException(); + } + + public Map<String, Float> getBoosts() { + return boosts; + } + } + + private class SubsonicAnalyzer extends StandardAnalyzer { + private SubsonicAnalyzer() { + super(LUCENE_VERSION); + } + + @Override + public TokenStream tokenStream(String fieldName, Reader reader) { + TokenStream result = super.tokenStream(fieldName, reader); + return new ASCIIFoldingFilter(result); + } + + @Override + public TokenStream reusableTokenStream(String fieldName, Reader reader) throws IOException { + class SavedStreams { + StandardTokenizer tokenStream; + TokenStream filteredTokenStream; + } + + SavedStreams streams = (SavedStreams) getPreviousTokenStream(); + if (streams == null) { + streams = new SavedStreams(); + setPreviousTokenStream(streams); + streams.tokenStream = new StandardTokenizer(LUCENE_VERSION, reader); + streams.filteredTokenStream = new StandardFilter(streams.tokenStream); + streams.filteredTokenStream = new LowerCaseFilter(streams.filteredTokenStream); + streams.filteredTokenStream = new StopFilter(true, streams.filteredTokenStream, STOP_WORDS_SET); + streams.filteredTokenStream = new ASCIIFoldingFilter(streams.filteredTokenStream); + } else { + streams.tokenStream.reset(reader); + } + streams.tokenStream.setMaxTokenLength(DEFAULT_MAX_TOKEN_LENGTH); + + return streams.filteredTokenStream; + } + } +} + + diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java new file mode 100644 index 00000000..d6ca871d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java @@ -0,0 +1,303 @@ +/* + 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.service; + +import java.io.File; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.GrantedAuthorityImpl; +import org.acegisecurity.providers.dao.DaoAuthenticationProvider; +import org.acegisecurity.userdetails.UserDetails; +import org.acegisecurity.userdetails.UserDetailsService; +import org.acegisecurity.userdetails.UsernameNotFoundException; +import org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper; +import org.springframework.dao.DataAccessException; + +import net.sf.ehcache.Ehcache; +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.UserDao; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.util.FileUtil; + +/** + * Provides security-related services for authentication and authorization. + * + * @author Sindre Mehus + */ +public class SecurityService implements UserDetailsService { + + private static final Logger LOG = Logger.getLogger(SecurityService.class); + + private UserDao userDao; + private SettingsService settingsService; + private Ehcache userCache; + + /** + * Locates the user based on the username. + * + * @param username The username presented to the {@link DaoAuthenticationProvider} + * @return A fully populated user record (never <code>null</code>) + * @throws UsernameNotFoundException if the user could not be found or the user has no GrantedAuthority. + * @throws DataAccessException If user could not be found for a repository-specific reason. + */ + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { + User user = getUserByName(username); + if (user == null) { + throw new UsernameNotFoundException("User \"" + username + "\" was not found."); + } + + String[] roles = userDao.getRolesForUser(username); + GrantedAuthority[] authorities = new GrantedAuthority[roles.length]; + for (int i = 0; i < roles.length; i++) { + authorities[i] = new GrantedAuthorityImpl("ROLE_" + roles[i].toUpperCase()); + } + + // If user is LDAP authenticated, disable user. The proper authentication should in that case + // be done by SubsonicLdapBindAuthenticator. + boolean enabled = !user.isLdapAuthenticated(); + + return new org.acegisecurity.userdetails.User(username, user.getPassword(), enabled, true, true, true, authorities); + } + + /** + * Returns the currently logged-in user for the given HTTP request. + * + * @param request The HTTP request. + * @return The logged-in user, or <code>null</code>. + */ + public User getCurrentUser(HttpServletRequest request) { + String username = getCurrentUsername(request); + return username == null ? null : userDao.getUserByName(username); + } + + /** + * Returns the name of the currently logged-in user. + * + * @param request The HTTP request. + * @return The name of the logged-in user, or <code>null</code>. + */ + public String getCurrentUsername(HttpServletRequest request) { + return new SecurityContextHolderAwareRequestWrapper(request, null).getRemoteUser(); + } + + /** + * Returns the user with the given username. + * + * @param username The username used when logging in. + * @return The user, or <code>null</code> if not found. + */ + public User getUserByName(String username) { + return userDao.getUserByName(username); + } + + /** + * Returns the user with the given email address. + * + * @param email The email address. + * @return The user, or <code>null</code> if not found. + */ + public User getUserByEmail(String email) { + return userDao.getUserByEmail(email); + } + + /** + * Returns all users. + * + * @return Possibly empty array of all users. + */ + public List<User> getAllUsers() { + return userDao.getAllUsers(); + } + + /** + * Returns whether the given user has administrative rights. + */ + public boolean isAdmin(String username) { + if (User.USERNAME_ADMIN.equals(username)) { + return true; + } + User user = getUserByName(username); + return user != null && user.isAdminRole(); + } + + /** + * Creates a new user. + * + * @param user The user to create. + */ + public void createUser(User user) { + userDao.createUser(user); + LOG.info("Created user " + user.getUsername()); + } + + /** + * Deletes the user with the given username. + * + * @param username The username. + */ + public void deleteUser(String username) { + userDao.deleteUser(username); + LOG.info("Deleted user " + username); + userCache.remove(username); + } + + /** + * Updates the given user. + * + * @param user The user to update. + */ + public void updateUser(User user) { + userDao.updateUser(user); + userCache.remove(user.getUsername()); + } + + /** + * Updates the byte counts for given user. + * + * @param user The user to update, may be <code>null</code>. + * @param bytesStreamedDelta Increment bytes streamed count with this value. + * @param bytesDownloadedDelta Increment bytes downloaded count with this value. + * @param bytesUploadedDelta Increment bytes uploaded count with this value. + */ + public void updateUserByteCounts(User user, long bytesStreamedDelta, long bytesDownloadedDelta, long bytesUploadedDelta) { + if (user == null) { + return; + } + + user.setBytesStreamed(user.getBytesStreamed() + bytesStreamedDelta); + user.setBytesDownloaded(user.getBytesDownloaded() + bytesDownloadedDelta); + user.setBytesUploaded(user.getBytesUploaded() + bytesUploadedDelta); + + userDao.updateUser(user); + } + + /** + * Returns whether the given file may be read. + * + * @return Whether the given file may be read. + */ + public boolean isReadAllowed(File file) { + // Allowed to read from both music folder and podcast folder. + return isInMusicFolder(file) || isInPodcastFolder(file); + } + + /** + * Returns whether the given file may be written, created or deleted. + * + * @return Whether the given file may be written, created or deleted. + */ + public boolean isWriteAllowed(File file) { + // Only allowed to write podcasts or cover art. + boolean isPodcast = isInPodcastFolder(file); + boolean isCoverArt = isInMusicFolder(file) && file.getName().startsWith("cover."); + + return isPodcast || isCoverArt; + } + + /** + * Returns whether the given file may be uploaded. + * + * @return Whether the given file may be uploaded. + */ + public boolean isUploadAllowed(File file) { + return isInMusicFolder(file) && !FileUtil.exists(file); + } + + /** + * Returns whether the given file is located in one of the music folders (or any of their sub-folders). + * + * @param file The file in question. + * @return Whether the given file is located in one of the music folders. + */ + private boolean isInMusicFolder(File file) { + return getMusicFolderForFile(file) != null; + } + + private MusicFolder getMusicFolderForFile(File file) { + List<MusicFolder> folders = settingsService.getAllMusicFolders(false, true); + String path = file.getPath(); + for (MusicFolder folder : folders) { + if (isFileInFolder(path, folder.getPath().getPath())) { + return folder; + } + } + return null; + } + + /** + * Returns whether the given file is located in the Podcast folder (or any of its sub-folders). + * + * @param file The file in question. + * @return Whether the given file is located in the Podcast folder. + */ + private boolean isInPodcastFolder(File file) { + String podcastFolder = settingsService.getPodcastFolder(); + return isFileInFolder(file.getPath(), podcastFolder); + } + + public String getRootFolderForFile(File file) { + MusicFolder folder = getMusicFolderForFile(file); + if (folder != null) { + return folder.getPath().getPath(); + } + + if (isInPodcastFolder(file)) { + return settingsService.getPodcastFolder(); + } + return null; + } + + /** + * Returns whether the given file is located in the given folder (or any of its sub-folders). + * If the given file contains the expression ".." (indicating a reference to the parent directory), + * this method will return <code>false</code>. + * + * @param file The file in question. + * @param folder The folder in question. + * @return Whether the given file is located in the given folder. + */ + protected boolean isFileInFolder(String file, String folder) { + // Deny access if file contains ".." surrounded by slashes (or end of line). + if (file.matches(".*(/|\\\\)\\.\\.(/|\\\\|$).*")) { + return false; + } + + // Convert slashes. + file = file.replace('\\', '/'); + folder = folder.replace('\\', '/'); + + return file.toUpperCase().startsWith(folder.toUpperCase()); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setUserDao(UserDao userDao) { + this.userDao = userDao; + } + + public void setUserCache(Ehcache userCache) { + this.userCache = userCache; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.java new file mode 100644 index 00000000..4a7f95b4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.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.service; + +import javax.xml.parsers.SAXParser; + +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; + +/** + * Locates services for objects that are not part of the Spring context. + * + * @author Sindre Mehus + */ +@Deprecated +public class ServiceLocator { + + private static SettingsService settingsService; + + private ServiceLocator() { + } + + public static SettingsService getSettingsService() { + return settingsService; + } + + public static void setSettingsService(SettingsService settingsService) { + ServiceLocator.settingsService = settingsService; + } +} + diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java new file mode 100644 index 00000000..afb5cdc7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java @@ -0,0 +1,1254 @@ +/* + 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.service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.HttpConnectionParams; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.AvatarDao; +import net.sourceforge.subsonic.dao.InternetRadioDao; +import net.sourceforge.subsonic.dao.MusicFolderDao; +import net.sourceforge.subsonic.dao.UserDao; +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.util.FileUtil; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +/** + * Provides persistent storage of application settings and preferences. + * + * @author Sindre Mehus + */ +public class SettingsService { + + // Subsonic home directory. + private static final File SUBSONIC_HOME_WINDOWS = new File("c:/subsonic"); + private static final File SUBSONIC_HOME_OTHER = new File("/var/subsonic"); + + // Global settings. + private static final String KEY_INDEX_STRING = "IndexString"; + private static final String KEY_IGNORED_ARTICLES = "IgnoredArticles"; + private static final String KEY_SHORTCUTS = "Shortcuts"; + private static final String KEY_PLAYLIST_FOLDER = "PlaylistFolder"; + private static final String KEY_MUSIC_FILE_TYPES = "MusicFileTypes"; + private static final String KEY_VIDEO_FILE_TYPES = "VideoFileTypes"; + private static final String KEY_COVER_ART_FILE_TYPES = "CoverArtFileTypes"; + private static final String KEY_COVER_ART_LIMIT = "CoverArtLimit"; + private static final String KEY_WELCOME_TITLE = "WelcomeTitle"; + private static final String KEY_WELCOME_SUBTITLE = "WelcomeSubtitle"; + private static final String KEY_WELCOME_MESSAGE = "WelcomeMessage2"; + private static final String KEY_LOGIN_MESSAGE = "LoginMessage"; + private static final String KEY_LOCALE_LANGUAGE = "LocaleLanguage"; + private static final String KEY_LOCALE_COUNTRY = "LocaleCountry"; + private static final String KEY_LOCALE_VARIANT = "LocaleVariant"; + private static final String KEY_THEME_ID = "Theme"; + private static final String KEY_INDEX_CREATION_INTERVAL = "IndexCreationInterval"; + private static final String KEY_INDEX_CREATION_HOUR = "IndexCreationHour"; + private static final String KEY_FAST_CACHE_ENABLED = "FastCacheEnabled"; + private static final String KEY_PODCAST_UPDATE_INTERVAL = "PodcastUpdateInterval"; + private static final String KEY_PODCAST_FOLDER = "PodcastFolder"; + private static final String KEY_PODCAST_EPISODE_RETENTION_COUNT = "PodcastEpisodeRetentionCount"; + private static final String KEY_PODCAST_EPISODE_DOWNLOAD_COUNT = "PodcastEpisodeDownloadCount"; + private static final String KEY_DOWNLOAD_BITRATE_LIMIT = "DownloadBitrateLimit"; + private static final String KEY_UPLOAD_BITRATE_LIMIT = "UploadBitrateLimit"; + private static final String KEY_STREAM_PORT = "StreamPort"; + private static final String KEY_LICENSE_EMAIL = "LicenseEmail"; + private static final String KEY_LICENSE_CODE = "LicenseCode"; + private static final String KEY_LICENSE_DATE = "LicenseDate"; + private static final String KEY_DOWNSAMPLING_COMMAND = "DownsamplingCommand3"; + private static final String KEY_JUKEBOX_COMMAND = "JukeboxCommand"; + private static final String KEY_REWRITE_URL = "RewriteUrl"; + private static final String KEY_LDAP_ENABLED = "LdapEnabled"; + private static final String KEY_LDAP_URL = "LdapUrl"; + private static final String KEY_LDAP_MANAGER_DN = "LdapManagerDn"; + private static final String KEY_LDAP_MANAGER_PASSWORD = "LdapManagerPassword"; + private static final String KEY_LDAP_SEARCH_FILTER = "LdapSearchFilter"; + private static final String KEY_LDAP_AUTO_SHADOWING = "LdapAutoShadowing"; + private static final String KEY_GETTING_STARTED_ENABLED = "GettingStartedEnabled"; + private static final String KEY_PORT_FORWARDING_ENABLED = "PortForwardingEnabled"; + private static final String KEY_PORT = "Port"; + private static final String KEY_HTTPS_PORT = "HttpsPort"; + private static final String KEY_URL_REDIRECTION_ENABLED = "UrlRedirectionEnabled"; + private static final String KEY_URL_REDIRECT_FROM = "UrlRedirectFrom"; + private static final String KEY_URL_REDIRECT_TRIAL_EXPIRES = "UrlRedirectTrialExpires"; + private static final String KEY_URL_REDIRECT_CONTEXT_PATH = "UrlRedirectContextPath"; + private static final String KEY_REST_TRIAL_EXPIRES = "RestTrialExpires-"; + private static final String KEY_VIDEO_TRIAL_EXPIRES = "VideoTrialExpires"; + private static final String KEY_SERVER_ID = "ServerId"; + private static final String KEY_SETTINGS_CHANGED = "SettingsChanged"; + private static final String KEY_LAST_SCANNED = "LastScanned"; + private static final String KEY_ORGANIZE_BY_FOLDER_STRUCTURE = "OrganizeByFolderStructure"; + private static final String KEY_SORT_ALBUMS_BY_YEAR = "SortAlbumsByYear"; + + // Default values. + private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)"; + private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les"; + private static final String DEFAULT_SHORTCUTS = "New Incoming Podcast"; + private static final String DEFAULT_PLAYLIST_FOLDER = Util.getDefaultPlaylistFolder(); + private static final String DEFAULT_MUSIC_FILE_TYPES = "mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn"; + private static final String DEFAULT_VIDEO_FILE_TYPES = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts"; + private static final String DEFAULT_COVER_ART_FILE_TYPES = "cover.jpg folder.jpg jpg jpeg gif png"; + private static final int DEFAULT_COVER_ART_LIMIT = 30; + private static final String DEFAULT_WELCOME_TITLE = "Welcome to Subsonic!"; + private static final String DEFAULT_WELCOME_SUBTITLE = null; + private static final String DEFAULT_WELCOME_MESSAGE = "__Welcome to Subsonic!__\n" + + "\\\\ \\\\\n" + + "Subsonic is a free, web-based media streamer, providing ubiquitous access to your music. \n" + + "\\\\ \\\\\n" + + "Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple " + + "players simultaneously, for instance to one player in your kitchen and another in your living room.\n" + + "\\\\ \\\\\n" + + "To change or remove this message, log in with administrator rights and go to {link:Settings > General|generalSettings.view}."; + private static final String DEFAULT_LOGIN_MESSAGE = null; + private static final String DEFAULT_LOCALE_LANGUAGE = "en"; + private static final String DEFAULT_LOCALE_COUNTRY = ""; + private static final String DEFAULT_LOCALE_VARIANT = ""; + private static final String DEFAULT_THEME_ID = "default"; + private static final int DEFAULT_INDEX_CREATION_INTERVAL = 1; + private static final int DEFAULT_INDEX_CREATION_HOUR = 3; + private static final boolean DEFAULT_FAST_CACHE_ENABLED = false; + private static final int DEFAULT_PODCAST_UPDATE_INTERVAL = 24; + private static final String DEFAULT_PODCAST_FOLDER = Util.getDefaultPodcastFolder(); + private static final int DEFAULT_PODCAST_EPISODE_RETENTION_COUNT = 10; + private static final int DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT = 1; + private static final long DEFAULT_DOWNLOAD_BITRATE_LIMIT = 0; + private static final long DEFAULT_UPLOAD_BITRATE_LIMIT = 0; + private static final long DEFAULT_STREAM_PORT = 0; + private static final String DEFAULT_LICENSE_EMAIL = null; + private static final String DEFAULT_LICENSE_CODE = null; + private static final String DEFAULT_LICENSE_DATE = null; + private static final String DEFAULT_DOWNSAMPLING_COMMAND = "ffmpeg -i %s -ab %bk -v 0 -f mp3 -"; + private static final String DEFAULT_JUKEBOX_COMMAND = "ffmpeg -ss %o -i %s -v 0 -f au -"; + private static final boolean DEFAULT_REWRITE_URL = true; + private static final boolean DEFAULT_LDAP_ENABLED = false; + private static final String DEFAULT_LDAP_URL = "ldap://host.domain.com:389/cn=Users,dc=domain,dc=com"; + private static final String DEFAULT_LDAP_MANAGER_DN = null; + private static final String DEFAULT_LDAP_MANAGER_PASSWORD = null; + private static final String DEFAULT_LDAP_SEARCH_FILTER = "(sAMAccountName={0})"; + private static final boolean DEFAULT_LDAP_AUTO_SHADOWING = false; + private static final boolean DEFAULT_PORT_FORWARDING_ENABLED = false; + private static final boolean DEFAULT_GETTING_STARTED_ENABLED = true; + private static final int DEFAULT_PORT = 80; + private static final int DEFAULT_HTTPS_PORT = 0; + private static final boolean DEFAULT_URL_REDIRECTION_ENABLED = false; + private static final String DEFAULT_URL_REDIRECT_FROM = "yourname"; + private static final String DEFAULT_URL_REDIRECT_TRIAL_EXPIRES = null; + private static final String DEFAULT_URL_REDIRECT_CONTEXT_PATH = null; + private static final String DEFAULT_REST_TRIAL_EXPIRES = null; + private static final String DEFAULT_VIDEO_TRIAL_EXPIRES = null; + private static final String DEFAULT_SERVER_ID = null; + private static final long DEFAULT_SETTINGS_CHANGED = 0L; + private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true; + private static final boolean DEFAULT_SORT_ALBUMS_BY_YEAR = true; + + // Array of obsolete keys. Used to clean property file. + private static final List<String> OBSOLETE_KEYS = Arrays.asList("PortForwardingPublicPort", "PortForwardingLocalPort", + "DownsamplingCommand", "DownsamplingCommand2", "AutoCoverBatch", "MusicMask", "VideoMask", "CoverArtMask"); + + private static final String LOCALES_FILE = "/net/sourceforge/subsonic/i18n/locales.txt"; + private static final String THEMES_FILE = "/net/sourceforge/subsonic/theme/themes.txt"; + + private static final Logger LOG = Logger.getLogger(SettingsService.class); + + private Properties properties = new Properties(); + private List<Theme> themes; + private List<Locale> locales; + private InternetRadioDao internetRadioDao; + private MusicFolderDao musicFolderDao; + private UserDao userDao; + private AvatarDao avatarDao; + private VersionService versionService; + + private String[] cachedCoverArtFileTypesArray; + private String[] cachedMusicFileTypesArray; + private String[] cachedVideoFileTypesArray; + private List<MusicFolder> cachedMusicFolders; + + private static File subsonicHome; + + private boolean licenseValidated = true; + + public SettingsService() { + File propertyFile = getPropertyFile(); + + if (propertyFile.exists()) { + FileInputStream in = null; + try { + in = new FileInputStream(propertyFile); + properties.load(in); + } catch (Exception x) { + LOG.error("Unable to read from property file.", x); + } finally { + IOUtils.closeQuietly(in); + } + + // Remove obsolete properties. + for (Iterator<Object> iterator = properties.keySet().iterator(); iterator.hasNext();) { + String key = (String) iterator.next(); + if (OBSOLETE_KEYS.contains(key)) { + LOG.debug("Removing obsolete property [" + key + ']'); + iterator.remove(); + } + } + } + + save(false); + } + + /** + * Register in service locator so that non-Spring objects can access me. + * This method is invoked automatically by Spring. + */ + public void init() { + ServiceLocator.setSettingsService(this); + validateLicenseAsync(); + } + + public void save() { + save(true); + } + + public void save(boolean updateChangedDate) { + if (updateChangedDate) { + setProperty(KEY_SETTINGS_CHANGED, String.valueOf(System.currentTimeMillis())); + } + + OutputStream out = null; + try { + out = new FileOutputStream(getPropertyFile()); + properties.store(out, "Subsonic preferences. NOTE: This file is automatically generated."); + } catch (Exception x) { + LOG.error("Unable to write to property file.", x); + } finally { + IOUtils.closeQuietly(out); + } + } + + private File getPropertyFile() { + return new File(getSubsonicHome(), "subsonic.properties"); + } + + /** + * Returns the Subsonic home directory. + * + * @return The Subsonic home directory, if it exists. + * @throws RuntimeException If directory doesn't exist. + */ + public static synchronized File getSubsonicHome() { + + if (subsonicHome != null) { + return subsonicHome; + } + + File home; + + String overrideHome = System.getProperty("subsonic.home"); + if (overrideHome != null) { + home = new File(overrideHome); + } else { + boolean isWindows = System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows"); + home = isWindows ? SUBSONIC_HOME_WINDOWS : SUBSONIC_HOME_OTHER; + } + + // Attempt to create home directory if it doesn't exist. + if (!home.exists() || !home.isDirectory()) { + boolean success = home.mkdirs(); + if (success) { + subsonicHome = home; + } else { + String message = "The directory " + home + " does not exist. Please create it and make it writable. " + + "(You can override the directory location by specifying -Dsubsonic.home=... when " + + "starting the servlet container.)"; + System.err.println("ERROR: " + message); + } + } else { + subsonicHome = home; + } + + return home; + } + + private boolean getBoolean(String key, boolean defaultValue) { + return Boolean.valueOf(properties.getProperty(key, String.valueOf(defaultValue))); + } + + private void setBoolean(String key, boolean value) { + setProperty(key, String.valueOf(value)); + } + + public String getIndexString() { + return properties.getProperty(KEY_INDEX_STRING, DEFAULT_INDEX_STRING); + } + + public void setIndexString(String indexString) { + setProperty(KEY_INDEX_STRING, indexString); + } + + public String getIgnoredArticles() { + return properties.getProperty(KEY_IGNORED_ARTICLES, DEFAULT_IGNORED_ARTICLES); + } + + public String[] getIgnoredArticlesAsArray() { + return getIgnoredArticles().split("\\s+"); + } + + public void setIgnoredArticles(String ignoredArticles) { + setProperty(KEY_IGNORED_ARTICLES, ignoredArticles); + } + + public String getShortcuts() { + return properties.getProperty(KEY_SHORTCUTS, DEFAULT_SHORTCUTS); + } + + public String[] getShortcutsAsArray() { + return StringUtil.split(getShortcuts()); + } + + public void setShortcuts(String shortcuts) { + setProperty(KEY_SHORTCUTS, shortcuts); + } + + public String getPlaylistFolder() { + return properties.getProperty(KEY_PLAYLIST_FOLDER, DEFAULT_PLAYLIST_FOLDER); + } + + public String getMusicFileTypes() { + return properties.getProperty(KEY_MUSIC_FILE_TYPES, DEFAULT_MUSIC_FILE_TYPES); + } + + public synchronized void setMusicFileTypes(String fileTypes) { + setProperty(KEY_MUSIC_FILE_TYPES, fileTypes); + cachedMusicFileTypesArray = null; + } + + public synchronized String[] getMusicFileTypesAsArray() { + if (cachedMusicFileTypesArray == null) { + cachedMusicFileTypesArray = toStringArray(getMusicFileTypes()); + } + return cachedMusicFileTypesArray; + } + + public String getVideoFileTypes() { + return properties.getProperty(KEY_VIDEO_FILE_TYPES, DEFAULT_VIDEO_FILE_TYPES); + } + + public synchronized void setVideoFileTypes(String fileTypes) { + setProperty(KEY_VIDEO_FILE_TYPES, fileTypes); + cachedVideoFileTypesArray = null; + } + + public synchronized String[] getVideoFileTypesAsArray() { + if (cachedVideoFileTypesArray == null) { + cachedVideoFileTypesArray = toStringArray(getVideoFileTypes()); + } + return cachedVideoFileTypesArray; + } + + public String getCoverArtFileTypes() { + return properties.getProperty(KEY_COVER_ART_FILE_TYPES, DEFAULT_COVER_ART_FILE_TYPES); + } + + public synchronized void setCoverArtFileTypes(String fileTypes) { + setProperty(KEY_COVER_ART_FILE_TYPES, fileTypes); + cachedCoverArtFileTypesArray = null; + } + + public synchronized String[] getCoverArtFileTypesAsArray() { + if (cachedCoverArtFileTypesArray == null) { + cachedCoverArtFileTypesArray = toStringArray(getCoverArtFileTypes()); + } + return cachedCoverArtFileTypesArray; + } + + public int getCoverArtLimit() { + return Integer.parseInt(properties.getProperty(KEY_COVER_ART_LIMIT, "" + DEFAULT_COVER_ART_LIMIT)); + } + + public void setCoverArtLimit(int limit) { + setProperty(KEY_COVER_ART_LIMIT, "" + limit); + } + + public String getWelcomeTitle() { + return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_TITLE, DEFAULT_WELCOME_TITLE)); + } + + public void setWelcomeTitle(String title) { + setProperty(KEY_WELCOME_TITLE, title); + } + + public String getWelcomeSubtitle() { + return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_SUBTITLE, DEFAULT_WELCOME_SUBTITLE)); + } + + public void setWelcomeSubtitle(String subtitle) { + setProperty(KEY_WELCOME_SUBTITLE, subtitle); + } + + public String getWelcomeMessage() { + return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_MESSAGE, DEFAULT_WELCOME_MESSAGE)); + } + + public void setWelcomeMessage(String message) { + setProperty(KEY_WELCOME_MESSAGE, message); + } + + public String getLoginMessage() { + return StringUtils.trimToNull(properties.getProperty(KEY_LOGIN_MESSAGE, DEFAULT_LOGIN_MESSAGE)); + } + + public void setLoginMessage(String message) { + setProperty(KEY_LOGIN_MESSAGE, message); + } + + /** + * Returns the number of days between automatic index creation, of -1 if automatic index + * creation is disabled. + */ + public int getIndexCreationInterval() { + return Integer.parseInt(properties.getProperty(KEY_INDEX_CREATION_INTERVAL, "" + DEFAULT_INDEX_CREATION_INTERVAL)); + } + + /** + * Sets the number of days between automatic index creation, of -1 if automatic index + * creation is disabled. + */ + public void setIndexCreationInterval(int days) { + setProperty(KEY_INDEX_CREATION_INTERVAL, String.valueOf(days)); + } + + /** + * Returns the hour of day (0 - 23) when automatic index creation should run. + */ + public int getIndexCreationHour() { + return Integer.parseInt(properties.getProperty(KEY_INDEX_CREATION_HOUR, String.valueOf(DEFAULT_INDEX_CREATION_HOUR))); + } + + /** + * Sets the hour of day (0 - 23) when automatic index creation should run. + */ + public void setIndexCreationHour(int hour) { + setProperty(KEY_INDEX_CREATION_HOUR, String.valueOf(hour)); + } + + public boolean isFastCacheEnabled() { + return getBoolean(KEY_FAST_CACHE_ENABLED, DEFAULT_FAST_CACHE_ENABLED); + } + + public void setFastCacheEnabled(boolean enabled) { + setBoolean(KEY_FAST_CACHE_ENABLED, enabled); + } + + /** + * Returns the number of hours between Podcast updates, of -1 if automatic updates + * are disabled. + */ + public int getPodcastUpdateInterval() { + return Integer.parseInt(properties.getProperty(KEY_PODCAST_UPDATE_INTERVAL, String.valueOf(DEFAULT_PODCAST_UPDATE_INTERVAL))); + } + + /** + * Sets the number of hours between Podcast updates, of -1 if automatic updates + * are disabled. + */ + public void setPodcastUpdateInterval(int hours) { + setProperty(KEY_PODCAST_UPDATE_INTERVAL, String.valueOf(hours)); + } + + /** + * Returns the number of Podcast episodes to keep (-1 to keep all). + */ + public int getPodcastEpisodeRetentionCount() { + return Integer.parseInt(properties.getProperty(KEY_PODCAST_EPISODE_RETENTION_COUNT, String.valueOf(DEFAULT_PODCAST_EPISODE_RETENTION_COUNT))); + } + + /** + * Sets the number of Podcast episodes to keep (-1 to keep all). + */ + public void setPodcastEpisodeRetentionCount(int count) { + setProperty(KEY_PODCAST_EPISODE_RETENTION_COUNT, String.valueOf(count)); + } + + /** + * Returns the number of Podcast episodes to download (-1 to download all). + */ + public int getPodcastEpisodeDownloadCount() { + return Integer.parseInt(properties.getProperty(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, String.valueOf(DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT))); + } + + /** + * Sets the number of Podcast episodes to download (-1 to download all). + */ + public void setPodcastEpisodeDownloadCount(int count) { + setProperty(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, String.valueOf(count)); + } + + /** + * Returns the Podcast download folder. + */ + public String getPodcastFolder() { + return properties.getProperty(KEY_PODCAST_FOLDER, DEFAULT_PODCAST_FOLDER); + } + + /** + * Sets the Podcast download folder. + */ + public void setPodcastFolder(String folder) { + setProperty(KEY_PODCAST_FOLDER, folder); + } + + /** + * @return The download bitrate limit in Kbit/s. Zero if unlimited. + */ + public long getDownloadBitrateLimit() { + return Long.parseLong(properties.getProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + DEFAULT_DOWNLOAD_BITRATE_LIMIT)); + } + + /** + * @param limit The download bitrate limit in Kbit/s. Zero if unlimited. + */ + public void setDownloadBitrateLimit(long limit) { + setProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + limit); + } + + /** + * @return The upload bitrate limit in Kbit/s. Zero if unlimited. + */ + public long getUploadBitrateLimit() { + return Long.parseLong(properties.getProperty(KEY_UPLOAD_BITRATE_LIMIT, "" + DEFAULT_UPLOAD_BITRATE_LIMIT)); + } + + /** + * @param limit The upload bitrate limit in Kbit/s. Zero if unlimited. + */ + public void setUploadBitrateLimit(long limit) { + setProperty(KEY_UPLOAD_BITRATE_LIMIT, "" + limit); + } + + /** + * @return The non-SSL stream port. Zero if disabled. + */ + public int getStreamPort() { + return Integer.parseInt(properties.getProperty(KEY_STREAM_PORT, "" + DEFAULT_STREAM_PORT)); + } + + /** + * @param port The non-SSL stream port. Zero if disabled. + */ + public void setStreamPort(int port) { + setProperty(KEY_STREAM_PORT, "" + port); + } + + public String getLicenseEmail() { + return properties.getProperty(KEY_LICENSE_EMAIL, DEFAULT_LICENSE_EMAIL); + } + + public void setLicenseEmail(String email) { + setProperty(KEY_LICENSE_EMAIL, email); + } + + public String getLicenseCode() { + return properties.getProperty(KEY_LICENSE_CODE, DEFAULT_LICENSE_CODE); + } + + public void setLicenseCode(String code) { + setProperty(KEY_LICENSE_CODE, code); + } + + public Date getLicenseDate() { + String value = properties.getProperty(KEY_LICENSE_DATE, DEFAULT_LICENSE_DATE); + return value == null ? null : new Date(Long.parseLong(value)); + } + + public void setLicenseDate(Date date) { + String value = (date == null ? null : String.valueOf(date.getTime())); + setProperty(KEY_LICENSE_DATE, value); + } + + public boolean isLicenseValid() { + return isLicenseValid(getLicenseEmail(), getLicenseCode()) && licenseValidated; + } + + public boolean isLicenseValid(String email, String license) { + if (email == null || license == null) { + return false; + } + return license.equalsIgnoreCase(StringUtil.md5Hex(email.toLowerCase())); + } + + public String getDownsamplingCommand() { + return properties.getProperty(KEY_DOWNSAMPLING_COMMAND, DEFAULT_DOWNSAMPLING_COMMAND); + } + + public void setDownsamplingCommand(String command) { + setProperty(KEY_DOWNSAMPLING_COMMAND, command); + } + + public String getJukeboxCommand() { + return properties.getProperty(KEY_JUKEBOX_COMMAND, DEFAULT_JUKEBOX_COMMAND); + } + + public boolean isRewriteUrlEnabled() { + return getBoolean(KEY_REWRITE_URL, DEFAULT_REWRITE_URL); + } + + public void setRewriteUrlEnabled(boolean rewriteUrl) { + setBoolean(KEY_REWRITE_URL, rewriteUrl); + } + + public boolean isLdapEnabled() { + return getBoolean(KEY_LDAP_ENABLED, DEFAULT_LDAP_ENABLED); + } + + public void setLdapEnabled(boolean ldapEnabled) { + setBoolean(KEY_LDAP_ENABLED, ldapEnabled); + } + + public String getLdapUrl() { + return properties.getProperty(KEY_LDAP_URL, DEFAULT_LDAP_URL); + } + + public void setLdapUrl(String ldapUrl) { + properties.setProperty(KEY_LDAP_URL, ldapUrl); + } + + public String getLdapSearchFilter() { + return properties.getProperty(KEY_LDAP_SEARCH_FILTER, DEFAULT_LDAP_SEARCH_FILTER); + } + + public void setLdapSearchFilter(String ldapSearchFilter) { + properties.setProperty(KEY_LDAP_SEARCH_FILTER, ldapSearchFilter); + } + + public String getLdapManagerDn() { + return properties.getProperty(KEY_LDAP_MANAGER_DN, DEFAULT_LDAP_MANAGER_DN); + } + + public void setLdapManagerDn(String ldapManagerDn) { + properties.setProperty(KEY_LDAP_MANAGER_DN, ldapManagerDn); + } + + public String getLdapManagerPassword() { + String s = properties.getProperty(KEY_LDAP_MANAGER_PASSWORD, DEFAULT_LDAP_MANAGER_PASSWORD); + try { + return StringUtil.utf8HexDecode(s); + } catch (Exception x) { + LOG.warn("Failed to decode LDAP manager password.", x); + return s; + } + } + + public void setLdapManagerPassword(String ldapManagerPassword) { + try { + ldapManagerPassword = StringUtil.utf8HexEncode(ldapManagerPassword); + } catch (Exception x) { + LOG.warn("Failed to encode LDAP manager password.", x); + } + properties.setProperty(KEY_LDAP_MANAGER_PASSWORD, ldapManagerPassword); + } + + public boolean isLdapAutoShadowing() { + return getBoolean(KEY_LDAP_AUTO_SHADOWING, DEFAULT_LDAP_AUTO_SHADOWING); + } + + public void setLdapAutoShadowing(boolean ldapAutoShadowing) { + setBoolean(KEY_LDAP_AUTO_SHADOWING, ldapAutoShadowing); + } + + public boolean isGettingStartedEnabled() { + return getBoolean(KEY_GETTING_STARTED_ENABLED, DEFAULT_GETTING_STARTED_ENABLED); + } + + public void setGettingStartedEnabled(boolean isGettingStartedEnabled) { + setBoolean(KEY_GETTING_STARTED_ENABLED, isGettingStartedEnabled); + } + + public boolean isPortForwardingEnabled() { + return getBoolean(KEY_PORT_FORWARDING_ENABLED, DEFAULT_PORT_FORWARDING_ENABLED); + } + + public void setPortForwardingEnabled(boolean isPortForwardingEnabled) { + setBoolean(KEY_PORT_FORWARDING_ENABLED, isPortForwardingEnabled); + } + + public int getPort() { + return Integer.valueOf(properties.getProperty(KEY_PORT, String.valueOf(DEFAULT_PORT))); + } + + public void setPort(int port) { + setProperty(KEY_PORT, String.valueOf(port)); + } + + public int getHttpsPort() { + return Integer.valueOf(properties.getProperty(KEY_HTTPS_PORT, String.valueOf(DEFAULT_HTTPS_PORT))); + } + + public void setHttpsPort(int httpsPort) { + setProperty(KEY_HTTPS_PORT, String.valueOf(httpsPort)); + } + + public boolean isUrlRedirectionEnabled() { + return getBoolean(KEY_URL_REDIRECTION_ENABLED, DEFAULT_URL_REDIRECTION_ENABLED); + } + + public void setUrlRedirectionEnabled(boolean isUrlRedirectionEnabled) { + setBoolean(KEY_URL_REDIRECTION_ENABLED, isUrlRedirectionEnabled); + } + + public String getUrlRedirectFrom() { + return properties.getProperty(KEY_URL_REDIRECT_FROM, DEFAULT_URL_REDIRECT_FROM); + } + + public void setUrlRedirectFrom(String urlRedirectFrom) { + properties.setProperty(KEY_URL_REDIRECT_FROM, urlRedirectFrom); + } + + public Date getUrlRedirectTrialExpires() { + String value = properties.getProperty(KEY_URL_REDIRECT_TRIAL_EXPIRES, DEFAULT_URL_REDIRECT_TRIAL_EXPIRES); + return value == null ? null : new Date(Long.parseLong(value)); + } + + public void setUrlRedirectTrialExpires(Date date) { + String value = (date == null ? null : String.valueOf(date.getTime())); + setProperty(KEY_URL_REDIRECT_TRIAL_EXPIRES, value); + } + + public Date getVideoTrialExpires() { + String value = properties.getProperty(KEY_VIDEO_TRIAL_EXPIRES, DEFAULT_VIDEO_TRIAL_EXPIRES); + return value == null ? null : new Date(Long.parseLong(value)); + } + + public void setVideoTrialExpires(Date date) { + String value = (date == null ? null : String.valueOf(date.getTime())); + setProperty(KEY_VIDEO_TRIAL_EXPIRES, value); + } + + public String getUrlRedirectContextPath() { + return properties.getProperty(KEY_URL_REDIRECT_CONTEXT_PATH, DEFAULT_URL_REDIRECT_CONTEXT_PATH); + } + + public void setUrlRedirectContextPath(String contextPath) { + properties.setProperty(KEY_URL_REDIRECT_CONTEXT_PATH, contextPath); + } + + public Date getRESTTrialExpires(String client) { + String value = properties.getProperty(KEY_REST_TRIAL_EXPIRES + client, DEFAULT_REST_TRIAL_EXPIRES); + return value == null ? null : new Date(Long.parseLong(value)); + } + + public void setRESTTrialExpires(String client, Date date) { + String value = (date == null ? null : String.valueOf(date.getTime())); + setProperty(KEY_REST_TRIAL_EXPIRES + client, value); + } + + public String getServerId() { + return properties.getProperty(KEY_SERVER_ID, DEFAULT_SERVER_ID); + } + + public void setServerId(String serverId) { + properties.setProperty(KEY_SERVER_ID, serverId); + } + + public long getSettingsChanged() { + return Long.parseLong(properties.getProperty(KEY_SETTINGS_CHANGED, String.valueOf(DEFAULT_SETTINGS_CHANGED))); + } + + public Date getLastScanned() { + String lastScanned = properties.getProperty(KEY_LAST_SCANNED); + return lastScanned == null ? null : new Date(Long.parseLong(lastScanned)); + } + + public void setLastScanned(Date date) { + if (date == null) { + properties.remove(KEY_LAST_SCANNED); + } else { + properties.setProperty(KEY_LAST_SCANNED, String.valueOf(date.getTime())); + } + } + + public boolean isOrganizeByFolderStructure() { + return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE); + } + + public void setOrganizeByFolderStructure(boolean b) { + setBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, b); + } + + public boolean isSortAlbumsByYear() { + return getBoolean(KEY_SORT_ALBUMS_BY_YEAR, DEFAULT_SORT_ALBUMS_BY_YEAR); + } + + public void setSortAlbumsByYear(boolean b) { + setBoolean(KEY_SORT_ALBUMS_BY_YEAR, b); + } + + /** + * Returns the locale (for language, date format etc). + * + * @return The locale. + */ + public Locale getLocale() { + String language = properties.getProperty(KEY_LOCALE_LANGUAGE, DEFAULT_LOCALE_LANGUAGE); + String country = properties.getProperty(KEY_LOCALE_COUNTRY, DEFAULT_LOCALE_COUNTRY); + String variant = properties.getProperty(KEY_LOCALE_VARIANT, DEFAULT_LOCALE_VARIANT); + + return new Locale(language, country, variant); + } + + /** + * Sets the locale (for language, date format etc.) + * + * @param locale The locale. + */ + public void setLocale(Locale locale) { + setProperty(KEY_LOCALE_LANGUAGE, locale.getLanguage()); + setProperty(KEY_LOCALE_COUNTRY, locale.getCountry()); + setProperty(KEY_LOCALE_VARIANT, locale.getVariant()); + } + + /** + * Returns the ID of the theme to use. + * + * @return The theme ID. + */ + public String getThemeId() { + return properties.getProperty(KEY_THEME_ID, DEFAULT_THEME_ID); + } + + /** + * Sets the ID of the theme to use. + * + * @param themeId The theme ID + */ + public void setThemeId(String themeId) { + setProperty(KEY_THEME_ID, themeId); + } + + /** + * Returns a list of available themes. + * + * @return A list of available themes. + */ + public synchronized Theme[] getAvailableThemes() { + if (themes == null) { + themes = new ArrayList<Theme>(); + try { + InputStream in = SettingsService.class.getResourceAsStream(THEMES_FILE); + String[] lines = StringUtil.readLines(in); + for (String line : lines) { + String[] elements = StringUtil.split(line); + if (elements.length == 2) { + themes.add(new Theme(elements[0], elements[1])); + } else { + LOG.warn("Failed to parse theme from line: [" + line + "]."); + } + } + } catch (IOException x) { + LOG.error("Failed to resolve list of themes.", x); + themes.add(new Theme("default", "Subsonic default")); + } + } + return themes.toArray(new Theme[themes.size()]); + } + + /** + * Returns a list of available locales. + * + * @return A list of available locales. + */ + public synchronized Locale[] getAvailableLocales() { + if (locales == null) { + locales = new ArrayList<Locale>(); + try { + InputStream in = SettingsService.class.getResourceAsStream(LOCALES_FILE); + String[] lines = StringUtil.readLines(in); + + for (String line : lines) { + locales.add(parseLocale(line)); + } + + } catch (IOException x) { + LOG.error("Failed to resolve list of locales.", x); + locales.add(Locale.ENGLISH); + } + } + return locales.toArray(new Locale[locales.size()]); + } + + private Locale parseLocale(String line) { + String[] s = line.split("_"); + String language = s[0]; + String country = ""; + String variant = ""; + + if (s.length > 1) { + country = s[1]; + } + if (s.length > 2) { + variant = s[2]; + } + return new Locale(language, country, variant); + } + + /** + * Returns the "brand" name. Normally, this is just "Subsonic". + * + * @return The brand name. + */ + public String getBrand() { + return "Subsonic"; + } + + /** + * Returns all music folders. Non-existing and disabled folders are not included. + * + * @return Possibly empty list of all music folders. + */ + public List<MusicFolder> getAllMusicFolders() { + return getAllMusicFolders(false, false); + } + + /** + * Returns all music folders. + * + * @param includeDisabled Whether to include disabled folders. + * @param includeNonExisting Whether to include non-existing folders. + * @return Possibly empty list of all music folders. + */ + public List<MusicFolder> getAllMusicFolders(boolean includeDisabled, boolean includeNonExisting) { + if (cachedMusicFolders == null) { + cachedMusicFolders = musicFolderDao.getAllMusicFolders(); + } + + List<MusicFolder> result = new ArrayList<MusicFolder>(cachedMusicFolders.size()); + for (MusicFolder folder : cachedMusicFolders) { + if ((includeDisabled || folder.isEnabled()) && (includeNonExisting || FileUtil.exists(folder.getPath()))) { + result.add(folder); + } + } + return result; + } + + /** + * Returns the music folder with the given ID. + * + * @param id The ID. + * @return The music folder with the given ID, or <code>null</code> if not found. + */ + public MusicFolder getMusicFolderById(Integer id) { + List<MusicFolder> all = getAllMusicFolders(); + for (MusicFolder folder : all) { + if (id.equals(folder.getId())) { + return folder; + } + } + return null; + } + + /** + * Creates a new music folder. + * + * @param musicFolder The music folder to create. + */ + public void createMusicFolder(MusicFolder musicFolder) { + musicFolderDao.createMusicFolder(musicFolder); + cachedMusicFolders = null; + } + + /** + * Deletes the music folder with the given ID. + * + * @param id The ID of the music folder to delete. + */ + public void deleteMusicFolder(Integer id) { + musicFolderDao.deleteMusicFolder(id); + cachedMusicFolders = null; + } + + /** + * Updates the given music folder. + * + * @param musicFolder The music folder to update. + */ + public void updateMusicFolder(MusicFolder musicFolder) { + musicFolderDao.updateMusicFolder(musicFolder); + cachedMusicFolders = null; + } + + /** + * Returns all internet radio stations. Disabled stations are not returned. + * + * @return Possibly empty list of all internet radio stations. + */ + public List<InternetRadio> getAllInternetRadios() { + return getAllInternetRadios(false); + } + + /** + * Returns the internet radio station with the given ID. + * + * @param id The ID. + * @return The internet radio station with the given ID, or <code>null</code> if not found. + */ + public InternetRadio getInternetRadioById(Integer id) { + for (InternetRadio radio : getAllInternetRadios()) { + if (id.equals(radio.getId())) { + return radio; + } + } + return null; + } + + /** + * Returns all internet radio stations. + * + * @param includeAll Whether disabled stations should be included. + * @return Possibly empty list of all internet radio stations. + */ + public List<InternetRadio> getAllInternetRadios(boolean includeAll) { + List<InternetRadio> all = internetRadioDao.getAllInternetRadios(); + List<InternetRadio> result = new ArrayList<InternetRadio>(all.size()); + for (InternetRadio folder : all) { + if (includeAll || folder.isEnabled()) { + result.add(folder); + } + } + return result; + } + + /** + * Creates a new internet radio station. + * + * @param radio The internet radio station to create. + */ + public void createInternetRadio(InternetRadio radio) { + internetRadioDao.createInternetRadio(radio); + } + + /** + * Deletes the internet radio station with the given ID. + * + * @param id The internet radio station ID. + */ + public void deleteInternetRadio(Integer id) { + internetRadioDao.deleteInternetRadio(id); + } + + /** + * Updates the given internet radio station. + * + * @param radio The internet radio station to update. + */ + public void updateInternetRadio(InternetRadio radio) { + internetRadioDao.updateInternetRadio(radio); + } + + /** + * Returns settings for the given user. + * + * @param username The username. + * @return User-specific settings. Never <code>null</code>. + */ + public UserSettings getUserSettings(String username) { + UserSettings settings = userDao.getUserSettings(username); + return settings == null ? createDefaultUserSettings(username) : settings; + } + + private UserSettings createDefaultUserSettings(String username) { + UserSettings settings = new UserSettings(username); + settings.setFinalVersionNotificationEnabled(true); + settings.setBetaVersionNotificationEnabled(false); + settings.setShowNowPlayingEnabled(true); + settings.setShowChatEnabled(true); + settings.setPartyModeEnabled(false); + settings.setNowPlayingAllowed(true); + settings.setLastFmEnabled(false); + settings.setLastFmUsername(null); + settings.setLastFmPassword(null); + settings.setChanged(new Date()); + + UserSettings.Visibility playlist = settings.getPlaylistVisibility(); + playlist.setCaptionCutoff(35); + playlist.setArtistVisible(true); + playlist.setAlbumVisible(true); + playlist.setYearVisible(true); + playlist.setDurationVisible(true); + playlist.setBitRateVisible(true); + playlist.setFormatVisible(true); + playlist.setFileSizeVisible(true); + + UserSettings.Visibility main = settings.getMainVisibility(); + main.setCaptionCutoff(35); + main.setTrackNumberVisible(true); + main.setArtistVisible(true); + main.setDurationVisible(true); + + return settings; + } + + /** + * Updates settings for the given username. + * + * @param settings The user-specific settings. + */ + public void updateUserSettings(UserSettings settings) { + userDao.updateUserSettings(settings); + } + + /** + * Returns all system avatars. + * + * @return All system avatars. + */ + public List<Avatar> getAllSystemAvatars() { + return avatarDao.getAllSystemAvatars(); + } + + /** + * Returns the system avatar with the given ID. + * + * @param id The system avatar ID. + * @return The avatar or <code>null</code> if not found. + */ + public Avatar getSystemAvatar(int id) { + return avatarDao.getSystemAvatar(id); + } + + /** + * Returns the custom avatar for the given user. + * + * @param username The username. + * @return The avatar or <code>null</code> if not found. + */ + public Avatar getCustomAvatar(String username) { + return avatarDao.getCustomAvatar(username); + } + + /** + * Sets the custom avatar for the given user. + * + * @param avatar The avatar, or <code>null</code> to remove the avatar. + * @param username The username. + */ + public void setCustomAvatar(Avatar avatar, String username) { + avatarDao.setCustomAvatar(avatar, username); + } + + private void setProperty(String key, String value) { + if (value == null) { + properties.remove(key); + } else { + properties.setProperty(key, value); + } + } + + private String[] toStringArray(String s) { + List<String> result = new ArrayList<String>(); + StringTokenizer tokenizer = new StringTokenizer(s, " "); + while (tokenizer.hasMoreTokens()) { + result.add(tokenizer.nextToken()); + } + + return result.toArray(new String[result.size()]); + } + + private void validateLicense() { + String email = getLicenseEmail(); + Date date = getLicenseDate(); + + if (email == null || date == null) { + licenseValidated = false; + return; + } + + licenseValidated = true; + + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 120000); + HttpConnectionParams.setSoTimeout(client.getParams(), 120000); + HttpGet method = new HttpGet("http://subsonic.org/backend/validateLicense.view" + "?email=" + StringUtil.urlEncode(email) + + "&date=" + date.getTime() + "&version=" + versionService.getLocalVersion()); + try { + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + String content = client.execute(method, responseHandler); + licenseValidated = content != null && content.contains("true"); + if (!licenseValidated) { + LOG.warn("License key is not valid."); + } + } catch (Throwable x) { + LOG.warn("Failed to validate license.", x); + } finally { + client.getConnectionManager().shutdown(); + } + } + + public void validateLicenseAsync() { + new Thread() { + @Override + public void run() { + validateLicense(); + } + }.start(); + } + + public void setInternetRadioDao(InternetRadioDao internetRadioDao) { + this.internetRadioDao = internetRadioDao; + } + + public void setMusicFolderDao(MusicFolderDao musicFolderDao) { + this.musicFolderDao = musicFolderDao; + } + + public void setUserDao(UserDao userDao) { + this.userDao = userDao; + } + + public void setAvatarDao(AvatarDao avatarDao) { + this.avatarDao = avatarDao; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java new file mode 100644 index 00000000..cf5860e6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.java @@ -0,0 +1,133 @@ +/* + 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.service; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import net.sourceforge.subsonic.domain.MediaFile; +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.RandomStringUtils; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.ShareDao; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.domain.User; + +/** + * Provides services for sharing media. + * + * @author Sindre Mehus + * @see Share + */ +public class ShareService { + + private static final Logger LOG = Logger.getLogger(ShareService.class); + + private ShareDao shareDao; + private SecurityService securityService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + + public List<Share> getAllShares() { + return shareDao.getAllShares(); + } + + public List<Share> getSharesForUser(User user) { + List<Share> result = new ArrayList<Share>(); + for (Share share : getAllShares()) { + if (user.isAdminRole() || ObjectUtils.equals(user.getUsername(), share.getUsername())) { + result.add(share); + } + } + return result; + } + + public Share getShareById(int id) { + return shareDao.getShareById(id); + } + + public List<MediaFile> getSharedFiles(int id) { + List<MediaFile> result = new ArrayList<MediaFile>(); + for (String path : shareDao.getSharedFiles(id)) { + try { + result.add(mediaFileService.getMediaFile(path)); + } catch (Exception x) { + // Ignored + } + } + return result; + } + + public Share createShare(HttpServletRequest request, List<MediaFile> files) throws Exception { + + Share share = new Share(); + share.setName(RandomStringUtils.random(5, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")); + share.setCreated(new Date()); + share.setUsername(securityService.getCurrentUsername(request)); + + Calendar expires = Calendar.getInstance(); + expires.add(Calendar.YEAR, 1); + share.setExpires(expires.getTime()); + + shareDao.createShare(share); + for (MediaFile file : files) { + shareDao.createSharedFiles(share.getId(), file.getPath()); + } + LOG.info("Created share '" + share.getName() + "' with " + files.size() + " file(s)."); + + return share; + } + + public void updateShare(Share share) { + shareDao.updateShare(share); + } + + public void deleteShare(int id) { + shareDao.deleteShare(id); + } + + public String getShareBaseUrl() { + return "http://" + settingsService.getUrlRedirectFrom() + ".subsonic.org/share/"; + } + + public String getShareUrl(Share share) { + return getShareBaseUrl() + share.getName(); + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setShareDao(ShareDao shareDao) { + this.shareDao = shareDao; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + 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/service/StatusService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.java new file mode 100644 index 00000000..a893166a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.java @@ -0,0 +1,134 @@ +/* + 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.service; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Provides services for maintaining the list of stream, download and upload statuses. + * <p/> + * Note that for stream statuses, the last inactive status is also stored. + * + * @author Sindre Mehus + * @see TransferStatus + */ +public class StatusService { + + private final List<TransferStatus> streamStatuses = new ArrayList<TransferStatus>(); + private final List<TransferStatus> downloadStatuses = new ArrayList<TransferStatus>(); + private final List<TransferStatus> uploadStatuses = new ArrayList<TransferStatus>(); + + // Maps from player ID to latest inactive stream status. + private final Map<String, TransferStatus> inactiveStreamStatuses = new LinkedHashMap<String, TransferStatus>(); + + public synchronized TransferStatus createStreamStatus(Player player) { + // Reuse existing status, if possible. + TransferStatus status = inactiveStreamStatuses.get(player.getId()); + if (status != null) { + status.setActive(true); + } else { + status = createStatus(player, streamStatuses); + } + return status; + } + + public synchronized void removeStreamStatus(TransferStatus status) { + // Move it to the map of inactive statuses. + status.setActive(false); + inactiveStreamStatuses.put(status.getPlayer().getId(), status); + streamStatuses.remove(status); + } + + public synchronized List<TransferStatus> getAllStreamStatuses() { + + List<TransferStatus> result = new ArrayList<TransferStatus>(streamStatuses); + + // Add inactive status for those players that have no active status. + Set<String> activePlayers = new HashSet<String>(); + for (TransferStatus status : streamStatuses) { + activePlayers.add(status.getPlayer().getId()); + } + + for (Map.Entry<String, TransferStatus> entry : inactiveStreamStatuses.entrySet()) { + if (!activePlayers.contains(entry.getKey())) { + result.add(entry.getValue()); + } + } + return result; + } + + public synchronized List<TransferStatus> getStreamStatusesForPlayer(Player player) { + List<TransferStatus> result = new ArrayList<TransferStatus>(); + for (TransferStatus status : streamStatuses) { + if (status.getPlayer().getId().equals(player.getId())) { + result.add(status); + } + } + + // If no active statuses exists, add the inactive one. + if (result.isEmpty()) { + TransferStatus inactiveStatus = inactiveStreamStatuses.get(player.getId()); + if (inactiveStatus != null) { + result.add(inactiveStatus); + } + } + + return result; + } + + public synchronized TransferStatus createDownloadStatus(Player player) { + return createStatus(player, downloadStatuses); + } + + public synchronized void removeDownloadStatus(TransferStatus status) { + downloadStatuses.remove(status); + } + + public synchronized List<TransferStatus> getAllDownloadStatuses() { + return new ArrayList<TransferStatus>(downloadStatuses); + } + + public synchronized TransferStatus createUploadStatus(Player player) { + return createStatus(player, uploadStatuses); + } + + public synchronized void removeUploadStatus(TransferStatus status) { + uploadStatuses.remove(status); + } + + public synchronized List<TransferStatus> getAllUploadStatuses() { + return new ArrayList<TransferStatus>(uploadStatuses); + } + + private synchronized TransferStatus createStatus(Player player, List<TransferStatus> statusList) { + TransferStatus status = new TransferStatus(); + status.setPlayer(player); + statusList.add(status); + return status; + } + +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java new file mode 100644 index 00000000..2c8b9c5e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java @@ -0,0 +1,530 @@ +/* + 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.service; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.controller.VideoPlayerController; +import net.sourceforge.subsonic.dao.TranscodingDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.domain.VideoTranscodingSettings; +import net.sourceforge.subsonic.io.TranscodeInputStream; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.filefilter.PrefixFileFilter; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +/** + * Provides services for transcoding media. Transcoding is the process of + * converting an audio stream to a different format and/or bit rate. The latter is + * also called downsampling. + * + * @author Sindre Mehus + * @see TranscodeInputStream + */ +public class TranscodingService { + + private static final Logger LOG = Logger.getLogger(TranscodingService.class); + + private TranscodingDao transcodingDao; + private SettingsService settingsService; + private PlayerService playerService; + + /** + * Returns all transcodings. + * + * @return Possibly empty list of all transcodings. + */ + public List<Transcoding> getAllTranscodings() { + return transcodingDao.getAllTranscodings(); + } + + /** + * Returns all active transcodings for the given player. Only enabled transcodings are returned. + * + * @param player The player. + * @return All active transcodings for the player. + */ + public List<Transcoding> getTranscodingsForPlayer(Player player) { + return transcodingDao.getTranscodingsForPlayer(player.getId()); + } + + /** + * Sets the list of active transcodings for the given player. + * + * @param player The player. + * @param transcodingIds ID's of the active transcodings. + */ + public void setTranscodingsForPlayer(Player player, int[] transcodingIds) { + transcodingDao.setTranscodingsForPlayer(player.getId(), transcodingIds); + } + + /** + * Sets the list of active transcodings for the given player. + * + * @param player The player. + * @param transcodings The active transcodings. + */ + public void setTranscodingsForPlayer(Player player, List<Transcoding> transcodings) { + int[] transcodingIds = new int[transcodings.size()]; + for (int i = 0; i < transcodingIds.length; i++) { + transcodingIds[i] = transcodings.get(i).getId(); + } + setTranscodingsForPlayer(player, transcodingIds); + } + + + /** + * Creates a new transcoding. + * + * @param transcoding The transcoding to create. + */ + public void createTranscoding(Transcoding transcoding) { + transcodingDao.createTranscoding(transcoding); + + // Activate this transcoding for all players? + if (transcoding.isDefaultActive()) { + for (Player player : playerService.getAllPlayers()) { + List<Transcoding> transcodings = getTranscodingsForPlayer(player); + transcodings.add(transcoding); + setTranscodingsForPlayer(player, transcodings); + } + } + } + + /** + * Deletes the transcoding with the given ID. + * + * @param id The transcoding ID. + */ + public void deleteTranscoding(Integer id) { + transcodingDao.deleteTranscoding(id); + } + + /** + * Updates the given transcoding. + * + * @param transcoding The transcoding to update. + */ + public void updateTranscoding(Transcoding transcoding) { + transcodingDao.updateTranscoding(transcoding); + } + + /** + * Returns whether transcoding is required for the given media file and player combination. + * + * @param mediaFile The media file. + * @param player The player. + * @return Whether transcoding will be performed if invoking the + * {@link #getTranscodedInputStream} method with the same arguments. + */ + public boolean isTranscodingRequired(MediaFile mediaFile, Player player) { + return getTranscoding(mediaFile, player, null) != null; + } + + /** + * Returns the suffix for the given player and media file, taking transcodings into account. + * + * @param player The player in question. + * @param file The media file. + * @param preferredTargetFormat Used to select among multiple applicable transcodings. May be {@code null}. + * @return The file suffix, e.g., "mp3". + */ + public String getSuffix(Player player, MediaFile file, String preferredTargetFormat) { + Transcoding transcoding = getTranscoding(file, player, preferredTargetFormat); + return transcoding != null ? transcoding.getTargetFormat() : file.getFormat(); + } + + /** + * Creates parameters for a possibly transcoded or downsampled input stream for the given media file and player combination. + * <p/> + * A transcoding is applied if it is applicable for the format of the given file, and is activated for the + * given player. + * <p/> + * If no transcoding is applicable, the file may still be downsampled, given that the player is configured + * with a bit rate limit which is higher than the actual bit rate of the file. + * <p/> + * Otherwise, a normal input stream to the original file is returned. + * + * @param mediaFile The media file. + * @param player The player. + * @param maxBitRate Overrides the per-player and per-user bitrate limit. May be {@code null}. + * @param preferredTargetFormat Used to select among multiple applicable transcodings. May be {@code null}. + * @param videoTranscodingSettings Parameters used when transcoding video. May be {@code null}. + * @return Parameters to be used in the {@link #getTranscodedInputStream} method. + */ + public Parameters getParameters(MediaFile mediaFile, Player player, Integer maxBitRate, String preferredTargetFormat, + VideoTranscodingSettings videoTranscodingSettings) { + + Parameters parameters = new Parameters(mediaFile, videoTranscodingSettings); + + TranscodeScheme transcodeScheme = getTranscodeScheme(player); + if (maxBitRate == null && transcodeScheme != TranscodeScheme.OFF) { + maxBitRate = transcodeScheme.getMaxBitRate(); + } + + Transcoding transcoding = getTranscoding(mediaFile, player, preferredTargetFormat); + if (transcoding != null) { + parameters.setTranscoding(transcoding); + if (maxBitRate == null) { + maxBitRate = mediaFile.isVideo() ? VideoPlayerController.DEFAULT_BIT_RATE : 128; + } + } else if (maxBitRate != null) { + boolean supported = isDownsamplingSupported(mediaFile); + Integer bitRate = mediaFile.getBitRate(); + if (supported && bitRate != null && bitRate > maxBitRate) { + parameters.setDownsample(true); + } + } + + parameters.setMaxBitRate(maxBitRate); + return parameters; + } + + /** + * Returns a possibly transcoded or downsampled input stream for the given music file and player combination. + * <p/> + * A transcoding is applied if it is applicable for the format of the given file, and is activated for the + * given player. + * <p/> + * If no transcoding is applicable, the file may still be downsampled, given that the player is configured + * with a bit rate limit which is higher than the actual bit rate of the file. + * <p/> + * Otherwise, a normal input stream to the original file is returned. + * + * @param parameters As returned by {@link #getParameters}. + * @return A possible transcoded or downsampled input stream. + * @throws IOException If an I/O error occurs. + */ + public InputStream getTranscodedInputStream(Parameters parameters) throws IOException { + try { + + if (parameters.getTranscoding() != null) { + return createTranscodedInputStream(parameters); + } + + if (parameters.downsample) { + return createDownsampledInputStream(parameters); + } + + } catch (Exception x) { + LOG.warn("Failed to transcode " + parameters.getMediaFile() + ". Using original.", x); + } + + return new FileInputStream(parameters.getMediaFile().getFile()); + } + + + /** + * Returns the strictest transcoding scheme defined for the player and the user. + */ + private TranscodeScheme getTranscodeScheme(Player player) { + String username = player.getUsername(); + if (username != null) { + UserSettings userSettings = settingsService.getUserSettings(username); + return player.getTranscodeScheme().strictest(userSettings.getTranscodeScheme()); + } + + return player.getTranscodeScheme(); + } + + /** + * Returns an input stream by applying the given transcoding to the given music file. + * + * @param parameters Transcoding parameters. + * @return The transcoded input stream. + * @throws IOException If an I/O error occurs. + */ + private InputStream createTranscodedInputStream(Parameters parameters) + throws IOException { + + Transcoding transcoding = parameters.getTranscoding(); + Integer maxBitRate = parameters.getMaxBitRate(); + VideoTranscodingSettings videoTranscodingSettings = parameters.getVideoTranscodingSettings(); + MediaFile mediaFile = parameters.getMediaFile(); + + TranscodeInputStream in = createTranscodeInputStream(transcoding.getStep1(), maxBitRate, videoTranscodingSettings, mediaFile, null); + + if (transcoding.getStep2() != null) { + in = createTranscodeInputStream(transcoding.getStep2(), maxBitRate, videoTranscodingSettings, mediaFile, in); + } + + if (transcoding.getStep3() != null) { + in = createTranscodeInputStream(transcoding.getStep3(), maxBitRate, videoTranscodingSettings, mediaFile, in); + } + + return in; + } + + /** + * Creates a transcoded input stream by interpreting the given command line string. + * This includes the following: + * <ul> + * <li>Splitting the command line string to an array.</li> + * <li>Replacing occurrences of "%s" with the path of the given music file.</li> + * <li>Replacing occurrences of "%t" with the title of the given music file.</li> + * <li>Replacing occurrences of "%l" with the album name of the given music file.</li> + * <li>Replacing occurrences of "%a" with the artist name of the given music file.</li> + * <li>Replacing occurrcences of "%b" with the max bitrate.</li> + * <li>Replacing occurrcences of "%o" with the video time offset (used for scrubbing).</li> + * <li>Replacing occurrcences of "%w" with the video image width.</li> + * <li>Replacing occurrcences of "%h" with the video image height.</li> + * <li>Prepending the path of the transcoder directory if the transcoder is found there.</li> + * </ul> + * + * @param command The command line string. + * @param maxBitRate The maximum bitrate to use. May not be {@code null}. + * @param videoTranscodingSettings Parameters used when transcoding video. May be {@code null}. + * @param mediaFile The media file. + * @param in Data to feed to the process. May be {@code null}. @return The newly created input stream. + */ + private TranscodeInputStream createTranscodeInputStream(String command, Integer maxBitRate, + VideoTranscodingSettings videoTranscodingSettings, MediaFile mediaFile, InputStream in) throws IOException { + + String title = mediaFile.getTitle(); + String album = mediaFile.getAlbumName(); + String artist = mediaFile.getArtist(); + + if (title == null) { + title = "Unknown Song"; + } + if (album == null) { + title = "Unknown Album"; + } + if (artist == null) { + title = "Unknown Artist"; + } + + List<String> result = new LinkedList<String>(Arrays.asList(StringUtil.split(command))); + result.set(0, getTranscodeDirectory().getPath() + File.separatorChar + result.get(0)); + + File tmpFile = null; + + for (int i = 1; i < result.size(); i++) { + String cmd = result.get(i); + if (cmd.contains("%b")) { + cmd = cmd.replace("%b", String.valueOf(maxBitRate)); + } + if (cmd.contains("%t")) { + cmd = cmd.replace("%t", title); + } + if (cmd.contains("%l")) { + cmd = cmd.replace("%l", album); + } + if (cmd.contains("%a")) { + cmd = cmd.replace("%a", artist); + } + if (cmd.contains("%o") && videoTranscodingSettings != null) { + cmd = cmd.replace("%o", String.valueOf(videoTranscodingSettings.getTimeOffset())); + } + if (cmd.contains("%w") && videoTranscodingSettings != null) { + cmd = cmd.replace("%w", String.valueOf(videoTranscodingSettings.getWidth())); + } + if (cmd.contains("%h") && videoTranscodingSettings != null) { + cmd = cmd.replace("%h", String.valueOf(videoTranscodingSettings.getHeight())); + } + if (cmd.contains("%s")) { + + // Work-around for filename character encoding problem on Windows. + // Create temporary file, and feed this to the transcoder. + String path = mediaFile.getFile().getAbsolutePath(); + if (Util.isWindows() && !mediaFile.isVideo() && !StringUtils.isAsciiPrintable(path)) { + tmpFile = File.createTempFile("subsonic", "." + FilenameUtils.getExtension(path)); + tmpFile.deleteOnExit(); + FileUtils.copyFile(new File(path), tmpFile); + LOG.debug("Created tmp file: " + tmpFile); + cmd = cmd.replace("%s", tmpFile.getPath()); + } else { + cmd = cmd.replace("%s", path); + } + } + + result.set(i, cmd); + } + return new TranscodeInputStream(new ProcessBuilder(result), in, tmpFile); + } + + /** + * Returns an applicable transcoding for the given file and player, or <code>null</code> if no + * transcoding should be done. + */ + private Transcoding getTranscoding(MediaFile mediaFile, Player player, String preferredTargetFormat) { + + List<Transcoding> applicableTranscodings = new LinkedList<Transcoding>(); + String suffix = mediaFile.getFormat(); + + for (Transcoding transcoding : getTranscodingsForPlayer(player)) { + for (String sourceFormat : transcoding.getSourceFormatsAsArray()) { + if (sourceFormat.equalsIgnoreCase(suffix)) { + if (isTranscodingInstalled(transcoding)) { + applicableTranscodings.add(transcoding); + } + } + } + } + + if (applicableTranscodings.isEmpty()) { + return null; + } + + for (Transcoding transcoding : applicableTranscodings) { + if (transcoding.getTargetFormat().equalsIgnoreCase(preferredTargetFormat)) { + return transcoding; + } + } + + return applicableTranscodings.get(0); + } + + /** + * Returns a downsampled input stream to the music file. + * + * @param parameters Downsample parameters. + * @throws IOException If an I/O error occurs. + */ + private InputStream createDownsampledInputStream(Parameters parameters) throws IOException { + String command = settingsService.getDownsamplingCommand(); + return createTranscodeInputStream(command, parameters.getMaxBitRate(), parameters.getVideoTranscodingSettings(), + parameters.getMediaFile(), null); + } + + /** + * Returns whether downsampling is supported (i.e., whether LAME is installed or not.) + * + * @param mediaFile If not null, returns whether downsampling is supported for this file. + * @return Whether downsampling is supported. + */ + public boolean isDownsamplingSupported(MediaFile mediaFile) { + if (mediaFile != null) { + boolean isMp3 = "mp3".equalsIgnoreCase(mediaFile.getFormat()); + if (!isMp3) { + return false; + } + } + + String commandLine = settingsService.getDownsamplingCommand(); + return isTranscodingStepInstalled(commandLine); + } + + private boolean isTranscodingInstalled(Transcoding transcoding) { + return isTranscodingStepInstalled(transcoding.getStep1()) && + isTranscodingStepInstalled(transcoding.getStep2()) && + isTranscodingStepInstalled(transcoding.getStep3()); + } + + private boolean isTranscodingStepInstalled(String step) { + if (StringUtils.isEmpty(step)) { + return true; + } + String executable = StringUtil.split(step)[0]; + PrefixFileFilter filter = new PrefixFileFilter(executable); + String[] matches = getTranscodeDirectory().list(filter); + return matches != null && matches.length > 0; + } + + /** + * Returns the directory in which all transcoders are installed. + */ + public File getTranscodeDirectory() { + File dir = new File(SettingsService.getSubsonicHome(), "transcode"); + if (!dir.exists()) { + boolean ok = dir.mkdir(); + if (ok) { + LOG.info("Created directory " + dir); + } else { + LOG.warn("Failed to create directory " + dir); + } + } + return dir; + } + + public void setTranscodingDao(TranscodingDao transcodingDao) { + this.transcodingDao = transcodingDao; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public static class Parameters { + private boolean downsample; + private final MediaFile mediaFile; + private final VideoTranscodingSettings videoTranscodingSettings; + private Integer maxBitRate; + private Transcoding transcoding; + + public Parameters(MediaFile mediaFile, VideoTranscodingSettings videoTranscodingSettings) { + this.mediaFile = mediaFile; + this.videoTranscodingSettings = videoTranscodingSettings; + } + + public void setMaxBitRate(Integer maxBitRate) { + this.maxBitRate = maxBitRate; + } + + public boolean isDownsample() { + return downsample; + } + + public void setDownsample(boolean downsample) { + this.downsample = downsample; + } + + public boolean isTranscode() { + return transcoding != null; + } + + public void setTranscoding(Transcoding transcoding) { + this.transcoding = transcoding; + } + + public Transcoding getTranscoding() { + return transcoding; + } + + public MediaFile getMediaFile() { + return mediaFile; + } + + public Integer getMaxBitRate() { + return maxBitRate; + } + + public VideoTranscodingSettings getVideoTranscodingSettings() { + return videoTranscodingSettings; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java new file mode 100644 index 00000000..e24e6409 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java @@ -0,0 +1,267 @@ +/* + 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.service; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Version; +import org.apache.commons.io.IOUtils; +import org.apache.http.client.HttpClient; +import org.apache.http.client.ResponseHandler; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.params.HttpConnectionParams; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.StringReader; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Provides version-related services, including functionality for determining whether a newer + * version of Subsonic is available. + * + * @author Sindre Mehus + */ +public class VersionService { + + private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); + private static final Logger LOG = Logger.getLogger(VersionService.class); + + private Version localVersion; + private Version latestFinalVersion; + private Version latestBetaVersion; + private Date localBuildDate; + private String localBuildNumber; + + /** + * Time when latest version was fetched (in milliseconds). + */ + private long lastVersionFetched; + + /** + * Only fetch last version this often (in milliseconds.). + */ + private static final long LAST_VERSION_FETCH_INTERVAL = 7L * 24L * 3600L * 1000L; // One week + + /** + * URL from which to fetch latest versions. + */ + private static final String VERSION_URL = "http://subsonic.org/backend/version.view"; + + /** + * Returns the version number for the locally installed Subsonic version. + * + * @return The version number for the locally installed Subsonic version. + */ + public synchronized Version getLocalVersion() { + if (localVersion == null) { + try { + localVersion = new Version(readLineFromResource("/version.txt")); + LOG.info("Resolved local Subsonic version to: " + localVersion); + } catch (Exception x) { + LOG.warn("Failed to resolve local Subsonic version.", x); + } + } + return localVersion; + } + + /** + * Returns the version number for the latest available Subsonic final version. + * + * @return The version number for the latest available Subsonic final version, or <code>null</code> + * if the version number can't be resolved. + */ + public synchronized Version getLatestFinalVersion() { + refreshLatestVersion(); + return latestFinalVersion; + } + + /** + * Returns the version number for the latest available Subsonic beta version. + * + * @return The version number for the latest available Subsonic beta version, or <code>null</code> + * if the version number can't be resolved. + */ + public synchronized Version getLatestBetaVersion() { + refreshLatestVersion(); + return latestBetaVersion; + } + + /** + * Returns the build date for the locally installed Subsonic version. + * + * @return The build date for the locally installed Subsonic version, or <code>null</code> + * if the build date can't be resolved. + */ + public synchronized Date getLocalBuildDate() { + if (localBuildDate == null) { + try { + String date = readLineFromResource("/build_date.txt"); + localBuildDate = DATE_FORMAT.parse(date); + } catch (Exception x) { + LOG.warn("Failed to resolve local Subsonic build date.", x); + } + } + return localBuildDate; + } + + /** + * Returns the build number for the locally installed Subsonic version. + * + * @return The build number for the locally installed Subsonic version, or <code>null</code> + * if the build number can't be resolved. + */ + public synchronized String getLocalBuildNumber() { + if (localBuildNumber == null) { + try { + localBuildNumber = readLineFromResource("/build_number.txt"); + } catch (Exception x) { + LOG.warn("Failed to resolve local Subsonic build number.", x); + } + } + return localBuildNumber; + } + + /** + * Returns whether a new final version of Subsonic is available. + * + * @return Whether a new final version of Subsonic is available. + */ + public boolean isNewFinalVersionAvailable() { + Version latest = getLatestFinalVersion(); + Version local = getLocalVersion(); + + if (latest == null || local == null) { + return false; + } + + return local.compareTo(latest) < 0; + } + + /** + * Returns whether a new beta version of Subsonic is available. + * + * @return Whether a new beta version of Subsonic is available. + */ + public boolean isNewBetaVersionAvailable() { + Version latest = getLatestBetaVersion(); + Version local = getLocalVersion(); + + if (latest == null || local == null) { + return false; + } + + return local.compareTo(latest) < 0; + } + + /** + * Reads the first line from the resource with the given name. + * + * @param resourceName The resource name. + * @return The first line of the resource. + */ + private String readLineFromResource(String resourceName) { + InputStream in = VersionService.class.getResourceAsStream(resourceName); + if (in == null) { + return null; + } + BufferedReader reader = null; + try { + + reader = new BufferedReader(new InputStreamReader(in)); + return reader.readLine(); + + } catch (IOException x) { + return null; + } finally { + IOUtils.closeQuietly(reader); + IOUtils.closeQuietly(in); + } + } + + /** + * Refreshes the latest final and beta versions. + */ + private void refreshLatestVersion() { + long now = System.currentTimeMillis(); + boolean isOutdated = now - lastVersionFetched > LAST_VERSION_FETCH_INTERVAL; + + if (isOutdated) { + try { + lastVersionFetched = now; + readLatestVersion(); + } catch (Exception x) { + LOG.warn("Failed to resolve latest Subsonic version.", x); + } + } + } + + /** + * Resolves the latest available Subsonic version by screen-scraping a web page. + * + * @throws IOException If an I/O error occurs. + */ + private void readLatestVersion() throws IOException { + + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000); + HttpConnectionParams.setSoTimeout(client.getParams(), 10000); + HttpGet method = new HttpGet(VERSION_URL + "?v=" + getLocalVersion()); + String content; + try { + + ResponseHandler<String> responseHandler = new BasicResponseHandler(); + content = client.execute(method, responseHandler); + + } finally { + client.getConnectionManager().shutdown(); + } + + BufferedReader reader = new BufferedReader(new StringReader(content)); + Pattern finalPattern = Pattern.compile("SUBSONIC_FULL_VERSION_BEGIN(.*)SUBSONIC_FULL_VERSION_END"); + Pattern betaPattern = Pattern.compile("SUBSONIC_BETA_VERSION_BEGIN(.*)SUBSONIC_BETA_VERSION_END"); + + try { + String line = reader.readLine(); + while (line != null) { + Matcher finalMatcher = finalPattern.matcher(line); + if (finalMatcher.find()) { + latestFinalVersion = new Version(finalMatcher.group(1)); + LOG.info("Resolved latest Subsonic final version to: " + latestFinalVersion); + } + Matcher betaMatcher = betaPattern.matcher(line); + if (betaMatcher.find()) { + latestBetaVersion = new Version(betaMatcher.group(1)); + LOG.info("Resolved latest Subsonic beta version to: " + latestBetaVersion); + } + line = reader.readLine(); + } + + } finally { + reader.close(); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java new file mode 100644 index 00000000..902387be --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.java @@ -0,0 +1,207 @@ +/* + 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.service.jukebox; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.util.concurrent.atomic.AtomicReference; + +import javax.sound.sampled.AudioFormat; +import javax.sound.sampled.AudioSystem; +import javax.sound.sampled.FloatControl; +import javax.sound.sampled.SourceDataLine; + +import org.apache.commons.io.IOUtils; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.service.JukeboxService; + +import static net.sourceforge.subsonic.service.jukebox.AudioPlayer.State.*; + +/** + * A simple wrapper for playing sound from an input stream. + * <p/> + * Supports pause and resume, but not restarting. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class AudioPlayer { + + private static final Logger LOG = Logger.getLogger(JukeboxService.class); + + private final InputStream in; + private final Listener listener; + private final SourceDataLine line; + private final AtomicReference<State> state = new AtomicReference<State>(PAUSED); + private FloatControl gainControl; + + public AudioPlayer(InputStream in, Listener listener) throws Exception { + this.in = new BufferedInputStream(in); + this.listener = listener; + + AudioFormat format = AudioSystem.getAudioFileFormat(this.in).getFormat(); + line = AudioSystem.getSourceDataLine(format); + line.open(format); + LOG.debug("Opened line " + line); + + if (line.isControlSupported(FloatControl.Type.MASTER_GAIN)) { + gainControl = (FloatControl) line.getControl(FloatControl.Type.MASTER_GAIN); + setGain(0.5f); + } + new AudioDataWriter(); + } + + /** + * Starts (or resumes) the player. This only has effect if the current state is + * {@link State#PAUSED}. + */ + public synchronized void play() { + if (state.get() == PAUSED) { + line.start(); + setState(PLAYING); + } + } + + /** + * Pauses the player. This only has effect if the current state is + * {@link State#PLAYING}. + */ + public synchronized void pause() { + if (state.get() == PLAYING) { + setState(PAUSED); + line.stop(); + line.flush(); + } + } + + /** + * Closes the player, releasing all resources. After this the player state is + * {@link State#CLOSED} (unless the current state is {@link State#EOM}). + */ + public synchronized void close() { + if (state.get() != CLOSED && state.get() != EOM) { + setState(CLOSED); + } + + try { + line.stop(); + } catch (Throwable x) { + LOG.warn("Failed to stop player: " + x, x); + } + try { + if (line.isOpen()) { + line.close(); + LOG.debug("Closed line " + line); + } + } catch (Throwable x) { + LOG.warn("Failed to close player: " + x, x); + } + IOUtils.closeQuietly(in); + } + + /** + * Returns the player state. + */ + public State getState() { + return state.get(); + } + + /** + * Sets the gain. + * + * @param gain The gain between 0.0 and 1.0. + */ + public void setGain(float gain) { + if (gainControl != null) { + + double minGainDB = gainControl.getMinimum(); + double maxGainDB = gainControl.getMaximum(); + double ampGainDB = 0.5f * maxGainDB - minGainDB; + double cste = Math.log(10.0) / 20; + double valueDB = minGainDB + (1 / cste) * Math.log(1 + (Math.exp(cste * ampGainDB) - 1) * gain); + + valueDB = Math.min(valueDB, maxGainDB); + valueDB = Math.max(valueDB, minGainDB); + + gainControl.setValue((float) valueDB); + } + } + + /** + * Returns the position in seconds. + */ + public int getPosition() { + return (int) (line.getMicrosecondPosition() / 1000000L); + } + + private void setState(State state) { + if (this.state.getAndSet(state) != state && listener != null) { + listener.stateChanged(this, state); + } + } + + private class AudioDataWriter implements Runnable { + + public AudioDataWriter() { + new Thread(this).start(); + } + + public void run() { + try { + byte[] buffer = new byte[8192]; + + while (true) { + + switch (state.get()) { + case CLOSED: + case EOM: + return; + case PAUSED: + Thread.sleep(250); + break; + case PLAYING: + int n = in.read(buffer); + if (n == -1) { + setState(EOM); + return; + } + line.write(buffer, 0, n); + break; + } + } + } catch (Throwable x) { + LOG.warn("Error when copying audio data: " + x, x); + } finally { + close(); + } + } + } + + public interface Listener { + void stateChanged(AudioPlayer player, State state); + } + + public static enum State { + PAUSED, + PLAYING, + CLOSED, + EOM + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java new file mode 100644 index 00000000..30ed2847 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java @@ -0,0 +1,75 @@ +package net.sourceforge.subsonic.service.jukebox; + +import java.awt.FlowLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.FileInputStream; + +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JSlider; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class PlayerTest implements AudioPlayer.Listener { + + private AudioPlayer player; + + public PlayerTest() throws Exception { + player = new AudioPlayer(new FileInputStream("i:\\tmp\\foo.au"), this); + createGUI(); + } + + private void createGUI() { + JFrame frame = new JFrame(); + + JButton startButton = new JButton("Start"); + JButton stopButton = new JButton("Stop"); + JButton resetButton = new JButton("Reset"); + final JSlider gainSlider = new JSlider(0, 1000); + + startButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + player.play(); + } + }); + stopButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + player.pause(); + } + }); + resetButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + player.close(); + } + }); + gainSlider.addChangeListener(new ChangeListener() { + public void stateChanged(ChangeEvent e) { + float gain = (float) gainSlider.getValue() / 1000.0F; + player.setGain(gain); + } + }); + + frame.setLayout(new FlowLayout()); + frame.add(startButton); + frame.add(stopButton); + frame.add(resetButton); + frame.add(gainSlider); + + frame.pack(); + frame.setVisible(true); + } + + public static void main(String[] args) throws Exception { + new PlayerTest(); + } + + public void stateChanged(AudioPlayer player, AudioPlayer.State state) { + System.out.println(state); + } +} + diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.java new file mode 100644 index 00000000..897f39d4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.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.service.metadata; + +import java.io.File; + +import net.sourceforge.subsonic.domain.MediaFile; + +/** + * Parses meta data by guessing artist, album and song title based on the path of the file. + * + * @author Sindre Mehus + */ +public class DefaultMetaDataParser extends MetaDataParser { + + /** + * Parses meta data for the given file. No guessing or reformatting is done. + * + * @param file The file to parse. + * @return Meta data for the file. + */ + public MetaData getRawMetaData(File file) { + MetaData metaData = new MetaData(); + metaData.setArtist(guessArtist(file)); + metaData.setAlbumName(guessAlbum(file, metaData.getArtist())); + metaData.setTitle(guessTitle(file)); + return metaData; + } + + /** + * Updates the given file with the given meta data. + * This method has no effect. + * + * @param file The file to update. + * @param metaData The new meta data. + */ + public void setMetaData(MediaFile file, MetaData metaData) { + } + + /** + * Returns whether this parser supports tag editing (using the {@link #setMetaData} method). + * + * @return Always false. + */ + public boolean isEditingSupported() { + return false; + } + + /** + * Returns whether this parser is applicable to the given file. + * + * @param file The file in question. + * @return Whether this parser is applicable to the given file. + */ + public boolean isApplicable(File file) { + return file.isFile(); + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java new file mode 100644 index 00000000..60ae1750 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/FFmpegParser.java @@ -0,0 +1,170 @@ +/* + 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.service.metadata; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.io.InputStreamReaderThread; +import net.sourceforge.subsonic.service.ServiceLocator; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.commons.io.FilenameUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses meta data from video files using FFmpeg (http://ffmpeg.org/). + * <p/> + * Currently duration, bitrate and dimension are supported. + * + * @author Sindre Mehus + */ +public class FFmpegParser extends MetaDataParser { + + private static final Logger LOG = Logger.getLogger(FFmpegParser.class); + private static final Pattern DURATION_PATTERN = Pattern.compile("Duration: (\\d+):(\\d+):(\\d+).(\\d+)"); + private static final Pattern BITRATE_PATTERN = Pattern.compile("bitrate: (\\d+) kb/s"); + private static final Pattern DIMENSION_PATTERN = Pattern.compile("Video.*?, (\\d+)x(\\d+)"); + private static final Pattern PAR_PATTERN = Pattern.compile("PAR (\\d+):(\\d+)"); + + private TranscodingService transcodingService; + + /** + * Parses meta data for the given music file. No guessing or reformatting is done. + * + * + * @param file The music file to parse. + * @return Meta data for the file. + */ + @Override + public MetaData getRawMetaData(File file) { + + MetaData metaData = new MetaData(); + + try { + + File ffmpeg = new File(transcodingService.getTranscodeDirectory(), "ffmpeg"); + + String[] command = new String[]{ffmpeg.getAbsolutePath(), "-i", file.getAbsolutePath()}; + Process process = Runtime.getRuntime().exec(command); + InputStream stdout = process.getInputStream(); + InputStream stderr = process.getErrorStream(); + + // Consume stdout, we're not interested in that. + new InputStreamReaderThread(stdout, "ffmpeg", true).start(); + + // Read everything from stderr. It will contain text similar to: + // Input #0, avi, from 'foo.avi': + // Duration: 00:00:33.90, start: 0.000000, bitrate: 2225 kb/s + // Stream #0.0: Video: mpeg4, yuv420p, 352x240 [PAR 1:1 DAR 22:15], 29.97 fps, 29.97 tbr, 29.97 tbn, 30k tbc + // Stream #0.1: Audio: pcm_s16le, 44100 Hz, 2 channels, s16, 1411 kb/s + String[] lines = StringUtil.readLines(stderr); + + Integer width = null; + Integer height = null; + Double par = 1.0; + for (String line : lines) { + + Matcher matcher = DURATION_PATTERN.matcher(line); + if (matcher.find()) { + int hours = Integer.parseInt(matcher.group(1)); + int minutes = Integer.parseInt(matcher.group(2)); + int seconds = Integer.parseInt(matcher.group(3)); + metaData.setDurationSeconds(hours * 3600 + minutes * 60 + seconds); + } + + matcher = BITRATE_PATTERN.matcher(line); + if (matcher.find()) { + metaData.setBitRate(Integer.valueOf(matcher.group(1))); + } + + matcher = DIMENSION_PATTERN.matcher(line); + if (matcher.find()) { + width = Integer.valueOf(matcher.group(1)); + height = Integer.valueOf(matcher.group(2)); + } + + // PAR = Pixel Aspect Rate + matcher = PAR_PATTERN.matcher(line); + if (matcher.find()) { + int a = Integer.parseInt(matcher.group(1)); + int b = Integer.parseInt(matcher.group(2)); + if (a > 0 && b > 0) { + par = (double) a / (double) b; + } + } + } + + if (width != null && height != null) { + width = (int) Math.round(width.doubleValue() * par); + metaData.setWidth(width); + metaData.setHeight(height); + } + + + } catch (Throwable x) { + LOG.warn("Error when parsing metadata in " + file, x); + } + + return metaData; + } + + /** + * Not supported. + */ + @Override + public void setMetaData(MediaFile file, MetaData metaData) { + throw new RuntimeException("setMetaData() not supported in " + getClass().getSimpleName()); + } + + /** + * Returns whether this parser supports tag editing (using the {@link #setMetaData} method). + * + * @return Always false. + */ + @Override + public boolean isEditingSupported() { + return false; + } + + /** + * Returns whether this parser is applicable to the given file. + * + * @param file The file in question. + * @return Whether this parser is applicable to the given file. + */ + @Override + public boolean isApplicable(File file) { + String format = FilenameUtils.getExtension(file.getName()).toLowerCase(); + + for (String s : ServiceLocator.getSettingsService().getVideoFileTypesAsArray()) { + if (format.equals(s)) { + return true; + } + } + return false; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java new file mode 100644 index 00000000..8fa7659a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java @@ -0,0 +1,296 @@ +/* + 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.service.metadata; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.MediaFile; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.jaudiotagger.audio.AudioFile; +import org.jaudiotagger.audio.AudioFileIO; +import org.jaudiotagger.audio.AudioHeader; +import org.jaudiotagger.tag.FieldKey; +import org.jaudiotagger.tag.Tag; +import org.jaudiotagger.tag.datatype.Artwork; +import org.jaudiotagger.tag.reference.GenreTypes; + +import java.io.File; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.logging.LogManager; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses meta data from audio files using the Jaudiotagger library + * (http://www.jthink.net/jaudiotagger/) + * + * @author Sindre Mehus + */ +public class JaudiotaggerParser extends MetaDataParser { + + private static final Logger LOG = Logger.getLogger(JaudiotaggerParser.class); + private static final Pattern GENRE_PATTERN = Pattern.compile("\\((\\d+)\\).*"); + private static final Pattern TRACK_NUMBER_PATTERN = Pattern.compile("(\\d+)/\\d+"); + + static { + try { + LogManager.getLogManager().reset(); + } catch (Throwable x) { + LOG.warn("Failed to turn off logging from Jaudiotagger.", x); + } + } + + /** + * Parses meta data for the given music file. No guessing or reformatting is done. + * + * + * @param file The music file to parse. + * @return Meta data for the file. + */ + @Override + public MetaData getRawMetaData(File file) { + + MetaData metaData = new MetaData(); + + try { + AudioFile audioFile = AudioFileIO.read(file); + Tag tag = audioFile.getTag(); + if (tag != null) { + metaData.setAlbumName(getTagField(tag, FieldKey.ALBUM)); + metaData.setTitle(getTagField(tag, FieldKey.TITLE)); + metaData.setYear(parseInteger(getTagField(tag, FieldKey.YEAR))); + metaData.setGenre(mapGenre(getTagField(tag, FieldKey.GENRE))); + metaData.setDiscNumber(parseInteger(getTagField(tag, FieldKey.DISC_NO))); + metaData.setTrackNumber(parseTrackNumber(getTagField(tag, FieldKey.TRACK))); + + String songArtist = getTagField(tag, FieldKey.ARTIST); + String albumArtist = getTagField(tag, FieldKey.ALBUM_ARTIST); + metaData.setArtist(StringUtils.isBlank(albumArtist) ? songArtist : albumArtist); + } + + AudioHeader audioHeader = audioFile.getAudioHeader(); + if (audioHeader != null) { + metaData.setVariableBitRate(audioHeader.isVariableBitRate()); + metaData.setBitRate((int) audioHeader.getBitRateAsNumber()); + metaData.setDurationSeconds(audioHeader.getTrackLength()); + } + + + } catch (Throwable x) { + LOG.warn("Error when parsing tags in " + file, x); + } + + return metaData; + } + + private String getTagField(Tag tag, FieldKey fieldKey) { + try { + return StringUtils.trimToNull(tag.getFirst(fieldKey)); + } catch (Exception x) { + // Ignored. + return null; + } + } + + /** + * Returns all tags supported by id3v1. + */ + public static SortedSet<String> getID3V1Genres() { + return new TreeSet<String>(GenreTypes.getInstanceOf().getAlphabeticalValueList()); + } + + /** + * Sometimes the genre is returned as "(17)" or "(17)Rock", instead of "Rock". This method + * maps the genre ID to the corresponding text. + */ + private String mapGenre(String genre) { + if (genre == null) { + return null; + } + Matcher matcher = GENRE_PATTERN.matcher(genre); + if (matcher.matches()) { + int genreId = Integer.parseInt(matcher.group(1)); + if (genreId >= 0 && genreId < GenreTypes.getInstanceOf().getSize()) { + return GenreTypes.getInstanceOf().getValueForId(genreId); + } + } + return genre; + } + + /** + * Parses the track number from the given string. Also supports + * track numbers on the form "4/12". + */ + private Integer parseTrackNumber(String trackNumber) { + if (trackNumber == null) { + return null; + } + + Integer result = null; + + try { + result = new Integer(trackNumber); + } catch (NumberFormatException x) { + Matcher matcher = TRACK_NUMBER_PATTERN.matcher(trackNumber); + if (matcher.matches()) { + try { + result = Integer.valueOf(matcher.group(1)); + } catch (NumberFormatException e) { + return null; + } + } + } + + if (Integer.valueOf(0).equals(result)) { + return null; + } + return result; + } + + private Integer parseInteger(String s) { + s = StringUtils.trimToNull(s); + if (s == null) { + return null; + } + try { + Integer result = Integer.valueOf(s); + if (Integer.valueOf(0).equals(result)) { + return null; + } + return result; + } catch (NumberFormatException x) { + return null; + } + } + + /** + * Updates the given file with the given meta data. + * + * @param file The music file to update. + * @param metaData The new meta data. + */ + @Override + public void setMetaData(MediaFile file, MetaData metaData) { + + try { + AudioFile audioFile = AudioFileIO.read(file.getFile()); + Tag tag = audioFile.getTagOrCreateAndSetDefault(); + + tag.setField(FieldKey.ARTIST, StringUtils.trimToEmpty(metaData.getArtist())); + tag.setField(FieldKey.ALBUM_ARTIST, StringUtils.trimToEmpty(metaData.getArtist())); + tag.setField(FieldKey.ALBUM, StringUtils.trimToEmpty(metaData.getAlbumName())); + tag.setField(FieldKey.TITLE, StringUtils.trimToEmpty(metaData.getTitle())); + tag.setField(FieldKey.GENRE, StringUtils.trimToEmpty(metaData.getGenre())); + + Integer track = metaData.getTrackNumber(); + if (track == null) { + tag.deleteField(FieldKey.TRACK); + } else { + tag.setField(FieldKey.TRACK, String.valueOf(track)); + } + + Integer year = metaData.getYear(); + if (year == null) { + tag.deleteField(FieldKey.YEAR); + } else { + tag.setField(FieldKey.YEAR, String.valueOf(year)); + } + + audioFile.commit(); + + } catch (Throwable x) { + LOG.warn("Failed to update tags for file " + file, x); + throw new RuntimeException("Failed to update tags for file " + file + ". " + x.getMessage(), x); + } + } + + /** + * Returns whether this parser supports tag editing (using the {@link #setMetaData} method). + * + * @return Always true. + */ + @Override + public boolean isEditingSupported() { + return true; + } + + /** + * Returns whether this parser is applicable to the given file. + * + * @param file The music file in question. + * @return Whether this parser is applicable to the given file. + */ + @Override + public boolean isApplicable(File file) { + if (!file.isFile()) { + return false; + } + + String format = FilenameUtils.getExtension(file.getName()).toLowerCase(); + + return format.equals("mp3") || + format.equals("m4a") || + format.equals("aac") || + format.equals("ogg") || + format.equals("flac") || + format.equals("wav") || + format.equals("mpc") || + format.equals("mp+") || + format.equals("ape") || + format.equals("wma"); + } + + /** + * Returns whether cover art image data is available in the given file. + * + * @param file The music file. + * @return Whether cover art image data is available. + */ + public boolean isImageAvailable(MediaFile file) { + try { + return getArtwork(file) != null; + } catch (Throwable x) { + LOG.warn("Failed to find cover art tag in " + file, x); + return false; + } + } + + /** + * Returns the cover art image data embedded in the given file. + * + * @param file The music file. + * @return The embedded cover art image data, or <code>null</code> if not available. + */ + public byte[] getImageData(MediaFile file) { + try { + return getArtwork(file).getBinaryData(); + } catch (Throwable x) { + LOG.warn("Failed to find cover art tag in " + file, x); + return null; + } + } + + private Artwork getArtwork(MediaFile file) throws Exception { + AudioFile audioFile = AudioFileIO.read(file.getFile()); + Tag tag = audioFile.getTag(); + return tag == null ? null : tag.getFirstArtwork(); + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java new file mode 100644 index 00000000..d3fa08a0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java @@ -0,0 +1,135 @@ +/* + 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.service.metadata; + +/** + * Contains meta-data (song title, artist, album etc) for a music file. + * @author Sindre Mehus + */ +public class MetaData { + + private Integer discNumber; + private Integer trackNumber; + private String title; + private String artist; + private String albumName; + private String genre; + private Integer year; + private Integer bitRate; + private boolean variableBitRate; + private Integer durationSeconds; + private Integer width; + private Integer height; + + public Integer getDiscNumber() { + return discNumber; + } + + public void setDiscNumber(Integer discNumber) { + this.discNumber = discNumber; + } + + public Integer getTrackNumber() { + return trackNumber; + } + + public void setTrackNumber(Integer trackNumber) { + this.trackNumber = trackNumber; + } + + 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 getAlbumName() { + return albumName; + } + + public void setAlbumName(String albumName) { + this.albumName = albumName; + } + + public String getGenre() { + return genre; + } + + public void setGenre(String genre) { + this.genre = genre; + } + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getBitRate() { + return bitRate; + } + + public void setBitRate(Integer bitRate) { + this.bitRate = bitRate; + } + + public boolean getVariableBitRate() { + return variableBitRate; + } + + public void setVariableBitRate(boolean variableBitRate) { + this.variableBitRate = variableBitRate; + } + + public Integer getDurationSeconds() { + return durationSeconds; + } + + public void setDurationSeconds(Integer durationSeconds) { + this.durationSeconds = durationSeconds; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + public Integer getHeight() { + return height; + } + + public void setHeight(Integer height) { + this.height = height; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java new file mode 100644 index 00000000..2ed70acc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.java @@ -0,0 +1,162 @@ +/* + 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.service.metadata; + +import java.io.File; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.service.ServiceLocator; +import net.sourceforge.subsonic.service.SettingsService; + + +/** + * Parses meta data from media files. + * + * @author Sindre Mehus + */ +public abstract class MetaDataParser { + + /** + * Parses meta data for the given file. + * + * @param file The file to parse. + * @return Meta data for the file, never null. + */ + public MetaData getMetaData(File file) { + + MetaData metaData = getRawMetaData(file); + String artist = metaData.getArtist(); + String album = metaData.getAlbumName(); + String title = metaData.getTitle(); + + if (artist == null) { + artist = guessArtist(file); + } + if (album == null) { + album = guessAlbum(file, artist); + } + if (title == null) { + title = guessTitle(file); + } + + title = removeTrackNumberFromTitle(title, metaData.getTrackNumber()); + metaData.setArtist(artist); + metaData.setAlbumName(album); + metaData.setTitle(title); + + return metaData; + } + + /** + * Parses meta data for the given file. No guessing or reformatting is done. + * + * + * @param file The file to parse. + * @return Meta data for the file. + */ + public abstract MetaData getRawMetaData(File file); + + /** + * Updates the given file with the given meta data. + * + * @param file The file to update. + * @param metaData The new meta data. + */ + public abstract void setMetaData(MediaFile file, MetaData metaData); + + /** + * Returns whether this parser is applicable to the given file. + * + * @param file The file in question. + * @return Whether this parser is applicable to the given file. + */ + public abstract boolean isApplicable(File file); + + /** + * Returns whether this parser supports tag editing (using the {@link #setMetaData} method). + * + * @return Whether tag editing is supported. + */ + public abstract boolean isEditingSupported(); + + /** + * Guesses the artist for the given file. + */ + public String guessArtist(File file) { + File parent = file.getParentFile(); + if (isRoot(parent)) { + return null; + } + File grandParent = parent.getParentFile(); + return isRoot(grandParent) ? null : grandParent.getName(); + } + + /** + * Guesses the album for the given file. + */ + public String guessAlbum(File file, String artist) { + File parent = file.getParentFile(); + String album = isRoot(parent) ? null : parent.getName(); + if (artist != null && album != null) { + album = album.replace(artist + " - ", ""); + } + return album; + } + + /** + * Guesses the title for the given file. + */ + public String guessTitle(File file) { + return removeTrackNumberFromTitle(FilenameUtils.getBaseName(file.getPath()), null); + } + + private boolean isRoot(File file) { + SettingsService settings = ServiceLocator.getSettingsService(); + List<MusicFolder> folders = settings.getAllMusicFolders(false, true); + for (MusicFolder folder : folders) { + if (file.equals(folder.getPath())) { + return true; + } + } + return false; + } + + /** + * Removes any prefixed track number from the given title string. + * + * @param title The title with or without a prefixed track number, e.g., "02 - Back In Black". + * @param trackNumber If specified, this is the "true" track number. + * @return The title with the track number removed, e.g., "Back In Black". + */ + protected String removeTrackNumberFromTitle(String title, Integer trackNumber) { + title = title.trim(); + + // Don't remove numbers if true track number is given, and title does not start with it. + if (trackNumber != null && !title.matches("0?" + trackNumber + "[\\.\\- ].*")) { + return title; + } + + String result = title.replaceFirst("^\\d{2}[\\.\\- ]+", ""); + return result.length() == 0 ? title : result; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java new file mode 100644 index 00000000..31b56be4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParserFactory.java @@ -0,0 +1,51 @@ +/* + 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.service.metadata; + +import java.io.File; +import java.util.List; + +/** + * Factory for creating meta-data parsers. + * + * @author Sindre Mehus + */ +public class MetaDataParserFactory { + + private List<MetaDataParser> parsers; + + public void setParsers(List<MetaDataParser> parsers) { + this.parsers = parsers; + } + + /** + * Returns a meta-data parser for the given file. + * + * @param file The file in question. + * @return An applicable parser, or <code>null</code> if no parser is found. + */ + public MetaDataParser getParser(File file) { + for (MetaDataParser parser : parsers) { + if (parser.isApplicable(file)) { + return parser; + } + } + return null; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.java new file mode 100644 index 00000000..c7c09677 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/EscapeJavaScriptTag.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.taglib; + +import org.apache.commons.lang.StringEscapeUtils; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspTagException; +import javax.servlet.jsp.tagext.BodyTagSupport; +import java.io.IOException; + +/** + * Escapes the characters in a <code>String</code> using JavaScript String rules. + * <p/> + * Escapes any values it finds into their JavaScript String form. + * Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) + * <p/> + * So a tab becomes the characters <code>'\\'</code> and + * <code>'t'</code>. + * <p/> + * The only difference between Java strings and JavaScript strings + * is that in JavaScript, a single quote must be escaped. + * <p/> + * Example: + * <pre> + * input string: He didn't say, "Stop!" + * output string: He didn\'t say, \"Stop!\" + * </pre> + * + * @author Sindre Mehus + */ +public class EscapeJavaScriptTag extends BodyTagSupport { + + private String string; + + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + public int doEndTag() throws JspException { + try { + pageContext.getOut().print(StringEscapeUtils.escapeJavaScript(string)); + } catch (IOException x) { + throw new JspTagException(x); + } + return EVAL_PAGE; + } + + public void release() { + string = null; + super.release(); + } + + public String getString() { + return string; + } + + public void setString(String string) { + this.string = string; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java new file mode 100644 index 00000000..0279316b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/FormatBytesTag.java @@ -0,0 +1,76 @@ +/* + 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.taglib; + +import net.sourceforge.subsonic.util.*; +import org.springframework.web.servlet.support.*; + +import javax.servlet.http.*; +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; +import java.io.*; +import java.util.*; + +/** + * Converts a byte-count to a formatted string suitable for display to the user, with respect + * to the current locale. + * <p/> + * For instance: + * <ul> + * <li><code>format(918)</code> returns <em>"918 B"</em>.</li> + * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li> + * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li> + * </ul> + * This class assumes that 1 KB is 1024 bytes. + * + * @author Sindre Mehus + */ +public class FormatBytesTag extends BodyTagSupport { + + private long bytes; + + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + public int doEndTag() throws JspException { + Locale locale = RequestContextUtils.getLocale((HttpServletRequest) pageContext.getRequest()); + String result = StringUtil.formatBytes(bytes, locale); + + try { + pageContext.getOut().print(result); + } catch (IOException x) { + throw new JspTagException(x); + } + return EVAL_PAGE; + } + + public void release() { + bytes = 0L; + super.release(); + } + + public long getBytes() { + return bytes; + } + + public void setBytes(long bytes) { + this.bytes = bytes; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.java new file mode 100644 index 00000000..1043902e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/ParamTag.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.taglib; + +import javax.servlet.jsp.tagext.*; +import javax.servlet.jsp.*; + +/** + * A tag representing an URL query parameter. + * + * @see ParamTag + * @author Sindre Mehus + */ +public class ParamTag extends TagSupport { + + private String name; + private String value; + + public int doEndTag() throws JspTagException { + + // Add parameter name and value to surrounding 'url' tag. + UrlTag tag = (UrlTag) findAncestorWithClass(this, UrlTag.class); + if (tag == null) { + throw new JspTagException("'sub:param' tag used outside 'sub:url'"); + } + tag.addParameter(name, value); + return EVAL_PAGE; + } + + public void release() { + name = null; + value = null; + super.release(); + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java new file mode 100644 index 00000000..141ba847 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/UrlTag.java @@ -0,0 +1,207 @@ +/* + 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.taglib; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.filter.ParameterDecodingFilter; +import net.sourceforge.subsonic.util.StringUtil; +import org.apache.taglibs.standard.tag.common.core.UrlSupport; +import org.apache.commons.lang.CharUtils; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspTagException; +import javax.servlet.jsp.PageContext; +import javax.servlet.jsp.tagext.BodyTagSupport; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; + +/** + * Creates a URL with optional query parameters. Similar to 'c:url', but + * you may specify which character encoding to use for the URL query + * parameters. If no encoding is specified, the following steps are performed: + * <ul> + * <li>Parameter values are encoded as the hexadecimal representation of the UTF-8 bytes of the original string.</li> + * <li>Parameter names are prepended with the suffix "Utf8Hex"</li> + * <li>Note: Nothing is done with the parameter name or value if the value only contains ASCII alphanumeric characters.</li> + * </ul> + * <p/> + * (The problem with c:url is that is uses the same encoding as the http response, + * but most(?) servlet container assumes that ISO-8859-1 is used.) + * + * @author Sindre Mehus + */ +public class UrlTag extends BodyTagSupport { + + private String DEFAULT_ENCODING = "Utf8Hex"; + private static final Logger LOG = Logger.getLogger(UrlTag.class); + + private String var; + private String value; + private String encoding = DEFAULT_ENCODING; + private List<Parameter> parameters = new ArrayList<Parameter>(); + + public int doStartTag() throws JspException { + parameters.clear(); + return EVAL_BODY_BUFFERED; + } + + public int doEndTag() throws JspException { + + // Rewrite and encode the url. + String result = formatUrl(); + + // Store or print the output + if (var != null) + pageContext.setAttribute(var, result, PageContext.PAGE_SCOPE); + else { + try { + pageContext.getOut().print(result); + } catch (IOException x) { + throw new JspTagException(x); + } + } + return EVAL_PAGE; + } + + private String formatUrl() throws JspException { + String baseUrl = UrlSupport.resolveUrl(value, null, pageContext); + + StringBuffer result = new StringBuffer(); + result.append(baseUrl); + if (!parameters.isEmpty()) { + result.append('?'); + + for (int i = 0; i < parameters.size(); i++) { + Parameter parameter = parameters.get(i); + try { + result.append(parameter.getName()); + if (isUtf8Hex() && !isAsciiAlphaNumeric(parameter.getValue())) { + result.append(ParameterDecodingFilter.PARAM_SUFFIX); + } + + result.append('='); + if (parameter.getValue() != null) { + result.append(encode(parameter.getValue())); + } + if (i < parameters.size() - 1) { + result.append("&"); + } + + } catch (UnsupportedEncodingException x) { + throw new JspTagException(x); + } + } + } + return result.toString(); + } + + private String encode(String s) throws UnsupportedEncodingException { + if (isUtf8Hex()) { + if (isAsciiAlphaNumeric(s)) { + return s; + } + + try { + return StringUtil.utf8HexEncode(s); + } catch (Exception x) { + LOG.error("Failed to utf8hex-encode the string '" + s + "'.", x); + return s; + } + } + + return URLEncoder.encode(s, encoding); + } + + private boolean isUtf8Hex() { + return DEFAULT_ENCODING.equals(encoding); + } + + private boolean isAsciiAlphaNumeric(String s) { + if (s == null) { + return true; + } + + for (int i = 0; i < s.length(); i++) { + if (!CharUtils.isAsciiAlphanumeric(s.charAt(i))) { + return false; + } + } + return true; + } + + public void release() { + var = null; + value = null; + encoding = DEFAULT_ENCODING; + parameters.clear(); + super.release(); + } + + public void addParameter(String name, String value) { + parameters.add(new Parameter(name, value)); + } + + public String getVar() { + return var; + } + + public void setVar(String var) { + this.var = var; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * A URL query parameter. + */ + private static class Parameter { + private String name; + private String value; + + private Parameter(String name, String value) { + this.name = name; + this.value = value; + } + + private String getName() { + return name; + } + + private String getValue() { + return value; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.java new file mode 100644 index 00000000..e099bd1e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/taglib/WikiTag.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.taglib; + +import org.radeox.api.engine.*; +import org.radeox.api.engine.context.*; +import org.radeox.engine.*; +import org.radeox.engine.context.*; +import org.apache.commons.lang.*; + +import javax.servlet.jsp.*; +import javax.servlet.jsp.tagext.*; +import java.io.*; + +/** + * Renders a Wiki text with markup to HTML, using the Radeox render engine. + * + * @author Sindre Mehus + */ +public class WikiTag extends BodyTagSupport { + + private static final RenderContext RENDER_CONTEXT = new BaseRenderContext(); + private static final RenderEngine RENDER_ENGINE = new BaseRenderEngine(); + + private String text; + + public int doStartTag() throws JspException { + return EVAL_BODY_BUFFERED; + } + + public int doEndTag() throws JspException { + String result; + synchronized (RENDER_ENGINE) { + result = RENDER_ENGINE.render(StringEscapeUtils.unescapeXml(text), RENDER_CONTEXT); + } + try { + pageContext.getOut().print(result); + } catch (IOException x) { + throw new JspTagException(x); + } + return EVAL_PAGE; + } + + public void release() { + text = null; + super.release(); + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java new file mode 100644 index 00000000..874c2e9c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeResolver.java @@ -0,0 +1,117 @@ +/* + 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.theme; + +import net.sourceforge.subsonic.service.*; +import net.sourceforge.subsonic.domain.*; +import org.springframework.web.servlet.*; + +import javax.servlet.http.*; +import java.util.*; + +/** + * Theme resolver implementation which returns the theme selected in the settings. + * + * @author Sindre Mehus + */ +public class SubsonicThemeResolver implements ThemeResolver { + + private SecurityService securityService; + private SettingsService settingsService; + private Set<String> themeIds; + + /** + * Resolve the current theme name via the given request. + * + * @param request Request to be used for resolution + * @return The current theme name + */ + public String resolveThemeName(HttpServletRequest request) { + String themeId = (String) request.getAttribute("subsonic.theme"); + if (themeId != null) { + return themeId; + } + + // Optimization: Cache theme in the request. + themeId = doResolveThemeName(request); + request.setAttribute("subsonic.theme", themeId); + + return themeId; + } + + private String doResolveThemeName(HttpServletRequest request) { + String themeId = null; + + // Look for user-specific theme. + String username = securityService.getCurrentUsername(request); + if (username != null) { + UserSettings userSettings = settingsService.getUserSettings(username); + if (userSettings != null) { + themeId = userSettings.getThemeId(); + } + } + + if (themeId != null && themeExists(themeId)) { + return themeId; + } + + // Return system theme. + themeId = settingsService.getThemeId(); + return themeExists(themeId) ? themeId : "default"; + } + + /** + * Returns whether the theme with the given ID exists. + * @param themeId The theme ID. + * @return Whether the theme with the given ID exists. + */ + private synchronized boolean themeExists(String themeId) { + // Lazily create set of theme IDs. + if (themeIds == null) { + themeIds = new HashSet<String>(); + Theme[] themes = settingsService.getAvailableThemes(); + for (Theme theme : themes) { + themeIds.add(theme.getId()); + } + } + + return themeIds.contains(themeId); + } + + /** + * Set the current theme name to the given one. This method is not supported. + * + * @param request Request to be used for theme name modification + * @param response Response to be used for theme name modification + * @param themeName The new theme name + * @throws UnsupportedOperationException If the ThemeResolver implementation + * does not support dynamic changing of the theme + */ + public void setThemeName(HttpServletRequest request, HttpServletResponse response, String themeName) { + throw new UnsupportedOperationException("Cannot change theme - use a different theme resolution strategy"); + } + + 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/theme/SubsonicThemeSource.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.java new file mode 100644 index 00000000..61db6516 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.java @@ -0,0 +1,49 @@ +/* + 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.theme; + +import org.springframework.ui.context.support.ResourceBundleThemeSource; +import org.springframework.context.MessageSource; +import org.springframework.context.support.ResourceBundleMessageSource; + +/** + * Theme source implementation which uses two resource bundles: the + * theme specific (e.g., barents.properties), and the default (default.properties). + * + * @author Sindre Mehus + */ +public class SubsonicThemeSource extends ResourceBundleThemeSource { + + private String defaultResourceBundle; + + @Override + protected MessageSource createMessageSource(String basename) { + ResourceBundleMessageSource messageSource = (ResourceBundleMessageSource) super.createMessageSource(basename); + + ResourceBundleMessageSource parentMessageSource = new ResourceBundleMessageSource(); + parentMessageSource.setBasename(defaultResourceBundle); + messageSource.setParentMessageSource(parentMessageSource); + + return messageSource; + } + + public void setDefaultResourceBundle(String defaultResourceBundle) { + this.defaultResourceBundle = defaultResourceBundle; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java new file mode 100644 index 00000000..f9b89bb7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItem.java @@ -0,0 +1,51 @@ +/* + 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.upload; + +import org.apache.commons.fileupload.disk.DiskFileItem; + +import java.io.File; +import java.io.OutputStream; +import java.io.IOException; + +/** + * Extension of Commons FileUpload for monitoring the upload progress. + * + * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net + */ +public class MonitoredDiskFileItem extends DiskFileItem { + private MonitoredOutputStream mos; + private UploadListener listener; + + public MonitoredDiskFileItem(String fieldName, String contentType, boolean isFormField, String fileName, int sizeThreshold, + File repository, UploadListener listener) { + super(fieldName, contentType, isFormField, fileName, sizeThreshold, repository); + this.listener = listener; + if (fileName != null) { + listener.start(fileName); + } + } + + public OutputStream getOutputStream() throws IOException { + if (mos == null) { + mos = new MonitoredOutputStream(super.getOutputStream(), listener); + } + return mos; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java new file mode 100644 index 00000000..b5d6125d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredDiskFileItemFactory.java @@ -0,0 +1,47 @@ +/* + 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.upload; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; + +import java.io.File; + +/** + * Extension of Commons FileUpload for monitoring the upload progress. + * + * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net + */ +public class MonitoredDiskFileItemFactory extends DiskFileItemFactory { + private UploadListener listener; + + public MonitoredDiskFileItemFactory(UploadListener listener) { + super(); + this.listener = listener; + } + + public MonitoredDiskFileItemFactory(int sizeThreshold, File repository, UploadListener listener) { + super(sizeThreshold, repository); + this.listener = listener; + } + + public FileItem createItem(String fieldName, String contentType, boolean isFormField, String fileName) { + return new MonitoredDiskFileItem(fieldName, contentType, isFormField, fileName, getSizeThreshold(), getRepository(), listener); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.java new file mode 100644 index 00000000..c7f0d525 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/MonitoredOutputStream.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.upload; + +import java.io.OutputStream; +import java.io.IOException; + +/** + * Extension of Commons FileUpload for monitoring the upload progress. + * + * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net + */ +public class MonitoredOutputStream extends OutputStream { + private OutputStream target; + private UploadListener listener; + + public MonitoredOutputStream(OutputStream target, UploadListener listener) { + this.target = target; + this.listener = listener; + } + + public void write(byte[] b, int off, int len) throws IOException { + target.write(b, off, len); + listener.bytesRead(len); + } + + public void write(byte[] b) throws IOException { + target.write(b); + listener.bytesRead(b.length); + } + + public void write(int b) throws IOException { + target.write(b); + listener.bytesRead(1); + } + + public void close() throws IOException { + target.close(); + } + + public void flush() throws IOException { + target.flush(); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java new file mode 100644 index 00000000..7eac415a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/upload/UploadListener.java @@ -0,0 +1,29 @@ +/* + 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.upload; + +/** + * Extension of Commons FileUpload for monitoring the upload progress. + * + * @author Pierre-Alexandre Losson -- http://www.telio.be/blog -- plosson@users.sourceforge.net + */ +public interface UploadListener { + void start(String fileName); + void bytesRead(long bytesRead); +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java new file mode 100644 index 00000000..fb240d5f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/BoundedList.java @@ -0,0 +1,71 @@ +/* + 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.util; + +import java.util.*; + +/** + * Simple implementation of a bounded list. If the maximum size is reached, adding a new element will + * remove the first element in the list. + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2005/05/09 20:01:25 $ + */ +public class BoundedList<E> extends LinkedList<E> { + private int maxSize; + + /** + * Creates a new bounded list with the given maximum size. + * @param maxSize The maximum number of elements the list may hold. + */ + public BoundedList(int maxSize) { + this.maxSize = maxSize; + } + + /** + * Adds an element to the tail of the list. If the list is full, the first element is removed. + * @param e The element to add. + * @return Always <code>true</code>. + */ + public boolean add(E e) { + if (isFull()) { + removeFirst(); + } + return super.add(e); + } + + /** + * Adds an element to the head of list. If the list is full, the last element is removed. + * @param e The element to add. + */ + public void addFirst(E e) { + if (isFull()) { + removeLast(); + } + super.addFirst(e); + } + + /** + * Returns whether the list if full. + * @return Whether the list is full. + */ + private boolean isFull() { + return size() == maxSize; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java new file mode 100644 index 00000000..e91758ef --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java @@ -0,0 +1,186 @@ +/* + 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.util; + +import java.io.Closeable; +import java.io.File; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Arrays; + +import net.sourceforge.subsonic.Logger; + +/** + * Miscellaneous file utility methods. + * + * @author Sindre Mehus + */ +public final class FileUtil { + + private static final Logger LOG = Logger.getLogger(FileUtil.class); + + /** + * Disallow external instantiation. + */ + private FileUtil() { + } + + public static boolean isFile(final File file) { + return timed(new FileTask<Boolean>("isFile", file) { + @Override + public Boolean execute() { + return file.isFile(); + } + }); + } + + public static boolean isDirectory(final File file) { + return timed(new FileTask<Boolean>("isDirectory", file) { + @Override + public Boolean execute() { + return file.isDirectory(); + } + }); + } + + public static boolean exists(final File file) { + return timed(new FileTask<Boolean>("exists", file) { + @Override + public Boolean execute() { + return file.exists(); + } + }); + } + + public static long lastModified(final File file) { + return timed(new FileTask<Long>("lastModified", file) { + @Override + public Long execute() { + return file.lastModified(); + } + }); + } + + public static long length(final File file) { + return timed(new FileTask<Long>("length", file) { + @Override + public Long execute() { + return file.length(); + } + }); + } + + /** + * Similar to {@link File#listFiles()}, but never returns null. + * Instead a warning is logged, and an empty array is returned. + */ + public static File[] listFiles(final File dir) { + File[] files = timed(new FileTask<File[]>("listFiles", dir) { + @Override + public File[] execute() { + return dir.listFiles(); + } + }); + + if (files == null) { + LOG.warn("Failed to list children for " + dir.getPath()); + return new File[0]; + } + return files; + } + + /** + * Similar to {@link File#listFiles(FilenameFilter)}, but never returns null. + * Instead a warning is logged, and an empty array is returned. + */ + public static File[] listFiles(final File dir, final FilenameFilter filter, boolean sort) { + File[] files = timed(new FileTask<File[]>("listFiles2", dir) { + @Override + public File[] execute() { + return dir.listFiles(filter); + } + }); + if (files == null) { + LOG.warn("Failed to list children for " + dir.getPath()); + return new File[0]; + } + if (sort) { + Arrays.sort(files); + } + return files; + } + + /** + * Returns a short path for the given file. The path consists of the name + * of the parent directory and the given file. + */ + public static String getShortPath(File file) { + if (file == null) { + return null; + } + File parent = file.getParentFile(); + if (parent == null) { + return file.getName(); + } + return parent.getName() + File.separator + file.getName(); + } + + /** + * Closes the "closable", ignoring any excepetions. + * + * @param closeable The Closable to close, may be {@code null}. + */ + public static void closeQuietly(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + // Ignored + } + } + } + + private static <T> T timed(FileTask<T> task) { +// long t0 = System.nanoTime(); +// try { + return task.execute(); +// } finally { +// long t1 = System.nanoTime(); +// LOG.debug((t1 - t0) / 1000L + " microsec, " + task); +// } + } + + private abstract static class FileTask<T> { + + private final String name; + private final File file; + + public FileTask(String name, File file) { + this.name = name; + this.file = file; + } + + public abstract T execute(); + + @Override + public String toString() { + return name + ", " + file; + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java new file mode 100644 index 00000000..7edecaa2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.java @@ -0,0 +1,54 @@ +/* + 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.util; + +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class Pair<S, T> implements Serializable { + + private S first; + private T second; + + public Pair() { + } + + public Pair(S first, T second) { + this.first = first; + this.second = second; + } + + public S getFirst() { + return first; + } + + public void setFirst(S first) { + this.first = first; + } + + public T getSecond() { + return second; + } + + public void setSecond(T second) { + this.second = second; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java new file mode 100644 index 00000000..ebad9fbf --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java @@ -0,0 +1,537 @@ +/* + 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.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLEncoder; +import java.security.MessageDigest; +import java.text.DateFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.NumberFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang.math.LongRange; + +/** + * Miscellaneous string utility methods. + * + * @author Sindre Mehus + */ +public final class StringUtil { + + public static final String ENCODING_LATIN = "ISO-8859-1"; + public static final String ENCODING_UTF8 = "UTF-8"; + private static final DateFormat ISO_8601_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + + private static final String[][] HTML_SUBSTITUTIONS = { + {"&", "&"}, + {"<", "<"}, + {">", ">"}, + {"'", "'"}, + {"\"", """}, + }; + + private static final String[][] MIME_TYPES = { + {"mp3", "audio/mpeg"}, + {"ogg", "audio/ogg"}, + {"oga", "audio/ogg"}, + {"ogx", "application/ogg"}, + {"aac", "audio/mp4"}, + {"m4a", "audio/mp4"}, + {"flac", "audio/flac"}, + {"wav", "audio/x-wav"}, + {"wma", "audio/x-ms-wma"}, + {"ape", "audio/x-monkeys-audio"}, + {"mpc", "audio/x-musepack"}, + {"shn", "audio/x-shn"}, + + {"flv", "video/x-flv"}, + {"avi", "video/avi"}, + {"mpg", "video/mpeg"}, + {"mpeg", "video/mpeg"}, + {"mp4", "video/mp4"}, + {"m4v", "video/x-m4v"}, + {"mkv", "video/x-matroska"}, + {"mov", "video/quicktime"}, + {"wmv", "video/x-ms-wmv"}, + {"ogv", "video/ogg"}, + {"divx", "video/divx"}, + {"m2ts", "video/MP2T"}, + + {"gif", "image/gif"}, + {"jpg", "image/jpeg"}, + {"jpeg", "image/jpeg"}, + {"png", "image/png"}, + {"bmp", "image/bmp"}, + }; + + private static final String[] FILE_SYSTEM_UNSAFE = {"/", "\\", "..", ":", "\"", "?", "*", "|"}; + + /** + * Disallow external instantiation. + */ + private StringUtil() { + } + + /** + * Returns the specified string converted to a format suitable for + * HTML. All single-quote, double-quote, greater-than, less-than and + * ampersand characters are replaces with their corresponding HTML + * Character Entity code. + * + * @param s the string to convert + * @return the converted string + */ + public static String toHtml(String s) { + if (s == null) { + return null; + } + for (String[] substitution : HTML_SUBSTITUTIONS) { + if (s.contains(substitution[0])) { + s = s.replaceAll(substitution[0], substitution[1]); + } + } + return s; + } + + + /** + * Formats the given date to a ISO-8601 date/time format, and UTC timezone. + * <p/> + * The returned date uses the following format: 2007-12-17T14:57:17 + * + * @param date The date to format + * @return The corresponding ISO-8601 formatted string. + */ + public static String toISO8601(Date date) { + if (date == null) { + return null; + } + + synchronized (ISO_8601_DATE_FORMAT) { + return ISO_8601_DATE_FORMAT.format(date); + } + } + + /** + * Removes the suffix (the substring after the last dot) of the given string. The dot is + * also removed. + * + * @param s The string in question, e.g., "foo.mp3". + * @return The string without the suffix, e.g., "foo". + */ + public static String removeSuffix(String s) { + int index = s.lastIndexOf('.'); + return index == -1 ? s : s.substring(0, index); + } + + /** + * Returns the proper MIME type for the given suffix. + * + * @param suffix The suffix, e.g., "mp3" or ".mp3". + * @return The corresponding MIME type, e.g., "audio/mpeg". If no MIME type is found, + * <code>application/octet-stream</code> is returned. + */ + public static String getMimeType(String suffix) { + for (String[] map : MIME_TYPES) { + if (map[0].equalsIgnoreCase(suffix) || ('.' + map[0]).equalsIgnoreCase(suffix)) { + return map[1]; + } + } + return "application/octet-stream"; + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + * <ul> + * <li><code>format(918)</code> returns <em>"918 B"</em>.</li> + * <li><code>format(98765)</code> returns <em>"96 KB"</em>.</li> + * <li><code>format(1238476)</code> returns <em>"1.2 MB"</em>.</li> + * </ul> + * This method assumes that 1 KB is 1024 bytes. + * + * @param byteCount The number of bytes. + * @param locale The locale used for formatting. + * @return The formatted string. + */ + public static synchronized String formatBytes(long byteCount, Locale locale) { + + // More than 1 GB? + if (byteCount >= 1024 * 1024 * 1024) { + NumberFormat gigaByteFormat = new DecimalFormat("0.00 GB", new DecimalFormatSymbols(locale)); + return gigaByteFormat.format((double) byteCount / (1024 * 1024 * 1024)); + } + + // More than 1 MB? + if (byteCount >= 1024 * 1024) { + NumberFormat megaByteFormat = new DecimalFormat("0.0 MB", new DecimalFormatSymbols(locale)); + return megaByteFormat.format((double) byteCount / (1024 * 1024)); + } + + // More than 1 KB? + if (byteCount >= 1024) { + NumberFormat kiloByteFormat = new DecimalFormat("0 KB", new DecimalFormatSymbols(locale)); + return kiloByteFormat.format((double) byteCount / 1024); + } + + return byteCount + " B"; + } + + /** + * Formats a duration with minutes and seconds, e.g., "93:45" + */ + public static String formatDuration(int seconds) { + int minutes = seconds / 60; + int secs = seconds % 60; + + StringBuilder builder = new StringBuilder(6); + builder.append(minutes).append(":"); + if (secs < 10) { + builder.append("0"); + } + builder.append(secs); + return builder.toString(); + } + + /** + * Splits the input string. White space is interpreted as separator token. Double quotes + * are interpreted as grouping operator. <br/> + * For instance, the input <code>"u2 rem "greatest hits""</code> will return an array with + * three elements: <code>{"u2", "rem", "greatest hits"}</code> + * + * @param input The input string. + * @return Array of elements. + */ + public static String[] split(String input) { + if (input == null) { + return new String[0]; + } + + Pattern pattern = Pattern.compile("\".*?\"|\\S+"); + Matcher matcher = pattern.matcher(input); + + List<String> result = new ArrayList<String>(); + while (matcher.find()) { + String element = matcher.group(); + if (element.startsWith("\"") && element.endsWith("\"") && element.length() > 1) { + element = element.substring(1, element.length() - 1); + } + result.add(element); + } + + return result.toArray(new String[result.size()]); + } + + /** + * Reads lines from the given input stream. All lines are trimmed. Empty lines and lines starting + * with "#" are skipped. The input stream is always closed by this method. + * + * @param in The input stream to read from. + * @return Array of lines. + * @throws IOException If an I/O error occurs. + */ + public static String[] readLines(InputStream in) throws IOException { + BufferedReader reader = null; + + try { + reader = new BufferedReader(new InputStreamReader(in)); + List<String> result = new ArrayList<String>(); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + line = line.trim(); + if (!line.startsWith("#") && line.length() > 0) { + result.add(line); + } + } + return result.toArray(new String[result.size()]); + + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(reader); + } + } + + /** + * Converts the given string of whitespace-separated integers to an <code>int</code> array. + * + * @param s String consisting of integers separated by whitespace. + * @return The corresponding array of ints. + * @throws NumberFormatException If string contains non-parseable text. + */ + public static int[] parseInts(String s) { + if (s == null) { + return new int[0]; + } + + String[] strings = StringUtils.split(s); + int[] ints = new int[strings.length]; + for (int i = 0; i < strings.length; i++) { + ints[i] = Integer.parseInt(strings[i]); + } + return ints; + } + + /** + * Change protocol from "https" to "http" for the given URL. The port number is also changed, + * but not if the given URL is already "http". + * + * @param url The original URL. + * @param port The port number to use, for instance 443. + * @return The transformed URL. + * @throws MalformedURLException If the original URL is invalid. + */ + public static String toHttpUrl(String url, int port) throws MalformedURLException { + URL u = new URL(url); + if ("https".equals(u.getProtocol())) { + return new URL("http", u.getHost(), port, u.getFile()).toString(); + } + return url; + } + + /** + * Determines whether a is equal to b, taking null into account. + * + * @return Whether a and b are equal, or both null. + */ + public static boolean isEqual(Object a, Object b) { + return a == null ? b == null : a.equals(b); + } + + /** + * Parses a locale from the given string. + * + * @param s The locale string. Should be formatted as per the documentation in {@link Locale#toString()}. + * @return The locale. + */ + public static Locale parseLocale(String s) { + if (s == null) { + return null; + } + + String[] elements = s.split("_"); + + if (elements.length == 0) { + return new Locale(s, "", ""); + } + if (elements.length == 1) { + return new Locale(elements[0], "", ""); + } + if (elements.length == 2) { + return new Locale(elements[0], elements[1], ""); + } + return new Locale(elements[0], elements[1], elements[2]); + } + + /** + * URL-encodes the input value using UTF-8. + */ + public static String urlEncode(String s) { + try { + return URLEncoder.encode(s, StringUtil.ENCODING_UTF8); + } catch (UnsupportedEncodingException x) { + throw new RuntimeException(x); + } + } + + /** + * Encodes the given string by using the hexadecimal representation of its UTF-8 bytes. + * + * @param s The string to encode. + * @return The encoded string. + */ + public static String utf8HexEncode(String s) { + if (s == null) { + return null; + } + byte[] utf8; + try { + utf8 = s.getBytes(ENCODING_UTF8); + } catch (UnsupportedEncodingException x) { + throw new RuntimeException(x); + } + return String.valueOf(Hex.encodeHex(utf8)); + } + + /** + * Decodes the given string by using the hexadecimal representation of its UTF-8 bytes. + * + * @param s The string to decode. + * @return The decoded string. + * @throws Exception If an error occurs. + */ + public static String utf8HexDecode(String s) throws Exception { + if (s == null) { + return null; + } + return new String(Hex.decodeHex(s.toCharArray()), ENCODING_UTF8); + } + + /** + * Calculates the MD5 digest and returns the value as a 32 character hex string. + * + * @param s Data to digest. + * @return MD5 digest as a hex string. + */ + public static String md5Hex(String s) { + if (s == null) { + return null; + } + + try { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + return new String(Hex.encodeHex(md5.digest(s.getBytes(ENCODING_UTF8)))); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + + /** + * Returns the file part of an URL. For instance: + * <p/> + * <code> + * getUrlFile("http://archive.ncsa.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html") + * </code> + * <p/> + * will return "url-primer.html". + * + * @param url The URL in question. + * @return The file part, or <code>null</code> if no file can be resolved. + */ + public static String getUrlFile(String url) { + try { + String path = new URL(url).getPath(); + if (StringUtils.isBlank(path) || path.endsWith("/")) { + return null; + } + + File file = new File(path); + String filename = file.getName(); + if (StringUtils.isBlank(filename)) { + return null; + } + return filename; + + } catch (MalformedURLException x) { + return null; + } + } + + /** + * Rewrites the URL by changing the protocol, host and port. + * + * @param urlToRewrite The URL to rewrite. + * @param urlWithProtocolHostAndPort Use protocol, host and port from this URL. + * @return The rewritten URL, or an unchanged URL if either argument is not a proper URL. + */ + public static String rewriteUrl(String urlToRewrite, String urlWithProtocolHostAndPort) { + if (urlToRewrite == null) { + return null; + } + + try { + URL urlA = new URL(urlToRewrite); + URL urlB = new URL(urlWithProtocolHostAndPort); + + URL result = new URL(urlB.getProtocol(), urlB.getHost(), urlB.getPort(), urlA.getFile()); + return result.toExternalForm(); + } catch (MalformedURLException x) { + return urlToRewrite; + } + } + + /** + * Makes a given filename safe by replacing special characters like slashes ("/" and "\") + * with dashes ("-"). + * + * @param filename The filename in question. + * @return The filename with special characters replaced by underscores. + */ + public static String fileSystemSafe(String filename) { + for (String s : FILE_SYSTEM_UNSAFE) { + filename = filename.replace(s, "-"); + } + return filename; + } + + /** + * Parses the given string as a HTTP header byte range. See chapter 14.36.1 in RFC 2068 + * for details. + * <p/> + * Only a subset of the allowed syntaxes are supported. Only ranges which specify first-byte-pos + * are supported. The last-byte-pos is optional. + * + * @param range The range from the HTTP header, for instance "bytes=0-499" or "bytes=500-" + * @return A range object (using inclusive values). If the last-byte-pos is not given, the end of + * the returned range is {@link Long#MAX_VALUE}. The method returns <code>null</code> if the syntax + * of the given range is not supported. + */ + public static LongRange parseRange(String range) { + if (range == null) { + return null; + } + + Pattern pattern = Pattern.compile("bytes=(\\d+)-(\\d*)"); + Matcher matcher = pattern.matcher(range); + + if (matcher.matches()) { + String firstString = matcher.group(1); + String lastString = StringUtils.trimToNull(matcher.group(2)); + + long first = Long.parseLong(firstString); + long last = lastString == null ? Long.MAX_VALUE : Long.parseLong(lastString); + + if (first > last) { + return null; + } + + return new LongRange(first, last); + } + return null; + } + + public static String removeMarkup(String s) { + if (s == null) { + return null; + } + return s.replaceAll("<.*?>", ""); + } + + public static String getRESTProtocolVersion() { + // TODO: Read from xsd. + return "1.8.0"; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java new file mode 100644 index 00000000..ec7175a2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java @@ -0,0 +1,127 @@ +/* + 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.util; + +import net.sourceforge.subsonic.Logger; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.Inet4Address; +import java.util.Enumeration; +import java.util.Random; + +/** + * Miscellaneous general utility methods. + * + * @author Sindre Mehus + */ +public final class Util { + + private static final Logger LOG = Logger.getLogger(Util.class); + private static final Random RANDOM = new Random(System.currentTimeMillis()); + + /** + * Disallow external instantiation. + */ + private Util() { + } + + public static String getDefaultMusicFolder() { + String def = isWindows() ? "c:\\music" : "/var/music"; + return System.getProperty("subsonic.defaultMusicFolder", def); + } + + public static String getDefaultPodcastFolder() { + String def = isWindows() ? "c:\\music\\Podcast" : "/var/music/Podcast"; + return System.getProperty("subsonic.defaultPodcastFolder", def); + } + + public static String getDefaultPlaylistFolder() { + String def = isWindows() ? "c:\\playlists" : "/var/playlists"; + return System.getProperty("subsonic.defaultPlaylistFolder", def); + } + + public static boolean isWindows() { + return System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows"); + } + + public static boolean isWindowsInstall() { + return "true".equals(System.getProperty("subsonic.windowsInstall")); + } + + /** + * Similar to {@link ServletResponse#setContentLength(int)}, but this + * method supports lengths bigger than 2GB. + * <p/> + * See http://blogger.ziesemer.com/2008/03/suns-version-of-640k-2gb.html + * + * @param response The HTTP response. + * @param length The content length. + */ + public static void setContentLength(HttpServletResponse response, long length) { + if (length <= Integer.MAX_VALUE) { + response.setContentLength((int) length); + } else { + response.setHeader("Content-Length", String.valueOf(length)); + } + } + + /** + * Returns the local IP address. + * @return The local IP, or the loopback address (127.0.0.1) if not found. + */ + public static String getLocalIpAddress() { + try { + + // Try the simple way first. + InetAddress address = InetAddress.getLocalHost(); + if (!address.isLoopbackAddress()) { + return address.getHostAddress(); + } + + // Iterate through all network interfaces, looking for a suitable IP. + Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + Enumeration<InetAddress> addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) { + return addr.getHostAddress(); + } + } + } + + } catch (Throwable x) { + LOG.warn("Failed to resolve local IP address.", x); + } + + return "127.0.0.1"; + } + + public static int randomInt(int min, int max) { + if (min >= max) { + return 0; + } + return min + RANDOM.nextInt(max - min); + + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java new file mode 100644 index 00000000..a572ac0f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/XMLBuilder.java @@ -0,0 +1,328 @@ +/* + 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.util; + +import org.apache.commons.lang.StringEscapeUtils; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.XML; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Stack; + + +/** + * Simplifies building of XML documents. + * <p/> + * <b>Example:</b><br/> + * The following code: + * <pre> + * XMLBuilder builder = XMLBuilder.createXMLBuilder(); + * builder.add("foo").add("bar"); + * builder.add("zonk", 42); + * builder.end().end(); + * System.out.println(builder.toString()); + * </pre> + * produces the following XML: + * <pre> + * <foo> + * <bar> + * <zonk>42</zonk> + * </bar> + * </foo> + * </pre> + * This class is <em>not</em> thread safe. + * <p/> + * Also supports JSON and JSONP formats. + * + * @author Sindre Mehus + */ +public class XMLBuilder { + + private static final String INDENTATION = " "; + private static final String NEWLINE = "\n"; + + private final Writer writer = new StringWriter(); + private final Stack<String> elementStack = new Stack<String>(); + private final boolean json; + private final String jsonpCallback; + + public static XMLBuilder createXMLBuilder() { + return new XMLBuilder(false, null); + } + + public static XMLBuilder createJSONBuilder() { + return new XMLBuilder(true, null); + } + + public static XMLBuilder createJSONPBuilder(String callback) { + return new XMLBuilder(true, callback); + } + + /** + * Creates a new instance. + * + * @param json Whether to produce JSON rather than XML. + * @param jsonpCallback Name of javascript callback for JSONP. + */ + private XMLBuilder(boolean json, String jsonpCallback) { + this.json = json; + this.jsonpCallback = jsonpCallback; + } + + /** + * Adds an XML preamble, with the given encoding. The preamble will typically + * look like this: + * <p/> + * <code><?xml version="1.0" encoding="UTF-8"?></code> + * + * @param encoding The encoding to put in the preamble. + * @return A reference to this object. + */ + public XMLBuilder preamble(String encoding) throws IOException { + writer.write("<?xml version=\"1.0\" encoding=\""); + writer.write(encoding); + writer.write("\"?>"); + newline(); + return this; + } + + /** + * Adds an element with the given name and a single attribute. + * + * @param element The element name. + * @param attributeKey The attributes key. + * @param attributeValue The attributes value. + * @param close Whether to close the element. + * @return A reference to this object. + */ + public XMLBuilder add(String element, String attributeKey, Object attributeValue, boolean close) throws IOException { + return add(element, close, new Attribute(attributeKey, attributeValue)); + } + + /** + * Adds an element with the given name and attributes. + * + * @param element The element name. + * @param close Whether to close the element. + * @param attributes The element attributes. + * @return A reference to this object. + */ + public XMLBuilder add(String element, boolean close, Attribute... attributes) throws IOException { + return add(element, Arrays.asList(attributes), close); + } + + /** + * Adds an element with the given name and attributes. + * + * @param element The element name. + * @param attributes The element attributes. + * @param close Whether to close the element. + * @return A reference to this object. + */ + public XMLBuilder add(String element, Iterable<Attribute> attributes, boolean close) throws IOException { + return add(element, attributes, null, close); + } + + /** + * Adds an element with the given name, attributes and character data. + * + * @param element The element name. + * @param attributes The element attributes. + * @param text The character data. + * @param close Whether to close the element. + * @return A reference to this object. + */ + public XMLBuilder add(String element, Iterable<Attribute> attributes, String text, boolean close) throws IOException { + indent(); + elementStack.push(element); + writer.write('<'); + writer.write(element); + + if (attributes == null) { + attributes = Collections.emptyList(); + } + + Iterator<Attribute> iterator = attributes.iterator(); + + if (iterator.hasNext()) { + writer.write(' '); + } + while (iterator.hasNext()) { + Attribute attribute = iterator.next(); + attribute.append(writer); + if (iterator.hasNext()) { + writer.write(' '); + } + } + + if (close && text == null) { + elementStack.pop(); + writer.write("/>"); + } else { + writer.write('>'); + } + + if (text != null) { + writer.write(text); + + if (close) { + elementStack.pop(); + writer.write("</"); + writer.write(element); + writer.write('>'); + } + } + + newline(); + return this; + } + + /** + * Closes the current element. + * + * @return A reference to this object. + * @throws IllegalStateException If there are no unclosed elements. + */ + public XMLBuilder end() throws IllegalStateException, IOException { + if (elementStack.isEmpty()) { + throw new IllegalStateException("There are no unclosed elements."); + } + + String element = elementStack.pop(); + indent(); + writer.write("</"); + writer.write(element); + writer.write('>'); + newline(); + return this; + } + + /** + * Closes all unclosed elements. + * + * @return A reference to this object. + */ + public XMLBuilder endAll() throws IOException { + while (!elementStack.isEmpty()) { + end(); + } + return this; + } + + /** + * Returns the XML document as a string. + */ + @Override + public String toString() { + String xml = writer.toString(); + if (!json) { + return xml; + } + try { + JSONObject jsonObject = XML.toJSONObject(xml); + + if (jsonpCallback != null) { + return jsonpCallback + "(" + jsonObject.toString(1) + ");"; + } + + return jsonObject.toString(1); + } catch (JSONException x) { + throw new RuntimeException("Failed to convert from XML to JSON.", x); + } + } + + private void indent() throws IOException { + int depth = elementStack.size(); + for (int i = 0; i < depth; i++) { + writer.write(INDENTATION); + } + } + + private void newline() throws IOException { + writer.write(NEWLINE); + } + + /** + * An XML element attribute. + */ + public static class Attribute { + + private final String key; + private final Object value; + + public Attribute(String key, Object value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public Object getValue() { + return value; + } + + private void append(Writer writer) throws IOException { + if (key != null && value != null) { + writer.write(key); + writer.write("=\""); + writer.write(StringEscapeUtils.escapeXml(value.toString())); + writer.write("\""); + } + } + } + + /** + * A set of attributes. + */ + public static class AttributeSet implements Iterable<Attribute> { + + private final Map<String, Attribute> attributes = new LinkedHashMap<String, Attribute>(); + + public void add(Attribute attribute) { + attributes.put(attribute.getKey(), attribute); + } + + public void add(String key, Object value) { + if (key != null && value != null) { + add(new Attribute(key, value)); + } + } + + public void addAll(Iterable<Attribute> attributes) { + for (Attribute attribute : attributes) { + add(attribute); + } + } + + public Iterator<Attribute> iterator() { + return attributes.values().iterator(); + } + } + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java new file mode 100644 index 00000000..276eacb0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/DonateValidator.java @@ -0,0 +1,51 @@ +/* + 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.validator; + +import org.springframework.validation.Validator; +import org.springframework.validation.Errors; +import net.sourceforge.subsonic.command.PasswordSettingsCommand; +import net.sourceforge.subsonic.command.DonateCommand; +import net.sourceforge.subsonic.controller.DonateController; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Validator for {@link DonateController}. + * + * @author Sindre Mehus + */ +public class DonateValidator implements Validator { + private SettingsService settingsService; + + public boolean supports(Class clazz) { + return clazz.equals(DonateCommand.class); + } + + public void validate(Object obj, Errors errors) { + DonateCommand command = (DonateCommand) obj; + + if (!settingsService.isLicenseValid(command.getEmailAddress(), command.getLicense())) { + errors.rejectValue("license", "donate.invalidlicense"); + } + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java new file mode 100644 index 00000000..12fb06ce --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PasswordSettingsValidator.java @@ -0,0 +1,45 @@ +/* + 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.validator; + +import org.springframework.validation.*; +import net.sourceforge.subsonic.command.*; +import net.sourceforge.subsonic.controller.*; + +/** + * Validator for {@link PasswordSettingsController}. + * + * @author Sindre Mehus + */ +public class PasswordSettingsValidator implements Validator { + + public boolean supports(Class clazz) { + return clazz.equals(PasswordSettingsCommand.class); + } + + public void validate(Object obj, Errors errors) { + PasswordSettingsCommand command = (PasswordSettingsCommand) obj; + + if (command.getPassword() == null || command.getPassword().length() == 0) { + errors.rejectValue("password", "usersettings.nopassword"); + } else if (!command.getPassword().equals(command.getConfirmPassword())) { + errors.rejectValue("password", "usersettings.wrongpassword"); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.java new file mode 100644 index 00000000..3445b7d8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/UserSettingsValidator.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.validator; + +import net.sourceforge.subsonic.command.UserSettingsCommand; +import net.sourceforge.subsonic.controller.UserSettingsController; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.lang.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +/** + * Validator for {@link UserSettingsController}. + * + * @author Sindre Mehus + */ +public class UserSettingsValidator implements Validator { + + private SecurityService securityService; + private SettingsService settingsService; + + /** + * {@inheritDoc} + */ + public boolean supports(Class clazz) { + return clazz.equals(UserSettingsCommand.class); + } + + /** + * {@inheritDoc} + */ + public void validate(Object obj, Errors errors) { + UserSettingsCommand command = (UserSettingsCommand) obj; + String username = command.getUsername(); + String email = StringUtils.trimToNull(command.getEmail()); + String password = StringUtils.trimToNull(command.getPassword()); + String confirmPassword = command.getConfirmPassword(); + + if (command.isNew()) { + if (username == null || username.length() == 0) { + errors.rejectValue("username", "usersettings.nousername"); + } else if (securityService.getUserByName(username) != null) { + errors.rejectValue("username", "usersettings.useralreadyexists"); + } else if (email == null) { + errors.rejectValue("email", "usersettings.noemail"); + } else if (command.isLdapAuthenticated() && !settingsService.isLdapEnabled()) { + errors.rejectValue("password", "usersettings.ldapdisabled"); + } else if (command.isLdapAuthenticated() && password != null) { + errors.rejectValue("password", "usersettings.passwordnotsupportedforldap"); + } + } + + if ((command.isNew() || command.isPasswordChange()) && !command.isLdapAuthenticated()) { + if (password == null) { + errors.rejectValue("password", "usersettings.nopassword"); + } else if (!password.equals(confirmPassword)) { + errors.rejectValue("password", "usersettings.wrongpassword"); + } + } + + if (command.isPasswordChange() && command.isLdapAuthenticated()) { + errors.rejectValue("password", "usersettings.passwordnotsupportedforldap"); + } + + } + + 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/org/json/CDL.java b/subsonic-main/src/main/java/org/json/CDL.java new file mode 100644 index 00000000..a1885aad --- /dev/null +++ b/subsonic-main/src/main/java/org/json/CDL.java @@ -0,0 +1,279 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * This provides static methods to convert comma delimited text into a + * JSONArray, and to covert a JSONArray into comma delimited text. Comma + * delimited text is a very popular format for data interchange. It is + * understood by most database, spreadsheet, and organizer programs. + * <p> + * Each row of text represents a row in a table or a data record. Each row + * ends with a NEWLINE character. Each row contains one or more values. + * Values are separated by commas. A value can contain any character except + * for comma, unless is is wrapped in single quotes or double quotes. + * <p> + * The first row usually contains the names of the columns. + * <p> + * A comma delimited list can be converted into a JSONArray of JSONObjects. + * The names for the elements in the JSONObjects can be taken from the names + * in the first row. + * @author JSON.org + * @version 2010-12-24 + */ +public class CDL { + + /** + * Get the next value. The value can be wrapped in quotes. The value can + * be empty. + * @param x A JSONTokener of the source text. + * @return The value string, or null if empty. + * @throws JSONException if the quoted string is badly formed. + */ + private static String getValue(JSONTokener x) throws JSONException { + char c; + char q; + StringBuffer sb; + do { + c = x.next(); + } while (c == ' ' || c == '\t'); + switch (c) { + case 0: + return null; + case '"': + case '\'': + q = c; + sb = new StringBuffer(); + for (;;) { + c = x.next(); + if (c == q) { + break; + } + if (c == 0 || c == '\n' || c == '\r') { + throw x.syntaxError("Missing close quote '" + q + "'."); + } + sb.append(c); + } + return sb.toString(); + case ',': + x.back(); + return ""; + default: + x.back(); + return x.nextTo(','); + } + } + + /** + * Produce a JSONArray of strings from a row of comma delimited values. + * @param x A JSONTokener of the source text. + * @return A JSONArray of strings. + * @throws JSONException + */ + public static JSONArray rowToJSONArray(JSONTokener x) throws JSONException { + JSONArray ja = new JSONArray(); + for (;;) { + String value = getValue(x); + char c = x.next(); + if (value == null || + (ja.length() == 0 && value.length() == 0 && c != ',')) { + return null; + } + ja.put(value); + for (;;) { + if (c == ',') { + break; + } + if (c != ' ') { + if (c == '\n' || c == '\r' || c == 0) { + return ja; + } + throw x.syntaxError("Bad character '" + c + "' (" + + (int)c + ")."); + } + c = x.next(); + } + } + } + + /** + * Produce a JSONObject from a row of comma delimited text, using a + * parallel JSONArray of strings to provides the names of the elements. + * @param names A JSONArray of names. This is commonly obtained from the + * first row of a comma delimited text file using the rowToJSONArray + * method. + * @param x A JSONTokener of the source text. + * @return A JSONObject combining the names and values. + * @throws JSONException + */ + public static JSONObject rowToJSONObject(JSONArray names, JSONTokener x) + throws JSONException { + JSONArray ja = rowToJSONArray(x); + return ja != null ? ja.toJSONObject(names) : null; + } + + /** + * Produce a comma delimited text row from a JSONArray. Values containing + * the comma character will be quoted. Troublesome characters may be + * removed. + * @param ja A JSONArray of strings. + * @return A string ending in NEWLINE. + */ + public static String rowToString(JSONArray ja) { + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < ja.length(); i += 1) { + if (i > 0) { + sb.append(','); + } + Object object = ja.opt(i); + if (object != null) { + String string = object.toString(); + if (string.length() > 0 && (string.indexOf(',') >= 0 || + string.indexOf('\n') >= 0 || string.indexOf('\r') >= 0 || + string.indexOf(0) >= 0 || string.charAt(0) == '"')) { + sb.append('"'); + int length = string.length(); + for (int j = 0; j < length; j += 1) { + char c = string.charAt(j); + if (c >= ' ' && c != '"') { + sb.append(c); + } + } + sb.append('"'); + } else { + sb.append(string); + } + } + } + sb.append('\n'); + return sb.toString(); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string, + * using the first row as a source of names. + * @param string The comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new JSONTokener(string)); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string, + * using the first row as a source of names. + * @param x The JSONTokener containing the comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(JSONTokener x) throws JSONException { + return toJSONArray(rowToJSONArray(x), x); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string + * using a supplied JSONArray as the source of element names. + * @param names A JSONArray of strings. + * @param string The comma delimited text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(JSONArray names, String string) + throws JSONException { + return toJSONArray(names, new JSONTokener(string)); + } + + /** + * Produce a JSONArray of JSONObjects from a comma delimited text string + * using a supplied JSONArray as the source of element names. + * @param names A JSONArray of strings. + * @param x A JSONTokener of the source text. + * @return A JSONArray of JSONObjects. + * @throws JSONException + */ + public static JSONArray toJSONArray(JSONArray names, JSONTokener x) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (;;) { + JSONObject jo = rowToJSONObject(names, x); + if (jo == null) { + break; + } + ja.put(jo); + } + if (ja.length() == 0) { + return null; + } + return ja; + } + + + /** + * Produce a comma delimited text from a JSONArray of JSONObjects. The + * first row will be a list of names obtained by inspecting the first + * JSONObject. + * @param ja A JSONArray of JSONObjects. + * @return A comma delimited text. + * @throws JSONException + */ + public static String toString(JSONArray ja) throws JSONException { + JSONObject jo = ja.optJSONObject(0); + if (jo != null) { + JSONArray names = jo.names(); + if (names != null) { + return rowToString(names) + toString(names, ja); + } + } + return null; + } + + /** + * Produce a comma delimited text from a JSONArray of JSONObjects using + * a provided list of names. The list of names is not included in the + * output. + * @param names A JSONArray of strings. + * @param ja A JSONArray of JSONObjects. + * @return A comma delimited text. + * @throws JSONException + */ + public static String toString(JSONArray names, JSONArray ja) + throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < ja.length(); i += 1) { + JSONObject jo = ja.optJSONObject(i); + if (jo != null) { + sb.append(rowToString(jo.toJSONArray(names))); + } + } + return sb.toString(); + } +} diff --git a/subsonic-main/src/main/java/org/json/Cookie.java b/subsonic-main/src/main/java/org/json/Cookie.java new file mode 100644 index 00000000..a2d9c4ed --- /dev/null +++ b/subsonic-main/src/main/java/org/json/Cookie.java @@ -0,0 +1,169 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * Convert a web browser cookie specification to a JSONObject and back. + * JSON and Cookies are both notations for name/value pairs. + * @author JSON.org + * @version 2010-12-24 + */ +public class Cookie { + + /** + * Produce a copy of a string in which the characters '+', '%', '=', ';' + * and control characters are replaced with "%hh". This is a gentle form + * of URL encoding, attempting to cause as little distortion to the + * string as possible. The characters '=' and ';' are meta characters in + * cookies. By convention, they are escaped using the URL-encoding. This is + * only a convention, not a standard. Often, cookies are expected to have + * encoded values. We encode '=' and ';' because we must. We encode '%' and + * '+' because they are meta characters in URL encoding. + * @param string The source string. + * @return The escaped result. + */ + public static String escape(String string) { + char c; + String s = string.trim(); + StringBuffer sb = new StringBuffer(); + int length = s.length(); + for (int i = 0; i < length; i += 1) { + c = s.charAt(i); + if (c < ' ' || c == '+' || c == '%' || c == '=' || c == ';') { + sb.append('%'); + sb.append(Character.forDigit((char)((c >>> 4) & 0x0f), 16)); + sb.append(Character.forDigit((char)(c & 0x0f), 16)); + } else { + sb.append(c); + } + } + return sb.toString(); + } + + + /** + * Convert a cookie specification string into a JSONObject. The string + * will contain a name value pair separated by '='. The name and the value + * will be unescaped, possibly converting '+' and '%' sequences. The + * cookie properties may follow, separated by ';', also represented as + * name=value (except the secure property, which does not have a value). + * The name will be stored under the key "name", and the value will be + * stored under the key "value". This method does not do checking or + * validation of the parameters. It only converts the cookie string into + * a JSONObject. + * @param string The cookie specification string. + * @return A JSONObject containing "name", "value", and possibly other + * members. + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + String name; + JSONObject jo = new JSONObject(); + Object value; + JSONTokener x = new JSONTokener(string); + jo.put("name", x.nextTo('=')); + x.next('='); + jo.put("value", x.nextTo(';')); + x.next(); + while (x.more()) { + name = unescape(x.nextTo("=;")); + if (x.next() != '=') { + if (name.equals("secure")) { + value = Boolean.TRUE; + } else { + throw x.syntaxError("Missing '=' in cookie parameter."); + } + } else { + value = unescape(x.nextTo(';')); + x.next(); + } + jo.put(name, value); + } + return jo; + } + + + /** + * Convert a JSONObject into a cookie specification string. The JSONObject + * must contain "name" and "value" members. + * If the JSONObject contains "expires", "domain", "path", or "secure" + * members, they will be appended to the cookie specification string. + * All other members are ignored. + * @param jo A JSONObject + * @return A cookie specification string + * @throws JSONException + */ + public static String toString(JSONObject jo) throws JSONException { + StringBuffer sb = new StringBuffer(); + + sb.append(escape(jo.getString("name"))); + sb.append("="); + sb.append(escape(jo.getString("value"))); + if (jo.has("expires")) { + sb.append(";expires="); + sb.append(jo.getString("expires")); + } + if (jo.has("domain")) { + sb.append(";domain="); + sb.append(escape(jo.getString("domain"))); + } + if (jo.has("path")) { + sb.append(";path="); + sb.append(escape(jo.getString("path"))); + } + if (jo.optBoolean("secure")) { + sb.append(";secure"); + } + return sb.toString(); + } + + /** + * Convert <code>%</code><i>hh</i> sequences to single characters, and + * convert plus to space. + * @param string A string that may contain + * <code>+</code> <small>(plus)</small> and + * <code>%</code><i>hh</i> sequences. + * @return The unescaped string. + */ + public static String unescape(String string) { + int length = string.length(); + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < length; ++i) { + char c = string.charAt(i); + if (c == '+') { + c = ' '; + } else if (c == '%' && i + 2 < length) { + int d = JSONTokener.dehexchar(string.charAt(i + 1)); + int e = JSONTokener.dehexchar(string.charAt(i + 2)); + if (d >= 0 && e >= 0) { + c = (char)(d * 16 + e); + i += 2; + } + } + sb.append(c); + } + return sb.toString(); + } +} diff --git a/subsonic-main/src/main/java/org/json/CookieList.java b/subsonic-main/src/main/java/org/json/CookieList.java new file mode 100644 index 00000000..1111135f --- /dev/null +++ b/subsonic-main/src/main/java/org/json/CookieList.java @@ -0,0 +1,90 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Iterator; + +/** + * Convert a web browser cookie list string to a JSONObject and back. + * @author JSON.org + * @version 2010-12-24 + */ +public class CookieList { + + /** + * Convert a cookie list into a JSONObject. A cookie list is a sequence + * of name/value pairs. The names are separated from the values by '='. + * The pairs are separated by ';'. The names and the values + * will be unescaped, possibly converting '+' and '%' sequences. + * + * To add a cookie to a cooklist, + * cookielistJSONObject.put(cookieJSONObject.getString("name"), + * cookieJSONObject.getString("value")); + * @param string A cookie list string + * @return A JSONObject + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + JSONTokener x = new JSONTokener(string); + while (x.more()) { + String name = Cookie.unescape(x.nextTo('=')); + x.next('='); + jo.put(name, Cookie.unescape(x.nextTo(';'))); + x.next(); + } + return jo; + } + + + /** + * Convert a JSONObject into a cookie list. A cookie list is a sequence + * of name/value pairs. The names are separated from the values by '='. + * The pairs are separated by ';'. The characters '%', '+', '=', and ';' + * in the names and values are replaced by "%hh". + * @param jo A JSONObject + * @return A cookie list string + * @throws JSONException + */ + public static String toString(JSONObject jo) throws JSONException { + boolean b = false; + Iterator keys = jo.keys(); + String string; + StringBuffer sb = new StringBuffer(); + while (keys.hasNext()) { + string = keys.next().toString(); + if (!jo.isNull(string)) { + if (b) { + sb.append(';'); + } + sb.append(Cookie.escape(string)); + sb.append("="); + sb.append(Cookie.escape(jo.getString(string))); + b = true; + } + } + return sb.toString(); + } +} diff --git a/subsonic-main/src/main/java/org/json/HTTP.java b/subsonic-main/src/main/java/org/json/HTTP.java new file mode 100644 index 00000000..cc8203d1 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/HTTP.java @@ -0,0 +1,163 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Iterator; + +/** + * Convert an HTTP header to a JSONObject and back. + * @author JSON.org + * @version 2010-12-24 + */ +public class HTTP { + + /** Carriage return/line feed. */ + public static final String CRLF = "\r\n"; + + /** + * Convert an HTTP header string into a JSONObject. It can be a request + * header or a response header. A request header will contain + * <pre>{ + * Method: "POST" (for example), + * "Request-URI": "/" (for example), + * "HTTP-Version": "HTTP/1.1" (for example) + * }</pre> + * A response header will contain + * <pre>{ + * "HTTP-Version": "HTTP/1.1" (for example), + * "Status-Code": "200" (for example), + * "Reason-Phrase": "OK" (for example) + * }</pre> + * In addition, the other parameters in the header will be captured, using + * the HTTP field names as JSON names, so that <pre> + * Date: Sun, 26 May 2002 18:06:04 GMT + * Cookie: Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s + * Cache-Control: no-cache</pre> + * become + * <pre>{... + * Date: "Sun, 26 May 2002 18:06:04 GMT", + * Cookie: "Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s", + * "Cache-Control": "no-cache", + * ...}</pre> + * It does no further checking or conversion. It does not parse dates. + * It does not do '%' transforms on URLs. + * @param string An HTTP header string. + * @return A JSONObject containing the elements and attributes + * of the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + HTTPTokener x = new HTTPTokener(string); + String token; + + token = x.nextToken(); + if (token.toUpperCase().startsWith("HTTP")) { + +// Response + + jo.put("HTTP-Version", token); + jo.put("Status-Code", x.nextToken()); + jo.put("Reason-Phrase", x.nextTo('\0')); + x.next(); + + } else { + +// Request + + jo.put("Method", token); + jo.put("Request-URI", x.nextToken()); + jo.put("HTTP-Version", x.nextToken()); + } + +// Fields + + while (x.more()) { + String name = x.nextTo(':'); + x.next(':'); + jo.put(name, x.nextTo('\0')); + x.next(); + } + return jo; + } + + + /** + * Convert a JSONObject into an HTTP header. A request header must contain + * <pre>{ + * Method: "POST" (for example), + * "Request-URI": "/" (for example), + * "HTTP-Version": "HTTP/1.1" (for example) + * }</pre> + * A response header must contain + * <pre>{ + * "HTTP-Version": "HTTP/1.1" (for example), + * "Status-Code": "200" (for example), + * "Reason-Phrase": "OK" (for example) + * }</pre> + * Any other members of the JSONObject will be output as HTTP fields. + * The result will end with two CRLF pairs. + * @param jo A JSONObject + * @return An HTTP header string. + * @throws JSONException if the object does not contain enough + * information. + */ + public static String toString(JSONObject jo) throws JSONException { + Iterator keys = jo.keys(); + String string; + StringBuffer sb = new StringBuffer(); + if (jo.has("Status-Code") && jo.has("Reason-Phrase")) { + sb.append(jo.getString("HTTP-Version")); + sb.append(' '); + sb.append(jo.getString("Status-Code")); + sb.append(' '); + sb.append(jo.getString("Reason-Phrase")); + } else if (jo.has("Method") && jo.has("Request-URI")) { + sb.append(jo.getString("Method")); + sb.append(' '); + sb.append('"'); + sb.append(jo.getString("Request-URI")); + sb.append('"'); + sb.append(' '); + sb.append(jo.getString("HTTP-Version")); + } else { + throw new JSONException("Not enough material for an HTTP header."); + } + sb.append(CRLF); + while (keys.hasNext()) { + string = keys.next().toString(); + if (!"HTTP-Version".equals(string) && !"Status-Code".equals(string) && + !"Reason-Phrase".equals(string) && !"Method".equals(string) && + !"Request-URI".equals(string) && !jo.isNull(string)) { + sb.append(string); + sb.append(": "); + sb.append(jo.getString(string)); + sb.append(CRLF); + } + } + sb.append(CRLF); + return sb.toString(); + } +} diff --git a/subsonic-main/src/main/java/org/json/HTTPTokener.java b/subsonic-main/src/main/java/org/json/HTTPTokener.java new file mode 100644 index 00000000..86fed61d --- /dev/null +++ b/subsonic-main/src/main/java/org/json/HTTPTokener.java @@ -0,0 +1,77 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The HTTPTokener extends the JSONTokener to provide additional methods + * for the parsing of HTTP headers. + * @author JSON.org + * @version 2010-12-24 + */ +public class HTTPTokener extends JSONTokener { + + /** + * Construct an HTTPTokener from a string. + * @param string A source string. + */ + public HTTPTokener(String string) { + super(string); + } + + + /** + * Get the next token or string. This is used in parsing HTTP headers. + * @throws JSONException + * @return A String. + */ + public String nextToken() throws JSONException { + char c; + char q; + StringBuffer sb = new StringBuffer(); + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == '"' || c == '\'') { + q = c; + for (;;) { + c = next(); + if (c < ' ') { + throw syntaxError("Unterminated string."); + } + if (c == q) { + return sb.toString(); + } + sb.append(c); + } + } + for (;;) { + if (c == 0 || Character.isWhitespace(c)) { + return sb.toString(); + } + sb.append(c); + c = next(); + } + } +} diff --git a/subsonic-main/src/main/java/org/json/JSONArray.java b/subsonic-main/src/main/java/org/json/JSONArray.java new file mode 100644 index 00000000..4ae610f0 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONArray.java @@ -0,0 +1,920 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having <code>get</code> and <code>opt</code> + * methods for accessing the values by index, and <code>put</code> methods for + * adding or replacing values. The values can be any of these types: + * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>, + * <code>Number</code>, <code>String</code>, or the + * <code>JSONObject.NULL object</code>. + * <p> + * The constructor can convert a JSON text into a Java object. The + * <code>toString</code> method converts to JSON text. + * <p> + * A <code>get</code> method returns a value if one can be found, and throws an + * exception if one cannot be found. An <code>opt</code> method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + * <p> + * The generic <code>get()</code> and <code>opt()</code> methods return an + * object which you can cast or query for type. There are also typed + * <code>get</code> and <code>opt</code> methods that do type checking and type + * coercion for you. + * <p> + * The texts produced by the <code>toString</code> methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + * <ul> + * <li>An extra <code>,</code> <small>(comma)</small> may appear just + * before the closing bracket.</li> + * <li>The <code>null</code> value will be inserted when there + * is <code>,</code> <small>(comma)</small> elision.</li> + * <li>Strings may be quoted with <code>'</code> <small>(single + * quote)</small>.</li> + * <li>Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, + * and if they do not contain any of these characters: + * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers + * and if they are not the reserved words <code>true</code>, + * <code>false</code>, or <code>null</code>.</li> + * <li>Values can be separated by <code>;</code> <small>(semicolon)</small> as + * well as by <code>,</code> <small>(comma)</small>.</li> + * </ul> + + * @author JSON.org + * @version 2011-12-19 + */ +public class JSONArray { + + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private final ArrayList myArrayList; + + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * @param x A JSONTokener + * @throws JSONException If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + if (x.nextClean() != ']') { + x.back(); + for (;;) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + + /** + * Construct a JSONArray from a source JSON text. + * @param source A string that begins with + * <code>[</code> <small>(left bracket)</small> + * and ends with <code>]</code> <small>(right bracket)</small>. + * @throws JSONException If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + + /** + * Construct a JSONArray from a Collection. + * @param collection A Collection. + */ + public JSONArray(Collection collection) { + this.myArrayList = new ArrayList(); + if (collection != null) { + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + this.myArrayList.add(JSONObject.wrap(iter.next())); + } + } + } + + + /** + * Construct a JSONArray from an array + * @throws JSONException If not an array. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new JSONException( +"JSONArray initial value should be a string or collection or array."); + } + } + + + /** + * Get the object value associated with an index. + * @param index + * The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + + /** + * Get the boolean value associated with an index. + * The string values "true" and "false" are converted to boolean. + * + * @param index The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException If there is no value for the index or if the + * value is not convertible to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONArray[" + index + "] is not a boolean."); + } + + + /** + * Get the double value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot + * be converted to a number. + */ + public double getDouble(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number + ? ((Number)object).doubleValue() + : Double.parseDouble((String)object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + + "] is not a number."); + } + } + + + /** + * Get the int value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value is not a number. + */ + public int getInt(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number + ? ((Number)object).intValue() + : Integer.parseInt((String)object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + + "] is not a number."); + } + } + + + /** + * Get the JSONArray associated with an index. + * @param index The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException If there is no value for the index. or if the + * value is not a JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray)object; + } + throw new JSONException("JSONArray[" + index + + "] is not a JSONArray."); + } + + + /** + * Get the JSONObject associated with an index. + * @param index subscript + * @return A JSONObject value. + * @throws JSONException If there is no value for the index or if the + * value is not a JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject)object; + } + throw new JSONException("JSONArray[" + index + + "] is not a JSONObject."); + } + + + /** + * Get the long value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot + * be converted to a number. + */ + public long getLong(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number + ? ((Number)object).longValue() + : Long.parseLong((String)object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + + "] is not a number."); + } + } + + + /** + * Get the string associated with an index. + * @param index The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException If there is no string value for the index. + */ + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String)object; + } + throw new JSONException("JSONArray[" + index + "] not a string."); + } + + + /** + * Determine if the value is null. + * @param index The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + + /** + * Make a string from the contents of this JSONArray. The + * <code>separator</code> string is inserted between each element. + * Warning: This method assumes that the data structure is acyclical. + * @param separator A string that will be inserted between the elements. + * @return a string. + * @throws JSONException If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = this.length(); + StringBuffer sb = new StringBuffer(); + + for (int i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(separator); + } + sb.append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return this.myArrayList.size(); + } + + + /** + * Get the optional object value associated with an index. + * @param index The index must be between 0 and length() - 1. + * @return An object value, or null if there is no + * object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= this.length()) + ? null + : this.myArrayList.get(index); + } + + + /** + * Get the optional boolean value associated with an index. + * It returns false if there is no value at that index, + * or if the value is not Boolean.TRUE or the String "true". + * + * @param index The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + + /** + * Get the optional boolean value associated with an index. + * It returns the defaultValue if there is no value at that index or if + * it is not a Boolean or the String "true" or "false" (case insensitive). + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional double value associated with an index. + * NaN is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + + /** + * Get the optional double value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index subscript + * @param defaultValue The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + try { + return this.getDouble(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional int value associated with an index. + * Zero is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return this.optInt(index, 0); + } + + + /** + * Get the optional int value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + try { + return this.getInt(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional JSONArray associated with an index. + * @param index subscript + * @return A JSONArray value, or null if the index has no value, + * or if the value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray)o : null; + } + + + /** + * Get the optional JSONObject associated with an index. + * Null is returned if the key is not found, or null if the index has + * no value, or if the value is not a JSONObject. + * + * @param index The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject)o : null; + } + + + /** + * Get the optional long value associated with an index. + * Zero is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return this.optLong(index, 0); + } + + + /** + * Get the optional long value associated with an index. + * The defaultValue is returned if there is no value for the index, + * or if the value is not a number and cannot be converted to a number. + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + try { + return this.getLong(index); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value + * is not a string and is not null, then it is coverted to a string. + * + * @param index The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return this.optString(index, ""); + } + + + /** + * Get the optional string associated with an index. + * The defaultValue is returned if the key is not found. + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) + ? defaultValue + : object.toString(); + } + + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + this.put(value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONArray which is produced from a Collection. + * @param value A Collection value. + * @return this. + */ + public JSONArray put(Collection value) { + this.put(new JSONArray(value)); + return this; + } + + + /** + * Append a double value. This increases the array's length by one. + * + * @param value A double value. + * @throws JSONException if the value is not finite. + * @return this. + */ + public JSONArray put(double value) throws JSONException { + Double d = new Double(value); + JSONObject.testValidity(d); + this.put(d); + return this; + } + + + /** + * Append an int value. This increases the array's length by one. + * + * @param value An int value. + * @return this. + */ + public JSONArray put(int value) { + this.put(new Integer(value)); + return this; + } + + + /** + * Append an long value. This increases the array's length by one. + * + * @param value A long value. + * @return this. + */ + public JSONArray put(long value) { + this.put(new Long(value)); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONObject which is produced from a Map. + * @param value A Map value. + * @return this. + */ + public JSONArray put(Map value) { + this.put(new JSONObject(value)); + return this; + } + + + /** + * Append an object value. This increases the array's length by one. + * @param value An object value. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + */ + public JSONArray put(Object value) { + this.myArrayList.add(value); + return this; + } + + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * @param index The subscript. + * @param value A boolean value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONArray which is produced from a Collection. + * @param index The subscript. + * @param value A Collection value. + * @return this. + * @throws JSONException If the index is negative or if the value is + * not finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + this.put(index, new JSONArray(value)); + return this; + } + + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value A double value. + * @return this. + * @throws JSONException If the index is negative or if the value is + * not finite. + */ + public JSONArray put(int index, double value) throws JSONException { + this.put(index, new Double(value)); + return this; + } + + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value An int value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + this.put(index, new Integer(value)); + return this; + } + + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad + * it out. + * @param index The subscript. + * @param value A long value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + this.put(index, new Long(value)); + return this; + } + + + /** + * Put a value in the JSONArray, where the value will be a + * JSONObject that is produced from a Map. + * @param index The subscript. + * @param value The Map value. + * @return this. + * @throws JSONException If the index is negative or if the the value is + * an invalid number. + */ + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * @param index The subscript. + * @param value The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + * @throws JSONException If the index is negative or if the the value is + * an invalid number. + */ + public JSONArray put(int index, Object value) throws JSONException { + JSONObject.testValidity(value); + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + this.myArrayList.set(index, value); + } else { + while (index != this.length()) { + this.put(JSONObject.NULL); + } + this.put(value); + } + return this; + } + + + /** + * Remove an index and close the hole. + * @param index The index of the element to be removed. + * @return The value that was associated with the index, + * or null if there was no value. + */ + public Object remove(int index) { + Object o = this.opt(index); + this.myArrayList.remove(index); + return o; + } + + + /** + * Produce a JSONObject by combining a JSONArray of names with the values + * of this JSONArray. + * @param names A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.length() == 0 || this.length() == 0) { + return null; + } + JSONObject jo = new JSONObject(); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + + /** + * Make a JSON text of this JSONArray. For compactness, no + * unnecessary whitespace is added. If it is not possible to produce a + * syntactically correct JSON text then null will be returned instead. This + * could occur if the array contains an invalid number. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, transmittable + * representation of the array. + */ + public String toString() { + try { + return '[' + this.join(",") + ']'; + } catch (Exception e) { + return null; + } + } + + + /** + * Make a prettyprinted JSON text of this JSONArray. + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with <code>[</code> <small>(left bracket)</small> and ending + * with <code>]</code> <small>(right bracket)</small>. + * @throws JSONException + */ + public String toString(int indentFactor) throws JSONException { + return this.toString(indentFactor, 0); + } + + + /** + * Make a prettyprinted JSON text of this JSONArray. + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @param indent The indention of the top level. + * @return a printable, displayable, transmittable + * representation of the array. + * @throws JSONException + */ + String toString(int indentFactor, int indent) throws JSONException { + int len = this.length(); + if (len == 0) { + return "[]"; + } + int i; + StringBuffer sb = new StringBuffer("["); + if (len == 1) { + sb.append(JSONObject.valueToString(this.myArrayList.get(0), + indentFactor, indent)); + } else { + int newindent = indent + indentFactor; + sb.append('\n'); + for (i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(",\n"); + } + for (int j = 0; j < newindent; j += 1) { + sb.append(' '); + } + sb.append(JSONObject.valueToString(this.myArrayList.get(i), + indentFactor, newindent)); + } + sb.append('\n'); + for (i = 0; i < indent; i += 1) { + sb.append(' '); + } + } + sb.append(']'); + return sb.toString(); + } + + + /** + * Write the contents of the JSONArray as JSON text to a writer. + * For compactness, no whitespace is added. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + try { + boolean b = false; + int len = this.length(); + + writer.write('['); + + for (int i = 0; i < len; i += 1) { + if (b) { + writer.write(','); + } + Object v = this.myArrayList.get(i); + if (v instanceof JSONObject) { + ((JSONObject)v).write(writer); + } else if (v instanceof JSONArray) { + ((JSONArray)v).write(writer); + } else { + writer.write(JSONObject.valueToString(v)); + } + b = true; + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/org/json/JSONException.java b/subsonic-main/src/main/java/org/json/JSONException.java new file mode 100644 index 00000000..3ec8fb99 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONException.java @@ -0,0 +1,28 @@ +package org.json; + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * @author JSON.org + * @version 2010-12-24 + */ +public class JSONException extends Exception { + private static final long serialVersionUID = 0; + private Throwable cause; + + /** + * Constructs a JSONException with an explanatory message. + * @param message Detail about the reason for the exception. + */ + public JSONException(String message) { + super(message); + } + + public JSONException(Throwable cause) { + super(cause.getMessage()); + this.cause = cause; + } + + public Throwable getCause() { + return this.cause; + } +} diff --git a/subsonic-main/src/main/java/org/json/JSONML.java b/subsonic-main/src/main/java/org/json/JSONML.java new file mode 100644 index 00000000..d20a9c3c --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONML.java @@ -0,0 +1,465 @@ +package org.json; + +/* +Copyright (c) 2008 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Iterator; + + +/** + * This provides static methods to convert an XML text into a JSONArray or + * JSONObject, and to covert a JSONArray or JSONObject into an XML text using + * the JsonML transform. + * @author JSON.org + * @version 2011-11-24 + */ +public class JSONML { + + /** + * Parse XML values and store them in a JSONArray. + * @param x The XMLTokener containing the source string. + * @param arrayForm true if array form, false if object form. + * @param ja The JSONArray that is containing the current tag or null + * if we are at the outermost level. + * @return A JSONArray if the value is the outermost tag, otherwise null. + * @throws JSONException + */ + private static Object parse( + XMLTokener x, + boolean arrayForm, + JSONArray ja + ) throws JSONException { + String attribute; + char c; + String closeTag = null; + int i; + JSONArray newja = null; + JSONObject newjo = null; + Object token; + String tagName = null; + +// Test for and skip past these forms: +// <!-- ... --> +// <![ ... ]]> +// <! ... > +// <? ... ?> + + while (true) { + if (!x.more()) { + throw x.syntaxError("Bad XML"); + } + token = x.nextContent(); + if (token == XML.LT) { + token = x.nextToken(); + if (token instanceof Character) { + if (token == XML.SLASH) { + +// Close tag </ + + token = x.nextToken(); + if (!(token instanceof String)) { + throw new JSONException( + "Expected a closing name instead of '" + + token + "'."); + } + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped close tag"); + } + return token; + } else if (token == XML.BANG) { + +// <! + + c = x.next(); + if (c == '-') { + if (x.next() == '-') { + x.skipPast("-->"); + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if (token.equals("CDATA") && x.next() == '[') { + if (ja != null) { + ja.put(x.nextCDATA()); + } + } else { + throw x.syntaxError("Expected 'CDATA['"); + } + } else { + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after '<!'."); + } else if (token == XML.LT) { + i += 1; + } else if (token == XML.GT) { + i -= 1; + } + } while (i > 0); + } + } else if (token == XML.QUEST) { + +// <? + + x.skipPast("?>"); + } else { + throw x.syntaxError("Misshaped tag"); + } + +// Open tag < + + } else { + if (!(token instanceof String)) { + throw x.syntaxError("Bad tagName '" + token + "'."); + } + tagName = (String)token; + newja = new JSONArray(); + newjo = new JSONObject(); + if (arrayForm) { + newja.put(tagName); + if (ja != null) { + ja.put(newja); + } + } else { + newjo.put("tagName", tagName); + if (ja != null) { + ja.put(newjo); + } + } + token = null; + for (;;) { + if (token == null) { + token = x.nextToken(); + } + if (token == null) { + throw x.syntaxError("Misshaped tag"); + } + if (!(token instanceof String)) { + break; + } + +// attribute = value + + attribute = (String)token; + if (!arrayForm && (attribute == "tagName" || attribute == "childNode")) { + throw x.syntaxError("Reserved attribute."); + } + token = x.nextToken(); + if (token == XML.EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + newjo.accumulate(attribute, XML.stringToValue((String)token)); + token = null; + } else { + newjo.accumulate(attribute, ""); + } + } + if (arrayForm && newjo.length() > 0) { + newja.put(newjo); + } + +// Empty tag <.../> + + if (token == XML.SLASH) { + if (x.nextToken() != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + if (ja == null) { + if (arrayForm) { + return newja; + } else { + return newjo; + } + } + +// Content, between <...> and </...> + + } else { + if (token != XML.GT) { + throw x.syntaxError("Misshaped tag"); + } + closeTag = (String)parse(x, arrayForm, newja); + if (closeTag != null) { + if (!closeTag.equals(tagName)) { + throw x.syntaxError("Mismatched '" + tagName + + "' and '" + closeTag + "'"); + } + tagName = null; + if (!arrayForm && newja.length() > 0) { + newjo.put("childNodes", newja); + } + if (ja == null) { + if (arrayForm) { + return newja; + } else { + return newjo; + } + } + } + } + } + } else { + if (ja != null) { + ja.put(token instanceof String + ? XML.stringToValue((String)token) + : token); + } + } + } + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child tags. + * Comments, prologs, DTDs, and <code><[ [ ]]></code> are ignored. + * @param string The source string. + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONArray toJSONArray(String string) throws JSONException { + return toJSONArray(new XMLTokener(string)); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONArray using the JsonML transform. Each XML tag is represented as + * a JSONArray in which the first element is the tag name. If the tag has + * attributes, then the second element will be JSONObject containing the + * name/value pairs. If the tag contains children, then strings and + * JSONArrays will represent the child content and tags. + * Comments, prologs, DTDs, and <code><[ [ ]]></code> are ignored. + * @param x An XMLTokener. + * @return A JSONArray containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONArray toJSONArray(XMLTokener x) throws JSONException { + return (JSONArray)parse(x, true, null); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and <code><[ [ ]]></code> are ignored. + * @param x An XMLTokener of the XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(XMLTokener x) throws JSONException { + return (JSONObject)parse(x, false, null); + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject using the JsonML transform. Each XML tag is represented as + * a JSONObject with a "tagName" property. If the tag has attributes, then + * the attributes will be in the JSONObject as properties. If the tag + * contains children, the object will have a "childNodes" property which + * will be an array of strings and JsonML JSONObjects. + + * Comments, prologs, DTDs, and <code><[ [ ]]></code> are ignored. + * @param string The XML source text. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + return toJSONObject(new XMLTokener(string)); + } + + + /** + * Reverse the JSONML transformation, making an XML text from a JSONArray. + * @param ja A JSONArray. + * @return An XML string. + * @throws JSONException + */ + public static String toString(JSONArray ja) throws JSONException { + int i; + JSONObject jo; + String key; + Iterator keys; + int length; + Object object; + StringBuffer sb = new StringBuffer(); + String tagName; + String value; + +// Emit <tagName + + tagName = ja.getString(0); + XML.noSpace(tagName); + tagName = XML.escape(tagName); + sb.append('<'); + sb.append(tagName); + + object = ja.opt(1); + if (object instanceof JSONObject) { + i = 2; + jo = (JSONObject)object; + +// Emit the attributes + + keys = jo.keys(); + while (keys.hasNext()) { + key = keys.next().toString(); + XML.noSpace(key); + value = jo.optString(key); + if (value != null) { + sb.append(' '); + sb.append(XML.escape(key)); + sb.append('='); + sb.append('"'); + sb.append(XML.escape(value)); + sb.append('"'); + } + } + } else { + i = 1; + } + +//Emit content in body + + length = ja.length(); + if (i >= length) { + sb.append('/'); + sb.append('>'); + } else { + sb.append('>'); + do { + object = ja.get(i); + i += 1; + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } + } + } while (i < length); + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } + + /** + * Reverse the JSONML transformation, making an XML text from a JSONObject. + * The JSONObject must contain a "tagName" property. If it has children, + * then it must have a "childNodes" property containing an array of objects. + * The other properties are attributes with string values. + * @param jo A JSONObject. + * @return An XML string. + * @throws JSONException + */ + public static String toString(JSONObject jo) throws JSONException { + StringBuffer sb = new StringBuffer(); + int i; + JSONArray ja; + String key; + Iterator keys; + int length; + Object object; + String tagName; + String value; + +//Emit <tagName + + tagName = jo.optString("tagName"); + if (tagName == null) { + return XML.escape(jo.toString()); + } + XML.noSpace(tagName); + tagName = XML.escape(tagName); + sb.append('<'); + sb.append(tagName); + +//Emit the attributes + + keys = jo.keys(); + while (keys.hasNext()) { + key = keys.next().toString(); + if (!"tagName".equals(key) && !"childNodes".equals(key)) { + XML.noSpace(key); + value = jo.optString(key); + if (value != null) { + sb.append(' '); + sb.append(XML.escape(key)); + sb.append('='); + sb.append('"'); + sb.append(XML.escape(value)); + sb.append('"'); + } + } + } + +//Emit content in body + + ja = jo.optJSONArray("childNodes"); + if (ja == null) { + sb.append('/'); + sb.append('>'); + } else { + sb.append('>'); + length = ja.length(); + for (i = 0; i < length; i += 1) { + object = ja.get(i); + if (object != null) { + if (object instanceof String) { + sb.append(XML.escape(object.toString())); + } else if (object instanceof JSONObject) { + sb.append(toString((JSONObject)object)); + } else if (object instanceof JSONArray) { + sb.append(toString((JSONArray)object)); + } else { + sb.append(object.toString()); + } + } + } + sb.append('<'); + sb.append('/'); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/org/json/JSONObject.java b/subsonic-main/src/main/java/org/json/JSONObject.java new file mode 100644 index 00000000..f8ee3590 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONObject.java @@ -0,0 +1,1630 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.IOException; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.ResourceBundle; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its + * external form is a string wrapped in curly braces with colons between the + * names and values, and commas between the values and names. The internal form + * is an object having <code>get</code> and <code>opt</code> methods for + * accessing the values by name, and <code>put</code> methods for adding or + * replacing values by name. The values can be any of these types: + * <code>Boolean</code>, <code>JSONArray</code>, <code>JSONObject</code>, + * <code>Number</code>, <code>String</code>, or the <code>JSONObject.NULL</code> + * object. A JSONObject constructor can be used to convert an external form + * JSON text into an internal form whose values can be retrieved with the + * <code>get</code> and <code>opt</code> methods, or to convert values into a + * JSON text using the <code>put</code> and <code>toString</code> methods. + * A <code>get</code> method returns a value if one can be found, and throws an + * exception if one cannot be found. An <code>opt</code> method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + * <p> + * The generic <code>get()</code> and <code>opt()</code> methods return an + * object, which you can cast or query for type. There are also typed + * <code>get</code> and <code>opt</code> methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + * <p> + * The <code>put</code> methods add or replace values in an object. For example, + * <pre>myString = new JSONObject().put("JSON", "Hello, World!").toString();</pre> + * produces the string <code>{"JSON": "Hello, World"}</code>. + * <p> + * The texts produced by the <code>toString</code> methods strictly conform to + * the JSON syntax rules. + * The constructors are more forgiving in the texts they will accept: + * <ul> + * <li>An extra <code>,</code> <small>(comma)</small> may appear just + * before the closing brace.</li> + * <li>Strings may be quoted with <code>'</code> <small>(single + * quote)</small>.</li> + * <li>Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, + * and if they do not contain any of these characters: + * <code>{ } [ ] / \ : , = ; #</code> and if they do not look like numbers + * and if they are not the reserved words <code>true</code>, + * <code>false</code>, or <code>null</code>.</li> + * <li>Keys can be followed by <code>=</code> or <code>=></code> as well as + * by <code>:</code>.</li> + * <li>Values can be followed by <code>;</code> <small>(semicolon)</small> as + * well as by <code>,</code> <small>(comma)</small>.</li> + * </ul> + * @author JSON.org + * @version 2011-11-24 + */ +public class JSONObject { + + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * @return NULL. + */ + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * @param object An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object + * or null. + */ + public boolean equals(Object object) { + return object == null || object == this; + } + + /** + * Get the "null" string value. + * @return The string "null". + */ + public String toString() { + return "null"; + } + } + + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + + /** + * It is sometimes more convenient and less ambiguous to have a + * <code>NULL</code> object than to use Java's <code>null</code> value. + * <code>JSONObject.NULL.equals(null)</code> returns <code>true</code>. + * <code>JSONObject.NULL.toString()</code> returns <code>"null"</code>. + */ + public static final Object NULL = new Null(); + + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + this.map = new HashMap(); + } + + + /** + * Construct a JSONObject from a subset of another JSONObject. + * An array of strings is used to identify the keys that should be copied. + * Missing keys are ignored. + * @param jo A JSONObject. + * @param names An array of strings. + * @throws JSONException + * @exception JSONException If a value is a non-finite number or if a name is duplicated. + */ + public JSONObject(JSONObject jo, String[] names) { + this(); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + + /** + * Construct a JSONObject from a JSONTokener. + * @param x A JSONTokener object containing the source string. + * @throws JSONException If there is a syntax error in the source string + * or a duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (;;) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + +// The key is followed by ':'. We will also tolerate '=' or '=>'. + + c = x.nextClean(); + if (c == '=') { + if (x.next() != '>') { + x.back(); + } + } else if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + this.putOnce(key, x.nextValue()); + +// Pairs are separated by ','. We will also tolerate ';'. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + + /** + * Construct a JSONObject from a Map. + * + * @param map A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + */ + public JSONObject(Map map) { + this.map = new HashMap(); + if (map != null) { + Iterator i = map.entrySet().iterator(); + while (i.hasNext()) { + Map.Entry e = (Map.Entry)i.next(); + Object value = e.getValue(); + if (value != null) { + this.map.put(e.getKey(), wrap(value)); + } + } + } + } + + + /** + * Construct a JSONObject from an Object using bean getters. + * It reflects on all of the public methods of the object. + * For each of the methods with no parameters and a name starting + * with <code>"get"</code> or <code>"is"</code> followed by an uppercase letter, + * the method is invoked, and a key and the value returned from the getter method + * are put into the new JSONObject. + * + * The key is formed by removing the <code>"get"</code> or <code>"is"</code> prefix. + * If the second remaining character is not upper case, then the first + * character is converted to lower case. + * + * For example, if an object has a method named <code>"getName"</code>, and + * if the result of calling <code>object.getName()</code> is <code>"Larry Fine"</code>, + * then the JSONObject will contain <code>"name": "Larry Fine"</code>. + * + * @param bean An object that has getter methods that should be used + * to make a JSONObject. + */ + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings + * from the names array, and the values will be the field values associated + * with those keys in the object. If a key is not found or not visible, + * then it will not be copied into the new JSONObject. + * @param object An object that has fields that should be used to make a + * JSONObject. + * @param names An array of strings, the names of the fields to be obtained + * from the object. + */ + public JSONObject(Object object, String names[]) { + this(); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + + /** + * Construct a JSONObject from a source JSON text string. + * This is the most commonly used JSONObject constructor. + * @param source A string beginning + * with <code>{</code> <small>(left brace)</small> and ending + * with <code>}</code> <small>(right brace)</small>. + * @exception JSONException If there is a syntax error in the source + * string or a duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + + /** + * Construct a JSONObject from a ResourceBundle. + * @param baseName The ResourceBundle base name. + * @param locale The Locale to load the ResourceBundle for. + * @throws JSONException If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key instanceof String) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String)key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String)key)); + } + } + } + + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a + * JSONArray is stored under the key to hold all of the accumulated values. + * If there is already a JSONArray, then the new value is appended to it. + * In contrast, the put method replaces the previous value. + * + * If only one value is accumulated that is not a JSONArray, then the + * result will be the same as using put. But if multiple values are + * accumulated, then the result will be like append. + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the value is an invalid number + * or if the key is null. + */ + public JSONObject accumulate( + String key, + Object value + ) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, value instanceof JSONArray + ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray)object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the key is null or if the current value + * associated with the key is not a JSONArray. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray)object).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + + /** + * Produce a string from a double. The string "null" will be returned if + * the number is not finite. + * @param d A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && + string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + + /** + * Get the value object associated with a key. + * + * @param key A key string. + * @return The object associated with the key. + * @throws JSONException if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + + "] not found."); + } + return object; + } + + + /** + * Get the boolean value associated with a key. + * + * @param key A key string. + * @return The truth. + * @throws JSONException + * if the value is not a Boolean or the String "true" or "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) || + (object instanceof String && + ((String)object).equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + + /** + * Get the double value associated with a key. + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or + * if the value is not a Number object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number + ? ((Number)object).doubleValue() + : Double.parseDouble((String)object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number."); + } + } + + + /** + * Get the int value associated with a key. + * + * @param key A key string. + * @return The integer value. + * @throws JSONException if the key is not found or if the value cannot + * be converted to an integer. + */ + public int getInt(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number + ? ((Number)object).intValue() + : Integer.parseInt((String)object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not an int."); + } + } + + + /** + * Get the JSONArray value associated with a key. + * + * @param key A key string. + * @return A JSONArray which is the value. + * @throws JSONException if the key is not found or + * if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray)object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + + /** + * Get the JSONObject value associated with a key. + * + * @param key A key string. + * @return A JSONObject which is the value. + * @throws JSONException if the key is not found or + * if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject)object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + + /** + * Get the long value associated with a key. + * + * @param key A key string. + * @return The long value. + * @throws JSONException if the key is not found or if the value cannot + * be converted to a long. + */ + public long getLong(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number + ? ((Number)object).longValue() + : Long.parseLong((String)object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a long."); + } + } + + + /** + * Get an array of field names from a JSONObject. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + int length = jo.length(); + if (length == 0) { + return null; + } + Iterator iterator = jo.keys(); + String[] names = new String[length]; + int i = 0; + while (iterator.hasNext()) { + names[i] = (String)iterator.next(); + i += 1; + } + return names; + } + + + /** + * Get an array of field names from an Object. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + + /** + * Get the string associated with a key. + * + * @param key A key string. + * @return A string which is the value. + * @throws JSONException if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String)object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] not a string."); + } + + + /** + * Determine if the JSONObject contains a specific key. + * @param key A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if + * it is an Integer, Long, Double, or Float, then add one to it. + * @param key A key string. + * @return this. + * @throws JSONException If there is already a property with this name + * that is not an Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, ((Integer)value).intValue() + 1); + } else if (value instanceof Long) { + this.put(key, ((Long)value).longValue() + 1); + } else if (value instanceof Double) { + this.put(key, ((Double)value).doubleValue() + 1); + } else if (value instanceof Float) { + this.put(key, ((Float)value).floatValue() + 1); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + + /** + * Determine if the value associated with the key is null or if there is + * no value. + * @param key A key string. + * @return true if there is no value associated with the key or if + * the value is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + + /** + * Get an enumeration of the keys of the JSONObject. + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.map.keySet().iterator(); + } + + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + JSONArray ja = new JSONArray(); + Iterator keys = this.keys(); + while (keys.hasNext()) { + ja.put(keys.next()); + } + return ja.length() == 0 ? null : ja; + } + + /** + * Produce a string from a Number. + * @param number A Number + * @return A String. + * @throws JSONException If n is a non-finite number. + */ + public static String numberToString(Number number) + throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + +// Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 && + string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + + /** + * Get an optional value associated with a key. + * @param key A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + + /** + * Get an optional boolean associated with a key. + * It returns false if there is no such key, or if the value is not + * Boolean.TRUE or the String "true". + * + * @param key A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + + /** + * Get an optional boolean associated with a key. + * It returns the defaultValue if there is no such key, or if it is not + * a Boolean or the String "true" or "false" (case insensitive). + * + * @param key A key string. + * @param defaultValue The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + try { + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional double associated with a key, + * or NaN if there is no such key or if its value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + + /** + * Get an optional double associated with a key, or the + * defaultValue if there is no such key or if its value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + try { + return this.getDouble(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional int value associated with a key, + * or zero if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + + /** + * Get an optional int value associated with a key, + * or the default if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + try { + return this.getInt(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional JSONArray associated with a key. + * It returns null if there is no such key, or if its value is not a + * JSONArray. + * + * @param key A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray)o : null; + } + + + /** + * Get an optional JSONObject associated with a key. + * It returns null if there is no such key, or if its value is not a + * JSONObject. + * + * @param key A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject)object : null; + } + + + /** + * Get an optional long value associated with a key, + * or zero if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + + /** + * Get an optional long value associated with a key, + * or the default if there is no such key or if the value is not a number. + * If the value is a string, an attempt will be made to evaluate it as + * a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + try { + return this.getLong(key); + } catch (Exception e) { + return defaultValue; + } + } + + + /** + * Get an optional string associated with a key. + * It returns an empty string if there is no such key. If the value is not + * a string and is not null, then it is converted to a string. + * + * @param key A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + + /** + * Get an optional string associated with a key. + * It returns the defaultValue if there is no such key. + * + * @param key A key string. + * @param defaultValue The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + + private void populateMap(Object bean) { + Class klass = bean.getClass(); + +// If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass + ? klass.getMethods() + : klass.getDeclaredMethods(); + for (int i = 0; i < methods.length; i += 1) { + try { + Method method = methods[i]; + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName(); + String key = ""; + if (name.startsWith("get")) { + if ("getClass".equals(name) || + "getDeclaringClass".equals(name)) { + key = ""; + } else { + key = name.substring(3); + } + } else if (name.startsWith("is")) { + key = name.substring(2); + } + if (key.length() > 0 && + Character.isUpperCase(key.charAt(0)) && + method.getParameterTypes().length == 0) { + if (key.length() == 1) { + key = key.toLowerCase(); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase() + + key.substring(1); + } + + Object result = method.invoke(bean, (Object[])null); + if (result != null) { + this.map.put(key, wrap(result)); + } + } + } + } catch (Exception ignore) { + } + } + } + + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key A key string. + * @param value A boolean which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * @param key A key string. + * @param value A Collection value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Collection value) throws JSONException { + this.put(key, new JSONArray(value)); + return this; + } + + + /** + * Put a key/double pair in the JSONObject. + * + * @param key A key string. + * @param value A double which is the value. + * @return this. + * @throws JSONException If the key is null or if the number is invalid. + */ + public JSONObject put(String key, double value) throws JSONException { + this.put(key, new Double(value)); + return this; + } + + + /** + * Put a key/int pair in the JSONObject. + * + * @param key A key string. + * @param value An int which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + this.put(key, new Integer(value)); + return this; + } + + + /** + * Put a key/long pair in the JSONObject. + * + * @param key A key string. + * @param value A long which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + this.put(key, new Long(value)); + return this; + } + + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * @param key A key string. + * @param value A Map value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Map value) throws JSONException { + this.put(key, new JSONObject(value)); + return this; + } + + + /** + * Put a key/value pair in the JSONObject. If the value is null, + * then the key will be removed from the JSONObject if it is present. + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, + * or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is non-finite number + * or if the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + + /** + * Put a key/value pair in the JSONObject, but only if the key and the + * value are both non-null, and only if there is not already a member + * with that name. + * @param key + * @param value + * @return his. + * @throws JSONException if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + this.put(key, value); + } + return this; + } + + + /** + * Put a key/value pair in the JSONObject, but only if the + * key and the value are both non-null. + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, String, + * or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + this.put(key, value); + } + return this; + } + + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within </, producing <\/, + * allowing JSON text to be delivered in HTML. In JSON text, a string + * cannot contain a control character or an unescaped quote or backslash. + * @param string A String + * @return A String correctly formatted for insertion in a JSON text. + */ + public static String quote(String string) { + if (string == null || string.length() == 0) { + return "\"\""; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + StringBuffer sb = new StringBuffer(len + 4); + + sb.append('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + sb.append('\\'); + sb.append(c); + break; + case '/': + if (b == '<') { + sb.append('\\'); + } + sb.append(c); + break; + case '\b': + sb.append("\\b"); + break; + case '\t': + sb.append("\\t"); + break; + case '\n': + sb.append("\\n"); + break; + case '\f': + sb.append("\\f"); + break; + case '\r': + sb.append("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') || + (c >= '\u2000' && c < '\u2100')) { + hhhh = "000" + Integer.toHexString(c); + sb.append("\\u" + hhhh.substring(hhhh.length() - 4)); + } else { + sb.append(c); + } + } + } + sb.append('"'); + return sb.toString(); + } + + /** + * Remove a name and its value, if present. + * @param key The name to be removed. + * @return The value that was associated with the name, + * or null if there was no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * @param string A String. + * @return A simple JSON value. + */ + public static Object stringToValue(String string) { + Double d; + if (string.equals("")) { + return string; + } + if (string.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (string.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (string.equalsIgnoreCase("null")) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. + * If a number cannot be produced, then the value will just + * be a string. Note that the plus and implied string + * conventions are non-standard. A JSON parser may accept + * non-JSON forms as long as it accepts all correct JSON forms. + */ + + char b = string.charAt(0); + if ((b >= '0' && b <= '9') || b == '.' || b == '-' || b == '+') { + try { + if (string.indexOf('.') > -1 || + string.indexOf('e') > -1 || string.indexOf('E') > -1) { + d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = new Long(string); + if (myLong.longValue() == myLong.intValue()) { + return new Integer(myLong.intValue()); + } else { + return myLong; + } + } + } catch (Exception ignore) { + } + } + return string; + } + + + /** + * Throw an exception if the object is a NaN or infinite number. + * @param o The object to test. + * @throws JSONException If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double)o).isInfinite() || ((Double)o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float)o).isInfinite() || ((Float)o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * @param names A JSONArray containing a list of key strings. This + * determines the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace + * is added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable + * representation of the object, beginning + * with <code>{</code> <small>(left brace)</small> and ending + * with <code>}</code> <small>(right brace)</small>. + */ + public String toString() { + try { + Iterator keys = this.keys(); + StringBuffer sb = new StringBuffer("{"); + + while (keys.hasNext()) { + if (sb.length() > 1) { + sb.append(','); + } + Object o = keys.next(); + sb.append(quote(o.toString())); + sb.append(':'); + sb.append(valueToString(this.map.get(o))); + } + sb.append('}'); + return sb.toString(); + } catch (Exception e) { + return null; + } + } + + + /** + * Make a prettyprinted JSON text of this JSONObject. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @return a printable, displayable, portable, transmittable + * representation of the object, beginning + * with <code>{</code> <small>(left brace)</small> and ending + * with <code>}</code> <small>(right brace)</small>. + * @throws JSONException If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + return this.toString(indentFactor, 0); + } + + + /** + * Make a prettyprinted JSON text of this JSONObject. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @param indent The indentation of the top level. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with <code>{</code> <small>(left brace)</small> and ending + * with <code>}</code> <small>(right brace)</small>. + * @throws JSONException If the object contains an invalid number. + */ + String toString(int indentFactor, int indent) throws JSONException { + int i; + int length = this.length(); + if (length == 0) { + return "{}"; + } + Iterator keys = this.keys(); + int newindent = indent + indentFactor; + Object object; + StringBuffer sb = new StringBuffer("{"); + if (length == 1) { + object = keys.next(); + sb.append(quote(object.toString())); + sb.append(": "); + sb.append(valueToString(this.map.get(object), indentFactor, + indent)); + } else { + while (keys.hasNext()) { + object = keys.next(); + if (sb.length() > 1) { + sb.append(",\n"); + } else { + sb.append('\n'); + } + for (i = 0; i < newindent; i += 1) { + sb.append(' '); + } + sb.append(quote(object.toString())); + sb.append(": "); + sb.append(valueToString(this.map.get(object), indentFactor, + newindent)); + } + if (sb.length() > 1) { + sb.append('\n'); + for (i = 0; i < indent; i += 1) { + sb.append(' '); + } + } + } + sb.append('}'); + return sb.toString(); + } + + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce + * the JSON text. The method is required to produce a strictly + * conforming text. If the object does not contain a toJSONString + * method (which is the most common case), then a text will be + * produced by other means. If the value is an array or Collection, + * then a JSONArray will be made from it and its toJSONString method + * will be called. If the value is a MAP, then a JSONObject will be made + * from it and its toJSONString method will be called. Otherwise, the + * value's toString method will be called, and the result will be quoted. + * + * <p> + * Warning: This method assumes that the data structure is acyclical. + * @param value The value to be serialized. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with <code>{</code> <small>(left brace)</small> and ending + * with <code>}</code> <small>(right brace)</small>. + * @throws JSONException If the value is or contains an invalid number. + */ + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + Object object; + try { + object = ((JSONString)value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object instanceof String) { + return (String)object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean || value instanceof JSONObject || + value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + return new JSONObject((Map)value).toString(); + } + if (value instanceof Collection) { + return new JSONArray((Collection)value).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + return quote(value.toString()); + } + + + /** + * Make a prettyprinted JSON text of an object value. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * @param value The value to be serialized. + * @param indentFactor The number of spaces to add to each level of + * indentation. + * @param indent The indentation of the top level. + * @return a printable, displayable, transmittable + * representation of the object, beginning + * with <code>{</code> <small>(left brace)</small> and ending + * with <code>}</code> <small>(right brace)</small>. + * @throws JSONException If the object contains an invalid number. + */ + static String valueToString( + Object value, + int indentFactor, + int indent + ) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + try { + if (value instanceof JSONString) { + Object o = ((JSONString)value).toJSONString(); + if (o instanceof String) { + return (String)o; + } + } + } catch (Exception ignore) { + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean) { + return value.toString(); + } + if (value instanceof JSONObject) { + return ((JSONObject)value).toString(indentFactor, indent); + } + if (value instanceof JSONArray) { + return ((JSONArray)value).toString(indentFactor, indent); + } + if (value instanceof Map) { + return new JSONObject((Map)value).toString(indentFactor, indent); + } + if (value instanceof Collection) { + return new JSONArray((Collection)value).toString(indentFactor, indent); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(indentFactor, indent); + } + return quote(value.toString()); + } + + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If + * it is a map, wrap it in a JSONObject. If it is a standard property + * (Double, String, et al) then it is already wrapped. Otherwise, if it + * comes from one of the java packages, turn it into a string. And if + * it doesn't, try to wrap it in a JSONObject. If the wrapping fails, + * then null is returned. + * + * @param object The object to wrap + * @return The wrapped value + */ + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray || + NULL.equals(object) || object instanceof JSONString || + object instanceof Byte || object instanceof Character || + object instanceof Short || object instanceof Integer || + object instanceof Long || object instanceof Boolean || + object instanceof Float || object instanceof Double || + object instanceof String) { + return object; + } + + if (object instanceof Collection) { + return new JSONArray((Collection)object); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + return new JSONObject((Map)object); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null + ? objectPackage.getName() + : ""; + if ( + objectPackageName.startsWith("java.") || + objectPackageName.startsWith("javax.") || + object.getClass().getClassLoader() == null + ) { + return object.toString(); + } + return new JSONObject(object); + } catch(Exception exception) { + return null; + } + } + + + /** + * Write the contents of the JSONObject as JSON text to a writer. + * For compactness, no whitespace is added. + * <p> + * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + try { + boolean commanate = false; + Iterator keys = this.keys(); + writer.write('{'); + + while (keys.hasNext()) { + if (commanate) { + writer.write(','); + } + Object key = keys.next(); + writer.write(quote(key.toString())); + writer.write(':'); + Object value = this.map.get(key); + if (value instanceof JSONObject) { + ((JSONObject)value).write(writer); + } else if (value instanceof JSONArray) { + ((JSONArray)value).write(writer); + } else { + writer.write(valueToString(value)); + } + commanate = true; + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/org/json/JSONString.java b/subsonic-main/src/main/java/org/json/JSONString.java new file mode 100644 index 00000000..6efd68e7 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONString.java @@ -0,0 +1,18 @@ +package org.json; +/** + * The <code>JSONString</code> interface allows a <code>toJSONString()</code> + * method so that a class can change the behavior of + * <code>JSONObject.toString()</code>, <code>JSONArray.toString()</code>, + * and <code>JSONWriter.value(</code>Object<code>)</code>. The + * <code>toJSONString</code> method will be used instead of the default behavior + * of using the Object's <code>toString()</code> method and quoting the result. + */ +public interface JSONString { + /** + * The <code>toJSONString</code> method allows a class to produce its own JSON + * serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/subsonic-main/src/main/java/org/json/JSONStringer.java b/subsonic-main/src/main/java/org/json/JSONStringer.java new file mode 100644 index 00000000..32c9f7f4 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONStringer.java @@ -0,0 +1,78 @@ +package org.json; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.io.StringWriter; + +/** + * JSONStringer provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONStringer can produce one JSON text. + * <p> + * A JSONStringer instance provides a <code>value</code> method for appending + * values to the + * text, and a <code>key</code> + * method for adding keys before values in objects. There are <code>array</code> + * and <code>endArray</code> methods that make and bound array values, and + * <code>object</code> and <code>endObject</code> methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting cascade style. For example, <pre> + * myString = new JSONStringer() + * .object() + * .key("JSON") + * .value("Hello, World!") + * .endObject() + * .toString();</pre> which produces the string <pre> + * {"JSON":"Hello, World!"}</pre> + * <p> + * The first method called must be <code>array</code> or <code>object</code>. + * There are no methods for adding commas or colons. JSONStringer adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + * <p> + * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2008-09-18 + */ +public class JSONStringer extends JSONWriter { + /** + * Make a fresh JSONStringer. It can be used to build one JSON text. + */ + public JSONStringer() { + super(new StringWriter()); + } + + /** + * Return the JSON text. This method is used to obtain the product of the + * JSONStringer instance. It will return <code>null</code> if there was a + * problem in the construction of the JSON text (such as the calls to + * <code>array</code> were not properly balanced with calls to + * <code>endArray</code>). + * @return The JSON text. + */ + public String toString() { + return this.mode == 'd' ? this.writer.toString() : null; + } +} diff --git a/subsonic-main/src/main/java/org/json/JSONTokener.java b/subsonic-main/src/main/java/org/json/JSONTokener.java new file mode 100644 index 00000000..f323f6e6 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONTokener.java @@ -0,0 +1,446 @@ +package org.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * @author JSON.org + * @version 2011-11-24 + */ +public class JSONTokener { + + private int character; + private boolean eof; + private int index; + private int line; + private char previous; + private final Reader reader; + private boolean usePrevious; + + + /** + * Construct a JSONTokener from a Reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.line = 1; + } + + + /** + * Construct a JSONTokener from an InputStream. + */ + public JSONTokener(InputStream inputStream) throws JSONException { + this(new InputStreamReader(inputStream)); + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + */ + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.index -= 1; + this.character -= 1; + this.usePrevious = true; + this.eof = false; + } + + + /** + * Get the hex value of a character (base16). + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return this.eof && !this.usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * @return true if not yet at the end of the source. + */ + public boolean more() throws JSONException { + this.next(); + if (this.end()) { + return false; + } + this.back(); + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + + if (c <= 0) { // End of stream + this.eof = true; + c = 0; + } + } + this.index += 1; + if (this.previous == '\r') { + this.line += 1; + this.character = c == '\n' ? 0 : 1; + } else if (c == '\n') { + this.line += 1; + this.character = 0; + } else { + this.character += 1; + } + this.previous = (char) c; + return this.previous; + } + + + /** + * Consume the next character, and check that it matches a specified + * character. + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException + * Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + + /** + * Get the next char in the string, skipping whitespace. + * @throws JSONException + * @return A character, or 0 if there are no more characters. + */ + public char nextClean() throws JSONException { + for (;;) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * @param quote The quoting character, either + * <code>"</code> <small>(double quote)</small> or + * <code>'</code> <small>(single quote)</small>. + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char)Integer.parseInt(this.next(4), 16)); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * @param delimiter A delimiter character. + * @return A string. + */ + public String nextTo(char delimiter) throws JSONException { + StringBuffer sb = new StringBuffer(); + for (;;) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * @throws JSONException If syntax error. + * + * @return An object. + */ + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuffer sb = new StringBuffer(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + this.back(); + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + */ + public char skipTo(char to) throws JSONException { + char c; + try { + int startIndex = this.index; + int startCharacter = this.character; + int startLine = this.line; + this.reader.mark(Integer.MAX_VALUE); + do { + c = this.next(); + if (c == 0) { + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return c; + } + } while (c != to); + } catch (IOException exc) { + throw new JSONException(exc); + } + + this.back(); + return c; + } + + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/org/json/JSONWriter.java b/subsonic-main/src/main/java/org/json/JSONWriter.java new file mode 100644 index 00000000..35b60d90 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/JSONWriter.java @@ -0,0 +1,327 @@ +package org.json; + +import java.io.IOException; +import java.io.Writer; + +/* +Copyright (c) 2006 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * JSONWriter provides a quick and convenient way of producing JSON text. + * The texts produced strictly conform to JSON syntax rules. No whitespace is + * added, so the results are ready for transmission or storage. Each instance of + * JSONWriter can produce one JSON text. + * <p> + * A JSONWriter instance provides a <code>value</code> method for appending + * values to the + * text, and a <code>key</code> + * method for adding keys before values in objects. There are <code>array</code> + * and <code>endArray</code> methods that make and bound array values, and + * <code>object</code> and <code>endObject</code> methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting a cascade style. For example, <pre> + * new JSONWriter(myWriter) + * .object() + * .key("JSON") + * .value("Hello, World!") + * .endObject();</pre> which writes <pre> + * {"JSON":"Hello, World!"}</pre> + * <p> + * The first method called must be <code>array</code> or <code>object</code>. + * There are no methods for adding commas or colons. JSONWriter adds them for + * you. Objects and arrays can be nested up to 20 levels deep. + * <p> + * This can sometimes be easier than using a JSONObject to build a string. + * @author JSON.org + * @version 2011-11-24 + */ +public class JSONWriter { + private static final int maxdepth = 200; + + /** + * The comma flag determines if a comma should be output before the next + * value. + */ + private boolean comma; + + /** + * The current mode. Values: + * 'a' (array), + * 'd' (done), + * 'i' (initial), + * 'k' (key), + * 'o' (object). + */ + protected char mode; + + /** + * The object/array stack. + */ + private final JSONObject stack[]; + + /** + * The stack top index. A value of 0 indicates that the stack is empty. + */ + private int top; + + /** + * The writer that will receive the output. + */ + protected Writer writer; + + /** + * Make a fresh JSONWriter. It can be used to build one JSON text. + */ + public JSONWriter(Writer w) { + this.comma = false; + this.mode = 'i'; + this.stack = new JSONObject[maxdepth]; + this.top = 0; + this.writer = w; + } + + /** + * Append a value. + * @param string A string value. + * @return this + * @throws JSONException If the value is out of sequence. + */ + private JSONWriter append(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null pointer"); + } + if (this.mode == 'o' || this.mode == 'a') { + try { + if (this.comma && this.mode == 'a') { + this.writer.write(','); + } + this.writer.write(string); + } catch (IOException e) { + throw new JSONException(e); + } + if (this.mode == 'o') { + this.mode = 'k'; + } + this.comma = true; + return this; + } + throw new JSONException("Value out of sequence."); + } + + /** + * Begin appending a new array. All values until the balancing + * <code>endArray</code> will be appended to this array. The + * <code>endArray</code> method must be called to mark the array's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter array() throws JSONException { + if (this.mode == 'i' || this.mode == 'o' || this.mode == 'a') { + this.push(null); + this.append("["); + this.comma = false; + return this; + } + throw new JSONException("Misplaced array."); + } + + /** + * End something. + * @param mode Mode + * @param c Closing character + * @return this + * @throws JSONException If unbalanced. + */ + private JSONWriter end(char mode, char c) throws JSONException { + if (this.mode != mode) { + throw new JSONException(mode == 'a' + ? "Misplaced endArray." + : "Misplaced endObject."); + } + this.pop(mode); + try { + this.writer.write(c); + } catch (IOException e) { + throw new JSONException(e); + } + this.comma = true; + return this; + } + + /** + * End an array. This method most be called to balance calls to + * <code>array</code>. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endArray() throws JSONException { + return this.end('a', ']'); + } + + /** + * End an object. This method most be called to balance calls to + * <code>object</code>. + * @return this + * @throws JSONException If incorrectly nested. + */ + public JSONWriter endObject() throws JSONException { + return this.end('k', '}'); + } + + /** + * Append a key. The key will be associated with the next value. In an + * object, every value must be preceded by a key. + * @param string A key string. + * @return this + * @throws JSONException If the key is out of place. For example, keys + * do not belong in arrays or if the key is null. + */ + public JSONWriter key(String string) throws JSONException { + if (string == null) { + throw new JSONException("Null key."); + } + if (this.mode == 'k') { + try { + this.stack[this.top - 1].putOnce(string, Boolean.TRUE); + if (this.comma) { + this.writer.write(','); + } + this.writer.write(JSONObject.quote(string)); + this.writer.write(':'); + this.comma = false; + this.mode = 'o'; + return this; + } catch (IOException e) { + throw new JSONException(e); + } + } + throw new JSONException("Misplaced key."); + } + + + /** + * Begin appending a new object. All keys and values until the balancing + * <code>endObject</code> will be appended to this object. The + * <code>endObject</code> method must be called to mark the object's end. + * @return this + * @throws JSONException If the nesting is too deep, or if the object is + * started in the wrong place (for example as a key or after the end of the + * outermost array or object). + */ + public JSONWriter object() throws JSONException { + if (this.mode == 'i') { + this.mode = 'o'; + } + if (this.mode == 'o' || this.mode == 'a') { + this.append("{"); + this.push(new JSONObject()); + this.comma = false; + return this; + } + throw new JSONException("Misplaced object."); + + } + + + /** + * Pop an array or object scope. + * @param c The scope to close. + * @throws JSONException If nesting is wrong. + */ + private void pop(char c) throws JSONException { + if (this.top <= 0) { + throw new JSONException("Nesting error."); + } + char m = this.stack[this.top - 1] == null ? 'a' : 'k'; + if (m != c) { + throw new JSONException("Nesting error."); + } + this.top -= 1; + this.mode = this.top == 0 + ? 'd' + : this.stack[this.top - 1] == null + ? 'a' + : 'k'; + } + + /** + * Push an array or object scope. + * @param c The scope to open. + * @throws JSONException If nesting is too deep. + */ + private void push(JSONObject jo) throws JSONException { + if (this.top >= maxdepth) { + throw new JSONException("Nesting too deep."); + } + this.stack[this.top] = jo; + this.mode = jo == null ? 'a' : 'k'; + this.top += 1; + } + + + /** + * Append either the value <code>true</code> or the value + * <code>false</code>. + * @param b A boolean. + * @return this + * @throws JSONException + */ + public JSONWriter value(boolean b) throws JSONException { + return this.append(b ? "true" : "false"); + } + + /** + * Append a double value. + * @param d A double. + * @return this + * @throws JSONException If the number is not finite. + */ + public JSONWriter value(double d) throws JSONException { + return this.value(new Double(d)); + } + + /** + * Append a long value. + * @param l A long. + * @return this + * @throws JSONException + */ + public JSONWriter value(long l) throws JSONException { + return this.append(Long.toString(l)); + } + + + /** + * Append an object value. + * @param object The object to append. It can be null, or a Boolean, Number, + * String, JSONObject, or JSONArray, or an object that implements JSONString. + * @return this + * @throws JSONException If the value is out of sequence. + */ + public JSONWriter value(Object object) throws JSONException { + return this.append(JSONObject.valueToString(object)); + } +} diff --git a/subsonic-main/src/main/java/org/json/XML.java b/subsonic-main/src/main/java/org/json/XML.java new file mode 100644 index 00000000..82455b33 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/XML.java @@ -0,0 +1,508 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +import java.util.Iterator; + + +/** + * This provides static methods to convert an XML text into a JSONObject, + * and to covert a JSONObject into an XML text. + * @author JSON.org + * @version 2011-02-11 + */ +public class XML { + + /** The Character '&'. */ + public static final Character AMP = new Character('&'); + + /** The Character '''. */ + public static final Character APOS = new Character('\''); + + /** The Character '!'. */ + public static final Character BANG = new Character('!'); + + /** The Character '='. */ + public static final Character EQ = new Character('='); + + /** The Character '>'. */ + public static final Character GT = new Character('>'); + + /** The Character '<'. */ + public static final Character LT = new Character('<'); + + /** The Character '?'. */ + public static final Character QUEST = new Character('?'); + + /** The Character '"'. */ + public static final Character QUOT = new Character('"'); + + /** The Character '/'. */ + public static final Character SLASH = new Character('/'); + + /** + * Replace special characters with XML escapes: + * <pre> + * & <small>(ampersand)</small> is replaced by &amp; + * < <small>(less than)</small> is replaced by &lt; + * > <small>(greater than)</small> is replaced by &gt; + * " <small>(double quote)</small> is replaced by &quot; + * </pre> + * @param string The string to be escaped. + * @return The escaped string. + */ + public static String escape(String string) { + StringBuffer sb = new StringBuffer(); + for (int i = 0, length = string.length(); i < length; i++) { + char c = string.charAt(i); + switch (c) { + case '&': + sb.append("&"); + break; + case '<': + sb.append("<"); + break; + case '>': + sb.append(">"); + break; + case '"': + sb.append("""); + break; + case '\'': + sb.append("'"); + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Throw an exception if the string contains whitespace. + * Whitespace is not allowed in tagNames and attributes. + * @param string + * @throws JSONException + */ + public static void noSpace(String string) throws JSONException { + int i, length = string.length(); + if (length == 0) { + throw new JSONException("Empty string."); + } + for (i = 0; i < length; i += 1) { + if (Character.isWhitespace(string.charAt(i))) { + throw new JSONException("'" + string + + "' contains a space character."); + } + } + } + + /** + * Scan the content following the named tag, attaching it to the context. + * @param x The XMLTokener containing the source string. + * @param context The JSONObject that will include the new material. + * @param name The tag name. + * @return true if the close tag is processed. + * @throws JSONException + */ + private static boolean parse(XMLTokener x, JSONObject context, + String name) throws JSONException { + char c; + int i; + JSONObject jsonobject = null; + String string; + String tagName; + Object token; + +// Test for and skip past these forms: +// <!-- ... --> +// <! ... > +// <![ ... ]]> +// <? ... ?> +// Report errors for these forms: +// <> +// <= +// << + + token = x.nextToken(); + +// <! + + if (token == BANG) { + c = x.next(); + if (c == '-') { + if (x.next() == '-') { + x.skipPast("-->"); + return false; + } + x.back(); + } else if (c == '[') { + token = x.nextToken(); + if ("CDATA".equals(token)) { + if (x.next() == '[') { + string = x.nextCDATA(); + if (string.length() > 0) { + context.accumulate("content", string); + } + return false; + } + } + throw x.syntaxError("Expected 'CDATA['"); + } + i = 1; + do { + token = x.nextMeta(); + if (token == null) { + throw x.syntaxError("Missing '>' after '<!'."); + } else if (token == LT) { + i += 1; + } else if (token == GT) { + i -= 1; + } + } while (i > 0); + return false; + } else if (token == QUEST) { + +// <? + + x.skipPast("?>"); + return false; + } else if (token == SLASH) { + +// Close tag </ + + token = x.nextToken(); + if (name == null) { + throw x.syntaxError("Mismatched close tag " + token); + } + if (!token.equals(name)) { + throw x.syntaxError("Mismatched " + name + " and " + token); + } + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped close tag"); + } + return true; + + } else if (token instanceof Character) { + throw x.syntaxError("Misshaped tag"); + +// Open tag < + + } else { + tagName = (String)token; + token = null; + jsonobject = new JSONObject(); + for (;;) { + if (token == null) { + token = x.nextToken(); + } + +// attribute = value + + if (token instanceof String) { + string = (String)token; + token = x.nextToken(); + if (token == EQ) { + token = x.nextToken(); + if (!(token instanceof String)) { + throw x.syntaxError("Missing value"); + } + jsonobject.accumulate(string, + XML.stringToValue((String)token)); + token = null; + } else { + jsonobject.accumulate(string, ""); + } + +// Empty tag <.../> + + } else if (token == SLASH) { + if (x.nextToken() != GT) { + throw x.syntaxError("Misshaped tag"); + } + if (jsonobject.length() > 0) { + context.accumulate(tagName, jsonobject); + } else { + context.accumulate(tagName, ""); + } + return false; + +// Content, between <...> and </...> + + } else if (token == GT) { + for (;;) { + token = x.nextContent(); + if (token == null) { + if (tagName != null) { + throw x.syntaxError("Unclosed tag " + tagName); + } + return false; + } else if (token instanceof String) { + string = (String)token; + if (string.length() > 0) { + jsonobject.accumulate("content", + XML.stringToValue(string)); + } + +// Nested element + + } else if (token == LT) { + if (parse(x, jsonobject, tagName)) { + if (jsonobject.length() == 0) { + context.accumulate(tagName, ""); + } else if (jsonobject.length() == 1 && + jsonobject.opt("content") != null) { + context.accumulate(tagName, + jsonobject.opt("content")); + } else { + context.accumulate(tagName, jsonobject); + } + return false; + } + } + } + } else { + throw x.syntaxError("Misshaped tag"); + } + } + } + } + + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. This is much less ambitious than + * JSONObject.stringToValue, especially because it does not attempt to + * convert plus forms, octal forms, hex forms, or E forms lacking decimal + * points. + * @param string A String. + * @return A simple JSON value. + */ + public static Object stringToValue(String string) { + if ("".equals(string)) { + return string; + } + if ("true".equalsIgnoreCase(string)) { + return Boolean.TRUE; + } + if ("false".equalsIgnoreCase(string)) { + return Boolean.FALSE; + } + if ("null".equalsIgnoreCase(string)) { + return JSONObject.NULL; + } + if ("0".equals(string)) { + return new Integer(0); + } + +// If it might be a number, try converting it. If that doesn't work, +// return the string. + + try { + char initial = string.charAt(0); + boolean negative = false; + if (initial == '-') { + initial = string.charAt(1); + negative = true; + } + if (initial == '0' && string.charAt(negative ? 2 : 1) == '0') { + return string; + } + if ((initial >= '0' && initial <= '9')) { + if (string.indexOf('.') >= 0) { + return Double.valueOf(string); + } else if (string.indexOf('e') < 0 && string.indexOf('E') < 0) { + Long myLong = new Long(string); + if (myLong.longValue() == myLong.intValue()) { + return new Integer(myLong.intValue()); + } else { + return myLong; + } + } + } + } catch (Exception ignore) { + } + return string; + } + + + /** + * Convert a well-formed (but not necessarily valid) XML string into a + * JSONObject. Some information may be lost in this transformation + * because JSON is a data format and XML is a document format. XML uses + * elements, attributes, and content text, while JSON uses unordered + * collections of name/value pairs and arrays of values. JSON does not + * does not like to distinguish between elements and attributes. + * Sequences of similar elements are represented as JSONArrays. Content + * text may be placed in a "content" member. Comments, prologs, DTDs, and + * <code><[ [ ]]></code> are ignored. + * @param string The source string. + * @return A JSONObject containing the structured data from the XML string. + * @throws JSONException + */ + public static JSONObject toJSONObject(String string) throws JSONException { + JSONObject jo = new JSONObject(); + XMLTokener x = new XMLTokener(string); + while (x.more() && x.skipPast("<")) { + parse(x, jo, null); + } + return jo; + } + + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * @param object A JSONObject. + * @return A string. + * @throws JSONException + */ + public static String toString(Object object) throws JSONException { + return toString(object, null); + } + + + /** + * Convert a JSONObject into a well-formed, element-normal XML string. + * @param object A JSONObject. + * @param tagName The optional name of the enclosing tag. + * @return A string. + * @throws JSONException + */ + public static String toString(Object object, String tagName) + throws JSONException { + StringBuffer sb = new StringBuffer(); + int i; + JSONArray ja; + JSONObject jo; + String key; + Iterator keys; + int length; + String string; + Object value; + if (object instanceof JSONObject) { + +// Emit <tagName> + + if (tagName != null) { + sb.append('<'); + sb.append(tagName); + sb.append('>'); + } + +// Loop thru the keys. + + jo = (JSONObject)object; + keys = jo.keys(); + while (keys.hasNext()) { + key = keys.next().toString(); + value = jo.opt(key); + if (value == null) { + value = ""; + } + if (value instanceof String) { + string = (String)value; + } else { + string = null; + } + +// Emit content in body + + if ("content".equals(key)) { + if (value instanceof JSONArray) { + ja = (JSONArray)value; + length = ja.length(); + for (i = 0; i < length; i += 1) { + if (i > 0) { + sb.append('\n'); + } + sb.append(escape(ja.get(i).toString())); + } + } else { + sb.append(escape(value.toString())); + } + +// Emit an array of similar keys + + } else if (value instanceof JSONArray) { + ja = (JSONArray)value; + length = ja.length(); + for (i = 0; i < length; i += 1) { + value = ja.get(i); + if (value instanceof JSONArray) { + sb.append('<'); + sb.append(key); + sb.append('>'); + sb.append(toString(value)); + sb.append("</"); + sb.append(key); + sb.append('>'); + } else { + sb.append(toString(value, key)); + } + } + } else if ("".equals(value)) { + sb.append('<'); + sb.append(key); + sb.append("/>"); + +// Emit a new tag <k> + + } else { + sb.append(toString(value, key)); + } + } + if (tagName != null) { + +// Emit the </tagname> close tag + + sb.append("</"); + sb.append(tagName); + sb.append('>'); + } + return sb.toString(); + +// XML does not have good support for arrays. If an array appears in a place +// where XML is lacking, synthesize an <array> element. + + } else { + if (object.getClass().isArray()) { + object = new JSONArray(object); + } + if (object instanceof JSONArray) { + ja = (JSONArray)object; + length = ja.length(); + for (i = 0; i < length; i += 1) { + sb.append(toString(ja.opt(i), tagName == null ? "array" : tagName)); + } + return sb.toString(); + } else { + string = (object == null) ? "null" : escape(object.toString()); + return (tagName == null) ? "\"" + string + "\"" : + (string.length() == 0) ? "<" + tagName + "/>" : + "<" + tagName + ">" + string + "</" + tagName + ">"; + } + } + } +}
\ No newline at end of file diff --git a/subsonic-main/src/main/java/org/json/XMLTokener.java b/subsonic-main/src/main/java/org/json/XMLTokener.java new file mode 100644 index 00000000..c7ca95f2 --- /dev/null +++ b/subsonic-main/src/main/java/org/json/XMLTokener.java @@ -0,0 +1,365 @@ +package org.json; + +/* +Copyright (c) 2002 JSON.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/** + * The XMLTokener extends the JSONTokener to provide additional methods + * for the parsing of XML texts. + * @author JSON.org + * @version 2010-12-24 + */ +public class XMLTokener extends JSONTokener { + + + /** The table of entity values. It initially contains Character values for + * amp, apos, gt, lt, quot. + */ + public static final java.util.HashMap entity; + + static { + entity = new java.util.HashMap(8); + entity.put("amp", XML.AMP); + entity.put("apos", XML.APOS); + entity.put("gt", XML.GT); + entity.put("lt", XML.LT); + entity.put("quot", XML.QUOT); + } + + /** + * Construct an XMLTokener from a string. + * @param s A source string. + */ + public XMLTokener(String s) { + super(s); + } + + /** + * Get the text in the CDATA block. + * @return The string up to the <code>]]></code>. + * @throws JSONException If the <code>]]></code> is not found. + */ + public String nextCDATA() throws JSONException { + char c; + int i; + StringBuffer sb = new StringBuffer(); + for (;;) { + c = next(); + if (end()) { + throw syntaxError("Unclosed CDATA"); + } + sb.append(c); + i = sb.length() - 3; + if (i >= 0 && sb.charAt(i) == ']' && + sb.charAt(i + 1) == ']' && sb.charAt(i + 2) == '>') { + sb.setLength(i); + return sb.toString(); + } + } + } + + + /** + * Get the next XML outer token, trimming whitespace. There are two kinds + * of tokens: the '<' character which begins a markup tag, and the content + * text between markup tags. + * + * @return A string, or a '<' Character, or null if there is no more + * source text. + * @throws JSONException + */ + public Object nextContent() throws JSONException { + char c; + StringBuffer sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + if (c == 0) { + return null; + } + if (c == '<') { + return XML.LT; + } + sb = new StringBuffer(); + for (;;) { + if (c == '<' || c == 0) { + back(); + return sb.toString().trim(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + c = next(); + } + } + + + /** + * Return the next entity. These entities are translated to Characters: + * <code>& ' > < "</code>. + * @param ampersand An ampersand character. + * @return A Character or an entity String if the entity is not recognized. + * @throws JSONException If missing ';' in XML entity. + */ + public Object nextEntity(char ampersand) throws JSONException { + StringBuffer sb = new StringBuffer(); + for (;;) { + char c = next(); + if (Character.isLetterOrDigit(c) || c == '#') { + sb.append(Character.toLowerCase(c)); + } else if (c == ';') { + break; + } else { + throw syntaxError("Missing ';' in XML entity: &" + sb); + } + } + String string = sb.toString(); + Object object = entity.get(string); + return object != null ? object : ampersand + string + ";"; + } + + + /** + * Returns the next XML meta token. This is used for skipping over <!...> + * and <?...?> structures. + * @return Syntax characters (<code>< > / = ! ?</code>) are returned as + * Character, and strings and names are returned as Boolean. We don't care + * what the values actually are. + * @throws JSONException If a string is not properly closed or if the XML + * is badly structured. + */ + public Object nextMeta() throws JSONException { + char c; + char q; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped meta tag"); + case '<': + return XML.LT; + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + case '"': + case '\'': + q = c; + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return Boolean.TRUE; + } + } + default: + for (;;) { + c = next(); + if (Character.isWhitespace(c)) { + return Boolean.TRUE; + } + switch (c) { + case 0: + case '<': + case '>': + case '/': + case '=': + case '!': + case '?': + case '"': + case '\'': + back(); + return Boolean.TRUE; + } + } + } + } + + + /** + * Get the next XML Token. These tokens are found inside of angle + * brackets. It may be one of these characters: <code>/ > = ! ?</code> or it + * may be a string wrapped in single quotes or double quotes, or it may be a + * name. + * @return a String or a Character. + * @throws JSONException If the XML is not well formed. + */ + public Object nextToken() throws JSONException { + char c; + char q; + StringBuffer sb; + do { + c = next(); + } while (Character.isWhitespace(c)); + switch (c) { + case 0: + throw syntaxError("Misshaped element"); + case '<': + throw syntaxError("Misplaced '<'"); + case '>': + return XML.GT; + case '/': + return XML.SLASH; + case '=': + return XML.EQ; + case '!': + return XML.BANG; + case '?': + return XML.QUEST; + +// Quoted string + + case '"': + case '\'': + q = c; + sb = new StringBuffer(); + for (;;) { + c = next(); + if (c == 0) { + throw syntaxError("Unterminated string"); + } + if (c == q) { + return sb.toString(); + } + if (c == '&') { + sb.append(nextEntity(c)); + } else { + sb.append(c); + } + } + default: + +// Name + + sb = new StringBuffer(); + for (;;) { + sb.append(c); + c = next(); + if (Character.isWhitespace(c)) { + return sb.toString(); + } + switch (c) { + case 0: + return sb.toString(); + case '>': + case '/': + case '=': + case '!': + case '?': + case '[': + case ']': + back(); + return sb.toString(); + case '<': + case '"': + case '\'': + throw syntaxError("Bad character in a name"); + } + } + } + } + + + /** + * Skip characters until past the requested string. + * If it is not found, we are left at the end of the source with a result of false. + * @param to A string to skip past. + * @throws JSONException + */ + public boolean skipPast(String to) throws JSONException { + boolean b; + char c; + int i; + int j; + int offset = 0; + int length = to.length(); + char[] circle = new char[length]; + + /* + * First fill the circle buffer with as many characters as are in the + * to string. If we reach an early end, bail. + */ + + for (i = 0; i < length; i += 1) { + c = next(); + if (c == 0) { + return false; + } + circle[i] = c; + } + /* + * We will loop, possibly for all of the remaining characters. + */ + for (;;) { + j = offset; + b = true; + /* + * Compare the circle buffer with the to string. + */ + for (i = 0; i < length; i += 1) { + if (circle[j] != to.charAt(i)) { + b = false; + break; + } + j += 1; + if (j >= length) { + j -= length; + } + } + /* + * If we exit the loop with b intact, then victory is ours. + */ + if (b) { + return true; + } + /* + * Get the next character. If there isn't one, then defeat is ours. + */ + c = next(); + if (c == 0) { + return false; + } + /* + * Shove the character in the circle buffer and advance the + * circle offset. The offset is mod n. + */ + circle[offset] = c; + offset += 1; + if (offset >= length) { + offset -= length; + } + } + } +} |