commit a40ad1f7ef65f0058141d316bba3cdc3a5aee1ae Author: Sindre Mehus Date: Sun May 1 17:57:16 2016 +0200 First commit diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..60abc957 --- /dev/null +++ b/pom.xml @@ -0,0 +1,185 @@ + + + + 4.0.0 + net.sourceforge.subsonic + subsonic + 5.3 + Subsonic + pom + + Subsonic + http://subsonic.org/ + + 2004 + + + true + iso-8859-1 + 2.4.2 + + + + + local1 + Local Repository 1 + file:repo + + + local2 + Local Repository 2 + file:../repo + + + java_net + download.java.net + http://download.java.net/maven/2/ + + + teleal + teleal + http://teleal.org/m2 + + + + + + local1 + Local Repository 1 + file:repo + + + local2 + Local Repository 2 + file:../repo + + + Codehaus Repository + http://repository.codehaus.org/ + + + + + scm:svn:svn+ssh://sindre_mehus@svn.code.sf.net/p/subsonic/code/trunk + scm:svn:svn+ssh://sindre_mehus@svn.code.sf.net/p/subsonic/code/trunk + https://sourceforge.net/p/subsonic/code + + + + continuum + + + mail + +
sindre@activeobjects.no
+
+
+
+
+ + + + Sindre Mehus + sindre@activeobjects.no + + + + + subsonic-rest-api + subsonic-sonos-api + subsonic-main + + + + + full + + subsonic-booter + subsonic-installer-windows + subsonic-installer-mac + subsonic-installer-debian + subsonic-installer-rpm + subsonic-backend + subsonic-site + subsonic-assembly + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + + org.apache.maven.plugins + maven-assembly-plugin + 2.2.1 + + + org.apache.maven.plugins + maven-antrun-plugin + 1.6 + + + org.apache.maven.plugins + maven-dependency-plugin + 2.2 + + + org.codehaus.mojo + buildnumber-maven-plugin + 1.0 + + + org.apache.cxf + cxf-codegen-plugin + ${cxf.version} + + + org.jvnet.jaxb2.maven2 + maven-jaxb2-plugin + 0.8.3 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 1.6 + 1.6 + ISO-8859-1 + false + 1.6 + true + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + analyze + + analyze-only + + + ${failOnDependencyWarning} + true + + + + + + + +
\ No newline at end of file diff --git a/repo/ant-zip/ant-zip/1.6.2/ant-zip-1.6.2.jar b/repo/ant-zip/ant-zip/1.6.2/ant-zip-1.6.2.jar new file mode 100644 index 00000000..3a676072 Binary files /dev/null and b/repo/ant-zip/ant-zip/1.6.2/ant-zip-1.6.2.jar differ diff --git a/repo/ant-zip/ant-zip/1.6.2/ant-zip-1.6.2.pom b/repo/ant-zip/ant-zip/1.6.2/ant-zip-1.6.2.pom new file mode 100644 index 00000000..6c10c209 --- /dev/null +++ b/repo/ant-zip/ant-zip/1.6.2/ant-zip-1.6.2.pom @@ -0,0 +1,6 @@ + + 4.0.0 + ant-zip + ant-zip + 1.6.2 + \ No newline at end of file diff --git a/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1-sources.jar b/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1-sources.jar new file mode 100644 index 00000000..e7c090f6 Binary files /dev/null and b/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1-sources.jar differ diff --git a/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1.jar b/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1.jar new file mode 100644 index 00000000..401155ce Binary files /dev/null and b/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1.jar differ diff --git a/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1.pom b/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1.pom new file mode 100644 index 00000000..06069e7e --- /dev/null +++ b/repo/com/hoodcomputing/natpmp/0.1/natpmp-0.1.pom @@ -0,0 +1,6 @@ + + 4.0.0 + com.hoodcomputing + natpmp + 0.1 + \ No newline at end of file diff --git a/repo/com/jgoodies/forms/1.1.0/forms-1.1.0-sources.jar b/repo/com/jgoodies/forms/1.1.0/forms-1.1.0-sources.jar new file mode 100644 index 00000000..f3f746cf Binary files /dev/null and b/repo/com/jgoodies/forms/1.1.0/forms-1.1.0-sources.jar differ diff --git a/repo/com/jgoodies/forms/1.1.0/forms-1.1.0.jar b/repo/com/jgoodies/forms/1.1.0/forms-1.1.0.jar new file mode 100644 index 00000000..50c1eb84 Binary files /dev/null and b/repo/com/jgoodies/forms/1.1.0/forms-1.1.0.jar differ diff --git a/repo/com/jgoodies/forms/1.1.0/forms-1.1.0.pom b/repo/com/jgoodies/forms/1.1.0/forms-1.1.0.pom new file mode 100644 index 00000000..d4360c26 --- /dev/null +++ b/repo/com/jgoodies/forms/1.1.0/forms-1.1.0.pom @@ -0,0 +1,6 @@ + + 4.0.0 + com.jgoodies + forms + 1.1.0 + \ No newline at end of file diff --git a/repo/com/jgoodies/looks/2.1.4/looks-2.1.4.jar b/repo/com/jgoodies/looks/2.1.4/looks-2.1.4.jar new file mode 100644 index 00000000..d2c47c74 Binary files /dev/null and b/repo/com/jgoodies/looks/2.1.4/looks-2.1.4.jar differ diff --git a/repo/com/jgoodies/looks/2.1.4/looks-2.1.4.pom b/repo/com/jgoodies/looks/2.1.4/looks-2.1.4.pom new file mode 100644 index 00000000..85be5a64 --- /dev/null +++ b/repo/com/jgoodies/looks/2.1.4/looks-2.1.4.pom @@ -0,0 +1,6 @@ + + 4.0.0 + com.jgoodies + looks + 2.1.4 + \ No newline at end of file diff --git a/repo/com/oracle/appbundler/1.0/appbundler-1.0.jar b/repo/com/oracle/appbundler/1.0/appbundler-1.0.jar new file mode 100644 index 00000000..ef30f1cb Binary files /dev/null and b/repo/com/oracle/appbundler/1.0/appbundler-1.0.jar differ diff --git a/repo/com/oracle/appbundler/1.0/appbundler-1.0.pom b/repo/com/oracle/appbundler/1.0/appbundler-1.0.pom new file mode 100644 index 00000000..52993a10 --- /dev/null +++ b/repo/com/oracle/appbundler/1.0/appbundler-1.0.pom @@ -0,0 +1,25 @@ + + + + 4.0.0 + com.oracle + appbundler + 1.0 + \ No newline at end of file diff --git a/repo/javax/xml/jaxrpc/1.1/jaxrpc-1.1.jar b/repo/javax/xml/jaxrpc/1.1/jaxrpc-1.1.jar new file mode 100644 index 00000000..fe0b047c Binary files /dev/null and b/repo/javax/xml/jaxrpc/1.1/jaxrpc-1.1.jar differ diff --git a/repo/javax/xml/jaxrpc/1.1/jaxrpc-1.1.pom b/repo/javax/xml/jaxrpc/1.1/jaxrpc-1.1.pom new file mode 100644 index 00000000..5efadbb6 --- /dev/null +++ b/repo/javax/xml/jaxrpc/1.1/jaxrpc-1.1.pom @@ -0,0 +1,6 @@ + + 4.0.0 + javax.xml + jaxrpc + 1.1 + \ No newline at end of file diff --git a/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4-sources.jar b/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4-sources.jar new file mode 100644 index 00000000..fceec13d Binary files /dev/null and b/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4-sources.jar differ diff --git a/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4.jar b/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4.jar new file mode 100644 index 00000000..271e919c Binary files /dev/null and b/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4.jar differ diff --git a/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4.pom b/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4.pom new file mode 100644 index 00000000..fcfff1c9 --- /dev/null +++ b/repo/net/sbbi/sbbi-upnplib/1.0.4/sbbi-upnplib-1.0.4.pom @@ -0,0 +1,6 @@ + + 4.0.0 + net.sbbi + sbbi-upnplib + 1.0.4 + \ No newline at end of file diff --git a/repo/net/sourceforge/jarbundler/jarbundler/2.1.0/jarbundler-2.1.0.jar b/repo/net/sourceforge/jarbundler/jarbundler/2.1.0/jarbundler-2.1.0.jar new file mode 100644 index 00000000..51279879 Binary files /dev/null and b/repo/net/sourceforge/jarbundler/jarbundler/2.1.0/jarbundler-2.1.0.jar differ diff --git a/repo/net/sourceforge/jarbundler/jarbundler/2.1.0/jarbundler-2.1.0.pom b/repo/net/sourceforge/jarbundler/jarbundler/2.1.0/jarbundler-2.1.0.pom new file mode 100644 index 00000000..8fb3340c --- /dev/null +++ b/repo/net/sourceforge/jarbundler/jarbundler/2.1.0/jarbundler-2.1.0.pom @@ -0,0 +1,6 @@ + + 4.0.0 + net.sourceforge.jarbundler + jarbundler + 2.1.0 + \ No newline at end of file diff --git a/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8-sources.jar b/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8-sources.jar new file mode 100644 index 00000000..ce7c4bc1 Binary files /dev/null and b/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8-sources.jar differ diff --git a/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8.jar b/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8.jar new file mode 100644 index 00000000..03460d70 Binary files /dev/null and b/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8.jar differ diff --git a/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8.pom b/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8.pom new file mode 100644 index 00000000..4090c285 --- /dev/null +++ b/repo/net/tanesha/recaptcha4j/recaptcha4j/0.0.8/recaptcha4j-0.0.8.pom @@ -0,0 +1,6 @@ + + 4.0.0 + net.tanesha.recaptcha4j + recaptcha4j + 0.0.8 + \ No newline at end of file diff --git a/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1-sources.jar b/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1-sources.jar new file mode 100644 index 00000000..14439044 Binary files /dev/null and b/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1-sources.jar differ diff --git a/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1.jar b/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1.jar new file mode 100644 index 00000000..ab2f4e01 Binary files /dev/null and b/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1.jar differ diff --git a/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1.pom b/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1.pom new file mode 100644 index 00000000..87f6a6ca --- /dev/null +++ b/repo/org/directwebremoting/dwr/3.0.rc1/dwr-3.0.rc1.pom @@ -0,0 +1,6 @@ + + 4.0.0 + org.directwebremoting + dwr + 3.0.rc1 + \ No newline at end of file diff --git a/repo/org/terracotta/ehcache-probe/1.0.3/ehcache-probe-1.0.3.jar b/repo/org/terracotta/ehcache-probe/1.0.3/ehcache-probe-1.0.3.jar new file mode 100644 index 00000000..214620a2 Binary files /dev/null and b/repo/org/terracotta/ehcache-probe/1.0.3/ehcache-probe-1.0.3.jar differ diff --git a/repo/org/terracotta/ehcache-probe/1.0.3/ehcache-probe-1.0.3.pom b/repo/org/terracotta/ehcache-probe/1.0.3/ehcache-probe-1.0.3.pom new file mode 100644 index 00000000..076683b0 --- /dev/null +++ b/repo/org/terracotta/ehcache-probe/1.0.3/ehcache-probe-1.0.3.pom @@ -0,0 +1,6 @@ + + 4.0.0 + org.terracotta + ehcache-probe + 1.0.3 + \ No newline at end of file diff --git a/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16-sources.jar b/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16-sources.jar new file mode 100644 index 00000000..51073311 Binary files /dev/null and b/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16-sources.jar differ diff --git a/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16.jar b/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16.jar new file mode 100644 index 00000000..1c4a041e Binary files /dev/null and b/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16.jar differ diff --git a/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16.pom b/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16.pom new file mode 100644 index 00000000..484852ae --- /dev/null +++ b/repo/org/wetorrent/weupnp/2009-10-16/weupnp-2009-10-16.pom @@ -0,0 +1,6 @@ + + 4.0.0 + org.wetorrent + weupnp + 2009-10-16 + \ No newline at end of file diff --git a/subsonic-assembly/pom.xml b/subsonic-assembly/pom.xml new file mode 100644 index 00000000..25cbbeab --- /dev/null +++ b/subsonic-assembly/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-assembly + pom + Subsonic Assembly + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + + net.sourceforge.subsonic + subsonic-main + ${project.version} + war + + + + + + subsonic-${project.version} + + + + + maven-assembly-plugin + + + src/main/assembly/war.xml + src/main/assembly/standalone.xml + src/main/assembly/src.xml + + + + + + + maven-antrun-plugin + + + validate + + + + + + + run + + + + + + + + + diff --git a/subsonic-assembly/src/main/assembly/src.xml b/subsonic-assembly/src/main/assembly/src.xml new file mode 100644 index 00000000..d1e88c97 --- /dev/null +++ b/subsonic-assembly/src/main/assembly/src.xml @@ -0,0 +1,56 @@ + + + + src + + zip + + false + + + + .. + + pom.xml + + + + + + ../repo + repo + + + + ../subsonic-main + + src/** + pom.xml + target/classes/build_number.txt + LICENSE.TXT + README.TXT + Getting Started.html + + subsonic-main + + + + ../subsonic-rest-api + + src/** + pom.xml + + subsonic-rest-api + + + + ../subsonic-sonos-api + + src/** + pom.xml + + subsonic-sonos-api + + + + diff --git a/subsonic-assembly/src/main/assembly/standalone.xml b/subsonic-assembly/src/main/assembly/standalone.xml new file mode 100644 index 00000000..7be83f08 --- /dev/null +++ b/subsonic-assembly/src/main/assembly/standalone.xml @@ -0,0 +1,43 @@ + + + + standalone + + tar.gz + + false + + + ../subsonic-main + + + README.TXT + LICENSE.TXT + Getting Started.html + + + + ../subsonic-main/target + + + *.war + + + + ../subsonic-booter/target + + + subsonic-booter-jar-with-dependencies.jar + + + + ../subsonic-booter/src/main/script + + + subsonic.sh + subsonic.bat + + 0777 + + + \ No newline at end of file diff --git a/subsonic-assembly/src/main/assembly/war.xml b/subsonic-assembly/src/main/assembly/war.xml new file mode 100644 index 00000000..aa10ac4c --- /dev/null +++ b/subsonic-assembly/src/main/assembly/war.xml @@ -0,0 +1,27 @@ + + + + war + + zip + + false + + + ../subsonic-main + + + README.TXT + LICENSE.TXT + Getting Started.html + + + + ../subsonic-main/target + + + *.war + + + + \ No newline at end of file diff --git a/subsonic-booter/pom.xml b/subsonic-booter/pom.xml new file mode 100644 index 00000000..3481d07b --- /dev/null +++ b/subsonic-booter/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-booter + Subsonic Booter + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + + + org.mortbay.jetty + jetty + 6.1.5 + + + + org.mortbay.jetty + jetty-embedded + 6.1.5 + + + javax.servlet.jsp + jsp-api + + + runtime + + + + org.mortbay.jetty + jsp-2.0 + 6.1.5 + pom + runtime + + + + com.jgoodies + looks + 2.1.4 + + + + com.jgoodies + forms + 1.1.0 + + + + org.springframework + spring + 2.5.6 + + + + commons-io + commons-io + 1.3.1 + + + + + + subsonic-booter + + + + maven-assembly-plugin + + + jar-with-dependencies + + + ${basedir}/src/main/resources/META-INF/MANIFEST.MF + + + + + + + make-assembly + package + + attached + + + + + + + + + diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java new file mode 100644 index 00000000..b7879e12 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/Main.java @@ -0,0 +1,65 @@ +package net.sourceforge.subsonic.booter; + +import java.util.Arrays; +import java.util.List; + +import javax.swing.JOptionPane; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import net.sourceforge.subsonic.booter.agent.SettingsPanel; +import net.sourceforge.subsonic.booter.agent.SubsonicAgent; + +/** + * Application entry point for Subsonic booter. + *

+ * Use command line argument "-agent" to start the Windows service monitoring agent, + * or "-mac" to start the Mac version of the deployer. + * + * @author Sindre Mehus + */ +public class Main { + + public Main(String contextName, List args) { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext" + contextName + ".xml"); + + if ("-agent".equals(contextName)) { + + SubsonicAgent agent = (SubsonicAgent) context.getBean("agent"); + SettingsPanel settingsPanel = (SettingsPanel) context.getBean("settingsPanel"); + + agent.setElevated(args.contains("-elevated")); + + if (args.contains("-balloon")) { + agent.showTrayIconMessage(); + } + + if (args.contains("-stop")) { + agent.startOrStopService(false); + agent.showStatusPanel(); + } else if (args.contains("-start")) { + agent.startOrStopService(true); + agent.showStatusPanel(); + } + + if (args.contains("-settings")) { + String[] settings = args.get(args.indexOf("-settings") + 1).split(","); + try { + agent.showSettingsPanel(); + settingsPanel.saveSettings(Integer.valueOf(settings[0]), Integer.valueOf(settings[1]), Integer.valueOf(settings[2]), settings[3]); + settingsPanel.readValues(); + } catch (Exception x) { + JOptionPane.showMessageDialog(settingsPanel, x.getMessage(), "Error", JOptionPane.WARNING_MESSAGE); + } + } + } + } + + public static void main(String[] args) { + String context = "-deployer"; + if (args.length > 0) { + context = args[0]; + } + new Main(context, Arrays.asList(args)); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java new file mode 100644 index 00000000..c225410f --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SettingsPanel.java @@ -0,0 +1,375 @@ +package net.sourceforge.subsonic.booter.agent; + +import com.jgoodies.forms.builder.DefaultFormBuilder; +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import com.jgoodies.forms.layout.FormLayout; +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployer; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.Writer; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.Format; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Panel displaying the settings of the Subsonic service. + * + * @author Sindre Mehus + */ +public class SettingsPanel extends JPanel implements SubsonicListener { + + private static final Format INTEGER_FORMAT = new DecimalFormat("0", DecimalFormatSymbols.getInstance(Locale.UK)); + + private final SubsonicAgent subsonicAgent; + private JFormattedTextField portTextField; + private JCheckBox httpsPortCheckBox; + private JFormattedTextField httpsPortTextField; + private JComboBox contextPathComboBox; + private JFormattedTextField memoryTextField; + private JButton defaultButton; + private JButton saveButton; + public SettingsPanel(SubsonicAgent subsonicAgent) { + this.subsonicAgent = subsonicAgent; + createComponents(); + configureComponents(); + layoutComponents(); + addBehaviour(); + readValues(); + subsonicAgent.addListener(this); + } + + public void readValues() { + portTextField.setValue(getPortFromOptionsFile()); + memoryTextField.setValue(getMemoryLimitFromOptionsFile()); + contextPathComboBox.setSelectedItem(getContextPathFromOptionsFile()); + int httpsPort = getHttpsPortFromOptionsFile(); + boolean httpsEnabled = httpsPort != 0; + httpsPortTextField.setValue(httpsEnabled ? httpsPort : 4443); + httpsPortTextField.setEnabled(httpsEnabled); + httpsPortCheckBox.setSelected(httpsEnabled); + } + + private int getHttpsPortFromOptionsFile() { + try { + String s = grep("-Dsubsonic.httpsPort=(\\d+)"); + return Integer.parseInt(s); + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_HTTPS_PORT; + } + } + + private int getPortFromOptionsFile() { + try { + String s = grep("-Dsubsonic.port=(\\d+)"); + return Integer.parseInt(s); + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_PORT; + } + } + + private int getMemoryLimitFromOptionsFile() { + try { + String s = grep("-Xmx(\\d+)m"); + return Integer.parseInt(s); + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_MEMORY_LIMIT; + } + } + + private String getContextPathFromOptionsFile() { + try { + String s = grep("-Dsubsonic.contextPath=(.*)"); + if (s == null) { + throw new NullPointerException(); + } + return s; + } catch (Exception x) { + x.printStackTrace(); + return SubsonicDeployer.DEFAULT_CONTEXT_PATH; + } + } + + private void createComponents() { + portTextField = new JFormattedTextField(INTEGER_FORMAT); + httpsPortTextField = new JFormattedTextField(INTEGER_FORMAT); + httpsPortCheckBox = new JCheckBox("Enable https on port"); + contextPathComboBox = new JComboBox(); + memoryTextField = new JFormattedTextField(INTEGER_FORMAT); + defaultButton = new JButton("Restore defaults"); + saveButton = new JButton("Save settings"); + } + + private void configureComponents() { + contextPathComboBox.setEditable(true); + contextPathComboBox.addItem("/"); + contextPathComboBox.addItem("/subsonic"); + contextPathComboBox.addItem("/music"); + } + + private void layoutComponents() { + FormLayout layout = new FormLayout("d, 6dlu, max(d;30dlu):grow"); + DefaultFormBuilder builder = new DefaultFormBuilder(layout); + builder.append("Port number", portTextField); + builder.append(httpsPortCheckBox, httpsPortTextField); + builder.append("Memory limit (MB)", memoryTextField); + builder.append("Context path", contextPathComboBox); + + setBorder(Borders.DIALOG_BORDER); + + setLayout(new BorderLayout(12, 12)); + add(builder.getPanel(), BorderLayout.CENTER); + add(ButtonBarFactory.buildCenteredBar(defaultButton, saveButton), BorderLayout.SOUTH); + } + + private void addBehaviour() { + saveButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + try { + subsonicAgent.checkElevation("-settings", getMemoryLimit() + "," + getPort() + "," + getHttpsPort() + "," + getContextPath()); + saveSettings(getMemoryLimit(), getPort(), getHttpsPort(), getContextPath()); + } catch (Exception x) { + JOptionPane.showMessageDialog(SettingsPanel.this, x.getMessage(), "Error", JOptionPane.WARNING_MESSAGE); + } + } + }); + + defaultButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + portTextField.setValue(SubsonicDeployer.DEFAULT_PORT); + httpsPortTextField.setValue(4443); + httpsPortTextField.setEnabled(false); + httpsPortCheckBox.setSelected(false); + memoryTextField.setValue(SubsonicDeployer.DEFAULT_MEMORY_LIMIT); + contextPathComboBox.setSelectedItem(SubsonicDeployer.DEFAULT_CONTEXT_PATH); + } + }); + + httpsPortCheckBox.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + httpsPortTextField.setEnabled(httpsPortCheckBox.isSelected()); + } + }); + } + + private String getContextPath() throws SettingsException { + String contextPath = (String) contextPathComboBox.getSelectedItem(); + if (contextPath.contains(" ") || !contextPath.startsWith("/")) { + throw new SettingsException("Please specify a valid context path."); + } + return contextPath; + } + + private int getMemoryLimit() throws SettingsException { + int memoryLimit; + try { + memoryLimit = ((Number) memoryTextField.getValue()).intValue(); + if (memoryLimit < 5) { + throw new Exception(); + } + } catch (Exception x) { + throw new SettingsException("Please specify a valid memory limit.", x); + } + return memoryLimit; + } + + private int getPort() throws SettingsException { + int port; + try { + port = ((Number) portTextField.getValue()).intValue(); + if (port < 1 || port > 65535) { + throw new Exception(); + } + } catch (Exception x) { + throw new SettingsException("Please specify a valid port number.", x); + } + return port; + } + + private int getHttpsPort() throws SettingsException { + if (!httpsPortCheckBox.isSelected()) { + return 0; + } + + int port; + try { + port = ((Number) httpsPortTextField.getValue()).intValue(); + if (port < 1 || port > 65535) { + throw new Exception(); + } + } catch (Exception x) { + throw new SettingsException("Please specify a valid https port number.", x); + } + return port; + } + + public void saveSettings(int memoryLimit, int port, int httpsPort, String contextPath) throws SettingsException { + File file = getOptionsFile(); + + java.util.List lines = readLines(file); + java.util.List newLines = new ArrayList(); + + boolean memoryLimitAdded = false; + boolean portAdded = false; + boolean httpsPortAdded = false; + boolean contextPathAdded = false; + + for (String line : lines) { + if (line.startsWith("-Xmx")) { + newLines.add("-Xmx" + memoryLimit + "m"); + memoryLimitAdded = true; + } else if (line.startsWith("-Dsubsonic.port=")) { + newLines.add("-Dsubsonic.port=" + port); + portAdded = true; + } else if (line.startsWith("-Dsubsonic.httpsPort=")) { + newLines.add("-Dsubsonic.httpsPort=" + httpsPort); + httpsPortAdded = true; + } else if (line.startsWith("-Dsubsonic.contextPath=")) { + newLines.add("-Dsubsonic.contextPath=" + contextPath); + contextPathAdded = true; + } else { + newLines.add(line); + } + } + + if (!memoryLimitAdded) { + newLines.add("-Xmx" + memoryLimit + "m"); + } + if (!portAdded) { + newLines.add("-Dsubsonic.port=" + port); + } + if (!httpsPortAdded) { + newLines.add("-Dsubsonic.httpsPort=" + httpsPort); + } + if (!contextPathAdded) { + newLines.add("-Dsubsonic.contextPath=" + contextPath); + } + + writeLines(file, newLines); + + JOptionPane.showMessageDialog(SettingsPanel.this, + "Please restart Subsonic for the new settings to take effect.", + "Settings changed", JOptionPane.INFORMATION_MESSAGE); + + } + + private File getOptionsFile() throws SettingsException { + File file = new File("subsonic-service.exe.vmoptions"); + if (!file.isFile() || !file.exists()) { + throw new SettingsException("File " + file.getAbsolutePath() + " not found."); + } + return file; + } + + private List readLines(File file) throws SettingsException { + List lines = new ArrayList(); + BufferedReader reader = null; + try { + reader = new BufferedReader(new FileReader(file)); + for (String line = reader.readLine(); line != null; line = reader.readLine()) { + lines.add(line); + } + return lines; + } catch (IOException x) { + throw new SettingsException("Failed to read from file " + file.getAbsolutePath(), x); + } finally { + closeQuietly(reader); + } + } + + private void writeLines(File file, List lines) throws SettingsException { + PrintWriter writer = null; + try { + writer = new PrintWriter(new FileWriter(file)); + for (String line : lines) { + writer.println(line); + } + } catch (IOException x) { + throw new SettingsException("Failed to write to file " + file.getAbsolutePath(), x); + } finally { + closeQuietly(writer); + } + } + + private String grep(String regexp) throws SettingsException { + Pattern pattern = Pattern.compile(regexp); + File file = getOptionsFile(); + for (String line : readLines(file)) { + Matcher matcher = pattern.matcher(line); + if (matcher.matches()) { + return matcher.group(1); + } + } + return null; + } + + private void closeQuietly(Reader reader) { + if (reader == null) { + return; + } + + try { + reader.close(); + } catch (IOException x) { + // Intentionally ignored. + } + } + + private void closeQuietly(Writer writer) { + if (writer == null) { + return; + } + + try { + writer.close(); + } catch (IOException x) { + // Intentionally ignored. + } + } + + public void notifyDeploymentStatus(DeploymentStatus deploymentStatus) { + // Nothing here yet. + } + + public void notifyServiceStatus(String serviceStatus) { + // Nothing here yet. + } + + public static class SettingsException extends Exception { + + public SettingsException(String message, Throwable cause) { + super(message, cause); + } + + public SettingsException(String message) { + this(message, null); + } + + @Override + public String getMessage() { + if (getCause() == null || getCause().getMessage() == null) { + return super.getMessage(); + } + return super.getMessage() + " " + getCause().getMessage(); + } + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java new file mode 100644 index 00000000..91625f19 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/StatusPanel.java @@ -0,0 +1,116 @@ +package net.sourceforge.subsonic.booter.agent; + +import com.jgoodies.forms.builder.DefaultFormBuilder; +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import com.jgoodies.forms.layout.FormLayout; +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.text.DateFormat; +import java.util.Locale; + +/** + * Panel displaying the status of the Subsonic service. + * + * @author Sindre Mehus + */ +public class StatusPanel extends JPanel implements SubsonicListener { + + private static final DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.US); + + private final SubsonicAgent subsonicAgent; + + private JTextField statusTextField; + private JTextField startedTextField; + private JTextField memoryTextField; + private JTextArea errorTextField; + private JButton startButton; + private JButton stopButton; + private JButton urlButton; + + public StatusPanel(SubsonicAgent subsonicAgent) { + this.subsonicAgent = subsonicAgent; + createComponents(); + configureComponents(); + layoutComponents(); + addBehaviour(); + subsonicAgent.addListener(this); + } + + private void createComponents() { + statusTextField = new JTextField(); + startedTextField = new JTextField(); + memoryTextField = new JTextField(); + errorTextField = new JTextArea(3, 24); + startButton = new JButton("Start"); + stopButton = new JButton("Stop"); + urlButton = new JButton(); + } + + private void configureComponents() { + statusTextField.setEditable(false); + startedTextField.setEditable(false); + memoryTextField.setEditable(false); + errorTextField.setEditable(false); + + errorTextField.setLineWrap(true); + errorTextField.setBorder(startedTextField.getBorder()); + + urlButton.setBorderPainted(false); + urlButton.setContentAreaFilled(false); + urlButton.setForeground(Color.BLUE.darker()); + urlButton.setHorizontalAlignment(SwingConstants.LEFT); + } + + private void layoutComponents() { + JPanel buttons = ButtonBarFactory.buildRightAlignedBar(startButton, stopButton); + + FormLayout layout = new FormLayout("right:d, 6dlu, max(d;30dlu):grow"); + DefaultFormBuilder builder = new DefaultFormBuilder(layout, this); + builder.append("Service status", statusTextField); + builder.append("", buttons); + builder.appendParagraphGapRow(); + builder.nextRow(); + builder.append("Started on", startedTextField); + builder.append("Memory used", memoryTextField); + builder.append("Error message", errorTextField); + builder.append("Server address", urlButton); + + setBorder(Borders.DIALOG_BORDER); + } + + private void addBehaviour() { + urlButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + subsonicAgent.openBrowser(); + } + }); + startButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + subsonicAgent.checkElevation("-start"); + subsonicAgent.startOrStopService(true); + } + }); + stopButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + subsonicAgent.checkElevation("-stop"); + subsonicAgent.startOrStopService(false); + } + }); + } + + public void notifyDeploymentStatus(DeploymentStatus status) { + startedTextField.setText(status == null ? null : DATE_FORMAT.format(status.getStartTime())); + memoryTextField.setText(status == null ? null : status.getMemoryUsed() + " MB"); + errorTextField.setText(status == null ? null : status.getErrorMessage()); + urlButton.setText(status == null ? null : status.getURL()); + } + + public void notifyServiceStatus(String serviceStatus) { + statusTextField.setText(serviceStatus); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java new file mode 100644 index 00000000..a9bb526e --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicAgent.java @@ -0,0 +1,201 @@ +package net.sourceforge.subsonic.booter.agent; + +import java.awt.Desktop; +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.swing.JOptionPane; +import javax.swing.UIManager; + +import org.apache.commons.io.IOUtils; + +import com.jgoodies.looks.plastic.PlasticXPLookAndFeel; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +/** + * Responsible for deploying the Subsonic web app in + * the embedded Jetty container. + * + * @author Sindre Mehus + */ +public class SubsonicAgent { + + private final List listeners = new ArrayList(); + private final TrayController trayController; + private SubsonicFrame frame; + private final SubsonicDeployerService service; + private static final int POLL_INTERVAL_DEPLOYMENT_INFO_SECONDS = 5; + private static final int POLL_INTERVAL_SERVICE_STATUS_SECONDS = 5; + private String url; + private boolean serviceStatusPollingEnabled; + private boolean elevated; + + public SubsonicAgent(SubsonicDeployerService service) { + this.service = service; + setLookAndFeel(); + trayController = new TrayController(this); + startPolling(); + } + + public void setFrame(SubsonicFrame frame) { + this.frame = frame; + } + + private void setLookAndFeel() { + // Set look-and-feel. + try { + UIManager.setLookAndFeel(new PlasticXPLookAndFeel()); + } catch (Throwable x) { + System.err.println("Failed to set look-and-feel.\n" + x); + } + } + + private void startPolling() { + ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); + Runnable runnable = new Runnable() { + public void run() { + try { + notifyDeploymentInfo(service.getDeploymentInfo()); + } catch (Throwable x) { + notifyDeploymentInfo(null); + } + } + }; + executor.scheduleWithFixedDelay(runnable, 0, POLL_INTERVAL_DEPLOYMENT_INFO_SECONDS, TimeUnit.SECONDS); + + runnable = new Runnable() { + public void run() { + if (serviceStatusPollingEnabled) { + try { + notifyServiceStatus(getServiceStatus()); + } catch (Throwable x) { + notifyServiceStatus(null); + } + } + } + }; + executor.scheduleWithFixedDelay(runnable, 0, POLL_INTERVAL_SERVICE_STATUS_SECONDS, TimeUnit.SECONDS); + } + + private String getServiceStatus() throws Exception { + Process process = Runtime.getRuntime().exec("subsonic-service.exe -status"); + return IOUtils.toString(process.getInputStream()); + } + + public void setServiceStatusPollingEnabled(boolean enabled) { + serviceStatusPollingEnabled = enabled; + } + + public void startOrStopService(boolean start) { + try { + String cmd = "subsonic-service.exe " + (start ? "-start" : "-stop"); + System.err.println("Executing: " + cmd); + + Runtime.getRuntime().exec(cmd); + } catch (Exception x) { + x.printStackTrace(); + } + } + + /** + * If necessary, restart agent with elevated rights. + */ + public void checkElevation(String... args) { + + if (isElevationNeeded() && !isElevated()) { + try { + List command = new ArrayList(); + command.add("cmd"); + command.add("/c"); + command.add("subsonic-agent-elevated.exe"); + command.addAll(Arrays.asList(args)); + + ProcessBuilder builder = new ProcessBuilder(); + builder.command(command); + System.err.println("Executing: " + command + " with current dir: " + System.getProperty("user.dir")); + builder.start(); + System.exit(0); + } catch (Exception x) { + JOptionPane.showMessageDialog(frame, "Failed to elevate Subsonic Control Panel. " + x, "Error", JOptionPane.WARNING_MESSAGE); + x.printStackTrace(); + } + } + } + + public void setElevated(boolean elevated) { + this.elevated = elevated; + } + + private boolean isElevated() { + return elevated; + } + + /** + * Returns whether UAC elevation is necessary (to start/stop services etc). + */ + private boolean isElevationNeeded() { + + String osVersion = System.getProperty("os.version"); + try { + int majorVersion = Integer.parseInt(osVersion.substring(0, osVersion.indexOf("."))); + + // Elevation is necessary in Windows Vista (os.version=6.1) and later. + return majorVersion >= 6; + } catch (Exception x) { + System.err.println("Failed to resolve OS version from '" + osVersion + "'\n" + x); + return false; + } + } + + public void addListener(SubsonicListener listener) { + listeners.add(listener); + } + + private void notifyDeploymentInfo(DeploymentStatus status) { + if (status != null) { + url = status.getURL(); + } + + for (SubsonicListener listener : listeners) { + listener.notifyDeploymentStatus(status); + } + } + + private void notifyServiceStatus(String status) { + for (SubsonicListener listener : listeners) { + listener.notifyServiceStatus(status); + } + } + + public void showStatusPanel() { + frame.showStatusPanel(); + } + + public void showSettingsPanel() { + frame.showSettingsPanel(); + } + + public void showTrayIconMessage() { + trayController.showMessage(); + } + + public void exit() { + trayController.uninstallComponents(); + System.exit(0); + } + + public void openBrowser() { + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Throwable x) { + x.printStackTrace(); + } + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java new file mode 100644 index 00000000..32ee5230 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicFrame.java @@ -0,0 +1,113 @@ +package net.sourceforge.subsonic.booter.agent; + +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import net.sourceforge.subsonic.booter.Main; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Method; +import java.util.Arrays; + +/** + * Frame that is activated by the tray icon. Contains a tabbed pane + * with status and settings panels. + * + * @author Sindre Mehus + */ +public class SubsonicFrame extends JFrame { + + private final SubsonicAgent subsonicAgent; + + private final StatusPanel statusPanel; + private final SettingsPanel settingsPanel; + private JTabbedPane tabbedPane; + private JButton closeButton; + + public SubsonicFrame(SubsonicAgent subsonicAgent, StatusPanel statusPanel, SettingsPanel settingsPanel) { + super("Subsonic Control Panel"); + this.subsonicAgent = subsonicAgent; + this.statusPanel = statusPanel; + this.settingsPanel = settingsPanel; + createComponents(); + layoutComponents(); + addBehaviour(); + setupIcons(); + + pack(); + centerComponent(); + } + + private void setupIcons() { + Toolkit toolkit = Toolkit.getDefaultToolkit(); + + // Window.setIconImages() was added in Java 1.6. Since Subsonic only requires 1.5, we + // use reflection to invoke it. + try { + Method method = Window.class.getMethod("setIconImages", java.util.List.class); + java.util.List images = Arrays.asList( + toolkit.createImage(Main.class.getResource("/images/subsonic-16.png")), + toolkit.createImage(Main.class.getResource("/images/subsonic-32.png")), + toolkit.createImage(Main.class.getResource("/images/subsonic-512.png"))); + method.invoke(this, images); + } catch (Throwable x) { + // Fallback to old method. + setIconImage(toolkit.createImage(Main.class.getResource("/images/subsonic-32.png"))); + } + } + + public void centerComponent() { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation(screenSize.width / 2 - getWidth() / 2, + screenSize.height / 2 - getHeight() / 2); + } + + private void createComponents() { + tabbedPane = new JTabbedPane(); + closeButton = new JButton("Close"); + } + + private void layoutComponents() { + tabbedPane.add("Status", statusPanel); + tabbedPane.add("Settings", settingsPanel); + + JPanel pane = (JPanel) getContentPane(); + pane.setLayout(new BorderLayout(10, 10)); + pane.add(tabbedPane, BorderLayout.CENTER); + pane.add(ButtonBarFactory.buildCloseBar(closeButton), BorderLayout.SOUTH); + + pane.setBorder(Borders.TABBED_DIALOG_BORDER); + } + + private void addBehaviour() { + closeButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setVisible(false); + } + }); + } + + @Override + public void setVisible(boolean b) { + super.setVisible(b); + subsonicAgent.setServiceStatusPollingEnabled(b); + } + + public void showStatusPanel() { + settingsPanel.readValues(); + tabbedPane.setSelectedComponent(statusPanel); + pack(); + setVisible(true); + toFront(); + } + + public void showSettingsPanel() { + settingsPanel.readValues(); + tabbedPane.setSelectedComponent(settingsPanel); + pack(); + setVisible(true); + toFront(); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java new file mode 100644 index 00000000..d6239c0d --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/SubsonicListener.java @@ -0,0 +1,28 @@ +package net.sourceforge.subsonic.booter.agent; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; + +/** + * Callback interface implemented by GUI classes that wants to be notified when + * the state of the Subsonic deployment changes. + * + * @author Sindre Mehus + */ +public interface SubsonicListener { + + /** + * Invoked when new information about the Subsonic deployment is available. + * + * @param deploymentStatus The new deployment status, or null if an + * error occurred while retrieving the status. + */ + void notifyDeploymentStatus(DeploymentStatus deploymentStatus); + + /** + * Invoked when new information about the Subsonic Windows service is available. + * + * @param serviceStatus The new service status, or null if an + * error occurred while retrieving the status. + */ + void notifyServiceStatus(String serviceStatus); +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java new file mode 100644 index 00000000..2be918e8 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/agent/TrayController.java @@ -0,0 +1,125 @@ +package net.sourceforge.subsonic.booter.agent; + +import java.awt.Image; +import java.awt.MenuItem; +import java.awt.PopupMenu; +import java.awt.SystemTray; +import java.awt.Toolkit; +import java.awt.TrayIcon; +import java.awt.event.ActionEvent; +import java.net.URL; + +import javax.swing.AbstractAction; +import javax.swing.Action; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; + +/** + * Controls the Subsonic tray icon. + * + * @author Sindre Mehus + */ +public class TrayController implements SubsonicListener { + + private final SubsonicAgent subsonicAgent; + private TrayIcon trayIcon; + + private Action openAction; + private Action controlPanelAction; + private Action hideAction; + private Image startedImage; + private Image stoppedImage; + + public TrayController(SubsonicAgent subsonicAgent) { + this.subsonicAgent = subsonicAgent; + try { + createActions(); + createComponents(); + addBehaviour(); + installComponents(); + subsonicAgent.addListener(this); + } catch (Throwable x) { + System.err.println("Disabling tray support."); + } + } + + public void showMessage() { + trayIcon.displayMessage("Subsonic", "Subsonic is now running. Click this balloon to get started.", + TrayIcon.MessageType.INFO); + } + + private void createActions() { + openAction = new AbstractAction("Open Subsonic in Browser") { + public void actionPerformed(ActionEvent e) { + subsonicAgent.openBrowser(); + } + }; + + controlPanelAction = new AbstractAction("Subsonic Control Panel") { + public void actionPerformed(ActionEvent e) { + subsonicAgent.showStatusPanel(); + } + }; + + + hideAction = new AbstractAction("Hide Tray Icon") { + public void actionPerformed(ActionEvent e) { + subsonicAgent.exit(); + } + }; + } + + private void createComponents() { + startedImage = createImage("/images/subsonic-started-16.png"); + stoppedImage = createImage("/images/subsonic-stopped-16.png"); + + PopupMenu menu = new PopupMenu(); + menu.add(createMenuItem(openAction)); + menu.add(createMenuItem(controlPanelAction)); + menu.addSeparator(); + menu.add(createMenuItem(hideAction)); + + trayIcon = new TrayIcon(stoppedImage, "Subsonic Music Streamer", menu); + } + + private Image createImage(String resourceName) { + URL url = getClass().getResource(resourceName); + return Toolkit.getDefaultToolkit().createImage(url); + } + + private MenuItem createMenuItem(Action action) { + MenuItem menuItem = new MenuItem((String) action.getValue(Action.NAME)); + menuItem.addActionListener(action); + return menuItem; + } + + private void addBehaviour() { + trayIcon.addActionListener(controlPanelAction); + } + + private void installComponents() throws Throwable { + SystemTray.getSystemTray().add(trayIcon); + } + + public void uninstallComponents() { + try { + SystemTray.getSystemTray().remove(trayIcon); + } catch (Throwable x) { + System.err.println("Disabling tray support."); + } + } + + private void setTrayImage(Image image) { + if (trayIcon.getImage() != image) { + trayIcon.setImage(image); + } + } + + public void notifyDeploymentStatus(DeploymentStatus deploymentStatus) { + setTrayImage(deploymentStatus == null ? stoppedImage : startedImage); + } + + public void notifyServiceStatus(String serviceStatus) { + // Nothing here, but could potentially change tray icon and menu. + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java new file mode 100644 index 00000000..3b237dc1 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/DeploymentStatus.java @@ -0,0 +1,44 @@ +package net.sourceforge.subsonic.booter.deployer; + +import java.util.Date; +import java.io.Serializable; + +/** + * @author Sindre Mehus + */ +public class DeploymentStatus implements Serializable { + + private final Date startTime; + private final String url; + private final String httpsUrl; + private final int memoryUsed; + private final String errorMessage; + + public DeploymentStatus(Date startTime, String url, String httpsUrl, int memoryUsed, String errorMessage) { + this.startTime = startTime; + this.url = url; + this.httpsUrl = httpsUrl; + this.memoryUsed = memoryUsed; + this.errorMessage = errorMessage; + } + + public String getURL() { + return url; + } + + public String getHttpsUrl() { + return httpsUrl; + } + + public Date getStartTime() { + return startTime; + } + + public int getMemoryUsed() { + return memoryUsed; + } + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java new file mode 100644 index 00000000..779d5073 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployer.java @@ -0,0 +1,338 @@ +package net.sourceforge.subsonic.booter.deployer; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.Writer; +import java.net.BindException; +import java.util.Date; +import java.util.jar.JarFile; +import java.util.zip.ZipEntry; + +import org.apache.commons.io.IOUtils; +import org.mortbay.jetty.Server; +import org.mortbay.jetty.nio.SelectChannelConnector; +import org.mortbay.jetty.security.Constraint; +import org.mortbay.jetty.security.ConstraintMapping; +import org.mortbay.jetty.security.SslSocketConnector; +import org.mortbay.jetty.webapp.WebAppContext; + +/** + * Responsible for deploying the Subsonic web app in + * the embedded Jetty container. + *

+ * The following system properties may be used to customize the behaviour: + *

+ * + * @author Sindre Mehus + */ +public class SubsonicDeployer implements SubsonicDeployerService { + + public static final String DEFAULT_HOST = "0.0.0.0"; + public static final int DEFAULT_PORT = 4040; + public static final int DEFAULT_HTTPS_PORT = 0; + public static final int DEFAULT_MEMORY_LIMIT = 150; + public static final String DEFAULT_CONTEXT_PATH = "/"; + public static final String DEFAULT_WAR = "subsonic.war"; + private static final int MAX_IDLE_TIME_MILLIS = 7 * 24 * 60 * 60 * 1000; // One week. + private static final int HEADER_BUFFER_SIZE = 64 * 1024; + + // 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"); + + private Throwable exception; + private File subsonicHome; + private final Date startTime; + + public SubsonicDeployer() { + + // Enable shutdown hook for Ehcache. + System.setProperty("net.sf.ehcache.enableShutdownHook", "true"); + + startTime = new Date(); + createLinkFile(); + deployWebApp(); + } + + private void createLinkFile() { + if ("true".equals(System.getProperty("subsonic.createLinkFile"))) { + Writer writer = null; + try { + writer = new FileWriter("subsonic.url"); + writer.append("[InternetShortcut]"); + writer.append(System.getProperty("line.separator")); + writer.append("URL=").append(getUrl()); + writer.flush(); + } catch (Throwable x) { + System.err.println("Failed to create subsonic.url."); + x.printStackTrace(); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException x) { + // Ignored + } + } + } + } + } + + private void deployWebApp() { + try { + Server server = new Server(); + SelectChannelConnector connector = new SelectChannelConnector(); + connector.setMaxIdleTime(MAX_IDLE_TIME_MILLIS); + connector.setHeaderBufferSize(HEADER_BUFFER_SIZE); + connector.setHost(getHost()); + connector.setPort(getPort()); + if (isHttpsEnabled()) { + connector.setConfidentialPort(getHttpsPort()); + } + server.addConnector(connector); + + if (isHttpsEnabled()) { + SslSocketConnector sslConnector = new SslSocketConnector(); + sslConnector.setMaxIdleTime(MAX_IDLE_TIME_MILLIS); + sslConnector.setHeaderBufferSize(HEADER_BUFFER_SIZE); + sslConnector.setHost(getHost()); + sslConnector.setPort(getHttpsPort()); + sslConnector.setKeystore(System.getProperty("subsonic.ssl.keystore", getClass().getResource("/subsonic.keystore").toExternalForm())); + sslConnector.setPassword(System.getProperty("subsonic.ssl.password", "subsonic")); + server.addConnector(sslConnector); + } + + WebAppContext context = new WebAppContext(); + context.setTempDirectory(getJettyDirectory()); + context.setContextPath(getContextPath()); + context.setWar(getWar()); + context.setOverrideDescriptor("/web-jetty.xml"); + + if (isHttpsEnabled()) { + + // Allow non-https for streaming and cover art (for Chromecast, UPnP, Sonos etc) + context.getSecurityHandler().setConstraintMappings(new ConstraintMapping[]{ + createConstraintMapping("/stream", Constraint.DC_NONE), + createConstraintMapping("/coverArt.view", Constraint.DC_NONE), + createConstraintMapping("/ws/*", Constraint.DC_NONE), + createConstraintMapping("/sonos/*", Constraint.DC_NONE), + createConstraintMapping("/", Constraint.DC_CONFIDENTIAL) + }); + } + + server.addHandler(context); + server.start(); + + System.err.println("Subsonic running on: " + getUrl()); + if (isHttpsEnabled()) { + System.err.println(" and: " + getHttpsUrl()); + } + + } catch (Throwable x) { + x.printStackTrace(); + exception = x; + } + } + + private ConstraintMapping createConstraintMapping(String pathSpec, int dataConstraint) { + ConstraintMapping constraintMapping = new ConstraintMapping(); + Constraint constraint = new Constraint(); + constraint.setDataConstraint(dataConstraint); + constraintMapping.setPathSpec(pathSpec); + constraintMapping.setConstraint(constraint); + return constraintMapping; + } + + private File getJettyDirectory() { + File dir = new File(getSubsonicHome(), "jetty"); + String buildNumber = getSubsonicBuildNumber(); + if (buildNumber != null) { + dir = new File(dir, buildNumber); + } + System.err.println("Extracting webapp to " + dir); + + if (!dir.exists() && !dir.mkdirs()) { + System.err.println("Failed to create directory " + dir); + } + + return dir; + } + + private String getSubsonicBuildNumber() { + File war = new File(getWar()); + InputStream in = null; + try { + if (war.isFile()) { + JarFile jar = new JarFile(war); + ZipEntry entry = jar.getEntry("WEB-INF\\classes\\build_number.txt"); + if (entry == null) { + entry = jar.getEntry("WEB-INF/classes/build_number.txt"); + } + in = jar.getInputStream(entry); + } else { + in = new FileInputStream(war.getPath() + "/WEB-INF/classes/build_number.txt"); + } + return IOUtils.toString(in); + + } catch (Exception x) { + System.err.println("Failed to resolve build number from WAR " + war + ": " + x); + return null; + } finally { + IOUtils.closeQuietly(in); + } + } + + private String getContextPath() { + return System.getProperty("subsonic.contextPath", DEFAULT_CONTEXT_PATH); + } + + + private String getWar() { + String war = System.getProperty("subsonic.war"); + if (war == null) { + war = DEFAULT_WAR; + } + + File file = new File(war); + if (file.exists()) { + System.err.println("Using WAR file: " + file.getAbsolutePath()); + } else { + System.err.println("Error: WAR file not found: " + file.getAbsolutePath()); + } + + return war; + } + + private String getHost() { + return System.getProperty("subsonic.host", DEFAULT_HOST); + } + + private int getPort() { + int port = DEFAULT_PORT; + + String portString = System.getProperty("subsonic.port"); + if (portString != null) { + port = Integer.parseInt(portString); + } + + // Also set it so that the webapp can read it. + System.setProperty("subsonic.port", String.valueOf(port)); + + return port; + } + + private int getHttpsPort() { + int port = DEFAULT_HTTPS_PORT; + + String portString = System.getProperty("subsonic.httpsPort"); + if (portString != null) { + port = Integer.parseInt(portString); + } + + // Also set it so that the webapp can read it. + System.setProperty("subsonic.httpsPort", String.valueOf(port)); + + return port; + } + + private boolean isHttpsEnabled() { + return getHttpsPort() > 0; + } + + public String getErrorMessage() { + if (exception == null) { + return null; + } + if (exception instanceof BindException) { + return "Address already in use. Please change port number."; + } + + return exception.toString(); + } + + public int getMemoryUsed() { + long freeBytes = Runtime.getRuntime().freeMemory(); + long totalBytes = Runtime.getRuntime().totalMemory(); + long usedBytes = totalBytes - freeBytes; + return (int) Math.round(usedBytes / 1024.0 / 1024.0); + } + + private String getUrl() { + String host = DEFAULT_HOST.equals(getHost()) ? "localhost" : getHost(); + StringBuilder url = new StringBuilder("http://").append(host); + if (getPort() != 80) { + url.append(":").append(getPort()); + } + url.append(getContextPath()); + return url.toString(); + } + + private String getHttpsUrl() { + if (!isHttpsEnabled()) { + return null; + } + + String host = DEFAULT_HOST.equals(getHost()) ? "localhost" : getHost(); + StringBuilder url = new StringBuilder("https://").append(host); + if (getHttpsPort() != 443) { + url.append(":").append(getHttpsPort()); + } + url.append(getContextPath()); + return url.toString(); + } + + /** + * Returns the Subsonic home directory. + * + * @return The Subsonic home directory, if it exists. + * @throws RuntimeException If directory doesn't exist. + */ + private 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; + } + + public DeploymentStatus getDeploymentInfo() { + return new DeploymentStatus(startTime, getUrl(), getHttpsUrl(), getMemoryUsed(), getErrorMessage()); + } +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java new file mode 100644 index 00000000..a0bf087f --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/deployer/SubsonicDeployerService.java @@ -0,0 +1,17 @@ +package net.sourceforge.subsonic.booter.deployer; + +/** + * RMI interface implemented by the Subsonic deployer and used by the agent. + * + * @author Sindre Mehus + */ +public interface SubsonicDeployerService { + + /** + * Returns information about the Subsonic deployment, such + * as URL, memory consumption, start time etc. + * + * @return Deployment information. + */ + DeploymentStatus getDeploymentInfo(); +} diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java new file mode 100644 index 00000000..f20671f8 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/StatusPanel.java @@ -0,0 +1,115 @@ +package net.sourceforge.subsonic.booter.mac; + +import java.awt.Color; +import java.awt.Desktop; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.URI; +import java.text.DateFormat; +import java.util.Locale; + +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingConstants; +import javax.swing.Timer; + +import com.jgoodies.forms.builder.DefaultFormBuilder; +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.layout.FormLayout; + +import net.sourceforge.subsonic.booter.deployer.DeploymentStatus; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +/** + * Panel displaying the status of the Subsonic service. + * + * @author Sindre Mehus + */ +public class StatusPanel extends JPanel { + + private static final DateFormat DATE_FORMAT = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM, Locale.US); + + private final SubsonicDeployerService deployer; + + private JTextField startedTextField; + private JTextField memoryTextField; + private JTextArea errorTextField; + private JButton urlButton; + + public StatusPanel(SubsonicDeployerService deployer) { + this.deployer = deployer; + createComponents(); + configureComponents(); + layoutComponents(); + addBehaviour(); + } + + private void createComponents() { + startedTextField = new JTextField(); + memoryTextField = new JTextField(); + errorTextField = new JTextArea(3, 24); + urlButton = new JButton(); + } + + private void configureComponents() { + startedTextField.setEditable(false); + memoryTextField.setEditable(false); + errorTextField.setEditable(false); + + errorTextField.setLineWrap(true); + errorTextField.setBorder(startedTextField.getBorder()); + + urlButton.setBorderPainted(false); + urlButton.setContentAreaFilled(false); + urlButton.setForeground(Color.BLUE.darker()); + urlButton.setHorizontalAlignment(SwingConstants.LEFT); + } + + private void layoutComponents() { + FormLayout layout = new FormLayout("right:d, 6dlu, max(d;30dlu):grow"); + DefaultFormBuilder builder = new DefaultFormBuilder(layout, this); + builder.append("Started on", startedTextField); + builder.append("Memory used", memoryTextField); + builder.append("Error message", errorTextField); + builder.append("Server address", urlButton); + + setBorder(Borders.DIALOG_BORDER); + } + + private void addBehaviour() { + urlButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + openBrowser(); + } + }); + + Timer timer = new Timer(3000, new ActionListener() { + public void actionPerformed(ActionEvent e) { + updateStatus(deployer.getDeploymentInfo()); + } + }); + updateStatus(deployer.getDeploymentInfo()); + timer.start(); + } + + private void openBrowser() { + String url = urlButton.getText(); + if (url == null) { + return; + } + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Throwable x) { + x.printStackTrace(); + } + } + + private void updateStatus(DeploymentStatus status) { + startedTextField.setText(status == null ? null : DATE_FORMAT.format(status.getStartTime())); + memoryTextField.setText(status == null ? null : status.getMemoryUsed() + " MB"); + errorTextField.setText(status == null ? null : status.getErrorMessage()); + urlButton.setText(status == null ? null : status.getURL()); + } +} \ No newline at end of file diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java new file mode 100644 index 00000000..65731f31 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicController.java @@ -0,0 +1,89 @@ +package net.sourceforge.subsonic.booter.mac; + +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.net.URL; +import java.net.URI; + +/** + * Controller for the Mac booter. + * + * @author Sindre Mehus + */ +public class SubsonicController { + + private final SubsonicDeployerService deployer; + private final SubsonicFrame frame; + private Action openAction; + private Action controlPanelAction; + private Action quitAction; + + public SubsonicController(SubsonicDeployerService deployer, SubsonicFrame frame) { + this.deployer = deployer; + this.frame = frame; + createActions(); + createComponents(); + } + + private void createActions() { + openAction = new AbstractAction("Open Subsonic Web Page") { + public void actionPerformed(ActionEvent e) { + openBrowser(); + } + }; + + controlPanelAction = new AbstractAction("Subsonic Control Panel") { + public void actionPerformed(ActionEvent e) { + frame.setActive(false); + frame.setActive(true); + } + }; + + quitAction = new AbstractAction("Quit Subsonic") { + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }; + } + + private void createComponents() { + PopupMenu menu = new PopupMenu(); + menu.add(createMenuItem(openAction)); + menu.add(createMenuItem(controlPanelAction)); + menu.addSeparator(); + menu.add(createMenuItem(quitAction)); + + URL url = getClass().getResource("/images/subsonic-21.png"); + Image image = Toolkit.getDefaultToolkit().createImage(url); + TrayIcon trayIcon = new TrayIcon(image, "Subsonic Music Streamer", menu); + trayIcon.setImageAutoSize(false); + + try { + SystemTray.getSystemTray().add(trayIcon); + } catch (Throwable x) { + System.err.println("Failed to add tray icon."); + } + } + + private MenuItem createMenuItem(Action action) { + MenuItem menuItem = new MenuItem((String) action.getValue(Action.NAME)); + menuItem.addActionListener(action); + return menuItem; + } + + private void openBrowser() { + String url = deployer.getDeploymentInfo().getURL(); + if (url == null) { + return; + } + try { + Desktop.getDesktop().browse(new URI(url)); + } catch (Throwable x) { + x.printStackTrace(); + } + } + +} \ No newline at end of file diff --git a/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java new file mode 100644 index 00000000..2a492e45 --- /dev/null +++ b/subsonic-booter/src/main/java/net/sourceforge/subsonic/booter/mac/SubsonicFrame.java @@ -0,0 +1,82 @@ +package net.sourceforge.subsonic.booter.mac; + +import com.jgoodies.forms.factories.Borders; +import com.jgoodies.forms.factories.ButtonBarFactory; +import net.sourceforge.subsonic.booter.Main; +import net.sourceforge.subsonic.booter.deployer.SubsonicDeployerService; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.net.URL; + +/** + * Frame with Subsonic status. Used on Mac installs. + * + * @author Sindre Mehus + */ +public class SubsonicFrame extends JFrame { + + private final SubsonicDeployerService deployer; + private StatusPanel statusPanel; + private JButton hideButton; + private JButton exitButton; + + public SubsonicFrame(SubsonicDeployerService deployer) { + super("Subsonic"); + this.deployer = deployer; + createComponents(); + layoutComponents(); + addBehaviour(); + + URL url = Main.class.getResource("/images/subsonic-512.png"); + setIconImage(Toolkit.getDefaultToolkit().createImage(url)); + } + + public void setActive(boolean active) { + if (active) { + pack(); + centerComponent(); + setVisible(true); + toFront(); + } else { + dispose(); + } + } + + private void centerComponent() { + Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + setLocation(screenSize.width / 2 - getWidth() / 2, + screenSize.height / 2 - getHeight() / 2); + } + + private void createComponents() { + statusPanel = new StatusPanel(deployer); + hideButton = new JButton("Hide"); + exitButton = new JButton("Exit"); + } + + private void layoutComponents() { + JPanel pane = (JPanel) getContentPane(); + pane.setLayout(new BorderLayout(10, 10)); + pane.add(statusPanel, BorderLayout.CENTER); + pane.add(ButtonBarFactory.buildRightAlignedBar(hideButton, exitButton), BorderLayout.SOUTH); + + pane.setBorder(Borders.DIALOG_BORDER); + } + + private void addBehaviour() { + hideButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + setActive(false); + } + }); + exitButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + System.exit(0); + } + }); + } + +} \ No newline at end of file diff --git a/subsonic-booter/src/main/resources/META-INF/MANIFEST.MF b/subsonic-booter/src/main/resources/META-INF/MANIFEST.MF new file mode 100644 index 00000000..2eafdfc8 --- /dev/null +++ b/subsonic-booter/src/main/resources/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 +Main-Class: net.sourceforge.subsonic.booter.Main diff --git a/subsonic-booter/src/main/resources/applicationContext-agent.xml b/subsonic-booter/src/main/resources/applicationContext-agent.xml new file mode 100644 index 00000000..a08e1111 --- /dev/null +++ b/subsonic-booter/src/main/resources/applicationContext-agent.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/subsonic-booter/src/main/resources/applicationContext-deployer.xml b/subsonic-booter/src/main/resources/applicationContext-deployer.xml new file mode 100644 index 00000000..26dca501 --- /dev/null +++ b/subsonic-booter/src/main/resources/applicationContext-deployer.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/subsonic-booter/src/main/resources/applicationContext-mac.xml b/subsonic-booter/src/main/resources/applicationContext-mac.xml new file mode 100644 index 00000000..1ca20178 --- /dev/null +++ b/subsonic-booter/src/main/resources/applicationContext-mac.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/subsonic-booter/src/main/resources/images/subsonic-16.png b/subsonic-booter/src/main/resources/images/subsonic-16.png new file mode 100644 index 00000000..eba8bb57 Binary files /dev/null and b/subsonic-booter/src/main/resources/images/subsonic-16.png differ diff --git a/subsonic-booter/src/main/resources/images/subsonic-21.png b/subsonic-booter/src/main/resources/images/subsonic-21.png new file mode 100644 index 00000000..6ce85a4f Binary files /dev/null and b/subsonic-booter/src/main/resources/images/subsonic-21.png differ diff --git a/subsonic-booter/src/main/resources/images/subsonic-32.png b/subsonic-booter/src/main/resources/images/subsonic-32.png new file mode 100644 index 00000000..b30ed059 Binary files /dev/null and b/subsonic-booter/src/main/resources/images/subsonic-32.png differ diff --git a/subsonic-booter/src/main/resources/images/subsonic-512.png b/subsonic-booter/src/main/resources/images/subsonic-512.png new file mode 100644 index 00000000..77f183e0 Binary files /dev/null and b/subsonic-booter/src/main/resources/images/subsonic-512.png differ diff --git a/subsonic-booter/src/main/resources/images/subsonic-started-16.png b/subsonic-booter/src/main/resources/images/subsonic-started-16.png new file mode 100644 index 00000000..8bfcd647 Binary files /dev/null and b/subsonic-booter/src/main/resources/images/subsonic-started-16.png differ diff --git a/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png b/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png new file mode 100644 index 00000000..c2d637cc Binary files /dev/null and b/subsonic-booter/src/main/resources/images/subsonic-stopped-16.png differ diff --git a/subsonic-booter/src/main/resources/subsonic.keystore b/subsonic-booter/src/main/resources/subsonic.keystore new file mode 100644 index 00000000..54e1589c Binary files /dev/null and b/subsonic-booter/src/main/resources/subsonic.keystore differ diff --git a/subsonic-booter/src/main/resources/web-jetty.xml b/subsonic-booter/src/main/resources/web-jetty.xml new file mode 100644 index 00000000..0282119f --- /dev/null +++ b/subsonic-booter/src/main/resources/web-jetty.xml @@ -0,0 +1,39 @@ + + + + + + + + GzipFilter + org.mortbay.servlet.GzipFilter + + mimeTypes + text/xml,application/json,text/javascript + + + + GzipFilter + /rest/* + + + \ No newline at end of file diff --git a/subsonic-booter/src/main/script/subsonic.bat b/subsonic-booter/src/main/script/subsonic.bat new file mode 100644 index 00000000..2f2b4b13 --- /dev/null +++ b/subsonic-booter/src/main/script/subsonic.bat @@ -0,0 +1,24 @@ +@echo off + +REM The directory where Subsonic will create files. Make sure it is writable. +set SUBSONIC_HOME=c:\subsonic + +REM The host name or IP address on which to bind Subsonic. Only relevant if you have +REM multiple network interfaces and want to make Subsonic available on only one of them. +REM The default value 0.0.0.0 will bind Subsonic to all available network interfaces. +set SUBSONIC_HOST=0.0.0.0 + +REM The port on which Subsonic will listen for incoming HTTP traffic. +set SUBSONIC_PORT=4040 + +REM The port on which Subsonic will listen for incoming HTTPS traffic (0 to disable). +set SUBSONIC_HTTPS_PORT=0 + +REM The context path (i.e., the last part of the Subsonic URL). Typically "/" or "/subsonic". +set SUBSONIC_CONTEXT_PATH=/ + +REM The memory limit (max Java heap size) in megabytes. +set MAX_MEMORY=150 + +java -Xmx%MAX_MEMORY%m -Dsubsonic.home=%SUBSONIC_HOME% -Dsubsonic.host=%SUBSONIC_HOST% -Dsubsonic.port=%SUBSONIC_PORT% -Dsubsonic.httpsPort=%SUBSONIC_HTTPS_PORT% -Dsubsonic.contextPath=%SUBSONIC_CONTEXT_PATH% -jar subsonic-booter-jar-with-dependencies.jar + diff --git a/subsonic-booter/src/main/script/subsonic.sh b/subsonic-booter/src/main/script/subsonic.sh new file mode 100755 index 00000000..4022fb72 --- /dev/null +++ b/subsonic-booter/src/main/script/subsonic.sh @@ -0,0 +1,134 @@ +#!/bin/sh + +################################################################################### +# Shell script for starting Subsonic. See http://subsonic.org. +# +# Author: Sindre Mehus +################################################################################### + +SUBSONIC_HOME=/var/subsonic +SUBSONIC_HOST=0.0.0.0 +SUBSONIC_PORT=4040 +SUBSONIC_HTTPS_PORT=0 +SUBSONIC_CONTEXT_PATH=/ +SUBSONIC_MAX_MEMORY=150 +SUBSONIC_PIDFILE= +SUBSONIC_DEFAULT_MUSIC_FOLDER=/var/music +SUBSONIC_DEFAULT_PODCAST_FOLDER=/var/music/Podcast +SUBSONIC_DEFAULT_PLAYLIST_FOLDER=/var/playlists + +quiet=0 + +usage() { + echo "Usage: subsonic.sh [options]" + echo " --help This small usage guide." + echo " --home=DIR The directory where Subsonic will create files." + echo " Make sure it is writable. Default: /var/subsonic" + echo " --host=HOST The host name or IP address on which to bind Subsonic." + echo " Only relevant if you have multiple network interfaces and want" + echo " to make Subsonic available on only one of them. The default value" + echo " will bind Subsonic to all available network interfaces. Default: 0.0.0.0" + echo " --port=PORT The port on which Subsonic will listen for" + echo " incoming HTTP traffic. Default: 4040" + echo " --https-port=PORT The port on which Subsonic will listen for" + echo " incoming HTTPS traffic. Default: 0 (disabled)" + echo " --context-path=PATH The context path, i.e., the last part of the Subsonic" + echo " URL. Typically '/' or '/subsonic'. Default '/'" + echo " --max-memory=MB The memory limit (max Java heap size) in megabytes." + echo " Default: 100" + echo " --pidfile=PIDFILE Write PID to this file. Default not created." + echo " --quiet Don't print anything to standard out. Default false." + echo " --default-music-folder=DIR Configure Subsonic to use this folder for music. This option " + echo " only has effect the first time Subsonic is started. Default '/var/music'" + echo " --default-podcast-folder=DIR Configure Subsonic to use this folder for Podcasts. This option " + echo " only has effect the first time Subsonic is started. Default '/var/music/Podcast'" + echo " --default-playlist-folder=DIR Configure Subsonic to use this folder for playlists. This option " + echo " only has effect the first time Subsonic is started. Default '/var/playlists'" + exit 1 +} + +# Parse arguments. +while [ $# -ge 1 ]; do + case $1 in + --help) + usage + ;; + --home=?*) + SUBSONIC_HOME=${1#--home=} + ;; + --host=?*) + SUBSONIC_HOST=${1#--host=} + ;; + --port=?*) + SUBSONIC_PORT=${1#--port=} + ;; + --https-port=?*) + SUBSONIC_HTTPS_PORT=${1#--https-port=} + ;; + --context-path=?*) + SUBSONIC_CONTEXT_PATH=${1#--context-path=} + ;; + --max-memory=?*) + SUBSONIC_MAX_MEMORY=${1#--max-memory=} + ;; + --pidfile=?*) + SUBSONIC_PIDFILE=${1#--pidfile=} + ;; + --quiet) + quiet=1 + ;; + --default-music-folder=?*) + SUBSONIC_DEFAULT_MUSIC_FOLDER=${1#--default-music-folder=} + ;; + --default-podcast-folder=?*) + SUBSONIC_DEFAULT_PODCAST_FOLDER=${1#--default-podcast-folder=} + ;; + --default-playlist-folder=?*) + SUBSONIC_DEFAULT_PLAYLIST_FOLDER=${1#--default-playlist-folder=} + ;; + *) + usage + ;; + esac + shift +done + +# Use JAVA_HOME if set, otherwise assume java is in the path. +JAVA=java +if [ -e "${JAVA_HOME}" ] + then + JAVA=${JAVA_HOME}/bin/java +fi + +# Create Subsonic home directory. +mkdir -p ${SUBSONIC_HOME} +LOG=${SUBSONIC_HOME}/subsonic_sh.log +rm -f ${LOG} + +cd $(dirname $0) +if [ -L $0 ] && ([ -e /bin/readlink ] || [ -e /usr/bin/readlink ]); then + cd $(dirname $(readlink $0)) +fi + +${JAVA} -Xmx${SUBSONIC_MAX_MEMORY}m \ + -Dsubsonic.home=${SUBSONIC_HOME} \ + -Dsubsonic.host=${SUBSONIC_HOST} \ + -Dsubsonic.port=${SUBSONIC_PORT} \ + -Dsubsonic.httpsPort=${SUBSONIC_HTTPS_PORT} \ + -Dsubsonic.contextPath=${SUBSONIC_CONTEXT_PATH} \ + -Dsubsonic.defaultMusicFolder=${SUBSONIC_DEFAULT_MUSIC_FOLDER} \ + -Dsubsonic.defaultPodcastFolder=${SUBSONIC_DEFAULT_PODCAST_FOLDER} \ + -Dsubsonic.defaultPlaylistFolder=${SUBSONIC_DEFAULT_PLAYLIST_FOLDER} \ + -Djava.awt.headless=true \ + -verbose:gc \ + -jar subsonic-booter-jar-with-dependencies.jar > ${LOG} 2>&1 & + +# Write pid to pidfile if it is defined. +if [ $SUBSONIC_PIDFILE ]; then + echo $! > ${SUBSONIC_PIDFILE} +fi + +if [ $quiet = 0 ]; then + echo Started Subsonic [PID $!, ${LOG}] +fi + diff --git a/subsonic-installer-debian/pom.xml b/subsonic-installer-debian/pom.xml new file mode 100644 index 00000000..08dc7e5c --- /dev/null +++ b/subsonic-installer-debian/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-installer-debian + pom + Subsonic Installer for Debian + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + + + debian + + + debian + + + + + + + maven-antrun-plugin + + + dpkg + compile + + + Creating Debian package... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + run + + + + + + + + + + + + diff --git a/subsonic-installer-debian/src/DEBIAN/conffiles b/subsonic-installer-debian/src/DEBIAN/conffiles new file mode 100644 index 00000000..06573282 --- /dev/null +++ b/subsonic-installer-debian/src/DEBIAN/conffiles @@ -0,0 +1 @@ +/etc/default/subsonic diff --git a/subsonic-installer-debian/src/DEBIAN/control b/subsonic-installer-debian/src/DEBIAN/control new file mode 100644 index 00000000..21caa735 --- /dev/null +++ b/subsonic-installer-debian/src/DEBIAN/control @@ -0,0 +1,18 @@ +Package: subsonic +Version: @VERSION@ +Section: Multimedia +Priority: optional +Recommends: ffmpeg +Architecture: all +Maintainer: Sindre Mehus +Description: A web-based music streamer, jukebox and Podcast receiver + Subsonic is a web-based music streamer, jukebox and Podcast receiver, + providing access to your music collection wherever you are. Use it + to share your music with friends, or to listen to your music while away + from home. + . + Apps for Android, iPhone and Windows Phone are also available. + . + Java 1.6 or higher is required to run Subsonic. + . + Subsonic can be found at http://subsonic.org diff --git a/subsonic-installer-debian/src/DEBIAN/postinst b/subsonic-installer-debian/src/DEBIAN/postinst new file mode 100644 index 00000000..da88dd9e --- /dev/null +++ b/subsonic-installer-debian/src/DEBIAN/postinst @@ -0,0 +1,16 @@ +#! /bin/sh + +set -e + +ln -sf /usr/share/subsonic/subsonic.sh /usr/bin/subsonic + +chmod 750 /var/subsonic + +# Clear jetty cache. +rm -rf /var/subsonic/jetty + +# Configure Subsonic service. +update-rc.d subsonic defaults 99 + +# Start Subsonic service. +invoke-rc.d subsonic start diff --git a/subsonic-installer-debian/src/DEBIAN/postrm b/subsonic-installer-debian/src/DEBIAN/postrm new file mode 100644 index 00000000..1ecc392d --- /dev/null +++ b/subsonic-installer-debian/src/DEBIAN/postrm @@ -0,0 +1,9 @@ +#! /bin/sh + +set -e + +# Remove symlink. +rm -f /usr/bin/subsonic + +# Remove startup scripts. +update-rc.d -f subsonic remove diff --git a/subsonic-installer-debian/src/DEBIAN/preinst b/subsonic-installer-debian/src/DEBIAN/preinst new file mode 100644 index 00000000..a5b7f1ca --- /dev/null +++ b/subsonic-installer-debian/src/DEBIAN/preinst @@ -0,0 +1,15 @@ +#! /bin/sh + +set -e + +# Stop Subsonic service. +if [ -e /etc/init.d/subsonic ]; then + invoke-rc.d subsonic stop +fi + +# Backup database. +if [ -e /var/subsonic/db ]; then + rm -rf /var/subsonic/db.backup + cp -R /var/subsonic/db /var/subsonic/db.backup +fi + diff --git a/subsonic-installer-debian/src/DEBIAN/prerm b/subsonic-installer-debian/src/DEBIAN/prerm new file mode 100644 index 00000000..e15501e5 --- /dev/null +++ b/subsonic-installer-debian/src/DEBIAN/prerm @@ -0,0 +1,8 @@ +#! /bin/sh + +set -e + +# Stop Subsonic service. +if [ -e /etc/init.d/subsonic ]; then + invoke-rc.d subsonic stop +fi diff --git a/subsonic-installer-debian/src/etc/default/subsonic b/subsonic-installer-debian/src/etc/default/subsonic new file mode 100644 index 00000000..c056edfd --- /dev/null +++ b/subsonic-installer-debian/src/etc/default/subsonic @@ -0,0 +1,25 @@ +# +# This is the configuration file for the Subsonic service +# (/etc/init.d/subsonic) +# +# To change the startup parameters of Subsonic, modify +# the SUBSONIC_ARGS variable below. +# +# Type "/usr/share/subsonic/subsonic.sh --help" on the command line to read an +# explanation of the different options. +# +# For example, to specify that Subsonic should use port 80 (for http) +# and 443 (for https), and use a Java memory heap size of 200 MB, use +# the following: +# +# SUBSONIC_ARGS="--port=80 --https-port=443 --max-memory=200" + +SUBSONIC_ARGS="--max-memory=150" + + +# The user which should run the Subsonic process. Default "root". +# Note that non-root users are by default not allowed to use ports +# below 1024. Also make sure to grant the user write permissions in +# the music directories, otherwise changing album art and tags will fail. + +SUBSONIC_USER=root \ No newline at end of file diff --git a/subsonic-installer-debian/src/etc/init.d/subsonic b/subsonic-installer-debian/src/etc/init.d/subsonic new file mode 100644 index 00000000..b45c1ed9 --- /dev/null +++ b/subsonic-installer-debian/src/etc/init.d/subsonic @@ -0,0 +1,138 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: subsonic +# Required-Start: $remote_fs $syslog +# Required-Stop: $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Subsonic daemon +# Description: Starts the Subsonic daemon. Subsonic is a web-based +# music streamer, jukebox and Podcast receiver. +# See http://subsonic.org for more details. +### END INIT INFO + +# Author: Sindre Mehus + +# To change the startup parameters of Subsonic, modify the service +# configuration file /etc/default/subsonic rather than this file. +[ -r /etc/default/subsonic ] && . /etc/default/subsonic + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Subsonic Daemon" +NAME=subsonic +PIDFILE=/var/run/$NAME.pid +DAEMON=/usr/bin/$NAME +DAEMON_ARGS="--pidfile=$PIDFILE $SUBSONIC_ARGS" +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Run as root if SUBSONIC_USER is not set. +[ "$SUBSONIC_USER" = "" ] && SUBSONIC_USER=root + +# Make sure Subsonic is started with system locale +if [ -r /etc/default/locale ]; then + . /etc/default/locale + export LANG +fi + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.0-6) to ensure that this file is present. +. /lib/lsb/init-functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + + if [ -e $PIDFILE ] + then + ps -p $(cat $PIDFILE) > /dev/null + [ "$?" = 0 ] && return 1 + fi + + touch $PIDFILE + chown $SUBSONIC_USER $PIDFILE + [ -e /var/subsonic ] && chown -R $SUBSONIC_USER /var/subsonic + [ -e /tmp/subsonic ] && chown -R $SUBSONIC_USER /tmp/subsonic + + start-stop-daemon --start -c $SUBSONIC_USER --pidfile $PIDFILE --exec $DAEMON -- $DAEMON_ARGS || return 2 +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/30/KILL/5 --pidfile $PIDFILE + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$DAEMON" "$NAME" && exit 0 || exit $? + ;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/subsonic-installer-mac/pom.xml b/subsonic-installer-mac/pom.xml new file mode 100644 index 00000000..0096369e --- /dev/null +++ b/subsonic-installer-mac/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-installer-mac + pom + Subsonic Installer for Mac + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + + + net.sourceforge.subsonic + subsonic-main + ${project.version} + war + + + + net.sourceforge.subsonic + subsonic-booter + ${project.version} + + + + + + + mac + + + mac + + + + + + + maven-antrun-plugin + + + compile + + + + + + + + + + + + + + + + + + + + + + + + + + + + + run + + + + + + com.oracle + appbundler + 1.0 + + + + + + + + + + diff --git a/subsonic-installer-mac/src/postinstall.sh b/subsonic-installer-mac/src/postinstall.sh new file mode 100644 index 00000000..95ddc230 --- /dev/null +++ b/subsonic-installer-mac/src/postinstall.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +SUBSONIC_HOME="/Library/Application Support/Subsonic" + +chmod oug+rwx "$SUBSONIC_HOME" +chown root:admin "$SUBSONIC_HOME" + +chmod oug+rx "$SUBSONIC_HOME/transcode" +chown root:admin "$SUBSONIC_HOME/transcode" + +rm -rf "$SUBSONIC_HOME/jetty" + +echo Subsonic installation done diff --git a/subsonic-installer-mac/src/preinstall.sh b/subsonic-installer-mac/src/preinstall.sh new file mode 100755 index 00000000..84055f5e --- /dev/null +++ b/subsonic-installer-mac/src/preinstall.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +SUBSONIC_HOME="/Library/Application Support/Subsonic" + +# Backup database. + +if [ -e "$SUBSONIC_HOME/db" ]; then + rm -rf "$SUBSONIC_HOME/db.backup" + cp -R "$SUBSONIC_HOME/db" "$SUBSONIC_HOME/db.backup" +fi + diff --git a/subsonic-installer-mac/src/subsonic.icns b/subsonic-installer-mac/src/subsonic.icns new file mode 100644 index 00000000..804ce79d Binary files /dev/null and b/subsonic-installer-mac/src/subsonic.icns differ diff --git a/subsonic-installer-mac/src/subsonic.pkgproj b/subsonic-installer-mac/src/subsonic.pkgproj new file mode 100755 index 00000000..f8240032 --- /dev/null +++ b/subsonic-installer-mac/src/subsonic.pkgproj @@ -0,0 +1,883 @@ + + + + + PACKAGES + + + PACKAGE_FILES + + DEFAULT_INSTALL_LOCATION + / + HIERARCHY + + CHILDREN + + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + ../target/Subsonic.app + PATH_TYPE + 1 + PERMISSIONS + 493 + TYPE + 3 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Utilities + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 80 + PATH + Applications + PATH_TYPE + 0 + PERMISSIONS + 509 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + + CHILDREN + + GID + 80 + PATH + ../../subsonic-transcode/mac/ffmpeg + PATH_TYPE + 1 + PERMISSIONS + 493 + TYPE + 3 + UID + 0 + + + GID + 80 + PATH + transcode + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 2 + UID + 0 + + + GID + 80 + PATH + Subsonic + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 2 + UID + 0 + + + GID + 80 + PATH + Application Support + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Documentation + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Filesystems + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Frameworks + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Input Methods + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Internet Plug-Ins + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchAgents + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + LaunchDaemons + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PreferencePanes + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Preferences + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 80 + PATH + Printers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + PrivilegedHelperTools + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickLook + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + QuickTime + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Screen Savers + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Scripts + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Services + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + GID + 0 + PATH + Widgets + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + Library + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + Extensions + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + Library + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + System + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + CHILDREN + + + CHILDREN + + GID + 0 + PATH + Shared + PATH_TYPE + 0 + PERMISSIONS + 1023 + TYPE + 1 + UID + 0 + + + GID + 80 + PATH + Users + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + + GID + 0 + PATH + / + PATH_TYPE + 0 + PERMISSIONS + 493 + TYPE + 1 + UID + 0 + + PAYLOAD_TYPE + 0 + VERSION + 2 + + PACKAGE_SCRIPTS + + POSTINSTALL_PATH + + PATH + postinstall.sh + PATH_TYPE + 1 + + PREINSTALL_PATH + + PATH + preinstall.sh + PATH_TYPE + 1 + + RESOURCES + + + PACKAGE_SETTINGS + + AUTHENTICATION + 1 + CONCLUSION_ACTION + 0 + IDENTIFIER + net.sourceforge.subsonic + NAME + subsonic + OVERWRITE_PERMISSIONS + + VERSION + 1.0 + + UUID + 1804C6F2-A8D3-496B-B75B-8945F1A8CFEE + + + PROJECT + + PROJECT_COMMENTS + + NOTES + + PCFET0NUWVBFIGh0bWwgUFVCTElDICItLy9XM0MvL0RURCBIVE1M + IDQuMDEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvVFIvaHRtbDQv + c3RyaWN0LmR0ZCI+CjxodG1sPgo8aGVhZD4KPG1ldGEgaHR0cC1l + cXVpdj0iQ29udGVudC1UeXBlIiBjb250ZW50PSJ0ZXh0L2h0bWw7 + IGNoYXJzZXQ9VVRGLTgiPgo8bWV0YSBodHRwLWVxdWl2PSJDb250 + ZW50LVN0eWxlLVR5cGUiIGNvbnRlbnQ9InRleHQvY3NzIj4KPHRp + dGxlPjwvdGl0bGU+CjxtZXRhIG5hbWU9IkdlbmVyYXRvciIgY29u + dGVudD0iQ29jb2EgSFRNTCBXcml0ZXIiPgo8bWV0YSBuYW1lPSJD + b2NvYVZlcnNpb24iIGNvbnRlbnQ9IjEyNjUuMTkiPgo8c3R5bGUg + dHlwZT0idGV4dC9jc3MiPgo8L3N0eWxlPgo8L2hlYWQ+Cjxib2R5 + Pgo8L2JvZHk+CjwvaHRtbD4K + + + PROJECT_PRESENTATION + + BACKGROUND + + ALIGNMENT + 6 + BACKGROUND_PATH + + PATH + subsonic.png + PATH_TYPE + 1 + + CUSTOM + 1 + SCALING + 2 + + INSTALLATION_STEPS + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewIntroductionController + INSTALLER_PLUGIN + Introduction + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewReadMeController + INSTALLER_PLUGIN + ReadMe + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewLicenseController + INSTALLER_PLUGIN + License + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewDestinationSelectController + INSTALLER_PLUGIN + TargetSelect + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewInstallationTypeController + INSTALLER_PLUGIN + PackageSelection + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewInstallationController + INSTALLER_PLUGIN + Install + LIST_TITLE_KEY + InstallerSectionTitle + + + ICPRESENTATION_CHAPTER_VIEW_CONTROLLER_CLASS + ICPresentationViewSummaryController + INSTALLER_PLUGIN + Summary + LIST_TITLE_KEY + InstallerSectionTitle + + + INTRODUCTION + + LOCALIZATIONS + + + SUMMARY + + LOCALIZATIONS + + + TITLE + + LOCALIZATIONS + + + LANGUAGE + English + VALUE + Subsonic + + + + + PROJECT_REQUIREMENTS + + LIST + + POSTINSTALL_PATH + + PREINSTALL_PATH + + RESOURCES + + ROOT_VOLUME_ONLY + + + PROJECT_SETTINGS + + ADVANCED_OPTIONS + + BUILD_FORMAT + 0 + BUILD_PATH + + PATH + ../target + PATH_TYPE + 1 + + EXCLUDED_FILES + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + .DS_Store + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove .DS_Store files + PROXY_TOOLTIP + Remove ".DS_Store" files created by the Finder. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + .pbdevelopment + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove .pbdevelopment files + PROXY_TOOLTIP + Remove ".pbdevelopment" files created by ProjectBuilder or Xcode. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + CVS + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .cvsignore + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + .cvspass + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + .svn + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .git + TYPE + 1 + + + REGULAR_EXPRESSION + + STRING + .gitignore + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Remove SCM metadata + PROXY_TOOLTIP + Remove helper files and folders used by the CVS, SVN or Git Source Code Management systems. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + classes.nib + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + designable.db + TYPE + 0 + + + REGULAR_EXPRESSION + + STRING + info.nib + TYPE + 0 + + + PROTECTED + + PROXY_NAME + Optimize nib files + PROXY_TOOLTIP + Remove "classes.nib", "info.nib" and "designable.nib" files within .nib bundles. + STATE + + + + PATTERNS_ARRAY + + + REGULAR_EXPRESSION + + STRING + Resources Disabled + TYPE + 1 + + + PROTECTED + + PROXY_NAME + Remove Resources Disabled folders + PROXY_TOOLTIP + Remove "Resources Disabled" folders. + STATE + + + + SEPARATOR + + + + NAME + subsonic + + + TYPE + 0 + VERSION + 2 + + diff --git a/subsonic-installer-mac/src/subsonic.png b/subsonic-installer-mac/src/subsonic.png new file mode 100644 index 00000000..1ed82fb0 Binary files /dev/null and b/subsonic-installer-mac/src/subsonic.png differ diff --git a/subsonic-installer-rpm/pom.xml b/subsonic-installer-rpm/pom.xml new file mode 100644 index 00000000..9f4bd34a --- /dev/null +++ b/subsonic-installer-rpm/pom.xml @@ -0,0 +1,122 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-installer-rpm + pom + Subsonic Installer for RPM + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + + + rpm + + + rpm + + + + + + + org.codehaus.mojo + buildnumber-maven-plugin + + + validate + + create + + + + + false + false + + + + + maven-antrun-plugin + + + rpm + compile + + + Creating RPM package... + + + + + + + + + + + .beta1 + .beta2 + .beta3 + .beta4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + run + + + + + + + + + + + diff --git a/subsonic-installer-rpm/src/etc/init.d/subsonic b/subsonic-installer-rpm/src/etc/init.d/subsonic new file mode 100644 index 00000000..8c17ea44 --- /dev/null +++ b/subsonic-installer-rpm/src/etc/init.d/subsonic @@ -0,0 +1,104 @@ +#!/bin/bash +# +# subsonic This shell script takes care of starting and stopping Subsonic +# +# chkconfig: - 80 20 +# +### BEGIN INIT INFO +# Provides: subsonic +# Required-Start: $network $syslog +# Required-Stop: $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Subsonic daemon +# Description: Starts the Subsonic daemon. Subsonic is a web-based +# music streamer, jukebox and Podcast receiver. +# See http://subsonic.org for more details. +### END INIT INFO + +# Author: Sindre Mehus + +# To change the startup parameters of Subsonic, modify the service +# configuration file /etc/sysconfig/subsonic rather than this file. +[ -r /etc/sysconfig/subsonic ] && . /etc/sysconfig/subsonic + +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="Subsonic Daemon" +NAME=subsonic +PIDFILE=/var/run/$NAME.pid +LOCKFILE=/var/lock/subsys/$NAME +DAEMON=/usr/bin/$NAME +DAEMON_ARGS="--pidfile=$PIDFILE $SUBSONIC_ARGS" +SCRIPTNAME=/etc/init.d/$NAME + +# Exit if the package is not installed. +[ -x "$DAEMON" ] || exit 0 + +# Run as root if SUBSONIC_USER is not set. +[ "$SUBSONIC_USER" = "" ] && SUBSONIC_USER=root + +# Source function library. +. /etc/init.d/functions + +# +# Function that starts the daemon/service +# +do_start() +{ + # Check if daemon is already running. + if [ -e $PIDFILE ] + then + ps -p $(cat $PIDFILE) > /dev/null + [ "$?" = 0 ] && return 1 + fi + + touch $PIDFILE + chown $SUBSONIC_USER $PIDFILE + [ -e /var/subsonic ] && chown -R $SUBSONIC_USER /var/subsonic + [ -e /tmp/subsonic ] && chown -R $SUBSONIC_USER /tmp/subsonic + + echo $"Starting $NAME ..." + su -c "$DAEMON $DAEMON_ARGS" $SUBSONIC_USER + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && touch $LOCKFILE + return $RETVAL +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Check if pidfile exists + [ ! -e $PIDFILE ] && return 1 + + echo -n $"Stopping $NAME ..." + killproc -p $PIDFILE $DAEMON + RETVAL=$? + echo + [ $RETVAL -eq 0 ] && rm -f $LOCKFILE + return $RETVAL +} + +case "$1" in + start) + do_start + ;; + stop) + do_stop + ;; + status) + status -p $PIDFILE "$NAME" + ;; + restart|force-reload) + do_stop + do_start + ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/subsonic-installer-rpm/src/etc/sysconfig/subsonic b/subsonic-installer-rpm/src/etc/sysconfig/subsonic new file mode 100644 index 00000000..25fa2453 --- /dev/null +++ b/subsonic-installer-rpm/src/etc/sysconfig/subsonic @@ -0,0 +1,25 @@ +# +# This is the configuration file for the Subsonic service +# (/etc/init.d/subsonic) +# +# To change the startup parameters of Subsonic, modify +# the SUBSONIC_ARGS variable below. +# +# Type "/usr/share/subsonic/subsonic.sh --help" on the command line to read an +# explanation of the different options. +# +# For example, to specify that Subsonic should use port 80 (for http) +# and 443 (for https), and use a Java memory heap size of 200 MB, use +# the following: +# +# SUBSONIC_ARGS="--port=80 --https-port=443 --max-memory=200" + +SUBSONIC_ARGS="--max-memory=150" + + +# The user which should run the Subsonic process. Default "root". +# Note that non-root users are by default not allowed to use ports +# below 1024. Also make sure to grant the user write permissions in +# the music directories, otherwise changing album art and tags will fail. + +SUBSONIC_USER=root diff --git a/subsonic-installer-rpm/src/subsonic.spec b/subsonic-installer-rpm/src/subsonic.spec new file mode 100644 index 00000000..2e145b5f --- /dev/null +++ b/subsonic-installer-rpm/src/subsonic.spec @@ -0,0 +1,78 @@ +Name: subsonic +Version: @VERSION@ +Release: @BUILD_NUMBER@ +Summary: A web-based music streamer, jukebox and Podcast receiver + +Group: Applications/Multimedia +License: GPLv3 +URL: http://subsonic.org + +%description +Subsonic is a web-based music streamer, jukebox and Podcast receiver, +providing access to your music collection wherever you are. Use it +to share your music with friends, or to listen to your music while away +from home. + +Apps for Android, iPhone and Windows Phone are also available. + +Java 1.6 or higher is required to run Subsonic. + +Subsonic can be found at http://subsonic.org + +%files +%defattr(644,root,root,755) +/usr/share/subsonic/subsonic-booter-jar-with-dependencies.jar +/usr/share/subsonic/subsonic.war +%attr(755,root,root) /usr/share/subsonic/subsonic.sh +%attr(755,root,root) /etc/init.d/subsonic +%attr(755,root,root) /var/subsonic/transcode/ffmpeg +%attr(755,root,root) /var/subsonic/transcode/lame +%config(noreplace) /etc/sysconfig/subsonic + +%pre +# Stop Subsonic service. +if [ -e /etc/init.d/subsonic ]; then + service subsonic stop +fi + +# Backup database. +if [ -e /var/subsonic/db ]; then + rm -rf /var/subsonic/db.backup + cp -R /var/subsonic/db /var/subsonic/db.backup +fi + +exit 0 + +%post +ln -sf /usr/share/subsonic/subsonic.sh /usr/bin/subsonic +chmod 750 /var/subsonic + +# Clear jetty cache. +rm -rf /var/subsonic/jetty + +# For SELinux: Set security context +chcon -t java_exec_t /etc/init.d/subsonic 2>/dev/null + +# Configure and start Subsonic service. +chkconfig --add subsonic +service subsonic start + +exit 0 + +%preun +# Only do it if uninstalling, not upgrading. +if [ $1 = 0 ] ; then + + # Stop the service. + [ -e /etc/init.d/subsonic ] && service subsonic stop + + # Remove symlink. + rm -f /usr/bin/subsonic + + # Remove startup scripts. + chkconfig --del subsonic + +fi + +exit 0 + diff --git a/subsonic-installer-windows/pom.xml b/subsonic-installer-windows/pom.xml new file mode 100644 index 00000000..0d6d0314 --- /dev/null +++ b/subsonic-installer-windows/pom.xml @@ -0,0 +1,106 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-installer-windows + pom + Subsonic Installer for Windows + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + c:/Program Files/exe4j + c:/Program Files (x86)/NSIS + + + + + + net.sourceforge.subsonic + subsonic-main + ${project.version} + war + + + + net.sourceforge.subsonic + subsonic-booter + ${project.version} + + + + + + + + windows + + + windows + + + + + + + maven-antrun-plugin + + + exe4j + compile + + + Compiling exe4j... + + + + + + + + + + + + + + + + + + + run + + + + + nsis + compile + + + Compiling NSIS script... + + + + + + + + + run + + + + + + + + + + + diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-16.ico b/subsonic-installer-windows/src/main/exe4j/subsonic-16.ico new file mode 100644 index 00000000..d2c13383 Binary files /dev/null and b/subsonic-installer-windows/src/main/exe4j/subsonic-16.ico differ diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-agent-elevated.exe.vmoptions b/subsonic-installer-windows/src/main/exe4j/subsonic-agent-elevated.exe.vmoptions new file mode 100644 index 00000000..291ff123 --- /dev/null +++ b/subsonic-installer-windows/src/main/exe4j/subsonic-agent-elevated.exe.vmoptions @@ -0,0 +1 @@ +-Xmx16m diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-agent-elevated.exe4j b/subsonic-installer-windows/src/main/exe4j/subsonic-agent-elevated.exe4j new file mode 100644 index 00000000..b6c2d9a3 --- /dev/null +++ b/subsonic-installer-windows/src/main/exe4j/subsonic-agent-elevated.exe4j @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-agent.exe.vmoptions b/subsonic-installer-windows/src/main/exe4j/subsonic-agent.exe.vmoptions new file mode 100644 index 00000000..291ff123 --- /dev/null +++ b/subsonic-installer-windows/src/main/exe4j/subsonic-agent.exe.vmoptions @@ -0,0 +1 @@ +-Xmx16m diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-agent.exe4j b/subsonic-installer-windows/src/main/exe4j/subsonic-agent.exe4j new file mode 100644 index 00000000..5611e45f --- /dev/null +++ b/subsonic-installer-windows/src/main/exe4j/subsonic-agent.exe4j @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-service.exe.vmoptions b/subsonic-installer-windows/src/main/exe4j/subsonic-service.exe.vmoptions new file mode 100644 index 00000000..72a857fe --- /dev/null +++ b/subsonic-installer-windows/src/main/exe4j/subsonic-service.exe.vmoptions @@ -0,0 +1,6 @@ +-Xmx150m +-verbose:gc +-Dsubsonic.host=0.0.0.0 +-Dsubsonic.port=4040 +-Dsubsonic.httpsPort=0 +-Dsubsonic.contextPath=/ diff --git a/subsonic-installer-windows/src/main/exe4j/subsonic-service.exe4j b/subsonic-installer-windows/src/main/exe4j/subsonic-service.exe4j new file mode 100644 index 00000000..2b870b59 --- /dev/null +++ b/subsonic-installer-windows/src/main/exe4j/subsonic-service.exe4j @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/subsonic-installer-windows/src/main/nsis/jre-8u31-windows-i586-iftw.exe b/subsonic-installer-windows/src/main/nsis/jre-8u31-windows-i586-iftw.exe new file mode 100644 index 00000000..a122cd5e Binary files /dev/null and b/subsonic-installer-windows/src/main/nsis/jre-8u31-windows-i586-iftw.exe differ diff --git a/subsonic-installer-windows/src/main/nsis/subsonic.nsi b/subsonic-installer-windows/src/main/nsis/subsonic.nsi new file mode 100644 index 00000000..1a40bc82 --- /dev/null +++ b/subsonic-installer-windows/src/main/nsis/subsonic.nsi @@ -0,0 +1,213 @@ +# subsonic.nsi + +!include "WordFunc.nsh" +!include "MUI.nsh" + +!insertmacro VersionCompare + +# The name of the installer +Name "Subsonic" + +# The default installation directory +InstallDir $PROGRAMFILES\Subsonic + +# Registry key to check for directory (so if you install again, it will +# overwrite the old one automatically) +InstallDirRegKey HKLM "Software\Subsonic" "Install_Dir" + +#-------------------------------- +#Interface Configuration + +!define MUI_HEADERIMAGE +!define MUI_HEADERIMAGE_BITMAP "${NSISDIR}\Contrib\Graphics\Header\orange.bmp" +!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\Getting Started.html" +!define MUI_FINISHPAGE_SHOWREADME_TEXT "View Getting Started document" + +#-------------------------------- +# Pages + +# This page checks for JRE +Page custom CheckInstalledJRE + +!insertmacro MUI_PAGE_WELCOME +!insertmacro MUI_PAGE_DIRECTORY +!insertmacro MUI_PAGE_INSTFILES +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_WELCOME +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +# Languages +!insertmacro MUI_LANGUAGE "English" + +Section "Subsonic" + + SectionIn RO + + # Install for all users + SetShellVarContext "all" + + # Take backup of existing subsonic-service.exe.vmoptions + CopyFiles /SILENT $INSTDIR\subsonic-service.exe.vmoptions $TEMP\subsonic-service.exe.vmoptions + + # Silently uninstall existing version. + ExecWait '"$INSTDIR\uninstall.exe" /S _?=$INSTDIR' + + # Remove previous Jetty temp directory. + RMDir /r "c:\subsonic\jetty" + + # Backup database. + RMDir /r "c:\subsonic\db.backup" + CreateDirectory "c:\subsonic\db.backup" + CopyFiles /SILENT "c:\subsonic\db\*" "c:\subsonic\db.backup" + + # Set output path to the installation directory. + SetOutPath $INSTDIR + + # Write files. + File ..\..\..\target\subsonic-agent.exe + File ..\..\..\target\subsonic-agent.exe.vmoptions + File ..\..\..\target\subsonic-agent-elevated.exe + File ..\..\..\target\subsonic-agent-elevated.exe.vmoptions + File ..\..\..\target\subsonic-service.exe + File ..\..\..\target\subsonic-service.exe.vmoptions + File ..\..\..\..\subsonic-booter\target\subsonic-booter-jar-with-dependencies.jar + File ..\..\..\..\subsonic-main\README.TXT + File ..\..\..\..\subsonic-main\LICENSE.TXT + File "..\..\..\..\subsonic-main\Getting Started.html" + File ..\..\..\..\subsonic-main\target\subsonic.war + File ..\..\..\..\subsonic-main\target\classes\version.txt + File ..\..\..\..\subsonic-main\target\classes\build_number.txt + + # Write the installation path into the registry + WriteRegStr HKLM SOFTWARE\Subsonic "Install_Dir" "$INSTDIR" + + # Write the uninstall keys for Windows + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Subsonic" "DisplayName" "Subsonic" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Subsonic" "UninstallString" '"$INSTDIR\uninstall.exe"' + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Subsonic" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Subsonic" "NoRepair" 1 + WriteUninstaller "uninstall.exe" + + # Restore subsonic-service.exe.vmoptions + CopyFiles /SILENT $TEMP\subsonic-service.exe.vmoptions $INSTDIR\subsonic-service.exe.vmoptions + Delete $TEMP\subsonic-service.exe.vmoptions + + # Write transcoding pack files. + SetOutPath "c:\subsonic\transcode" + File ..\..\..\..\subsonic-transcode\windows\*.* + + # Add Windows Firewall exception. + # (Requires NSIS plugin found on http://nsis.sourceforge.net/NSIS_Simple_Firewall_Plugin to be installed + # as NSIS_HOME/Plugins/SimpleFC.dll) + + SimpleFC::AdvAddRule "Subsonic Service (TCP)" "" "6" "1" "1" "7" "1" "$INSTDIR\subsonic-service.exe" "" "" "Subsonic" "" "" "" "" + SimpleFC::AdvAddRule "Subsonic Service (UDP)" "" "17" "1" "1" "7" "1" "$INSTDIR\subsonic-service.exe" "" "" "Subsonic" "" "" "" "" + SimpleFC::AdvAddRule "Subsonic Agent (TCP)" "" "6" "1" "1" "7" "1" "$INSTDIR\subsonic-agent.exe" "" "" "Subsonic" "" "" "" "" + SimpleFC::AdvAddRule "Subsonic Agent (UDP)" "" "17" "1" "1" "7" "1" "$INSTDIR\subsonic-agent.exe" "" "" "Subsonic" "" "" "" "" + SimpleFC::AdvAddRule "Subsonic Agent Elevated (TCP)" "" "6" "1" "1" "7" "1" "$INSTDIR\subsonic-agent-elevated.exe" "" "" "Subsonic" "" "" "" "" + SimpleFC::AdvAddRule "Subsonic Agent Elevated (UDP)" "" "17" "1" "1" "7" "1" "$INSTDIR\subsonic-agent-elevated.exe" "" "" "Subsonic" "" "" "" "" + + # Install and start service. + ExecWait '"$INSTDIR\subsonic-service.exe" -install' + ExecWait '"$INSTDIR\subsonic-service.exe" -start' + + # Start agent. + Exec '"$INSTDIR\subsonic-agent-elevated.exe" -balloon' + +SectionEnd + + +Section "Start Menu Shortcuts" + + CreateDirectory "$SMPROGRAMS\Subsonic" + CreateShortCut "$SMPROGRAMS\Subsonic\Open Subsonic.lnk" "$INSTDIR\subsonic.url" "" "$INSTDIR\subsonic-agent.exe" 0 + CreateShortCut "$SMPROGRAMS\Subsonic\Subsonic Tray Icon.lnk" "$INSTDIR\subsonic-agent.exe" "-balloon" "$INSTDIR\subsonic-agent.exe" 0 + CreateShortCut "$SMPROGRAMS\Subsonic\Start Subsonic Service.lnk" "$INSTDIR\subsonic-service.exe" "-start" "$INSTDIR\subsonic-service.exe" 0 + CreateShortCut "$SMPROGRAMS\Subsonic\Stop Subsonic Service.lnk" "$INSTDIR\subsonic-service.exe" "-stop" "$INSTDIR\subsonic-service.exe" 0 + CreateShortCut "$SMPROGRAMS\Subsonic\Uninstall Subsonic.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 + CreateShortCut "$SMPROGRAMS\Subsonic\Getting Started.lnk" "$INSTDIR\Getting Started.html" "" "$INSTDIR\Getting Started.html" 0 + + CreateShortCut "$SMSTARTUP\Subsonic.lnk" "$INSTDIR\subsonic-agent.exe" "" "$INSTDIR\subsonic-agent.exe" 0 + +SectionEnd + + +# Uninstaller + +Section "Uninstall" + + # Uninstall for all users + SetShellVarContext "all" + + # Stop and uninstall service if present. + ExecWait '"$INSTDIR\subsonic-service.exe" -stop' + ExecWait '"$INSTDIR\subsonic-service.exe" -uninstall' + + # Stop agent by killing it. + # (Requires NSIS plugin found on http://nsis.sourceforge.net/Processes_plug-in to be installed + # as NSIS_HOME/Plugins/Processes.dll) + Processes::KillProcess "subsonic-agent" + Processes::KillProcess "subsonic-agent-elevated" + Processes::KillProcess "ffmpeg" + + # Remove registry keys + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Subsonic" + DeleteRegKey HKLM SOFTWARE\Subsonic + + # Remove files. + Delete "$SMSTARTUP\Subsonic.lnk" + RMDir /r "$SMPROGRAMS\Subsonic" + Delete "$INSTDIR\build_number.txt" + Delete "$INSTDIR\elevate.exe" + Delete "$INSTDIR\Getting Started.html" + Delete "$INSTDIR\LICENSE.TXT" + Delete "$INSTDIR\README.TXT" + Delete "$INSTDIR\subsonic.url" + Delete "$INSTDIR\subsonic.war" + Delete "$INSTDIR\subsonic-agent.exe" + Delete "$INSTDIR\subsonic-agent.exe.vmoptions" + Delete "$INSTDIR\subsonic-agent-elevated.exe" + Delete "$INSTDIR\subsonic-agent-elevated.exe.vmoptions" + Delete "$INSTDIR\subsonic-booter-jar-with-dependencies.jar" + Delete "$INSTDIR\subsonic-service.exe" + Delete "$INSTDIR\subsonic-service.exe.vmoptions" + Delete "$INSTDIR\uninstall.exe" + Delete "$INSTDIR\version.txt" + RMDir /r "$INSTDIR\log" + RMDir "$INSTDIR" + + # Remove Windows Firewall exception. + # (Requires NSIS plugin found on http://nsis.sourceforge.net/NSIS_Simple_Firewall_Plugin to be installed + # as NSIS_HOME/Plugins/SimpleFC.dll) + SimpleFC::AdvRemoveRule "Subsonic Service (TCP)" + SimpleFC::AdvRemoveRule "Subsonic Service (UDP)" + SimpleFC::AdvRemoveRule "Subsonic Agent (TCP)" + SimpleFC::AdvRemoveRule "Subsonic Agent (UDP)" + SimpleFC::AdvRemoveRule "Subsonic Agent Elevated (TCP)" + SimpleFC::AdvRemoveRule "Subsonic Agent Elevated (UDP)" + +SectionEnd + + +Function CheckInstalledJRE + # Read the value from the registry into the $0 register + ReadRegStr $0 HKLM "SOFTWARE\JavaSoft\Java Runtime Environment" CurrentVersion + + # Check JRE version. At least 1.6 is required. + # $1=0 Versions are equal + # $1=1 Installed version is newer + # $1=2 Installed version is older (or non-existent) + ${VersionCompare} $0 "1.6" $1 + IntCmp $1 2 InstallJRE 0 0 + Return + + InstallJRE: + # Launch Java web installer. + MessageBox MB_OK "Java was not found and will now be installed." + File /oname=$TEMP\jre-setup.exe jre-8u31-windows-i586-iftw.exe + ExecWait '"$TEMP\jre-setup.exe"' $0 + Delete "$TEMP\jre-setup.exe" + +FunctionEnd diff --git a/subsonic-main/Getting Started.html b/subsonic-main/Getting Started.html new file mode 100644 index 00000000..a5160bd8 --- /dev/null +++ b/subsonic-main/Getting Started.html @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/subsonic-main/LICENSE.TXT b/subsonic-main/LICENSE.TXT new file mode 100644 index 00000000..20d40b6b --- /dev/null +++ b/subsonic-main/LICENSE.TXT @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. \ No newline at end of file diff --git a/subsonic-main/README.TXT b/subsonic-main/README.TXT new file mode 100644 index 00000000..f9a620ba --- /dev/null +++ b/subsonic-main/README.TXT @@ -0,0 +1,16 @@ + +WELCOME TO SUBSONIC! +-------------------- + +Subsonic is a free, web-based media streamer and jukebox. + +More information, including installation instructions, is found at http://subsonic.org/ + +Subsonic is free software: you can redistribute it and/or modify it under the terms of the +GNU General Public License. + +Subsonic is developed and maintained by Sindre Mehus (sindre@activeobjects.no). + +If you have any questions, comments or suggestions for improvements, please visit the +Subsonic Forum (http://forum.subsonic.org). + diff --git a/subsonic-main/dreamplug.txt b/subsonic-main/dreamplug.txt new file mode 100644 index 00000000..cbf4ccab --- /dev/null +++ b/subsonic-main/dreamplug.txt @@ -0,0 +1,102 @@ + +Dreamplug +=========== +The Access Point uap0 is set to 192.168.1.1. If your router already uses this set of IP address, +you will run into a conflict and the internet connection will go. +Two things you can do. + change the default ip address in /root/init_setup.sh + and you can take down the AP if you are purely not interested in it. +ifconfig uap0 down +and comment out the call to the init_setup.sh in /etc/rc.local + http://www.newit.co.uk/forum/index.php?topic=1078.15 +http://plugcomputer.org/plugwiki/index.php/New_Plugger_How_To +sdf +Login : root +Password: nosoup4u + +type wlan.sh command to switch from WLAN AP to client mode. + + +----- + +root@ubuntu:~# apt-get install openjdk-6-jre +Reading package lists... Done +Building dependency tree +Reading state information... Done +The following extra packages will be installed: + libgif4 libice6 libpulse0 libsm6 libxi6 libxrender1 libxtst6 +Suggested packages: + pulseaudio icedtea6-plugin +The following NEW packages will be installed: + libgif4 libice6 libpulse0 libsm6 libxi6 libxrender1 libxtst6 openjdk-6-jre +0 upgraded, 8 newly installed, 0 to remove and 35 not upgraded. +Need to get 615kB of archives. +After this operation, 1978kB of additional disk space will be used. +Do you want to continue [Y/n]? +Get:1 http://ports.ubuntu.com jaunty/main libgif4 4.1.6-6 [39.2kB] +Get:2 http://ports.ubuntu.com jaunty/main libice6 2:1.0.4-1 [44.3kB] +Get:3 http://ports.ubuntu.com jaunty/main libsm6 2:1.1.0-1 [21.6kB] +Get:4 http://ports.ubuntu.com jaunty-updates/main libpulse0 1:0.9.14-0ubuntu20.3 [178kB] +Get:5 http://ports.ubuntu.com jaunty-updates/main libxi6 2:1.2.0-1ubuntu1.1 [26.5kB] +Get:6 http://ports.ubuntu.com jaunty/main libxrender1 1:0.9.4-2 [28.2kB] +Get:7 http://ports.ubuntu.com jaunty/main libxtst6 2:1.0.3-1ubuntu2 [12.8kB] +Get:8 http://ports.ubuntu.com jaunty-updates/main openjdk-6-jre 6b18-1.8.2-4ubuntu1~9.04.1 [264kB] +Fetched 615kB in 0s (726kB/s) +Selecting previously deselected package libgif4. +(Reading database ... 21520 files and directories currently installed.) +Unpacking libgif4 (from .../libgif4_4.1.6-6_armel.deb) ... +Selecting previously deselected package libice6. +Unpacking libice6 (from .../libice6_2%3a1.0.4-1_armel.deb) ... +Selecting previously deselected package libsm6. +Unpacking libsm6 (from .../libsm6_2%3a1.1.0-1_armel.deb) ... +Selecting previously deselected package libpulse0. +Unpacking libpulse0 (from .../libpulse0_1%3a0.9.14-0ubuntu20.3_armel.deb) ... +Selecting previously deselected package libxi6. +Unpacking libxi6 (from .../libxi6_2%3a1.2.0-1ubuntu1.1_armel.deb) ... +Selecting previously deselected package libxrender1. +Unpacking libxrender1 (from .../libxrender1_1%3a0.9.4-2_armel.deb) ... +Selecting previously deselected package libxtst6. +Unpacking libxtst6 (from .../libxtst6_2%3a1.0.3-1ubuntu2_armel.deb) ... +Selecting previously deselected package openjdk-6-jre. +Unpacking openjdk-6-jre (from .../openjdk-6-jre_6b18-1.8.2-4ubuntu1~9.04.1_armel.deb) ... +Setting up libgif4 (4.1.6-6) ... + +Setting up libice6 (2:1.0.4-1) ... + +Setting up libsm6 (2:1.1.0-1) ... + +Setting up libpulse0 (1:0.9.14-0ubuntu20.3) ... + +Setting up libxi6 (2:1.2.0-1ubuntu1.1) ... + +Setting up libxrender1 (1:0.9.4-2) ... + +Setting up libxtst6 (2:1.0.3-1ubuntu2) ... + +Setting up openjdk-6-jre (6b18-1.8.2-4ubuntu1~9.04.1) ... + +Processing triggers for libc6 ... +ldconfig deferred processing now taking place +root@ubuntu:~# + +root@ubuntu:~# java -version +java version "1.6.0_18" +OpenJDK Runtime Environment (IcedTea6 1.8.2) (6b18-1.8.2-4ubuntu1~9.04.1) +OpenJDK Zero VM (build 14.0-b16, mixed mode) + +root@ubuntu:/tmp# dpkg -i subsonic-4.4.deb +Selecting previously deselected package subsonic. +(Reading database ... 21622 files and directories currently installed.) +Unpacking subsonic (from subsonic-4.4.deb) ... +Setting up subsonic (4.4) ... + Adding system startup for /etc/init.d/subsonic ... + /etc/rc0.d/K99subsonic -> ../init.d/subsonic + /etc/rc1.d/K99subsonic -> ../init.d/subsonic + /etc/rc6.d/K99subsonic -> ../init.d/subsonic + /etc/rc2.d/S99subsonic -> ../init.d/subsonic + /etc/rc3.d/S99subsonic -> ../init.d/subsonic + /etc/rc4.d/S99subsonic -> ../init.d/subsonic + /etc/rc5.d/S99subsonic -> ../init.d/subsonic +Started Subsonic [PID 2596, /var/subsonic/subsonic_sh.log] + +http://192.168.0.100:4040 \ No newline at end of file diff --git a/subsonic-main/pom.xml b/subsonic-main/pom.xml new file mode 100644 index 00000000..71068686 --- /dev/null +++ b/subsonic-main/pom.xml @@ -0,0 +1,412 @@ + + + 4.0.0 + net.sourceforge.subsonic + subsonic-main + war + Subsonic Main + + + net.sourceforge.subsonic + subsonic + 5.3 + + + + + + net.sourceforge.subsonic + subsonic-rest-api + ${project.version} + + + + net.sourceforge.subsonic + subsonic-sonos-api + ${project.version} + + + + org.springframework + spring + 2.5.6 + + + + org.springframework + spring-webmvc + 2.5.6 + + + org.springframework + spring-beans + + + org.springframework + spring-core + + + org.springframework + spring-context + + + org.springframework + spring-context-support + + + org.springframework + spring-web + + + + + + org.acegisecurity + acegi-security + 1.0.5 + + + org.springframework + spring-core + + + org.springframework + spring-remoting + + + org.springframework + spring-jdbc + + + org.springframework + spring-support + + + + + + org.apache.lucene + lucene-core + 3.0.3 + + + + cglib + cglib + 2.1_3 + runtime + + + + commons-fileupload + commons-fileupload + 1.2 + + + + commons-codec + commons-codec + 1.2 + + + + commons-io + commons-io + 1.3.1 + + + + commons-lang + commons-lang + 2.1 + + + + com.google.guava + guava-base + r03 + + + + com.google.guava + guava-collections + r03 + + + + org.apache.httpcomponents + httpcore + 4.2.4 + + + + org.apache.httpcomponents + httpclient + 4.2.4 + + + + hsqldb + hsqldb + 1.8.0.7 + runtime + + + + radeox + radeox + 1.0-b2 + + + + log4j + log4j + 1.2.16 + runtime + + + + org.slf4j + slf4j-log4j12 + 1.6.1 + runtime + + + + org.directwebremoting + dwr + 3.0.rc1 + + + + + com.yahoo.platform.yui + yuicompressor + 2.3.6 + runtime + + + + ant-zip + ant-zip + 1.6.2 + + + + org + jaudiotagger + 2.0.3 + + + + jfree + jfreechart + 1.0.11 + + + junit + junit + + + gnujaxp + gnujaxp + + + + + + jdom + jdom + 1.0 + + + + net.sf.ehcache + ehcache-core + 2.5.0 + + + + org.eclipse.persistence + org.eclipse.persistence.moxy + 2.5.1 + + + + javax.servlet + servlet-api + 2.4 + provided + + + + javax.servlet + jsp-api + 2.0 + provided + + + + javax.servlet + jstl + 1.1.2 + runtime + + + + taglibs + standard + 1.1.2 + + + + taglibs + string + 1.1.0 + runtime + + + + com.hoodcomputing + natpmp + 0.1 + + + + junit + junit + 4.1 + test + + + + org.fourthline.cling + cling-core + 2.0.1 + + + + org.fourthline.cling + cling-support + 2.0.1 + + + + org.seamless + seamless-util + 1.0-alpha2 + + + + net.tanesha.recaptcha4j + recaptcha4j + 0.0.8 + + + + de.u-mass + lastfm-java + 0.1.2 + + + + org.apache.cxf + cxf-rt-transports-http + ${cxf.version} + + + org.springframework + spring-web + + + runtime + + + + org.apache.cxf + cxf-rt-frontend-jaxws + ${cxf.version} + + + + org.apache.cxf + cxf-rt-bindings-soap + ${cxf.version} + + + + org.apache.cxf + cxf-rt-databinding-jaxb + ${cxf.version} + + + + org.apache.cxf + cxf-api + ${cxf.version} + + + + org.apache.cxf + cxf-common-utilities + ${cxf.version} + + + + + + + full + + + + org.codehaus.mojo + buildnumber-maven-plugin + + + validate + + create + + + + + false + false + + + + + + + + + subsonic + + + + maven-antrun-plugin + + + generate-resources + + + + + ${buildNumber} + ${DSTAMP} + ${project.version} + + + + run + + + + + + + + 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..c8ccfd9d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/Logger.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic; + +import net.sourceforge.subsonic.domain.Version; +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. + *
+ * 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 entries = Collections.synchronizedList(new BoundedList(50)); + private static PrintWriter writer; + private static Boolean debugEnabled; + + /** + * 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[entries.size()]); + } + + 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) { + if (isDebugEnabled()) { + add(Level.DEBUG, message, error); + } + } + + private static boolean isDebugEnabled() { + if (debugEnabled == null) { + VersionService versionService = ServiceLocator.getVersionService(); + if (versionService == null) { + return true; // versionService not yet available. + } + Version localVersion = versionService.getLocalVersion(); + debugEnabled = localVersion == null || localVersion.getBeta() != 0; + } + return debugEnabled; + } + + /** + * 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); + } + 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() { + StringBuilder builder = new StringBuilder(); + builder.append('[').append(DATE_FORMAT.format(date)).append("] "); + builder.append(level).append(' '); + builder.append(category).append(" - "); + builder.append(message); + + if (error != null) { + builder.append('\n').append(ExceptionUtils.getFullStackTrace(error)); + } + return builder.toString(); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ArtistInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ArtistInfo.java new file mode 100644 index 00000000..f2c0e85d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/ArtistInfo.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 . + * + * Copyright 2014 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.ajax; + +import java.util.List; + +import net.sourceforge.subsonic.domain.ArtistBio; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class ArtistInfo { + + private final List similarArtists; + private final ArtistBio artistBio; + private final List topSongs; + + public ArtistInfo(List similarArtists, ArtistBio artistBio, List topSongs) { + this.similarArtists = similarArtists; + this.artistBio = artistBio; + this.topSongs = topSongs; + } + + public List getSimilarArtists() { + return similarArtists; + } + + public ArtistBio getArtistBio() { + return artistBio; + } + + public List getTopSongs() { + return topSongs; + } +} 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 . + + 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 messages = new BoundedList(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 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(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 messages; + private final long revision; + + public Messages(List messages, long revision) { + this.messages = messages; + this.revision = revision; + } + + public List 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 . + + 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..2d9b801d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/CoverArtService.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; + +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. + *

+ * 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, null 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; + OutputStream output = 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." + suffix + ".backup")); + + // Write file. + output = new FileOutputStream(newCoverFile); + IOUtils.copy(input, output); + + MediaFile dir = mediaFileService.getMediaFile(path); + + // Refresh database. + mediaFileService.refreshMediaFile(dir); + dir = mediaFileService.getMediaFile(dir.getId()); + + // Rename existing cover files if new cover file is not the preferred. + try { + while (true) { + File coverFile = mediaFileService.getCoverArt(dir); + if (coverFile != null && !isMediaFile(coverFile) && !newCoverFile.equals(coverFile)) { + if (!coverFile.renameTo(new File(coverFile.getCanonicalPath() + ".old"))) { + LOG.warn("Unable to rename old image file " + coverFile); + break; + } + LOG.info("Renamed old image file " + coverFile); + + // Must refresh again. + mediaFileService.refreshMediaFile(dir); + dir = mediaFileService.getMediaFile(dir.getId()); + } else { + break; + } + } + } catch (Exception x) { + LOG.warn("Failed to rename existing cover file.", x); + } + + } finally { + IOUtils.closeQuietly(input); + IOUtils.closeQuietly(output); + client.getConnectionManager().shutdown(); + } + } + + private boolean isMediaFile(File file) { + return !mediaFileService.filterMediaFiles(new File[]{file}).isEmpty(); + } + + 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..2d8b62b1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsInfo.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 . + + 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; + private boolean tryLater; + + 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; + } + + public void setTryLater(boolean tryLater) { + this.tryLater = tryLater; + } + + public boolean isTryLater() { + return tryLater; + } +} 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..ce683b98 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/LyricsService.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import java.io.IOException; +import java.io.StringReader; +import java.net.SocketException; + +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 org.jdom.Document; +import org.jdom.Element; +import org.jdom.Namespace; +import org.jdom.input.SAXBuilder; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Provides AJAX-enabled services for retrieving song lyrics from chartlyrics.com. + *

+ * See http://www.chartlyrics.com/api.aspx for details. + *

+ * 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 null . + */ + 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 (SocketException x) { + lyrics.setTryLater(true); + } 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 = StringUtils.trimToNull(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 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..de04b4a7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/MultiService.java @@ -0,0 +1,137 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.directwebremoting.WebContextFactory; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.ArtistBio; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.LastFmService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.NetworkService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Provides miscellaneous AJAX-enabled services. + *

+ * 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; + private MediaFileService mediaFileService; + private LastFmService lastFmService; + private SecurityService securityService; + private SettingsService settingsService; + + /** + * 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 ArtistInfo getArtistInfo(int mediaFileId, int maxSimilarArtists, int maxTopSongs) { + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + List similarArtists = getSimilarArtists(mediaFileId, maxSimilarArtists); + ArtistBio artistBio = lastFmService.getArtistBio(mediaFile); + List topSongs = getTopSongs(mediaFile, maxTopSongs); + + return new ArtistInfo(similarArtists, artistBio, topSongs); + } + + private List getTopSongs(MediaFile mediaFile, int limit) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + List result = new ArrayList(); + List files = lastFmService.getTopSongs(mediaFile, limit, musicFolders); + mediaFileService.populateStarredDate(files, username); + for (MediaFile file : files) { + result.add(new TopSong(file.getId(), file.getTitle(), file.getArtist(), file.getAlbumName(), + file.getDurationString(), file.getStarredDate() != null)); + } + return result; + } + + private List getSimilarArtists(int mediaFileId, int limit) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + MediaFile artist = mediaFileService.getMediaFile(mediaFileId); + List similarArtists = lastFmService.getSimilarArtists(artist, limit, false, musicFolders); + SimilarArtist[] result = new SimilarArtist[similarArtists.size()]; + for (int i = 0; i < result.length; i++) { + MediaFile similarArtist = similarArtists.get(i); + result[i] = new SimilarArtist(similarArtist.getId(), similarArtist.getName()); + } + return Arrays.asList(result); + } + + public void setShowSideBar(boolean show) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + UserSettings userSettings = settingsService.getUserSettings(username); + userSettings.setShowSideBar(show); + userSettings.setChanged(new Date()); + settingsService.updateUserSettings(userSettings); + } + + public void setNetworkService(NetworkService networkService) { + this.networkService = networkService; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setLastFmService(LastFmService lastFmService) { + this.lastFmService = lastFmService; + } + + 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/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 . + + 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..0167d212 --- /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 . + + 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 playerId; + 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 avatarUrl; + private final int minutesAgo; + + public NowPlayingInfo(String playerId, String user, String artist, String title, String tooltip, String streamUrl, String albumUrl, + String lyricsUrl, String coverArtUrl, String avatarUrl, int minutesAgo) { + this.playerId = playerId; + 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.avatarUrl = avatarUrl; + this.minutesAgo = minutesAgo; + } + + public String getPlayerId() { + return playerId; + } + + 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 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..b29c8491 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/NowPlayingService.java @@ -0,0 +1,176 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.directwebremoting.WebContext; +import org.directwebremoting.WebContextFactory; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PlayStatus; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.UserSettings; +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; + +/** + * 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 static final Logger LOG = Logger.getLogger(NowPlayingService.class); + + private PlayerService playerService; + private StatusService statusService; + private SettingsService settingsService; + private MediaScannerService mediaScannerService; + + /** + * Returns details about what the current player is playing. + * + * @return Details about what the current player is playing, or null if not playing anything. + */ + public NowPlayingInfo getNowPlayingForCurrentPlayer() throws Exception { + WebContext webContext = WebContextFactory.get(); + Player player = playerService.getPlayer(webContext.getHttpServletRequest(), webContext.getHttpServletResponse()); + + for (NowPlayingInfo info : getNowPlaying()) { + if (player.getId().equals(info.getPlayerId())) { + return info; + } + } + return null; + } + + /** + * Returns details about what all users are currently playing. + * + * @return Details about what all users are currently playing. + */ + public List getNowPlaying() throws Exception { + try { + return convert(statusService.getPlayStatuses()); + } catch (Throwable x) { + LOG.error("Unexpected error in getNowPlaying: " + x, x); + return Collections.emptyList(); + } + } + + /** + * Returns media folder scanning status. + */ + public ScanInfo getScanningStatus() { + return new ScanInfo(mediaScannerService.isScanning(), mediaScannerService.getScanCount()); + } + + private List convert(List playStatuses) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String url = request.getRequestURL().toString(); + List result = new ArrayList(); + for (PlayStatus status : playStatuses) { + + Player player = status.getPlayer(); + MediaFile mediaFile = status.getMediaFile(); + String username = player.getUsername(); + if (username == null) { + continue; + } + UserSettings userSettings = settingsService.getUserSettings(username); + if (!userSettings.isNowPlayingAllowed()) { + continue; + } + + 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 = null; + if (!mediaFile.isVideo()) { + lyricsUrl = url.replaceFirst("/dwr/.*", "/lyrics.view?artistUtf8Hex=" + StringUtil.utf8HexEncode(artist) + + "&songUtf8Hex=" + StringUtil.utf8HexEncode(title)); + } + String coverArtUrl = url.replaceFirst("/dwr/.*", "/coverArt.view?size=60&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?usernameUtf8Hex=" + StringUtil.utf8HexEncode(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); + 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.getMinutesAgo(); + + if (minutesAgo < 60) { + result.add(new NowPlayingInfo(player.getId(),username, artist, title, tooltip, streamUrl, albumUrl, lyricsUrl, + coverArtUrl, 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; + } +} 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..cc34e7ec --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueInfo.java @@ -0,0 +1,217 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import java.util.List; + +import net.sourceforge.subsonic.util.StringUtil; + +/** + * The playlist of a player. + * + * @author Sindre Mehus + */ +public class PlayQueueInfo { + + private final List entries; + private final boolean stopEnabled; + private final boolean repeatEnabled; + private final boolean sendM3U; + private final float gain; + private int startPlayerAt = -1; + private long startPlayerAtPosition; // millis + + public PlayQueueInfo(List entries, boolean stopEnabled, boolean repeatEnabled, boolean sendM3U, float gain) { + this.entries = entries; + this.stopEnabled = stopEnabled; + this.repeatEnabled = repeatEnabled; + this.sendM3U = sendM3U; + this.gain = gain; + } + + public List getEntries() { + return entries; + } + + public String getDurationAsString() { + int durationSeconds = 0; + for (Entry entry : entries) { + if (entry.getDuration() != null) { + durationSeconds += entry.getDuration(); + } + } + return StringUtil.formatDuration(durationSeconds); + } + + public boolean isStopEnabled() { + return stopEnabled; + } + + public boolean isSendM3U() { + return sendM3U; + } + + public boolean isRepeatEnabled() { + return repeatEnabled; + } + + public float getGain() { + return gain; + } + + public int getStartPlayerAt() { + return startPlayerAt; + } + + public PlayQueueInfo setStartPlayerAt(int startPlayerAt) { + this.startPlayerAt = startPlayerAt; + return this; + } + + public long getStartPlayerAtPosition() { + return startPlayerAtPosition; + } + + public PlayQueueInfo setStartPlayerAtPosition(long startPlayerAtPosition) { + this.startPlayerAtPosition = startPlayerAtPosition; + return this; + } + + 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; + private final String remoteStreamUrl; + private final String coverArtUrl; + private final String remoteCoverArtUrl; + + 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, String remoteStreamUrl, String coverArtUrl, String remoteCoverArtUrl) { + 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; + this.remoteStreamUrl = remoteStreamUrl; + this.coverArtUrl = coverArtUrl; + this.remoteCoverArtUrl = remoteCoverArtUrl; + } + + 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; + } + + public String getRemoteStreamUrl() { + return remoteStreamUrl; + } + + public String getCoverArtUrl() { + return coverArtUrl; + } + + public String getRemoteCoverArtUrl() { + return remoteCoverArtUrl; + } + } +} \ 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..3fbabbe6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlayQueueService.java @@ -0,0 +1,737 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import java.util.ArrayList; +import java.util.Arrays; +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 org.directwebremoting.WebContextFactory; +import org.springframework.web.servlet.support.RequestContextUtils; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.dao.PlayQueueDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.PodcastStatus; +import net.sourceforge.subsonic.domain.SavedPlayQueue; +import net.sourceforge.subsonic.domain.UrlRedirectType; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.JukeboxService; +import net.sourceforge.subsonic.service.LastFmService; +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.TranscodingService; +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 + */ +@SuppressWarnings("UnusedDeclaration") +public class PlayQueueService { + + private PlayerService playerService; + private JukeboxService jukeboxService; + private TranscodingService transcodingService; + private SettingsService settingsService; + private MediaFileService mediaFileService; + private LastFmService lastFmService; + private SecurityService securityService; + private SearchService searchService; + private RatingService ratingService; + private PodcastService podcastService; + private net.sourceforge.subsonic.service.PlaylistService playlistService; + private MediaFileDao mediaFileDao; + private PlayQueueDao playQueueDao; + + /** + * 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 void savePlayQueue(int currentSongIndex, long positionMillis) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + Player player = getCurrentPlayer(request, response); + String username = securityService.getCurrentUsername(request); + PlayQueue playQueue = player.getPlayQueue(); + List ids = MediaFile.toIdList(playQueue.getFiles()); + + Integer currentId = currentSongIndex == -1 ? null : playQueue.getFile(currentSongIndex).getId(); + SavedPlayQueue savedPlayQueue = new SavedPlayQueue(null, username, ids, currentId, positionMillis, new Date(), "Subsonic"); + playQueueDao.savePlayQueue(savedPlayQueue); + } + + public PlayQueueInfo loadPlayQueue() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + String username = securityService.getCurrentUsername(request); + SavedPlayQueue savedPlayQueue = playQueueDao.getPlayQueue(username); + + if (savedPlayQueue == null) { + return convert(request, player, false); + } + + PlayQueue playQueue = player.getPlayQueue(); + playQueue.clear(); + for (Integer mediaFileId : savedPlayQueue.getMediaFileIds()) { + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + if (mediaFile != null) { + playQueue.addFiles(true, mediaFile); + } + } + PlayQueueInfo result = convert(request, player, false); + + Integer currentId = savedPlayQueue.getCurrentMediaFileId(); + int currentIndex = -1; + long positionMillis = savedPlayQueue.getPositionMillis() == null ? 0L : savedPlayQueue.getPositionMillis(); + if (currentId != null) { + MediaFile current = mediaFileService.getMediaFile(currentId); + currentIndex = playQueue.getFiles().indexOf(current); + if (currentIndex != -1) { + result.setStartPlayerAt(currentIndex); + result.setStartPlayerAtPosition(positionMillis); + } + } + + boolean serverSidePlaylist = !player.isExternalWithPlaylist(); + if (serverSidePlaylist && currentIndex != -1) { + doSkip(request, response, currentIndex, (int) (positionMillis / 1000L)); + } + + return result; + } + + 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); + + if (file.isFile()) { + String username = securityService.getCurrentUsername(request); + boolean queueFollowingSongs = settingsService.getUserSettings(username).isQueueFollowingSongs(); + List songs; + if (queueFollowingSongs) { + MediaFile dir = mediaFileService.getParentOf(file); + songs = mediaFileService.getChildrenOf(dir, true, false, true); + if (!songs.isEmpty()) { + int index = songs.indexOf(file); + songs = songs.subList(index, songs.size()); + } + } else { + songs = Arrays.asList(file); + } + return doPlay(request, player, songs).setStartPlayerAt(0); + } else { + List songs = mediaFileService.getDescendantsOf(file, true); + return doPlay(request, player, songs).setStartPlayerAt(0); + } + } + + /** + * @param index Start playing at this index, or play whole playlist if {@code null}. + */ + public PlayQueueInfo playPlaylist(int id, Integer index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + String username = securityService.getCurrentUsername(request); + boolean queueFollowingSongs = settingsService.getUserSettings(username).isQueueFollowingSongs(); + + List files = playlistService.getFilesInPlaylist(id, true); + if (!files.isEmpty() && index != null) { + if (queueFollowingSongs) { + files = files.subList(index, files.size()); + } else { + files = Arrays.asList(files.get(index)); + } + } + + // Remove non-present files + Iterator iterator = files.iterator(); + while (iterator.hasNext()) { + MediaFile file = iterator.next(); + if (!file.isPresent()) { + iterator.remove(); + } + } + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files).setStartPlayerAt(0); + } + + /** + * @param index Start playing at this index, or play all top songs if {@code null}. + */ + public PlayQueueInfo playTopSong(int id, Integer index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + String username = securityService.getCurrentUsername(request); + boolean queueFollowingSongs = settingsService.getUserSettings(username).isQueueFollowingSongs(); + + List musicFolders = settingsService.getMusicFoldersForUser(username); + List files = lastFmService.getTopSongs(mediaFileService.getMediaFile(id), 50, musicFolders); + if (!files.isEmpty() && index != null) { + if (queueFollowingSongs) { + files = files.subList(index, files.size()); + } else { + files = Arrays.asList(files.get(index)); + } + } + + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files).setStartPlayerAt(0); + } + + public PlayQueueInfo playPodcastChannel(int id) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + List episodes = podcastService.getEpisodes(id); + List files = new ArrayList(); + for (PodcastEpisode episode : episodes) { + if (episode.getStatus() == PodcastStatus.COMPLETED) { + MediaFile mediaFile = mediaFileService.getMediaFile(episode.getMediaFileId()); + if (mediaFile != null && mediaFile.isPresent()) { + files.add(mediaFile); + } + } + } + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files).setStartPlayerAt(0); + } + + public PlayQueueInfo playPodcastEpisode(int id) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + PodcastEpisode episode = podcastService.getEpisode(id, false); + List allEpisodes = podcastService.getEpisodes(episode.getChannelId()); + List files = new ArrayList(); + + String username = securityService.getCurrentUsername(request); + boolean queueFollowingSongs = settingsService.getUserSettings(username).isQueueFollowingSongs(); + + for (PodcastEpisode ep : allEpisodes) { + if (ep.getStatus() == PodcastStatus.COMPLETED) { + MediaFile mediaFile = mediaFileService.getMediaFile(ep.getMediaFileId()); + if (mediaFile != null && mediaFile.isPresent() && + (ep.getId().equals(episode.getId()) || queueFollowingSongs && !files.isEmpty())) { + files.add(mediaFile); + } + } + } + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files).setStartPlayerAt(0); + } + + public PlayQueueInfo playNewestPodcastEpisode(Integer index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + List episodes = podcastService.getNewestEpisodes(10); + List files = Lists.transform(episodes, new Function() { + @Override + public MediaFile apply(PodcastEpisode episode) { + return mediaFileService.getMediaFile(episode.getMediaFileId()); + } + }); + + String username = securityService.getCurrentUsername(request); + boolean queueFollowingSongs = settingsService.getUserSettings(username).isQueueFollowingSongs(); + + if (!files.isEmpty() && index != null) { + if (queueFollowingSongs) { + files = files.subList(index, files.size()); + } else { + files = Arrays.asList(files.get(index)); + } + } + + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files).setStartPlayerAt(0); + } + + public PlayQueueInfo playStarred() throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + + String username = securityService.getCurrentUsername(request); + List musicFolders = settingsService.getMusicFoldersForUser(username); + List files = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders); + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, files).setStartPlayerAt(0); + } + + public PlayQueueInfo playShuffle(String albumListType, int offset, int count, String genre, String decade) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + String username = securityService.getCurrentUsername(request); + UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + + MusicFolder selectedMusicFolder = settingsService.getSelectedMusicFolder(username); + List musicFolders = settingsService.getMusicFoldersForUser(username, + selectedMusicFolder == null ? null : selectedMusicFolder.getId()); + List albums; + if ("highest".equals(albumListType)) { + albums = ratingService.getHighestRatedAlbums(offset, count, musicFolders); + } else if ("frequent".equals(albumListType)) { + albums = mediaFileService.getMostFrequentlyPlayedAlbums(offset, count, musicFolders); + } else if ("recent".equals(albumListType)) { + albums = mediaFileService.getMostRecentlyPlayedAlbums(offset, count, musicFolders); + } else if ("newest".equals(albumListType)) { + albums = mediaFileService.getNewestAlbums(offset, count, musicFolders); + } else if ("starred".equals(albumListType)) { + albums = mediaFileService.getStarredAlbums(offset, count, username, musicFolders); + } else if ("random".equals(albumListType)) { + albums = searchService.getRandomAlbums(count, musicFolders); + } else if ("alphabetical".equals(albumListType)) { + albums = mediaFileService.getAlphabeticalAlbums(offset, count, true, musicFolders); + } else if ("decade".equals(albumListType)) { + int fromYear = Integer.parseInt(decade); + int toYear = fromYear + 9; + albums = mediaFileService.getAlbumsByYear(offset, count, fromYear, toYear, musicFolders); + } else if ("genre".equals(albumListType)) { + albums = mediaFileService.getAlbumsByGenre(offset, count, genre, musicFolders); + } else { + albums = Collections.emptyList(); + } + + List songs = new ArrayList(); + for (MediaFile album : albums) { + songs.addAll(mediaFileService.getChildrenOf(album, true, false, false)); + } + Collections.shuffle(songs); + songs = songs.subList(0, Math.min(40, songs.size())); + + Player player = getCurrentPlayer(request, response); + return doPlay(request, player, songs).setStartPlayerAt(0); + } + + private PlayQueueInfo doPlay(HttpServletRequest request, Player player, List files) throws Exception { + if (player.isWeb()) { + mediaFileService.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 randomFiles = mediaFileService.getRandomSongsForParent(file, count); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().addFiles(false, randomFiles); + player.getPlayQueue().setRandomSearchCriteria(null); + return convert(request, player, true).setStartPlayerAt(0); + } + + public PlayQueueInfo playSimilar(int id, int count) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + MediaFile artist = mediaFileService.getMediaFile(id); + String username = securityService.getCurrentUsername(request); + List musicFolders = settingsService.getMusicFoldersForUser(username); + List similarSongs = lastFmService.getSimilarSongs(artist, count, musicFolders); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().addFiles(false, similarSongs); + return convert(request, player, true).setStartPlayerAt(0); + } + + public PlayQueueInfo add(int id) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doAdd(request, response, new int[]{id}, null); + } + + public PlayQueueInfo addAt(int id, int index) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + return doAdd(request, response, new int[]{id}, index); + } + + public PlayQueueInfo doAdd(HttpServletRequest request, HttpServletResponse response, int[] ids, Integer index) throws Exception { + Player player = getCurrentPlayer(request, response); + List files = new ArrayList(ids.length); + for (int id : ids) { + MediaFile ancestor = mediaFileService.getMediaFile(id); + files.addAll(mediaFileService.getDescendantsOf(ancestor, true)); + } + if (player.isWeb()) { + mediaFileService.removeVideoFiles(files); + } + if (index != null) { + player.getPlayQueue().addFilesAt(files, index); + } else { + 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, null); + + 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 rearrange(int[] indexes) throws Exception { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = getCurrentPlayer(request, response); + player.getPlayQueue().rearrange(indexes); + 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); + } + + private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean serverSidePlaylist) throws Exception { + return convert(request, player, serverSidePlaylist, 0); + } + + private PlayQueueInfo convert(HttpServletRequest request, Player player, boolean serverSidePlaylist, int offset) throws Exception { + String url = request.getRequestURL().toString(); + + if (serverSidePlaylist && player.isJukebox()) { + jukeboxService.updateJukebox(player, offset); + } + boolean isCurrentPlayer = player.getIpAddress() != null && player.getIpAddress().equals(request.getRemoteAddr()); + + boolean m3uSupported = player.isExternal() || player.isExternalWithPlaylist(); + serverSidePlaylist = player.isAutoControlEnabled() && m3uSupported && isCurrentPlayer && serverSidePlaylist; + Locale locale = RequestContextUtils.getLocale(request); + + List entries = new ArrayList(); + 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()); + String coverArtUrl = url.replaceFirst("/dwr/.*", "/coverArt.view?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 remoteStreamUrl = settingsService.rewriteRemoteUrl(streamUrl); + String remoteCoverArtUrl = settingsService.rewriteRemoteUrl(coverArtUrl); + + 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, remoteStreamUrl, + coverArtUrl, remoteCoverArtUrl)); + } + boolean isStopEnabled = playQueue.getStatus() == PlayQueue.Status.PLAYING && !player.isExternalWithPlaylist(); + float gain = jukeboxService.getGain(); + return new PlayQueueInfo(entries, isStopEnabled, playQueue.isRepeatEnabled(), serverSidePlaylist, 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 setLastFmService(LastFmService lastFmService) { + this.lastFmService = lastFmService; + } + + 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 setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public void setPlayQueueDao(PlayQueueDao playQueueDao) { + this.playQueueDao = playQueueDao; + } + + 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..dec2f57e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistInfo.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 . + + 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 entries; + + public PlaylistInfo(Playlist playlist, List entries) { + this.playlist = playlist; + this.entries = entries; + } + + public Playlist getPlaylist() { + return playlist; + } + + public List 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; + private final boolean present; + + public Entry(int id, String title, String artist, String album, String durationAsString, boolean starred, boolean present) { + this.id = id; + this.title = title; + this.artist = artist; + this.album = album; + this.durationAsString = durationAsString; + this.starred = starred; + this.present = present; + } + + 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; + } + + public boolean isPresent() { + return present; + } + } +} 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..16005240 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/PlaylistService.java @@ -0,0 +1,266 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.i18n.SubsonicLocaleResolver; +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.directwebremoting.WebContextFactory; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.ResourceBundle; + +/** + * 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; + private PlayerService playerService; + private SubsonicLocaleResolver localeResolver; + + public List getReadablePlaylists() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + return playlistService.getReadablePlaylistsForUser(username); + } + + public List 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 files = playlistService.getFilesInPlaylist(id, true); + + String username = securityService.getCurrentUsername(request); + mediaFileService.populateStarredDate(files, username); + populateAccess(files, username); + return new PlaylistInfo(playlist, createEntries(files)); + } + + private void populateAccess(List files, String username) { + for (MediaFile file : files) { + if (!securityService.isFolderAccessAllowed(file, username)) { + file.setPresent(false); + } + } + } + + public List createEmptyPlaylist() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + Locale locale = localeResolver.resolveLocale(request); + 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.setShared(false); + playlist.setName(dateFormat.format(now)); + + playlistService.createPlaylist(playlist); + return getReadablePlaylists(); + } + + public int createPlaylistForPlayQueue() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + HttpServletResponse response = WebContextFactory.get().getHttpServletResponse(); + Player player = playerService.getPlayer(request, response); + Locale locale = localeResolver.resolveLocale(request); + 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.setShared(false); + playlist.setName(dateFormat.format(now)); + + playlistService.createPlaylist(playlist); + playlistService.setFilesInPlaylist(playlist.getId(), player.getPlayQueue().getFiles()); + + return playlist.getId(); + } + + public int createPlaylistForStarredSongs() { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + Locale locale = localeResolver.resolveLocale(request); + DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT, locale); + + Date now = new Date(); + Playlist playlist = new Playlist(); + String username = securityService.getCurrentUsername(request); + playlist.setUsername(username); + playlist.setCreated(now); + playlist.setChanged(now); + playlist.setShared(false); + + ResourceBundle bundle = ResourceBundle.getBundle("net.sourceforge.subsonic.i18n.ResourceBundle", locale); + playlist.setName(bundle.getString("top.starred") + " " + dateFormat.format(now)); + + playlistService.createPlaylist(playlist); + List musicFolders = settingsService.getMusicFoldersForUser(username); + List songs = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders); + playlistService.setFilesInPlaylist(playlist.getId(), songs); + + return playlist.getId(); + } + + public void appendToPlaylist(int playlistId, List mediaFileIds) { + List files = playlistService.getFilesInPlaylist(playlistId, true); + for (Integer mediaFileId : mediaFileIds) { + MediaFile file = mediaFileService.getMediaFile(mediaFileId); + if (file != null) { + files.add(file); + } + } + playlistService.setFilesInPlaylist(playlistId, files); + } + + private List createEntries(List files) { + List result = new ArrayList(); + for (MediaFile file : files) { + result.add(new PlaylistInfo.Entry(file.getId(), file.getTitle(), file.getArtist(), file.getAlbumName(), + file.getDurationString(), file.getStarredDate() != null, file.isPresent())); + } + + return result; + } + + public PlaylistInfo toggleStar(int id, int index) { + HttpServletRequest request = WebContextFactory.get().getHttpServletRequest(); + String username = securityService.getCurrentUsername(request); + List files = playlistService.getFilesInPlaylist(id, true); + 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 files = playlistService.getFilesInPlaylist(id, true); + files.remove(index); + playlistService.setFilesInPlaylist(id, files); + return getPlaylist(id); + } + + public PlaylistInfo up(int id, int index) { + List files = playlistService.getFilesInPlaylist(id, true); + if (index > 0) { + MediaFile file = files.remove(index); + files.add(index - 1, file); + playlistService.setFilesInPlaylist(id, files); + } + return getPlaylist(id); + } + + public PlaylistInfo rearrange(int id, int[] indexes) { + List files = playlistService.getFilesInPlaylist(id, true); + MediaFile[] newFiles = new MediaFile[files.size()]; + for (int i = 0; i < indexes.length; i++) { + newFiles[i] = files.get(indexes[i]); + } + playlistService.setFilesInPlaylist(id, Arrays.asList(newFiles)); + return getPlaylist(id); + } + + public PlaylistInfo down(int id, int index) { + List files = playlistService.getFilesInPlaylist(id, true); + 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 shared) { + Playlist playlist = playlistService.getPlaylist(id); + playlist.setName(name); + playlist.setComment(comment); + playlist.setShared(shared); + 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; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setLocaleResolver(SubsonicLocaleResolver localeResolver) { + this.localeResolver = localeResolver; + } +} \ 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 . + + 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/SimilarArtist.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/SimilarArtist.java new file mode 100644 index 00000000..09d6cd1f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/SimilarArtist.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 . + * + * Copyright 2014 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +/** + * Contains info about a similar artist. + * + * @author Sindre Mehus + */ +public class SimilarArtist { + + private final int mediaFileId; + private final String artistName; + + public SimilarArtist(int mediaFileId, String artistName) { + this.mediaFileId = mediaFileId; + this.artistName = artistName; + } + + public int getMediaFileId() { + return mediaFileId; + } + + public String getArtistName() { + return artistName; + } +} \ No newline at end of file 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..5d014e06 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/StarService.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 . + + 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. + *

+ * 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..6814e89e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TagService.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.ObjectUtils; +import org.apache.commons.lang.StringUtils; + +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; + +/** + * 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."; + } + + if (StringUtils.equals(artist, file.getArtist()) && + StringUtils.equals(album, file.getAlbumName()) && + StringUtils.equals(title, file.getTitle()) && + ObjectUtils.equals(yearNumber, file.getYear()) && + StringUtils.equals(genre, file.getGenre()) && + ObjectUtils.equals(trackNumber, file.getTrackNumber())) { + return "SKIPPED"; + } + + MetaData newMetaData = parser.getMetaData(file.getFile()); + + // Note: album artist is intentionally set, as it is not user-changeable. + 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); + mediaFileService.refreshMediaFile(mediaFileService.getParentOf(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/TopSong.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TopSong.java new file mode 100644 index 00000000..8e28fcfe --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/ajax/TopSong.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.ajax; + +/** + * See {@link ArtistInfo}. + * + * @author Sindre Mehus + */ +public class TopSong { + + 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 TopSong(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/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 . + + 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 . + + 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 . + + 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..402bd3bb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/AdvancedSettingsCommand.java @@ -0,0 +1,129 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.command; + +import net.sourceforge.subsonic.controller.AdvancedSettingsController; + +/** + * Command used in {@link AdvancedSettingsController}. + * + * @author Sindre Mehus + */ +public class AdvancedSettingsCommand { + + private String downloadLimit; + private String uploadLimit; + private boolean ldapEnabled; + private String ldapUrl; + private String ldapSearchFilter; + private String ldapManagerDn; + private String ldapManagerPassword; + private boolean ldapAutoShadowing; + private String brand; + private boolean isReloadNeeded; + private boolean toast; + + public String getDownloadLimit() { + return downloadLimit; + } + + public void setDownloadLimit(String downloadLimit) { + this.downloadLimit = downloadLimit; + } + + public String getUploadLimit() { + return uploadLimit; + } + + 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; + } + + public boolean isToast() { + return toast; + } + + public void setToast(boolean toast) { + this.toast = toast; + } +} 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 . + + 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..ade1c501 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/GeneralSettingsCommand.java @@ -0,0 +1,202 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 playlistFolder; + 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; + private boolean toast; + + public String getPlaylistFolder() { + return playlistFolder; + } + + public void setPlaylistFolder(String playlistFolder) { + this.playlistFolder = playlistFolder; + } + + 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; + } + + public boolean isToast() { + return toast; + } + + public void setToast(boolean toast) { + this.toast = toast; + } +} 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 . + + 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 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 getMusicFolders() { + return musicFolders; + } + + public void setMusicFolders(List 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..3b9b3aa7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/NetworkSettingsCommand.java @@ -0,0 +1,100 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.command; + +import net.sourceforge.subsonic.domain.LicenseInfo; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class NetworkSettingsCommand { + + private boolean portForwardingEnabled; + private boolean urlRedirectionEnabled; + private String urlRedirectFrom; + private String urlRedirectCustomUrl; + private String urlRedirectType; + private int port; + private boolean toast; + private LicenseInfo licenseInfo; + + 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 String getUrlRedirectCustomUrl() { + return urlRedirectCustomUrl; + } + + public void setUrlRedirectCustomUrl(String urlRedirectCustomUrl) { + this.urlRedirectCustomUrl = urlRedirectCustomUrl; + } + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public boolean isToast() { + return toast; + } + + public void setToast(boolean toast) { + this.toast = toast; + } + + public void setLicenseInfo(LicenseInfo licenseInfo) { + this.licenseInfo = licenseInfo; + } + + public LicenseInfo getLicenseInfo() { + return licenseInfo; + } + + public String getUrlRedirectType() { + return urlRedirectType; + } + + public void setUrlRedirectType(String urlRedirectType) { + this.urlRedirectType = urlRedirectType; + } +} 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..21986023 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PasswordSettingsCommand.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 . + + 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; + private boolean toast; + + 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; + } + + public boolean isToast() { + return toast; + } + + public void setToast(boolean toast) { + this.toast = toast; + } +} \ 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..28098f9a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PersonalSettingsCommand.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.command; + +import java.util.List; + +import net.sourceforge.subsonic.controller.PersonalSettingsController; +import net.sourceforge.subsonic.domain.AlbumListType; +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; + +/** + * 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 String albumListId; + private AlbumListType[] albumLists; + private int avatarId; + private List avatars; + private Avatar customAvatar; + private UserSettings.Visibility mainVisibility; + private UserSettings.Visibility playlistVisibility; + private boolean partyModeEnabled; + private boolean showNowPlayingEnabled; + private boolean showChatEnabled; + private boolean showArtistInfoEnabled; + private boolean nowPlayingAllowed; + private boolean autoHidePlayQueue; + private boolean finalVersionNotificationEnabled; + private boolean betaVersionNotificationEnabled; + private boolean songNotificationEnabled; + private boolean queueFollowingSongs; + 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 String getAlbumListId() { + return albumListId; + } + + public void setAlbumListId(String albumListId) { + this.albumListId = albumListId; + } + + public AlbumListType[] getAlbumLists() { + return albumLists; + } + + public void setAlbumLists(AlbumListType[] albumLists) { + this.albumLists = albumLists; + } + + public int getAvatarId() { + return avatarId; + } + + public void setAvatarId(int avatarId) { + this.avatarId = avatarId; + } + + public List getAvatars() { + return avatars; + } + + public void setAvatars(List 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 isShowArtistInfoEnabled() { + return showArtistInfoEnabled; + } + + public void setShowArtistInfoEnabled(boolean showArtistInfoEnabled) { + this.showArtistInfoEnabled = showArtistInfoEnabled; + } + + 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 void setSongNotificationEnabled(boolean songNotificationEnabled) { + this.songNotificationEnabled = songNotificationEnabled; + } + + public boolean isSongNotificationEnabled() { + return songNotificationEnabled; + } + + public boolean isAutoHidePlayQueue() { + return autoHidePlayQueue; + } + + public void setAutoHidePlayQueue(boolean autoHidePlayQueue) { + this.autoHidePlayQueue = autoHidePlayQueue; + } + + 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; + } + + public boolean isQueueFollowingSongs() { + return queueFollowingSongs; + } + + public void setQueueFollowingSongs(boolean queueFollowingSongs) { + this.queueFollowingSongs = queueFollowingSongs; + } +} 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..9ee354ec --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PlayerSettingsCommand.java @@ -0,0 +1,227 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.command; + +import java.util.Date; +import java.util.List; + +import net.sourceforge.subsonic.controller.PlayerSettingsController; +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 technologyName; + private String transcodeSchemeName; + private boolean transcodingSupported; + private String transcodeDirectory; + private List allTranscodings; + private int[] activeTranscodingIds; + private EnumHolder[] technologyHolders; + private EnumHolder[] transcodeSchemeHolders; + 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 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 getAllTranscodings() { + return allTranscodings; + } + + public void setAllTranscodings(List 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 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..3a07cd14 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PodcastSettingsCommand.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 . + + 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; + private boolean toast; + + 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; + } + + public boolean isToast() { + return toast; + } + + public void setToast(boolean toast) { + this.toast = toast; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PremiumSettingsCommand.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PremiumSettingsCommand.java new file mode 100644 index 00000000..92b10c67 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/PremiumSettingsCommand.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.command; + +import org.apache.commons.lang.StringUtils; + +import net.sourceforge.subsonic.controller.PremiumSettingsController; +import net.sourceforge.subsonic.domain.LicenseInfo; +import net.sourceforge.subsonic.domain.User; + +/** + * Command used in {@link PremiumSettingsController}. + * + * @author Sindre Mehus + */ +public class PremiumSettingsCommand { + + private String path; + private String brand; + private LicenseInfo licenseInfo; + private String licenseCode; + private boolean forceChange; + private boolean submissionError; + private User user; + private boolean toast; + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getBrand() { + return brand; + } + + public void setBrand(String brand) { + this.brand = brand; + } + + public LicenseInfo getLicenseInfo() { + return licenseInfo; + } + + public String getLicenseCode() { + return licenseCode; + } + + public void setLicenseCode(String licenseCode) { + this.licenseCode = StringUtils.trimToNull(licenseCode); + } + + public void setLicenseInfo(LicenseInfo licenseInfo) { + this.licenseInfo = licenseInfo; + } + + public boolean isForceChange() { + return forceChange; + } + + public void setForceChange(boolean forceChange) { + this.forceChange = forceChange; + } + + public boolean isSubmissionError() { + return submissionError; + } + + public void setSubmissionError(boolean submissionError) { + this.submissionError = submissionError; + } + + public void setUser(User user) { + this.user = user; + } + + public User getUser() { + return user; + } + + public void setToast(boolean toast) { + this.toast = toast; + } + + public boolean isToast() { + return toast; + } +} 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 . + + 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 artists; + private List albums; + private List 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 getArtists() { + return artists; + } + + public void setArtists(List artists) { + this.artists = artists; + } + + public List getAlbums() { + return albums; + } + + public void setAlbums(List albums) { + this.albums = albums; + } + + public List getSongs() { + return songs; + } + + public void setSongs(List 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..2acff8be --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/command/UserSettingsCommand.java @@ -0,0 +1,316 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.command; + +import net.sourceforge.subsonic.controller.UserSettingsController; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.TranscodeScheme; +import net.sourceforge.subsonic.domain.User; + +import java.util.List; + +/** + * 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 users; + private boolean isAdmin; + private boolean isPasswordChange; + private boolean isNewUser; + private boolean isDeleteUser; + private String password; + private String confirmPassword; + private String email; + private boolean isLdapAuthenticated; + private boolean isLdapEnabled; + private List allMusicFolders; + private int[] allowedMusicFolderIds; + + private String transcodeSchemeName; + private EnumHolder[] transcodeSchemeHolders; + private boolean transcodingSupported; + private String transcodeDirectory; + private boolean toast; + private boolean reload; + + 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 getUsers() { + return users; + } + + public void setUsers(List 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 isNewUser() { + return isNewUser; + } + + public void setNewUser(boolean isNewUser) { + this.isNewUser = isNewUser; + } + + public boolean isDeleteUser() { + return isDeleteUser; + } + + public void setDeleteUser(boolean deleteUser) { + this.isDeleteUser = deleteUser; + } + + 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 List getAllMusicFolders() { + return allMusicFolders; + } + + public void setAllMusicFolders(List allMusicFolders) { + this.allMusicFolders = allMusicFolders; + } + + public int[] getAllowedMusicFolderIds() { + return allowedMusicFolderIds; + } + + public void setAllowedMusicFolderIds(int[] allowedMusicFolderIds) { + this.allowedMusicFolderIds = allowedMusicFolderIds; + } + + 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(); + } + + public void setToast(boolean toast) { + this.toast = toast; + } + + public boolean isToast() { + return toast; + } + + public boolean isReload() { + return reload; + } + + public void setReload(boolean reload) { + this.reload = reload; + } +} \ 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 . + + 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..0228a6fb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AdvancedSettingsController.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 . + + 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.setDownloadLimit(String.valueOf(settingsService.getDownloadBitrateLimit())); + command.setUploadLimit(String.valueOf(settingsService.getUploadBitrateLimit())); + 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.setToast(true); + command.setReloadNeeded(false); + + try { + settingsService.setDownloadBitrateLimit(Long.parseLong(command.getDownloadLimit())); + } catch (NumberFormatException x) { /* Intentionally ignored. */ } + try { + settingsService.setUploadBitrateLimit(Long.parseLong(command.getUploadLimit())); + } 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 . + + 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/AutoCoverDemo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AutoCoverDemo.java new file mode 100644 index 00000000..e110c438 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AutoCoverDemo.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 . + + Copyright 2013 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.io.IOException; + +import javax.swing.JComponent; +import javax.swing.JFrame; +import javax.swing.JPanel; + +import org.apache.commons.lang.RandomStringUtils; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class AutoCoverDemo { + + public static void main(String[] args) throws IOException { + JFrame frame = new JFrame(); + JPanel panel = new JPanel(); + panel.add(new AlbumComponent(110, 110)); + panel.add(new AlbumComponent(150, 150)); + panel.add(new AlbumComponent(200, 200)); + panel.add(new AlbumComponent(300, 300)); + panel.add(new AlbumComponent(400, 240)); + panel.add(new AlbumComponent(240, 400)); + + panel.setBackground(Color.LIGHT_GRAY); + frame.add(panel); + frame.setSize(1000, 800); + frame.setVisible(true); + } + + private static class AlbumComponent extends JComponent { + private final int width; + private final int height; + + public AlbumComponent(int width, int height) { + this.width = width; + this.height = height; + setPreferredSize(new Dimension(width, height)); + } + + @Override + protected void paintComponent(Graphics g) { + String key = RandomStringUtils.random(5); + new CoverArtController.AutoCover((Graphics2D) g, key, "Artist with a very long name", "Album", width, height).paintCover(); + } + } +} 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..2d4f9d3d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/AvatarController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +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.Controller; +import org.springframework.web.servlet.mvc.LastModified; + +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * 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); + long result = avatar == null ? -1L : avatar.getCreatedDate().getTime(); + + String username = request.getParameter("username"); + if (username != null) { + UserSettings userSettings = settingsService.getUserSettings(username); + result = Math.max(result, userSettings.getChanged().getTime()); + } + + return result; + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + Avatar avatar = getAvatar(request); + + if (avatar == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + response.setContentType(avatar.getMimeType()); + response.getOutputStream().write(avatar.getData()); + return null; + } + + private Avatar getAvatar(HttpServletRequest request) { + String id = request.getParameter("id"); + boolean forceCustom = ServletRequestUtils.getBooleanParameter(request, "forceCustom", false); + + 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.CUSTOM || forceCustom) { + return settingsService.getCustomAvatar(username); + } + return settingsService.getSystemAvatar(userSettings.getSystemAvatarId()); + } + + 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 . + + 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 map = new HashMap(); + 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 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 . + + 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 map = new HashMap(); + 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..22f386d5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/CoverArtController.java @@ -0,0 +1,731 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +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; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Semaphore; + +import javax.imageio.ImageIO; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +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 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.CoverArtScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.Transcoding; +import net.sourceforge.subsonic.domain.VideoTranscodingSettings; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.PodcastService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * 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-"; + public static final String PLAYLIST_COVERART_PREFIX = "pl-"; + public static final String PODCAST_COVERART_PREFIX = "pod-"; + + private static final Logger LOG = Logger.getLogger(CoverArtController.class); + + private MediaFileService mediaFileService; + private TranscodingService transcodingService; + private SettingsService settingsService; + private PlaylistService playlistService; + private PodcastService podcastService; + private ArtistDao artistDao; + private AlbumDao albumDao; + private Semaphore semaphore; + + public void init() { + semaphore = new Semaphore(settingsService.getCoverArtConcurrency()); + } + + public long getLastModified(HttpServletRequest request) { + CoverArtRequest coverArtRequest = createCoverArtRequest(request); + long result = coverArtRequest.lastModified(); +// LOG.info("getLastModified - " + coverArtRequest + ": " + new Date(result)); + return result; + } + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + CoverArtRequest coverArtRequest = createCoverArtRequest(request); +// LOG.info("handleRequest - " + coverArtRequest); + Integer size = ServletRequestUtils.getIntParameter(request, "size"); + + // Send fallback image if no ID is given. (No need to cache it, since it will be cached in browser.) + if (coverArtRequest == null) { + sendFallback(size, response); + return null; + } + + // Optimize if no scaling is required. + if (size == null && coverArtRequest.getCoverArt() != null) { +// LOG.info("sendUnscaled - " + coverArtRequest); + sendUnscaled(coverArtRequest, response); + return null; + } + + // Send cached image, creating it if necessary. + if (size == null) { + size = CoverArtScheme.LARGE.getSize() * 2; + } + try { + File cachedImage = getCachedImage(coverArtRequest, size); + sendImage(cachedImage, response); + } catch (IOException e) { + sendFallback(size, response); + } + + return null; + } + + private CoverArtRequest createCoverArtRequest(HttpServletRequest request) { + String id = request.getParameter("id"); + if (id == null) { + return null; + } + + if (id.startsWith(ALBUM_COVERART_PREFIX)) { + return createAlbumCoverArtRequest(Integer.valueOf(id.replace(ALBUM_COVERART_PREFIX, ""))); + } + if (id.startsWith(ARTIST_COVERART_PREFIX)) { + return createArtistCoverArtRequest(Integer.valueOf(id.replace(ARTIST_COVERART_PREFIX, ""))); + } + if (id.startsWith(PLAYLIST_COVERART_PREFIX)) { + return createPlaylistCoverArtRequest(Integer.valueOf(id.replace(PLAYLIST_COVERART_PREFIX, ""))); + } + if (id.startsWith(PODCAST_COVERART_PREFIX)) { + return createPodcastCoverArtRequest(Integer.valueOf(id.replace(PODCAST_COVERART_PREFIX, "")), request); + } + return createMediaFileCoverArtRequest(Integer.valueOf(id), request); + } + + private CoverArtRequest createAlbumCoverArtRequest(int id) { + Album album = albumDao.getAlbum(id); + return album == null ? null : new AlbumCoverArtRequest(album); + } + + private CoverArtRequest createArtistCoverArtRequest(int id) { + Artist artist = artistDao.getArtist(id); + return artist == null ? null : new ArtistCoverArtRequest(artist); + } + + private PlaylistCoverArtRequest createPlaylistCoverArtRequest(int id) { + Playlist playlist = playlistService.getPlaylist(id); + return playlist == null ? null : new PlaylistCoverArtRequest(playlist); + } + + private CoverArtRequest createPodcastCoverArtRequest(int id, HttpServletRequest request) { + PodcastChannel channel = podcastService.getChannel(id); + if (channel == null) { + return null; + } + if (channel.getMediaFileId() == null) { + return new PodcastCoverArtRequest(channel); + } + return createMediaFileCoverArtRequest(channel.getMediaFileId(), request); + } + + private CoverArtRequest createMediaFileCoverArtRequest(int id, HttpServletRequest request) { + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile == null) { + return null; + } + if (mediaFile.isVideo()) { + int offset = ServletRequestUtils.getIntParameter(request, "offset", 60); + return new VideoCoverArtRequest(mediaFile, offset); + } + return new MediaFileCoverArtRequest(mediaFile); + } + + private void sendImage(File file, HttpServletResponse response) throws IOException { + response.setContentType(StringUtil.getMimeType(FilenameUtils.getExtension(file.getName()))); + InputStream in = new FileInputStream(file); + try { + IOUtils.copy(in, response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private void sendFallback(Integer size, HttpServletResponse response) throws IOException { + if (response.getContentType() == null) { + response.setContentType(StringUtil.getMimeType("jpeg")); + } + 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(CoverArtRequest coverArtRequest, HttpServletResponse response) throws IOException { + File file = coverArtRequest.getCoverArt(); + JaudiotaggerParser parser = new JaudiotaggerParser(); + if (!parser.isApplicable(file)) { + response.setContentType(StringUtil.getMimeType(FilenameUtils.getExtension(file.getName()))); + } + InputStream in = null; + try { + in = getImageInputStream(file); + IOUtils.copy(in, response.getOutputStream()); + } finally { + IOUtils.closeQuietly(in); + } + } + + private File getCachedImage(CoverArtRequest request, int size) throws IOException { + String hash = DigestUtils.md5Hex(request.getKey()); + String encoding = request.getCoverArt() != null ? "jpeg" : "png"; + File cachedImage = new File(getImageCacheDirectory(size), hash + "." + encoding); + + // Synchronize to avoid concurrent writing to the same file. + synchronized (hash.intern()) { + + // Is cache missing or obsolete? + if (!cachedImage.exists() || request.lastModified() > cachedImage.lastModified()) { +// LOG.info("Cache MISS - " + request + " (" + size + ")"); + OutputStream out = null; + try { + semaphore.acquire(); + BufferedImage image = request.createImage(size); + if (image == null) { + throw new Exception("Unable to decode image."); + } + out = new FileOutputStream(cachedImage); + ImageIO.write(image, encoding, out); + + } catch (Throwable x) { + // Delete corrupt (probably empty) thumbnail cache. + LOG.warn("Failed to create thumbnail for " + request, x); + IOUtils.closeQuietly(out); + cachedImage.delete(); + throw new IOException("Failed to create thumbnail for " + request + ". " + x.getMessage()); + + } finally { + semaphore.release(); + IOUtils.closeQuietly(out); + } + } else { +// LOG.info("Cache HIT - " + request + " (" + size + ")"); + } + 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 InputStream getImageInputStreamForVideo(MediaFile mediaFile, int width, int height, int offset) throws Exception { + VideoTranscodingSettings videoSettings = new VideoTranscodingSettings(width, height, offset, 0, false); + TranscodingService.Parameters parameters = new TranscodingService.Parameters(mediaFile, videoSettings); + String command = settingsService.getVideoImageCommand(); + parameters.setTranscoding(new Transcoding(null, null, null, null, command, null, null, false)); + return transcodingService.getTranscodedInputStream(parameters); + } + + 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 setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setArtistDao(ArtistDao artistDao) { + this.artistDao = artistDao; + } + + public void setAlbumDao(AlbumDao albumDao) { + this.albumDao = albumDao; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + private abstract class CoverArtRequest { + + protected File coverArt; + + private CoverArtRequest() { + } + + private CoverArtRequest(String coverArtPath) { + this.coverArt = coverArtPath == null ? null : new File(coverArtPath); + } + + private File getCoverArt() { + return coverArt; + } + + public abstract String getKey(); + + public abstract long lastModified(); + + public BufferedImage createImage(int size) { + if (coverArt != null) { + InputStream in = null; + try { + in = getImageInputStream(coverArt); + return scale(ImageIO.read(in), size, size); + } catch (Throwable x) { + LOG.warn("Failed to process cover art " + coverArt + ": " + x, x); + } finally { + IOUtils.closeQuietly(in); + } + } + return createAutoCover(size, size); + } + + protected BufferedImage createAutoCover(int width, int height) { + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + AutoCover autoCover = new AutoCover(graphics, getKey(), getArtist(), getAlbum(), width, height); + autoCover.paintCover(); + graphics.dispose(); + return image; + } + + public abstract String getAlbum(); + + public abstract String getArtist(); + } + + private class ArtistCoverArtRequest extends CoverArtRequest { + + private final Artist artist; + + private ArtistCoverArtRequest(Artist artist) { + super(artist.getCoverArtPath()); + this.artist = artist; + } + + @Override + public String getKey() { + return artist.getCoverArtPath() != null ? artist.getCoverArtPath() : (ARTIST_COVERART_PREFIX + artist.getId()); + } + + @Override + public long lastModified() { + return coverArt != null ? coverArt.lastModified() : artist.getLastScanned().getTime(); + } + + @Override + public String getAlbum() { + return null; + } + + @Override + public String getArtist() { + return artist.getName(); + } + + @Override + public String toString() { + return "Artist " + artist.getId() + " - " + artist.getName(); + } + } + + private class AlbumCoverArtRequest extends CoverArtRequest { + + private final Album album; + + private AlbumCoverArtRequest(Album album) { + super(album.getCoverArtPath()); + this.album = album; + } + + @Override + public String getKey() { + return album.getCoverArtPath() != null ? album.getCoverArtPath() : (ALBUM_COVERART_PREFIX + album.getId()); + } + + @Override + public long lastModified() { + return coverArt != null ? coverArt.lastModified() : album.getLastScanned().getTime(); + } + + @Override + public String getAlbum() { + return album.getName(); + } + + @Override + public String getArtist() { + return album.getArtist(); + } + + @Override + public String toString() { + return "Album " + album.getId() + " - " + album.getName(); + } + } + + private class PlaylistCoverArtRequest extends CoverArtRequest { + + private final Playlist playlist; + + private PlaylistCoverArtRequest(Playlist playlist) { + super(null); + this.playlist = playlist; + } + + @Override + public String getKey() { + return PLAYLIST_COVERART_PREFIX + playlist.getId(); + } + + @Override + public long lastModified() { + return playlist.getChanged().getTime(); + } + + @Override + public String getAlbum() { + return null; + } + + @Override + public String getArtist() { + return playlist.getName(); + } + + @Override + public String toString() { + return "Playlist " + playlist.getId() + " - " + playlist.getName(); + } + + @Override + public BufferedImage createImage(int size) { + List albums = getRepresentativeAlbums(); + if (albums.isEmpty()) { + return createAutoCover(size, size); + } + if (albums.size() < 4) { + return new MediaFileCoverArtRequest(albums.get(0)).createImage(size); + } + + BufferedImage image = new BufferedImage(size, size, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = image.createGraphics(); + + int half = size / 2; + graphics.drawImage(new MediaFileCoverArtRequest(albums.get(0)).createImage(half), null, 0, 0); + graphics.drawImage(new MediaFileCoverArtRequest(albums.get(1)).createImage(half), null, half, 0); + graphics.drawImage(new MediaFileCoverArtRequest(albums.get(2)).createImage(half), null, 0, half); + graphics.drawImage(new MediaFileCoverArtRequest(albums.get(3)).createImage(half), null, half, half); + graphics.dispose(); + return image; + } + + private List getRepresentativeAlbums() { + Set albums = new LinkedHashSet(); + for (MediaFile song : playlistService.getFilesInPlaylist(playlist.getId())) { + MediaFile album = mediaFileService.getParentOf(song); + if (album != null && !mediaFileService.isRoot(album)) { + albums.add(album); + } + } + return new ArrayList(albums); + } + } + + private class PodcastCoverArtRequest extends CoverArtRequest { + + private final PodcastChannel channel; + + public PodcastCoverArtRequest(PodcastChannel channel) { + this.channel = channel; + } + + @Override + public String getKey() { + return PODCAST_COVERART_PREFIX + channel.getId(); + } + + @Override + public long lastModified() { + return -1; + } + + @Override + public String getAlbum() { + return null; + } + + @Override + public String getArtist() { + return channel.getTitle() != null ? channel.getTitle() : channel.getUrl(); + } + } + + private class MediaFileCoverArtRequest extends CoverArtRequest { + + private final MediaFile mediaFile; + private final MediaFile dir; + + private MediaFileCoverArtRequest(MediaFile mediaFile) { + this.mediaFile = mediaFile; + dir = mediaFile.isDirectory() ? mediaFile : mediaFileService.getParentOf(mediaFile); + coverArt = mediaFileService.getCoverArt(mediaFile); + } + + @Override + public String getKey() { + return coverArt != null ? coverArt.getPath() : dir.getPath(); + } + + @Override + public long lastModified() { + return coverArt != null ? coverArt.lastModified() : dir.getChanged().getTime(); + } + + @Override + public String getAlbum() { + return dir.getName(); + } + + @Override + public String getArtist() { + return dir.getAlbumArtist() != null ? dir.getAlbumArtist() : dir.getArtist(); + } + + @Override + public String toString() { + return "Media file " + mediaFile.getId() + " - " + mediaFile; + } + } + + private class VideoCoverArtRequest extends CoverArtRequest { + + private final MediaFile mediaFile; + private final int offset; + + private VideoCoverArtRequest(MediaFile mediaFile, int offset) { + this.mediaFile = mediaFile; + this.offset = offset; + } + + @Override + public BufferedImage createImage(int size) { + int height = size; + int width = height * 16 / 9; + InputStream in = null; + try { + in = getImageInputStreamForVideo(mediaFile, width, height, offset); + BufferedImage result = ImageIO.read(in); + if (result == null) { + throw new NullPointerException(); + } + return result; + } catch (Throwable x) { + LOG.warn("Failed to process cover art for " + mediaFile + ": " + x, x); + } finally { + IOUtils.closeQuietly(in); + } + return createAutoCover(width, height); + } + + @Override + public String getKey() { + return mediaFile.getPath() + "/" + offset; + } + + @Override + public long lastModified() { + return mediaFile.getChanged().getTime(); + } + + @Override + public String getAlbum() { + return null; + } + + @Override + public String getArtist() { + return mediaFile.getName(); + } + + @Override + public String toString() { + return "Video file " + mediaFile.getId() + " - " + mediaFile; + } + } + + static class AutoCover { + + private final static int[] COLORS = {0x33B5E5, 0xAA66CC, 0x99CC00, 0xFFBB33, 0xFF4444}; + private final Graphics2D graphics; + private final String artist; + private final String album; + private final int width; + private final int height; + private final Color color; + + public AutoCover(Graphics2D graphics, String key, String artist, String album, int width, int height) { + this.graphics = graphics; + this.artist = artist; + this.album = album; + this.width = width; + this.height = height; + + int hash = key.hashCode(); + int rgb = COLORS[Math.abs(hash) % COLORS.length]; + this.color = new Color(rgb); + } + + public void paintCover() { + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + graphics.setPaint(color); + graphics.fillRect(0, 0, width, height); + + int y = height * 2 / 3; + graphics.setPaint(new GradientPaint(0, y, new Color(82, 82, 82), 0, height, Color.BLACK)); + graphics.fillRect(0, y, width, height / 3); + + graphics.setPaint(Color.WHITE); + float fontSize = 3.0f + height * 0.07f; + Font font = new Font(Font.SANS_SERIF, Font.BOLD, (int) fontSize); + graphics.setFont(font); + + if (album != null) { + graphics.drawString(album, width * 0.05f, height * 0.6f); + } + if (artist != null) { + graphics.drawString(artist, width * 0.05f, height * 0.8f); + } + + int borderWidth = height / 50; + graphics.fillRect(0, 0, borderWidth, height); + graphics.fillRect(width - borderWidth, 0, height - borderWidth, height); + graphics.fillRect(0, 0, width, borderWidth); + graphics.fillRect(0, height - borderWidth, width, height); + } + } +} 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 . + + 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 map = new HashMap(); + + 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/DLNASettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DLNASettingsController.java new file mode 100644 index 00000000..5eaa45d2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DLNASettingsController.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 . + * + * Copyright 2013 (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.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.UPnPService; + +/** + * Controller for the page used to administrate the UPnP/DLNA server settings. + * + * @author Sindre Mehus + */ +public class DLNASettingsController extends ParameterizableViewController { + + private UPnPService upnpService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map map = new HashMap(); + + if (isFormSubmission(request)) { + handleParameters(request); + map.put("toast", true); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("dlnaEnabled", settingsService.isDlnaEnabled()); + map.put("dlnaServerName", settingsService.getDlnaServerName()); + map.put("licenseInfo", settingsService.getLicenseInfo()); + + 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) { + boolean dlnaEnabled = ServletRequestUtils.getBooleanParameter(request, "dlnaEnabled", false); + String dlnaServerName = StringUtils.trimToNull(request.getParameter("dlnaServerName")); + if (dlnaServerName == null) { + dlnaServerName = "Subsonic"; + } + + upnpService.setMediaServerEnabled(false); + settingsService.setDlnaEnabled(dlnaEnabled); + settingsService.setDlnaServerName(dlnaServerName); + settingsService.save(); + upnpService.setMediaServerEnabled(dlnaEnabled); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setUpnpService(UPnPService upnpService) { + this.upnpService = upnpService; + } +} \ 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..f207feb4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/DownloadController.java @@ -0,0 +1,414 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +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.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +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 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.Playlist; +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.HttpRange; +import net.sourceforge.subsonic.util.Util; + +/** + * 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 = getMediaFile(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 { + + User user = securityService.getCurrentUser(request); + TransferStatus status = null; + try { + + status = statusService.createDownloadStatus(playerService.getPlayer(request, response, false, false)); + + MediaFile mediaFile = getMediaFile(request); + + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + String playerId = request.getParameter("player"); + int[] indexes = request.getParameter("i") == null ? null : ServletRequestUtils.getIntParameters(request, "i"); + + if (mediaFile != null) { + response.setIntHeader("ETag", mediaFile.getId()); + response.setHeader("Accept-Ranges", "bytes"); + } + + HttpRange range = HttpRange.valueOf(request.getHeader("Range")); + if (range != null) { + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + LOG.info("Got HTTP range: " + range); + } + + if (mediaFile != null) { + if (!securityService.isFolderAccessAllowed(mediaFile, user.getUsername())) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, + "Access to file " + mediaFile.getId() + " is forbidden for user " + user.getUsername()); + return null; + } + + if (mediaFile.isFile()) { + downloadFile(response, status, mediaFile.getFile(), range); + } else { + List children = mediaFileService.getChildrenOf(mediaFile, true, false, true); + String zipFileName = FilenameUtils.getBaseName(mediaFile.getPath()) + ".zip"; + File coverArtFile = indexes == null ? mediaFile.getCoverArtFile() : null; + downloadFiles(response, status, children, indexes, coverArtFile, range, zipFileName); + } + + } else if (playlistId != null) { + List songs = playlistService.getFilesInPlaylist(playlistId); + Playlist playlist = playlistService.getPlaylist(playlistId); + downloadFiles(response, status, songs, null, null, range, playlist.getName() + ".zip"); + + } else if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + PlayQueue playQueue = player.getPlayQueue(); + playQueue.setName("Playlist"); + downloadFiles(response, status, playQueue.getFiles(), indexes, null, range, "download.zip"); + } + + } finally { + if (status != null) { + statusService.removeDownloadStatus(status); + securityService.updateUserByteCounts(user, 0L, status.getBytesTransfered(), 0L); + } + } + + return null; + } + + private MediaFile getMediaFile(HttpServletRequest request) throws ServletRequestBindingException { + Integer id = ServletRequestUtils.getIntParameter(request, "id"); + return id == null ? null : mediaFileService.getMediaFile(id); + } + + /** + * 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 null. + * @throws IOException If an I/O error occurs. + */ + private void downloadFile(HttpServletResponse response, TransferStatus status, File file, HttpRange 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*=UTF-8''" + encodeAsRFC5987(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()); + } + + private String encodeAsRFC5987(String string) throws UnsupportedEncodingException { + byte[] stringAsByteArray = string.getBytes("UTF-8"); + char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + byte[] attrChar = {'!', '#', '$', '&', '+', '-', '.', '^', '_', '`', '|', '~', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'}; + StringBuilder sb = new StringBuilder(); + for (byte b : stringAsByteArray) { + if (Arrays.binarySearch(attrChar, b) >= 0) { + sb.append((char) b); + } else { + sb.append('%'); + sb.append(digits[0x0f & (b >>> 4)]); + sb.append(digits[b & 0x0f]); + } + } + return sb.toString(); + } + + /** + * 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 null. + * @param coverArtFile The cover art file to include, may be {@code null}. + *@param range The byte range, may be null. + * @param zipFileName The name of the resulting zip file. @throws IOException If an I/O error occurs. + */ + private void downloadFiles(HttpServletResponse response, TransferStatus status, List files, int[] indexes, File coverArtFile, HttpRange range, String zipFileName) throws IOException { + if (indexes != null && indexes.length == 1) { + downloadFile(response, status, files.get(indexes[0]).getFile(), range); + return; + } + + LOG.info("Starting to download '" + zipFileName + "' to " + status.getPlayer()); + response.setContentType("application/x-download"); + response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + encodeAsRFC5987(zipFileName)); + + ZipOutputStream out = new ZipOutputStream(RangeOutputStream.wrap(response.getOutputStream(), range)); + out.setMethod(ZipOutputStream.STORED); // No compression. + + List filesToDownload = new ArrayList(); + 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); + } + if (coverArtFile != null && coverArtFile.exists()) { + zip(out, coverArtFile.getParentFile(), coverArtFile, 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 null. + * @throws IOException If an I/O error occurs. + */ + private void copyFileToStream(File file, OutputStream out, TransferStatus status, HttpRange 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.contains(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 null. + * @throws IOException If an I/O error occurs. + */ + private void zip(ZipOutputStream out, File root, File file, TransferStatus status, HttpRange 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..9b7befd9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/EditTagsController.java @@ -0,0 +1,197 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.ArrayList; +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.io.FilenameUtils; +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; +import net.sourceforge.subsonic.service.metadata.JaudiotaggerParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; + +/** + * 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 files = mediaFileService.getChildrenOf(dir, true, false, true, false); + + Map map = new HashMap(); + 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 songs = new ArrayList(); + 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()); + + Song song = new Song(); + song.setId(file.getId()); + song.setFileName(FilenameUtils.getBaseName(file.getPath())); + song.setTrack(file.getTrackNumber()); + song.setSuggestedTrack(index + 1); + song.setTitle(file.getTitle()); + song.setSuggestedTitle(parser.guessTitle(file.getFile())); + song.setArtist(file.getArtist()); + song.setAlbum(file.getAlbumName()); + song.setYear(file.getYear()); + song.setGenre(file.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..31a20209 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ExternalPlayerController.java @@ -0,0 +1,125 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +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 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.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Share; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.ShareService; + +/** + * Controller for the page used to play shared music (Twitter, Facebook etc). + * + * @author Sindre Mehus + */ +public class ExternalPlayerController extends ParameterizableViewController { + + private SettingsService settingsService; + private PlayerService playerService; + private ShareService shareService; + private MediaFileService mediaFileService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map map = new HashMap(); + + String pathInfo = request.getPathInfo(); + + if (pathInfo == null || !pathInfo.startsWith("/")) { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + + Share share = shareService.getShareByName(pathInfo.substring(1)); + + if (share != null && share.getExpires() != null && share.getExpires().before(new Date())) { + share = null; + } + + if (share != null) { + share.setLastVisited(new Date()); + share.setVisitCount(share.getVisitCount() + 1); + shareService.updateShare(share); + } + + Player player = playerService.getGuestPlayer(request); + + map.put("share", share); + map.put("songs", getSongs(share, player.getUsername())); + map.put("redirectUrl", settingsService.getUrlRedirectUrl()); + map.put("player", player.getId()); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List getSongs(Share share, String username) throws IOException { + List result = new ArrayList(); + + List musicFolders = settingsService.getMusicFoldersForUser(username); + + if (share != null) { + for (MediaFile file : shareService.getSharedFiles(share.getId(), musicFolders)) { + if (file.getFile().exists()) { + if (file.isDirectory()) { + result.addAll(mediaFileService.getChildrenOf(file, true, false, true)); + } else { + result.add(file); + } + } + } + } + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setShareService(ShareService shareService) { + this.shareService = shareService; + } + + 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..e3e43432 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/GeneralSettingsController.java @@ -0,0 +1,119 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.GeneralSettingsCommand; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * 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.setPlaylistFolder(settingsService.getPlaylistFolder()); + 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.setToast(true); + 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.setPlaylistFolder(command.getPlaylistFolder()); + 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/HLSController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HLSController.java new file mode 100644 index 00000000..13b1b19b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HLSController.java @@ -0,0 +1,201 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.awt.Dimension; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +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 net.sourceforge.subsonic.domain.MediaFile; +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.util.Pair; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller which produces the HLS (Http Live Streaming) playlist. + * + * @author Sindre Mehus + */ +public class HLSController implements Controller { + + private static final int SEGMENT_DURATION = 10; + private static final Pattern BITRATE_PATTERN = Pattern.compile("(\\d+)(@(\\d+)x(\\d+))?"); + + private PlayerService playerService; + private MediaFileService mediaFileService; + private SecurityService securityService; + + public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { + + response.setHeader("Access-Control-Allow-Origin", "*"); + + int id = ServletRequestUtils.getIntParameter(request, "id"); + MediaFile mediaFile = mediaFileService.getMediaFile(id); + Player player = playerService.getPlayer(request, response); + String username = player.getUsername(); + + if (mediaFile == null) { + response.sendError(HttpServletResponse.SC_NOT_FOUND, "Media file not found: " + id); + return null; + } + + if (username != null && !securityService.isFolderAccessAllowed(mediaFile, username)) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, + "Access to file " + mediaFile.getId() + " is forbidden for user " + username); + return null; + } + + Integer duration = mediaFile.getDurationSeconds(); + if (duration == null || duration == 0) { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unknown duration for media file: " + id); + return null; + } + + response.setContentType("application/vnd.apple.mpegurl"); + response.setCharacterEncoding(StringUtil.ENCODING_UTF8); + List> bitRates = parseBitRates(request); + PrintWriter writer = response.getWriter(); + if (bitRates.size() > 1) { + generateVariantPlaylist(request, id, player, bitRates, writer); + } else { + generateNormalPlaylist(request, id, player, bitRates.size() == 1 ? bitRates.get(0) : null, duration, writer); + } + + return null; + } + + private List> parseBitRates(HttpServletRequest request) throws IllegalArgumentException { + List> result = new ArrayList>(); + String[] bitRates = request.getParameterValues("bitRate"); + if (bitRates != null) { + for (String bitRate : bitRates) { + result.add(parseBitRate(bitRate)); + } + } + return result; + } + + /** + * Parses a string containing the bitrate and an optional width/height, e.g., 1200@640x480 + */ + protected Pair parseBitRate(String bitRate) throws IllegalArgumentException { + + Matcher matcher = BITRATE_PATTERN.matcher(bitRate); + if (!matcher.matches()) { + throw new IllegalArgumentException("Invalid bitrate specification: " + bitRate); + } + int kbps = Integer.parseInt(matcher.group(1)); + if (matcher.group(3) == null) { + return new Pair(kbps, null); + } else { + int width = Integer.parseInt(matcher.group(3)); + int height = Integer.parseInt(matcher.group(4)); + return new Pair(kbps, new Dimension(width, height)); + } + } + + private void generateVariantPlaylist(HttpServletRequest request, int id, Player player, List> bitRates, PrintWriter writer) { + writer.println("#EXTM3U"); + writer.println("#EXT-X-VERSION:1"); +// writer.println("#EXT-X-TARGETDURATION:" + SEGMENT_DURATION); + + String contextPath = getContextPath(request); + for (Pair bitRate : bitRates) { + Integer kbps = bitRate.getFirst(); + writer.println("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + kbps * 1000L); + writer.print(contextPath + "hls/hls.m3u8?id=" + id + "&player=" + player.getId() + "&bitRate=" + kbps); + Dimension dimension = bitRate.getSecond(); + if (dimension != null) { + writer.print("@" + dimension.width + "x" + dimension.height); + } + writer.println(); + } +// writer.println("#EXT-X-ENDLIST"); + } + + private void generateNormalPlaylist(HttpServletRequest request, int id, Player player, Pair bitRate, int totalDuration, PrintWriter writer) { + writer.println("#EXTM3U"); + writer.println("#EXT-X-VERSION:1"); + writer.println("#EXT-X-TARGETDURATION:" + SEGMENT_DURATION); + + for (int i = 0; i < totalDuration / SEGMENT_DURATION; i++) { + int offset = i * SEGMENT_DURATION; + writer.println("#EXTINF:" + SEGMENT_DURATION + ","); + writer.println(createStreamUrl(request, player, id, offset, SEGMENT_DURATION, bitRate)); + } + + int remainder = totalDuration % SEGMENT_DURATION; + if (remainder > 0) { + writer.println("#EXTINF:" + remainder + ","); + int offset = totalDuration - remainder; + writer.println(createStreamUrl(request, player, id, offset, remainder, bitRate)); + } + writer.println("#EXT-X-ENDLIST"); + } + + private String createStreamUrl(HttpServletRequest request, Player player, int id, int offset, int duration, Pair bitRate) { + StringBuilder builder = new StringBuilder(); + builder.append(getContextPath(request)).append("stream/stream.ts?id=").append(id).append("&hls=true&timeOffset=").append(offset) + .append("&player=").append(player.getId()).append("&duration=").append(duration); + if (bitRate != null) { + builder.append("&maxBitRate=").append(bitRate.getFirst()); + Dimension dimension = bitRate.getSecond(); + if (dimension != null) { + builder.append("&size=").append(dimension.width).append("x").append(dimension.height); + } + } + return builder.toString(); + } + + private String getContextPath(HttpServletRequest request) { + String contextPath = request.getContextPath(); + if (StringUtils.isEmpty(contextPath)) { + contextPath = "/"; + } else { + contextPath += "/"; + } + return contextPath; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} 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..276c0f27 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HelpController.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 . + + 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.Logger; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.VersionService; + +/** + * Controller for the help page. + * + * @author Sindre Mehus + */ +public class HelpController extends ParameterizableViewController { + + private VersionService versionService; + private SettingsService settingsService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map map = new HashMap(); + + 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("licenseInfo", settingsService.getLicenseInfo()); + map.put("user", securityService.getCurrentUser(request)); + 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; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} 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..4ef59d2c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/HomeController.java @@ -0,0 +1,372 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +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 org.springframework.web.servlet.view.RedirectView; + +import net.sourceforge.subsonic.domain.AlbumListType; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.Genre; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +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 static org.springframework.web.bind.ServletRequestUtils.getIntParameter; +import static org.springframework.web.bind.ServletRequestUtils.getStringParameter; + +/** + * Controller for the home page. + * + * @author Sindre Mehus + */ +public class HomeController extends ParameterizableViewController { + + private static final int LIST_SIZE = 40; + + 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 listOffset = getIntParameter(request, "listOffset", 0); + AlbumListType listType = AlbumListType.fromId(getStringParameter(request, "listType")); + if (listType == null) { + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + listType = userSettings.getDefaultAlbumList(); + } + + MusicFolder selectedMusicFolder = settingsService.getSelectedMusicFolder(user.getUsername()); + List musicFolders = settingsService.getMusicFoldersForUser(user.getUsername(), + selectedMusicFolder == null ? null : selectedMusicFolder.getId()); + + Map map = new HashMap(); + List albums = Collections.emptyList(); + switch (listType) { + case HIGHEST: + albums = getHighestRated(listOffset, LIST_SIZE, musicFolders); + break; + case FREQUENT: + albums = getMostFrequent(listOffset, LIST_SIZE, musicFolders); + break; + case RECENT: + albums = getMostRecent(listOffset, LIST_SIZE, musicFolders); + break; + case NEWEST: + albums = getNewest(listOffset, LIST_SIZE, musicFolders); + break; + case STARRED: + albums = getStarred(listOffset, LIST_SIZE, user.getUsername(), musicFolders); + break; + case RANDOM: + albums = getRandom(LIST_SIZE, musicFolders); + break; + case ALPHABETICAL: + albums = getAlphabetical(listOffset, LIST_SIZE, true, musicFolders); + break; + case DECADE: + List decades = createDecades(); + map.put("decades", decades); + int decade = getIntParameter(request, "decade", decades.get(0)); + map.put("decade", decade); + albums = getByYear(listOffset, LIST_SIZE, decade, decade + 9, musicFolders); + break; + case GENRE: + List genres = mediaFileService.getGenres(true); + map.put("genres", genres); + if (!genres.isEmpty()) { + String genre = getStringParameter(request, "genre", genres.get(0).getName()); + map.put("genre", genre); + albums = getByGenre(listOffset, LIST_SIZE, genre, musicFolders); + } + break; + default: + break; + } + + 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("musicFoldersExist", !settingsService.getAllMusicFolders().isEmpty()); + map.put("listType", listType.getId()); + map.put("listSize", LIST_SIZE); + map.put("coverArtSize", CoverArtScheme.MEDIUM.getSize()); + map.put("listOffset", listOffset); + map.put("musicFolder", selectedMusicFolder); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List getHighestRated(int offset, int count, List musicFolders) { + List result = new ArrayList(); + for (MediaFile mediaFile : ratingService.getHighestRatedAlbums(offset, count, musicFolders)) { + Album album = createAlbum(mediaFile); + album.setRating((int) Math.round(ratingService.getAverageRating(mediaFile) * 10.0D)); + result.add(album); + } + return result; + } + + private List getMostFrequent(int offset, int count, List musicFolders) { + List result = new ArrayList(); + for (MediaFile mediaFile : mediaFileService.getMostFrequentlyPlayedAlbums(offset, count, musicFolders)) { + Album album = createAlbum(mediaFile); + album.setPlayCount(mediaFile.getPlayCount()); + result.add(album); + } + return result; + } + + private List getMostRecent(int offset, int count, List musicFolders) { + List result = new ArrayList(); + for (MediaFile mediaFile : mediaFileService.getMostRecentlyPlayedAlbums(offset, count, musicFolders)) { + Album album = createAlbum(mediaFile); + album.setLastPlayed(mediaFile.getLastPlayed()); + result.add(album); + } + return result; + } + + private List getNewest(int offset, int count, List musicFolders) throws IOException { + List result = new ArrayList(); + for (MediaFile file : mediaFileService.getNewestAlbums(offset, count, musicFolders)) { + Album album = createAlbum(file); + Date created = file.getCreated(); + if (created == null) { + created = file.getChanged(); + } + album.setCreated(created); + result.add(album); + } + return result; + } + + private List getStarred(int offset, int count, String username, List musicFolders) throws IOException { + List result = new ArrayList(); + for (MediaFile file : mediaFileService.getStarredAlbums(offset, count, username, musicFolders)) { + result.add(createAlbum(file)); + } + return result; + } + + private List getRandom(int count, List musicFolders) throws IOException { + List result = new ArrayList(); + for (MediaFile file : searchService.getRandomAlbums(count, musicFolders)) { + result.add(createAlbum(file)); + } + return result; + } + + private List getAlphabetical(int offset, int count, boolean byArtist, List musicFolders) throws IOException { + List result = new ArrayList(); + for (MediaFile file : mediaFileService.getAlphabeticalAlbums(offset, count, byArtist, musicFolders)) { + result.add(createAlbum(file)); + } + return result; + } + + private List getByYear(int offset, int count, int fromYear, int toYear, List musicFolders) { + List result = new ArrayList(); + for (MediaFile file : mediaFileService.getAlbumsByYear(offset, count, fromYear, toYear, musicFolders)) { + Album album = createAlbum(file); + album.setYear(file.getYear()); + result.add(album); + } + return result; + } + + private List createDecades() { + List result = new ArrayList(); + int decade = Calendar.getInstance().get(Calendar.YEAR) / 10; + for (int i = 0; i < 10; i++) { + result.add((decade - i) * 10); + } + return result; + } + + private List getByGenre(int offset, int count, String genre, List musicFolders) { + List result = new ArrayList(); + for (MediaFile file : mediaFileService.getAlbumsByGenre(offset, count, genre, musicFolders)) { + result.add(createAlbum(file)); + } + return result; + } + + private Album createAlbum(MediaFile file) { + Album album = new Album(); + album.setId(file.getId()); + album.setPath(file.getPath()); + album.setArtist(file.getArtist()); + album.setAlbumTitle(file.getAlbumName()); + album.setCoverArtPath(file.getCoverArtPath()); + return album; + } + + 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. + */ + 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; + private Integer year; + + 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; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getYear() { + return year; + } + } +} 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..2c59a386 --- /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 . + + 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 map = new HashMap(); + + 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(), null); + 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..13026147 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/InternetRadioSettingsController.java @@ -0,0 +1,122 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 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 map = new HashMap(); + + 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 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 || homepageUrl != null) { + if (name == null) { + return "internetradiosettings.noname"; + } + if (streamUrl == null) { + return "internetradiosettings.nourl"; + } + 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/JAXBWriter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/JAXBWriter.java new file mode 100644 index 00000000..79b063d5 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/JAXBWriter.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.InputStream; +import java.io.StringWriter; +import java.util.Date; +import java.util.GregorianCalendar; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.datatype.DatatypeFactory; +import javax.xml.datatype.XMLGregorianCalendar; + +import org.apache.commons.io.IOUtils; +import org.eclipse.persistence.jaxb.JAXBContext; +import org.eclipse.persistence.jaxb.MarshallerProperties; +import org.jdom.Attribute; +import org.jdom.Document; +import org.jdom.input.SAXBuilder; +import org.subsonic.restapi.Error; +import org.subsonic.restapi.ObjectFactory; +import org.subsonic.restapi.Response; +import org.subsonic.restapi.ResponseStatus; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.util.StringUtil; + +import static org.springframework.web.bind.ServletRequestUtils.getStringParameter; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class JAXBWriter { + + private static final Logger LOG = Logger.getLogger(JAXBWriter.class); + + private final javax.xml.bind.JAXBContext jaxbContext; + private final DatatypeFactory datatypeFactory; + private final String restProtocolVersion; + + public JAXBWriter() { + try { + jaxbContext = JAXBContext.newInstance(Response.class); + datatypeFactory = DatatypeFactory.newInstance(); + restProtocolVersion = getRESTProtocolVersion(); + } catch (Exception x) { + throw new RuntimeException(x); + } + } + + private Marshaller createXmlMarshaller() throws JAXBException { + Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_ENCODING, StringUtil.ENCODING_UTF8); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + return marshaller; + } + + private Marshaller createJsonMarshaller() throws JAXBException { + Marshaller marshaller = jaxbContext.createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_ENCODING, StringUtil.ENCODING_UTF8); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, "application/json"); + marshaller.setProperty(MarshallerProperties.JSON_INCLUDE_ROOT, true); + return marshaller; + } + + private String getRESTProtocolVersion() throws Exception { + InputStream in = null; + try { + in = StringUtil.class.getResourceAsStream("/subsonic-rest-api.xsd"); + Document document = new SAXBuilder().build(in); + Attribute version = document.getRootElement().getAttribute("version"); + return version.getValue(); + } finally { + IOUtils.closeQuietly(in); + } + } + + public String getRestProtocolVersion() { + return restProtocolVersion; + } + + public Response createResponse(boolean ok) { + Response response = new ObjectFactory().createResponse(); + response.setStatus(ok ? ResponseStatus.OK : ResponseStatus.FAILED); + response.setVersion(restProtocolVersion); + return response; + } + + public void writeResponse(HttpServletRequest request, HttpServletResponse httpResponse, Response jaxbResponse) throws Exception { + + String format = getStringParameter(request, "f", "xml"); + String jsonpCallback = request.getParameter("callback"); + boolean json = "json".equals(format); + boolean jsonp = "jsonp".equals(format) && jsonpCallback != null; + Marshaller marshaller; + + if (json) { + marshaller = createJsonMarshaller(); + httpResponse.setContentType("application/json"); + } else if (jsonp) { + marshaller = createJsonMarshaller(); + httpResponse.setContentType("text/javascript"); + } else { + marshaller = createXmlMarshaller(); + httpResponse.setContentType("text/xml"); + } + + httpResponse.setCharacterEncoding(StringUtil.ENCODING_UTF8); + + try { + StringWriter writer = new StringWriter(); + if (jsonp) { + writer.append(jsonpCallback).append('('); + } + marshaller.marshal(new ObjectFactory().createSubsonicResponse(jaxbResponse), writer); + if (jsonp) { + writer.append(");"); + } + httpResponse.getWriter().append(writer.getBuffer()); + } catch (Exception x) { + LOG.error("Failed to marshal JAXB", x); + throw x; + } + } + + public void writeErrorResponse(HttpServletRequest request, HttpServletResponse response, + RESTController.ErrorCode code, String message) throws Exception { + Response res = createResponse(false); + Error error = new Error(); + res.setError(error); + error.setCode(code.getCode()); + error.setMessage(message); + writeResponse(request, response, res); + } + + public XMLGregorianCalendar convertDate(Date date) { + if (date == null) { + return null; + } + + GregorianCalendar c = new GregorianCalendar(); + c.setTime(date); + return datatypeFactory.newXMLGregorianCalendar(c).normalize(); + } +} 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..881f98a8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/LeftController.java @@ -0,0 +1,200 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +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 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 org.springframework.web.servlet.support.RequestContextUtils; + +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicFolderContent; +import net.sourceforge.subsonic.domain.UserSettings; +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 { + + // 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 MusicIndexService musicIndexService; + private PlayerService playerService; + + /** + * Note: This class intentionally does not implement org.springframework.web.servlet.mvc.LastModified + * as we don't need browser-side caching of left.jsp. This method is only used by RESTController. + */ + 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 allMusicFolders = settingsService.getMusicFoldersForUser(username); + MusicFolder selectedMusicFolder = settingsService.getSelectedMusicFolder(username); + 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 { + boolean musicFolderChanged = saveSelectedMusicFolder(request); + Map map = new HashMap(); + + MediaLibraryStatistics statistics = mediaScannerService.getStatistics(); + Locale locale = RequestContextUtils.getLocale(request); + + boolean refresh = ServletRequestUtils.getBooleanParameter(request, "refresh", false); + if (refresh) { + settingsService.clearMusicFolderCache(); + } + + String username = securityService.getCurrentUsername(request); + List allMusicFolders = settingsService.getMusicFoldersForUser(username); + MusicFolder selectedMusicFolder = settingsService.getSelectedMusicFolder(username); + List musicFoldersToUse = selectedMusicFolder == null ? allMusicFolders : Arrays.asList(selectedMusicFolder); + UserSettings userSettings = settingsService.getUserSettings(username); + MusicFolderContent musicFolderContent = musicIndexService.getMusicFolderContent(musicFoldersToUse, refresh); + + 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", musicIndexService.getShortcuts(musicFoldersToUse)); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("organizeByFolderStructure", settingsService.isOrganizeByFolderStructure()); + map.put("musicFolderChanged", musicFolderChanged); + + 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 boolean saveSelectedMusicFolder(HttpServletRequest request) { + if (request.getParameter("musicFolderId") == null) { + return false; + } + 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); + + return true; + } + + 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 setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + +} 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 . + + 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 map = new HashMap(); + + 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..63d27611 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/M3UController.java @@ -0,0 +1,122 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.Controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller which produces the M3U playlist. + * + * @author Sindre Mehus + */ +public class M3UController implements Controller { + + private PlayerService playerService; + private SettingsService settingsService; + private TranscodingService transcodingService; + + 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); + } + + url = settingsService.rewriteRemoteUrl(url); + + 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 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..a70025bb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MainController.java @@ -0,0 +1,288 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +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.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaFileComparator; +import net.sourceforge.subsonic.domain.MusicFolder; +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.PlayerService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the main page. + * + * @author Sindre Mehus + */ +public class MainController extends AbstractController { + + 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 map = new HashMap(); + + Player player = playerService.getPlayer(request, response); + List 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?")); + } + + String username = securityService.getCurrentUsername(request); + if (!securityService.isFolderAccessAllowed(dir, username)) { + return new ModelAndView(new RedirectView("accessDenied.view")); + } + + List children = mediaFiles.size() == 1 ? mediaFileService.getChildrenOf(dir, true, true, true) : getMultiFolderChildren(mediaFiles); + List files = new ArrayList(); + List subDirs = new ArrayList(); + for (MediaFile child : children) { + if (child.isFile()) { + files.add(child); + } else { + subDirs.add(child); + } + } + + UserSettings userSettings = settingsService.getUserSettings(username); + + mediaFileService.populateStarredDate(dir, username); + mediaFileService.populateStarredDate(children, username); + + map.put("dir", dir); + map.put("files", files); + map.put("subDirs", subDirs); + map.put("ancestors", getAncestors(dir)); + map.put("coverArtSizeMedium", CoverArtScheme.MEDIUM.getSize()); + map.put("coverArtSizeLarge", CoverArtScheme.LARGE.getSize()); + map.put("player", player); + map.put("user", securityService.getCurrentUser(request)); + map.put("visibility", userSettings.getMainVisibility()); + map.put("showAlbumYear", settingsService.isSortAlbumsByYear()); + map.put("showArtistInfo", userSettings.isShowArtistInfoEnabled()); + map.put("partyMode", userSettings.isPartyModeEnabled()); + map.put("brand", settingsService.getBrand()); + map.put("showAd", !settingsService.isLicenseValid() && adService.showAd()); + map.put("viewAsList", isViewAsList(request, userSettings)); + if (dir.isAlbum()) { + map.put("sieblingAlbums", getSieblingAlbums(dir)); + map.put("artist", guessArtist(children)); + map.put("album", guessAlbum(children)); + } + + 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); + + String view; + if (isVideoOnly(children)) { + view = "videoMain"; + } else if (dir.isAlbum()) { + view = "albumMain"; + } else { + view = "artistMain"; + } + + ModelAndView result = new ModelAndView(view); + result.addObject("model", map); + return result; + } + + private boolean isViewAsList(HttpServletRequest request, UserSettings userSettings) { + boolean viewAsList = ServletRequestUtils.getBooleanParameter(request, "viewAsList", userSettings.isViewAsList()); + if (viewAsList != userSettings.isViewAsList()) { + userSettings.setViewAsList(viewAsList); + userSettings.setChanged(new Date()); + settingsService.updateUserSettings(userSettings); + } + return viewAsList; + } + + private boolean isVideoOnly(List children) { + boolean videoFound = false; + for (MediaFile child : children) { + if (child.isAudio()) { + return false; + } + if (child.isVideo()) { + videoFound = true; + } + } + return videoFound; + } + + private List getMediaFiles(HttpServletRequest request) { + List mediaFiles = new ArrayList(); + 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 children) { + for (MediaFile child : children) { + if (child.isFile() && child.getArtist() != null) { + return child.getArtist(); + } + } + return null; + } + + private String guessAlbum(List children) { + for (MediaFile child : children) { + if (child.isFile() && child.getArtist() != null) { + return child.getAlbumName(); + } + } + return null; + } + + private List getMultiFolderChildren(List mediaFiles) throws IOException { + SortedSet result = new TreeSet(new MediaFileComparator(settingsService.isSortAlbumsByYear())); + for (MediaFile mediaFile : mediaFiles) { + if (mediaFile.isFile()) { + mediaFile = mediaFileService.getParentOf(mediaFile); + } + result.addAll(mediaFileService.getChildrenOf(mediaFile, true, true, true)); + } + return new ArrayList(result); + } + + private List getAncestors(MediaFile dir) throws IOException { + LinkedList result = new LinkedList(); + + 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 List getSieblingAlbums(MediaFile dir) { + List result = new ArrayList(); + + MediaFile parent = mediaFileService.getParentOf(dir); + if (!mediaFileService.isRoot(parent)) { + List sieblings = mediaFileService.getChildrenOf(parent, false, true, true); + for (MediaFile siebling : sieblings) { + if (siebling.isAlbum() && !siebling.equals(dir)) { + result.add(siebling); + } + } + } + return result; + } + + 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..1361e1c9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MoreController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.io.File; +import java.util.Calendar; +import java.util.HashMap; +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.MusicFolder; +import net.sourceforge.subsonic.domain.Player; +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 net.sourceforge.subsonic.util.StringUtil; + +/** + * 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 map = new HashMap(); + + User user = securityService.getCurrentUser(request); + + String uploadDirectory = null; + List musicFolders = settingsService.getMusicFoldersForUser(user.getUsername()); + if (musicFolders.size() > 0) { + uploadDirectory = new File(musicFolders.get(0).getPath(), "Incoming").getPath(); + } + + + StringBuilder jamstashUrl = new StringBuilder("http://jamstash.com/#/settings?u=" + StringUtil.urlEncode(user.getUsername()) + "&url="); + if (settingsService.isUrlRedirectionEnabled()) { + jamstashUrl.append(StringUtil.urlEncode(settingsService.getUrlRedirectUrl())); + } else { + jamstashUrl.append(StringUtil.urlEncode(request.getRequestURL().toString().replaceAll("/more.view.*", ""))); + } + + Player player = playerService.getPlayer(request, response); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + map.put("user", user); + map.put("uploadDirectory", uploadDirectory); + map.put("genres", mediaFileService.getGenres(false)); + map.put("currentYear", Calendar.getInstance().get(Calendar.YEAR)); + map.put("musicFolders", musicFolders); + map.put("clientSidePlaylist", player.isExternalWithPlaylist() || player.isWeb()); + map.put("brand", settingsService.getBrand()); + map.put("jamstashUrl", jamstashUrl); + 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..efb97464 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MultiController.java @@ -0,0 +1,265 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.ArrayList; +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.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.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; +import org.springframework.web.servlet.view.RedirectView; + +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 net.tanesha.recaptcha.ReCaptcha; +import net.tanesha.recaptcha.ReCaptchaFactory; +import net.tanesha.recaptcha.ReCaptchaResponse; + +/** + * 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 map = new HashMap(); + 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 map = new HashMap(); + String usernameOrEmail = StringUtils.trimToNull(request.getParameter("usernameOrEmail")); + ReCaptcha captcha = ReCaptchaFactory.newSecureReCaptcha("6LcZ3OMSAAAAANkKMdFdaNopWu9iS03V-nLOuoiH", + "6LcZ3OMSAAAAAPaFg89mEzs-Ft0fIu7wxfKtkwmQ", false); + boolean showCaptcha = true; + + if (usernameOrEmail != null) { + + map.put("usernameOrEmail", usernameOrEmail); + User user = getUserByUsernameOrEmail(usernameOrEmail); + String challenge = request.getParameter("recaptcha_challenge_field"); + String uresponse = request.getParameter("recaptcha_response_field"); + ReCaptchaResponse captchaResponse = captcha.checkAnswer(request.getRemoteAddr(), challenge, uresponse); + + if (!captchaResponse.isValid()) { + map.put("error", "recover.error.invalidcaptcha"); + } else 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); + showCaptcha = false; + } else { + map.put("error", "recover.error.sendfailed"); + } + } + } + + if (showCaptcha) { + map.put("captcha", captcha.createRecaptchaHtml(null, null)); + } + + 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 params = new ArrayList(); + 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 map = new HashMap(); + 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 map = new HashMap(); + map.put("showRight", userSettings.isShowNowPlayingEnabled() || userSettings.isShowChatEnabled()); + map.put("autoHidePlayQueue", userSettings.isAutoHidePlayQueue()); + map.put("showSideBar", userSettings.isShowSideBar()); + 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..60f00ac4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/MusicFolderSettingsController.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 . + + 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 mediaFileDao; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + MusicFolderSettingsCommand command = new MusicFolderSettingsCommand(); + + if (request.getParameter("scanNow") != null) { + settingsService.clearMusicFolderCache(); + 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(); + mediaFileDao.expunge(); + } + + private List wrap(List musicFolders) { + ArrayList result = new ArrayList(); + 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 { + MusicFolder musicFolder = musicFolderInfo.toMusicFolder(); + if (musicFolder != null) { + settingsService.updateMusicFolder(musicFolder); + } + } + } + + 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 setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } +} \ 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..76a043fa --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NetworkSettingsController.java @@ -0,0 +1,83 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +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.domain.UrlRedirectType; +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 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.setUrlRedirectType(settingsService.getUrlRedirectType().name()); + command.setUrlRedirectFrom(settingsService.getUrlRedirectFrom()); + command.setUrlRedirectCustomUrl(settingsService.getUrlRedirectCustomUrl()); + command.setPort(settingsService.getPort()); + command.setLicenseInfo(settingsService.getLicenseInfo()); + + return command; + } + + protected void doSubmitAction(Object cmd) throws Exception { + NetworkSettingsCommand command = (NetworkSettingsCommand) cmd; + command.setToast(true); + + settingsService.setPortForwardingEnabled(command.isPortForwardingEnabled()); + settingsService.setUrlRedirectionEnabled(command.isUrlRedirectionEnabled()); + settingsService.setUrlRedirectType(UrlRedirectType.valueOf(command.getUrlRedirectType())); + settingsService.setUrlRedirectFrom(StringUtils.lowerCase(command.getUrlRedirectFrom())); + settingsService.setUrlRedirectCustomUrl(StringUtils.trimToEmpty(command.getUrlRedirectCustomUrl())); + + if (settingsService.getServerId() == null) { + Random rand = new Random(System.currentTimeMillis()); + settingsService.setServerId(String.valueOf(Math.abs(rand.nextLong()))); + } + + settingsService.save(); + networkService.initPortForwarding(0); + 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..93e2131e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/NowPlayingController.java @@ -0,0 +1,78 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.List; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.TransferStatus; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.StatusService; + +/** + * 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 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?id=" + dir.getId(); + } 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..b30e510f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PasswordSettingsController.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 . + + 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); + command.setToast(true); + } + + 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..5348493e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PersonalSettingsController.java @@ -0,0 +1,183 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.Date; +import java.util.Locale; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.PersonalSettingsCommand; +import net.sourceforge.subsonic.domain.AlbumListType; +import net.sourceforge.subsonic.domain.AvatarScheme; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * 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.setAlbumLists(AlbumListType.values()); + command.setAlbumListId(userSettings.getDefaultAlbumList().getId()); + command.setAvatars(settingsService.getAllSystemAvatars()); + command.setCustomAvatar(settingsService.getCustomAvatar(user.getUsername())); + command.setAvatarId(getAvatarId(userSettings)); + command.setPartyModeEnabled(userSettings.isPartyModeEnabled()); + command.setQueueFollowingSongs(userSettings.isQueueFollowingSongs()); + command.setShowNowPlayingEnabled(userSettings.isShowNowPlayingEnabled()); + command.setShowChatEnabled(userSettings.isShowChatEnabled()); + command.setShowArtistInfoEnabled(userSettings.isShowArtistInfoEnabled()); + command.setNowPlayingAllowed(userSettings.isNowPlayingAllowed()); + command.setMainVisibility(userSettings.getMainVisibility()); + command.setPlaylistVisibility(userSettings.getPlaylistVisibility()); + command.setFinalVersionNotificationEnabled(userSettings.isFinalVersionNotificationEnabled()); + command.setBetaVersionNotificationEnabled(userSettings.isBetaVersionNotificationEnabled()); + command.setSongNotificationEnabled(userSettings.isSongNotificationEnabled()); + command.setAutoHidePlayQueue(userSettings.isAutoHidePlayQueue()); + 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.setDefaultAlbumList(AlbumListType.fromId(command.getAlbumListId())); + settings.setPartyModeEnabled(command.isPartyModeEnabled()); + settings.setQueueFollowingSongs(command.isQueueFollowingSongs()); + settings.setShowNowPlayingEnabled(command.isShowNowPlayingEnabled()); + settings.setShowChatEnabled(command.isShowChatEnabled()); + settings.setShowArtistInfoEnabled(command.isShowArtistInfoEnabled()); + settings.setNowPlayingAllowed(command.isNowPlayingAllowed()); + settings.setMainVisibility(command.getMainVisibility()); + settings.setPlaylistVisibility(command.getPlaylistVisibility()); + settings.setFinalVersionNotificationEnabled(command.isFinalVersionNotificationEnabled()); + settings.setBetaVersionNotificationEnabled(command.isBetaVersionNotificationEnabled()); + settings.setSongNotificationEnabled(command.isSongNotificationEnabled()); + settings.setAutoHidePlayQueue(command.isAutoHidePlayQueue()); + 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..8952bb6b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayQueueController.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 . + + 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 map = new HashMap(); + 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()); + map.put("notify", userSettings.isSongNotificationEnabled()); + map.put("autoHide", userSettings.isAutoHidePlayQueue()); + map.put("licenseInfo", settingsService.getLicenseInfo()); + 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..ef2a7598 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlayerSettingsController.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 . + + 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.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 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.setTranscodeSchemeName(player.getTranscodeScheme().name()); + command.setTechnologyName(player.getTechnology().name()); + command.setAllTranscodings(transcodingService.getAllTranscodings()); + List 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.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.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 getPlayers(HttpServletRequest request) { + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + List players = playerService.getAllPlayers(); + List authorizedPlayers = new ArrayList(); + + 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..6cf24405 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +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 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 playlist page. + * + * @author Sindre Mehus + */ +public class PlaylistController extends ParameterizableViewController { + + private SecurityService securityService; + private PlaylistService playlistService; + private SettingsService settingsService; + private PlayerService playerService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map map = new HashMap(); + + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + Player player = playerService.getPlayer(request, response); + 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("player", player); + 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; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistsController.java new file mode 100644 index 00000000..d00322cb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PlaylistsController.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 . + * + * Copyright 2014 (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.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 net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the playlists page. + * + * @author Sindre Mehus + */ +public class PlaylistsController extends ParameterizableViewController { + + private SecurityService securityService; + private PlaylistService playlistService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map map = new HashMap(); + + User user = securityService.getCurrentUser(request); + List playlists = playlistService.getReadablePlaylistsForUser(user.getUsername()); + + map.put("playlists", playlists); + 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/PodcastChannelController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastChannelController.java new file mode 100644 index 00000000..4ef0d92b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastChannelController.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 . + * + * Copyright 2015 (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.service.PodcastService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * Controller for the "Podcast channel" page. + * + * @author Sindre Mehus + */ +public class PodcastChannelController extends ParameterizableViewController { + + private PodcastService podcastService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map map = new HashMap(); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + + int channelId = ServletRequestUtils.getRequiredIntParameter(request, "id"); + + map.put("user", securityService.getCurrentUser(request)); + map.put("channel", podcastService.getChannel(channelId)); + map.put("episodes", podcastService.getEpisodes(channelId)); + return result; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastChannelsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastChannelsController.java new file mode 100644 index 00000000..2e044ef2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastChannelsController.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 . + * + * Copyright 2015 (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.service.PodcastService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the "Podcast channels" page. + * + * @author Sindre Mehus + */ +public class PodcastChannelsController extends ParameterizableViewController { + + private PodcastService podcastService; + private SecurityService securityService; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map map = new HashMap(); + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + + Map> channels = new LinkedHashMap>(); + Map channelMap = new HashMap(); + for (PodcastChannel channel : podcastService.getAllChannels()) { + channels.put(channel, podcastService.getEpisodes(channel.getId())); + channelMap.put(channel.getId(), channel); + } + + map.put("user", securityService.getCurrentUser(request)); + map.put("channels", channels); + map.put("channelMap", channelMap); + map.put("newestEpisodes", podcastService.getNewestEpisodes(10)); + map.put("licenseInfo", settingsService.getLicenseInfo()); + 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/PodcastController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.java new file mode 100644 index 00000000..a3f1467b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastController.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 . + + 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 playlists = playlistService.getReadablePlaylistsForUser(username); + List podcasts = new ArrayList(); + + for (Playlist playlist : playlists) { + + List 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); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + url = StringUtil.rewriteUrl(url, referer); + } + + String enclosureUrl = url.replaceFirst("/podcast.*", "/stream?playlist=" + playlist.getId()); + enclosureUrl = settingsService.rewriteRemoteUrl(enclosureUrl); + + podcasts.add(new Podcast(playlist.getName(), publishDate, enclosureUrl, length, type)); + } + + Map map = new HashMap(); + + 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..20224823 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastReceiverAdminController.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 . + + 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.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +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; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * 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 { + Integer channelId = ServletRequestUtils.getIntParameter(request, "channelId"); + + if (request.getParameter("add") != null) { + String url = StringUtils.trim(request.getParameter("add")); + podcastService.createChannel(url); + return new ModelAndView(new RedirectView("podcastChannels.view")); + } + if (request.getParameter("downloadEpisode") != null) { + download(StringUtil.parseInts(request.getParameter("downloadEpisode"))); + return new ModelAndView(new RedirectView("podcastChannel.view?id=" + channelId)); + } + if (request.getParameter("deleteChannel") != null) { + podcastService.deleteChannel(channelId); + return new ModelAndView(new RedirectView("podcastChannels.view")); + } + if (request.getParameter("deleteEpisode") != null) { + for (int episodeId : StringUtil.parseInts(request.getParameter("deleteEpisode"))) { + podcastService.deleteEpisode(episodeId, true); + } + return new ModelAndView(new RedirectView("podcastChannel.view?id=" + channelId)); + } + if (request.getParameter("refresh") != null) { + if (channelId != null) { + podcastService.refreshChannel(channelId, true); + return new ModelAndView(new RedirectView("podcastChannel.view?id=" + channelId)); + } else { + podcastService.refreshAllChannels(true); + return new ModelAndView(new RedirectView("podcastChannels.view")); + } + } + + return new ModelAndView(new RedirectView("podcastChannels.view")); + } + + private void download(int[] episodeIds) { + for (Integer episodeId : episodeIds) { + 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/PodcastSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.java new file mode 100644 index 00000000..85be43d8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PodcastSettingsController.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 . + + 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; + command.setToast(true); + + 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/PremiumSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PremiumSettingsController.java new file mode 100644 index 00000000..9add5d32 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/PremiumSettingsController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.Date; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.validation.BindException; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import net.sourceforge.subsonic.command.PremiumSettingsCommand; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the Subsonic Premium page. + * + * @author Sindre Mehus + */ +public class PremiumSettingsController extends SimpleFormController { + + private SettingsService settingsService; + private SecurityService securityService; + + protected Object formBackingObject(HttpServletRequest request) throws Exception { + PremiumSettingsCommand command = new PremiumSettingsCommand(); + command.setPath(request.getParameter("path")); + command.setForceChange(request.getParameter("change") != null); + command.setLicenseInfo(settingsService.getLicenseInfo()); + command.setBrand(settingsService.getBrand()); + command.setUser(securityService.getCurrentUser(request)); + return command; + } + + protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object com, BindException errors) + throws Exception { + PremiumSettingsCommand command = (PremiumSettingsCommand) com; + Date now = new Date(); + + settingsService.setLicenseCode(command.getLicenseCode()); + settingsService.setLicenseEmail(command.getLicenseInfo().getLicenseEmail()); + settingsService.setLicenseDate(now); + settingsService.save(); + settingsService.scheduleLicenseValidation(); + + // Reflect changes in view. The validator will validate the license asynchronously. + command.setLicenseInfo(settingsService.getLicenseInfo()); + command.setToast(true); + + return new ModelAndView(getSuccessView(), errors.getModel()); + } + + 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/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 . + + 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..255608bc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RESTController.java @@ -0,0 +1,2520 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.ArrayList; +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 java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.multiaction.MultiActionController; +import org.subsonic.restapi.AlbumID3; +import org.subsonic.restapi.AlbumList; +import org.subsonic.restapi.AlbumList2; +import org.subsonic.restapi.AlbumWithSongsID3; +import org.subsonic.restapi.ArtistID3; +import org.subsonic.restapi.ArtistInfo; +import org.subsonic.restapi.ArtistInfo2; +import org.subsonic.restapi.ArtistWithAlbumsID3; +import org.subsonic.restapi.ArtistsID3; +import org.subsonic.restapi.Bookmarks; +import org.subsonic.restapi.ChatMessage; +import org.subsonic.restapi.ChatMessages; +import org.subsonic.restapi.Child; +import org.subsonic.restapi.Directory; +import org.subsonic.restapi.Genres; +import org.subsonic.restapi.Index; +import org.subsonic.restapi.IndexID3; +import org.subsonic.restapi.Indexes; +import org.subsonic.restapi.InternetRadioStation; +import org.subsonic.restapi.InternetRadioStations; +import org.subsonic.restapi.JukeboxPlaylist; +import org.subsonic.restapi.JukeboxStatus; +import org.subsonic.restapi.License; +import org.subsonic.restapi.Lyrics; +import org.subsonic.restapi.MediaType; +import org.subsonic.restapi.MusicFolders; +import org.subsonic.restapi.NewestPodcasts; +import org.subsonic.restapi.NowPlaying; +import org.subsonic.restapi.NowPlayingEntry; +import org.subsonic.restapi.PlaylistWithSongs; +import org.subsonic.restapi.Playlists; +import org.subsonic.restapi.PodcastStatus; +import org.subsonic.restapi.Podcasts; +import org.subsonic.restapi.Response; +import org.subsonic.restapi.SearchResult2; +import org.subsonic.restapi.SearchResult3; +import org.subsonic.restapi.Shares; +import org.subsonic.restapi.SimilarSongs; +import org.subsonic.restapi.SimilarSongs2; +import org.subsonic.restapi.Songs; +import org.subsonic.restapi.Starred; +import org.subsonic.restapi.Starred2; +import org.subsonic.restapi.TopSongs; +import org.subsonic.restapi.Users; +import org.subsonic.restapi.Videos; + +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.ajax.PlayQueueService; +import net.sourceforge.subsonic.command.UserSettingsCommand; +import net.sourceforge.subsonic.dao.AlbumDao; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.dao.BookmarkDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.dao.PlayQueueDao; +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.ArtistBio; +import net.sourceforge.subsonic.domain.Bookmark; +import net.sourceforge.subsonic.domain.Genre; +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.domain.LicenseInfo; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicFolderContent; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.PlayStatus; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.domain.SavedPlayQueue; +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.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.JukeboxService; +import net.sourceforge.subsonic.service.LastFmService; +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.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.Pair; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +import static net.sourceforge.subsonic.security.RESTRequestParameterProcessingFilter.decrypt; +import static org.springframework.web.bind.ServletRequestUtils.*; + +/** + * Multi-controller used for the REST API. + *

+ * For documentation, please refer to api.jsp. + *

+ * Note: Exceptions thrown from the methods are intercepted by RESTFilter. + * + * @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 LastFmService lastFmService; + private MusicIndexService musicIndexService; + private TranscodingService transcodingService; + private DownloadController downloadController; + private CoverArtController coverArtController; + private AvatarController avatarController; + private UserSettingsController userSettingsController; + private LeftController leftController; + private StatusService statusService; + private StreamController streamController; + private HLSController hlsController; + 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; + private BookmarkDao bookmarkDao; + private PlayQueueDao playQueueDao; + + private final Map bookmarkCache = new ConcurrentHashMap(); + private final JAXBWriter jaxbWriter = new JAXBWriter(); + + public void init() { + refreshBookmarkCache(); + } + + private void refreshBookmarkCache() { + bookmarkCache.clear(); + for (Bookmark bookmark : bookmarkDao.getBookmarks()) { + bookmarkCache.put(BookmarkKey.forBookmark(bookmark), bookmark); + } + } + + @SuppressWarnings("UnusedDeclaration") + public void ping(HttpServletRequest request, HttpServletResponse response) throws Exception { + Response res = createResponse(); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getLicense(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + License license = new License(); + + LicenseInfo licenseInfo = settingsService.getLicenseInfo(); + + license.setEmail(licenseInfo.getLicenseEmail()); + license.setValid(licenseInfo.isLicenseValid()); + license.setLicenseExpires(jaxbWriter.convertDate(licenseInfo.getLicenseExpires())); + license.setTrialExpires(jaxbWriter.convertDate(licenseInfo.getTrialExpires())); + + Response res = createResponse(); + res.setLicense(license); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getMusicFolders(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + MusicFolders musicFolders = new MusicFolders(); + String username = securityService.getCurrentUsername(request); + for (MusicFolder musicFolder : settingsService.getMusicFoldersForUser(username)) { + org.subsonic.restapi.MusicFolder mf = new org.subsonic.restapi.MusicFolder(); + mf.setId(musicFolder.getId()); + mf.setName(musicFolder.getName()); + musicFolders.getMusicFolder().add(mf); + } + Response res = createResponse(); + res.setMusicFolders(musicFolders); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getIndexes(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Response res = createResponse(); + String username = securityService.getCurrentUser(request).getUsername(); + + long ifModifiedSince = getLongParameter(request, "ifModifiedSince", 0L); + long lastModified = leftController.getLastModified(request); + + if (lastModified <= ifModifiedSince) { + jaxbWriter.writeResponse(request, response, res); + return; + } + + Indexes indexes = new Indexes(); + indexes.setLastModified(lastModified); + indexes.setIgnoredArticles(settingsService.getIgnoredArticles()); + + List musicFolders = settingsService.getMusicFoldersForUser(username); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + if (musicFolderId != null) { + for (MusicFolder musicFolder : musicFolders) { + if (musicFolderId.equals(musicFolder.getId())) { + musicFolders = Collections.singletonList(musicFolder); + break; + } + } + } + + for (MediaFile shortcut : musicIndexService.getShortcuts(musicFolders)) { + indexes.getShortcut().add(createJaxbArtist(shortcut, username)); + } + + MusicFolderContent musicFolderContent = musicIndexService.getMusicFolderContent(musicFolders, false); + + for (Map.Entry> entry : musicFolderContent.getIndexedArtists().entrySet()) { + Index index = new Index(); + indexes.getIndex().add(index); + index.setName(entry.getKey().getIndex()); + + for (MusicIndex.SortableArtistWithMediaFiles artist : entry.getValue()) { + for (MediaFile mediaFile : artist.getMediaFiles()) { + if (mediaFile.isDirectory()) { + Date starredDate = mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username); + org.subsonic.restapi.Artist a = new org.subsonic.restapi.Artist(); + index.getArtist().add(a); + a.setId(String.valueOf(mediaFile.getId())); + a.setName(artist.getName()); + a.setStarred(jaxbWriter.convertDate(starredDate)); + + if (mediaFile.isAlbum()) { + a.setAverageRating(ratingService.getAverageRating(mediaFile)); + a.setUserRating(ratingService.getRatingForUser(username, mediaFile)); + } + } + } + } + } + + // Add children + Player player = playerService.getPlayer(request, response); + + for (MediaFile singleSong : musicFolderContent.getSingleSongs()) { + indexes.getChild().add(createJaxbChild(player, singleSong, username)); + } + + res.setIndexes(indexes); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getGenres(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Genres genres = new Genres(); + + for (Genre genre : mediaFileDao.getGenres(false)) { + org.subsonic.restapi.Genre g = new org.subsonic.restapi.Genre(); + genres.getGenre().add(g); + g.setContent(genre.getName()); + g.setAlbumCount(genre.getAlbumCount()); + g.setSongCount(genre.getSongCount()); + } + Response res = createResponse(); + res.setGenres(genres); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getSongsByGenre(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + Songs songs = new Songs(); + + String genre = getRequiredStringParameter(request, "genre"); + int offset = getIntParameter(request, "offset", 0); + int count = getIntParameter(request, "count", 10); + count = Math.max(0, Math.min(count, 500)); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + for (MediaFile mediaFile : mediaFileDao.getSongsByGenre(genre, offset, count, musicFolders)) { + songs.getSong().add(createJaxbChild(player, mediaFile, username)); + } + Response res = createResponse(); + res.setSongsByGenre(songs); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getArtists(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + + ArtistsID3 result = new ArtistsID3(); + result.setIgnoredArticles(settingsService.getIgnoredArticles()); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + List artists = artistDao.getAlphabetialArtists(0, Integer.MAX_VALUE, musicFolders); + SortedMap> indexedArtists = musicIndexService.getIndexedArtists(artists); + for (Map.Entry> entry : indexedArtists.entrySet()) { + IndexID3 index = new IndexID3(); + result.getIndex().add(index); + index.setName(entry.getKey().getIndex()); + for (MusicIndex.SortableArtistWithArtist sortableArtist : entry.getValue()) { + index.getArtist().add(createJaxbArtist(new ArtistID3(), sortableArtist.getArtist(), username)); + } + } + + Response res = createResponse(); + res.setArtists(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getSimilarSongs(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + int count = getIntParameter(request, "count", 50); + + SimilarSongs result = new SimilarSongs(); + + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile == null) { + error(request, response, ErrorCode.NOT_FOUND, "Media file not found."); + return; + } + List musicFolders = settingsService.getMusicFoldersForUser(username); + List similarSongs = lastFmService.getSimilarSongs(mediaFile, count, musicFolders); + Player player = playerService.getPlayer(request, response); + for (MediaFile similarSong : similarSongs) { + result.getSong().add(createJaxbChild(player, similarSong, username)); + } + + Response res = createResponse(); + res.setSimilarSongs(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getSimilarSongs2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + int count = getIntParameter(request, "count", 50); + + SimilarSongs2 result = new SimilarSongs2(); + + Artist artist = artistDao.getArtist(id); + if (artist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Artist not found."); + return; + } + + List musicFolders = settingsService.getMusicFoldersForUser(username); + List similarSongs = lastFmService.getSimilarSongs(artist, count, musicFolders); + Player player = playerService.getPlayer(request, response); + for (MediaFile similarSong : similarSongs) { + result.getSong().add(createJaxbChild(player, similarSong, username)); + } + + Response res = createResponse(); + res.setSimilarSongs2(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getTopSongs(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + + String artist = getRequiredStringParameter(request, "artist"); + int count = getIntParameter(request, "count", 50); + + TopSongs result = new TopSongs(); + + List musicFolders = settingsService.getMusicFoldersForUser(username); + List topSongs = lastFmService.getTopSongs(artist, count, musicFolders); + Player player = playerService.getPlayer(request, response); + for (MediaFile topSong : topSongs) { + result.getSong().add(createJaxbChild(player, topSong, username)); + } + + Response res = createResponse(); + res.setTopSongs(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getArtistInfo(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + int count = getIntParameter(request, "count", 20); + boolean includeNotPresent = ServletRequestUtils.getBooleanParameter(request, "includeNotPresent", false); + + ArtistInfo result = new ArtistInfo(); + + MediaFile mediaFile = mediaFileService.getMediaFile(id); + if (mediaFile == null) { + error(request, response, ErrorCode.NOT_FOUND, "Media file not found."); + return; + } + List musicFolders = settingsService.getMusicFoldersForUser(username); + List similarArtists = lastFmService.getSimilarArtists(mediaFile, count, includeNotPresent, musicFolders); + for (MediaFile similarArtist : similarArtists) { + result.getSimilarArtist().add(createJaxbArtist(similarArtist, username)); + } + ArtistBio artistBio = lastFmService.getArtistBio(mediaFile); + if (artistBio != null) { + result.setBiography(artistBio.getBiography()); + result.setMusicBrainzId(artistBio.getMusicBrainzId()); + result.setLastFmUrl(artistBio.getLastFmUrl()); + result.setSmallImageUrl(artistBio.getSmallImageUrl()); + result.setMediumImageUrl(artistBio.getMediumImageUrl()); + result.setLargeImageUrl(artistBio.getLargeImageUrl()); + } + + Response res = createResponse(); + res.setArtistInfo(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getArtistInfo2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + int count = getIntParameter(request, "count", 20); + boolean includeNotPresent = ServletRequestUtils.getBooleanParameter(request, "includeNotPresent", false); + + ArtistInfo2 result = new ArtistInfo2(); + + Artist artist = artistDao.getArtist(id); + if (artist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Artist not found."); + return; + } + + List musicFolders = settingsService.getMusicFoldersForUser(username); + List similarArtists = lastFmService.getSimilarArtists(artist, count, includeNotPresent, musicFolders); + for (Artist similarArtist : similarArtists) { + result.getSimilarArtist().add(createJaxbArtist(new ArtistID3(), similarArtist, username)); + } + ArtistBio artistBio = lastFmService.getArtistBio(artist); + if (artistBio != null) { + result.setBiography(artistBio.getBiography()); + result.setMusicBrainzId(artistBio.getMusicBrainzId()); + result.setLastFmUrl(artistBio.getLastFmUrl()); + result.setSmallImageUrl(artistBio.getSmallImageUrl()); + result.setMediumImageUrl(artistBio.getMediumImageUrl()); + result.setLargeImageUrl(artistBio.getLargeImageUrl()); + } + + Response res = createResponse(); + res.setArtistInfo2(result); + jaxbWriter.writeResponse(request, response, res); + } + + private T createJaxbArtist(T jaxbArtist, Artist artist, String username) { + jaxbArtist.setId(String.valueOf(artist.getId())); + jaxbArtist.setName(artist.getName()); + jaxbArtist.setStarred(jaxbWriter.convertDate(mediaFileDao.getMediaFileStarredDate(artist.getId(), username))); + jaxbArtist.setAlbumCount(artist.getAlbumCount()); + if (artist.getCoverArtPath() != null) { + jaxbArtist.setCoverArt(CoverArtController.ARTIST_COVERART_PREFIX + artist.getId()); + } + return jaxbArtist; + } + + private org.subsonic.restapi.Artist createJaxbArtist(MediaFile artist, String username) { + org.subsonic.restapi.Artist result = new org.subsonic.restapi.Artist(); + result.setId(String.valueOf(artist.getId())); + result.setName(artist.getArtist()); + Date starred = mediaFileDao.getMediaFileStarredDate(artist.getId(), username); + result.setStarred(jaxbWriter.convertDate(starred)); + return result; + } + + public void getArtist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + String username = securityService.getCurrentUsername(request); + int id = getRequiredIntParameter(request, "id"); + Artist artist = artistDao.getArtist(id); + if (artist == null) { + error(request, response, ErrorCode.NOT_FOUND, "Artist not found."); + return; + } + + List musicFolders = settingsService.getMusicFoldersForUser(username); + ArtistWithAlbumsID3 result = createJaxbArtist(new ArtistWithAlbumsID3(), artist, username); + for (Album album : albumDao.getAlbumsForArtist(artist.getName(), musicFolders)) { + result.getAlbum().add(createJaxbAlbum(new AlbumID3(), album, username)); + } + + Response res = createResponse(); + res.setArtist(result); + jaxbWriter.writeResponse(request, response, res); + } + + private T createJaxbAlbum(T jaxbAlbum, Album album, String username) { + jaxbAlbum.setId(String.valueOf(album.getId())); + jaxbAlbum.setName(album.getName()); + if (album.getArtist() != null) { + jaxbAlbum.setArtist(album.getArtist()); + Artist artist = artistDao.getArtist(album.getArtist()); + if (artist != null) { + jaxbAlbum.setArtistId(String.valueOf(artist.getId())); + } + } + if (album.getCoverArtPath() != null) { + jaxbAlbum.setCoverArt(CoverArtController.ALBUM_COVERART_PREFIX + album.getId()); + } + jaxbAlbum.setSongCount(album.getSongCount()); + jaxbAlbum.setDuration(album.getDurationSeconds()); + jaxbAlbum.setCreated(jaxbWriter.convertDate(album.getCreated())); + jaxbAlbum.setStarred(jaxbWriter.convertDate(albumDao.getAlbumStarredDate(album.getId(), username))); + jaxbAlbum.setYear(album.getYear()); + jaxbAlbum.setGenre(album.getGenre()); + return jaxbAlbum; + } + + private T createJaxbPlaylist(T jaxbPlaylist, Playlist playlist) { + jaxbPlaylist.setId(String.valueOf(playlist.getId())); + jaxbPlaylist.setName(playlist.getName()); + jaxbPlaylist.setComment(playlist.getComment()); + jaxbPlaylist.setOwner(playlist.getUsername()); + jaxbPlaylist.setPublic(playlist.isShared()); + jaxbPlaylist.setSongCount(playlist.getFileCount()); + jaxbPlaylist.setDuration(playlist.getDurationSeconds()); + jaxbPlaylist.setCreated(jaxbWriter.convertDate(playlist.getCreated())); + jaxbPlaylist.setChanged(jaxbWriter.convertDate(playlist.getChanged())); + jaxbPlaylist.setCoverArt(CoverArtController.PLAYLIST_COVERART_PREFIX + playlist.getId()); + + for (String username : playlistService.getPlaylistUsers(playlist.getId())) { + jaxbPlaylist.getAllowedUser().add(username); + } + return jaxbPlaylist; + } + + @SuppressWarnings("UnusedDeclaration") + public void getAlbum(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + Album album = albumDao.getAlbum(id); + if (album == null) { + error(request, response, ErrorCode.NOT_FOUND, "Album not found."); + return; + } + + AlbumWithSongsID3 result = createJaxbAlbum(new AlbumWithSongsID3(), album, username); + for (MediaFile mediaFile : mediaFileDao.getSongsForAlbum(album.getArtist(), album.getName())) { + result.getSong().add(createJaxbChild(player, mediaFile, username)); + } + + Response res = createResponse(); + res.setAlbum(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getSong(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + MediaFile song = mediaFileDao.getMediaFile(id); + if (song == null || song.isDirectory()) { + error(request, response, ErrorCode.NOT_FOUND, "Song not found."); + return; + } + if (!securityService.isFolderAccessAllowed(song, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Access denied"); + return; + } + + Response res = createResponse(); + res.setSong(createJaxbChild(player, song, username)); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getMusicDirectory(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int id = getRequiredIntParameter(request, "id"); + MediaFile dir = mediaFileService.getMediaFile(id); + if (dir == null) { + error(request, response, ErrorCode.NOT_FOUND, "Directory not found"); + return; + } + if (!securityService.isFolderAccessAllowed(dir, username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Access denied"); + return; + } + + MediaFile parent = mediaFileService.getParentOf(dir); + Directory directory = new Directory(); + directory.setId(String.valueOf(id)); + try { + if (!mediaFileService.isRoot(parent)) { + directory.setParent(String.valueOf(parent.getId())); + } + } catch (SecurityException x) { + // Ignored. + } + directory.setName(dir.getName()); + directory.setStarred(jaxbWriter.convertDate(mediaFileDao.getMediaFileStarredDate(id, username))); + + if (dir.isAlbum()) { + directory.setAverageRating(ratingService.getAverageRating(dir)); + directory.setUserRating(ratingService.getRatingForUser(username, dir)); + } + + for (MediaFile child : mediaFileService.getChildrenOf(dir, true, true, true)) { + directory.getChild().add(createJaxbChild(player, child, username)); + } + + Response res = createResponse(); + res.setDirectory(directory); + jaxbWriter.writeResponse(request, response, res); + } + + @Deprecated + public void search(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + 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(getIntParameter(request, "count", 20)); + criteria.setOffset(getIntParameter(request, "offset", 0)); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + SearchResult result = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG); + org.subsonic.restapi.SearchResult searchResult = new org.subsonic.restapi.SearchResult(); + searchResult.setOffset(result.getOffset()); + searchResult.setTotalHits(result.getTotalHits()); + + for (MediaFile mediaFile : result.getMediaFiles()) { + searchResult.getMatch().add(createJaxbChild(player, mediaFile, username)); + } + Response res = createResponse(); + res.setSearchResult(searchResult); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void search2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + SearchResult2 searchResult = new SearchResult2(); + + String query = request.getParameter("query"); + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(StringUtils.trimToEmpty(query)); + criteria.setCount(getIntParameter(request, "artistCount", 20)); + criteria.setOffset(getIntParameter(request, "artistOffset", 0)); + SearchResult artists = searchService.search(criteria, musicFolders, SearchService.IndexType.ARTIST); + for (MediaFile mediaFile : artists.getMediaFiles()) { + searchResult.getArtist().add(createJaxbArtist(mediaFile, username)); + } + + criteria.setCount(getIntParameter(request, "albumCount", 20)); + criteria.setOffset(getIntParameter(request, "albumOffset", 0)); + SearchResult albums = searchService.search(criteria, musicFolders, SearchService.IndexType.ALBUM); + for (MediaFile mediaFile : albums.getMediaFiles()) { + searchResult.getAlbum().add(createJaxbChild(player, mediaFile, username)); + } + + criteria.setCount(getIntParameter(request, "songCount", 20)); + criteria.setOffset(getIntParameter(request, "songOffset", 0)); + SearchResult songs = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG); + for (MediaFile mediaFile : songs.getMediaFiles()) { + searchResult.getSong().add(createJaxbChild(player, mediaFile, username)); + } + + Response res = createResponse(); + res.setSearchResult2(searchResult); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void search3(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + SearchResult3 searchResult = new SearchResult3(); + + String query = request.getParameter("query"); + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(StringUtils.trimToEmpty(query)); + criteria.setCount(getIntParameter(request, "artistCount", 20)); + criteria.setOffset(getIntParameter(request, "artistOffset", 0)); + SearchResult result = searchService.search(criteria, musicFolders, SearchService.IndexType.ARTIST_ID3); + for (Artist artist : result.getArtists()) { + searchResult.getArtist().add(createJaxbArtist(new ArtistID3(), artist, username)); + } + + criteria.setCount(getIntParameter(request, "albumCount", 20)); + criteria.setOffset(getIntParameter(request, "albumOffset", 0)); + result = searchService.search(criteria, musicFolders, SearchService.IndexType.ALBUM_ID3); + for (Album album : result.getAlbums()) { + searchResult.getAlbum().add(createJaxbAlbum(new AlbumID3(), album, username)); + } + + criteria.setCount(getIntParameter(request, "songCount", 20)); + criteria.setOffset(getIntParameter(request, "songOffset", 0)); + result = searchService.search(criteria, musicFolders, SearchService.IndexType.SONG); + for (MediaFile song : result.getMediaFiles()) { + searchResult.getSong().add(createJaxbChild(player, song, username)); + } + + Response res = createResponse(); + res.setSearchResult3(searchResult); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getPlaylists(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + 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; + } + + Playlists result = new Playlists(); + + for (Playlist playlist : playlistService.getReadablePlaylistsForUser(requestedUsername)) { + result.getPlaylist().add(createJaxbPlaylist(new org.subsonic.restapi.Playlist(), playlist)); + } + + Response res = createResponse(); + res.setPlaylists(result); + jaxbWriter.writeResponse(request, response, res); + } + + public void getPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int id = 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; + } + PlaylistWithSongs result = createJaxbPlaylist(new PlaylistWithSongs(), playlist); + for (MediaFile mediaFile : playlistService.getFilesInPlaylist(id)) { + if (securityService.isFolderAccessAllowed(mediaFile, username)) { + result.getEntry().add(createJaxbChild(player, mediaFile, username)); + } + } + + Response res = createResponse(); + res.setPlaylist(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + 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; + } + + boolean returnPlaylist = false; + String action = 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 = getRequiredIntParameter(request, "index"); + int offset = getIntParameter(request, "offset", 0); + playQueueService.doSkip(request, response, index, offset); + } else if ("add".equals(action)) { + int[] ids = getIntParameters(request, "id"); + playQueueService.doAdd(request, response, ids, null); + } else if ("set".equals(action)) { + int[] ids = getIntParameters(request, "id"); + playQueueService.doSet(request, response, ids); + } else if ("clear".equals(action)) { + playQueueService.doClear(request, response); + } else if ("remove".equals(action)) { + int index = getRequiredIntParameter(request, "index"); + playQueueService.doRemove(request, response, index); + } else if ("shuffle".equals(action)) { + playQueueService.doShuffle(request, response); + } else if ("setGain".equals(action)) { + float gain = 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 + "'."); + } + + 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(); + + + int currentIndex = controlsJukebox && !playQueue.isEmpty() ? playQueue.getIndex() : -1; + boolean playing = controlsJukebox && !playQueue.isEmpty() && playQueue.getStatus() == PlayQueue.Status.PLAYING; + float gain = jukeboxService.getGain(); + int position = controlsJukebox && !playQueue.isEmpty() ? jukeboxService.getPosition() : 0; + + Response res = createResponse(); + if (returnPlaylist) { + JukeboxPlaylist result = new JukeboxPlaylist(); + res.setJukeboxPlaylist(result); + result.setCurrentIndex(currentIndex); + result.setPlaying(playing); + result.setGain(gain); + result.setPosition(position); + for (MediaFile mediaFile : playQueue.getFiles()) { + result.getEntry().add(createJaxbChild(player, mediaFile, username)); + } + } else { + JukeboxStatus result = new JukeboxStatus(); + res.setJukeboxStatus(result); + result.setCurrentIndex(currentIndex); + result.setPlaying(playing); + result.setGain(gain); + result.setPosition(position); + } + + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void createPlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + Integer playlistId = 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.setShared(false); + playlist.setUsername(username); + playlistService.createPlaylist(playlist); + } + + List songs = new ArrayList(); + for (int id : getIntParameters(request, "songId")) { + MediaFile song = mediaFileService.getMediaFile(id); + if (song != null) { + songs.add(song); + } + } + playlistService.setFilesInPlaylist(playlist.getId(), songs); + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void updatePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + int id = 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 shared = getBooleanParameter(request, "public"); + if (shared != null) { + playlist.setShared(shared); + } + 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 songs = playlistService.getFilesInPlaylist(id); + boolean songsChanged = false; + + SortedSet tmp = new TreeSet(); + for (int songIndexToRemove : getIntParameters(request, "songIndexToRemove")) { + tmp.add(songIndexToRemove); + } + List songIndexesToRemove = new ArrayList(tmp); + Collections.reverse(songIndexesToRemove); + for (Integer songIndexToRemove : songIndexesToRemove) { + songs.remove(songIndexToRemove.intValue()); + songsChanged = true; + } + for (int songToAdd : getIntParameters(request, "songIdToAdd")) { + MediaFile song = mediaFileService.getMediaFile(songToAdd); + if (song != null) { + songs.add(song); + songsChanged = true; + } + } + if (songsChanged) { + playlistService.setFilesInPlaylist(id, songs); + } + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void deletePlaylist(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request, true); + String username = securityService.getCurrentUsername(request); + + int id = 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); + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void getAlbumList(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int size = getIntParameter(request, "size", 10); + int offset = getIntParameter(request, "offset", 0); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + size = Math.max(0, Math.min(size, 500)); + String type = getRequiredStringParameter(request, "type"); + + List albums; + if ("highest".equals(type)) { + albums = ratingService.getHighestRatedAlbums(offset, size, musicFolders); + } else if ("frequent".equals(type)) { + albums = mediaFileService.getMostFrequentlyPlayedAlbums(offset, size, musicFolders); + } else if ("recent".equals(type)) { + albums = mediaFileService.getMostRecentlyPlayedAlbums(offset, size, musicFolders); + } else if ("newest".equals(type)) { + albums = mediaFileService.getNewestAlbums(offset, size, musicFolders); + } else if ("starred".equals(type)) { + albums = mediaFileService.getStarredAlbums(offset, size, username, musicFolders); + } else if ("alphabeticalByArtist".equals(type)) { + albums = mediaFileService.getAlphabeticalAlbums(offset, size, true, musicFolders); + } else if ("alphabeticalByName".equals(type)) { + albums = mediaFileService.getAlphabeticalAlbums(offset, size, false, musicFolders); + } else if ("byGenre".equals(type)) { + albums = mediaFileService.getAlbumsByGenre(offset, size, getRequiredStringParameter(request, "genre"), musicFolders); + } else if ("byYear".equals(type)) { + albums = mediaFileService.getAlbumsByYear(offset, size, getRequiredIntParameter(request, "fromYear"), + getRequiredIntParameter(request, "toYear"), musicFolders); + } else if ("random".equals(type)) { + albums = searchService.getRandomAlbums(size, musicFolders); + } else { + throw new Exception("Invalid list type: " + type); + } + + AlbumList result = new AlbumList(); + for (MediaFile album : albums) { + result.getAlbum().add(createJaxbChild(player, album, username)); + } + + Response res = createResponse(); + res.setAlbumList(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getAlbumList2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + int size = getIntParameter(request, "size", 10); + int offset = getIntParameter(request, "offset", 0); + size = Math.max(0, Math.min(size, 500)); + String type = getRequiredStringParameter(request, "type"); + String username = securityService.getCurrentUsername(request); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + List albums; + if ("frequent".equals(type)) { + albums = albumDao.getMostFrequentlyPlayedAlbums(offset, size, musicFolders); + } else if ("recent".equals(type)) { + albums = albumDao.getMostRecentlyPlayedAlbums(offset, size, musicFolders); + } else if ("newest".equals(type)) { + albums = albumDao.getNewestAlbums(offset, size, musicFolders); + } else if ("alphabeticalByArtist".equals(type)) { + albums = albumDao.getAlphabetialAlbums(offset, size, true, musicFolders); + } else if ("alphabeticalByName".equals(type)) { + albums = albumDao.getAlphabetialAlbums(offset, size, false, musicFolders); + } else if ("byGenre".equals(type)) { + albums = albumDao.getAlbumsByGenre(offset, size, getRequiredStringParameter(request, "genre"), musicFolders); + } else if ("byYear".equals(type)) { + albums = albumDao.getAlbumsByYear(offset, size, getRequiredIntParameter(request, "fromYear"), + getRequiredIntParameter(request, "toYear"), musicFolders); + } else if ("starred".equals(type)) { + albums = albumDao.getStarredAlbums(offset, size, securityService.getCurrentUser(request).getUsername(), musicFolders); + } else if ("random".equals(type)) { + albums = searchService.getRandomAlbumsId3(size, musicFolders); + } else { + throw new Exception("Invalid list type: " + type); + } + AlbumList2 result = new AlbumList2(); + for (Album album : albums) { + result.getAlbum().add(createJaxbAlbum(new AlbumID3(), album, username)); + } + Response res = createResponse(); + res.setAlbumList2(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getRandomSongs(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int size = getIntParameter(request, "size", 10); + size = Math.max(0, Math.min(size, 500)); + String genre = getStringParameter(request, "genre"); + Integer fromYear = getIntParameter(request, "fromYear"); + Integer toYear = getIntParameter(request, "toYear"); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolders); + + Songs result = new Songs(); + for (MediaFile mediaFile : searchService.getRandomSongs(criteria)) { + result.getSong().add(createJaxbChild(player, mediaFile, username)); + } + Response res = createResponse(); + res.setRandomSongs(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getVideos(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int size = getIntParameter(request, "size", Integer.MAX_VALUE); + int offset = getIntParameter(request, "offset", 0); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + Videos result = new Videos(); + for (MediaFile mediaFile : mediaFileDao.getVideos(size, offset, musicFolders)) { + result.getVideo().add(createJaxbChild(player, mediaFile, username)); + } + Response res = createResponse(); + res.setVideos(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getNowPlaying(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + NowPlaying result = new NowPlaying(); + + for (PlayStatus status : statusService.getPlayStatuses()) { + + Player player = status.getPlayer(); + MediaFile mediaFile = status.getMediaFile(); + String username = player.getUsername(); + if (username == null) { + continue; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + if (!userSettings.isNowPlayingAllowed()) { + continue; + } + + long minutesAgo = status.getMinutesAgo(); + if (minutesAgo < 60) { + NowPlayingEntry entry = new NowPlayingEntry(); + entry.setUsername(username); + entry.setPlayerId(Integer.parseInt(player.getId())); + entry.setPlayerName(player.getName()); + entry.setMinutesAgo((int) minutesAgo); + result.getEntry().add(createJaxbChild(entry, player, mediaFile, username)); + } + } + + Response res = createResponse(); + res.setNowPlaying(result); + jaxbWriter.writeResponse(request, response, res); + } + + private Child createJaxbChild(Player player, MediaFile mediaFile, String username) { + return createJaxbChild(new Child(), player, mediaFile, username); + } + + private T createJaxbChild(T child, Player player, MediaFile mediaFile, String username) { + MediaFile parent = mediaFileService.getParentOf(mediaFile); + child.setId(String.valueOf(mediaFile.getId())); + try { + if (!mediaFileService.isRoot(parent)) { + child.setParent(String.valueOf(parent.getId())); + } + } catch (SecurityException x) { + // Ignored. + } + child.setTitle(mediaFile.getName()); + child.setAlbum(mediaFile.getAlbumName()); + child.setArtist(mediaFile.getArtist()); + child.setIsDir(mediaFile.isDirectory()); + child.setCoverArt(findCoverArt(mediaFile, parent)); + child.setYear(mediaFile.getYear()); + child.setGenre(mediaFile.getGenre()); + child.setCreated(jaxbWriter.convertDate(mediaFile.getCreated())); + child.setStarred(jaxbWriter.convertDate(mediaFileDao.getMediaFileStarredDate(mediaFile.getId(), username))); + child.setUserRating(ratingService.getRatingForUser(username, mediaFile)); + child.setAverageRating(ratingService.getAverageRating(mediaFile)); + + if (mediaFile.isFile()) { + child.setDuration(mediaFile.getDurationSeconds()); + child.setBitRate(mediaFile.getBitRate()); + child.setTrack(mediaFile.getTrackNumber()); + child.setDiscNumber(mediaFile.getDiscNumber()); + child.setSize(mediaFile.getFileSize()); + String suffix = mediaFile.getFormat(); + child.setSuffix(suffix); + child.setContentType(StringUtil.getMimeType(suffix)); + child.setIsVideo(mediaFile.isVideo()); + child.setPath(getRelativePath(mediaFile)); + + Bookmark bookmark = bookmarkCache.get(new BookmarkKey(username, mediaFile.getId())); + if (bookmark != null) { + child.setBookmarkPosition(bookmark.getPositionMillis()); + } + + if (mediaFile.getAlbumArtist() != null && mediaFile.getAlbumName() != null) { + Album album = albumDao.getAlbum(mediaFile.getAlbumArtist(), mediaFile.getAlbumName()); + if (album != null) { + child.setAlbumId(String.valueOf(album.getId())); + } + } + if (mediaFile.getArtist() != null) { + Artist artist = artistDao.getArtist(mediaFile.getArtist()); + if (artist != null) { + child.setArtistId(String.valueOf(artist.getId())); + } + } + switch (mediaFile.getMediaType()) { + case MUSIC: + child.setType(MediaType.MUSIC); + break; + case PODCAST: + child.setType(MediaType.PODCAST); + break; + case AUDIOBOOK: + child.setType(MediaType.AUDIOBOOK); + break; + case VIDEO: + child.setType(MediaType.VIDEO); + child.setOriginalWidth(mediaFile.getWidth()); + child.setOriginalHeight(mediaFile.getHeight()); + break; + default: + break; + } + + if (transcodingService.isTranscodingRequired(mediaFile, player)) { + String transcodedSuffix = transcodingService.getSuffix(player, mediaFile, null); + child.setTranscodedSuffix(transcodedSuffix); + child.setTranscodedContentType(StringUtil.getMimeType(transcodedSuffix)); + } + } + return child; + } + + private String findCoverArt(MediaFile mediaFile, MediaFile parent) { + MediaFile dir = mediaFile.isDirectory() ? mediaFile : parent; + if (dir != null && dir.getCoverArtPath() != null) { + return String.valueOf(dir.getId()); + } + return null; + } + + private String getRelativePath(MediaFile musicFile) { + + String filePath = musicFile.getPath(); + + // Convert slashes. + filePath = filePath.replace('\\', '/'); + + String filePathLower = filePath.toLowerCase(); + + List musicFolders = settingsService.getAllMusicFolders(false, true); + for (MusicFolder musicFolder : musicFolders) { + String folderPath = musicFolder.getPath().getPath(); + folderPath = folderPath.replace('\\', '/'); + String folderPathLower = folderPath.toLowerCase(); + if (!folderPathLower.endsWith("/")) { + folderPathLower += "/"; + } + + 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 ModelAndView hls(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; + } + int id = getRequiredIntParameter(request, "id"); + MediaFile video = mediaFileDao.getMediaFile(id); + if (video == null || video.isDirectory()) { + error(request, response, ErrorCode.NOT_FOUND, "Video not found."); + return null; + } + if (!securityService.isFolderAccessAllowed(video, user.getUsername())) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Access denied"); + return null; + } + hlsController.handleRequest(request, response); + return null; + } + + @SuppressWarnings("UnusedDeclaration") + public void scrobble(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + Player player = playerService.getPlayer(request, response); + + boolean submission = getBooleanParameter(request, "submission", true); + int[] ids = getRequiredIntParameters(request, "id"); + long[] times = getLongParameters(request, "time"); + if (times.length > 0 && times.length != ids.length) { + error(request, response, ErrorCode.GENERIC, "Wrong number of timestamps: " + times.length); + return; + } + + for (int i = 0; i < ids.length; i++) { + int id = ids[i]; + MediaFile file = mediaFileService.getMediaFile(id); + if (file == null) { + LOG.warn("File to scrobble not found: " + id); + continue; + } + Date time = times.length == 0 ? null : new Date(times[i]); + + statusService.addRemotePlay(new PlayStatus(file, player, time == null ? new Date() : time)); + mediaFileService.incrementPlayCount(file); + if (settingsService.getUserSettings(player.getUsername()).isLastFmEnabled()) { + audioScrobblerService.register(file, player.getUsername(), submission, time); + } + } + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void star(HttpServletRequest request, HttpServletResponse response) throws Exception { + starOrUnstar(request, response, true); + } + + @SuppressWarnings("UnusedDeclaration") + 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); + + String username = securityService.getCurrentUser(request).getUsername(); + for (int id : 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 : 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 : 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); + } + } + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void getStarred(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + Starred result = new Starred(); + for (MediaFile artist : mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username, musicFolders)) { + result.getArtist().add(createJaxbArtist(artist, username)); + } + for (MediaFile album : mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username, musicFolders)) { + result.getAlbum().add(createJaxbChild(player, album, username)); + } + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders)) { + result.getSong().add(createJaxbChild(player, song, username)); + } + Response res = createResponse(); + res.setStarred(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getStarred2(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + Integer musicFolderId = getIntParameter(request, "musicFolderId"); + List musicFolders = settingsService.getMusicFoldersForUser(username, musicFolderId); + + Starred2 result = new Starred2(); + for (Artist artist : artistDao.getStarredArtists(0, Integer.MAX_VALUE, username, musicFolders)) { + result.getArtist().add(createJaxbArtist(new ArtistID3(), artist, username)); + } + for (Album album : albumDao.getStarredAlbums(0, Integer.MAX_VALUE, username, musicFolders)) { + result.getAlbum().add(createJaxbAlbum(new AlbumID3(), album, username)); + } + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders)) { + result.getSong().add(createJaxbChild(player, song, username)); + } + Response res = createResponse(); + res.setStarred2(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getPodcasts(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + boolean includeEpisodes = getBooleanParameter(request, "includeEpisodes", true); + Integer channelId = getIntParameter(request, "id"); + + Podcasts result = new Podcasts(); + + for (PodcastChannel channel : podcastService.getAllChannels()) { + if (channelId == null || channelId.equals(channel.getId())) { + + org.subsonic.restapi.PodcastChannel c = new org.subsonic.restapi.PodcastChannel(); + result.getChannel().add(c); + + c.setId(String.valueOf(channel.getId())); + c.setUrl(channel.getUrl()); + c.setStatus(PodcastStatus.valueOf(channel.getStatus().name())); + c.setTitle(channel.getTitle()); + c.setDescription(channel.getDescription()); + c.setCoverArt(CoverArtController.PODCAST_COVERART_PREFIX + channel.getId()); + c.setOriginalImageUrl(channel.getImageUrl()); + c.setErrorMessage(channel.getErrorMessage()); + + if (includeEpisodes) { + List episodes = podcastService.getEpisodes(channel.getId()); + for (PodcastEpisode episode : episodes) { + c.getEpisode().add(createJaxbPodcastEpisode(player, username, episode)); + } + } + } + } + Response res = createResponse(); + res.setPodcasts(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getNewestPodcasts(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + int count = getIntParameter(request, "count", 20); + NewestPodcasts result = new NewestPodcasts(); + + for (PodcastEpisode episode : podcastService.getNewestEpisodes(count)) { + result.getEpisode().add(createJaxbPodcastEpisode(player, username, episode)); + } + + Response res = createResponse(); + res.setNewestPodcasts(result); + jaxbWriter.writeResponse(request, response, res); + } + + private org.subsonic.restapi.PodcastEpisode createJaxbPodcastEpisode(Player player, String username, PodcastEpisode episode) { + org.subsonic.restapi.PodcastEpisode e = new org.subsonic.restapi.PodcastEpisode(); + + String path = episode.getPath(); + if (path != null) { + MediaFile mediaFile = mediaFileService.getMediaFile(path); + e = createJaxbChild(new org.subsonic.restapi.PodcastEpisode(), player, mediaFile, username); + e.setStreamId(String.valueOf(mediaFile.getId())); + } + + e.setId(String.valueOf(episode.getId())); // Overwrites the previous "id" attribute. + e.setChannelId(String.valueOf(episode.getChannelId())); + e.setStatus(PodcastStatus.valueOf(episode.getStatus().name())); + e.setTitle(episode.getTitle()); + e.setDescription(episode.getDescription()); + e.setPublishDate(jaxbWriter.convertDate(episode.getPublishDate())); + return e; + } + + @SuppressWarnings("UnusedDeclaration") + public void refreshPodcasts(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isPodcastRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to administrate podcasts."); + return; + } + podcastService.refreshAllChannels(true); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void createPodcastChannel(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isPodcastRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to administrate podcasts."); + return; + } + + String url = getRequiredStringParameter(request, "url"); + podcastService.createChannel(url); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void deletePodcastChannel(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isPodcastRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to administrate podcasts."); + return; + } + + int id = getRequiredIntParameter(request, "id"); + podcastService.deleteChannel(id); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void deletePodcastEpisode(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isPodcastRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to administrate podcasts."); + return; + } + + int id = getRequiredIntParameter(request, "id"); + podcastService.deleteEpisode(id, true); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void downloadPodcastEpisode(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + if (!user.isPodcastRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, user.getUsername() + " is not authorized to administrate podcasts."); + return; + } + + int id = getRequiredIntParameter(request, "id"); + PodcastEpisode episode = podcastService.getEpisode(id, true); + if (episode == null) { + error(request, response, ErrorCode.NOT_FOUND, "Podcast episode " + id + " not found."); + return; + } + + podcastService.downloadEpisode(episode); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void getInternetRadioStations(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + InternetRadioStations result = new InternetRadioStations(); + for (InternetRadio radio : settingsService.getAllInternetRadios()) { + InternetRadioStation i = new InternetRadioStation(); + i.setId(String.valueOf(radio.getId())); + i.setName(radio.getName()); + i.setStreamUrl(radio.getStreamUrl()); + i.setHomePageUrl(radio.getHomepageUrl()); + result.getInternetRadioStation().add(i); + } + Response res = createResponse(); + res.setInternetRadioStations(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getBookmarks(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Player player = playerService.getPlayer(request, response); + String username = securityService.getCurrentUsername(request); + + Bookmarks result = new Bookmarks(); + for (Bookmark bookmark : bookmarkDao.getBookmarks(username)) { + org.subsonic.restapi.Bookmark b = new org.subsonic.restapi.Bookmark(); + result.getBookmark().add(b); + b.setPosition(bookmark.getPositionMillis()); + b.setUsername(bookmark.getUsername()); + b.setComment(bookmark.getComment()); + b.setCreated(jaxbWriter.convertDate(bookmark.getCreated())); + b.setChanged(jaxbWriter.convertDate(bookmark.getChanged())); + + MediaFile mediaFile = mediaFileService.getMediaFile(bookmark.getMediaFileId()); + b.setEntry(createJaxbChild(player, mediaFile, username)); + } + + Response res = createResponse(); + res.setBookmarks(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void createBookmark(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + int mediaFileId = getRequiredIntParameter(request, "id"); + long position = getRequiredLongParameter(request, "position"); + String comment = request.getParameter("comment"); + Date now = new Date(); + + Bookmark bookmark = new Bookmark(0, mediaFileId, position, username, comment, now, now); + bookmarkDao.createOrUpdateBookmark(bookmark); + refreshBookmarkCache(); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void deleteBookmark(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + String username = securityService.getCurrentUsername(request); + int mediaFileId = getRequiredIntParameter(request, "id"); + bookmarkDao.deleteBookmark(username, mediaFileId); + refreshBookmarkCache(); + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void getPlayQueue(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + Player player = playerService.getPlayer(request, response); + + SavedPlayQueue playQueue = playQueueDao.getPlayQueue(username); + if (playQueue == null) { + writeEmptyResponse(request, response); + return; + } + + org.subsonic.restapi.PlayQueue restPlayQueue = new org.subsonic.restapi.PlayQueue(); + restPlayQueue.setUsername(playQueue.getUsername()); + restPlayQueue.setCurrent(playQueue.getCurrentMediaFileId()); + restPlayQueue.setPosition(playQueue.getPositionMillis()); + restPlayQueue.setChanged(jaxbWriter.convertDate(playQueue.getChanged())); + restPlayQueue.setChangedBy(playQueue.getChangedBy()); + + for (Integer mediaFileId : playQueue.getMediaFileIds()) { + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + if (mediaFile != null) { + restPlayQueue.getEntry().add(createJaxbChild(player, mediaFile, username)); + } + } + + Response res = createResponse(); + res.setPlayQueue(restPlayQueue); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void savePlayQueue(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + String username = securityService.getCurrentUsername(request); + List mediaFileIds = Util.toIntegerList(getIntParameters(request, "id")); + Integer current = getIntParameter(request, "current"); + Long position = getLongParameter(request, "position"); + Date changed = new Date(); + String changedBy = getRequiredStringParameter(request, "c"); + + if (!mediaFileIds.contains(current)) { + error(request, response, ErrorCode.GENERIC, "Current track is not included in play queue"); + return; + } + + SavedPlayQueue playQueue = new SavedPlayQueue(null, username, mediaFileIds, current, position, changed, changedBy); + playQueueDao.savePlayQueue(playQueue); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + 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); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + Shares result = new Shares(); + for (Share share : shareService.getSharesForUser(user)) { + org.subsonic.restapi.Share s = createJaxbShare(share); + result.getShare().add(s); + + for (MediaFile mediaFile : shareService.getSharedFiles(share.getId(), musicFolders)) { + s.getEntry().add(createJaxbChild(player, mediaFile, username)); + } + } + Response res = createResponse(); + res.setShares(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + 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; + } + + List files = new ArrayList(); + for (int id : getRequiredIntParameters(request, "id")) { + files.add(mediaFileService.getMediaFile(id)); + } + + Share share = shareService.createShare(request, files); + share.setDescription(request.getParameter("description")); + long expires = getLongParameter(request, "expires", 0L); + if (expires != 0) { + share.setExpires(new Date(expires)); + } + shareService.updateShare(share); + + Shares result = new Shares(); + org.subsonic.restapi.Share s = createJaxbShare(share); + result.getShare().add(s); + + List musicFolders = settingsService.getMusicFoldersForUser(username); + + for (MediaFile mediaFile : shareService.getSharedFiles(share.getId(), musicFolders)) { + s.getEntry().add(createJaxbChild(player, mediaFile, username)); + } + + Response res = createResponse(); + res.setShares(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void deleteShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + int id = 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); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void updateShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + User user = securityService.getCurrentUser(request); + int id = 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); + writeEmptyResponse(request, response); + } + + private org.subsonic.restapi.Share createJaxbShare(Share share) { + org.subsonic.restapi.Share result = new org.subsonic.restapi.Share(); + result.setId(String.valueOf(share.getId())); + result.setUrl(shareService.getShareUrl(share)); + result.setUsername(share.getUsername()); + result.setCreated(jaxbWriter.convertDate(share.getCreated())); + result.setVisitCount(share.getVisitCount()); + result.setDescription(share.getDescription()); + result.setExpires(jaxbWriter.convertDate(share.getExpires())); + result.setLastVisited(jaxbWriter.convertDate(share.getLastVisited())); + return result; + } + + @SuppressWarnings("UnusedParameters") + public ModelAndView videoPlayer(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + Map map = new HashMap(); + int id = getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + + int timeOffset = 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", getIntParameter(request, "maxBitRate", VideoPlayerController.DEFAULT_BIT_RATE)); + map.put("duration", duration); + map.put("timeOffset", timeOffset); + map.put("bitRates", VideoPlayerController.BIT_RATES); + map.put("autoplay", getBooleanParameter(request, "autoplay", true)); + + ModelAndView result = new ModelAndView("rest/videoPlayer"); + result.addObject("model", map); + return result; + } + + @SuppressWarnings("UnusedDeclaration") + public ModelAndView getCoverArt(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + return coverArtController.handleRequest(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public ModelAndView getAvatar(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + return avatarController.handleRequest(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void changePassword(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + String username = getRequiredStringParameter(request, "username"); + String password = decrypt(getRequiredStringParameter(request, "password")); + + User authUser = securityService.getCurrentUser(request); + + boolean allowed = authUser.isAdminRole() + || username.equals(authUser.getUsername()) && authUser.isSettingsRole(); + + if (!allowed) { + 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); + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void getUser(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + String username = getRequiredStringParameter(request, "username"); + + 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; + } + + Response res = createResponse(); + res.setUser(createJaxbUser(requestedUser)); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void getUsers(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + + User currentUser = securityService.getCurrentUser(request); + if (!currentUser.isAdminRole()) { + error(request, response, ErrorCode.NOT_AUTHORIZED, currentUser.getUsername() + " is not authorized to get details for other users."); + return; + } + + Users result = new Users(); + for (User user : securityService.getAllUsers()) { + result.getUser().add(createJaxbUser(user)); + } + + Response res = createResponse(); + res.setUsers(result); + jaxbWriter.writeResponse(request, response, res); + } + + private org.subsonic.restapi.User createJaxbUser(User user) { + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + + org.subsonic.restapi.User result = new org.subsonic.restapi.User(); + result.setUsername(user.getUsername()); + result.setEmail(user.getEmail()); + result.setScrobblingEnabled(userSettings.isLastFmEnabled()); + result.setAdminRole(user.isAdminRole()); + result.setSettingsRole(user.isSettingsRole()); + result.setDownloadRole(user.isDownloadRole()); + result.setUploadRole(user.isUploadRole()); + result.setPlaylistRole(true); // Since 1.8.0 + result.setCoverArtRole(user.isCoverArtRole()); + result.setCommentRole(user.isCommentRole()); + result.setPodcastRole(user.isPodcastRole()); + result.setStreamRole(user.isStreamRole()); + result.setJukeboxRole(user.isJukeboxRole()); + result.setShareRole(user.isShareRole()); + + TranscodeScheme transcodeScheme = userSettings.getTranscodeScheme(); + if (transcodeScheme != null && transcodeScheme != TranscodeScheme.OFF) { + result.setMaxBitRate(transcodeScheme.getMaxBitRate()); + } + + List musicFolders = settingsService.getMusicFoldersForUser(user.getUsername()); + for (MusicFolder musicFolder : musicFolders) { + result.getFolder().add(musicFolder.getId()); + } + return result; + } + + @SuppressWarnings("UnusedDeclaration") + 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; + } + + UserSettingsCommand command = new UserSettingsCommand(); + command.setUsername(getRequiredStringParameter(request, "username")); + command.setPassword(decrypt(getRequiredStringParameter(request, "password"))); + command.setEmail(getRequiredStringParameter(request, "email")); + command.setLdapAuthenticated(getBooleanParameter(request, "ldapAuthenticated", false)); + command.setAdminRole(getBooleanParameter(request, "adminRole", false)); + command.setCommentRole(getBooleanParameter(request, "commentRole", false)); + command.setCoverArtRole(getBooleanParameter(request, "coverArtRole", false)); + command.setDownloadRole(getBooleanParameter(request, "downloadRole", false)); + command.setStreamRole(getBooleanParameter(request, "streamRole", true)); + command.setUploadRole(getBooleanParameter(request, "uploadRole", false)); + command.setJukeboxRole(getBooleanParameter(request, "jukeboxRole", false)); + command.setPodcastRole(getBooleanParameter(request, "podcastRole", false)); + command.setSettingsRole(getBooleanParameter(request, "settingsRole", true)); + command.setShareRole(getBooleanParameter(request, "shareRole", false)); + command.setTranscodeSchemeName(TranscodeScheme.OFF.name()); + + int[] folderIds = ServletRequestUtils.getIntParameters(request, "musicFolderId"); + if (folderIds.length == 0) { + folderIds = Util.toIntArray(MusicFolder.toIdList(settingsService.getAllMusicFolders())); + } + command.setAllowedMusicFolderIds(folderIds); + + userSettingsController.createUser(command); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void updateUser(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 update users."); + return; + } + + String username = getRequiredStringParameter(request, "username"); + User u = securityService.getUserByName(username); + UserSettings s = settingsService.getUserSettings(username); + + if (u == null) { + error(request, response, ErrorCode.NOT_FOUND, "No such user: " + username); + return; + } else if (User.USERNAME_ADMIN.equals(username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Not allowed to change admin user"); + return; + } + + UserSettingsCommand command = new UserSettingsCommand(); + command.setUsername(username); + command.setEmail(getStringParameter(request, "email", u.getEmail())); + command.setLdapAuthenticated(getBooleanParameter(request, "ldapAuthenticated", u.isLdapAuthenticated())); + command.setAdminRole(getBooleanParameter(request, "adminRole", u.isAdminRole())); + command.setCommentRole(getBooleanParameter(request, "commentRole", u.isCommentRole())); + command.setCoverArtRole(getBooleanParameter(request, "coverArtRole", u.isCoverArtRole())); + command.setDownloadRole(getBooleanParameter(request, "downloadRole", u.isDownloadRole())); + command.setStreamRole(getBooleanParameter(request, "streamRole", u.isDownloadRole())); + command.setUploadRole(getBooleanParameter(request, "uploadRole", u.isUploadRole())); + command.setJukeboxRole(getBooleanParameter(request, "jukeboxRole", u.isJukeboxRole())); + command.setPodcastRole(getBooleanParameter(request, "podcastRole", u.isPodcastRole())); + command.setSettingsRole(getBooleanParameter(request, "settingsRole", u.isSettingsRole())); + command.setShareRole(getBooleanParameter(request, "shareRole", u.isShareRole())); + + int maxBitRate = getIntParameter(request, "maxBitRate", s.getTranscodeScheme().getMaxBitRate()); + command.setTranscodeSchemeName(TranscodeScheme.fromMaxBitRate(maxBitRate).name()); + + if (hasParameter(request, "password")) { + command.setPassword(decrypt(getRequiredStringParameter(request, "password"))); + command.setPasswordChange(true); + } + + int[] folderIds = ServletRequestUtils.getIntParameters(request, "musicFolderId"); + if (folderIds.length == 0) { + folderIds = Util.toIntArray(MusicFolder.toIdList(settingsService.getMusicFoldersForUser(username))); + } + command.setAllowedMusicFolderIds(folderIds); + + userSettingsController.updateUser(command); + writeEmptyResponse(request, response); + } + + private boolean hasParameter(HttpServletRequest request, String name) { + return request.getParameter(name) != null; + } + + @SuppressWarnings("UnusedDeclaration") + 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; + } + + String username = getRequiredStringParameter(request, "username"); + if (User.USERNAME_ADMIN.equals(username)) { + error(request, response, ErrorCode.NOT_AUTHORIZED, "Not allowed to delete admin user"); + return; + } + + securityService.deleteUser(username); + + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + public void getChatMessages(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + long since = getLongParameter(request, "since", 0L); + + ChatMessages result = new ChatMessages(); + for (ChatService.Message message : chatService.getMessages(0L).getMessages()) { + long time = message.getDate().getTime(); + if (time > since) { + ChatMessage c = new ChatMessage(); + result.getChatMessage().add(c); + c.setUsername(message.getUsername()); + c.setTime(time); + c.setMessage(message.getContent()); + } + } + Response res = createResponse(); + res.setChatMessages(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void addChatMessage(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + chatService.doAddMessage(getRequiredStringParameter(request, "message"), request); + writeEmptyResponse(request, response); + } + + @SuppressWarnings("UnusedDeclaration") + 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); + + Lyrics result = new Lyrics(); + result.setArtist(lyrics.getArtist()); + result.setTitle(lyrics.getTitle()); + result.setContent(lyrics.getLyrics()); + + Response res = createResponse(); + res.setLyrics(result); + jaxbWriter.writeResponse(request, response, res); + } + + @SuppressWarnings("UnusedDeclaration") + public void setRating(HttpServletRequest request, HttpServletResponse response) throws Exception { + request = wrapRequest(request); + Integer rating = getRequiredIntParameter(request, "rating"); + if (rating == 0) { + rating = null; + } + + int id = 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); + + writeEmptyResponse(request, response); + } + + 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 Response createResponse() { + return jaxbWriter.createResponse(true); + } + + private void writeEmptyResponse(HttpServletRequest request, HttpServletResponse response) throws Exception { + jaxbWriter.writeResponse(request, response, createResponse()); + } + + public void error(HttpServletRequest request, HttpServletResponse response, ErrorCode code, String message) throws Exception { + jaxbWriter.writeErrorResponse(request, response, code, message); + } + + private String createPlayerIfNecessary(HttpServletRequest request, boolean jukebox) { + String username = request.getRemoteUser(); + String clientId = request.getParameter("c"); + if (jukebox) { + clientId += "-jukebox"; + } + + List 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 setHlsController(HLSController hlsController) { + this.hlsController = hlsController; + } + + public void setChatService(ChatService chatService) { + this.chatService = chatService; + } + + 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 void setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setBookmarkDao(BookmarkDao bookmarkDao) { + this.bookmarkDao = bookmarkDao; + } + + public void setLastFmService(LastFmService lastFmService) { + this.lastFmService = lastFmService; + } + + public void setPlayQueueDao(PlayQueueDao playQueueDao) { + this.playQueueDao = playQueueDao; + } + + public 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 upgrade to Subsonic Premium. 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; + } + } + + private static class BookmarkKey extends Pair { + private BookmarkKey(String username, int mediaFileId) { + super(username, mediaFileId); + } + + static BookmarkKey forBookmark(Bookmark b) { + return new BookmarkKey(b.getUsername(), b.getMediaFileId()); + } + } +} 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..6dd7f7b2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RandomPlayQueueController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.Arrays; +import java.util.Collections; +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.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.ParameterizableViewController; + +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.RandomSearchCriteria; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the creating a random play queue. + * + * @author Sindre Mehus + */ +public class RandomPlayQueueController extends ParameterizableViewController { + + private PlayerService playerService; + private List reloadFrames; + private SearchService searchService; + private SecurityService securityService; + private SettingsService settingsService; + + 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]); + } + + List musicFolders = getMusicFolders(request); + Player player = playerService.getPlayer(request, response); + PlayQueue playQueue = player.getPlayQueue(); + + RandomSearchCriteria criteria = new RandomSearchCriteria(size, genre, fromYear, toYear, musicFolders); + playQueue.addFiles(false, searchService.getRandomSongs(criteria)); + + if (request.getParameter("autoRandom") != null) { + playQueue.setRandomSearchCriteria(criteria); + } + + Map map = new HashMap(); + map.put("reloadFrames", reloadFrames); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + private List getMusicFolders(HttpServletRequest request) throws ServletRequestBindingException { + String username = securityService.getCurrentUsername(request); + Integer selectedMusicFolderId = ServletRequestUtils.getRequiredIntParameter(request, "musicFolderId"); + if (selectedMusicFolderId == -1) { + selectedMusicFolderId = null; + } + return settingsService.getMusicFoldersForUser(username, selectedMusicFolderId); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setReloadFrames(List reloadFrames) { + this.reloadFrames = reloadFrames; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + 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/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 . + + 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..74bc6c9c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/RightController.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 . + + 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.service.SettingsService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.VersionService; + +/** + * Controller for the right frame. + * + * @author Sindre Mehus + */ +public class RightController extends ParameterizableViewController { + + private SettingsService settingsService; + private SecurityService securityService; + private VersionService versionService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map map = new HashMap(); + ModelAndView result = super.handleRequestInternal(request, response); + + UserSettings userSettings = settingsService.getUserSettings(securityService.getCurrentUsername(request)); + 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()); + } + + map.put("brand", settingsService.getBrand()); + map.put("showNowPlaying", userSettings.isShowNowPlayingEnabled()); + map.put("showChat", userSettings.isShowChatEnabled()); + map.put("user", securityService.getCurrentUser(request)); + map.put("licenseInfo", settingsService.getLicenseInfo()); + + result.addObject("model", map); + return result; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } +} \ 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..db08d88a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SearchController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.List; + +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.MusicFolder; +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.SearchService; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * 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()); + + List musicFolders = settingsService.getMusicFoldersForUser(user.getUsername()); + String query = StringUtils.trimToNull(command.getQuery()); + + if (query != null) { + + SearchCriteria criteria = new SearchCriteria(); + criteria.setCount(MATCH_COUNT); + criteria.setQuery(query); + + SearchResult artists = searchService.search(criteria, musicFolders, SearchService.IndexType.ARTIST); + command.setArtists(artists.getMediaFiles()); + + SearchResult albums = searchService.search(criteria, musicFolders, SearchService.IndexType.ALBUM); + command.setAlbums(albums.getMediaFiles()); + + SearchResult songs = searchService.search(criteria, musicFolders, 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..50bec647 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetMusicFileInfoController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +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.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * 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 { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + String action = request.getParameter("action"); + + MediaFile mediaFile = mediaFileService.getMediaFile(id); + + if ("comment".equals(action)) { + mediaFile.setComment(StringUtil.toHtml(request.getParameter("comment"))); + mediaFileService.updateMediaFile(mediaFile); + } + + String url = "main.view?id=" + id; + 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..653ebaad --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SetRatingController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +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.AbstractController; +import org.springframework.web.servlet.view.RedirectView; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SecurityService; + +/** + * 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 { + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + Integer rating = ServletRequestUtils.getIntParameter(request, "rating"); + if (rating == 0) { + rating = null; + } + + MediaFile mediaFile = mediaFileService.getMediaFile(id); + String username = securityService.getCurrentUsername(request); + ratingService.setRatingForUser(username, mediaFile, rating); + + return new ModelAndView(new RedirectView("main.view?id=" + id)); + } + + 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 . + + 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..1b4b5bad --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareManagementController.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.Share; +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.ShareService; +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 javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 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 PlaylistService playlistService; + private SecurityService securityService; + + public ModelAndView createShare(HttpServletRequest request, HttpServletResponse response) throws Exception { + + List files = getMediaFiles(request); + MediaFile dir = null; + if (!files.isEmpty()) { + dir = files.get(0); + if (!dir.isAlbum()) { + dir = mediaFileService.getParentOf(dir); + } + } + + Map map = new HashMap(); + map.put("urlRedirectionEnabled", settingsService.isUrlRedirectionEnabled()); + map.put("dir", dir); + map.put("user", securityService.getCurrentUser(request)); + + Share share = shareService.createShare(request, files); + String description = getDescription(request); + if (description != null) { + share.setDescription(description); + shareService.updateShare(share); + } + + map.put("playUrl", shareService.getShareUrl(share)); + map.put("licenseInfo", settingsService.getLicenseInfo()); + + return new ModelAndView("createShare", "model", map); + } + + private String getDescription(HttpServletRequest request) throws ServletRequestBindingException { + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + return playlistId == null ? null : playlistService.getPlaylist(playlistId).getName(); + } + + private List getMediaFiles(HttpServletRequest request) throws Exception { + Integer id = ServletRequestUtils.getIntParameter(request, "id"); + String playerId = request.getParameter("player"); + Integer playlistId = ServletRequestUtils.getIntParameter(request, "playlist"); + + List result = new ArrayList(); + + if (id != null) { + MediaFile album = mediaFileService.getMediaFile(id); + int[] indexes = ServletRequestUtils.getIntParameters(request, "i"); + if (indexes.length == 0) { + return Arrays.asList(album); + } + List children = mediaFileService.getChildrenOf(album, true, false, true); + for (int index : indexes) { + result.add(children.get(index)); + } + } + + else if (playerId != null) { + Player player = playerService.getPlayerById(playerId); + PlayQueue playQueue = player.getPlayQueue(); + result = playQueue.getFiles(); + } + + else if (playlistId != null) { + result = playlistService.getFilesInPlaylist(playlistId); + } + + 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; + } + + 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/ShareSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java new file mode 100644 index 00000000..2d95df1c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/ShareSettingsController.java @@ -0,0 +1,183 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +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.lang.StringUtils; +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.domain.MusicFolder; +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.SettingsService; +import net.sourceforge.subsonic.service.ShareService; + +/** + * 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; + private SettingsService settingsService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map map = new HashMap(); + + if (isFormSubmission(request)) { + handleParameters(request); + map.put("toast", true); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("shareBaseUrl", shareService.getShareBaseUrl()); + map.put("shareInfos", getShareInfos(request)); + map.put("user", securityService.getCurrentUser(request)); + map.put("licenseInfo", settingsService.getLicenseInfo()); + + 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) { + 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); + } + } + + boolean deleteExpired = ServletRequestUtils.getBooleanParameter(request, "deleteExpired", false); + if (deleteExpired) { + Date now = new Date(); + for (Share share : shareService.getSharesForUser(user)) { + Date expires = share.getExpires(); + if (expires != null && expires.before(now)) { + shareService.deleteShare(share.getId()); + } + } + } + } + + private List getShareInfos(HttpServletRequest request) { + List result = new ArrayList(); + User user = securityService.getCurrentUser(request); + List musicFolders = settingsService.getMusicFoldersForUser(user.getUsername()); + + for (Share share : shareService.getSharesForUser(user)) { + List files = shareService.getSharedFiles(share.getId(), musicFolders); + 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 void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + 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/SonosSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SonosSettingsController.java new file mode 100644 index 00000000..59567130 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/SonosSettingsController.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 . + * + * Copyright 2015 (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.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.ParameterizableViewController; + +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.SonosService; + +/** + * Controller for the page used to administrate the Sonos music service settings. + * + * @author Sindre Mehus + */ +public class SonosSettingsController extends ParameterizableViewController { + + private SettingsService settingsService; + private SonosService sonosService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + Map map = new HashMap(); + + if (isFormSubmission(request)) { + handleParameters(request); + map.put("toast", true); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("sonosEnabled", settingsService.isSonosEnabled()); + map.put("sonosServiceName", settingsService.getSonosServiceName()); + map.put("licenseInfo", settingsService.getLicenseInfo()); + + 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) { + boolean sonosEnabled = ServletRequestUtils.getBooleanParameter(request, "sonosEnabled", false); + String sonosServiceName = StringUtils.trimToNull(request.getParameter("sonosServiceName")); + if (sonosServiceName == null) { + sonosServiceName = "Subsonic"; + } + + settingsService.setSonosEnabled(sonosEnabled); + settingsService.setSonosServiceName(sonosServiceName); + settingsService.save(); + + sonosService.setMusicServiceEnabled(false); + sonosService.setMusicServiceEnabled(sonosEnabled); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSonosService(SonosService sonosService) { + this.sonosService = sonosService; + } +} \ 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..90701383 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StarredController.java @@ -0,0 +1,109 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +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.ArrayList; +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 map = new HashMap(); + + User user = securityService.getCurrentUser(request); + String username = user.getUsername(); + UserSettings userSettings = settingsService.getUserSettings(username); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + List artists = mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username, musicFolders); + List albums = mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username, musicFolders); + List files = mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders); + mediaFileService.populateStarredDate(artists, username); + mediaFileService.populateStarredDate(albums, username); + mediaFileService.populateStarredDate(files, username); + + List songs = new ArrayList(); + List videos = new ArrayList(); + for (MediaFile file : files) { + (file.isVideo() ? videos : songs).add(file); + } + + map.put("user", user); + map.put("partyModeEnabled", userSettings.isPartyModeEnabled()); + map.put("player", playerService.getPlayer(request, response)); + map.put("coverArtSize", CoverArtScheme.MEDIUM.getSize()); + map.put("artists", artists); + map.put("albums", albums); + map.put("songs", songs); + map.put("videos", videos); + 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 . + + 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 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 . + + 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 map = new HashMap(); + + List streamStatuses = statusService.getAllStreamStatuses(); + List downloadStatuses = statusService.getAllDownloadStatuses(); + List uploadStatuses = statusService.getAllUploadStatuses(); + + Locale locale = RequestContextUtils.getLocale(request); + List transferStatuses = new ArrayList(); + + 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..440e1656 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/StreamController.java @@ -0,0 +1,447 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 org.apache.commons.io.IOUtils; +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.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.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.MediaFileService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SearchService; +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.service.sonos.SonosHelper; +import net.sourceforge.subsonic.util.HttpRange; +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); + } + + response.setHeader("Access-Control-Allow-Origin", "*"); + + 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; + HttpRange range = null; + + if (isSingleFile) { + + if (!securityService.isFolderAccessAllowed(file, user.getUsername())) { + response.sendError(HttpServletResponse.SC_FORBIDDEN, + "Access to file " + file.getId() + " is forbidden for user " + user.getUsername()); + return null; + } + + 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, null); + long fileLength = getFileLength(parameters); + boolean isConversion = parameters.isDownsample() || parameters.isTranscode(); + boolean estimateContentLength = ServletRequestUtils.getBooleanParameter(request, "estimateContentLength", false); + boolean isHls = ServletRequestUtils.getBooleanParameter(request, "hls", false); + + range = getRange(request, file); + if (range != null && !file.isVideo()) { + LOG.info("Got HTTP range: " + range); + response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); + Util.setContentLength(response, range.isClosed() ? range.size() : fileLength - range.getFirstBytePos()); + long lastBytePos = range.getLastBytePos() != null ? range.getLastBytePos() : fileLength - 1; + response.setHeader("Content-Range", "bytes " + range.getFirstBytePos() + "-" + lastBytePos + "/" + fileLength); + } else if (!isHls && (!isConversion || estimateContentLength)) { + Util.setContentLength(response, fileLength); + } + + if (isHls) { + response.setContentType(StringUtil.getMimeType("ts")); // HLS is always MPEG TS. + } else { + String transcodedSuffix = transcodingService.getSuffix(player, file, preferredTargetFormat); + boolean sonos = SonosHelper.SUBSONIC_CLIENT_ID.equals(player.getClientId()); + response.setContentType(StringUtil.getMimeType(transcodedSuffix, sonos)); + setContentDuration(response, file); + } + + if (file.isVideo() || isHls) { + 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 void setContentDuration(HttpServletResponse response, MediaFile file) { + if (file.getDurationSeconds() != null) { + response.setHeader("X-Content-Duration", String.format("%.1f", file.getDurationSeconds().doubleValue())); + } + } + + 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 HttpRange getRange(HttpServletRequest request, MediaFile file) { + + // First, look for "Range" HTTP header. + HttpRange range = HttpRange.valueOf(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 HttpRange 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 HttpRange(byteOffset, null); + + } 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); + int defaultDuration = file.getDurationSeconds() == null ? Integer.MAX_VALUE : file.getDurationSeconds() - timeOffset; + int duration = ServletRequestUtils.getIntParameter(request, "duration", defaultDuration); + boolean hls = ServletRequestUtils.getBooleanParameter(request, "hls", false); + + Dimension dim = getRequestedVideoSize(request.getParameter("size")); + if (dim == null) { + dim = getSuitableVideoSize(existingWidth, existingHeight, maxBitRate); + } + + return new VideoTranscodingSettings(dim.width, dim.height, timeOffset, duration, hls); + } + + 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(400, 224); + } + + int w; + if (maxBitRate < 400) { + w = 400; + } else if (maxBitRate < 600) { + w = 480; + } else if (maxBitRate < 1800) { + w = 640; + } else { + w = 960; + } + int h = even(w * 9 / 16); + + 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(); + h = (int) Math.round(w / 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..3f3506dc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TopController.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 . + + 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.AvatarScheme; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.UserSettings; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the top frame. + * + * @author Sindre Mehus + */ +public class TopController extends ParameterizableViewController { + + private SettingsService settingsService; + private SecurityService securityService; + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + Map map = new HashMap(); + + User user = securityService.getCurrentUser(request); + UserSettings userSettings = settingsService.getUserSettings(user.getUsername()); + + map.put("user", user); + map.put("showSideBar", userSettings.isShowSideBar()); + map.put("showAvatar", userSettings.getAvatarScheme() != AvatarScheme.NONE); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + 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/TranscodingSettingsController.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.java new file mode 100644 index 00000000..39176fd1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/TranscodingSettingsController.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 . + + 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 map = new HashMap(); + + if (isFormSubmission(request)) { + handleParameters(request, map); + map.put("toast", true); + } + + ModelAndView result = super.handleRequestInternal(request, response); + map.put("transcodings", transcodingService.getAllTranscodings()); + map.put("transcodeDirectory", transcodingService.getTranscodeDirectory()); + map.put("downsampleCommand", settingsService.getDownsamplingCommand()); + map.put("hlsCommand", settingsService.getHlsCommand()); + 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 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); + } + } + settingsService.setDownsamplingCommand(StringUtils.trim(request.getParameter("downsampleCommand"))); + settingsService.setHlsCommand(StringUtils.trim(request.getParameter("hlsCommand"))); + settingsService.save(); + } + + 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 . + + 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 map = new HashMap(); + List uploadedFiles = new ArrayList(); + List unzippedFiles = new ArrayList(); + 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 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 . + + 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 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..d0af59ea --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/UserSettingsController.java @@ -0,0 +1,189 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import net.sourceforge.subsonic.command.UserSettingsCommand; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.TranscodeScheme; +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.TranscodingService; +import net.sourceforge.subsonic.util.Util; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.bind.ServletRequestUtils; +import org.springframework.web.servlet.mvc.SimpleFormController; + +import javax.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +/** + * 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.setNewUser(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()); + command.setAllMusicFolders(settingsService.getAllMusicFolders()); + command.setAllowedMusicFolderIds(Util.toIntArray(getAllowedMusicFolderIds(user))); + + return command; + } + + private User getUser(HttpServletRequest request) throws ServletRequestBindingException { + Integer userIndex = ServletRequestUtils.getIntParameter(request, "userIndex"); + if (userIndex != null) { + List allUsers = securityService.getAllUsers(); + if (userIndex >= 0 && userIndex < allUsers.size()) { + return allUsers.get(userIndex); + } + } + return null; + } + + private List getAllowedMusicFolderIds(User user) { + List result = new ArrayList(); + List allowedMusicFolders = user == null + ? settingsService.getAllMusicFolders() + : settingsService.getMusicFoldersForUser(user.getUsername()); + + for (MusicFolder musicFolder : allowedMusicFolders) { + result.add(musicFolder.getId()); + } + return result; + } + + @Override + protected void doSubmitAction(Object comm) throws Exception { + UserSettingsCommand command = (UserSettingsCommand) comm; + + if (command.isDeleteUser()) { + deleteUser(command); + } else if (command.isNewUser()) { + 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); + } + + public 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); + + List allowedMusicFolderIds = Util.toIntegerList(command.getAllowedMusicFolderIds()); + settingsService.setMusicFoldersForUser(command.getUsername(), allowedMusicFolderIds); + } + + private void resetCommand(UserSettingsCommand command) { + command.setUser(null); + command.setUsers(securityService.getAllUsers()); + command.setDeleteUser(false); + command.setPasswordChange(false); + command.setNewUser(true); + command.setStreamRole(true); + command.setSettingsRole(true); + command.setPassword(null); + command.setConfirmPassword(null); + command.setEmail(null); + command.setTranscodeSchemeName(null); + command.setAllMusicFolders(settingsService.getAllMusicFolders()); + command.setAllowedMusicFolderIds(Util.toIntArray(getAllowedMusicFolderIds(null))); + command.setToast(true); + command.setReload(true); + } + + 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..141a0e90 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/VideoPlayerController.java @@ -0,0 +1,118 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.controller; + +import java.util.HashMap; +import java.util.LinkedHashMap; +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.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 net.sourceforge.subsonic.util.StringUtil; + +/** + * Controller for the page used to play videos. + * + * @author Sindre Mehus + */ +public class VideoPlayerController extends ParameterizableViewController { + + public static final int DEFAULT_BIT_RATE = 2000; + public static final int[] BIT_RATES = {200, 300, 400, 500, 700, 1000, 1200, 1500, 2000, 3000, 5000}; + + private MediaFileService mediaFileService; + private SettingsService settingsService; + private PlayerService playerService; + private SecurityService securityService; + + @Override + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception { + + User user = securityService.getCurrentUser(request); + Map map = new HashMap(); + int id = ServletRequestUtils.getRequiredIntParameter(request, "id"); + MediaFile file = mediaFileService.getMediaFile(id); + mediaFileService.populateStarredDate(file, user.getUsername()); + + Integer duration = file.getDurationSeconds(); + String playerId = playerService.getPlayer(request, response).getId(); + String url = request.getRequestURL().toString(); + String streamUrl = url.replaceFirst("/videoPlayer.view.*", "/stream?id=" + file.getId() + "&player=" + playerId); + String coverArtUrl = url.replaceFirst("/videoPlayer.view.*", "/coverArt.view?id=" + file.getId()); + + // Rewrite URLs in case we're behind a proxy. + if (settingsService.isRewriteUrlEnabled()) { + String referer = request.getHeader("referer"); + streamUrl = StringUtil.rewriteUrl(streamUrl, referer); + coverArtUrl = StringUtil.rewriteUrl(coverArtUrl, referer); + } + + String remoteStreamUrl = settingsService.rewriteRemoteUrl(streamUrl); + String remoteCoverArtUrl = settingsService.rewriteRemoteUrl(coverArtUrl); + + map.put("video", file); + map.put("streamUrl", streamUrl); + map.put("remoteStreamUrl", remoteStreamUrl); + map.put("remoteCoverArtUrl", remoteCoverArtUrl); + map.put("duration", duration); + map.put("bitRates", BIT_RATES); + map.put("defaultBitRate", DEFAULT_BIT_RATE); + map.put("licenseInfo", settingsService.getLicenseInfo()); + map.put("user", user); + + ModelAndView result = super.handleRequestInternal(request, response); + result.addObject("model", map); + return result; + } + + public static Map createSkipOffsets(int durationSeconds) { + LinkedHashMap result = new LinkedHashMap(); + 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; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } +} 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..416b61ae --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/controller/WapController.java @@ -0,0 +1,251 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 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.PlayQueue; +import net.sourceforge.subsonic.domain.Player; +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.MediaFileService; +import net.sourceforge.subsonic.service.MusicIndexService; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.service.SearchService; +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 map = new HashMap(); + + String username = securityService.getCurrentUsername(request); + List folders = settingsService.getMusicFoldersForUser(username); + + if (folders.isEmpty()) { + map.put("noMusic", true); + } else { + + SortedMap> allArtists = musicIndexService.getIndexedArtists(folders, false); + + // If an index is given as parameter, only show music files for this index. + String index = request.getParameter("index"); + if (index != null) { + List 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 children; + if (parent.isDirectory()) { + children = mediaFileService.getChildrenOf(parent, true, true, true); + } else { + children = new ArrayList(); + children.add(parent); + } + + Map map = new HashMap(); + 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 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 map = new HashMap(); + + 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 songs = playlistService.getFilesInPlaylist(ServletRequestUtils.getIntParameter(request, "id")); + playQueue.addFiles(false, songs); + } else if (request.getParameter("random") != null) { + List musicFolders = settingsService.getMusicFoldersForUser(securityService.getCurrentUsername(request)); + List randomFiles = searchService.getRandomSongs(new RandomSearchCriteria(20, null, null, null, musicFolders)); + 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 map = new HashMap(); + 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 username = securityService.getCurrentUsername(request); + String query = request.getParameter("query"); + + Map map = new HashMap(); + map.put("hits", search(query, username)); + + return new ModelAndView("wap/searchResult", "model", map); + } + + public ModelAndView settings(HttpServletRequest request, HttpServletResponse response) throws Exception { + String playerId = (String) request.getSession().getAttribute("player"); + + List allPlayers = playerService.getAllPlayers(); + User user = securityService.getCurrentUser(request); + List players = new ArrayList(); + Map map = new HashMap(); + + 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 search(String query, String username) throws IOException { + SearchCriteria criteria = new SearchCriteria(); + criteria.setQuery(query); + criteria.setOffset(0); + criteria.setCount(50); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + SearchResult result = searchService.search(criteria, musicFolders, 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..d1dba4d0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AbstractDao.java @@ -0,0 +1,175 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.springframework.jdbc.core.*; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +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(); + } + + /** + * Similar to {@link #getJdbcTemplate()}, but with named parameters. + */ + public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { + return daoHelper.getNamedParameterJdbcTemplate(); + } + + 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 millis = (System.nanoTime() - startTimeNano) / 1000000L; + + // Log queries that take more than 2 seconds. + if (millis > TimeUnit.SECONDS.toMillis(2L)) { + LOG.debug(millis + " ms: " + sql); + } + } + + protected List query(String sql, RowMapper rowMapper, Object... args) { + long t = System.nanoTime(); + List result = getJdbcTemplate().query(sql, args, rowMapper); + log(sql, t); + return result; + } + + protected List namedQuery(String sql, RowMapper rowMapper, Map args) { + long t = System.nanoTime(); + List result = getNamedParameterJdbcTemplate().query(sql, args, rowMapper); + log(sql, t); + return result; + } + + protected List queryForStrings(String sql, Object... args) { + long t = System.nanoTime(); + List result = getJdbcTemplate().queryForList(sql, args, String.class); + log(sql, t); + return result; + } + + protected List queryForInts(String sql, Object... args) { + long t = System.nanoTime(); + List result = getJdbcTemplate().queryForList(sql, args, Integer.class); + log(sql, t); + return result; + } + + protected List namedQueryForStrings(String sql, Map args) { + long t = System.nanoTime(); + List result = getNamedParameterJdbcTemplate().queryForList(sql, args, String.class); + log(sql, t); + return result; + } + + protected Integer queryForInt(String sql, Integer defaultValue, Object... args) { + long t = System.nanoTime(); + List 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 Integer namedQueryForInt(String sql, Integer defaultValue, Map args) { + long t = System.nanoTime(); + List list = getNamedParameterJdbcTemplate().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 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 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 queryOne(String sql, RowMapper rowMapper, Object... args) { + List list = query(sql, rowMapper, args); + return list.isEmpty() ? null : list.get(0); + } + + protected T namedQueryOne(String sql, RowMapper rowMapper, Map args) { + List list = namedQuery(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..5fd775ac --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/AlbumDao.java @@ -0,0 +1,369 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang.ObjectUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.domain.Album; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.util.FileUtil; + +/** + * Provides database services for albums. + * + * @author Sindre Mehus + */ +public class AlbumDao extends AbstractDao { + + private static final String COLUMNS = "id, path, name, artist, song_count, duration_seconds, cover_art_path, " + + "year, genre, play_count, last_played, comment, created, last_scanned, present, folder_id"; + + 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 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()) && FileUtil.exists(candidate.getPath())) { + 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 getAlbumsForArtist(final String artist, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("artist", artist); + put("folders", MusicFolder.toIdList(musicFolders)); + }}; + return namedQuery("select " + COLUMNS + " from album where artist = :artist and present and folder_id in (:folders) " + + "order by name", + rowMapper, args); + } + + /** + * Creates or updates an album. + * + * @param album The album to create/update. + */ + public synchronized void createOrUpdateAlbum(Album album) { + String sql = "update album set " + + "path=?," + + "song_count=?," + + "duration_seconds=?," + + "cover_art_path=?," + + "year=?," + + "genre=?," + + "play_count=?," + + "last_played=?," + + "comment=?," + + "created=?," + + "last_scanned=?," + + "present=?, " + + "folder_id=? " + + "where artist=? and name=?"; + + int n = update(sql, album.getPath(), album.getSongCount(), album.getDurationSeconds(), album.getCoverArtPath(), album.getYear(), + album.getGenre(), album.getPlayCount(), album.getLastPlayed(), album.getComment(), album.getCreated(), + album.getLastScanned(), album.isPresent(), album.getFolderId(), 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.getYear(), album.getGenre(), album.getPlayCount(), album.getLastPlayed(), + album.getComment(), album.getCreated(), album.getLastScanned(), album.isPresent(), album.getFolderId()); + } + + 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 + * @param musicFolders Only return albums from these folders. + * @return Albums in alphabetical order. + */ + public List getAlphabetialAlbums(final int offset, final int count, boolean byArtist, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + String orderBy = byArtist ? "artist, name" : "name"; + return namedQuery("select " + COLUMNS + " from album where present and folder_id in (:folders) " + + "order by " + orderBy + " limit :count offset :offset", rowMapper, args); + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums from these folders. + * @return The most frequently played albums. + */ + public List getMostFrequentlyPlayedAlbums(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + COLUMNS + " from album where play_count > 0 and present and folder_id in (:folders) " + + "order by play_count desc limit :count offset :offset", rowMapper, args); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums from these folders. + * @return The most recently played albums. + */ + public List getMostRecentlyPlayedAlbums(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + COLUMNS + " from album where last_played is not null and present and folder_id in (:folders) " + + "order by last_played desc limit :count offset :offset", rowMapper, args); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums from these folders. + * @return The most recently added albums. + */ + public List getNewestAlbums(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + COLUMNS + " from album where present and folder_id in (:folders) " + + "order by created desc limit :count offset :offset", rowMapper, args); + } + + /** + * 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. + * @param musicFolders Only return albums from these folders. + * @return The most recently starred albums for this user. + */ + public List getStarredAlbums(final int offset, final int count, final String username, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + put("username", username); + }}; + return namedQuery("select " + prefix(COLUMNS, "album") + " from starred_album, album where album.id = starred_album.album_id and " + + "album.present and album.folder_id in (:folders) and starred_album.username = :username " + + "order by starred_album.created desc limit :count offset :offset", + rowMapper, args); + } + + /** + * Returns albums in a genre. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param genre The genre name. + * @param musicFolders Only return albums from these folders. + * @return Albums in the genre. + */ + public List getAlbumsByGenre(final int offset, final int count, final String genre, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + put("genre", genre); + }}; + return namedQuery("select " + COLUMNS + " from album where present and folder_id in (:folders) " + + "and genre = :genre limit :count offset :offset", rowMapper, args); + } + + /** + * Returns albums within a year range. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param fromYear The first year in the range. + * @param toYear The last year in the range. + * @param musicFolders Only return albums from these folders. + * @return Albums in the year range. + */ + public List getAlbumsByYear(final int offset, final int count, final int fromYear, final int toYear, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + put("fromYear", fromYear); + put("toYear", toYear); + }}; + if (fromYear <= toYear) { + return namedQuery("select " + COLUMNS + " from album where present and folder_id in (:folders) " + + "and year between :fromYear and :toYear order by year limit :count offset :offset", + rowMapper, args); + } else { + return namedQuery("select " + COLUMNS + " from album where present and folder_id in (:folders) " + + "and year between :toYear and :fromYear order by year desc limit :count offset :offset", + rowMapper, args); + } + } + + public void markNonPresent(Date lastScanned) { + int minId = queryForInt("select top 1 id from album where last_scanned != ? and present", 0, lastScanned); + int maxId = queryForInt("select max(id) from album where last_scanned != ? and present", 0, lastScanned); + + 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 top 1 id from album where not present", 0); + int maxId = queryForInt("select max(id) from album where not present", 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 { + 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) == 0 ? null : rs.getInt(8), + rs.getString(9), + rs.getInt(10), + rs.getTimestamp(11), + rs.getString(12), + rs.getTimestamp(13), + rs.getTimestamp(14), + rs.getBoolean(15), + rs.getInt(16)); + } + } +} 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..fd3673a6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ArtistDao.java @@ -0,0 +1,213 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.MusicFolder; + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 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, folder_id"; + + 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 name. + * + * @param artistName The artist name. + * @param musicFolders Only return artists that have at least one album in these folders. + * @return The artist or null. + */ + public Artist getArtist(final String artistName, final List musicFolders) { + if (musicFolders.isEmpty()) { + return null; + } + Map args = new HashMap() {{ + put("name", artistName); + put("folders", MusicFolder.toIdList(musicFolders)); + }}; + + return namedQueryOne("select " + COLUMNS + " from artist where name = :name and folder_id in (:folders)", + rowMapper, args); + } + + /** + * 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=?," + + "folder_id=? " + + "where name=?"; + + int n = update(sql, artist.getCoverArtPath(), artist.getAlbumCount(), artist.getLastScanned(), artist.isPresent(), artist.getFolderId(), artist.getName()); + + if (n == 0) { + update("insert into artist (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null, + artist.getName(), artist.getCoverArtPath(), artist.getAlbumCount(), artist.getLastScanned(), artist.isPresent(), artist.getFolderId()); + } + + 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. + * @param musicFolders Only return artists that have at least one album in these folders. + * @return Artists in alphabetical order. + */ + public List getAlphabetialArtists(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + + return namedQuery("select " + COLUMNS + " from artist where present and folder_id in (:folders) " + + "order by name limit :count offset :offset", rowMapper, args); + } + + /** + * 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. + * @param musicFolders Only return artists that have at least one album in these folders. + * @return The most recently starred artists for this user. + */ + public List getStarredArtists(final int offset, final int count, final String username, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("folders", MusicFolder.toIdList(musicFolders)); + put("username", username); + put("count", count); + put("offset", offset); + }}; + + return namedQuery("select " + prefix(COLUMNS, "artist") + " from starred_artist, artist " + + "where artist.id = starred_artist.artist_id and " + + "artist.present and starred_artist.username = :username and " + + "artist.folder_id in (:folders) " + + "order by starred_artist.created desc limit :count offset :offset", + rowMapper, args); + } + + 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 top 1 id from artist where last_scanned != ? and present", 0, lastScanned); + int maxId = queryForInt("select max(id) from artist where last_scanned != ? and present", 0, lastScanned); + + 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 top 1 id from artist where not present", 0); + int maxId = queryForInt("select max(id) from artist where not present", 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 { + 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), + rs.getInt(7)); + } + } +} 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 . + + 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 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 null 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 null 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 null 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 { + 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/BookmarkDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/BookmarkDao.java new file mode 100644 index 00000000..5f9b6c28 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/BookmarkDao.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 . + + 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.Bookmark; + +/** + * Provides database services for media file bookmarks. + * + * @author Sindre Mehus + */ +public class BookmarkDao extends AbstractDao { + + private static final String COLUMNS = "id, media_file_id, position_millis, username, comment, created, changed"; + + private BookmarkRowMapper bookmarkRowMapper = new BookmarkRowMapper(); + + /** + * Returns all bookmarks. + * + * @return Possibly empty list of all bookmarks. + */ + public List getBookmarks() { + String sql = "select " + COLUMNS + " from bookmark"; + return query(sql, bookmarkRowMapper); + } + + /** + * Returns all bookmarks for a given user. + * + * @return Possibly empty list of all bookmarks for the user. + */ + public List getBookmarks(String username) { + String sql = "select " + COLUMNS + " from bookmark where username=?"; + return query(sql, bookmarkRowMapper, username); + } + + /** + * Creates or updates a bookmark. If created, the ID of the bookmark will be set by this method. + */ + public synchronized void createOrUpdateBookmark(Bookmark bookmark) { + int n = update("update bookmark set position_millis=?, comment=?, changed=? where media_file_id=? and username=?", + bookmark.getPositionMillis(), bookmark.getComment(), bookmark.getChanged(), bookmark.getMediaFileId(), bookmark.getUsername()); + + if (n == 0) { + update("insert into bookmark (" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", null, + bookmark.getMediaFileId(), bookmark.getPositionMillis(), bookmark.getUsername(), bookmark.getComment(), + bookmark.getCreated(), bookmark.getChanged()); + int id = queryForInt("select id from bookmark where media_file_id=? and username=?", 0, bookmark.getMediaFileId(), bookmark.getUsername()); + bookmark.setId(id); + } + } + + /** + * Deletes the bookmark for the given username and media file. + */ + public synchronized void deleteBookmark(String username, int mediaFileId) { + update("delete from bookmark where username=? and media_file_id=?", username, mediaFileId); + } + + private static class BookmarkRowMapper implements ParameterizedRowMapper { + public Bookmark mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Bookmark(rs.getInt(1), rs.getInt(2), rs.getLong(3), rs.getString(4), + rs.getString(5), rs.getTimestamp(6), rs.getTimestamp(7)); + } + } +} 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..a45ffafa --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelper.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +/** + * DAO helper class which creates the data source, and updates the database schema. + * + * @author Sindre Mehus + */ +public interface DaoHelper { + + /** + * Returns a JDBC template for performing database operations. + * + * @return A JDBC template. + */ + JdbcTemplate getJdbcTemplate(); + + /** + * Returns a named parameter JDBC template for performing database operations. + * + * @return A named parameter JDBC template. + */ + NamedParameterJdbcTemplate getNamedParameterJdbcTemplate(); + +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelperFactory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelperFactory.java new file mode 100644 index 00000000..20f837e7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/DaoHelperFactory.java @@ -0,0 +1,31 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.dao; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class DaoHelperFactory { + + public static DaoHelper create() { + return new HsqlDaoHelper(); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/HsqlDaoHelper.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/HsqlDaoHelper.java new file mode 100644 index 00000000..226ef52d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/HsqlDaoHelper.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import java.io.File; + +import javax.sql.DataSource; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; +import net.sourceforge.subsonic.dao.schema.hsql.Schema25; +import net.sourceforge.subsonic.dao.schema.hsql.Schema26; +import net.sourceforge.subsonic.dao.schema.hsql.Schema27; +import net.sourceforge.subsonic.dao.schema.hsql.Schema28; +import net.sourceforge.subsonic.dao.schema.hsql.Schema29; +import net.sourceforge.subsonic.dao.schema.hsql.Schema30; +import net.sourceforge.subsonic.dao.schema.hsql.Schema31; +import net.sourceforge.subsonic.dao.schema.hsql.Schema32; +import net.sourceforge.subsonic.dao.schema.hsql.Schema33; +import net.sourceforge.subsonic.dao.schema.hsql.Schema34; +import net.sourceforge.subsonic.dao.schema.hsql.Schema35; +import net.sourceforge.subsonic.dao.schema.hsql.Schema36; +import net.sourceforge.subsonic.dao.schema.hsql.Schema37; +import net.sourceforge.subsonic.dao.schema.hsql.Schema38; +import net.sourceforge.subsonic.dao.schema.hsql.Schema40; +import net.sourceforge.subsonic.dao.schema.hsql.Schema43; +import net.sourceforge.subsonic.dao.schema.hsql.Schema45; +import net.sourceforge.subsonic.dao.schema.hsql.Schema46; +import net.sourceforge.subsonic.dao.schema.hsql.Schema47; +import net.sourceforge.subsonic.dao.schema.hsql.Schema49; +import net.sourceforge.subsonic.dao.schema.hsql.Schema50; +import net.sourceforge.subsonic.dao.schema.hsql.Schema51; +import net.sourceforge.subsonic.dao.schema.hsql.Schema52; +import net.sourceforge.subsonic.dao.schema.hsql.Schema53; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * DAO helper class which creates the data source, and updates the database schema. + * + * @author Sindre Mehus + */ +public class HsqlDaoHelper implements DaoHelper { + + private static final Logger LOG = Logger.getLogger(HsqlDaoHelper.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(), new Schema49(), + new Schema50(), new Schema51(), new Schema52(), new Schema53()}; + private DataSource dataSource; + private static boolean shutdownHookAdded; + + public HsqlDaoHelper() { + 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("Shutting down database - Done!"); + } + }); + } + + /** + * Returns a JDBC template for performing database operations. + * + * @return A JDBC template. + */ + public JdbcTemplate getJdbcTemplate() { + return new JdbcTemplate(dataSource); + } + + public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { + return new NamedParameterJdbcTemplate(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 . + + 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 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 { + 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..989defd3 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MediaFileDao.java @@ -0,0 +1,621 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +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 org.apache.commons.lang.StringUtils; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.simple.ParameterizedRowMapper; + +import net.sourceforge.subsonic.domain.Genre; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; + +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 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 String GENRE_COLUMNS = "name, song_count, album_count"; + + public static final int VERSION = 4; + + private final RowMapper rowMapper = new MediaFileMapper(); + private final RowMapper musicFileInfoRowMapper = new MusicFileInfoMapper(); + private final RowMapper genreRowMapper = new GenreMapper(); + + /** + * 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 getChildrenOf(String path) { + return query("select " + COLUMNS + " from media_file where parent_path=? and present", rowMapper, path); + } + + public List getFilesInPlaylist(int playlistId) { + return query("select " + prefix(COLUMNS, "media_file") + " from playlist_file, media_file where " + + "media_file.id = playlist_file.media_file_id and " + + "playlist_file.playlist_id = ? " + + "order by playlist_file.id", rowMapper, playlistId); + } + + public List getSongsForAlbum(String artist, String album) { + return query("select " + COLUMNS + " from media_file where album_artist=? and album=? and present " + + "and type in (?,?,?) order by disc_number, track_number", rowMapper, + artist, album, MUSIC.name(), AUDIOBOOK.name(), PODCAST.name()); + } + + public List getVideos(final int count, final int offset, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", VIDEO.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + COLUMNS + " from media_file where type = :type and present and folder in (:folders) " + + "order by title limit :count offset :offset", rowMapper, args); + } + + public MediaFile getArtistByName(final String name, final List musicFolders) { + if (musicFolders.isEmpty()) { + return null; + } + Map args = new HashMap() {{ + put("type", DIRECTORY.name()); + put("name", name); + put("folders", MusicFolder.toPathList(musicFolders)); + }}; + return namedQueryOne("select " + COLUMNS + " from media_file where type = :type and artist = :name " + + "and present and folder in (:folders)", rowMapper, args); + } + + /** + * 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); + } + + public void deleteMediaFile(String path) { + update("update media_file set present=false, children_last_updated=? where path=?", new Date(0L), path); + } + + public List getGenres(boolean sortByAlbum) { + String orderBy = sortByAlbum ? "album_count" : "song_count"; + return query("select " + GENRE_COLUMNS + " from genre order by " + orderBy + " desc", genreRowMapper); + } + + public void updateGenres(List genres) { + update("delete from genre"); + for (Genre genre : genres) { + update("insert into genre(" + GENRE_COLUMNS + ") values(?, ?, ?)", + genre.getName(), genre.getSongCount(), genre.getAlbumCount()); + } + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The most frequently played albums. + */ + public List getMostFrequentlyPlayedAlbums(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + + return namedQuery("select " + COLUMNS + " from media_file where type = :type and play_count > 0 and present and folder in (:folders) " + + "order by play_count desc limit :count offset :offset", rowMapper, args); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The most recently played albums. + */ + public List getMostRecentlyPlayedAlbums(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + COLUMNS + " from media_file where type = :type and last_played is not null and present " + + "and folder in (:folders) order by last_played desc limit :count offset :offset", rowMapper, args); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The most recently added albums. + */ + public List getNewestAlbums(final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + + return namedQuery("select " + COLUMNS + " from media_file where type = :type and folder in (:folders) and present " + + "order by created desc limit :count offset :offset", rowMapper, args); + } + + /** + * 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 + * @param musicFolders Only return albums in these folders. + * @return Albums in alphabetical order. + */ + public List getAlphabeticalAlbums(final int offset, final int count, boolean byArtist, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + + String orderBy = byArtist ? "artist, album" : "album"; + return namedQuery("select " + COLUMNS + " from media_file where type = :type and folder in (:folders) and present " + + "order by " + orderBy + " limit :count offset :offset", rowMapper, args); + } + + /** + * Returns albums within a year range. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param fromYear The first year in the range. + * @param toYear The last year in the range. + * @param musicFolders Only return albums in these folders. + * @return Albums in the year range. + */ + public List getAlbumsByYear(final int offset, final int count, final int fromYear, final int toYear, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("fromYear", fromYear); + put("toYear", toYear); + put("count", count); + put("offset", offset); + }}; + + if (fromYear <= toYear) { + return namedQuery("select " + COLUMNS + " from media_file where type = :type and folder in (:folders) and present " + + "and year between :fromYear and :toYear order by year limit :count offset :offset", + rowMapper, args); + } else { + return namedQuery("select " + COLUMNS + " from media_file where type = :type and folder in (:folders) and present " + + "and year between :toYear and :fromYear order by year desc limit :count offset :offset", + rowMapper, args); + } + } + + /** + * Returns albums in a genre. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param genre The genre name. + * @param musicFolders Only return albums in these folders. + * @return Albums in the genre. + */ + public List getAlbumsByGenre(final int offset, final int count, final String genre, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("genre", genre); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + COLUMNS + " from media_file where type = :type and folder in (:folders) " + + "and present and genre = :genre limit :count offset :offset", rowMapper, args); + } + + public List getSongsByGenre(final String genre, final int offset, final int count, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("types", Arrays.asList(MUSIC.name(), PODCAST.name(), AUDIOBOOK.name())); + put("genre", genre); + put("count", count); + put("offset", offset); + put("folders", MusicFolder.toPathList(musicFolders)); + }}; + return namedQuery("select " + COLUMNS + " from media_file where type in (:types) and genre = :genre " + + "and present and folder in (:folders) limit :count offset :offset", + rowMapper, args); + } + + public List getSongsByArtist(String artist, int offset, int count) { + return query("select " + COLUMNS + " from media_file where type in (?,?,?) and artist=? and present limit ? offset ?", + rowMapper, MUSIC.name(), PODCAST.name(), AUDIOBOOK.name(), artist, count, offset); + } + + public MediaFile getSongByArtistAndTitle(final String artist, final String title, final List musicFolders) { + if (musicFolders.isEmpty() || StringUtils.isBlank(title) || StringUtils.isBlank(artist)) { + return null; + } + Map args = new HashMap() {{ + put("artist", artist); + put("title", title); + put("type", MUSIC.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + }}; + return namedQueryOne("select " + COLUMNS + " from media_file where artist = :artist " + + "and title = :title and type = :type and present and folder in (:folders)" , + rowMapper, args); + } + + /** + * 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. + * @param musicFolders Only return albums in these folders. + * @return The most recently starred albums for this user. + */ + public List getStarredAlbums(final int offset, final int count, final String username, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("username", username); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + prefix(COLUMNS, "media_file") + " from starred_media_file, media_file where media_file.id = starred_media_file.media_file_id and " + + "media_file.present and media_file.type = :type and media_file.folder in (:folders) and starred_media_file.username = :username " + + "order by starred_media_file.created desc limit :count offset :offset", + rowMapper, args); + } + + /** + * 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. + * @param musicFolders Only return albums in these folders. + * @return The most recently starred directories for this user. + */ + public List getStarredDirectories(final int offset, final int count, final String username, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("type", DIRECTORY.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("username", username); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + prefix(COLUMNS, "media_file") + " from starred_media_file, media_file " + + "where media_file.id = starred_media_file.media_file_id and " + + "media_file.present and media_file.type = :type and starred_media_file.username = :username and " + + "media_file.folder in (:folders) " + + "order by starred_media_file.created desc limit :count offset :offset", + rowMapper, args); + } + + /** + * 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. + * @param musicFolders Only return albums in these folders. + * @return The most recently starred files for this user. + */ + public List getStarredFiles(final int offset, final int count, final String username, + final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("types", Arrays.asList(MUSIC.name(), PODCAST.name(), AUDIOBOOK.name(), VIDEO.name())); + put("folders", MusicFolder.toPathList(musicFolders)); + put("username", username); + put("count", count); + put("offset", offset); + }}; + return namedQuery("select " + prefix(COLUMNS, "media_file") + " from starred_media_file, media_file where media_file.id = starred_media_file.media_file_id and " + + "media_file.present and media_file.type in (:types) and starred_media_file.username = :username and " + + "media_file.folder in (:folders) " + + "order by starred_media_file.created desc limit :count offset :offset", + rowMapper, args); + } + + public int getAlbumCount(final List musicFolders) { + if (musicFolders.isEmpty()) { + return 0; + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + }}; + return namedQueryForInt("select count(*) from media_file where type = :type and folder in (:folders) and present", 0, args); + } + + public int getPlayedAlbumCount(final List musicFolders) { + if (musicFolders.isEmpty()) { + return 0; + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + }}; + return namedQueryForInt("select count(*) from media_file where type = :type " + + "and play_count > 0 and present and folder in (:folders)", 0, args); + } + + public int getStarredAlbumCount(final String username, final List musicFolders) { + if (musicFolders.isEmpty()) { + return 0; + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("username", username); + }}; + return namedQueryForInt("select count(*) from starred_media_file, media_file " + + "where media_file.id = starred_media_file.media_file_id " + + "and media_file.type = :type " + + "and media_file.present " + + "and media_file.folder in (:folders) " + + "and starred_media_file.username = :username", + 0, args); + } + + 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); + } + + 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 top 1 id from media_file where last_scanned != ? and present", 0, lastScanned); + int maxId = queryForInt("select max(id) from media_file where last_scanned != ? and present", 0, lastScanned); + + 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 top 1 id from media_file where not present", 0); + int maxId = queryForInt("select max(id) from media_file where not present", 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 { + 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), + rs.getInt(30)); + } + } + + private static class MusicFileInfoMapper implements ParameterizedRowMapper { + 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; + } + } + + private static class GenreMapper implements ParameterizedRowMapper { + public Genre mapRow(ResultSet rs, int rowNum) throws SQLException { + return new Genre(rs.getString(1), rs.getInt(2), rs.getInt(3)); + } + } +} 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..4deca18e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/MusicFolderDao.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 . + + 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 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()); + + Integer id = queryForInt("select max(id) from music_folder", 0); + update("insert into music_folder_user (music_folder_id, username) select ?, username from user", id); + 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()); + } + + public List getMusicFoldersForUser(String username) { + String sql = "select " + prefix(COLUMNS, "music_folder") + " from music_folder, music_folder_user " + + "where music_folder.id = music_folder_user.music_folder_id and music_folder_user.username = ?"; + return query(sql, rowMapper, username); + } + + public void setMusicFoldersForUser(String username, List musicFolderIds) { + update("delete from music_folder_user where username = ?", username); + for (Integer musicFolderId : musicFolderIds) { + update("insert into music_folder_user(music_folder_id, username) values (?, ?)", musicFolderId, username); + } + } + + private static class MusicFolderRowMapper implements ParameterizedRowMapper { + 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/PlayQueueDao.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayQueueDao.java new file mode 100644 index 00000000..78abcb09 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayQueueDao.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 . + * + * Copyright 2015 (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.SavedPlayQueue; + +/** + * Provides database services for play queues + * + * @author Sindre Mehus + */ +public class PlayQueueDao extends AbstractDao { + + private static final String COLUMNS = "id, username, current, position_millis, changed, changed_by"; + private final RowMapper rowMapper = new PlayQueueMapper(); + + public synchronized SavedPlayQueue getPlayQueue(String username) { + SavedPlayQueue playQueue = queryOne("select " + COLUMNS + " from play_queue where username=?", rowMapper, username); + if (playQueue == null) { + return null; + } + List mediaFileIds = queryForInts("select media_file_id from play_queue_file where play_queue_id = ?", playQueue.getId()); + playQueue.setMediaFileIds(mediaFileIds); + return playQueue; + } + + public synchronized void savePlayQueue(SavedPlayQueue playQueue) { + update("delete from play_queue where username=?", playQueue.getUsername()); + update("insert into play_queue(" + COLUMNS + ") values (" + questionMarks(COLUMNS) + ")", + null, playQueue.getUsername(), playQueue.getCurrentMediaFileId(), playQueue.getPositionMillis(), + playQueue.getChanged(), playQueue.getChangedBy()); + int id = queryForInt("select max(id) from play_queue", 0); + playQueue.setId(id); + + for (Integer mediaFileId : playQueue.getMediaFileIds()) { + update("insert into play_queue_file(play_queue_id, media_file_id) values (?, ?)", id, mediaFileId); + } + } + + private static class PlayQueueMapper implements ParameterizedRowMapper { + public SavedPlayQueue mapRow(ResultSet rs, int rowNum) throws SQLException { + return new SavedPlayQueue(rs.getInt(1), + rs.getString(2), + null, + rs.getInt(3), + rs.getLong(4), + rs.getTimestamp(5), + rs.getString(6)); + } + } +} 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..04b1d9a3 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PlayerDao.java @@ -0,0 +1,192 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 playlists = Collections.synchronizedMap(new HashMap()); + + /** + * Returns all players. + * + * @return Possibly empty list of all users. + */ + public List 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 null. + * @return All relevant players. + */ + public List 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 null 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(), CoverArtScheme.MEDIUM.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 = ?," + + "transcode_scheme = ?, " + + "dynamic_ip = ?, " + + "technology = ?, " + + "client_id = ? " + + "where id = ?"; + update(sql, player.getName(), player.getType(), player.getUsername(), + player.getIpAddress(), player.isAutoControlEnabled(), + player.getLastSeen(), 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 { + 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++)); + col++; // Ignore cover art scheme. + 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..b26f986f --- /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 . + + 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 getReadablePlaylistsForUser(String username) { + + List result1 = getWritablePlaylistsForUser(username); + List result2 = query("select " + COLUMNS + " from playlist where is_public", rowMapper); + List 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 map = new TreeMap(); + 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(map.values()); + } + + public List 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 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.isShared(), 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 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 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.isShared(), playlist.getName(), playlist.getComment(), + new Date(), playlist.getImportedFrom(), playlist.getId()); + } + + private static class PlaylistMapper implements ParameterizedRowMapper { + 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..2765920b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/PodcastDao.java @@ -0,0 +1,191 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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, image_url, 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.getImageUrl(), + 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 getAllChannels() { + String sql = "select " + CHANNEL_COLUMNS + " from podcast_channel"; + return query(sql, channelRowMapper); + } + + /** + * Returns a single Podcast channel. + */ + public PodcastChannel getChannel(int channelId) { + String sql = "select " + CHANNEL_COLUMNS + " from podcast_channel where id=?"; + return queryOne(sql, channelRowMapper, channelId); + } + + /** + * 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=?, image_url=?, status=?, error_message=? where id=?"; + update(sql, channel.getUrl(), channel.getTitle(), channel.getDescription(), channel.getImageUrl(), + 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 getEpisodes(int channelId) { + String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where channel_id = ? " + + "and status != ? order by publish_date desc"; + return query(sql, episodeRowMapper, channelId, PodcastStatus.DELETED); + } + + /** + * Returns the N newest episodes. + * + * @return Possibly empty list of the newest Podcast episodes, sorted in + * reverse chronological order (newest episode first). + */ + public List getNewestEpisodes(int count) { + String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where status = ? and publish_date is not null " + + "order by publish_date desc limit ?"; + return query(sql, episodeRowMapper, PodcastStatus.COMPLETED, count); + } + + /** + * Returns the Podcast episode with the given ID. + * + * @param episodeId The Podcast episode ID. + * @return The episode or null if not found. + */ + public PodcastEpisode getEpisode(int episodeId) { + String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where id=?"; + return queryOne(sql, episodeRowMapper, episodeId); + } + + public PodcastEpisode getEpisodeByUrl(String url) { + String sql = "select " + EPISODE_COLUMNS + " from podcast_episode where url=?"; + return queryOne(sql, episodeRowMapper, url); + } + + /** + * 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), rs.getString(5), + PodcastStatus.valueOf(rs.getString(6)), rs.getString(7)); + } + } + + private static class PodcastEpisodeRowMapper implements ParameterizedRowMapper { + 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..fed91575 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/RatingDao.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.dao.EmptyResultDataAccessException; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; + +import static net.sourceforge.subsonic.domain.MediaFile.MediaType.ALBUM; + +/** + * Provides database services for ratings. + * + * @author Sindre Mehus + */ +public class RatingDao extends AbstractDao { + + /** + * Returns paths for the highest rated albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return Paths for the highest rated albums. + */ + public List getHighestRatedAlbums(final int offset, final int count, final List musicFolders) { + if (count < 1 || musicFolders.isEmpty()) { + return Collections.emptyList(); + } + + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("count", count); + put("offset", offset); + }}; + + String sql = "select user_rating.path from user_rating, media_file " + + "where user_rating.path=media_file.path and media_file.present and media_file.type = :type and media_file.folder in (:folders) " + + "group by path " + + "order by avg(rating) desc limit :count offset :offset"; + return namedQueryForStrings(sql, args); + } + + /** + * 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 null 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 null 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 null 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; + } + } + + public int getRatedAlbumCount(final String username, final List musicFolders) { + if (musicFolders.isEmpty()) { + return 0; + } + Map args = new HashMap() {{ + put("type", ALBUM.name()); + put("folders", MusicFolder.toPathList(musicFolders)); + put("username", username); + }}; + + return namedQueryForInt("select count(*) from user_rating, media_file " + + "where media_file.path = user_rating.path " + + "and media_file.type = :type " + + "and media_file.present " + + "and media_file.folder in (:folders) " + + "and user_rating.username = :username", + 0, args); + } +} 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..a1e8cae3 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/ShareDao.java @@ -0,0 +1,144 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; +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.domain.MusicFolder; +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 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 getSharedFiles(final int shareId, final List musicFolders) { + if (musicFolders.isEmpty()) { + return Collections.emptyList(); + } + Map args = new HashMap() {{ + put("shareId", shareId); + put("folders", MusicFolder.toPathList(musicFolders)); + }}; + return namedQuery("select share_file.path from share_file, media_file where share_id = :shareId and " + + "share_file.path = media_file.path and media_file.present and media_file.folder in (:folders)", + shareFileRowMapper, args); + } + + /** + * 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 { + 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 { + 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 . + + 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 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 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 { + 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..cf281d03 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/UserDao.java @@ -0,0 +1,360 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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.AlbumListType; +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, " + + "song_notification, main_track_number, main_artist, main_album, main_genre, " + + "main_year, main_bit_rate, main_duration, main_format, main_file_size, " + + "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, show_artist_info, auto_hide_play_queue, " + + "view_as_list, default_album_list, queue_following_songs, show_side_bar"; + + 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 null 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 null 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 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."); + } + + update("delete from user_role where username=?", username); + update("delete from player where username=?", username); + update("delete from user where username=?", 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 null 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(), + settings.isSongNotificationEnabled(), main.isTrackNumberVisible(), + main.isArtistVisible(), main.isAlbumVisible(), main.isGenreVisible(), main.isYearVisible(), + main.isBitRateVisible(), main.isDurationVisible(), main.isFormatVisible(), main.isFileSizeVisible(), + 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(), settings.isShowArtistInfoEnabled(), settings.isAutoHidePlayQueue(), + settings.isViewAsList(), settings.getDefaultAlbumList().getId(), settings.isQueueFollowingSongs(), + settings.isShowSideBar()}); + } + + 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 { + 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 { + 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.setSongNotificationEnabled(rs.getBoolean(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().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++)); + settings.setShowArtistInfoEnabled(rs.getBoolean(col++)); + settings.setAutoHidePlayQueue(rs.getBoolean(col++)); + settings.setViewAsList(rs.getBoolean(col++)); + settings.setDefaultAlbumList(AlbumListType.fromId(rs.getString(col++))); + settings.setQueueFollowingSongs(rs.getBoolean(col++)); + settings.setShowSideBar(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..20c3d9eb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/Schema.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 . + + 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; + } + + + protected boolean rowExists(JdbcTemplate template, String whereClause, String table) { + try { + int rowCount = template.queryForInt("select count(*) from " + table + " where " + whereClause); + return rowCount > 0; + } catch (Exception x) { + return false; + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema25.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema25.java new file mode 100644 index 00000000..55fbb895 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema25.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +/** + * 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")) { + + // Increase data file limit. See http://www.hsqldb.org/doc/guide/ch04.html + template.execute("set property \"hsqldb.cache_file_scale\" 8"); + + 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/hsql/Schema26.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema26.java new file mode 100644 index 00000000..c375e576 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema26.java @@ -0,0 +1,111 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.dao.schema.Schema; +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/hsql/Schema27.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema27.java new file mode 100644 index 00000000..55050143 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema27.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema28.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema28.java new file mode 100644 index 00000000..0f7b9143 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema28.java @@ -0,0 +1,112 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema29.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema29.java new file mode 100644 index 00000000..9d9631ce --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema29.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema30.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema30.java new file mode 100644 index 00000000..ec7d85a8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema30.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.*; +import net.sourceforge.subsonic.dao.schema.Schema; +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/hsql/Schema31.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema31.java new file mode 100644 index 00000000..25b6724c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema31.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema32.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema32.java new file mode 100644 index 00000000..683d4a29 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema32.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema33.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema33.java new file mode 100644 index 00000000..bbeb7000 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema33.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema34.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema34.java new file mode 100644 index 00000000..a00c38cb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema34.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema35.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema35.java new file mode 100644 index 00000000..09faf644 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema35.java @@ -0,0 +1,153 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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("/net/sourceforge/subsonic/dao/schema/" + 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/hsql/Schema36.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema36.java new file mode 100644 index 00000000..1ddd1fd7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema36.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema37.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema37.java new file mode 100644 index 00000000..ffb342cc --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema37.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema38.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema38.java new file mode 100644 index 00000000..bddeaee4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema38.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema40.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema40.java new file mode 100644 index 00000000..758daddb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema40.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema43.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema43.java new file mode 100644 index 00000000..f66382f0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema43.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema45.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema45.java new file mode 100644 index 00000000..a012cb11 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema45.java @@ -0,0 +1,78 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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/hsql/Schema46.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema46.java new file mode 100644 index 00000000..9410e6fb --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema46.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +/** + * 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(name, source_formats, target_format, step1) values('mp3 audio'," + + "'ogg oga aac m4a flac wav wma aif aiff ape mpc shn', 'mp3', " + + "'ffmpeg -i %s -ab %bk -v 0 -f mp3 -')"); + + template.execute("insert into transcoding2(name, source_formats, target_format, step1) values('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 -')"); + + 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/hsql/Schema47.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema47.java new file mode 100644 index 00000000..f2e7974e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema47.java @@ -0,0 +1,262 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +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."); + } + + // Added in 4.7.beta3 + if (!rowExists(template, "table_name='ALBUM' and column_name='NAME' and ordinal_position=1", + "information_schema.system_indexinfo")) { + template.execute("create index idx_album_name on album(name)"); + } + + 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."); + } + + if (!tableExists(template, "bookmark")) { + LOG.info("Database table 'bookmark' not found. Creating it."); + template.execute("create table bookmark (" + + "id identity," + + "media_file_id int not null," + + "position_millis bigint not null," + + "username varchar not null," + + "comment varchar," + + "created datetime not null," + + "changed 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_bookmark_media_file_id on bookmark(media_file_id)"); + template.execute("create index idx_bookmark_username on bookmark(username)"); + + LOG.info("Database table 'bookmark' was created successfully."); + } + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema49.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema49.java new file mode 100644 index 00000000..a9cfe5b1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema49.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 4.9. + * + * @author Sindre Mehus + */ +public class Schema49 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema49.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 21") == 0) { + LOG.info("Updating database schema to version 21."); + template.execute("insert into version values (21)"); + } + + if (!columnExists(template, "year", "album")) { + LOG.info("Database column 'album.year' not found. Creating it."); + template.execute("alter table album add year int"); + LOG.info("Database column 'album.year' was added successfully."); + } + + if (!columnExists(template, "genre", "album")) { + LOG.info("Database column 'album.genre' not found. Creating it."); + template.execute("alter table album add genre varchar"); + LOG.info("Database column 'album.genre' was added successfully."); + } + + if (!tableExists(template, "genre")) { + LOG.info("Database table 'genre' not found. Creating it."); + template.execute("create table genre (" + + "name varchar not null," + + "song_count int not null)"); + + LOG.info("Database table 'genre' was created successfully."); + } + + if (!columnExists(template, "album_count", "genre")) { + LOG.info("Database column 'genre.album_count' not found. Creating it."); + template.execute("alter table genre add album_count int default 0 not null"); + LOG.info("Database column 'genre.album_count' was added successfully."); + } + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema50.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema50.java new file mode 100644 index 00000000..917bf6e0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema50.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 5.0. + * + * @author Sindre Mehus + */ +public class Schema50 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema50.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 22") == 0) { + LOG.info("Updating database schema to version 22."); + template.execute("insert into version values (22)"); + + template.execute("insert into transcoding2(name, source_formats, target_format, step1, default_active) values('mkv video', " + + "'avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts', 'mkv', " + + "'ffmpeg -ss %o -i %s -c:v libx264 -preset superfast -b:v %bk -c:a libvorbis -f matroska -threads 0 -', 'true')"); + + template.execute("insert into player_transcoding2(player_id, transcoding_id) " + + "select distinct p.id, t.id from player p, transcoding2 t where t.name='mkv video'"); + LOG.info("Added mkv transcoding."); + } + + if (!columnExists(template, "song_notification", "user_settings")) { + LOG.info("Database column 'user_settings.song_notification' not found. Creating it."); + template.execute("alter table user_settings add song_notification boolean default true not null"); + LOG.info("Database column 'user_settings.song_notification' was added successfully."); + } + + // Added in 5.0.beta2 + if (template.queryForInt("select count(*) from version where version = 23") == 0) { + LOG.info("Updating database schema to version 23."); + template.execute("insert into version values (23)"); + template.execute("update transcoding2 set step1='ffmpeg -i %s -map 0:0 -b:a %bk -v 0 -f mp3 -' where name='mp3 audio'"); + } + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema51.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema51.java new file mode 100644 index 00000000..87295d6d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema51.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 . + * + * Copyright 2014 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 5.1. + * + * @author Sindre Mehus + */ +public class Schema51 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema51.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 23") == 0) { + LOG.info("Updating database schema to version 23."); + template.execute("insert into version values (23)"); + } + + if (!columnExists(template, "show_artist_info", "user_settings")) { + LOG.info("Database column 'user_settings.show_artist_info' not found. Creating it."); + template.execute("alter table user_settings add show_artist_info boolean default true not null"); + LOG.info("Database column 'user_settings.show_artist_info' was added successfully."); + } + + if (!columnExists(template, "auto_hide_play_queue", "user_settings")) { + LOG.info("Database column 'user_settings.auto_hide_play_queue' not found. Creating it."); + template.execute("alter table user_settings add auto_hide_play_queue boolean default true not null"); + LOG.info("Database column 'user_settings.auto_hide_play_queue' was added successfully."); + } + + if (!columnExists(template, "view_as_list", "user_settings")) { + LOG.info("Database column 'user_settings.view_as_list' not found. Creating it."); + template.execute("alter table user_settings add view_as_list boolean default false not null"); + LOG.info("Database column 'user_settings.view_as_list' was added successfully."); + } + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema52.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema52.java new file mode 100644 index 00000000..36e40c0f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema52.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 5.2. + * + * @author Sindre Mehus + */ +public class Schema52 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema52.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 24") == 0) { + LOG.info("Updating database schema to version 24."); + template.execute("insert into version values (24)"); + } + + if (!tableExists(template, "music_folder_user")) { + LOG.info("Database table 'music_folder_user' not found. Creating it."); + template.execute("create table music_folder_user (" + + "music_folder_id int not null," + + "username varchar not null, " + + "foreign key (username) references user(username) on delete cascade, " + + "foreign key (music_folder_id) references music_folder(id) on delete cascade)"); + template.execute("create index idx_music_folder_user_username on music_folder_user(username)"); + template.execute("insert into music_folder_user select music_folder.id, user.username from music_folder, user"); + LOG.info("Database table 'music_folder_user' was created successfully."); + } + + if (!columnExists(template, "folder_id", "album")) { + LOG.info("Database column 'album.folder_id' not found. Creating it."); + template.execute("alter table album add folder_id int"); + LOG.info("Database column 'album.folder_id' was added successfully."); + } + + if (!tableExists(template, "play_queue")) { + LOG.info("Database table 'play_queue' not found. Creating it."); + template.execute("create table play_queue (" + + "id identity," + + "username varchar not null," + + "current int," + + "position_millis bigint," + + "changed datetime not null," + + "changed_by varchar not null," + + "foreign key (username) references user(username) on delete cascade)"); + LOG.info("Database table 'play_queue' was created successfully."); + } + + if (!tableExists(template, "play_queue_file")) { + LOG.info("Database table 'play_queue_file' not found. Creating it."); + template.execute("create cached table play_queue_file (" + + "id identity," + + "play_queue_id int not null," + + "media_file_id int not null," + + "foreign key (play_queue_id) references play_queue(id) on delete cascade," + + "foreign key (media_file_id) references media_file(id) on delete cascade)"); + + LOG.info("Database table 'play_queue_file' was created successfully."); + } + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema53.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema53.java new file mode 100644 index 00000000..8578a6c1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/dao/schema/hsql/Schema53.java @@ -0,0 +1,83 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.dao.schema.hsql; + +import org.springframework.jdbc.core.JdbcTemplate; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.schema.Schema; +import net.sourceforge.subsonic.domain.AlbumListType; + +/** + * Used for creating and evolving the database schema. + * This class implements the database schema for Subsonic version 5.3. + * + * @author Sindre Mehus + */ +public class Schema53 extends Schema { + + private static final Logger LOG = Logger.getLogger(Schema53.class); + + @Override + public void execute(JdbcTemplate template) { + + if (template.queryForInt("select count(*) from version where version = 25") == 0) { + LOG.info("Updating database schema to version 25."); + template.execute("insert into version values (25)"); + } + + if (!rowExists(template, "table_name='PODCAST_EPISODE' and column_name='URL' and ordinal_position=1", + "information_schema.system_indexinfo")) { + template.execute("create index idx_podcast_episode_url on podcast_episode(url)"); + LOG.info("Created index for podcast_episode.url"); + } + + if (!columnExists(template, "default_album_list", "user_settings")) { + LOG.info("Database column 'user_settings.default_album_list' not found. Creating it."); + template.execute("alter table user_settings add default_album_list varchar default '" + + AlbumListType.RANDOM.getId() + "' not null"); + LOG.info("Database column 'user_settings.default_album_list' was added successfully."); + } + + if (!columnExists(template, "queue_following_songs", "user_settings")) { + LOG.info("Database column 'user_settings.queue_following_songs' not found. Creating it."); + template.execute("alter table user_settings add queue_following_songs boolean default true not null"); + LOG.info("Database column 'user_settings.queue_following_songs' was added successfully."); + } + + if (!columnExists(template, "image_url", "podcast_channel")) { + LOG.info("Database column 'podcast_channel.image_url' not found. Creating it."); + template.execute("alter table podcast_channel add image_url varchar"); + LOG.info("Database column 'podcast_channel.image_url' was added successfully."); + } + + if (!columnExists(template, "show_side_bar", "user_settings")) { + LOG.info("Database column 'user_settings.show_side_bar' not found. Creating it."); + template.execute("alter table user_settings add show_side_bar boolean default true not null"); + LOG.info("Database column 'user_settings.show_side_bar' was added successfully."); + } + + if (!columnExists(template, "folder_id", "artist")) { + LOG.info("Database column 'artist.folder_id' not found. Creating it."); + template.execute("alter table artist add folder_id int"); + LOG.info("Database column 'artist.folder_id' was added 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..93bea91c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Album.java @@ -0,0 +1,197 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 Integer year; + private String genre; + private int playCount; + private Date lastPlayed; + private String comment; + private Date created; + private Date lastScanned; + private boolean present; + private Integer folderId; + + public Album() { + } + + public Album(int id, String path, String name, String artist, int songCount, int durationSeconds, String coverArtPath, + Integer year, String genre, int playCount, Date lastPlayed, String comment, Date created, Date lastScanned, + boolean present, Integer folderId) { + this.id = id; + this.path = path; + this.name = name; + this.artist = artist; + this.songCount = songCount; + this.durationSeconds = durationSeconds; + this.coverArtPath = coverArtPath; + this.year = year; + this.genre = genre; + this.playCount = playCount; + this.lastPlayed = lastPlayed; + this.comment = comment; + this.created = created; + this.lastScanned = lastScanned; + this.folderId = folderId; + 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 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 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; + } + + public void setFolderId(Integer folderId) { + this.folderId = folderId; + } + + public Integer getFolderId() { + return folderId; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AlbumListType.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AlbumListType.java new file mode 100644 index 00000000..7508588e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/AlbumListType.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public enum AlbumListType { + + RANDOM("random", "Random"), + NEWEST("newest", "Recently Added"), + STARRED("starred", "Starred"), + HIGHEST("highest", "Top Rated"), + FREQUENT("frequent", "Most Played"), + RECENT("recent", "Recently Played"), + DECADE("decade", "By Decade"), + GENRE("genre", "By Genre"), + ALPHABETICAL("alphabetical", "All"); + + private final String id; + private final String description; + + AlbumListType(String id, String description) { + this.id = id; + this.description = description; + } + + public String getId() { + return id; + } + + public String getDescription() { + return description; + } + + public static AlbumListType fromId(String id) { + for (AlbumListType albumListType : values()) { + if (albumListType.id.equals(id)) { + return albumListType; + } + } + return null; + } +} 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..ef50da28 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Artist.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 . + + 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; + private Integer folderId; + + public Artist() { + } + + public Artist(int id, String name, String coverArtPath, int albumCount, Date lastScanned, boolean present, Integer folderId) { + this.id = id; + this.name = name; + this.coverArtPath = coverArtPath; + this.albumCount = albumCount; + this.lastScanned = lastScanned; + this.present = present; + this.folderId = folderId; + } + + 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; + } + + public void setFolderId(Integer folderId) { + this.folderId = folderId; + } + + public Integer getFolderId() { + return folderId; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/ArtistBio.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/ArtistBio.java new file mode 100644 index 00000000..02f8f40a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/ArtistBio.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 . + * + * Copyright 2014 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class ArtistBio { + + private final String biography; + private final String musicBrainzId; + private final String lastFmUrl; + private final String smallImageUrl; + private final String mediumImageUrl; + private final String largeImageUrl; + + public ArtistBio(String biography, String musicBrainzId, String lastFmUrl, String smallImageUrl, + String mediumImageUrl, String largeImageUrl) { + this.biography = biography; + this.musicBrainzId = musicBrainzId; + this.lastFmUrl = lastFmUrl; + this.smallImageUrl = smallImageUrl; + this.mediumImageUrl = mediumImageUrl; + this.largeImageUrl = largeImageUrl; + } + + public String getBiography() { + return biography; + } + + public String getMusicBrainzId() { + return musicBrainzId; + } + + public String getLastFmUrl() { + return lastFmUrl; + } + + public String getSmallImageUrl() { + return smallImageUrl; + } + + public String getMediumImageUrl() { + return mediumImageUrl; + } + + public String getLargeImageUrl() { + return largeImageUrl; + } +} 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 . + + 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 . + + 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/Bookmark.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Bookmark.java new file mode 100644 index 00000000..9daf6df1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Bookmark.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.util.Date; + +/** + * A bookmark within a media file, for a given user. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class Bookmark { + + private int id; + private int mediaFileId; + private long positionMillis; + private String username; + private String comment; + private Date created; + private Date changed; + + public Bookmark(int id, int mediaFileId, long positionMillis, String username, String comment, Date created, Date changed) { + this.id = id; + this.mediaFileId = mediaFileId; + this.positionMillis = positionMillis; + this.username = username; + this.comment = comment; + this.created = created; + this.changed = changed; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getMediaFileId() { + return mediaFileId; + } + + public void setMediaFileId(int mediaFileId) { + this.mediaFileId = mediaFileId; + } + + public long getPositionMillis() { + return positionMillis; + } + + public void setPositionMillis(long positionMillis) { + this.positionMillis = positionMillis; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + 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; + } +} 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 . + + 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..8e43f875 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/CoverArtScheme.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 . + + 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(110), + MEDIUM(160), + LARGE(300); + + 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/Genre.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Genre.java new file mode 100644 index 00000000..f6dcce1c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Genre.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +/** + * Represents a musical genre. + * + * @author Sindre Mehus + * @version $Revision: 1.2 $ $Date: 2005/12/25 13:48:46 $ + */ +public class Genre { + + private final String name; + private int songCount; + private int albumCount; + + public Genre(String name) { + this.name = name; + } + + public Genre(String name, int songCount, int albumCount) { + this.name = name; + this.songCount = songCount; + this.albumCount = albumCount; + } + + public String getName() { + return name; + } + + public int getSongCount() { + return songCount; + } + + public int getAlbumCount() { + return albumCount; + } + + public void incrementAlbumCount() { + albumCount++; + } + + public void incrementSongCount() { + songCount++; + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Genres.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Genres.java new file mode 100644 index 00000000..3f249a43 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Genres.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Represents a list of genres. + * + * @author Sindre Mehus + * @version $Revision: 1.2 $ $Date: 2005/12/25 13:48:46 $ + */ +public class Genres { + + private final Map genres = new HashMap(); + + public void incrementAlbumCount(String genreName) { + Genre genre = getOrCreateGenre(genreName); + genre.incrementAlbumCount(); + } + + public void incrementSongCount(String genreName) { + Genre genre = getOrCreateGenre(genreName); + genre.incrementSongCount(); + } + + private Genre getOrCreateGenre(String genreName) { + Genre genre = genres.get(genreName); + if (genre == null) { + genre = new Genre(genreName); + genres.put(genreName, genre); + } + return genre; + } + + public List getGenres() { + return new ArrayList(genres.values()); + } +} \ No newline at end of file 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 . + + 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/LicenseInfo.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/LicenseInfo.java new file mode 100644 index 00000000..4e61ce79 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/LicenseInfo.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.util.Date; + +import org.apache.commons.lang.StringUtils; + +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Controller for the "Podcast receiver" page. + * + * @author Sindre Mehus + */ +public class LicenseInfo { + + private String licenseEmail; + private boolean licenseValid; + private final Date trialExpires; + private long trialDaysLeft; + private final Date licenseExpires; + + public LicenseInfo(String licenseEmail, boolean licenseValid, Date trialExpires, + long trialDaysLeft, Date licenseExpires) { + this.licenseEmail = licenseEmail; + this.licenseValid = licenseValid; + this.trialExpires = trialExpires; + this.trialDaysLeft = trialDaysLeft; + this.licenseExpires = licenseExpires; + } + + public String getLicenseEmail() { + return licenseEmail; + } + + public void setLicenseEmail(String licenseEmail) { + this.licenseEmail = StringUtils.trimToNull(licenseEmail); + } + + public boolean isLicenseValid() { + return licenseValid; + } + + public void setLicenseValid(boolean licenseValid) { + this.licenseValid = licenseValid; + } + + public boolean isTrial() { + return trialExpires != null && !licenseValid; + } + + public boolean isTrialExpired() { + return trialExpires != null && (trialExpires.before(new Date()) || trialDaysLeft > SettingsService.TRIAL_DAYS + 1); + } + + public boolean isLicenseOrTrialValid() { + return isLicenseValid() || !isTrialExpired(); + } + + public Date getTrialExpires() { + return trialExpires; + } + + public long getTrialDaysLeft() { + return trialDaysLeft; + } + + public Date getLicenseExpires() { + return licenseExpires; + } +} 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..2ac11eed --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFile.java @@ -0,0 +1,473 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.io.File; +import java.util.Date; +import java.util.List; + +import org.apache.commons.io.FilenameUtils; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; + +import net.sourceforge.subsonic.util.FileUtil; + +/** + * 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; + private int version; + + 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, int version) { + 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; + this.version = version; + } + + 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; + } + + public int getVersion() { + return version; + } + + @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 List toIdList(List from) { + return Lists.transform(from, toId()); + } + + public static Function toId() { + return new Function() { + @Override + public Integer apply(MediaFile from) { + return from.getId(); + } + }; + } + + 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..70caf0fe --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaFileComparator.java @@ -0,0 +1,100 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.util.Comparator; + +import static net.sourceforge.subsonic.domain.MediaFile.MediaType.DIRECTORY; + +/** + * Comparator for sorting media files. + */ +public class MediaFileComparator implements Comparator { + + 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()) { + int n = a.getName().compareToIgnoreCase(b.getName()); + return n == 0 ? a.getPath().compareToIgnoreCase(b.getPath()) : n; // To make it consistent to MediaFile.equals() + } + + // 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.getPath().compareToIgnoreCase(b.getPath()); + } + + private > 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..a01fd5e9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MediaLibraryStatistics.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * 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 static final Logger LOG = Logger.getLogger(MediaLibraryStatistics.class); + + 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 MediaLibraryStatistics() { + } + + public void reset() { + artistCount = 0; + albumCount = 0; + songCount = 0; + totalLengthInBytes = 0; + totalDurationInSeconds = 0; + } + + public void incrementArtists(int n) { + artistCount += n; + } + + public void incrementAlbums(int n) { + albumCount += n; + } + + public void incrementSongs(int n) { + songCount += n; + } + + public void incrementTotalLengthInBytes(long n) { + totalLengthInBytes += n; + } + + public void incrementTotalDurationInSeconds(long n) { + totalDurationInSeconds += n; + } + + 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; + } + + public String format() { + return artistCount + " " + albumCount + " " + songCount + " " + totalLengthInBytes + " " + totalDurationInSeconds; + } + + public static MediaLibraryStatistics parse(String s) { + try { + String[] strings = StringUtil.split(s); + return new MediaLibraryStatistics( + Integer.parseInt(strings[0]), + Integer.parseInt(strings[1]), + Integer.parseInt(strings[2]), + Long.parseLong(strings[3]), + Long.parseLong(strings[4])); + } catch (Exception e) { + LOG.warn("Failed to parse media library statistics: " + s); + return new MediaLibraryStatistics(); + } + } +} 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..c539d21d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolder.java @@ -0,0 +1,196 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.io.File; +import java.io.Serializable; +import java.util.Date; +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.base.Objects; +import com.google.common.collect.Lists; + +/** + * 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; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return Objects.equal(id, ((MusicFolder) o).id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + + public static List toIdList(List from) { + return Lists.transform(from, toId()); + } + + public static List toPathList(List from) { + return Lists.transform(from, toPath()); + } + + public static Function toId() { + return new Function() { + @Override + public Integer apply(MusicFolder from) { + return from.getId(); + } + }; + } + + public static Function toPath() { + return new Function() { + @Override + public String apply(MusicFolder from) { + return from.getPath().getPath(); + } + }; + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolderContent.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolderContent.java new file mode 100644 index 00000000..444e584e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicFolderContent.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.domain; + +import java.util.List; +import java.util.SortedMap; + +/** +* @author Sindre Mehus +* @version $Id$ +*/ +public class MusicFolderContent { + + private final SortedMap> indexedArtists; + private final List singleSongs; + + public MusicFolderContent(SortedMap> indexedArtists, List singleSongs) { + this.indexedArtists = indexedArtists; + this.singleSongs = singleSongs; + } + + public SortedMap> getIndexedArtists() { + return indexedArtists; + } + + public List getSingleSongs() { + return singleSongs; + } + +} 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..33cf1fb3 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/MusicIndex.java @@ -0,0 +1,176 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.io.Serializable; +import java.text.CollationKey; +import java.text.Collator; +import java.util.ArrayList; +import java.util.List; + +/** + * A music index is a mapping from an index string to a list of prefixes. A complete index consists of a list of + * MusicIndex instances.

+ *

+ * For a normal alphabetical index, such a mapping would typically be "A" -> ["A"]. The index can also be used + * to group less frequently used letters, such as "X-Å" -> ["X", "Y", "Z", "Æ", "Ø", "Å"], or to make multiple + * indexes for frequently used letters, such as "SA" -> ["SA"] and "SO" -> ["SO"]

+ *

+ * 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 prefixes = new ArrayList(); + + /** + * 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 getPrefixes() { + return prefixes; + } + + /** + * Returns whether this object is equal to another one. + * + * @param o Object to compare to. + * @return true if, and only if, the other object is a MusicIndex 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 abstract static class SortableArtist implements Comparable { + + private final String name; + private final String sortableName; + private final CollationKey collationKey; + + public SortableArtist(String name, String sortableName, Collator collator) { + this.name = name; + this.sortableName = sortableName; + collationKey = collator.getCollationKey(sortableName); + } + + public String getName() { + return name; + } + + public String getSortableName() { + return sortableName; + } + + public int compareTo(SortableArtist other) { + return collationKey.compareTo(other.collationKey); + } + } + + public static class SortableArtistWithMediaFiles extends SortableArtist { + + private final List mediaFiles = new ArrayList(); + + public SortableArtistWithMediaFiles(String name, String sortableName, Collator collator) { + super(name, sortableName, collator); + } + + public void addMediaFile(MediaFile mediaFile) { + mediaFiles.add(mediaFile); + } + + public List getMediaFiles() { + return mediaFiles; + } + } + + public static class SortableArtistWithArtist extends SortableArtist { + + private final Artist artist; + + public SortableArtistWithArtist(String name, String sortableName, Artist artist, Collator collator) { + super(name, sortableName, collator); + this.artist = artist; + } + + public Artist getArtist() { + return artist; + } + } +} 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..8b52db81 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayQueue.java @@ -0,0 +1,456 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import org.apache.commons.lang.StringUtils; + +/** + * A play queue is a list of music files that are associated to a remote player. + * + * @author Sindre Mehus + */ +public class PlayQueue { + + private List files = new ArrayList(); + 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 filesBackup = new ArrayList(); + private int indexBackup = 0; + + /** + * Returns the user-defined name of the playlist. + * + * @return The name of the playlist, or null 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 null 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 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 mediaFiles The music files to add. + * @param index Where to add them. + * @throws IOException If an I/O error occurs. + */ + public synchronized void addFilesAt(Iterable mediaFiles, int index) throws IOException { + makeBackup(); + for (MediaFile mediaFile : mediaFiles) { + files.add(index, mediaFile); + index++; + } + 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 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 comparator = new Comparator() { + 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); + } + } + + /** + * Rearranges the playlist using the provided indexes. + */ + public synchronized void rearrange(int[] indexes) { + makeBackup(); + if (indexes == null || indexes.length != size()) { + return; + } + + MediaFile[] newFiles = new MediaFile[files.size()]; + for (int i = 0; i < indexes.length; i++) { + newFiles[i] = files.get(indexes[i]); + } + for (int i = 0; i < indexes.length; i++) { + if (index == indexes[i]) { + index = i; + break; + } + } + + files.clear(); + files.addAll(Arrays.asList(newFiles)); + } + + /** + * 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 filesTmp = new ArrayList(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 null 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 null 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(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/PlayStatus.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayStatus.java new file mode 100644 index 00000000..32d58c15 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PlayStatus.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.domain; + +import java.util.Date; + +/** + * Represents the playback of a track, possibly remote (e.g., a cached song on a mobile phone). + * + * @author Sindre Mehus + * @version $Id$ + */ +public class PlayStatus { + + private final MediaFile mediaFile; + private final Player player; + private final Date time; + + private final static long TTL_MILLIS = 6L * 60L * 60L * 1000L; // 6 hours + + public PlayStatus(MediaFile mediaFile, Player player, Date time) { + this.mediaFile = mediaFile; + this.player = player; + this.time = time; + } + + public MediaFile getMediaFile() { + return mediaFile; + } + + public Player getPlayer() { + return player; + } + + public Date getTime() { + return time; + } + + public boolean isExpired() { + return System.currentTimeMillis() > time.getTime() + TTL_MILLIS; + } + + public long getMinutesAgo() { + return (System.currentTimeMillis() - time.getTime()) / 1000L / 60L; + } +} 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..450293ee --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Player.java @@ -0,0 +1,319 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 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 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., Player 3 [admin] + * + * @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., Player 3 + * + * @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 . + + 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..e43fb21c --- /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 . + + 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 shared; + 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 shared, String name, String comment, int fileCount, + int durationSeconds, Date created, Date changed, String importedFrom) { + this.id = id; + this.username = username; + this.shared = shared; + 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 isShared() { + return shared; + } + + public void setShared(boolean shared) { + this.shared = shared; + } + + 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..98db7a37 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/PodcastChannel.java @@ -0,0 +1,113 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +/** + * 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 String imageUrl; + private PodcastStatus status; + private String errorMessage; + private Integer mediaFileId; + + public PodcastChannel(Integer id, String url, String title, String description, String imageUrl, + PodcastStatus status, String errorMessage) { + this.id = id; + this.url = url; + this.title = title; + this.description = description; + this.imageUrl = imageUrl; + 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 String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } + + 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 void setMediaFileId(Integer mediaFileId) { + this.mediaFileId = mediaFileId; + } + + public Integer getMediaFileId() { + return mediaFileId; + } +} \ 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..3a9b6741 --- /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 . + + 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 = title; + this.description = 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 . + + 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..c57555bd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/RandomSearchCriteria.java @@ -0,0 +1,73 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.util.List; + +/** + * 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 List musicFolders; + + /** + * Creates a new instance. + * + * @param count Maximum number of songs to return. + * @param genre Only return songs of the given genre. May be null. + * @param fromYear Only return songs released after (or in) this year. May be null. + * @param toYear Only return songs released before (or in) this year. May be null. + * @param musicFolders Only return songs from these music folder. May NOT be null. + */ + public RandomSearchCriteria(int count, String genre, Integer fromYear, Integer toYear, List musicFolders) { + this.count = count; + this.genre = genre; + this.fromYear = fromYear; + this.toYear = toYear; + this.musicFolders = musicFolders; + } + + public int getCount() { + return count; + } + + public String getGenre() { + return genre; + } + + public Integer getFromYear() { + return fromYear; + } + + public Integer getToYear() { + return toYear; + } + + public List getMusicFolders() { + return musicFolders; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SavedPlayQueue.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SavedPlayQueue.java new file mode 100644 index 00000000..5f8d1358 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SavedPlayQueue.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.domain; + +import java.util.Date; +import java.util.List; + +/** + * Used to save the play queue state for a user. + *

+ * Can be used to share the play queue (including currently playing track and position within + * that track) across client apps. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class SavedPlayQueue { + + private Integer id; + private String username; + private List mediaFileIds; + private Integer currentMediaFileId; + private Long positionMillis; + private Date changed; + private String changedBy; + + public SavedPlayQueue(Integer id, String username, List mediaFileIds, Integer currentMediaFileId, + Long positionMillis, Date changed, String changedBy) { + this.id = id; + this.username = username; + this.mediaFileIds = mediaFileIds; + this.currentMediaFileId = currentMediaFileId; + this.positionMillis = positionMillis; + this.changed = changed; + this.changedBy = changedBy; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public List getMediaFileIds() { + return mediaFileIds; + } + + public void setMediaFileIds(List mediaFileIds) { + this.mediaFileIds = mediaFileIds; + } + + public Integer getCurrentMediaFileId() { + return currentMediaFileId; + } + + public void setCurrentMediaFileId(Integer currentMediaFileId) { + this.currentMediaFileId = currentMediaFileId; + } + + public Long getPositionMillis() { + return positionMillis; + } + + public void setPositionMillis(Long positionMillis) { + this.positionMillis = positionMillis; + } + + public Date getChanged() { + return changed; + } + + public void setChanged(Date changed) { + this.changed = changed; + } + + public String getChangedBy() { + return changedBy; + } + + public void setChangedBy(String changedBy) { + this.changedBy = changedBy; + } +} 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..63596d1f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/SearchCriteria.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +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 . + + 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 mediaFiles = new ArrayList(); + private final List artists = new ArrayList(); + private final List albums = new ArrayList(); + + private int offset; + private int totalHits; + + public List getMediaFiles() { + return mediaFiles; + } + + public List getArtists() { + return artists; + } + + public List 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..6c75c5c1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Share.java @@ -0,0 +1,118 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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..37483520 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Theme.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 . + + 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 final String id; + private final String name; + private final String parent; + + public Theme(String id, String name, String parent) { + this.id = id; + this.name = name; + this.parent = parent; + } + + public Theme(String id, String name) { + this(id, name, null); + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getParent() { + return parent; + } +} 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..a43a8a16 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/TranscodeScheme.java @@ -0,0 +1,113 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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 null, 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 null if not found. + */ + public static TranscodeScheme valueOf(int maxBitRate) { + for (TranscodeScheme scheme : values()) { + if (scheme.getMaxBitRate() == maxBitRate) { + return scheme; + } + } + return null; + } + + public static TranscodeScheme fromMaxBitRate(int maxBitRate) { + for (TranscodeScheme transcodeScheme : TranscodeScheme.values()) { + if (maxBitRate == transcodeScheme.getMaxBitRate()) { + return transcodeScheme; + } + } + 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 . + + 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. + *
+ * 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 . + + 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 0 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 terminated status 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 { + + 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/UrlRedirectType.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UrlRedirectType.java new file mode 100644 index 00000000..23c75815 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UrlRedirectType.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.domain; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public enum UrlRedirectType { + NORMAL, + CUSTOM +} 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..fb7e1d7e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/User.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 . + + 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"; + public static final String USERNAME_GUEST = "guest"; + + 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..f0e0fcdd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/UserSettings.java @@ -0,0 +1,382 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.domain; + +import java.util.Date; +import java.util.Locale; + +/** + * 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 showArtistInfoEnabled; + private boolean finalVersionNotificationEnabled; + private boolean betaVersionNotificationEnabled; + private boolean songNotificationEnabled; + private boolean autoHidePlayQueue; + private boolean showSideBar; + private boolean viewAsList; + private boolean queueFollowingSongs; + private AlbumListType defaultAlbumList = AlbumListType.RANDOM; + 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 isShowArtistInfoEnabled() { + return showArtistInfoEnabled; + } + + public void setShowArtistInfoEnabled(boolean showArtistInfoEnabled) { + this.showArtistInfoEnabled = showArtistInfoEnabled; + } + + 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 isSongNotificationEnabled() { + return songNotificationEnabled; + } + + public void setSongNotificationEnabled(boolean songNotificationEnabled) { + this.songNotificationEnabled = songNotificationEnabled; + } + + 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 boolean isAutoHidePlayQueue() { + return autoHidePlayQueue; + } + + public void setAutoHidePlayQueue(boolean autoHidePlayQueue) { + this.autoHidePlayQueue = autoHidePlayQueue; + } + + public boolean isShowSideBar() { + return showSideBar; + } + + public void setShowSideBar(boolean showSideBar) { + this.showSideBar = showSideBar; + } + + public boolean isViewAsList() { + return viewAsList; + } + + public void setViewAsList(boolean viewAsList) { + this.viewAsList = viewAsList; + } + + public AlbumListType getDefaultAlbumList() { + return defaultAlbumList; + } + + public void setDefaultAlbumList(AlbumListType defaultAlbumList) { + this.defaultAlbumList = defaultAlbumList; + } + + 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; + } + + public boolean isQueueFollowingSongs() { + return queueFollowingSongs; + } + + public void setQueueFollowingSongs(boolean queueFollowingSongs) { + this.queueFollowingSongs = queueFollowingSongs; + } + + /** + * Configuration of what information to display about a song. + */ + public static class Visibility { + 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(boolean trackNumberVisible, boolean artistVisible, boolean albumVisible, + boolean genreVisible, boolean yearVisible, boolean bitRateVisible, + boolean durationVisible, boolean formatVisible, boolean fileSizeVisible) { + isTrackNumberVisible = trackNumberVisible; + isArtistVisible = artistVisible; + isAlbumVisible = albumVisible; + isGenreVisible = genreVisible; + isYearVisible = yearVisible; + isBitRateVisible = bitRateVisible; + isDurationVisible = durationVisible; + isFormatVisible = formatVisible; + isFileSizeVisible = fileSizeVisible; + } + + 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..fd329f8a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/Version.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 . + + 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 { + 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; + } + + public int getBeta() { + return beta; + } + + /** + * 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..06793a92 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/domain/VideoTranscodingSettings.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 . + + 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; + private final int duration; + private final boolean hls; + + public VideoTranscodingSettings(int width, int height, int timeOffset, int duration, boolean hls) { + this.width = width; + this.height = height; + this.timeOffset = timeOffset; + this.duration = duration; + this.hls = hls; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + public int getTimeOffset() { + return timeOffset; + } + + public int getDuration() { + return duration; + } + + public boolean isHls() { + return hls; + } +} 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..57e100ab --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/BootstrapVerificationFilter.java @@ -0,0 +1,120 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.filter; + +import net.sourceforge.subsonic.Logger; +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 javax.servlet.http.HttpServletRequest; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 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. + *

+ * (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 static final Logger LOG = Logger.getLogger(BootstrapVerificationFilter.class); + private boolean subsonicHomeVerified = false; + private final AtomicBoolean serverInfoLogged = new AtomicBoolean(); + + 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, "

The directory " + home + " does not exist. Please create it and make it writable, " + + "then restart the servlet container.

" + + "

(You can override the directory location by specifying -Dsubsonic.home=... when " + + "starting the servlet container.)

"); + + } else if (!directoryWritable(home)) { + error(res, "

The directory " + home + " is not writable. Please change file permissions, " + + "then restart the servlet container.

" + + "

(You can override the directory location by specifying -Dsubsonic.home=... when " + + "starting the servlet container.)

"); + + } else { + subsonicHomeVerified = true; + logServerInfo(req); + chain.doFilter(req, res); + } + } + + private void logServerInfo(ServletRequest req) { + if (!serverInfoLogged.getAndSet(true) && req instanceof HttpServletRequest) { + String serverInfo = ((HttpServletRequest) req).getSession().getServletContext().getServerInfo(); + LOG.info("Servlet container: " + serverInfo); + } + } + + 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("" + + "Subsonic Error" + + "" + + "

Subsonic Error

" + + error + + "" + + ""); + } + + 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 . + + 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. + *

+ * 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 result = new HashMap(); + + 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 v = new Vector(); + 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/RESTFilter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RESTFilter.java new file mode 100644 index 00000000..07dca5f0 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/filter/RESTFilter.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.filter; + +import java.io.IOException; + +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 org.springframework.web.bind.ServletRequestBindingException; +import org.springframework.web.util.NestedServletException; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.controller.JAXBWriter; +import net.sourceforge.subsonic.controller.RESTController; + +import static net.sourceforge.subsonic.controller.RESTController.ErrorCode.GENERIC; +import static net.sourceforge.subsonic.controller.RESTController.ErrorCode.MISSING_PARAMETER; + +/** + * Intercepts exceptions thrown by RESTController. + * + * Also adds the CORS response header (http://enable-cors.org) + * + * @author Sindre Mehus + * @version $Revision: 1.1 $ $Date: 2006/03/01 16:58:08 $ + */ +public class RESTFilter implements Filter { + + private static final Logger LOG = Logger.getLogger(RESTFilter.class); + + private final JAXBWriter jaxbWriter = new JAXBWriter(); + + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + try { + HttpServletResponse response = (HttpServletResponse) res; + response.setHeader("Access-Control-Allow-Origin", "*"); + chain.doFilter(req, res); + } catch (Throwable x) { + handleException(x, (HttpServletRequest) req, (HttpServletResponse) res); + } + } + + private void handleException(Throwable x, HttpServletRequest request, HttpServletResponse response) { + if (x instanceof NestedServletException && x.getCause() != null) { + x = x.getCause(); + } + + RESTController.ErrorCode code = (x instanceof ServletRequestBindingException) ? MISSING_PARAMETER : GENERIC; + String msg = getErrorMessage(x); + LOG.warn("Error in REST API: " + msg, x); + + try { + jaxbWriter.writeErrorResponse(request, response, code, msg); + } catch (Exception e) { + LOG.error("Failed to write error response.", e); + } + } + + private String getErrorMessage(Throwable x) { + if (x.getMessage() != null) { + return x.getMessage(); + } + return x.getClass().getSimpleName(); + } + + public void init(FilterConfig filterConfig) { + } + + public void destroy() { + } +} 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 . + + 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..58f6e2d6 --- /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 . + + 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.setHeader(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 . + + 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 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(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..44292043 --- /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 . + + 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.info('(' + 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..8aaf718c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/PlayQueueInputStream.java @@ -0,0 +1,158 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +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.VideoTranscodingSettings; +import net.sourceforge.subsonic.service.AudioScrobblerService; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.service.sonos.SonosHelper; +import net.sourceforge.subsonic.util.FileUtil; + +/** + * 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); + + // Don't scrobble REST players (except Sonos) + if (player.getClientId() == null || player.getClientId().equals(SonosHelper.SUBSONIC_CLIENT_ID)) { + audioScrobblerService.register(file, player.getUsername(), false, null); + } + + 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 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 { + // Don't scrobble REST players (except Sonos) + if (player.getClientId() == null || player.getClientId().equals(SonosHelper.SUBSONIC_CLIENT_ID)) { + audioScrobblerService.register(currentFile, player.getUsername(), true, null); + } + 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..01931fa1 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/io/RangeOutputStream.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.io; + +import net.sourceforge.subsonic.util.HttpRange; + +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 + */ +public class RangeOutputStream extends FilterOutputStream { + + private final HttpRange range; + + /** + * The current position. + */ + protected long pos; + + public RangeOutputStream(OutputStream out, HttpRange range) { + super(out); + this.range = range; + } + + /** + * Wraps the given output stream in a RangeOutputStream, using the values + * in the given range, unless the range is null in which case + * the original OutputStream is returned. + * + * @param out The output stream to wrap in a RangeOutputStream. + * @param range The range, may be null. + * @return The possibly wrapped output stream. + */ + public static OutputStream wrap(OutputStream out, HttpRange range) { + if (range == null) { + return out; + } + return new RangeOutputStream(out, range); + } + + /** + * Writes the byte if it's within the range. + * + * @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 (range.contains(pos)) { + super.write(b); + } + pos++; + } + + /** + * Writes the subset of the bytes that are within the range. + * + * @param b The bytes to write. + * @param off The offset to start at. + * @param len The number of bytes 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 { + long start = range.getFirstBytePos(); + Long end = range.getLastBytePos(); + if (pos + len >= start && (end == null || pos <= end)) { + long skipStart = Math.max(0, start - pos); + long newOff = off + skipStart; + long newLen = len - skipStart; + if (end != null) { + newLen = min(newLen, end - pos + 1, end - start + 1); + } + out.write(b, (int) newOff, (int) newLen); + } + pos += len; + } + + private long min(long a, long b, long c) { + return Math.min(a, Math.min(b, c)); + } +} + 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 . + + 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. + *

+ * Based on protocol description found on + * http://www.smackfu.com/stuff/programming/shoutcast.html + * + * @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..b7ba4ce2 --- /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 . + + 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 TranscodeInputStream 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 in 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.info(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 . + + 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 extraEnvVars = new HashMap(); + 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 . + + 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/LoginFailureLogger.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/LoginFailureLogger.java new file mode 100644 index 00000000..d52c3e35 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/LoginFailureLogger.java @@ -0,0 +1,37 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.security; + +import net.sourceforge.subsonic.Logger; + +/** + * Logs login failures. Can be used by tools like fail2ban for blocking IP addresses. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class LoginFailureLogger { + + private static final Logger LOG = Logger.getLogger(LoginFailureLogger.class); + + public void log(String remoteAddress, String username) { + LOG.info("Login failed for [" + username + "] from [" + remoteAddress + "]"); + } +} 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..2f930bfe --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/RESTRequestParameterProcessingFilter.java @@ -0,0 +1,223 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.security; + +import java.io.IOException; + +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 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.codec.digest.DigestUtils; +import org.apache.commons.lang.StringUtils; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.controller.JAXBWriter; +import net.sourceforge.subsonic.controller.RESTController; +import net.sourceforge.subsonic.domain.LicenseInfo; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.domain.Version; +import net.sourceforge.subsonic.service.SecurityService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * Performs authentication based on credentials being present in the HTTP request parameters. Also checks + * API versions and license information. + *

+ * 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 final JAXBWriter jaxbWriter = new JAXBWriter(); + private ProviderManager authenticationManager; + private SettingsService settingsService; + private SecurityService securityService; + private LoginFailureLogger loginFailureLogger; + + 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 salt = StringUtils.trimToNull(httpRequest.getParameter("s")); + String token = StringUtils.trimToNull(httpRequest.getParameter("t")); + String version = StringUtils.trimToNull(httpRequest.getParameter("v")); + String client = StringUtils.trimToNull(httpRequest.getParameter("c")); + + RESTController.ErrorCode errorCode = null; + + // The username and credentials parameters are not required if the user + // was previously authenticated, for example using Basic Auth. + boolean passwordOrTokenPresent = password != null || (salt != null && token != null); + Authentication previousAuth = SecurityContextHolder.getContext().getAuthentication(); + boolean missingCredentials = previousAuth == null && (username == null || !passwordOrTokenPresent); + if (missingCredentials || version == null || client == null) { + errorCode = RESTController.ErrorCode.MISSING_PARAMETER; + } + + if (errorCode == null) { + errorCode = checkAPIVersion(version); + } + + if (errorCode == null) { + errorCode = authenticate(username, password, salt, token, previousAuth); + } + + if (errorCode == null) { + errorCode = checkLicense(client); + } + + if (errorCode == null) { + chain.doFilter(request, response); + } else { + if (errorCode == RESTController.ErrorCode.NOT_AUTHENTICATED) { + loginFailureLogger.log(request.getRemoteAddr(), username); + } + SecurityContextHolder.getContext().setAuthentication(null); + sendErrorXml(httpRequest, httpResponse, errorCode); + } + } + + private RESTController.ErrorCode checkAPIVersion(String version) { + Version serverVersion = new Version(jaxbWriter.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, String salt, String token, Authentication previousAuth) { + + // Previously authenticated and username not overridden? + if (username == null && previousAuth != null) { + return null; + } + + if (salt != null && token != null) { + User user = securityService.getUserByName(username); + if (user == null) { + return RESTController.ErrorCode.NOT_AUTHENTICATED; + } + String expectedToken = DigestUtils.md5Hex(user.getPassword() + salt); + if (!expectedToken.equals(token)) { + return RESTController.ErrorCode.NOT_AUTHENTICATED; + } + + password = user.getPassword(); + } + + if (password != null) { + try { + UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); + Authentication authResult = authenticationManager.authenticate(authRequest); + SecurityContextHolder.getContext().setAuthentication(authResult); + return null; + } catch (AuthenticationException x) { + return RESTController.ErrorCode.NOT_AUTHENTICATED; + } + } + + return RESTController.ErrorCode.MISSING_PARAMETER; + } + + private RESTController.ErrorCode checkLicense(String client) { + LicenseInfo licenseInfo = settingsService.getLicenseInfo(); + if (licenseInfo.isLicenseOrTrialValid()) { + return null; + } + LOG.info("REST access for client '" + client + "' has expired."); + return RESTController.ErrorCode.NOT_LICENSED; + } + + 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 { + try { + jaxbWriter.writeErrorResponse(request, response, errorCode, errorCode.getMessage()); + } catch (Exception e) { + LOG.error("Failed to send error response.", e); + } + } + + public void init(FilterConfig filterConfig) throws ServletException { + } + + public void destroy() { + } + + public void setAuthenticationManager(ProviderManager authenticationManager) { + this.authenticationManager = authenticationManager; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setLoginFailureLogger(LoginFailureLogger loginFailureLogger) { + this.loginFailureLogger = loginFailureLogger; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/security/SubsonicApplicationEventListener.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/SubsonicApplicationEventListener.java new file mode 100644 index 00000000..b4fe5ed6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/security/SubsonicApplicationEventListener.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.security; + +import org.acegisecurity.event.authentication.AbstractAuthenticationFailureEvent; +import org.acegisecurity.providers.AbstractAuthenticationToken; +import org.acegisecurity.ui.WebAuthenticationDetails; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class SubsonicApplicationEventListener implements ApplicationListener { + + private LoginFailureLogger loginFailureLogger; + + @Override + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof AbstractAuthenticationFailureEvent) { + if (event.getSource() instanceof AbstractAuthenticationToken) { + AbstractAuthenticationToken token = (AbstractAuthenticationToken) event.getSource(); + Object details = token.getDetails(); + if (details instanceof WebAuthenticationDetails) { + loginFailureLogger.log(((WebAuthenticationDetails) details).getRemoteAddress(), String.valueOf(token.getPrincipal())); + } + } + } + + } + + public void setLoginFailureLogger(LoginFailureLogger loginFailureLogger) { + this.loginFailureLogger = loginFailureLogger; + } +} 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..b28d9d16 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AdService.java @@ -0,0 +1,44 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +/** + * Provides services for generating ads. + * + * @author Sindre Mehus + */ +public class AdService { + + private int adInterval; + private int pageCount; + + /** + * Returns whether an ad should be displayed. + */ + public boolean showAd() { + return pageCount++ % adInterval == 0; + } + + /** + * 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..eb1c99cf --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/AudioScrobblerService.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 . + + 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 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.MediaFile; +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. + *

+ * 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 RegistrationThread thread; + private final LinkedBlockingQueue queue = new LinkedBlockingQueue(); + + 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. + * @param time Event time, or {@code null} to use current time. + */ + public synchronized void register(MediaFile mediaFile, String username, boolean submission, Date time) { + + 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, time); + if (registrationData == null) { + return; + } + + try { + queue.put(registrationData); + } catch (InterruptedException x) { + LOG.warn("Interrupted while queuing Last.fm scrobble.", x); + } + } + + private RegistrationData createRegistrationData(MediaFile mediaFile, String username, boolean submission, Date time) { + + if (mediaFile == null || mediaFile.isVideo()) { + return null; + } + + UserSettings userSettings = settingsService.getUserSettings(username); + if (!userSettings.isLastFmEnabled() || userSettings.getLastFmUsername() == null || userSettings.getLastFmPassword() == null) { + return null; + } + + 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 = time == null ? new Date() : time; + 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.info("Successfully registered " + (registrationData.submission ? "submission" : "now playing") + + " for song '" + registrationData.title + "' for user " + registrationData.username + " at Last.fm: " + registrationData.time); + } + } + + /** + * Returns the following lines if authentication succeeds: + *

+ * 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" + *

+ * If authentication fails, null 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 params = new HashMap(); + 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 params = new HashMap(); + 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 parameters) throws IOException { + List params = new ArrayList(); + for (Map.Entry 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 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(60L * 1000L); // Wait 60 seconds. + } 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/ITunesParser.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ITunesParser.java new file mode 100644 index 00000000..03482b27 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ITunesParser.java @@ -0,0 +1,211 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.commons.io.IOUtils; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +import net.sourceforge.subsonic.util.StringUtil; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class ITunesParser { + + private String iTunesXml; + private final XMLInputFactory inputFactory; + + public ITunesParser(String iTunesXml) { + this.iTunesXml = iTunesXml; + inputFactory = XMLInputFactory.newFactory(); + inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false); + } + + public List parse() throws Exception { + List playlists = parsePlaylists(); + Map tracks = parseTracks(playlists); + populatePlaylistTracks(playlists, tracks); + + for (ITunesPlaylist p : playlists) { + System.out.println(p); + } + + return playlists; + } + + private List parsePlaylists() throws Exception { + List playlists = new ArrayList(); + + InputStream in = new FileInputStream(iTunesXml); + try { + ITunesPlaylist playlist = null; + + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(in); + while (streamReader.hasNext()) { + int code = streamReader.next(); + if (code == XMLStreamReader.START_ELEMENT) { + String key = readKey(streamReader); + + if ("Playlist ID".equals(key)) { + playlist = new ITunesPlaylist(readNextTag(streamReader)); + playlists.add(playlist); + } + + if (playlist != null) { + if ("Name".equals(key)) { + playlist.name = readNextTag(streamReader); + } else if ("Smart Info".equals(key)) { + playlist.smart = true; + } else if ("Visible".equals(key)) { + playlist.visible = false; + } else if ("Distinguished Kind".equals(key)) { + playlist.distinguishedKind = readNextTag(streamReader); + } else if ("Track ID".equals(key)) { + playlist.trackIds.add(readNextTag(streamReader)); + } + } + } + } + } finally { + IOUtils.closeQuietly(in); + } + + return Lists.newArrayList(Iterables.filter(playlists, new Predicate() { + @Override + public boolean apply(ITunesPlaylist input) { + return input.isIncluded(); + } + })); + } + + private Map parseTracks(List playlists) throws Exception { + Map result = new HashMap(); + SortedSet trackIds = new TreeSet(); + for (ITunesPlaylist playlist : playlists) { + trackIds.addAll(playlist.trackIds); + } + + InputStream in = new FileInputStream(iTunesXml); + + try { + XMLStreamReader streamReader = inputFactory.createXMLStreamReader(in); + String trackId = null; + while (streamReader.hasNext()) { + int code = streamReader.next(); + if (code == XMLStreamReader.START_ELEMENT) { + String key = readKey(streamReader); + if ("Track ID".equals(key)) { + trackId = readNextTag(streamReader); + } else if (trackId != null && trackIds.contains(trackId) && "Location".equals(key)) { + String location = readNextTag(streamReader); + File file = new File(StringUtil.urlDecode(new URL(location).getFile())); + result.put(trackId, file); + } + } + } + } finally { + IOUtils.closeQuietly(in); + } + return result; + } + + private void populatePlaylistTracks(List playlists, Map tracks) { + for (ITunesPlaylist playlist : playlists) { + for (String trackId : playlist.trackIds) { + File file = tracks.get(trackId); + if (file != null) { + playlist.trackFiles.add(file); + } + } + } + } + + private String readNextTag(XMLStreamReader streamReader) throws XMLStreamException { + while (streamReader.next() != XMLStreamConstants.START_ELEMENT) { + } + return streamReader.getElementText(); + } + + private String readKey(XMLStreamReader streamReader) { + try { + if (streamReader.getEventType() == XMLStreamConstants.START_ELEMENT && + "key".equals(streamReader.getName().getLocalPart())) { + return streamReader.getElementText(); + } + } catch (XMLStreamException e) { + // TODO + System.out.println(streamReader.getName().getLocalPart() + " " + e); + } + return null; + } + + // TODO + public static void main(String[] args) throws Exception { + new ITunesParser("/Users/sindre/Music/iTunes/iTunes Music Library.xml").parse(); + } + + private static class ITunesPlaylist { + + private final String id; + private String name; + private boolean smart; + private final List trackIds = new ArrayList(); + private final List trackFiles = new ArrayList(); + private boolean visible = true; + private String distinguishedKind; + + public ITunesPlaylist(String id) { + this.id = id; + } + + public boolean isIncluded() { + return !smart && visible && distinguishedKind == null; + } + + @Override + public String toString() { + StringBuilder s = new StringBuilder(id + " - " + name ); + for (File trackFile : trackFiles) { + s.append("\n " + trackFile); + } + return s.toString(); + } + } +} 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..0ef74602 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/JukeboxService.java @@ -0,0 +1,208 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.InputStream; + +import org.apache.commons.io.IOUtils; + +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.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 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 = AudioPlayer.DEFAULT_GAIN; + 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) { + int duration = file.getDurationSeconds() == null ? 0 : file.getDurationSeconds() - offset; + TranscodingService.Parameters parameters = new TranscodingService.Parameters(file, new VideoTranscodingSettings(0, 0, offset, duration, false)); + 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, null); + } + } + + 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/LastFmCache.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/LastFmCache.java new file mode 100644 index 00000000..ca9bbc7e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/LastFmCache.java @@ -0,0 +1,158 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2014 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Map; +import java.util.Properties; + +import org.apache.commons.io.IOUtils; + +import de.umass.lastfm.cache.Cache; +import de.umass.lastfm.cache.ExpirationPolicy; +import de.umass.lastfm.cache.FileSystemCache; + +/** + * Based on {@link FileSystemCache}, but properly closes files and enforces + * time-to-live (by ignoring HTTP header directives). + * + * @author Sindre Mehus + * @version $Id$ + */ +public class LastFmCache extends Cache { + + private final File cacheDir; + private final long ttl; + + public LastFmCache(File cacheDir, final long ttl) { + this.cacheDir = cacheDir; + this.ttl = ttl; + + setExpirationPolicy(new ExpirationPolicy() { + @Override + public long getExpirationTime(String method, Map params) { + return ttl; + } + }); + } + + @Override + public boolean contains(String cacheEntryName) { + return getXmlFile(cacheEntryName).exists(); + } + + @Override + public InputStream load(String cacheEntryName) { + FileInputStream in = null; + try { + in = new FileInputStream(getXmlFile(cacheEntryName)); + return new ByteArrayInputStream(IOUtils.toByteArray(in)); + } catch (Exception e) { + return null; + } finally { + IOUtils.closeQuietly(in); + } + } + + @Override + public void remove(String cacheEntryName) { + getXmlFile(cacheEntryName).delete(); + getMetaFile(cacheEntryName).delete(); + } + + @Override + public void store(String cacheEntryName, InputStream inputStream, long expirationDate) { + createCache(); + + OutputStream xmlOut = null; + OutputStream metaOut = null; + try { + File xmlFile = getXmlFile(cacheEntryName); + xmlOut = new FileOutputStream(xmlFile); + IOUtils.copy(inputStream, xmlOut); + + File metaFile = getMetaFile(cacheEntryName); + Properties properties = new Properties(); + + // Note: Ignore the given expirationDate, since Last.fm sets it to just one day ahead. + properties.setProperty("expiration-date", Long.toString(getExpirationDate())); + + metaOut = new FileOutputStream(metaFile); + properties.store(metaOut, null); + } catch (Exception e) { + // we ignore the exception. if something went wrong we just don't cache it. + } finally { + IOUtils.closeQuietly(xmlOut); + IOUtils.closeQuietly(metaOut); + } + } + + private long getExpirationDate() { + return System.currentTimeMillis() + ttl; + } + + private void createCache() { + if (!cacheDir.exists()) { + cacheDir.mkdirs(); + } + } + + @Override + public boolean isExpired(String cacheEntryName) { + File f = getMetaFile(cacheEntryName); + if (!f.exists()) { + return false; + } + InputStream in = null; + try { + Properties p = new Properties(); + in = new FileInputStream(f); + p.load(in); + long expirationDate = Long.valueOf(p.getProperty("expiration-date")); + return expirationDate < System.currentTimeMillis(); + } catch (Exception e) { + return false; + } finally { + IOUtils.closeQuietly(in); + } + } + + @Override + public void clear() { + for (File file : cacheDir.listFiles()) { + if (file.isFile()) { + file.delete(); + } + } + } + + private File getXmlFile(String cacheEntryName) { + return new File(cacheDir, cacheEntryName + ".xml"); + } + + private File getMetaFile(String cacheEntryName) { + return new File(cacheDir, cacheEntryName + ".meta"); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/LastFmService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/LastFmService.java new file mode 100644 index 00000000..b3baa72c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/LastFmService.java @@ -0,0 +1,383 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2014 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; + +import de.umass.lastfm.Artist; +import de.umass.lastfm.Caller; +import de.umass.lastfm.ImageSize; +import de.umass.lastfm.Track; +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.dao.ArtistDao; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.ArtistBio; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; + +/** + * Provides services from the Last.fm REST API. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class LastFmService { + + private static final String LAST_FM_KEY = "ece4499898a9440896dfdce5dab26bbf"; + private static final long CACHE_TIME_TO_LIVE_MILLIS = 6 * 30 * 24 * 3600 * 1000L; // 6 months + private static final Logger LOG = Logger.getLogger(LastFmService.class); + + private MediaFileDao mediaFileDao; + private MediaFileService mediaFileService; + private ArtistDao artistDao; + + public void init() { + Caller caller = Caller.getInstance(); + caller.setUserAgent("Subsonic"); + + File cacheDir = new File(SettingsService.getSubsonicHome(), "lastfmcache"); + caller.setCache(new LastFmCache(cacheDir, CACHE_TIME_TO_LIVE_MILLIS)); + } + + /** + * Returns similar artists, using last.fm REST API. + * + * @param mediaFile The media file (song, album or artist). + * @param count Max number of similar artists to return. + * @param includeNotPresent Whether to include artists that are not present in the media library. + * @param musicFolders Only return artists present in these folders. + * @return Similar artists, ordered by presence then similarity. + */ + public List getSimilarArtists(MediaFile mediaFile, int count, boolean includeNotPresent, List musicFolders) { + List result = new ArrayList(); + if (mediaFile == null) { + return result; + } + + String artistName = getArtistName(mediaFile); + try { + Collection similarArtists = Artist.getSimilar(getCanonicalArtistName(artistName), LAST_FM_KEY); + + // First select artists that are present. + for (Artist lastFmArtist : similarArtists) { + MediaFile similarArtist = mediaFileDao.getArtistByName(lastFmArtist.getName(), musicFolders); + if (similarArtist != null) { + result.add(similarArtist); + if (result.size() == count) { + return result; + } + } + } + + // Then fill up with non-present artists + if (includeNotPresent) { + for (Artist lastFmArtist : similarArtists) { + MediaFile similarArtist = mediaFileDao.getArtistByName(lastFmArtist.getName(), musicFolders); + if (similarArtist == null) { + MediaFile notPresentArtist = new MediaFile(); + notPresentArtist.setId(-1); + notPresentArtist.setArtist(lastFmArtist.getName()); + result.add(notPresentArtist); + if (result.size() == count) { + return result; + } + } + } + } + + } catch (Throwable x) { + LOG.warn("Failed to find similar artists for " + artistName, x); + } + return result; + } + + /** + * Returns similar artists, using last.fm REST API. + * + * @param artist The artist. + * @param count Max number of similar artists to return. + * @param includeNotPresent Whether to include artists that are not present in the media library. + * @param musicFolders Only return songs from artists in these folders. + * @return Similar artists, ordered by presence then similarity. + */ + public List getSimilarArtists(net.sourceforge.subsonic.domain.Artist artist, + int count, boolean includeNotPresent, List musicFolders) { + List result = new ArrayList(); + + try { + + // First select artists that are present. + Collection similarArtists = Artist.getSimilar(getCanonicalArtistName(artist.getName()), LAST_FM_KEY); + for (Artist lastFmArtist : similarArtists) { + net.sourceforge.subsonic.domain.Artist similarArtist = artistDao.getArtist(lastFmArtist.getName(), musicFolders); + if (similarArtist != null) { + result.add(similarArtist); + if (result.size() == count) { + return result; + } + } + } + + // Then fill up with non-present artists + if (includeNotPresent) { + for (Artist lastFmArtist : similarArtists) { + net.sourceforge.subsonic.domain.Artist similarArtist = artistDao.getArtist(lastFmArtist.getName()); + if (similarArtist == null) { + net.sourceforge.subsonic.domain.Artist notPresentArtist = new net.sourceforge.subsonic.domain.Artist(); + notPresentArtist.setId(-1); + notPresentArtist.setName(lastFmArtist.getName()); + result.add(notPresentArtist); + if (result.size() == count) { + return result; + } + } + } + } + + } catch (Throwable x) { + LOG.warn("Failed to find similar artists for " + artist.getName(), x); + } + return result; + } + + /** + * Returns songs from similar artists, using last.fm REST API. Typically used for artist radio features. + * + * @param artist The artist. + * @param count Max number of songs to return. + * @param musicFolders Only return songs from artists in these folders. + * @return Songs from similar artists; + */ + public List getSimilarSongs(net.sourceforge.subsonic.domain.Artist artist, int count, + List musicFolders) throws IOException { + List similarSongs = new ArrayList(); + + similarSongs.addAll(mediaFileDao.getSongsByArtist(artist.getName(), 0, 1000)); + for (net.sourceforge.subsonic.domain.Artist similarArtist : getSimilarArtists(artist, 100, false, musicFolders)) { + similarSongs.addAll(mediaFileDao.getSongsByArtist(similarArtist.getName(), 0, 1000)); + } + Collections.shuffle(similarSongs); + return similarSongs.subList(0, Math.min(count, similarSongs.size())); + } + + /** + * Returns songs from similar artists, using last.fm REST API. Typically used for artist radio features. + * + * @param mediaFile The media file (song, album or artist). + * @param count Max number of songs to return. + * @param musicFolders Only return songs from artists present in these folders. + * @return Songs from similar artists; + */ + public List getSimilarSongs(MediaFile mediaFile, int count, List musicFolders) { + List similarSongs = new ArrayList(); + + String artistName = getArtistName(mediaFile); + MediaFile artist = mediaFileDao.getArtistByName(artistName, musicFolders); + if (artist != null) { + similarSongs.addAll(mediaFileService.getRandomSongsForParent(artist, count)); + } + + for (MediaFile similarArtist : getSimilarArtists(mediaFile, 100, false, musicFolders)) { + similarSongs.addAll(mediaFileService.getRandomSongsForParent(similarArtist, count)); + } + Collections.shuffle(similarSongs); + return similarSongs.subList(0, Math.min(count, similarSongs.size())); + } + + /** + * Returns artist bio and images. + * + * @param mediaFile The media file (song, album or artist). + * @return Artist bio. + */ + public ArtistBio getArtistBio(MediaFile mediaFile) { + return getArtistBio(getCanonicalArtistName(getArtistName(mediaFile))); + } + + /** + * Returns artist bio and images. + * + * @param artist The artist. + * @return Artist bio. + */ + public ArtistBio getArtistBio(net.sourceforge.subsonic.domain.Artist artist) { + return getArtistBio(getCanonicalArtistName(artist.getName())); + } + + /** + * Returns top songs for the given artist, using last.fm REST API. + * + * @param artist The artist. + * @param count Max number of songs to return. + * @param musicFolders Only return songs present in these folders. + * @return Top songs for artist. + */ + public List getTopSongs(MediaFile artist, int count, List musicFolders) { + return getTopSongs(artist.getName(), count, musicFolders); + } + + /** + * Returns top songs for the given artist, using last.fm REST API. + * + * @param artistName The artist name. + * @param count Max number of songs to return. + * @param musicFolders Only return songs present in these folders. + * @return Top songs for artist. + */ + public List getTopSongs(String artistName, int count, List musicFolders) { + try { + if (StringUtils.isBlank(artistName) || count <= 0) { + return Collections.emptyList(); + } + + List result = new ArrayList(); + for (Track topTrack : Artist.getTopTracks(artistName, LAST_FM_KEY)) { + MediaFile song = mediaFileDao.getSongByArtistAndTitle(artistName, topTrack.getName(), musicFolders); + if (song != null) { + result.add(song); + if (result.size() == count) { + return result; + } + } + } + return result; + } catch (Throwable x) { + LOG.warn("Failed to find top songs for " + artistName, x); + return Collections.emptyList(); + } + } + + private ArtistBio getArtistBio(String artistName) { + try { + if (artistName == null) { + return null; + } + + Artist info = Artist.getInfo(artistName, LAST_FM_KEY); + if (info == null) { + return null; + } + return new ArtistBio(processWikiText(info.getWikiSummary()), + info.getMbid(), + info.getUrl(), + info.getImageURL(ImageSize.MEDIUM), + info.getImageURL(ImageSize.LARGE), + info.getImageURL(ImageSize.MEGA)); + } catch (Throwable x) { + LOG.warn("Failed to find artist bio for " + artistName, x); + return null; + } + } + + private String getCanonicalArtistName(String artistName) { + try { + if (artistName == null) { + return null; + } + + Artist info = Artist.getInfo(artistName, LAST_FM_KEY); + if (info == null) { + return null; + } + + String biography = processWikiText(info.getWikiSummary()); + String redirectedArtistName = getRedirectedArtist(biography); + return redirectedArtistName != null ? redirectedArtistName : artistName; + } catch (Throwable x) { + LOG.warn("Failed to find artist bio for " + artistName, x); + return null; + } + } + + private String getRedirectedArtist(String biography) { + /* + This is mistagged for The Boomtown Rats; + it would help Last.fm if you could correct your tags. + Boomtown Rats on Last.fm. + + -- or -- + + Fix your tags to The Chemical Brothers + Chemical Brothers on Last.fm. + */ + + if (biography == null) { + return null; + } + Pattern pattern = Pattern.compile("((This is mistagged for)|(Fix your tags to)).*class=\"bbcode_artist\">(.*?)"); + Matcher matcher = pattern.matcher(biography); + if (matcher.find()) { + return matcher.group(4); + } + return null; + } + + private String processWikiText(String text) { + /* + System of a Down is an Armenian American band, + formed in 1994 in Los Angeles, California, USA. All four members are of Armenian descent, and are widely known for their outspoken views expressed in + many of their songs confronting the Armenian Genocide of 1915 by the Ottoman Empire and the ongoing War on Terror by the US government. The band + consists of Serj Tankian (vocals), Daron Malakian (vocals, guitar), + Shavo Odadjian (bass, vocals) and John Dolmayan (drums). + Read more about System of a Down on Last.fm. + User-contributed text is available under the Creative Commons By-SA License and may also be available under the GNU FDL. + */ + + text = text.replaceAll("User-contributed text.*", ""); + text = text.replaceAll(". + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +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.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; + +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.Genre; +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 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; + private boolean memoryCacheEnabled = true; + + /** + * 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. + MediaFile result = getFromMemoryCache(file); + 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); + putInMemoryCache(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. + putInMemoryCache(file, result); + mediaFileDao.createOrUpdateMediaFile(result); + + return result; + } + + private MediaFile checkLastModified(MediaFile mediaFile, boolean useFastCache) { + if (useFastCache || (mediaFile.getVersion() >= MediaFileDao.VERSION && 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()); + } + + /** + * 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 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 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 result = new ArrayList(); + 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 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 set = new TreeSet(comparator); + set.addAll(result); + result = new ArrayList(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. + * + * @param sortByAlbum Whether to sort by album count, rather than song count. + * @return Sorted list of genres. + */ + public List getGenres(boolean sortByAlbum) { + return mediaFileDao.getGenres(sortByAlbum); + } + + /** + * Returns the most frequently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The most frequently played albums. + */ + public List getMostFrequentlyPlayedAlbums(int offset, int count, List musicFolders) { + return mediaFileDao.getMostFrequentlyPlayedAlbums(offset, count, musicFolders); + } + + /** + * Returns the most recently played albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The most recently played albums. + */ + public List getMostRecentlyPlayedAlbums(int offset, int count, List musicFolders) { + return mediaFileDao.getMostRecentlyPlayedAlbums(offset, count, musicFolders); + } + + /** + * Returns the most recently added albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The most recently added albums. + */ + public List getNewestAlbums(int offset, int count, List musicFolders) { + return mediaFileDao.getNewestAlbums(offset, count, musicFolders); + } + + /** + * 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. + * @param musicFolders Only return albums from these folders. + * @return The most recently starred albums for this user. + */ + public List getStarredAlbums(int offset, int count, String username, List musicFolders) { + return mediaFileDao.getStarredAlbums(offset, count, username, musicFolders); + } + + /** + * 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 + * @param musicFolders Only return albums in these folders. + * @return Albums in alphabetical order. + */ + public List getAlphabeticalAlbums(int offset, int count, boolean byArtist, List musicFolders) { + return mediaFileDao.getAlphabeticalAlbums(offset, count, byArtist, musicFolders); + } + + /** + * Returns albums within a year range. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param fromYear The first year in the range. + * @param toYear The last year in the range. + * @param musicFolders Only return albums in these folders. + * @return Albums in the year range. + */ + public List getAlbumsByYear(int offset, int count, int fromYear, int toYear, List musicFolders) { + return mediaFileDao.getAlbumsByYear(offset, count, fromYear, toYear, musicFolders); + } + + /** + * Returns albums in a genre. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param genre The genre name. + * @param musicFolders Only return albums in these folders. + * @return Albums in the genre. + */ + public List getAlbumsByGenre(int offset, int count, String genre, List musicFolders) { + return mediaFileDao.getAlbumsByGenre(offset, count, genre, musicFolders); + } + + /** + * Returns random songs for the give parent. + * + * @param parent The parent. + * @param count Max number of songs to return. + * @return Random songs. + */ + public List getRandomSongsForParent(MediaFile parent, int count) { + List children = getDescendantsOf(parent, false); + removeVideoFiles(children); + + if (children.isEmpty()) { + return children; + } + Collections.shuffle(children); + return children.subList(0, Math.min(count, children.size())); + } + + /** + * Removes video files from the given list. + */ + public void removeVideoFiles(List files) { + Iterator iterator = files.iterator(); + while (iterator.hasNext()) { + MediaFile file = iterator.next(); + if (file.isVideo()) { + iterator.remove(); + } + } + } + + public Date getMediaFileStarredDate(int id, String username) { + return mediaFileDao.getMediaFileStarredDate(id, username); + } + + public void populateStarredDate(List 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 storedChildren = mediaFileDao.getChildrenOf(parent.getPath()); + Map storedChildrenMap = new HashMap(); + for (MediaFile child : storedChildren) { + storedChildrenMap.put(child.getPath(), child); + } + + List 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); + } + + public List filterMediaFiles(File[] candidates) { + List result = new ArrayList(); + 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 single "." or "@eaDir" (thumbnail dir created on Synology devices). + String name = file.getName(); + return (name.startsWith(".") && !name.startsWith("..")) || name.startsWith("@eaDir") || name.equals("Thumbs.db"); + } + + private MediaFile createMediaFile(File file) { + + MediaFile existingFile = mediaFileDao.getMediaFile(file.getPath()); + + 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(existingFile == null ? 0 : existingFile.getPlayCount()); + mediaFile.setLastPlayed(existingFile == null ? null : existingFile.getLastPlayed()); + mediaFile.setComment(existingFile == null ? null : existingFile.getComment()); + 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.getAlbumArtist()); + 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, year and genre. + MetaDataParser parser = metaDataParserFactory.getParser(firstChild); + if (parser != null) { + MetaData metaData = parser.getMetaData(firstChild); + mediaFile.setArtist(metaData.getAlbumArtist()); + mediaFile.setAlbumName(metaData.getAlbumName()); + mediaFile.setYear(metaData.getYear()); + mediaFile.setGenre(metaData.getGenre()); + } + + // 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()); + } + + private void putInMemoryCache(File file, MediaFile mediaFile) { + if (memoryCacheEnabled) { + mediaFileMemoryCache.put(new Element(file, mediaFile)); + } + } + + private MediaFile getFromMemoryCache(File file) { + if (!memoryCacheEnabled) { + return null; + } + Element element = mediaFileMemoryCache.get(file); + return element == null ? null : (MediaFile) element.getObjectValue(); + } + + public void setMemoryCacheEnabled(boolean memoryCacheEnabled) { + this.memoryCacheEnabled = memoryCacheEnabled; + if (!memoryCacheEnabled) { + mediaFileMemoryCache.removeAll(); + } + } + + /** + * 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 getDescendantsOf(MediaFile ancestor, boolean sort) { + + if (ancestor.isFile()) { + return Arrays.asList(ancestor); + } + + List result = new ArrayList(); + + 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 int getAlbumCount(List musicFolders) { + return mediaFileDao.getAlbumCount(musicFolders); + } + + public int getPlayedAlbumCount(List musicFolders) { + return mediaFileDao.getPlayedAlbumCount(musicFolders); + } + + public int getStarredAlbumCount(String username, List musicFolders) { + return mediaFileDao.getStarredAlbumCount(username, musicFolders); + } + + public void clearMemoryCache() { + mediaFileMemoryCache.removeAll(); + } + + 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..7f6f4c0d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MediaScannerService.java @@ -0,0 +1,425 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.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 org.apache.commons.lang.ObjectUtils; + +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.Genres; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.util.FileUtil; + +/** + * 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 PlaylistService playlistService; + private MediaFileService mediaFileService; + private MediaFileDao mediaFileDao; + private ArtistDao artistDao; + private AlbumDao albumDao; + private int scanCount; + + public void init() { + deleteOldIndexFiles(); + statistics = settingsService.getMediaLibraryStatistics(); + 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(); + playlistService.importPlaylists(); + } + }; + + thread.setPriority(Thread.MIN_PRIORITY); + thread.start(); + } + + private void doScanLibrary() { + LOG.info("Starting to scan media library."); + + try { + Date lastScanned = new Date(); + + // Maps from artist name to album count. + Map albumCount = new HashMap(); + Genres genres = new Genres(); + + scanCount = 0; + statistics.reset(); + + mediaFileService.setMemoryCacheEnabled(false); + searchService.startIndexing(); + + mediaFileService.clearMemoryCache(); + + // Recurse through all files on disk. + for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { + MediaFile root = mediaFileService.getMediaFile(musicFolder.getPath(), false); + scanFile(root, musicFolder, lastScanned, albumCount, genres, false); + } + + // Scan podcast folder. + File podcastFolder = new File(settingsService.getPodcastFolder()); + if (podcastFolder.exists()) { + scanFile(mediaFileService.getMediaFile(podcastFolder), new MusicFolder(podcastFolder, null, true, null), + lastScanned, albumCount, genres, true); + } + + LOG.info("Scanned media library with " + scanCount + " entries."); + + LOG.info("Marking non-present files."); + mediaFileDao.markNonPresent(lastScanned); + LOG.info("Marking non-present artists."); + artistDao.markNonPresent(lastScanned); + LOG.info("Marking non-present albums."); + albumDao.markNonPresent(lastScanned); + + // Update statistics + statistics.incrementArtists(albumCount.size()); + for (Integer albums : albumCount.values()) { + statistics.incrementAlbums(albums); + } + + // Update genres + mediaFileDao.updateGenres(genres.getGenres()); + + settingsService.setMediaLibraryStatistics(statistics); + settingsService.setLastScanned(lastScanned); + settingsService.save(false); + LOG.info("Completed media library scan."); + + } catch (Throwable x) { + LOG.error("Failed to scan media library.", x); + } finally { + mediaFileService.setMemoryCacheEnabled(true); + searchService.stopIndexing(); + scanning = false; + } + } + + private void scanFile(MediaFile file, MusicFolder musicFolder, Date lastScanned, + Map albumCount, Genres genres, boolean isPodcast) { + 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, genres, isPodcast); + } + for (MediaFile child : mediaFileService.getChildrenOf(file, false, true, false, false)) { + scanFile(child, musicFolder, lastScanned, albumCount, genres, isPodcast); + } + } else { + if (!isPodcast) { + updateAlbum(file, musicFolder, lastScanned, albumCount); + updateArtist(file, musicFolder, lastScanned, albumCount); + } + statistics.incrementSongs(1); + } + + updateGenres(file, genres); + mediaFileDao.markPresent(file.getPath(), lastScanned); + artistDao.markPresent(file.getAlbumArtist(), lastScanned); + + if (file.getDurationSeconds() != null) { + statistics.incrementTotalDurationInSeconds(file.getDurationSeconds()); + } + if (file.getFileSize() != null) { + statistics.incrementTotalLengthInBytes(file.getFileSize()); + } + } + + private void updateGenres(MediaFile file, Genres genres) { + String genre = file.getGenre(); + if (genre == null) { + return; + } + if (file.isAlbum()) { + genres.incrementAlbumCount(genre); + } + else if (file.isAudio()) { + genres.incrementSongCount(genre); + } + } + + private void updateAlbum(MediaFile file, MusicFolder musicFolder, Date lastScanned, Map albumCount) { + String artist = file.getAlbumArtist() != null ? file.getAlbumArtist() : file.getArtist(); + if (file.getAlbumName() == null || artist == 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(artist); + album.setCreated(file.getChanged()); + } + if (file.getYear() != null) { + album.setYear(file.getYear()); + } + if (file.getGenre() != null) { + album.setGenre(file.getGenre()); + } + MediaFile parent = mediaFileService.getParentOf(file); + if (parent != null && parent.getCoverArtPath() != null) { + album.setCoverArtPath(parent.getCoverArtPath()); + } + + boolean firstEncounter = !lastScanned.equals(album.getLastScanned()); + if (firstEncounter) { + album.setFolderId(musicFolder.getId()); + album.setDurationSeconds(0); + album.setSongCount(0); + Integer n = albumCount.get(artist); + albumCount.put(artist, 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, MusicFolder musicFolder, Date lastScanned, Map albumCount) { + if (file.getAlbumArtist() == null || !file.isAudio()) { + return; + } + + Artist artist = artistDao.getArtist(file.getAlbumArtist()); + if (artist == null) { + artist = new Artist(); + artist.setName(file.getAlbumArtist()); + } + if (artist.getCoverArtPath() == null) { + MediaFile parent = mediaFileService.getParentOf(file); + if (parent != null) { + artist.setCoverArtPath(parent.getCoverArtPath()); + } + } + boolean firstEncounter = !lastScanned.equals(artist.getLastScanned()); + + if (firstEncounter) { + artist.setFolderId(musicFolder.getId()); + } + 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, musicFolder); + } + } + + /** + * 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; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +} 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..d8e4521b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/MusicIndexService.java @@ -0,0 +1,284 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.text.Collator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.StringTokenizer; +import java.util.TreeMap; + +import net.sourceforge.subsonic.domain.Artist; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicFolderContent; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.MusicIndex.SortableArtist; +import net.sourceforge.subsonic.util.FileUtil; + +/** + * Provides services for grouping artists by index. + * + * @author Sindre Mehus + */ +public class MusicIndexService { + + private SettingsService settingsService; + private MediaFileService mediaFileService; + + /** + * Returns a map from music indexes to sorted lists of artists that are direct children of the given music folders. + * + * @param folders The music folders. + * @param refresh Whether to look for updates by checking the last-modified timestamp of 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> getIndexedArtists(List folders, boolean refresh) throws IOException { + List artists = createSortableArtists(folders, refresh); + return sortArtists(artists); + } + + public SortedMap> getIndexedArtists(List artists) throws IOException { + List sortableArtists = createSortableArtists(artists); + return sortArtists(sortableArtists); + } + + public MusicFolderContent getMusicFolderContent(List musicFoldersToUse, boolean refresh) throws Exception { + SortedMap> indexedArtists = getIndexedArtists(musicFoldersToUse, refresh); + List singleSongs = getSingleSongs(musicFoldersToUse, refresh); + return new MusicFolderContent(indexedArtists, singleSongs); + } + + private List getSingleSongs(List folders, boolean refresh) throws IOException { + List result = new ArrayList(); + for (MusicFolder folder : folders) { + MediaFile parent = mediaFileService.getMediaFile(folder.getPath(), !refresh); + result.addAll(mediaFileService.getChildrenOf(parent, true, false, true, !refresh)); + } + return result; + } + + public List getShortcuts(List musicFoldersToUse) { + List result = new ArrayList(); + for (String shortcut : settingsService.getShortcutsAsArray()) { + for (MusicFolder musicFolder : musicFoldersToUse) { + File file = new File(musicFolder.getPath(), shortcut); + if (FileUtil.exists(file)) { + result.add(mediaFileService.getMediaFile(file, true)); + } + } + } + return result; + } + + private SortedMap> sortArtists(List artists) { + List indexes = createIndexesFromExpression(settingsService.getIndexString()); + Comparator indexComparator = new MusicIndexComparator(indexes); + + SortedMap> result = new TreeMap>(indexComparator); + + for (T artist : artists) { + MusicIndex index = getIndex(artist, indexes); + List artistSet = result.get(index); + if (artistSet == null) { + artistSet = new ArrayList(); + result.put(index, artistSet); + } + artistSet.add(artist); + } + + for (List artistList : result.values()) { + Collections.sort(artistList); + } + + 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:

+ *

+ * The expression "A" will create the index "A" -> ["A"]
+ * The expression "The" will create the index "The" -> ["The"]
+ * The expression "A(AÅÆ)" will create the index "A" -> ["A", "Å", "Æ"]
+ * The expression "X-Z(XYZ)" will create the index "X-Z" -> ["X", "Y", "Z"] + * + * @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 createIndexesFromExpression(String expr) { + List result = new ArrayList(); + + StringTokenizer tokenizer = new StringTokenizer(expr, " "); + while (tokenizer.hasMoreTokens()) { + MusicIndex index = createIndexFromExpression(tokenizer.nextToken()); + result.add(index); + } + + return result; + } + + private List createSortableArtists(List folders, boolean refresh) throws IOException { + String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); + String[] shortcuts = settingsService.getShortcutsAsArray(); + SortedMap artistMap = new TreeMap(); + Set shortcutSet = new HashSet(Arrays.asList(shortcuts)); + Collator collator = createCollator(); + + for (MusicFolder folder : folders) { + + MediaFile root = mediaFileService.getMediaFile(folder.getPath(), !refresh); + List children = mediaFileService.getChildrenOf(root, false, true, true, !refresh); + for (MediaFile child : children) { + if (shortcutSet.contains(child.getName())) { + continue; + } + + String sortableName = createSortableName(child.getName(), ignoredArticles); + MusicIndex.SortableArtistWithMediaFiles artist = artistMap.get(sortableName); + if (artist == null) { + artist = new MusicIndex.SortableArtistWithMediaFiles(child.getName(), sortableName, collator); + artistMap.put(sortableName, artist); + } + artist.addMediaFile(child); + } + } + + return new ArrayList(artistMap.values()); + } + + private List createSortableArtists(List artists) { + List result = new ArrayList(); + String[] ignoredArticles = settingsService.getIgnoredArticlesAsArray(); + Collator collator = createCollator(); + for (Artist artist : artists) { + String sortableName = createSortableName(artist.getName(), ignoredArticles); + result.add(new MusicIndex.SortableArtistWithArtist(artist.getName(), sortableName, artist, collator)); + } + + return result; + } + + /** + * Returns a collator to be used when sorting artists. + */ + private Collator createCollator() { + return Collator.getInstance(settingsService.getLocale()); + } + + 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(SortableArtist artist, List 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; + } + + private static class MusicIndexComparator implements Comparator, Serializable { + + private List indexes; + + public MusicIndexComparator(List 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..6a2ca100 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/NetworkService.java @@ -0,0 +1,345 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.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.commons.lang.StringUtils; +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.UrlRedirectType; +import net.sourceforge.subsonic.service.upnp.ClingRouter; +import net.sourceforge.subsonic.service.upnp.NATPMPRouter; +import net.sourceforge.subsonic.service.upnp.Router; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * 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 UPnPService upnpService; + 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(10); + initUrlRedirection(false); + } + + /** + * Configures UPnP port forwarding. + */ + public synchronized void initPortForwarding(int initialDelaySeconds) { + portForwardingStatus.setText("Idle"); + if (portForwardingFuture != null) { + portForwardingFuture.cancel(true); + } + portForwardingFuture = executor.scheduleWithFixedDelay(portForwardingTask, initialDelaySeconds, 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:8080" : "http://subsonic.org"; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setUpnpService(UPnPService upnpService) { + this.upnpService = upnpService; + } + + 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 = ClingRouter.findRouter(upnpService); + if (router != null) { + return router; + } + } catch (Throwable x) { + LOG.warn("Failed to find UPnP router using Cling 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() && settingsService.getUrlRedirectType() == UrlRedirectType.NORMAL; + HttpPost request = new HttpPost(enable ? URL_REDIRECTION_REGISTER_URL : URL_REDIRECTION_UNREGISTER_URL); + + int port = settingsService.getPort(); + boolean trial = !settingsService.isLicenseValid(); + Date trialExpires = settingsService.getTrialExpires(); + + List params = new ArrayList(); + 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", settingsService.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())); + testUrlRedirection = false; + break; + case HttpStatus.SC_OK: + urlRedirectionStatus.setText(enable ? "Successfully registered web address." : "Web address disabled."); + break; + default: + testUrlRedirection = false; + 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() { + + String urlToTest; + String url = URL_REDIRECTION_TEST_URL; + if (settingsService.getUrlRedirectType() == UrlRedirectType.NORMAL) { + url += "?redirectFrom=" + settingsService.getUrlRedirectFrom(); + urlToTest = settingsService.getUrlRedirectFrom() + ".subsonic.org"; + } else { + url += "?customUrl=" + settingsService.getUrlRedirectCustomUrl(); + urlToTest = settingsService.getUrlRedirectCustomUrl(); + } + + HttpGet request = new HttpGet(url); + HttpClient client = new DefaultHttpClient(); + HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000); + HttpConnectionParams.setSoTimeout(client.getParams(), 30000); + + try { + urlRedirectionStatus.setText("Testing web address " + urlToTest + ". 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..4e56a6b9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlayerService.java @@ -0,0 +1,346 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; + +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.domain.User; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * 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 getPlayer(request, response, true) . + */ + 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 = getNonRestPlayerByIpAddressAndUsername(request.getRemoteAddr(), username); + } + + // 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 null 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 (non-REST) 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 null if no such player exists. + */ + private Player getNonRestPlayerByIpAddressAndUsername(final String ipAddress, final String username) { + if (ipAddress == null) { + return null; + } + for (Player player : getAllPlayers()) { + boolean isRest = player.getClientId() != null; + boolean ipMatches = ipAddress.equals(player.getIpAddress()); + boolean userMatches = username == null || username.equals(player.getUsername()); + if (!isRest && 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 null 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 StringUtils.trimToNull(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 null. + * @return All relevant players. + */ + public List getPlayersForUserAndClientId(String username, String clientId) { + return playerDao.getPlayersForUserAndClientId(username, clientId); + } + + /** + * Returns all currently registered players. + * + * @return All currently registered players. + */ + public List 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 transcodings = transcodingService.getAllTranscodings(); + List defaultActiveTranscodings = new ArrayList(); + for (Transcoding transcoding : transcodings) { + if (transcoding.isDefaultActive()) { + defaultActiveTranscodings.add(transcoding); + } + } + + transcodingService.setTranscodingsForPlayer(player, defaultActiveTranscodings); + } + + /** + * Returns a player associated to the special "guest" user, creating it if necessary. + */ + public Player getGuestPlayer(HttpServletRequest request) { + + // Create guest user if necessary. + User user = securityService.getUserByName(User.USERNAME_GUEST); + if (user == null) { + user = new User(User.USERNAME_GUEST, RandomStringUtils.randomAlphanumeric(30), null); + user.setStreamRole(true); + securityService.createUser(user); + } + + // Look for existing player. + List players = getPlayersForUserAndClientId(User.USERNAME_GUEST, null); + if (!players.isEmpty()) { + return players.get(0); + } + + // Create player if necessary. + Player player = new Player(); + if (request != null ) { + player.setIpAddress(request.getRemoteAddr()); + } + player.setUsername(User.USERNAME_GUEST); + createPlayer(player); + + return player; + } + + 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..dbe33300 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PlaylistService.java @@ -0,0 +1,513 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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.MusicFolder; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.util.Pair; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +/** + * 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 List getAllPlaylists() { + return sort(playlistDao.getAllPlaylists()); + } + + public List getReadablePlaylistsForUser(String username) { + return sort(playlistDao.getReadablePlaylistsForUser(username)); + } + + public List getWritablePlaylistsForUser(String username) { + + // Admin users are allowed to modify all playlists that are visible to them. + if (securityService.isAdmin(username)) { + return getReadablePlaylistsForUser(username); + } + + return sort(playlistDao.getWritablePlaylistsForUser(username)); + } + + private List sort(List playlists) { + Collections.sort(playlists, new PlaylistComparator()); + return playlists; + } + + public Playlist getPlaylist(int id) { + return playlistDao.getPlaylist(id); + } + + public List getPlaylistUsers(int playlistId) { + return playlistDao.getPlaylistUsers(playlistId); + } + + public List getFilesInPlaylist(int id) { + return getFilesInPlaylist(id, false); + } + + public List getFilesInPlaylist(int id, boolean includeNotPresent) { + List files = mediaFileDao.getFilesInPlaylist(id); + if (includeNotPresent) { + return files; + } + List presentFiles = new ArrayList(files.size()); + for (MediaFile file : files) { + if (file.isPresent()) { + presentFiles.add(file); + } + } + return presentFiles; + } + + public void setFilesInPlaylist(int id, List files) { + playlistDao.setFilesInPlaylist(id, files); + } + + public void createPlaylist(Playlist playlist) { + playlistDao.createPlaylist(playlist); + } + + public void addPlaylistUser(int playlistId, String username) { + playlistDao.addPlaylistUser(playlistId, username); + } + + public void deletePlaylistUser(int playlistId, String username) { + playlistDao.deletePlaylistUser(playlistId, username); + } + + public boolean isReadAllowed(Playlist playlist, String username) { + if (username == null) { + return false; + } + if (username.equals(playlist.getUsername()) || playlist.isShared()) { + 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, Playlist existingPlaylist) throws Exception { + PlaylistFormat playlistFormat = getPlaylistFormat(format); + if (playlistFormat == null) { + throw new Exception("Unsupported playlist format: " + format); + } + + Pair, List> result = parseFiles(IOUtils.toByteArray(inputStream), playlistFormat); + if (result.getFirst().isEmpty() && !result.getSecond().isEmpty()) { + throw new Exception("No songs in the playlist were found."); + } + + for (String error : result.getSecond()) { + LOG.warn("File in playlist '" + fileName + "' not found: " + error); + } + Date now = new Date(); + Playlist playlist; + if (existingPlaylist == null) { + playlist = new Playlist(); + playlist.setUsername(username); + playlist.setCreated(now); + playlist.setChanged(now); + playlist.setShared(true); + playlist.setName(playlistName); + playlist.setComment("Auto-imported from " + fileName); + playlist.setImportedFrom(fileName); + createPlaylist(playlist); + } else { + playlist = existingPlaylist; + } + + setFilesInPlaylist(playlist.getId(), result.getFirst()); + + return playlist; + } + + private Pair, List> parseFiles(byte[] playlist, PlaylistFormat playlistFormat) throws IOException { + Pair, List> result = null; + + // Try with multiple encodings; use the one that finds the most files. + String[] encodings = {StringUtil.ENCODING_LATIN, StringUtil.ENCODING_UTF8, Charset.defaultCharset().name()}; + for (String encoding : encodings) { + Pair, List> files = parseFilesWithEncoding(playlist, playlistFormat, encoding); + if (result == null || result.getFirst().size() < files.getFirst().size()) { + result = files; + } + } + return result; + } + + private Pair, List> parseFilesWithEncoding(byte[] playlist, PlaylistFormat playlistFormat, String encoding) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(playlist), encoding)); + return playlistFormat.parse(reader, mediaFileService); + } + + public void exportPlaylist(int id, OutputStream out) throws Exception { + PrintWriter writer = new PrintWriter(new OutputStreamWriter(out, StringUtil.ENCODING_UTF8)); + new M3UFormat().format(getFilesInPlaylist(id, true), writer); + } + + public void importPlaylists() { + try { + LOG.info("Starting playlist import."); + doImportPlaylists(); + LOG.info("Completed playlist import."); + } catch (Throwable x) { + LOG.warn("Failed to import playlists: " + x, x); + } + } + + private void doImportPlaylists() throws Exception { + String playlistFolderPath = settingsService.getPlaylistFolder(); + if (playlistFolderPath == null) { + return; + } + File playlistFolder = new File(playlistFolderPath); + if (!playlistFolder.exists()) { + return; + } + + List allPlaylists = playlistDao.getAllPlaylists(); + for (File file : playlistFolder.listFiles()) { + try { + importPlaylistIfUpdated(file, allPlaylists); + } catch (Exception x) { + LOG.warn("Failed to auto-import playlist " + file + ". " + x.getMessage()); + } + } + } + + private void importPlaylistIfUpdated(File file, List allPlaylists) throws Exception { + String format = FilenameUtils.getExtension(file.getPath()); + if (getPlaylistFormat(format) == null) { + return; + } + + String fileName = file.getName(); + Playlist existingPlaylist = null; + for (Playlist playlist : allPlaylists) { + if (fileName.equals(playlist.getImportedFrom())) { + existingPlaylist = playlist; + if (file.lastModified() <= playlist.getChanged().getTime()) { + // Already imported and not changed since. + return; + } + } + } + InputStream in = new FileInputStream(file); + try { + importPlaylist(User.USERNAME_ADMIN, FilenameUtils.getBaseName(fileName), fileName, format, in, existingPlaylist); + LOG.info("Auto-imported playlist " + file); + } finally { + IOUtils.closeQuietly(in); + } + } + + private 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; + } + + 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 class PlaylistFormat { + public abstract Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException; + + public abstract void format(List files, PrintWriter writer) throws IOException; + + + protected MediaFile getMediaFile(String path) { + try { + File file = new File(path); + if (!file.exists()) { + return null; + } + + file = normalizePath(file); + if (file == null) { + return null; + } + MediaFile mediaFile = mediaFileService.getMediaFile(file); + if (mediaFile != null && mediaFile.exists()) { + return mediaFile; + } + } catch (SecurityException x) { + // Ignored + } catch (IOException x) { + // Ignored + } + return null; + } + + /** + * Paths in an external playlist may not have the same upper/lower case as in the (case sensitive) media_file table. + * This methods attempts to normalize the external path to match the one stored in the table. + */ + private File normalizePath(File file) throws IOException { + + // Only relevant for Windows where paths are case insensitive. + if (!Util.isWindows()) { + return file; + } + + // Find the most specific music folder. + String canonicalPath = file.getCanonicalPath(); + MusicFolder containingMusicFolder = null; + for (MusicFolder musicFolder : settingsService.getAllMusicFolders()) { + String musicFolderPath = musicFolder.getPath().getPath(); + if (canonicalPath.toLowerCase().startsWith(musicFolderPath.toLowerCase())) { + if (containingMusicFolder == null || containingMusicFolder.getPath().length() < musicFolderPath.length()) { + containingMusicFolder = musicFolder; + } + } + } + + if (containingMusicFolder == null) { + return null; + } + + return new File(containingMusicFolder.getPath().getPath() + canonicalPath.substring(containingMusicFolder.getPath().getPath().length())); + // TODO: Consider slashes. + } + } + + private class M3UFormat extends PlaylistFormat { + public Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { + List ok = new ArrayList(); + List error = new ArrayList(); + String line = reader.readLine(); + while (line != null) { + if (!line.startsWith("#")) { + MediaFile file = getMediaFile(line); + if (file != null) { + ok.add(file); + } else { + error.add(line); + } + } + line = reader.readLine(); + } + return new Pair, List>(ok, error); + } + + public void format(List files, PrintWriter writer) throws IOException { + writer.println("#EXTM3U"); + for (MediaFile file : files) { + writer.println(file.getPath()); + } + if (writer.checkError()) { + throw new IOException("Error when writing playlist"); + } + } + } + + /** + * Implementation of PLS playlist format. + */ + private class PLSFormat extends PlaylistFormat { + public Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { + List ok = new ArrayList(); + List error = new ArrayList(); + + Pattern pattern = Pattern.compile("^File\\d+=(.*)$"); + String line = reader.readLine(); + while (line != null) { + + Matcher matcher = pattern.matcher(line); + if (matcher.find()) { + String path = matcher.group(1); + MediaFile file = getMediaFile(path); + if (file != null) { + ok.add(file); + } else { + error.add(path); + } + } + line = reader.readLine(); + } + return new Pair, List>(ok, error); + } + + public void format(List files, PrintWriter writer) throws IOException { + writer.println("[playlist]"); + int counter = 0; + + for (MediaFile file : files) { + counter++; + writer.println("File" + counter + '=' + file.getPath()); + } + writer.println("NumberOfEntries=" + counter); + writer.println("Version=2"); + + if (writer.checkError()) { + throw new IOException("Error when writing playlist."); + } + } + } + + /** + * Implementation of XSPF (http://www.xspf.org/) playlist format. + */ + private class XSPFFormat extends PlaylistFormat { + public Pair, List> parse(BufferedReader reader, MediaFileService mediaFileService) throws IOException { + List ok = new ArrayList(); + List error = new ArrayList(); + + SAXBuilder builder = new SAXBuilder(); + Document document; + try { + document = builder.build(reader); + } catch (JDOMException x) { + LOG.warn("Failed to parse XSPF playlist.", x); + throw new IOException("Failed to parse XSPF playlist."); + } + + Element root = document.getRootElement(); + Namespace ns = root.getNamespace(); + Element trackList = root.getChild("trackList", ns); + List tracks = trackList.getChildren("track", ns); + + for (Object obj : tracks) { + Element track = (Element) obj; + String location = track.getChildText("location", ns); + if (location != null && location.startsWith("file://")) { + location = location.replaceFirst("file://", ""); + MediaFile file = getMediaFile(location); + if (file != null) { + ok.add(file); + } else { + error.add(location); + } + } + } + return new Pair, List>(ok, error); + } + + public void format(List files, PrintWriter writer) throws IOException { + writer.println(""); + writer.println(""); + writer.println(" "); + + for (MediaFile file : files) { + writer.println(" file://" + StringEscapeUtils.escapeXml(file.getPath()) + ""); + } + writer.println(" "); + writer.println(""); + + if (writer.checkError()) { + throw new IOException("Error when writing playlist."); + } + } + } + + private static class PlaylistComparator implements Comparator { + @Override + public int compare(Playlist p1, Playlist p2) { + return p1.getName().compareTo(p2.getName()); + } + } +} 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..a0c1c4d8 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/PodcastService.java @@ -0,0 +1,765 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.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; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.http.Header; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.ContentType; +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 com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; + +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.service.metadata.MetaData; +import net.sourceforge.subsonic.service.metadata.MetaDataParser; +import net.sourceforge.subsonic.service.metadata.MetaDataParserFactory; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * 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; + private MetaDataParserFactory metaDataParserFactory; + + 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() { + try { + // Clean up partial downloads. + for (PodcastChannel channel : getAllChannels()) { + for (PodcastEpisode episode : getEpisodes(channel.getId())) { + if (episode.getStatus() == PodcastStatus.DOWNLOADING) { + deleteEpisode(episode.getId(), false); + LOG.info("Deleted Podcast episode '" + episode.getTitle() + "' since download was interrupted."); + } + } + } + schedule(); + } catch (Throwable x) { + LOG.error("Failed to initialize PodcastService: " + x, x); + } + } + + 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"); + } + + /** + * Returns a single Podcast channel. + */ + public PodcastChannel getChannel(int channelId) { + PodcastChannel channel = podcastDao.getChannel(channelId); + addMediaFileIdToChannels(Arrays.asList(channel)); + return channel; + } + + /** + * Returns all Podcast channels. + * + * @return Possibly empty list of all Podcast channels. + */ + public List getAllChannels() { + return addMediaFileIdToChannels(podcastDao.getAllChannels()); + } + + private PodcastEpisode getEpisodeByUrl(String url) { + PodcastEpisode episode = podcastDao.getEpisodeByUrl(url); + if (episode == null) { + return null; + } + List episodes = Arrays.asList(episode); + episodes = filterAllowed(episodes); + addMediaFileIdToEpisodes(episodes); + return episodes.isEmpty() ? null : episodes.get(0); + } + + /** + * Returns all Podcast episodes for a given channel. + * + * @param channelId The Podcast channel ID. + * @return Possibly empty list of all Podcast episodes for the given channel, sorted in + * reverse chronological order (newest episode first). + */ + public List getEpisodes(int channelId) { + List episodes = filterAllowed(podcastDao.getEpisodes(channelId)); + return addMediaFileIdToEpisodes(episodes); + } + + /** + * Returns the N newest episodes. + * + * @return Possibly empty list of the newest Podcast episodes, sorted in + * reverse chronological order (newest episode first). + */ + public List getNewestEpisodes(int count) { + List episodes = addMediaFileIdToEpisodes(podcastDao.getNewestEpisodes(count)); + + return Lists.newArrayList(Iterables.filter(episodes, new Predicate() { + @Override + public boolean apply(PodcastEpisode episode) { + Integer mediaFileId = episode.getMediaFileId(); + if (mediaFileId == null) { + return false; + } + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + return mediaFile != null && mediaFile.isPresent(); + } + })); + } + + private List filterAllowed(List episodes) { + List result = new ArrayList(episodes.size()); + for (PodcastEpisode episode : episodes) { + if (episode.getPath() == null || securityService.isReadAllowed(new File(episode.getPath()))) { + result.add(episode); + } + } + return result; + } + + 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 List addMediaFileIdToEpisodes(List episodes) { + for (PodcastEpisode episode : episodes) { + if (episode.getPath() != null) { + MediaFile mediaFile = mediaFileService.getMediaFile(episode.getPath()); + if (mediaFile != null && mediaFile.isPresent()) { + episode.setMediaFileId(mediaFile.getId()); + } + } + } + return episodes; + } + + private List addMediaFileIdToChannels(List channels) { + for (PodcastChannel channel : channels) { + try { + File dir = getChannelDirectory(channel); + MediaFile mediaFile = mediaFileService.getMediaFile(dir); + if (mediaFile != null) { + channel.setMediaFileId(mediaFile.getId()); + } + } catch (Exception x) { + LOG.warn("Failed to resolve media file ID for podcast channel '" + channel.getTitle() + "': " + x, x); + } + } + return channels; + } + + public void refreshChannel(int channelId, boolean downloadEpisodes) { + refreshChannels(Arrays.asList(getChannel(channelId)), downloadEpisodes); + } + + public void refreshAllChannels(boolean downloadEpisodes) { + refreshChannels(getAllChannels(), downloadEpisodes); + } + + private void refreshChannels(final List 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(StringUtil.removeMarkup(channelElement.getChildTextTrim("title"))); + channel.setDescription(StringUtil.removeMarkup(channelElement.getChildTextTrim("description"))); + channel.setImageUrl(getChannelImageUrl(channelElement)); + channel.setStatus(PodcastStatus.COMPLETED); + channel.setErrorMessage(null); + podcastDao.updateChannel(channel); + + downloadImage(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(getErrorMessage(x)); + podcastDao.updateChannel(channel); + } finally { + IOUtils.closeQuietly(in); + client.getConnectionManager().shutdown(); + } + + if (downloadEpisodes) { + for (final PodcastEpisode episode : getEpisodes(channel.getId())) { + if (episode.getStatus() == PodcastStatus.NEW && episode.getUrl() != null) { + downloadEpisode(episode); + } + } + } + } + + private void downloadImage(PodcastChannel channel) { + HttpClient client = new DefaultHttpClient(); + InputStream in = null; + OutputStream out = null; + try { + String imageUrl = channel.getImageUrl(); + if (imageUrl == null) { + return; + } + + File dir = getChannelDirectory(channel); + MediaFile channelMediaFile = mediaFileService.getMediaFile(dir); + File existingCoverArt = mediaFileService.getCoverArt(channelMediaFile); + boolean imageFileExists = existingCoverArt != null && mediaFileService.getMediaFile(existingCoverArt) == null; + if (imageFileExists) { + return; + } + + HttpGet method = new HttpGet(imageUrl); + HttpResponse response = client.execute(method); + in = response.getEntity().getContent(); + out = new FileOutputStream(new File(dir, "cover." + getCoverArtSuffix(response))); + IOUtils.copy(in, out); + mediaFileService.refreshMediaFile(channelMediaFile); + } catch (Exception x) { + LOG.warn("Failed to download cover art for podcast channel '" + channel.getTitle() + "': " + x, x); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + client.getConnectionManager().shutdown(); + } + } + + private String getCoverArtSuffix(HttpResponse response) { + String result = null; + Header contentTypeHeader = response.getEntity().getContentType(); + if (contentTypeHeader != null && contentTypeHeader.getValue() != null) { + ContentType contentType = ContentType.parse(contentTypeHeader.getValue()); + String mimeType = contentType.getMimeType(); + result = StringUtil.getSuffix(mimeType); + } + return result == null ? "jpeg" : result; + } + + private String getChannelImageUrl(Element channelElement) { + String result = getITunesAttribute(channelElement, "image", "href"); + if (result == null) { + Element imageElement = channelElement.getChild("image"); + if (imageElement != null) { + result = imageElement.getChildTextTrim("url"); + } + } + return result; + } + + private String getErrorMessage(Exception x) { + return x.getMessage() != null ? x.getMessage() : x.toString(); + } + + public void downloadEpisode(final PodcastEpisode episode) { + Runnable task = new Runnable() { + public void run() { + doDownloadEpisode(episode); + } + }; + downloadExecutor.submit(task); + } + + private void refreshEpisodes(PodcastChannel channel, List episodeElements) { + + List episodes = new ArrayList(); + + 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"); + } + title = StringUtil.removeMarkup(title); + description = StringUtil.removeMarkup(description); + + Element enclosure = episodeElement.getChild("enclosure"); + if (enclosure == null) { + LOG.info("No enclosure found for episode " + title); + continue; + } + + String url = enclosure.getAttributeValue("url"); + url = sanitizeUrl(url); + if (url == null) { + LOG.info("No enclosure URL found for episode " + title); + continue; + } + + if (getEpisodeByUrl(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() { + 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 String getITunesAttribute(Element element, String childName, String attributeName) { + for (Namespace ns : ITUNES_NAMESPACES) { + Element elem = element.getChild(childName, ns); + if (elem != null) { + return StringUtils.trimToNull(elem.getAttributeValue(attributeName)); + } + } + return null; + } + + private void doDownloadEpisode(PodcastEpisode episode) { + InputStream in = null; + OutputStream out = null; + + if (isEpisodeDeleted(episode)) { + LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download."); + return; + } + + LOG.info("Starting to download Podcast from " + episode.getUrl()); + + HttpClient client = new DefaultHttpClient(); + try { + + if (!settingsService.getLicenseInfo().isLicenseOrTrialValid()) { + throw new Exception("Sorry, the trial period is expired."); + } + + 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; + + // Abort download if episode was deleted by user. + if (isEpisodeDeleted(episode)) { + break; + } + podcastDao.updateEpisode(episode); + } + } + + if (isEpisodeDeleted(episode)) { + LOG.info("Podcast " + episode.getUrl() + " was deleted. Aborting download."); + IOUtils.closeQuietly(out); + file.delete(); + } else { + addMediaFileIdToEpisodes(Arrays.asList(episode)); + episode.setBytesDownloaded(bytesDownloaded); + podcastDao.updateEpisode(episode); + LOG.info("Downloaded " + bytesDownloaded + " bytes from Podcast " + episode.getUrl()); + IOUtils.closeQuietly(out); + updateTags(file, episode); + 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(getErrorMessage(x)); + podcastDao.updateEpisode(episode); + } finally { + IOUtils.closeQuietly(in); + IOUtils.closeQuietly(out); + client.getConnectionManager().shutdown(); + } + } + + private boolean isEpisodeDeleted(PodcastEpisode episode) { + episode = podcastDao.getEpisode(episode.getId()); + return episode == null || episode.getStatus() == PodcastStatus.DELETED; + } + + private void updateTags(File file, PodcastEpisode episode) { + try { + MediaFile mediaFile = mediaFileService.getMediaFile(file, false); + if (StringUtils.isNotBlank(episode.getTitle())) { + MetaDataParser parser = metaDataParserFactory.getParser(file); + if (!parser.isEditingSupported()) { + return; + } + MetaData metaData = parser.getRawMetaData(file); + metaData.setTitle(episode.getTitle()); + parser.setMetaData(mediaFile, metaData); + mediaFileService.refreshMediaFile(mediaFile); + } + } catch (Exception x) { + LOG.warn("Failed to update tags for podcast " + episode.getUrl(), x); + } + } + + private synchronized void deleteObsoleteEpisodes(PodcastChannel channel) { + int episodeCount = settingsService.getPodcastEpisodeRetentionCount(); + if (episodeCount == -1) { + return; + } + + List episodes = getEpisodes(channel.getId()); + + // 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 channelDir = getChannelDirectory(channel); + + 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; + } + + private File getChannelDirectory(PodcastChannel channel) { + 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); + } + return channelDir; + } + + /** + * 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 episodes = getEpisodes(channelId); + 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; + } + + public void setMetaDataParserFactory(MetaDataParserFactory metaDataParserFactory) { + this.metaDataParserFactory = metaDataParserFactory; + } +} 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..8a56c491 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/RatingService.java @@ -0,0 +1,108 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +import net.sourceforge.subsonic.dao.RatingDao; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.util.FileUtil; + +/** + * Provides services for user ratings. + * + * @author Sindre Mehus + */ +public class RatingService { + + private RatingDao ratingDao; + private SecurityService securityService; + private MediaFileService mediaFileService; + + /** + * Returns the highest rated albums. + * + * @param offset Number of albums to skip. + * @param count Maximum number of albums to return. + * @param musicFolders Only return albums in these folders. + * @return The highest rated albums. + */ + public List getHighestRatedAlbums(int offset, int count, List musicFolders) { + List highestRated = ratingDao.getHighestRatedAlbums(offset, count, musicFolders); + List result = new ArrayList(); + 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 null 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 null 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 null if no rating is set. + */ + public Integer getRatingForUser(String username, MediaFile mediaFile) { + return ratingDao.getRatingForUser(username, mediaFile); + } + + public int getRatedAlbumCount(String username, List musicFolders) { + return ratingDao.getRatedAlbumCount(username, musicFolders); + } + + 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..45807795 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SearchService.java @@ -0,0 +1,615 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +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.analysis.tokenattributes.TermAttribute; +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.NumericRangeQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.ScoreDoc; +import org.apache.lucene.search.Searcher; +import org.apache.lucene.search.TermQuery; +import org.apache.lucene.search.TopDocs; +import org.apache.lucene.search.spans.SpanOrQuery; +import org.apache.lucene.search.spans.SpanQuery; +import org.apache.lucene.search.spans.SpanTermQuery; +import org.apache.lucene.store.Directory; +import org.apache.lucene.store.FSDirectory; +import org.apache.lucene.util.NumericUtils; +import org.apache.lucene.util.Version; + +import com.google.common.collect.Lists; + +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 static net.sourceforge.subsonic.service.SearchService.IndexType.*; + +/** + * 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 String FIELD_FOLDER_ID = "folderId"; + + private static final Version LUCENE_VERSION = Version.LUCENE_30; + private static final String LUCENE_DIR = "lucene2"; + + private MediaFileService mediaFileService; + 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, MusicFolder musicFolder) { + try { + artistId3Writer.addDocument(ARTIST_ID3.createDocument(artist, musicFolder)); + } 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, List musicFolders, 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()); + + BooleanQuery query = new BooleanQuery(); + query.add(queryParser.parse(analyzeQuery(criteria.getQuery())), BooleanClause.Occur.MUST); + + List musicFolderQueries = new ArrayList(); + for (MusicFolder musicFolder : musicFolders) { + if (indexType == ALBUM_ID3 || indexType == ARTIST_ID3) { + musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId())))); + } else { + musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath()))); + } + } + query.add(new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), BooleanClause.Occur.MUST); + + 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; + } + + private String analyzeQuery(String query) throws IOException { + StringBuilder result = new StringBuilder(); + ASCIIFoldingFilter filter = new ASCIIFoldingFilter(new StandardTokenizer(LUCENE_VERSION, new StringReader(query))); + TermAttribute termAttribute = filter.getAttribute(TermAttribute.class); + while (filter.incrementToken()) { + result.append(termAttribute.term()).append("* "); + } + return result.toString(); + } + + /** + * Returns a number of random songs. + * + * @param criteria Search criteria. + * @return List of random songs. + */ + public List getRandomSongs(RandomSearchCriteria criteria) { + List result = new ArrayList(); + + 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 rangeQuery = NumericRangeQuery.newIntRange(FIELD_YEAR, criteria.getFromYear(), criteria.getToYear(), true, true); + query.add(rangeQuery, BooleanClause.Occur.MUST); + } + + List musicFolderQueries = new ArrayList(); + for (MusicFolder musicFolder : criteria.getMusicFolders()) { + musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath()))); + } + query.add(new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])), BooleanClause.Occur.MUST); + + TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); + List scoreDocs = Lists.newArrayList(topDocs.scoreDocs); + Random random = new Random(System.currentTimeMillis()); + + while (!scoreDocs.isEmpty() && result.size() < criteria.getCount()) { + int index = random.nextInt(scoreDocs.size()); + Document doc = searcher.doc(scoreDocs.remove(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); + } + } + + } 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(" ", "").replace("-", ""); + } + + /** + * Returns a number of random albums. + * + * @param count Number of albums to return. + * @param musicFolders Only return albums from these folders. + * @return List of random albums. + */ + public List getRandomAlbums(int count, List musicFolders) { + List result = new ArrayList(); + + IndexReader reader = null; + try { + reader = createIndexReader(ALBUM); + Searcher searcher = new IndexSearcher(reader); + + List musicFolderQueries = new ArrayList(); + for (MusicFolder musicFolder : musicFolders) { + musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER, musicFolder.getPath().getPath()))); + } + Query query = new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); + + TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); + List scoreDocs = Lists.newArrayList(topDocs.scoreDocs); + Random random = new Random(System.currentTimeMillis()); + + while (!scoreDocs.isEmpty() && result.size() < count) { + int index = random.nextInt(scoreDocs.size()); + Document doc = searcher.doc(scoreDocs.remove(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. + * @param musicFolders Only return albums from these folders. + * @return List of random albums. + */ + public List getRandomAlbumsId3(int count, List musicFolders) { + List result = new ArrayList(); + + IndexReader reader = null; + try { + reader = createIndexReader(ALBUM_ID3); + Searcher searcher = new IndexSearcher(reader); + + List musicFolderQueries = new ArrayList(); + for (MusicFolder musicFolder : musicFolders) { + musicFolderQueries.add(new SpanTermQuery(new Term(FIELD_FOLDER_ID, NumericUtils.intToPrefixCoded(musicFolder.getId())))); + } + Query query = new SpanOrQuery(musicFolderQueries.toArray(new SpanQuery[musicFolderQueries.size()])); + TopDocs topDocs = searcher.search(query, null, Integer.MAX_VALUE); + List scoreDocs = Lists.newArrayList(topDocs.scoreDocs); + Random random = new Random(System.currentTimeMillis()); + + while (!scoreDocs.isEmpty() && result.size() < count) { + int index = random.nextInt(scoreDocs.size()); + Document doc = searcher.doc(scoreDocs.remove(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 void addIfNotNull(T value, List 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(), LUCENE_DIR); + } + + 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 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_FOLDER}, 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)); + } + if (mediaFile.getFolder() != null) { + doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); + } + + return doc; + } + }, + + ALBUM_ID3(new String[]{FIELD_ALBUM, FIELD_ARTIST, FIELD_FOLDER_ID}, 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)); + } + if (album.getFolderId() != null) { + doc.add(new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true).setIntValue(album.getFolderId())); + } + + return doc; + } + }, + + ARTIST(new String[]{FIELD_ARTIST, FIELD_FOLDER}, 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)); + } + if (mediaFile.getFolder() != null) { + doc.add(new Field(FIELD_FOLDER, mediaFile.getFolder(), Field.Store.NO, Field.Index.NOT_ANALYZED_NO_NORMS)); + } + + return doc; + } + }, + + ARTIST_ID3(new String[]{FIELD_ARTIST}, null) { + @Override + public Document createDocument(Artist artist, MusicFolder musicFolder) { + 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)); + doc.add(new NumericField(FIELD_FOLDER_ID, Field.Store.NO, true).setIntValue(musicFolder.getId())); + + return doc; + } + }; + + private final String[] fields; + private final Map boosts; + + private IndexType(String[] fields, String boostedField) { + this.fields = fields; + boosts = new HashMap(); + 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, MusicFolder musicFolder) { + throw new UnsupportedOperationException(); + } + + protected Document createDocument(Album album) { + throw new UnsupportedOperationException(); + } + + public Map 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..2a2853b6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SecurityService.java @@ -0,0 +1,318 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.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.MediaFile; +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 null) + * @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 null. + */ + 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 null. + */ + 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 null 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 null if not found. + */ + public User getUserByEmail(String email) { + return userDao.getUserByEmail(email); + } + + /** + * Returns all users. + * + * @return Possibly empty array of all users. + */ + public List 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); + settingsService.setMusicFoldersForUser(user.getUsername(), MusicFolder.toIdList(settingsService.getAllMusicFolders())); + 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 null. + * @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 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; + } + + public boolean isFolderAccessAllowed(MediaFile file, String username) { + if (isInPodcastFolder(file.getFile())) { + return true; + } + + for (MusicFolder musicFolder : settingsService.getMusicFoldersForUser(username)) { + if (musicFolder.getPath().getPath().equals(file.getFolder())) { + return true; + } + } + return false; + } + + /** + * 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 false. + * + * @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..22abe17d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ServiceLocator.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +/** + * Locates services for objects that are not part of the Spring context. + * + * @author Sindre Mehus + */ +@Deprecated +public class ServiceLocator { + + private static SettingsService settingsService; + private static VersionService versionService; + + private ServiceLocator() { + } + + public static SettingsService getSettingsService() { + return settingsService; + } + + public static void setSettingsService(SettingsService settingsService) { + ServiceLocator.settingsService = settingsService; + } + + public static VersionService getVersionService() { + return versionService; + } + + public static void setVersionService(VersionService versionService) { + ServiceLocator.versionService = versionService; + } +} + 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..ff3b84bd --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SettingsService.java @@ -0,0 +1,1477 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.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.Collections; +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 java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +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.AlbumListType; +import net.sourceforge.subsonic.domain.Avatar; +import net.sourceforge.subsonic.domain.InternetRadio; +import net.sourceforge.subsonic.domain.LicenseInfo; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.domain.UrlRedirectType; +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"); + + // Number of free trial days. + public static final long TRIAL_DAYS = 30L; + + // 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 = "CoverArtFileTypes2"; + private static final String KEY_COVER_ART_CONCURRENCY = "CoverArtConcurrency"; + 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_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 = "DownsamplingCommand4"; + private static final String KEY_HLS_COMMAND = "HlsCommand3"; + private static final String KEY_JUKEBOX_COMMAND = "JukeboxCommand2"; + private static final String KEY_VIDEO_IMAGE_COMMAND = "VideoImageCommand"; + 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_TYPE = "UrlRedirectType"; + private static final String KEY_URL_REDIRECT_FROM = "UrlRedirectFrom"; + private static final String KEY_URL_REDIRECT_CONTEXT_PATH = "UrlRedirectContextPath"; + private static final String KEY_URL_REDIRECT_CUSTOM_URL = "UrlRedirectCustomUrl"; + 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"; + private static final String KEY_MEDIA_LIBRARY_STATISTICS = "MediaLibraryStatistics"; + private static final String KEY_TRIAL_EXPIRES = "TrialExpires"; + private static final String KEY_DLNA_ENABLED = "DlnaEnabled"; + private static final String KEY_DLNA_SERVER_NAME = "DlnaServerName"; + private static final String KEY_SONOS_ENABLED = "SonosEnabled"; + private static final String KEY_SONOS_SERVICE_NAME = "SonosServiceName"; + private static final String KEY_SONOS_SERVICE_ID = "SonosServiceId"; + + // 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 cover.png cover.gif folder.jpg jpg jpeg gif png"; + private static final int DEFAULT_COVER_ART_CONCURRENCY = 4; + 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 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 -map 0:0 -b:a %bk -v 0 -f mp3 -"; + private static final String DEFAULT_HLS_COMMAND = "ffmpeg -ss %o -t %d -i %s -async 1 -b:v %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f mpegts -c:v libx264 -preset superfast -c:a libmp3lame -threads 0 -"; + private static final String DEFAULT_JUKEBOX_COMMAND = "ffmpeg -ss %o -i %s -map 0:0 -v 0 -ar 44100 -ac 2 -f s16be -"; + private static final String DEFAULT_VIDEO_IMAGE_COMMAND = "ffmpeg -r 1 -ss %o -t 1 -i %s -s %wx%h -v 0 -f mjpeg -"; + 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 UrlRedirectType DEFAULT_URL_REDIRECT_TYPE = UrlRedirectType.NORMAL; + private static final String DEFAULT_URL_REDIRECT_FROM = "yourname"; + private static final String DEFAULT_URL_REDIRECT_CONTEXT_PATH = System.getProperty("subsonic.contextPath", "").replaceAll("/", ""); + private static final String DEFAULT_URL_REDIRECT_CUSTOM_URL = "http://"; + 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; + private static final String DEFAULT_MEDIA_LIBRARY_STATISTICS = "0 0 0 0 0"; + private static final String DEFAULT_TRIAL_EXPIRES = null; + private static final boolean DEFAULT_DLNA_ENABLED = false; + private static final String DEFAULT_DLNA_SERVER_NAME = "Subsonic"; + private static final boolean DEFAULT_SONOS_ENABLED = false; + private static final String DEFAULT_SONOS_SERVICE_NAME = "Subsonic"; + private static final int DEFAULT_SONOS_SERVICE_ID = 242; + + // Array of obsolete keys. Used to clean property file. + private static final List OBSOLETE_KEYS = Arrays.asList("PortForwardingPublicPort", "PortForwardingLocalPort", + "DownsamplingCommand", "DownsamplingCommand2", "DownsamplingCommand3", "AutoCoverBatch", "MusicMask", + "VideoMask", "CoverArtMask, HlsCommand", "HlsCommand2", "JukeboxCommand", "UrlRedirectTrialExpires", "VideoTrialExpires", + "CoverArtFileTypes", "UrlRedirectCustomHost", "CoverArtLimit", "StreamPort"); + + 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 themes; + private List 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 cachedMusicFolders; + private final ConcurrentMap> cachedMusicFoldersPerUser = new ConcurrentHashMap>(); + + private static File subsonicHome; + + private boolean licenseValidated = true; + private Date licenseExpires; + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + private ScheduledFuture licenseValidationFuture; + + private static final long LICENSE_VALIDATION_DELAY_HOURS = 12; + private static final long LOCAL_IP_LOOKUP_DELAY_SECONDS = 60; + private String localIpAddress; + + 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 iterator = properties.keySet().iterator(); iterator.hasNext();) { + String key = (String) iterator.next(); + if (OBSOLETE_KEYS.contains(key)) { + LOG.info("Removing obsolete property [" + key + ']'); + iterator.remove(); + } + } + } + + // Start trial. + if (getTrialExpires() == null) { + Date expiryDate = new Date(System.currentTimeMillis() + TRIAL_DAYS * 24L * 3600L * 1000L); + setTrialExpires(expiryDate); + } + + save(false); + } + + /** + * Register in service locator so that non-Spring objects can access me. + * This method is invoked automatically by Spring. + */ + public void init() { + logServerInfo(); + ServiceLocator.setSettingsService(this); + scheduleLocalIpAddressLookup(); + scheduleLicenseValidation(); + } + + private void logServerInfo() { + LOG.info("Java: " + System.getProperty("java.version") + + ", OS: " + System.getProperty("os.name")); + } + + 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 int getInt(String key, int defaultValue) { + return Integer.valueOf(properties.getProperty(key, String.valueOf(defaultValue))); + } + + private void setInt(String key, int value) { + setProperty(key, String.valueOf(value)); + } + + private long getLong(String key, long defaultValue) { + return Long.valueOf(properties.getProperty(key, String.valueOf(defaultValue))); + } + + private void setLong(String key, long value) { + setProperty(key, String.valueOf(value)); + } + + 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)); + } + + private String getString(String key, String defaultValue) { + return properties.getProperty(key, defaultValue); + } + + private void setString(String key, String value) { + setProperty(key, 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 void setPlaylistFolder(String playlistFolder) { + setProperty(KEY_PLAYLIST_FOLDER, playlistFolder); + } + + 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 getCoverArtConcurrency() { + return getInt(KEY_COVER_ART_CONCURRENCY, DEFAULT_COVER_ART_CONCURRENCY); + } + + 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 getInt(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) { + setInt(KEY_INDEX_CREATION_INTERVAL, days); + } + + /** + * Returns the hour of day (0 - 23) when automatic index creation should run. + */ + public int getIndexCreationHour() { + return getInt(KEY_INDEX_CREATION_HOUR, DEFAULT_INDEX_CREATION_HOUR); + } + + /** + * Sets the hour of day (0 - 23) when automatic index creation should run. + */ + public void setIndexCreationHour(int hour) { + setInt(KEY_INDEX_CREATION_HOUR, 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 getInt(KEY_PODCAST_UPDATE_INTERVAL, DEFAULT_PODCAST_UPDATE_INTERVAL); + } + + /** + * Sets the number of hours between Podcast updates, of -1 if automatic updates + * are disabled. + */ + public void setPodcastUpdateInterval(int hours) { + setInt(KEY_PODCAST_UPDATE_INTERVAL, hours); + } + + /** + * Returns the number of Podcast episodes to keep (-1 to keep all). + */ + public int getPodcastEpisodeRetentionCount() { + return getInt(KEY_PODCAST_EPISODE_RETENTION_COUNT, DEFAULT_PODCAST_EPISODE_RETENTION_COUNT); + } + + /** + * Sets the number of Podcast episodes to keep (-1 to keep all). + */ + public void setPodcastEpisodeRetentionCount(int count) { + setInt(KEY_PODCAST_EPISODE_RETENTION_COUNT, count); + } + + /** + * Returns the number of Podcast episodes to download (-1 to download all). + */ + public int getPodcastEpisodeDownloadCount() { + return getInt(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT); + } + + /** + * Sets the number of Podcast episodes to download (-1 to download all). + */ + public void setPodcastEpisodeDownloadCount(int count) { + setInt(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, 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 getLong(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) { + setLong(KEY_UPLOAD_BITRATE_LIMIT, limit); + } + + 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 LicenseInfo getLicenseInfo() { + Date trialExpires = getTrialExpires(); + Date now = new Date(); + boolean trialValid = trialExpires.after(now); + long trialDaysLeft = trialValid ? (trialExpires.getTime() - now.getTime()) / (24L * 3600L * 1000L) : 0L; + + return new LicenseInfo(getLicenseEmail(), isLicenseValid(), trialExpires, trialDaysLeft, licenseExpires); + } + + public String getDownsamplingCommand() { + return properties.getProperty(KEY_DOWNSAMPLING_COMMAND, DEFAULT_DOWNSAMPLING_COMMAND); + } + + public void setDownsamplingCommand(String command) { + setProperty(KEY_DOWNSAMPLING_COMMAND, command); + } + + public String getHlsCommand() { + return properties.getProperty(KEY_HLS_COMMAND, DEFAULT_HLS_COMMAND); + } + + public void setHlsCommand(String command) { + setProperty(KEY_HLS_COMMAND, command); + } + + public String getJukeboxCommand() { + return properties.getProperty(KEY_JUKEBOX_COMMAND, DEFAULT_JUKEBOX_COMMAND); + } + public String getVideoImageCommand() { + return properties.getProperty(KEY_VIDEO_IMAGE_COMMAND, DEFAULT_VIDEO_IMAGE_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 getInt(KEY_PORT, DEFAULT_PORT); + } + + public void setPort(int port) { + setInt(KEY_PORT, port); + } + + public int getHttpsPort() { + return getInt(KEY_HTTPS_PORT, DEFAULT_HTTPS_PORT); + } + + public void setHttpsPort(int httpsPort) { + setInt(KEY_HTTPS_PORT, 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 getUrlRedirectUrl() { + if (getUrlRedirectType() == UrlRedirectType.NORMAL) { + return "http://" + getUrlRedirectFrom() + ".subsonic.org"; + } + return StringUtils.removeEnd(getUrlRedirectCustomUrl(), "/"); + } + + 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 UrlRedirectType getUrlRedirectType() { + return UrlRedirectType.valueOf(properties.getProperty(KEY_URL_REDIRECT_TYPE, DEFAULT_URL_REDIRECT_TYPE.name())); + } + + public void setUrlRedirectType(UrlRedirectType urlRedirectType) { + properties.setProperty(KEY_URL_REDIRECT_TYPE, urlRedirectType.name()); + } + + public Date getTrialExpires() { + String value = properties.getProperty(KEY_TRIAL_EXPIRES, DEFAULT_TRIAL_EXPIRES); + return value == null ? null : new Date(Long.parseLong(value)); + } + + private void setTrialExpires(Date date) { + String value = (date == null ? null : String.valueOf(date.getTime())); + setProperty(KEY_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 String getUrlRedirectCustomUrl() { + return StringUtils.trimToNull(properties.getProperty(KEY_URL_REDIRECT_CUSTOM_URL, DEFAULT_URL_REDIRECT_CUSTOM_URL)); + } + + public void setUrlRedirectCustomUrl(String customUrl) { + properties.setProperty(KEY_URL_REDIRECT_CUSTOM_URL, customUrl); + } + + 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 getLong(KEY_SETTINGS_CHANGED, 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 { + setLong(KEY_LAST_SCANNED, 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); + } + + public MediaLibraryStatistics getMediaLibraryStatistics() { + return MediaLibraryStatistics.parse(getString(KEY_MEDIA_LIBRARY_STATISTICS, DEFAULT_MEDIA_LIBRARY_STATISTICS)); + } + + public void setMediaLibraryStatistics(MediaLibraryStatistics statistics) { + setString(KEY_MEDIA_LIBRARY_STATISTICS, statistics.format()); + } + + /** + * 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(); + 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 if (elements.length == 3) { + themes.add(new Theme(elements[0], elements[1], elements[2])); + } 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(); + 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 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 getAllMusicFolders(boolean includeDisabled, boolean includeNonExisting) { + if (cachedMusicFolders == null) { + cachedMusicFolders = musicFolderDao.getAllMusicFolders(); + } + + List result = new ArrayList(cachedMusicFolders.size()); + for (MusicFolder folder : cachedMusicFolders) { + if ((includeDisabled || folder.isEnabled()) && (includeNonExisting || FileUtil.exists(folder.getPath()))) { + result.add(folder); + } + } + return result; + } + + /** + * Returns all music folders a user have access to. Non-existing and disabled folders are not included. + * + * @return Possibly empty list of music folders. + */ + public List getMusicFoldersForUser(String username) { + List result = cachedMusicFoldersPerUser.get(username); + if (result == null) { + result = musicFolderDao.getMusicFoldersForUser(username); + result.retainAll(getAllMusicFolders(false, false)); + cachedMusicFoldersPerUser.put(username, result); + } + return result; + } + + /** + * Returns all music folders a user have access to. Non-existing and disabled folders are not included. + * + * @param selectedMusicFolderId If non-null and included in the list of allowed music folders, this methods returns + * a list of only this music folder. + * @return Possibly empty list of music folders. + */ + public List getMusicFoldersForUser(String username, Integer selectedMusicFolderId) { + List allowed = getMusicFoldersForUser(username); + if (selectedMusicFolderId == null) { + return allowed; + } + MusicFolder selected = getMusicFolderById(selectedMusicFolderId); + return allowed.contains(selected) ? Arrays.asList(selected) : Collections.emptyList(); + } + + /** + * Returns the selected music folder for a given user, or {@code null} if all music folders should be displayed. + */ + public MusicFolder getSelectedMusicFolder(String username) { + UserSettings settings = getUserSettings(username); + int musicFolderId = settings.getSelectedMusicFolderId(); + + MusicFolder musicFolder = getMusicFolderById(musicFolderId); + List allowedMusicFolders = getMusicFoldersForUser(username); + return allowedMusicFolders.contains(musicFolder) ? musicFolder : null; + } + + public void setMusicFoldersForUser(String username, List musicFolderIds) { + musicFolderDao.setMusicFoldersForUser(username, musicFolderIds); + cachedMusicFoldersPerUser.remove(username); + } + + /** + * Returns the music folder with the given ID. + * + * @param id The ID. + * @return The music folder with the given ID, or null if not found. + */ + public MusicFolder getMusicFolderById(Integer id) { + List 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); + clearMusicFolderCache(); + } + + /** + * 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); + clearMusicFolderCache(); + } + + /** + * Updates the given music folder. + * + * @param musicFolder The music folder to update. + */ + public void updateMusicFolder(MusicFolder musicFolder) { + musicFolderDao.updateMusicFolder(musicFolder); + clearMusicFolderCache(); + } + + public void clearMusicFolderCache() { + cachedMusicFolders = null; + cachedMusicFoldersPerUser.clear(); + } + + /** + * Returns all internet radio stations. Disabled stations are not returned. + * + * @return Possibly empty list of all internet radio stations. + */ + public List 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 null 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 getAllInternetRadios(boolean includeAll) { + List all = internetRadioDao.getAllInternetRadios(); + List result = new ArrayList(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 null. + */ + 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.setSongNotificationEnabled(true); + settings.setShowNowPlayingEnabled(true); + settings.setShowChatEnabled(true); + settings.setPartyModeEnabled(false); + settings.setNowPlayingAllowed(true); + settings.setAutoHidePlayQueue(true); + settings.setShowSideBar(true); + settings.setShowArtistInfoEnabled(true); + settings.setViewAsList(false); + settings.setQueueFollowingSongs(true); + settings.setDefaultAlbumList(AlbumListType.RANDOM); + settings.setLastFmEnabled(false); + settings.setLastFmUsername(null); + settings.setLastFmPassword(null); + settings.setChanged(new Date()); + + UserSettings.Visibility playlist = settings.getPlaylistVisibility(); + 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.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 getAllSystemAvatars() { + return avatarDao.getAllSystemAvatars(); + } + + /** + * Returns the system avatar with the given ID. + * + * @param id The system avatar ID. + * @return The avatar or null 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 null 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 null to remove the avatar. + * @param username The username. + */ + public void setCustomAvatar(Avatar avatar, String username) { + avatarDao.setCustomAvatar(avatar, username); + } + + public boolean isDlnaEnabled() { + return getBoolean(KEY_DLNA_ENABLED, DEFAULT_DLNA_ENABLED); + } + + public void setDlnaEnabled(boolean dlnaEnabled) { + setBoolean(KEY_DLNA_ENABLED, dlnaEnabled); + } + + public String getDlnaServerName() { + return getString(KEY_DLNA_SERVER_NAME, DEFAULT_DLNA_SERVER_NAME); + } + + public void setDlnaServerName(String dlnaServerName) { + setString(KEY_DLNA_SERVER_NAME, dlnaServerName); + } + + public boolean isSonosEnabled() { + return getBoolean(KEY_SONOS_ENABLED, DEFAULT_SONOS_ENABLED); + } + + public void setSonosEnabled(boolean sonosEnabled) { + setBoolean(KEY_SONOS_ENABLED, sonosEnabled); + } + + public String getSonosServiceName() { + return getString(KEY_SONOS_SERVICE_NAME, DEFAULT_SONOS_SERVICE_NAME); + } + + public void setSonosServiceName(String sonosServiceName) { + setString(KEY_SONOS_SERVICE_NAME, sonosServiceName); + } + + public int getSonosServiceId() { + return getInt(KEY_SONOS_SERVICE_ID, DEFAULT_SONOS_SERVICE_ID); + } + + public void setSonosServiceId(int sonosServiceid) { + setInt(KEY_SONOS_SERVICE_ID, sonosServiceid); + } + + public String getLocalIpAddress() { + return localIpAddress; + } + + /** + * Rewrites an URL to make it accessible from remote clients. + */ + public String rewriteRemoteUrl(String localUrl) { + return StringUtil.rewriteRemoteUrl(localUrl, isUrlRedirectionEnabled(), getUrlRedirectType(), getUrlRedirectFrom(), + getUrlRedirectCustomUrl(), getUrlRedirectContextPath(), getLocalIpAddress(), + getPort()); + } + + private void setProperty(String key, String value) { + if (value == null) { + properties.remove(key); + } else { + properties.setProperty(key, value); + } + } + + private String[] toStringArray(String s) { + List result = new ArrayList(); + 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; + } + + 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 responseHandler = new BasicResponseHandler(); + String content = client.execute(method, responseHandler); + licenseValidated = content != null && !content.contains("false"); + if (!licenseValidated) { + LOG.warn("License key is not valid."); + } + String[] lines = StringUtils.split(content); + if (lines.length > 1) { + licenseExpires = new Date(Long.parseLong(lines[1])); + } + + } catch (Throwable x) { + LOG.warn("Failed to validate license.", x); + } finally { + client.getConnectionManager().shutdown(); + } + } + + public synchronized void scheduleLicenseValidation() { + if (licenseValidationFuture != null) { + licenseValidationFuture.cancel(true); + } + Runnable task = new Runnable() { + public void run() { + validateLicense(); + } + }; + licenseValidated = true; + licenseExpires = null; + + licenseValidationFuture = executor.scheduleWithFixedDelay(task, 0L, LICENSE_VALIDATION_DELAY_HOURS, TimeUnit.HOURS); + } + + private void scheduleLocalIpAddressLookup() { + Runnable task = new Runnable() { + public void run() { + localIpAddress = Util.getLocalIpAddress(); + } + }; + executor.scheduleWithFixedDelay(task, 0L, LOCAL_IP_LOOKUP_DELAY_SECONDS, TimeUnit.SECONDS); + } + + 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..f35b03d2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/ShareService.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 . + + 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 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.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +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 getAllShares() { + return shareDao.getAllShares(); + } + + public List getSharesForUser(User user) { + List result = new ArrayList(); + 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 Share getShareByName(String name) { + return shareDao.getShareByName(name); + } + + public List getSharedFiles(int id, List musicFolders) { + List result = new ArrayList(); + for (String path : shareDao.getSharedFiles(id, musicFolders)) { + try { + MediaFile mediaFile = mediaFileService.getMediaFile(path); + if (mediaFile != null) { + result.add(mediaFile); + } + } catch (Exception x) { + // Ignored + } + } + return result; + } + + public Share createShare(HttpServletRequest request, List 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 settingsService.getUrlRedirectUrl() + "/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/SonosService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SonosService.java new file mode 100644 index 00000000..d82c8eea --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/SonosService.java @@ -0,0 +1,686 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2015 (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.SortedMap; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.datatype.XMLGregorianCalendar; +import javax.xml.ws.Holder; +import javax.xml.ws.WebServiceContext; +import javax.xml.ws.handler.MessageContext; + +import org.apache.commons.lang.RandomStringUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.cxf.headers.Header; +import org.apache.cxf.helpers.CastUtils; +import org.apache.cxf.jaxb.JAXBDataBinding; +import org.apache.cxf.jaxws.context.WrappedMessageContext; +import org.apache.cxf.message.Message; +import org.w3c.dom.Node; + +import com.sonos.services._1.AbstractMedia; +import com.sonos.services._1.AddToContainerResult; +import com.sonos.services._1.ContentKey; +import com.sonos.services._1.CreateContainerResult; +import com.sonos.services._1.Credentials; +import com.sonos.services._1.DeleteContainerResult; +import com.sonos.services._1.DeviceAuthTokenResult; +import com.sonos.services._1.DeviceLinkCodeResult; +import com.sonos.services._1.ExtendedMetadata; +import com.sonos.services._1.GetExtendedMetadata; +import com.sonos.services._1.GetExtendedMetadataResponse; +import com.sonos.services._1.GetExtendedMetadataText; +import com.sonos.services._1.GetExtendedMetadataTextResponse; +import com.sonos.services._1.GetMediaMetadata; +import com.sonos.services._1.GetMediaMetadataResponse; +import com.sonos.services._1.GetMetadata; +import com.sonos.services._1.GetMetadataResponse; +import com.sonos.services._1.GetSessionId; +import com.sonos.services._1.GetSessionIdResponse; +import com.sonos.services._1.HttpHeaders; +import com.sonos.services._1.LastUpdate; +import com.sonos.services._1.MediaCollection; +import com.sonos.services._1.MediaList; +import com.sonos.services._1.MediaMetadata; +import com.sonos.services._1.MediaUriAction; +import com.sonos.services._1.RateItem; +import com.sonos.services._1.RateItemResponse; +import com.sonos.services._1.RelatedBrowse; +import com.sonos.services._1.RemoveFromContainerResult; +import com.sonos.services._1.RenameContainerResult; +import com.sonos.services._1.ReorderContainerResult; +import com.sonos.services._1.ReportPlaySecondsResult; +import com.sonos.services._1.Search; +import com.sonos.services._1.SearchResponse; +import com.sonos.services._1.SegmentMetadataList; +import com.sonos.services._1_1.SonosSoap; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.AlbumListType; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.User; +import net.sourceforge.subsonic.service.sonos.SonosHelper; +import net.sourceforge.subsonic.service.sonos.SonosServiceRegistration; +import net.sourceforge.subsonic.service.sonos.SonosSoapFault; + +/** + * For manual testing of this service: + * curl -s -X POST -H "Content-Type: text/xml;charset=UTF-8" -H 'SOAPACTION: "http://www.sonos.com/Services/1.1#getSessionId"' -d @getSessionId.xml http://localhost:4040/ws/Sonos | xmllint --format - + * + * @author Sindre Mehus + * @version $Id$ + */ +public class SonosService implements SonosSoap { + + private static final Logger LOG = Logger.getLogger(SonosService.class); + + public static final String ID_ROOT = "root"; + public static final String ID_SHUFFLE = "shuffle"; + public static final String ID_ALBUMLISTS = "albumlists"; + public static final String ID_PLAYLISTS = "playlists"; + public static final String ID_PODCASTS = "podcasts"; + public static final String ID_LIBRARY = "library"; + public static final String ID_STARRED = "starred"; + public static final String ID_STARRED_ARTISTS = "starred-artists"; + public static final String ID_STARRED_ALBUMS = "starred-albums"; + public static final String ID_STARRED_SONGS = "starred-songs"; + public static final String ID_SEARCH = "search"; + public static final String ID_SHUFFLE_MUSICFOLDER_PREFIX = "shuffle-musicfolder:"; + public static final String ID_SHUFFLE_ARTIST_PREFIX = "shuffle-artist:"; + public static final String ID_SHUFFLE_ALBUMLIST_PREFIX = "shuffle-albumlist:"; + public static final String ID_RADIO_ARTIST_PREFIX = "radio-artist:"; + public static final String ID_MUSICFOLDER_PREFIX = "musicfolder:"; + public static final String ID_PLAYLIST_PREFIX = "playlist:"; + public static final String ID_ALBUMLIST_PREFIX = "albumlist:"; + public static final String ID_PODCAST_CHANNEL_PREFIX = "podcast-channel:"; + public static final String ID_DECADE_PREFIX = "decade:"; + public static final String ID_GENRE_PREFIX = "genre:"; + public static final String ID_SIMILAR_ARTISTS_PREFIX = "similarartists:"; + + // Note: These must match the values in presentationMap.xml + public static final String ID_SEARCH_ARTISTS = "search-artists"; + public static final String ID_SEARCH_ALBUMS = "search-albums"; + public static final String ID_SEARCH_SONGS = "search-songs"; + + private SonosHelper sonosHelper; + private MediaFileService mediaFileService; + private SecurityService securityService; + private SettingsService settingsService; + private PlaylistService playlistService; + private UPnPService upnpService; + + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + /** + * The context for the request. This is used to get the Auth information + * form the headers as well as using the request url to build the correct + * media resource url. + */ + @Resource + private WebServiceContext context; + + private String localIp; + + public void init() { + executor.scheduleWithFixedDelay(new Runnable() { + @Override + public void run() { + registerIfLocalIpChanged(); + } + }, 8, 60, TimeUnit.SECONDS); + } + + private void registerIfLocalIpChanged() { + if (settingsService.isSonosEnabled()) { + if (localIp == null || !localIp.equals(settingsService.getLocalIpAddress())) { + localIp = settingsService.getLocalIpAddress(); + setMusicServiceEnabled(true); + } + } + } + + public void setMusicServiceEnabled(boolean enabled) { + List sonosControllers = upnpService.getSonosControllerHosts(); + if (sonosControllers.isEmpty()) { + LOG.info("No Sonos controller found"); + return; + } + LOG.info("Found Sonos controllers: " + sonosControllers); + + String sonosServiceName = settingsService.getSonosServiceName(); + int sonosServiceId = settingsService.getSonosServiceId(); + String subsonicBaseUrl = sonosHelper.getBaseUrl(getRequest()); + + for (String sonosController : sonosControllers) { + try { + new SonosServiceRegistration().setEnabled(subsonicBaseUrl, sonosController, enabled, + sonosServiceName, sonosServiceId); + break; + } catch (IOException x) { + LOG.warn(String.format("Failed to enable/disable music service in Sonos controller %s: %s", sonosController, x)); + } + } + } + + + @Override + public LastUpdate getLastUpdate() { + LastUpdate result = new LastUpdate(); + // Effectively disabling caching + result.setCatalog(RandomStringUtils.randomAlphanumeric(8)); + result.setFavorites(RandomStringUtils.randomAlphanumeric(8)); + return result; + } + + @Override + public GetMetadataResponse getMetadata(GetMetadata parameters) { + String id = parameters.getId(); + int index = parameters.getIndex(); + int count = parameters.getCount(); + String username = getUsername(); + HttpServletRequest request = getRequest(); + + LOG.debug(String.format("getMetadata: id=%s index=%s count=%s recursive=%s", id, index, count, parameters.isRecursive())); + + List media = null; + MediaList mediaList = null; + + if (ID_ROOT.equals(id)) { + media = sonosHelper.forRoot(); + } else { + if (ID_SHUFFLE.equals(id)) { + media = sonosHelper.forShuffle(count, username, request); + } else if (ID_LIBRARY.equals(id)) { + media = sonosHelper.forLibrary(username, request); + } else if (ID_PLAYLISTS.equals(id)) { + media = sonosHelper.forPlaylists(username, request); + } else if (ID_ALBUMLISTS.equals(id)) { + media = sonosHelper.forAlbumLists(); + } else if (ID_PODCASTS.equals(id)) { + media = sonosHelper.forPodcastChannels(); + } else if (ID_STARRED.equals(id)) { + media = sonosHelper.forStarred(); + } else if (ID_STARRED_ARTISTS.equals(id)) { + media = sonosHelper.forStarredArtists(username, request); + } else if (ID_STARRED_ALBUMS.equals(id)) { + media = sonosHelper.forStarredAlbums(username, request); + } else if (ID_STARRED_SONGS.equals(id)) { + media = sonosHelper.forStarredSongs(username, request); + } else if (ID_SEARCH.equals(id)) { + media = sonosHelper.forSearchCategories(); + } else if (id.startsWith(ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); + media = sonosHelper.forPlaylist(playlistId, username, request); + } else if (id.startsWith(ID_DECADE_PREFIX)) { + int decade = Integer.parseInt(id.replace(ID_DECADE_PREFIX, "")); + media = sonosHelper.forDecade(decade, username, request); + } else if (id.startsWith(ID_GENRE_PREFIX)) { + int genre = Integer.parseInt(id.replace(ID_GENRE_PREFIX, "")); + media = sonosHelper.forGenre(genre, username, request); + } else if (id.startsWith(ID_ALBUMLIST_PREFIX)) { + AlbumListType albumListType = AlbumListType.fromId(id.replace(ID_ALBUMLIST_PREFIX, "")); + mediaList = sonosHelper.forAlbumList(albumListType, index, count, username, request); + } else if (id.startsWith(ID_PODCAST_CHANNEL_PREFIX)) { + int channelId = Integer.parseInt(id.replace(ID_PODCAST_CHANNEL_PREFIX, "")); + media = sonosHelper.forPodcastChannel(channelId, username, request); + } else if (id.startsWith(ID_MUSICFOLDER_PREFIX)) { + int musicFolderId = Integer.parseInt(id.replace(ID_MUSICFOLDER_PREFIX, "")); + media = sonosHelper.forMusicFolder(musicFolderId, username, request); + } else if (id.startsWith(ID_SHUFFLE_MUSICFOLDER_PREFIX)) { + int musicFolderId = Integer.parseInt(id.replace(ID_SHUFFLE_MUSICFOLDER_PREFIX, "")); + media = sonosHelper.forShuffleMusicFolder(musicFolderId, count, username, request); + } else if (id.startsWith(ID_SHUFFLE_ARTIST_PREFIX)) { + int mediaFileId = Integer.parseInt(id.replace(ID_SHUFFLE_ARTIST_PREFIX, "")); + media = sonosHelper.forShuffleArtist(mediaFileId, count, username, request); + } else if (id.startsWith(ID_SHUFFLE_ALBUMLIST_PREFIX)) { + AlbumListType albumListType = AlbumListType.fromId(id.replace(ID_SHUFFLE_ALBUMLIST_PREFIX, "")); + media = sonosHelper.forShuffleAlbumList(albumListType, count, username, request); + } else if (id.startsWith(ID_RADIO_ARTIST_PREFIX)) { + int mediaFileId = Integer.parseInt(id.replace(ID_RADIO_ARTIST_PREFIX, "")); + media = sonosHelper.forRadioArtist(mediaFileId, count, username, request); + } else if (id.startsWith(ID_SIMILAR_ARTISTS_PREFIX)) { + int mediaFileId = Integer.parseInt(id.replace(ID_SIMILAR_ARTISTS_PREFIX, "")); + media = sonosHelper.forSimilarArtists(mediaFileId, username, request); + } else { + media = sonosHelper.forDirectoryContent(Integer.parseInt(id), username, request); + } + } + + if (mediaList == null) { + mediaList = SonosHelper.createSubList(index, count, media); + } + + LOG.debug(String.format("getMetadata result: id=%s index=%s count=%s total=%s", + id, mediaList.getIndex(), mediaList.getCount(), mediaList.getTotal())); + + GetMetadataResponse response = new GetMetadataResponse(); + response.setGetMetadataResult(mediaList); + return response; + } + + @Override + public GetExtendedMetadataResponse getExtendedMetadata(GetExtendedMetadata parameters) { + LOG.debug("getExtendedMetadata: " + parameters.getId()); + + int id = Integer.parseInt(parameters.getId()); + MediaFile mediaFile = mediaFileService.getMediaFile(id); + AbstractMedia abstractMedia = sonosHelper.forMediaFile(mediaFile, getUsername(), getRequest()); + + ExtendedMetadata extendedMetadata = new ExtendedMetadata(); + if (abstractMedia instanceof MediaCollection) { + extendedMetadata.setMediaCollection((MediaCollection) abstractMedia); + } else { + extendedMetadata.setMediaMetadata((MediaMetadata) abstractMedia); + } + + RelatedBrowse relatedBrowse = new RelatedBrowse(); + relatedBrowse.setType("RELATED_ARTISTS"); + relatedBrowse.setId(ID_SIMILAR_ARTISTS_PREFIX + id); + extendedMetadata.getRelatedBrowse().add(relatedBrowse); + + GetExtendedMetadataResponse response = new GetExtendedMetadataResponse(); + response.setGetExtendedMetadataResult(extendedMetadata); + return response; + } + + + @Override + public SearchResponse search(Search parameters) { + String id = parameters.getId(); + + SearchService.IndexType indexType; + if (ID_SEARCH_ARTISTS.equals(id)) { + indexType = SearchService.IndexType.ARTIST; + } else if (ID_SEARCH_ALBUMS.equals(id)) { + indexType = SearchService.IndexType.ALBUM; + } else if (ID_SEARCH_SONGS.equals(id)) { + indexType = SearchService.IndexType.SONG; + } else { + throw new IllegalArgumentException("Invalid search category: " + id); + } + + MediaList mediaList = sonosHelper.forSearch(parameters.getTerm(), parameters.getIndex(), + parameters.getCount(), indexType, getUsername(), getRequest()); + SearchResponse response = new SearchResponse(); + response.setSearchResult(mediaList); + return response; + } + + @Override + public GetSessionIdResponse getSessionId(GetSessionId parameters) { + LOG.debug("getSessionId: " + parameters.getUsername()); + User user = securityService.getUserByName(parameters.getUsername()); + if (user == null || !StringUtils.equals(user.getPassword(), parameters.getPassword())) { + throw new SonosSoapFault.LoginInvalid(); + } + + if (!settingsService.getLicenseInfo().isLicenseOrTrialValid()) { + throw new SonosSoapFault.LoginUnauthorized(); + } + + // Use username as session ID for easy access to it later. + GetSessionIdResponse result = new GetSessionIdResponse(); + result.setGetSessionIdResult(user.getUsername()); + return result; + } + + @Override + public GetMediaMetadataResponse getMediaMetadata(GetMediaMetadata parameters) { + LOG.debug("getMediaMetadata: " + parameters.getId()); + + GetMediaMetadataResponse response = new GetMediaMetadataResponse(); + + // This method is called whenever a playlist is modified. Don't know why. + // Return an empty response to avoid ugly log message. + if (parameters.getId().startsWith(ID_PLAYLIST_PREFIX)) { + return response; + } + + int id = Integer.parseInt(parameters.getId()); + MediaFile song = mediaFileService.getMediaFile(id); + + response.setGetMediaMetadataResult(sonosHelper.forSong(song, getUsername(), getRequest())); + + return response; + } + + @Override + public void getMediaURI(String id, MediaUriAction action, Integer secondsSinceExplicit, Holder result, + Holder httpHeaders, Holder uriTimeout) { + result.value = sonosHelper.getMediaURI(Integer.parseInt(id), getUsername(), getRequest()); + LOG.debug("getMediaURI: " + id + " -> " + result.value); + } + + @Override + public CreateContainerResult createContainer(String containerType, String title, String parentId, String seedId) { + Date now = new Date(); + Playlist playlist = new Playlist(); + playlist.setName(title); + playlist.setUsername(getUsername()); + playlist.setCreated(now); + playlist.setChanged(now); + playlist.setShared(false); + + playlistService.createPlaylist(playlist); + CreateContainerResult result = new CreateContainerResult(); + result.setId(ID_PLAYLIST_PREFIX + playlist.getId()); + addItemToPlaylist(playlist.getId(), seedId, -1); + + return result; + } + + @Override + public DeleteContainerResult deleteContainer(String id) { + if (id.startsWith(ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); + Playlist playlist = playlistService.getPlaylist(playlistId); + if (playlist != null && playlist.getUsername().equals(getUsername())) { + playlistService.deletePlaylist(playlistId); + } + } + return new DeleteContainerResult(); + } + + @Override + public RenameContainerResult renameContainer(String id, String title) { + if (id.startsWith(ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); + Playlist playlist = playlistService.getPlaylist(playlistId); + if (playlist != null && playlist.getUsername().equals(getUsername())) { + playlist.setName(title); + playlistService.updatePlaylist(playlist); + } + } + return new RenameContainerResult(); + } + + @Override + public AddToContainerResult addToContainer(String id, String parentId, int index, String updateId) { + if (parentId.startsWith(ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(parentId.replace(ID_PLAYLIST_PREFIX, "")); + Playlist playlist = playlistService.getPlaylist(playlistId); + if (playlist != null && playlist.getUsername().equals(getUsername())) { + addItemToPlaylist(playlistId, id, index); + } + } + return new AddToContainerResult(); + } + + private void addItemToPlaylist(int playlistId, String id, int index) { + if (StringUtils.isBlank(id)) { + return; + } + + GetMetadata parameters = new GetMetadata(); + parameters.setId(id); + parameters.setIndex(0); + parameters.setCount(Integer.MAX_VALUE); + GetMetadataResponse metadata = getMetadata(parameters); + List newSongs = new ArrayList(); + + for (AbstractMedia media : metadata.getGetMetadataResult().getMediaCollectionOrMediaMetadata()) { + if (StringUtils.isNumeric(media.getId())) { + MediaFile mediaFile = mediaFileService.getMediaFile(Integer.parseInt(media.getId())); + if (mediaFile != null && mediaFile.isFile()) { + newSongs.add(mediaFile); + } + } + } + List existingSongs = playlistService.getFilesInPlaylist(playlistId); + if (index == -1) { + index = existingSongs.size(); + } + + existingSongs.addAll(index, newSongs); + playlistService.setFilesInPlaylist(playlistId, existingSongs); + } + + @Override + public ReorderContainerResult reorderContainer(String id, String from, int to, String updateId) { + if (id.startsWith(ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); + Playlist playlist = playlistService.getPlaylist(playlistId); + if (playlist != null && playlist.getUsername().equals(getUsername())) { + + SortedMap indexToSong = new ConcurrentSkipListMap(); + List songs = playlistService.getFilesInPlaylist(playlistId); + for (int i = 0; i < songs.size(); i++) { + indexToSong.put(i, songs.get(i)); + } + + List movedSongs = new ArrayList(); + for (Integer i : parsePlaylistIndices(from)) { + movedSongs.add(indexToSong.remove(i)); + } + + List updatedSongs = new ArrayList(); + updatedSongs.addAll(indexToSong.headMap(to).values()); + updatedSongs.addAll(movedSongs); + updatedSongs.addAll(indexToSong.tailMap(to).values()); + + playlistService.setFilesInPlaylist(playlistId, updatedSongs); + } + } + return new ReorderContainerResult(); + } + + @Override + public RemoveFromContainerResult removeFromContainer(String id, String indices, String updateId) { + if (id.startsWith(ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(id.replace(ID_PLAYLIST_PREFIX, "")); + Playlist playlist = playlistService.getPlaylist(playlistId); + if (playlist != null && playlist.getUsername().equals(getUsername())) { + SortedSet indicesToRemove = parsePlaylistIndices(indices); + List songs = playlistService.getFilesInPlaylist(playlistId); + List updatedSongs = new ArrayList(); + for (int i = 0; i < songs.size(); i++) { + if (!indicesToRemove.contains(i)) { + updatedSongs.add(songs.get(i)); + } + } + playlistService.setFilesInPlaylist(playlistId, updatedSongs); + } + } + return new RemoveFromContainerResult(); + } + + protected SortedSet parsePlaylistIndices(String indices) { + // Comma-separated, may include ranges: 1,2,4-7 + SortedSet result = new TreeSet(); + + for (String part : StringUtils.split(indices, ',')) { + if (StringUtils.isNumeric(part)) { + result.add(Integer.parseInt(part)); + } else { + int dashIndex = part.indexOf("-"); + int from = Integer.parseInt(part.substring(0, dashIndex)); + int to = Integer.parseInt(part.substring(dashIndex + 1)); + for (int i = from; i <= to; i++) { + result.add(i); + } + } + } + return result; + } + + @Override + public String createItem(String favorite) { + int id = Integer.parseInt(favorite); + sonosHelper.star(id, getUsername()); + return favorite; + } + + @Override + public void deleteItem(String favorite) { + int id = Integer.parseInt(favorite); + sonosHelper.unstar(id, getUsername()); + } + + private HttpServletRequest getRequest() { + MessageContext messageContext = context == null ? null : context.getMessageContext(); + + // See org.apache.cxf.transport.http.AbstractHTTPDestination#HTTP_REQUEST + return messageContext == null ? null : (HttpServletRequest) messageContext.get("HTTP.REQUEST"); + } + + private String getUsername() { + MessageContext messageContext = context.getMessageContext(); + if (messageContext == null || !(messageContext instanceof WrappedMessageContext)) { + LOG.error("Message context is null or not an instance of WrappedMessageContext."); + return null; + } + + Message message = ((WrappedMessageContext) messageContext).getWrappedMessage(); + List
headers = CastUtils.cast((List) message.get(Header.HEADER_LIST)); + if (headers != null) { + for (Header h : headers) { + Object o = h.getObject(); + // Unwrap the node using JAXB + if (o instanceof Node) { + JAXBContext jaxbContext; + try { + // TODO: Check performance + jaxbContext = new JAXBDataBinding(Credentials.class).getContext(); + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + o = unmarshaller.unmarshal((Node) o); + } catch (JAXBException e) { + // failed to get the credentials object from the headers + LOG.error("JAXB error trying to unwrap credentials", e); + } + } + if (o instanceof Credentials) { + Credentials c = (Credentials) o; + + // Note: We're using the username as session ID. + String username = c.getSessionId(); + if (username == null) { + LOG.debug("No session id in credentials object, get from login"); + username = c.getLogin().getUsername(); + } + return username; + } else { + LOG.error("No credentials object"); + } + } + } else { + LOG.error("No headers found"); + } + return null; + } + + public void setSonosHelper(SonosHelper sonosHelper) { + this.sonosHelper = sonosHelper; + } + + @Override + public RateItemResponse rateItem(RateItem parameters) { + return null; + } + + @Override + public SegmentMetadataList getStreamingMetadata(String id, XMLGregorianCalendar startTime, int duration) { + return null; + } + + @Override + public GetExtendedMetadataTextResponse getExtendedMetadataText(GetExtendedMetadataText parameters) { + return null; + } + + @Override + public DeviceLinkCodeResult getDeviceLinkCode(String householdId) { + return null; + } + + @Override + public void reportAccountAction(String type) { + + } + + @Override + public void setPlayedSeconds(String id, int seconds) { + + } + + @Override + public ReportPlaySecondsResult reportPlaySeconds(String id, int seconds) { + return null; + } + + @Override + public DeviceAuthTokenResult getDeviceAuthToken(String householdId, String linkCode, String linkDeviceId) { + return null; + } + + @Override + public void reportStatus(String id, int errorCode, String message) { + } + + @Override + public String getScrollIndices(String id) { + return null; + } + + @Override + public void reportPlayStatus(String id, String status) { + + } + + @Override + public ContentKey getContentKey(String id, String uri) { + return null; + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSecurityService(SecurityService securityService) { + this.securityService = securityService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setUpnpService(UPnPService upnpService) { + this.upnpService = upnpService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +} 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..3d5e7544 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/StatusService.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayStatus; +import net.sourceforge.subsonic.domain.TransferStatus; + +import java.io.File; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +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. + *

+ * Note that for stream statuses, the last inactive status is also stored. + * + * @author Sindre Mehus + * @see TransferStatus + */ +public class StatusService { + + private MediaFileService mediaFileService; + + private final List streamStatuses = new ArrayList(); + private final List downloadStatuses = new ArrayList(); + private final List uploadStatuses = new ArrayList(); + private final List remotePlays = new ArrayList(); + + // Maps from player ID to latest inactive stream status. + private final Map inactiveStreamStatuses = new LinkedHashMap(); + + 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 getAllStreamStatuses() { + + List result = new ArrayList(streamStatuses); + + // Add inactive status for those players that have no active status. + Set activePlayers = new HashSet(); + for (TransferStatus status : streamStatuses) { + activePlayers.add(status.getPlayer().getId()); + } + + for (Map.Entry entry : inactiveStreamStatuses.entrySet()) { + if (!activePlayers.contains(entry.getKey())) { + result.add(entry.getValue()); + } + } + return result; + } + + public synchronized List getStreamStatusesForPlayer(Player player) { + List result = new ArrayList(); + 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 getAllDownloadStatuses() { + return new ArrayList(downloadStatuses); + } + + public synchronized TransferStatus createUploadStatus(Player player) { + return createStatus(player, uploadStatuses); + } + + public synchronized void removeUploadStatus(TransferStatus status) { + uploadStatuses.remove(status); + } + + public synchronized List getAllUploadStatuses() { + return new ArrayList(uploadStatuses); + } + + public synchronized void addRemotePlay(PlayStatus playStatus) { + Iterator iterator = remotePlays.iterator(); + while (iterator.hasNext()) { + PlayStatus rp = iterator.next(); + if (rp.isExpired()) { + iterator.remove(); + } + } + remotePlays.add(playStatus); + } + + public synchronized List getPlayStatuses() { + Map result = new LinkedHashMap(); + for (PlayStatus remotePlay : remotePlays) { + if (!remotePlay.isExpired()) { + result.put(remotePlay.getPlayer().getId(), remotePlay); + } + } + + List statuses = new ArrayList(); + statuses.addAll(inactiveStreamStatuses.values()); + statuses.addAll(streamStatuses); + + for (TransferStatus streamStatus : statuses) { + Player player = streamStatus.getPlayer(); + File file = streamStatus.getFile(); + if (file == null) { + continue; + } + MediaFile mediaFile = mediaFileService.getMediaFile(file); + if (player == null || mediaFile == null) { + continue; + } + Date time = new Date(System.currentTimeMillis() - streamStatus.getMillisSinceLastUpdate()); + result.put(player.getId(), new PlayStatus(mediaFile, player, time)); + } + return new ArrayList(result.values()); + } + + private synchronized TransferStatus createStatus(Player player, List statusList) { + TransferStatus status = new TransferStatus(); + status.setPlayer(player); + statusList.add(status); + return status; + } + + 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/TranscodingService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java new file mode 100644 index 00000000..f9e08b3b --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/TranscodingService.java @@ -0,0 +1,545 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.io.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; + +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 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; + +/** + * 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); + public static final String FORMAT_RAW = "raw"; + + private TranscodingDao transcodingDao; + private SettingsService settingsService; + private PlayerService playerService; + + /** + * Returns all transcodings. + * + * @return Possibly empty list of all transcodings. + */ + public List 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 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 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 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, false) != 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, false); + 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. + *

+ * A transcoding is applied if it is applicable for the format of the given file, and is activated for the + * given player. + *

+ * 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. + *

+ * 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(); + } + + boolean hls = videoTranscodingSettings != null && videoTranscodingSettings.isHls(); + Transcoding transcoding = getTranscoding(mediaFile, player, preferredTargetFormat, hls); + if (transcoding != null) { + parameters.setTranscoding(transcoding); + if (maxBitRate == null) { + maxBitRate = mediaFile.isVideo() ? VideoPlayerController.DEFAULT_BIT_RATE : TranscodeScheme.MAX_192.getMaxBitRate(); + } + } 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. + *

+ * A transcoding is applied if it is applicable for the format of the given file, and is activated for the + * given player. + *

+ * 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. + *

+ * 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: + *

    + *
  • Splitting the command line string to an array.
  • + *
  • Replacing occurrences of "%s" with the path of the given music file.
  • + *
  • Replacing occurrences of "%t" with the title of the given music file.
  • + *
  • Replacing occurrences of "%l" with the album name of the given music file.
  • + *
  • Replacing occurrences of "%a" with the artist name of the given music file.
  • + *
  • Replacing occurrcences of "%b" with the max bitrate.
  • + *
  • Replacing occurrcences of "%o" with the video time offset (used for scrubbing).
  • + *
  • Replacing occurrcences of "%d" with the video duration (used for HLS).
  • + *
  • Replacing occurrcences of "%w" with the video image width.
  • + *
  • Replacing occurrcences of "%h" with the video image height.
  • + *
  • Prepending the path of the transcoder directory if the transcoder is found there.
  • + *
+ * + * @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 result = new LinkedList(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("%d") && videoTranscodingSettings != null) { + cmd = cmd.replace("%d", String.valueOf(videoTranscodingSettings.getDuration())); + } + 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 null if no + * transcoding should be done. + */ + private Transcoding getTranscoding(MediaFile mediaFile, Player player, String preferredTargetFormat, boolean hls) { + + if (hls) { + return new Transcoding(null, "hls", mediaFile.getFormat(), "ts", settingsService.getHlsCommand(), null, null, true); + } + + if (FORMAT_RAW.equals(preferredTargetFormat)) { + return null; + } + + List applicableTranscodings = new LinkedList(); + 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 ffmpeg 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/UPnPService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/UPnPService.java new file mode 100644 index 00000000..776d1c24 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/UPnPService.java @@ -0,0 +1,200 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import java.net.URL; +import java.util.ArrayList; +import java.util.List; + +import org.fourthline.cling.UpnpService; +import org.fourthline.cling.UpnpServiceImpl; +import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder; +import org.fourthline.cling.model.DefaultServiceManager; +import org.fourthline.cling.model.meta.Device; +import org.fourthline.cling.model.meta.DeviceDetails; +import org.fourthline.cling.model.meta.DeviceIdentity; +import org.fourthline.cling.model.meta.Icon; +import org.fourthline.cling.model.meta.LocalDevice; +import org.fourthline.cling.model.meta.LocalService; +import org.fourthline.cling.model.meta.ManufacturerDetails; +import org.fourthline.cling.model.meta.ModelDetails; +import org.fourthline.cling.model.meta.RemoteDevice; +import org.fourthline.cling.model.types.DLNADoc; +import org.fourthline.cling.model.types.DeviceType; +import org.fourthline.cling.model.types.UDADeviceType; +import org.fourthline.cling.model.types.UDN; +import org.fourthline.cling.support.connectionmanager.ConnectionManagerService; +import org.fourthline.cling.support.model.ProtocolInfos; +import org.fourthline.cling.support.model.dlna.DLNAProfiles; +import org.fourthline.cling.support.model.dlna.DLNAProtocolInfo; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.Version; +import net.sourceforge.subsonic.service.upnp.ApacheUpnpServiceConfiguration; +import net.sourceforge.subsonic.service.upnp.FolderBasedContentDirectory; +import net.sourceforge.subsonic.service.upnp.MSMediaReceiverRegistrarService; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class UPnPService { + + private static final Logger LOG = Logger.getLogger(UPnPService.class); + + private SettingsService settingsService; + private VersionService versionService; + private UpnpService upnpService; + private FolderBasedContentDirectory folderBasedContentDirectory; + + public void init() { + startService(); + } + + public void startService() { + Runnable runnable = new Runnable() { + public void run() { + try { + LOG.info("Starting UPnP service..."); + createService(); + LOG.info("Starting UPnP service - Done!"); + } catch (Throwable x) { + LOG.error("Failed to start UPnP service: " + x, x); + } + } + }; + new Thread(runnable).start(); + } + + private synchronized void createService() throws Exception { + upnpService = new UpnpServiceImpl(new ApacheUpnpServiceConfiguration()); + + // Asynch search for other devices (most importantly UPnP-enabled routers for port-mapping) + upnpService.getControlPoint().search(); + + // Start DLNA media server? + setMediaServerEnabled(settingsService.isDlnaEnabled()); + + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + System.err.println("Shutting down UPnP service..."); + upnpService.shutdown(); + System.err.println("Shutting down UPnP service - Done!"); + } + }); + } + + public void setMediaServerEnabled(boolean enabled) { + if (enabled) { + try { + upnpService.getRegistry().addDevice(createMediaServerDevice()); + LOG.info("Enabling UPnP/DLNA media server"); + } catch (Exception x) { + LOG.error("Failed to start UPnP/DLNA media server: " + x, x); + } + } else { + upnpService.getRegistry().removeAllLocalDevices(); + LOG.info("Disabling UPnP/DLNA media server"); + } + } + + private LocalDevice createMediaServerDevice() throws Exception { + + String serverName = settingsService.getDlnaServerName(); + DeviceIdentity identity = new DeviceIdentity(UDN.uniqueSystemIdentifier(serverName)); + DeviceType type = new UDADeviceType("MediaServer", 1); + + // TODO: DLNACaps + Version version = versionService.getLocalVersion(); + String versionString = version == null ? null : version.toString(); + String licenseEmail = settingsService.getLicenseEmail(); + String licenseString = licenseEmail == null ? "Unlicensed" : ("Licensed to " + licenseEmail); + + DeviceDetails details = new DeviceDetails(serverName, new ManufacturerDetails(serverName), + new ModelDetails(serverName, licenseString, versionString), + new DLNADoc[]{new DLNADoc("DMS", DLNADoc.Version.V1_5)}, null); + + Icon icon = new Icon("image/png", 512, 512, 32, getClass().getResource("subsonic-512.png")); + + LocalService contentDirectoryservice = new AnnotationLocalServiceBinder().read(FolderBasedContentDirectory.class); + contentDirectoryservice.setManager(new DefaultServiceManager(contentDirectoryservice) { + + @Override + protected FolderBasedContentDirectory createServiceInstance() throws Exception { + return folderBasedContentDirectory; + } + }); + + final ProtocolInfos protocols = new ProtocolInfos(); + for (DLNAProfiles dlnaProfile : DLNAProfiles.values()) { + if (dlnaProfile == DLNAProfiles.NONE) { + continue; + } + try { + protocols.add(new DLNAProtocolInfo(dlnaProfile)); + } catch (Exception e) { + // Silently ignored. + } + } + + LocalService connetionManagerService = new AnnotationLocalServiceBinder().read(ConnectionManagerService.class); + connetionManagerService.setManager(new DefaultServiceManager(connetionManagerService) { + @Override + protected ConnectionManagerService createServiceInstance() throws Exception { + return new ConnectionManagerService(protocols, null); + } + }); + + // For compatibility with Microsoft + LocalService receiverService = new AnnotationLocalServiceBinder().read(MSMediaReceiverRegistrarService.class); + receiverService.setManager(new DefaultServiceManager(receiverService, MSMediaReceiverRegistrarService.class)); + + return new LocalDevice(identity, type, details, new Icon[]{icon}, new LocalService[]{contentDirectoryservice, connetionManagerService, receiverService}); + } + + public List getSonosControllerHosts() { + List result = new ArrayList(); + for (Device device : upnpService.getRegistry().getDevices(new DeviceType("schemas-upnp-org", "ZonePlayer"))) { + if (device instanceof RemoteDevice) { + URL descriptorURL = ((RemoteDevice) device).getIdentity().getDescriptorURL(); + if (descriptorURL != null) { + result.add(descriptorURL.getHost()); + } + } + } + return result; + } + + public UpnpService getUpnpService() { + return upnpService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setVersionService(VersionService versionService) { + this.versionService = versionService; + } + + public void setFolderBasedContentDirectory(FolderBasedContentDirectory folderBasedContentDirectory) { + this.folderBasedContentDirectory = folderBasedContentDirectory; + } +} 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..3510ed11 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/VersionService.java @@ -0,0 +1,271 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service; + +import 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"; + + public void init() { + ServiceLocator.setVersionService(this); + } + + /** + * 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 null + * 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 null + * 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 null + * 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 null + * 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 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..5a92916a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/AudioPlayer.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.jukebox; + +import java.io.IOException; +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. + *

+ * Supports pause and resume, but not restarting. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class AudioPlayer { + + public static final float DEFAULT_GAIN = 0.75f; + 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 = new AtomicReference(PAUSED); + private FloatControl gainControl; + + public AudioPlayer(InputStream in, Listener listener) throws Exception { + this.in = in; + this.listener = listener; + + AudioFormat format = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 44100.0F, 16, 2, 4, 44100.0F, true); + 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(DEFAULT_GAIN); + } + 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 = Math.min(0.0, gainControl.getMaximum()); // Don't use positive gain to avoid distortion. + 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[line.getBufferSize()]; + + while (true) { + + switch (state.get()) { + case CLOSED: + case EOM: + return; + case PAUSED: + Thread.sleep(250); + break; + case PLAYING: + // Fill buffer in order to ensure that write() receives an integral number of frames. + int n = fill(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(); + } + } + + private int fill(byte[] buffer) throws IOException { + int bytesRead = 0; + while (bytesRead < buffer.length) { + int n = in.read(buffer, bytesRead, buffer.length - bytesRead); + if (n == -1) { + return bytesRead == 0 ? -1 : bytesRead; + } + bytesRead += n; + } + return bytesRead; + } + } + + 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..12cb79c7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/jukebox/PlayerTest.java @@ -0,0 +1,81 @@ +package net.sourceforge.subsonic.service.jukebox; + +import javax.swing.*; +import javax.swing.event.ChangeEvent; +import javax.swing.event.ChangeListener; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.FileInputStream; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class PlayerTest implements AudioPlayer.Listener { + + private AudioPlayer player; + + public PlayerTest() throws Exception { + 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) { + createPlayer(); + player.play(); + } + }); + stopButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + player.pause(); + } + }); + resetButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + player.close(); + createPlayer(); + } + }); + 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); + } + + private void createPlayer() { + try { + player = new AudioPlayer(new FileInputStream("/Users/sindre/Downloads/sample.au"), this); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + 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..69bcc743 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/DefaultMetaDataParser.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.metadata; + +import net.sourceforge.subsonic.domain.MediaFile; + +import java.io.File; + +/** + * 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. + * + * @param file The file to parse. + * @return Meta data for the file. + */ + public MetaData getRawMetaData(File file) { + MetaData metaData = new MetaData(); + String artist = guessArtist(file); + metaData.setArtist(artist); + metaData.setAlbumArtist(artist); + metaData.setAlbumName(guessAlbum(file, artist)); + 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 . + + 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/). + *

+ * 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..ee2f4263 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/JaudiotaggerParser.java @@ -0,0 +1,327 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.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+"); + private static final Pattern YEAR_NUMBER_PATTERN = Pattern.compile("(\\d{4}).*"); + + 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(parseYear(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(songArtist) ? albumArtist : songArtist); + metaData.setAlbumArtist(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 getID3V1Genres() { + return new TreeSet(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 parseYear(String year) { + if (year == null) { + return null; + } + + Integer result = null; + + try { + result = new Integer(year); + } catch (NumberFormatException x) { + Matcher matcher = YEAR_NUMBER_PATTERN.matcher(year); + 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, StringUtils.trimToEmpty(metaData.getAlbumName())); + tag.setField(FieldKey.TITLE, StringUtils.trimToEmpty(metaData.getTitle())); + tag.setField(FieldKey.GENRE, StringUtils.trimToEmpty(metaData.getGenre())); + try { + tag.setField(FieldKey.ALBUM_ARTIST, StringUtils.trimToEmpty(metaData.getAlbumArtist())); + } catch (Exception x) { + // Silently ignored. ID3v1 doesn't support album artist. + } + + 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 null 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..b42f2dc7 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaData.java @@ -0,0 +1,144 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.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 albumArtist; + 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 getAlbumArtist() { + return albumArtist; + } + + public void setAlbumArtist(String albumArtist) { + this.albumArtist = albumArtist; + } + + 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..1d6bd6ac --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/metadata/MetaDataParser.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.metadata; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.service.ServiceLocator; +import net.sourceforge.subsonic.service.SettingsService; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; + +import java.io.File; +import java.util.List; + + +/** + * 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 albumArtist = metaData.getAlbumArtist(); + String album = metaData.getAlbumName(); + String title = metaData.getTitle(); + + if (artist == null) { + artist = guessArtist(file); + } + if (albumArtist == null) { + albumArtist = guessArtist(file); + } + if (album == null) { + album = guessAlbum(file, artist); + } + if (title == null) { + title = guessTitle(file); + } + + title = removeTrackNumberFromTitle(title, metaData.getTrackNumber()); + metaData.setArtist(artist); + metaData.setAlbumArtist(albumArtist); + 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 StringUtils.trim(FilenameUtils.getBaseName(file.getPath())); + } + + private boolean isRoot(File file) { + SettingsService settings = ServiceLocator.getSettingsService(); + List 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 missing, or if 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 . + + 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 parsers; + + public void setParsers(List parsers) { + this.parsers = parsers; + } + + /** + * Returns a meta-data parser for the given file. + * + * @param file The file in question. + * @return An applicable parser, or null 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/service/sonos/AlbumList.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/AlbumList.java new file mode 100644 index 00000000..40053b0a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/AlbumList.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service.sonos; + +import java.util.List; + +import net.sourceforge.subsonic.domain.MediaFile; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +class AlbumList { + + private final List albums; + private final int total; + + public AlbumList(List albums, int total) { + this.albums = albums; + this.total = total; + } + + public List getAlbums() { + return albums; + } + + public int getTotal() { + return total; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosFaultInterceptor.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosFaultInterceptor.java new file mode 100644 index 00000000..4ac15df6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosFaultInterceptor.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service.sonos; + +import javax.xml.namespace.QName; + +import org.apache.cxf.binding.soap.SoapMessage; +import org.apache.cxf.binding.soap.interceptor.AbstractSoapInterceptor; +import org.apache.cxf.helpers.DOMUtils; +import org.apache.cxf.interceptor.Fault; +import org.apache.cxf.phase.Phase; +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import net.sourceforge.subsonic.Logger; + +/** + * Intercepts all SonosSoapFault exceptions and builds a SOAP Fault. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class SonosFaultInterceptor extends AbstractSoapInterceptor { + + private static final Logger LOG = Logger.getLogger(SonosFaultInterceptor.class); + + /** + * Constructor, setting the phase to Marshal. This happens before the default Fault Interceptor + */ + public SonosFaultInterceptor() { + super(Phase.MARSHAL); + } + + /* + * Only handles instances of SonosSoapFault, all other exceptions fall through to the default Fault Interceptor + */ + @Override + public void handleMessage(SoapMessage message) throws Fault { + Fault fault = (Fault) message.getContent(Exception.class); + LOG.warn("Error: " + fault, fault); + + if (fault.getCause() instanceof SonosSoapFault) { + SonosSoapFault cause = (SonosSoapFault) fault.getCause(); + fault.setFaultCode(new QName(cause.getFaultCode())); + fault.setMessage(cause.getFaultCode()); + + Document document = DOMUtils.createDocument(); + Element details = document.createElement("detail"); + fault.setDetail(details); + + details.appendChild(document.createElement("ExceptionInfo")); + + Element sonosError = document.createElement("SonosError"); + sonosError.setTextContent(String.valueOf(cause.getSonosError())); + details.appendChild(sonosError); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosHelper.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosHelper.java new file mode 100644 index 00000000..20197e12 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosHelper.java @@ -0,0 +1,748 @@ +/* + * This file is part of Subsonic. + * + * Subsonic is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Subsonic is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Subsonic. If not, see . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service.sonos; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.List; + +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.lang.StringUtils; +import org.springframework.web.bind.ServletRequestUtils; + +import com.google.common.base.Predicate; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import com.sonos.services._1.AbstractMedia; +import com.sonos.services._1.AlbumArtUrl; +import com.sonos.services._1.ItemType; +import com.sonos.services._1.MediaCollection; +import com.sonos.services._1.MediaList; +import com.sonos.services._1.MediaMetadata; +import com.sonos.services._1.TrackMetadata; + +import net.sourceforge.subsonic.controller.CoverArtController; +import net.sourceforge.subsonic.dao.MediaFileDao; +import net.sourceforge.subsonic.domain.AlbumListType; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.Genre; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.MusicFolderContent; +import net.sourceforge.subsonic.domain.MusicIndex; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.domain.PlayerTechnology; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.domain.PodcastChannel; +import net.sourceforge.subsonic.domain.PodcastEpisode; +import net.sourceforge.subsonic.domain.PodcastStatus; +import net.sourceforge.subsonic.domain.SearchCriteria; +import net.sourceforge.subsonic.domain.SearchResult; +import net.sourceforge.subsonic.service.LastFmService; +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.PodcastService; +import net.sourceforge.subsonic.service.RatingService; +import net.sourceforge.subsonic.service.SearchService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.SonosService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; +import net.sourceforge.subsonic.util.Util; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class SonosHelper { + + public static final String SUBSONIC_CLIENT_ID = "sonos"; + + private MediaFileService mediaFileService; + private PlaylistService playlistService; + private PlayerService playerService; + private TranscodingService transcodingService; + private SettingsService settingsService; + private MusicIndexService musicIndexService; + private SearchService searchService; + private MediaFileDao mediaFileDao; + private RatingService ratingService; + private LastFmService lastFmService; + private PodcastService podcastService; + + public List forRoot() { + MediaMetadata shuffle = new MediaMetadata(); + shuffle.setItemType(ItemType.PROGRAM); + shuffle.setId(SonosService.ID_SHUFFLE); + shuffle.setTitle("Shuffle Play"); + + MediaCollection library = new MediaCollection(); + library.setItemType(ItemType.COLLECTION); + library.setId(SonosService.ID_LIBRARY); + library.setTitle("Browse Library"); + + MediaCollection playlists = new MediaCollection(); + playlists.setItemType(ItemType.FAVORITES); + playlists.setId(SonosService.ID_PLAYLISTS); + playlists.setTitle("Playlists"); + playlists.setUserContent(true); + playlists.setReadOnly(false); + + MediaCollection starred = new MediaCollection(); + starred.setItemType(ItemType.FAVORITES); + starred.setId(SonosService.ID_STARRED); + starred.setTitle("Starred"); + + MediaCollection albumlists = new MediaCollection(); + albumlists.setItemType(ItemType.COLLECTION); + albumlists.setId(SonosService.ID_ALBUMLISTS); + albumlists.setTitle("Album Lists"); + + MediaCollection podcasts = new MediaCollection(); + podcasts.setItemType(ItemType.COLLECTION); + podcasts.setId(SonosService.ID_PODCASTS); + podcasts.setTitle("Podcasts"); + + return Arrays.asList(shuffle, library, playlists, starred, albumlists, podcasts); + } + + public List forShuffle(int count, String username, HttpServletRequest request) { + return forShuffleMusicFolder(settingsService.getMusicFoldersForUser(username), count, username, request); + } + + public List forShuffleMusicFolder(int id, int count, String username, HttpServletRequest request) { + return forShuffleMusicFolder(settingsService.getMusicFoldersForUser(username, id), count, username, request); + } + + private List forShuffleMusicFolder(List musicFolders, int count, String username, HttpServletRequest request) { + List albums = searchService.getRandomAlbums(40, musicFolders); + List songs = new ArrayList(); + for (MediaFile album : albums) { + for (MediaFile file : filterMusic(mediaFileService.getChildrenOf(album, true, false, false))) { + songs.add(file); + } + } + Collections.shuffle(songs); + songs = songs.subList(0, Math.min(count, songs.size())); + return forMediaFiles(songs, username, request); + } + + public List forShuffleArtist(int mediaFileId, int count, String username, HttpServletRequest request) { + MediaFile artist = mediaFileService.getMediaFile(mediaFileId); + List songs = filterMusic(mediaFileService.getDescendantsOf(artist, false)); + Collections.shuffle(songs); + songs = songs.subList(0, Math.min(count, songs.size())); + return forMediaFiles(songs, username, request); + } + + public List forShuffleAlbumList(AlbumListType albumListType, int count, String username, HttpServletRequest request) { + AlbumList albumList = createAlbumList(albumListType, 0, 40, username); + + List songs = new ArrayList(); + for (MediaFile album : albumList.getAlbums()) { + songs.addAll(filterMusic(mediaFileService.getChildrenOf(album, true, false, false))); + } + Collections.shuffle(songs); + songs = songs.subList(0, Math.min(count, songs.size())); + return forMediaFiles(songs, username, request); + } + + public List forRadioArtist(int mediaFileId, int count, String username, HttpServletRequest request) { + MediaFile artist = mediaFileService.getMediaFile(mediaFileId); + List musicFolders = settingsService.getMusicFoldersForUser(username); + List songs = filterMusic(lastFmService.getSimilarSongs(artist, count, musicFolders)); + Collections.shuffle(songs); + songs = songs.subList(0, Math.min(count, songs.size())); + return forMediaFiles(songs, username, request); + } + + public List forLibrary(String username, HttpServletRequest request) { + List result = new ArrayList(); + + List musicFolders = settingsService.getMusicFoldersForUser(username); + if (musicFolders.size() == 1) { + return forMusicFolder(musicFolders.get(0), username, request); + } + + for (MusicFolder musicFolder : musicFolders) { + MediaCollection mediaCollection = new MediaCollection(); + mediaCollection.setItemType(ItemType.COLLECTION); + mediaCollection.setId(SonosService.ID_MUSICFOLDER_PREFIX + musicFolder.getId()); + mediaCollection.setTitle(musicFolder.getName()); + result.add(mediaCollection); + } + return result; + } + + public List forMusicFolder(int musicFolderId, String username, HttpServletRequest request) { + return forMusicFolder(settingsService.getMusicFolderById(musicFolderId), username, request); + } + + public List forMusicFolder(MusicFolder musicFolder, String username, HttpServletRequest request) { + try { + List result = new ArrayList(); + + MediaMetadata shuffle = new MediaMetadata(); + shuffle.setItemType(ItemType.PROGRAM); + shuffle.setId(SonosService.ID_SHUFFLE_MUSICFOLDER_PREFIX + musicFolder.getId()); + shuffle.setTitle("Shuffle Play"); + result.add(shuffle); + + for (MediaFile shortcut : musicIndexService.getShortcuts(Arrays.asList(musicFolder))) { + result.add(forDirectory(shortcut, request, username)); + } + + MusicFolderContent musicFolderContent = musicIndexService.getMusicFolderContent(Arrays.asList(musicFolder), false); + for (List artists : musicFolderContent.getIndexedArtists().values()) { + for (MusicIndex.SortableArtistWithMediaFiles artist : artists) { + for (MediaFile artistMediaFile : artist.getMediaFiles()) { + result.add(forDirectory(artistMediaFile, request, username)); + } + } + } + for (MediaFile song : musicFolderContent.getSingleSongs()) { + if (song.isAudio()) { + result.add(forSong(song, username, request)); + } + } + return result; + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public List forDirectoryContent(int mediaFileId, String username, HttpServletRequest request) { + List result = new ArrayList(); + MediaFile dir = mediaFileService.getMediaFile(mediaFileId); + List children = dir.isFile() ? Arrays.asList(dir) : mediaFileService.getChildrenOf(dir, true, true, true); + boolean isArtist = true; + for (MediaFile child : children) { + if (child.isDirectory()) { + result.add(forDirectory(child, request, username)); + isArtist &= child.isAlbum(); + } else if (child.isAudio()) { + isArtist = false; + result.add(forSong(child, username, request)); + } + } + + if (isArtist) { + MediaMetadata shuffle = new MediaMetadata(); + shuffle.setItemType(ItemType.PROGRAM); + shuffle.setId(SonosService.ID_SHUFFLE_ARTIST_PREFIX + mediaFileId); + shuffle.setTitle(String.format("Shuffle Play - %s", dir.getName())); + result.add(0, shuffle); + + MediaMetadata radio = new MediaMetadata(); + radio.setItemType(ItemType.PROGRAM); + radio.setId(SonosService.ID_RADIO_ARTIST_PREFIX + mediaFileId); + radio.setTitle(String.format("Artist Radio - %s", dir.getName())); + result.add(1, radio); + } + + return result; + } + + private MediaCollection forDirectory(MediaFile dir, HttpServletRequest request, String username) { + mediaFileService.populateStarredDate(dir, username); + MediaCollection mediaCollection = new MediaCollection(); + + mediaCollection.setId(String.valueOf(dir.getId())); + mediaCollection.setIsFavorite(dir.getStarredDate() != null); + if (dir.isAlbum()) { + mediaCollection.setItemType(ItemType.ALBUM); + mediaCollection.setArtist(dir.getArtist()); + mediaCollection.setTitle(dir.getName()); + mediaCollection.setCanPlay(true); + + AlbumArtUrl albumArtURI = new AlbumArtUrl(); + albumArtURI.setValue(getCoverArtUrl(String.valueOf(dir.getId()), request)); + mediaCollection.setAlbumArtURI(albumArtURI); + } else { + mediaCollection.setItemType(ItemType.CONTAINER); + mediaCollection.setTitle(dir.getName()); + } + return mediaCollection; + } + + public List forPlaylists(String username, HttpServletRequest request) { + List result = new ArrayList(); + for (Playlist playlist : playlistService.getReadablePlaylistsForUser(username)) { + MediaCollection mediaCollection = new MediaCollection(); + AlbumArtUrl albumArtURI = new AlbumArtUrl(); + albumArtURI.setValue(getCoverArtUrl(CoverArtController.PLAYLIST_COVERART_PREFIX + playlist.getId(), request)); + + mediaCollection.setId(SonosService.ID_PLAYLIST_PREFIX + playlist.getId()); + mediaCollection.setCanPlay(true); + mediaCollection.setReadOnly(!username.equals(playlist.getUsername())); + mediaCollection.setRenameable(username.equals(playlist.getUsername())); + mediaCollection.setUserContent(false); + mediaCollection.setItemType(ItemType.PLAYLIST); + mediaCollection.setArtist(playlist.getUsername()); + mediaCollection.setTitle(playlist.getName()); + mediaCollection.setAlbumArtURI(albumArtURI); + result.add(mediaCollection); + } + return result; + } + + public List forAlbumLists() { + List result = new ArrayList(); + + for (AlbumListType albumListType : AlbumListType.values()) { + MediaCollection mediaCollection = new MediaCollection(); + mediaCollection.setId(SonosService.ID_ALBUMLIST_PREFIX + albumListType.getId()); + mediaCollection.setItemType(ItemType.ALBUM_LIST); + mediaCollection.setTitle(albumListType.getDescription()); + result.add(mediaCollection); + } + return result; + } + + public List forPodcastChannels() { + List result = new ArrayList(); + for (PodcastChannel channel : podcastService.getAllChannels()) { + MediaCollection mediaCollection = new MediaCollection(); + mediaCollection.setId(SonosService.ID_PODCAST_CHANNEL_PREFIX + channel.getId()); + mediaCollection.setTitle(channel.getTitle()); + mediaCollection.setItemType(ItemType.TRACK); + result.add(mediaCollection); + } + return result; + } + + public List forPodcastChannel(int channelId, String username, HttpServletRequest request) { + List result = new ArrayList(); + for (PodcastEpisode episode : podcastService.getEpisodes(channelId)) { + if (episode.getStatus() == PodcastStatus.COMPLETED) { + Integer mediaFileId = episode.getMediaFileId(); + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + if (mediaFile != null) { + result.add(forMediaFile(mediaFile, username, request)); + } + } + } + return result; + } + + public MediaList forAlbumList(AlbumListType albumListType, int offset, int count, String username, HttpServletRequest request) { + if (albumListType == AlbumListType.DECADE) { + return forDecades(offset, count); + } + if (albumListType == AlbumListType.GENRE) { + return forGenres(offset, count); + } + + MediaList mediaList = new MediaList(); + + boolean includeShuffle = offset == 0; + if (includeShuffle) { + count--; + MediaMetadata shuffle = new MediaMetadata(); + shuffle.setItemType(ItemType.PROGRAM); + shuffle.setId(SonosService.ID_SHUFFLE_ALBUMLIST_PREFIX + albumListType.getId()); + shuffle.setTitle(String.format("Shuffle Play - %s", albumListType.getDescription())); + mediaList.getMediaCollectionOrMediaMetadata().add(shuffle); + } + + AlbumList albumList = createAlbumList(albumListType, offset - (includeShuffle ? 0 : 1), count, username); + for (MediaFile album : albumList.getAlbums()) { + mediaList.getMediaCollectionOrMediaMetadata().add(forDirectory(album, request, username)); + } + + mediaList.setIndex(offset); + mediaList.setCount(mediaList.getMediaCollectionOrMediaMetadata().size()); + mediaList.setTotal(albumList.getTotal() + 1); + return mediaList; + } + + private AlbumList createAlbumList(AlbumListType albumListType, int offset, int count, String username) { + List musicFolders = settingsService.getMusicFoldersForUser(username); + List albums = Collections.emptyList(); + int total = 0; + switch (albumListType) { + case RANDOM: + albums = searchService.getRandomAlbums(count, musicFolders); + total = mediaFileService.getAlbumCount(musicFolders); + break; + case NEWEST: + albums = mediaFileService.getNewestAlbums(offset, count, musicFolders); + total = mediaFileService.getAlbumCount(musicFolders); + break; + case STARRED: + albums = mediaFileService.getStarredAlbums(offset, count, username, musicFolders); + total = mediaFileService.getStarredAlbumCount(username, musicFolders); + break; + case HIGHEST: + albums = ratingService.getHighestRatedAlbums(offset, count, musicFolders); + total = ratingService.getRatedAlbumCount(username, musicFolders); + break; + case FREQUENT: + albums = mediaFileService.getMostFrequentlyPlayedAlbums(offset, count, musicFolders); + total = mediaFileService.getPlayedAlbumCount(musicFolders); + break; + case RECENT: + albums = mediaFileService.getMostRecentlyPlayedAlbums(offset, count, musicFolders); + total = mediaFileService.getPlayedAlbumCount(musicFolders); + break; + case ALPHABETICAL: + albums = mediaFileService.getAlphabeticalAlbums(offset, count, true, musicFolders); + total = mediaFileService.getAlbumCount(musicFolders); + break; + } + return new AlbumList(albums, total); + } + + private MediaList forDecades(int offset, int count) { + List mediaCollections = new ArrayList(); + int currentDecade = Calendar.getInstance().get(Calendar.YEAR) / 10; + for (int i = 0; i < 10; i++) { + int decade = (currentDecade - i) * 10; + MediaCollection mediaCollection = new MediaCollection(); + mediaCollection.setItemType(ItemType.ALBUM_LIST); + mediaCollection.setId(SonosService.ID_DECADE_PREFIX + decade); + mediaCollection.setTitle(String.valueOf(decade)); + mediaCollections.add(mediaCollection); + } + + return createSubList(offset, count, mediaCollections); + } + + private MediaList forGenres(int offset, int count) { + List mediaCollections = new ArrayList(); + List genres = mediaFileService.getGenres(true); + for (int i = 0; i < genres.size(); i++) { + Genre genre = genres.get(i); + MediaCollection mediaCollection = new MediaCollection(); + mediaCollection.setItemType(ItemType.ALBUM_LIST); + mediaCollection.setId(SonosService.ID_GENRE_PREFIX + i); + mediaCollection.setTitle(genre.getName() + " (" + genre.getAlbumCount() + ")"); + mediaCollections.add(mediaCollection); + } + + return createSubList(offset, count, mediaCollections); + } + + public List forDecade(int decade, String username, HttpServletRequest request) { + List musicFolders = settingsService.getMusicFoldersForUser(username); + List result = new ArrayList(); + for (MediaFile album : mediaFileService.getAlbumsByYear(0, Integer.MAX_VALUE, decade, decade + 9, musicFolders)) { + result.add(forDirectory(album, request, username)); + } + return result; + } + + public List forGenre(int genreIndex, String username, HttpServletRequest request) { + List musicFolders = settingsService.getMusicFoldersForUser(username); + Genre genre = mediaFileService.getGenres(true).get(genreIndex); + List result = new ArrayList(); + for (MediaFile album : mediaFileService.getAlbumsByGenre(0, Integer.MAX_VALUE, genre.getName(), musicFolders)) { + result.add(forDirectory(album, request, username)); + } + return result; + } + + public List forPlaylist(int playlistId, String username, HttpServletRequest request) { + List result = new ArrayList(); + for (MediaFile song : playlistService.getFilesInPlaylist(playlistId)) { + if (song.isAudio()) { + result.add(forSong(song, username, request)); + } + } + return result; + } + + public List forStarred() { + MediaCollection artists = new MediaCollection(); + artists.setItemType(ItemType.FAVORITES); + artists.setId(SonosService.ID_STARRED_ARTISTS); + artists.setTitle("Starred Artists"); + + MediaCollection albums = new MediaCollection(); + albums.setItemType(ItemType.FAVORITES); + albums.setId(SonosService.ID_STARRED_ALBUMS); + albums.setTitle("Starred Albums"); + + MediaCollection songs = new MediaCollection(); + songs.setItemType(ItemType.FAVORITES); + songs.setId(SonosService.ID_STARRED_SONGS); + songs.setCanPlay(true); + songs.setTitle("Starred Songs"); + + return Arrays.asList(artists, albums, songs); + } + + public List forStarredArtists(String username, HttpServletRequest request) { + List result = new ArrayList(); + List musicFolders = settingsService.getMusicFoldersForUser(username); + for (MediaFile artist : mediaFileDao.getStarredDirectories(0, Integer.MAX_VALUE, username, musicFolders)) { + MediaCollection mediaCollection = forDirectory(artist, request, username); + mediaCollection.setItemType(ItemType.ARTIST); + result.add(mediaCollection); + } + return result; + } + + public List forStarredAlbums(String username, HttpServletRequest request) { + List musicFolders = settingsService.getMusicFoldersForUser(username); + List result = new ArrayList(); + for (MediaFile album : mediaFileDao.getStarredAlbums(0, Integer.MAX_VALUE, username, musicFolders)) { + MediaCollection mediaCollection = forDirectory(album, request, username); + mediaCollection.setItemType(ItemType.ALBUM); + result.add(mediaCollection); + } + return result; + } + + public List forStarredSongs(String username, HttpServletRequest request) { + List musicFolders = settingsService.getMusicFoldersForUser(username); + List result = new ArrayList(); + for (MediaFile song : mediaFileDao.getStarredFiles(0, Integer.MAX_VALUE, username, musicFolders)) { + if (song.isAudio()) { + result.add(forSong(song, username, request)); + } + } + return result; + } + + public List forSearchCategories() { + MediaCollection artists = new MediaCollection(); + artists.setItemType(ItemType.ARTIST); + artists.setId(SonosService.ID_SEARCH_ARTISTS); + artists.setTitle("Artists"); + + MediaCollection albums = new MediaCollection(); + albums.setItemType(ItemType.ALBUM); + albums.setId(SonosService.ID_SEARCH_ALBUMS); + albums.setTitle("Albums"); + + MediaCollection songs = new MediaCollection(); + songs.setItemType(ItemType.TRACK); + songs.setId(SonosService.ID_SEARCH_SONGS); + songs.setTitle("Songs"); + + return Arrays.asList(artists, albums, songs); + } + + public MediaList forSearch(String query, int offset, int count, SearchService.IndexType indexType, String username, HttpServletRequest request) { + + SearchCriteria searchCriteria = new SearchCriteria(); + searchCriteria.setCount(count); + searchCriteria.setOffset(offset); + searchCriteria.setQuery(query); + List musicFolders = settingsService.getMusicFoldersForUser(username); + + SearchResult searchResult = searchService.search(searchCriteria, musicFolders, indexType); + + MediaList result = new MediaList(); + result.setTotal(searchResult.getTotalHits()); + result.setIndex(offset); + result.setCount(searchResult.getMediaFiles().size()); + for (MediaFile mediaFile : searchResult.getMediaFiles()) { + result.getMediaCollectionOrMediaMetadata().add(forMediaFile(mediaFile, username, request)); + } + + return result; + } + + public List forSimilarArtists(int mediaFileId, String username, HttpServletRequest request) { + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + List musicFolders = settingsService.getMusicFoldersForUser(username); + List similarArtists = lastFmService.getSimilarArtists(mediaFile, 100, false, musicFolders); + return forMediaFiles(similarArtists, username, request); + } + + private List forMediaFiles(List mediaFiles, String username, HttpServletRequest request) { + List result = new ArrayList(); + for (MediaFile mediaFile : mediaFiles) { + result.add(forMediaFile(mediaFile, username, request)); + } + return result; + } + + public AbstractMedia forMediaFile(MediaFile mediaFile, String username, HttpServletRequest request) { + return mediaFile.isFile() ? forSong(mediaFile, username, request) : forDirectory(mediaFile, request, username); + } + + public MediaMetadata forSong(MediaFile song, String username, HttpServletRequest request) { + Player player = createPlayerIfNecessary(username); + String suffix = transcodingService.getSuffix(player, song, null); + mediaFileService.populateStarredDate(song, username); + + MediaMetadata result = new MediaMetadata(); + result.setId(String.valueOf(song.getId())); + result.setItemType(ItemType.TRACK); + result.setMimeType(StringUtil.getMimeType(suffix, true)); + result.setTitle(song.getTitle()); + result.setGenre(song.getGenre()); + result.setIsFavorite(song.getStarredDate() != null); +// result.setDynamic();// TODO: For starred songs + + AlbumArtUrl albumArtURI = new AlbumArtUrl(); + albumArtURI.setValue(getCoverArtUrl(String.valueOf(song.getId()), request)); + + TrackMetadata trackMetadata = new TrackMetadata(); + trackMetadata.setArtist(song.getArtist()); + trackMetadata.setAlbumArtist(song.getAlbumArtist()); + trackMetadata.setAlbum(song.getAlbumName()); + trackMetadata.setAlbumArtURI(albumArtURI); + trackMetadata.setDuration(song.getDurationSeconds()); + trackMetadata.setTrackNumber(song.getTrackNumber()); + + MediaFile parent = mediaFileService.getParentOf(song); + if (parent != null && parent.isAlbum()) { + trackMetadata.setAlbumId(String.valueOf(parent.getId())); + } + result.setTrackMetadata(trackMetadata); + + return result; + } + + public void star(int id, String username) { + mediaFileDao.starMediaFile(id, username); + } + + public void unstar(int id, String username) { + mediaFileDao.unstarMediaFile(id, username); + } + + private String getCoverArtUrl(String id, HttpServletRequest request) { + return getBaseUrl(request) + "coverArt.view?id=" + id + "&size=" + CoverArtScheme.LARGE.getSize(); + } + + public static MediaList createSubList(int index, int count, List mediaCollections) { + MediaList result = new MediaList(); + List selectedMediaCollections = Util.subList(mediaCollections, index, count); + + result.setIndex(index); + result.setCount(selectedMediaCollections.size()); + result.setTotal(mediaCollections.size()); + result.getMediaCollectionOrMediaMetadata().addAll(selectedMediaCollections); + + return result; + } + + private List filterMusic(List files) { + return Lists.newArrayList(Iterables.filter(files, new Predicate() { + @Override + public boolean apply(MediaFile input) { + return input.getMediaType() == MediaFile.MediaType.MUSIC; + } + })); + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + public void setTranscodingService(TranscodingService transcodingService) { + this.transcodingService = transcodingService; + } + + public String getMediaURI(int mediaFileId, String username, HttpServletRequest request) { + Player player = createPlayerIfNecessary(username); + MediaFile song = mediaFileService.getMediaFile(mediaFileId); + + return getBaseUrl(request) + "stream?id=" + song.getId() + "&player=" + player.getId(); + } + + private Player createPlayerIfNecessary(String username) { + List players = playerService.getPlayersForUserAndClientId(username, SUBSONIC_CLIENT_ID); + + // If not found, create it. + if (players.isEmpty()) { + Player player = new Player(); + player.setUsername(username); + player.setClientId(SUBSONIC_CLIENT_ID); + player.setName("Sonos"); + player.setTechnology(PlayerTechnology.EXTERNAL_WITH_PLAYLIST); + playerService.createPlayer(player); + players = playerService.getPlayersForUserAndClientId(username, SUBSONIC_CLIENT_ID); + } + + return players.get(0); + } + + public String getBaseUrl(HttpServletRequest request) { + int port = settingsService.getPort(); + String contextPath = settingsService.getUrlRedirectContextPath(); + + // Note that the server IP can be overridden by the "ip" parameter. Used when Subsonic and Sonos are + // on different networks. + String ip = settingsService.getLocalIpAddress(); + if (request != null) { + ip = ServletRequestUtils.getStringParameter(request, "ip", ip); + } + + // Note: Serving media and cover art with http (as opposed to https) works when using jetty and SubsonicDeployer. + StringBuilder url = new StringBuilder("http://") + .append(ip) + .append(":") + .append(port) + .append("/"); + + if (StringUtils.isNotEmpty(contextPath)) { + url.append(contextPath).append("/"); + } + return url.toString(); + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } + + public void setMusicIndexService(MusicIndexService musicIndexService) { + this.musicIndexService = musicIndexService; + } + + public void setMediaFileDao(MediaFileDao mediaFileDao) { + this.mediaFileDao = mediaFileDao; + } + + public void setSearchService(SearchService searchService) { + this.searchService = searchService; + } + + public void setRatingService(RatingService ratingService) { + this.ratingService = ratingService; + } + + public void setLastFmService(LastFmService lastFmService) { + this.lastFmService = lastFmService; + } + + public void setPodcastService(PodcastService podcastService) { + this.podcastService = podcastService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosServiceRegistration.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosServiceRegistration.java new file mode 100644 index 00000000..abe5bf0f --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosServiceRegistration.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service.sonos; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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.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.util.Pair; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class SonosServiceRegistration { + + private static final Logger LOG = Logger.getLogger(SonosServiceRegistration.class); + + public void setEnabled(String subsonicBaseUrl, String sonosControllerIp, boolean enabled, String sonosServiceName, int sonosServiceId) throws IOException { + String localUrl = subsonicBaseUrl + "ws/Sonos"; + String controllerUrl = String.format("http://%s:1400/customsd", sonosControllerIp); + + LOG.info((enabled ? "Enabling" : "Disabling") + " Sonos music service, using Sonos controller IP " + sonosControllerIp + + ", SID " + sonosServiceId + ", and Subsonic URL " + localUrl); + + List> params = new ArrayList>(); + params.add(Pair.create("sid", String.valueOf(sonosServiceId))); + if (enabled) { + params.add(Pair.create("name", sonosServiceName)); + params.add(Pair.create("uri", localUrl)); + params.add(Pair.create("secureUri", localUrl)); + params.add(Pair.create("pollInterval", "1200")); + params.add(Pair.create("authType", "UserId")); + params.add(Pair.create("containerType", "MService")); + params.add(Pair.create("caps", "search")); + params.add(Pair.create("caps", "trFavorites")); + params.add(Pair.create("caps", "alFavorites")); + params.add(Pair.create("caps", "ucPlaylists")); + params.add(Pair.create("caps", "extendedMD")); + params.add(Pair.create("presentationMapVersion", "1")); + params.add(Pair.create("presentationMapUri", subsonicBaseUrl + "sonos/presentationMap.xml")); + params.add(Pair.create("stringsVersion", "5")); + params.add(Pair.create("stringsUri", subsonicBaseUrl + "sonos/strings.xml")); + } + + String result = execute(controllerUrl, params); + LOG.info("Sonos controller returned: " + result); + } + + private String execute(String url, List> parameters) throws IOException { + List params = new ArrayList(); + for (Pair parameter : parameters) { + params.add(new BasicNameValuePair(parameter.getFirst(), parameter.getSecond())); + } + + 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(), 10000); + HttpConnectionParams.setSoTimeout(client.getParams(), 10000); + + try { + ResponseHandler responseHandler = new BasicResponseHandler(); + return client.execute(request, responseHandler); + + } finally { + client.getConnectionManager().shutdown(); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosSoapFault.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosSoapFault.java new file mode 100644 index 00000000..a824cf21 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/sonos/SonosSoapFault.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 . + * + * Copyright 2015 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.service.sonos; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class SonosSoapFault extends RuntimeException { + + private final String faultCode; + + // Must match values in strings.xml + private final int sonosError; + + protected SonosSoapFault(String faultCode, int sonosError) { + this.faultCode = faultCode; + this.sonosError = sonosError; + } + + public String getFaultCode() { + return faultCode; + } + + public int getSonosError() { + return sonosError; + } + + public static class LoginInvalid extends SonosSoapFault { + + public LoginInvalid() { + super("Client.LoginInvalid", 0); + } + } + + public static class LoginUnauthorized extends SonosSoapFault { + + public LoginUnauthorized() { + super("Client.LoginUnauthorized", 1); + } + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/ApacheUpnpServiceConfiguration.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/ApacheUpnpServiceConfiguration.java new file mode 100644 index 00000000..eeadce90 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/ApacheUpnpServiceConfiguration.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 . + + Copyright 2013 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.upnp; + +import java.util.concurrent.Executors; + +import org.fourthline.cling.DefaultUpnpServiceConfiguration; +import org.fourthline.cling.transport.impl.apache.StreamClientConfigurationImpl; +import org.fourthline.cling.transport.impl.apache.StreamClientImpl; +import org.fourthline.cling.transport.impl.apache.StreamServerConfigurationImpl; +import org.fourthline.cling.transport.impl.apache.StreamServerImpl; +import org.fourthline.cling.transport.spi.NetworkAddressFactory; +import org.fourthline.cling.transport.spi.StreamClient; +import org.fourthline.cling.transport.spi.StreamServer; + +/** + * UPnP configuration which uses Apache HttpComponents. Needed to make UPnP work + * when deploying on Tomcat. + * + * @author Sindre Mehus + * @version $Id$ + */ +public class ApacheUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration { + + @Override + public StreamClient createStreamClient() { + return new StreamClientImpl(new StreamClientConfigurationImpl(Executors.newCachedThreadPool())); + } + + @Override + public StreamServer createStreamServer(NetworkAddressFactory networkAddressFactory) { + return new StreamServerImpl(new StreamServerConfigurationImpl(networkAddressFactory.getStreamListenPort())); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/ClingRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/ClingRouter.java new file mode 100644 index 00000000..fd8c8c29 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/ClingRouter.java @@ -0,0 +1,143 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.upnp; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collection; +import java.util.concurrent.Semaphore; +import java.util.concurrent.atomic.AtomicReference; + +import org.fourthline.cling.UpnpService; +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Device; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.support.igd.PortMappingListener; +import org.fourthline.cling.support.igd.callback.PortMappingAdd; +import org.fourthline.cling.support.igd.callback.PortMappingDelete; +import org.fourthline.cling.support.model.PortMapping; + +import net.sourceforge.subsonic.service.UPnPService; + +/** +* @author Sindre Mehus +* @version $Id$ +*/ +public class ClingRouter implements Router { + + private final Service connectionService; + private final UpnpService upnpService; + + public static ClingRouter findRouter(UPnPService upnpService) { + final Service connectionService = findConnectionService(upnpService.getUpnpService()); + if (connectionService == null) { + return null; + } + return new ClingRouter(connectionService, upnpService.getUpnpService()); + } + + /** + * Returns the UPnP service used for port mapping. + */ + private static Service findConnectionService(UpnpService upnpService) { + + class ConnectionServiceDiscoverer extends PortMappingListener { + ConnectionServiceDiscoverer() { + super(new PortMapping[0]); + } + + @Override + public Service discoverConnectionService(Device device) { + return super.discoverConnectionService(device); + } + } + + ConnectionServiceDiscoverer discoverer = new ConnectionServiceDiscoverer(); + Collection devices = upnpService.getRegistry().getDevices(); + for (Device device : devices) { + Service service = discoverer.discoverConnectionService(device); + if (service != null) { + return service; + } + } + return null; + } + + public ClingRouter(Service connectionService, UpnpService upnpService) { + this.connectionService = connectionService; + this.upnpService = upnpService; + } + + public void addPortMapping(int externalPort, int internalPort, int leaseDuration) throws Exception { + addPortMappingImpl(connectionService, internalPort); + } + + public void deletePortMapping(int externalPort, int internalPort) throws Exception { + deletePortMappingImpl(connectionService, internalPort); + } + + private void addPortMappingImpl(Service connectionService, int port) throws Exception { + final Semaphore gotReply = new Semaphore(0); + final AtomicReference error = new AtomicReference(); + upnpService.getControlPoint().execute( + new PortMappingAdd(connectionService, createPortMapping(port)) { + + @Override + public void success(ActionInvocation invocation) { + gotReply.release(); + } + + @Override + public void failure(ActionInvocation invocation, UpnpResponse response, String defaultMsg) { + error.set(String.valueOf(response) + ": " + defaultMsg); + gotReply.release(); + } + } + ); + gotReply.acquire(); + if (error.get() != null) { + throw new Exception(error.get()); + } + } + + private void deletePortMappingImpl(Service connectionService, int port) throws Exception { + final Semaphore gotReply = new Semaphore(0); + upnpService.getControlPoint().execute( + new PortMappingDelete(connectionService, createPortMapping(port)) { + + @Override + public void success(ActionInvocation invocation) { + gotReply.release(); + } + + @Override + public void failure(ActionInvocation invocation, UpnpResponse response, String defaultMsg) { + gotReply.release(); + } + } + ); + gotReply.acquire(); + } + + private PortMapping createPortMapping(int port) throws UnknownHostException { + String localIp = InetAddress.getLocalHost().getHostAddress(); + return new PortMapping(port, localIp, PortMapping.Protocol.TCP, "Subsonic"); + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/FolderBasedContentDirectory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/FolderBasedContentDirectory.java new file mode 100644 index 00000000..9702e999 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/FolderBasedContentDirectory.java @@ -0,0 +1,288 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.upnp; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Arrays; +import java.util.List; + +import org.fourthline.cling.support.contentdirectory.ContentDirectoryErrorCode; +import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; +import org.fourthline.cling.support.model.BrowseFlag; +import org.fourthline.cling.support.model.BrowseResult; +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.PersonWithRole; +import org.fourthline.cling.support.model.SortCriterion; +import org.fourthline.cling.support.model.WriteStatus; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.MusicAlbum; +import org.fourthline.cling.support.model.container.PlaylistContainer; +import org.fourthline.cling.support.model.container.StorageFolder; +import org.fourthline.cling.support.model.item.Item; +import org.fourthline.cling.support.model.item.MusicTrack; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.domain.CoverArtScheme; +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.MediaLibraryStatistics; +import net.sourceforge.subsonic.domain.MusicFolder; +import net.sourceforge.subsonic.domain.Playlist; +import net.sourceforge.subsonic.service.MediaFileService; +import net.sourceforge.subsonic.service.PlaylistService; +import net.sourceforge.subsonic.util.Util; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class FolderBasedContentDirectory extends SubsonicContentDirectory { + + private static final Logger LOG = Logger.getLogger(FolderBasedContentDirectory.class); + private static final String CONTAINER_ID_PLAYLIST_ROOT = "playlists"; + private static final String CONTAINER_ID_PLAYLIST_PREFIX = "playlist-"; + private static final String CONTAINER_ID_FOLDER_PREFIX = "folder-"; + private MediaFileService mediaFileService; + private PlaylistService playlistService; + + @Override + public BrowseResult browse(String objectId, BrowseFlag browseFlag, String filter, long firstResult, + long maxResults, SortCriterion[] orderby) throws ContentDirectoryException { + + if (!settingsService.getLicenseInfo().isLicenseOrTrialValid()) { + LOG.warn("UPnP/DLNA media server not available. Please upgrade to Subsonic Premium."); + throw new ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, "Please upgrade to Subsonic Premium"); + } + + LOG.info("UPnP request - objectId: " + objectId + ", browseFlag: " + browseFlag + ", filter: " + filter + + ", firstResult: " + firstResult + ", maxResults: " + maxResults); + + // maxResult == 0 means all. + if (maxResults == 0) { + maxResults = Integer.MAX_VALUE; + } + + try { + if (CONTAINER_ID_ROOT.equals(objectId)) { + return browseFlag == BrowseFlag.METADATA ? browseRootMetadata() : browseRoot(firstResult, maxResults); + } + if (CONTAINER_ID_PLAYLIST_ROOT.equals(objectId)) { + return browseFlag == BrowseFlag.METADATA ? browsePlaylistRootMetadata() : browsePlaylistRoot(firstResult, maxResults); + } + if (objectId.startsWith(CONTAINER_ID_PLAYLIST_PREFIX)) { + int playlistId = Integer.parseInt(objectId.replace(CONTAINER_ID_PLAYLIST_PREFIX, "")); + Playlist playlist = playlistService.getPlaylist(playlistId); + return browseFlag == BrowseFlag.METADATA ? browsePlaylistMetadata(playlist) : browsePlaylist(playlist, firstResult, maxResults); + } + + int mediaFileId = Integer.parseInt(objectId.replace(CONTAINER_ID_FOLDER_PREFIX, "")); + MediaFile mediaFile = mediaFileService.getMediaFile(mediaFileId); + return browseFlag == BrowseFlag.METADATA ? browseMediaFileMetadata(mediaFile) : browseMediaFile(mediaFile, firstResult, maxResults); + + } catch (Throwable x) { + LOG.error("UPnP error: " + x, x); + throw new ContentDirectoryException(ContentDirectoryErrorCode.CANNOT_PROCESS, x.toString()); + } + } + + private BrowseResult browseRootMetadata() throws Exception { + StorageFolder root = new StorageFolder(); + root.setId(CONTAINER_ID_ROOT); + root.setParentID("-1"); + + MediaLibraryStatistics statistics = settingsService.getMediaLibraryStatistics(); + root.setStorageUsed(statistics == null ? 0 : statistics.getTotalLengthInBytes()); + root.setTitle("Subsonic Media"); + root.setRestricted(true); + root.setSearchable(false); + root.setWriteStatus(WriteStatus.NOT_WRITABLE); + + List musicFolders = settingsService.getAllMusicFolders(); + root.setChildCount(musicFolders.size() + 1); // +1 for playlists + + DIDLContent didl = new DIDLContent(); + didl.addContainer(root); + return createBrowseResult(didl, 1, 1); + } + + private BrowseResult browsePlaylistRootMetadata() throws Exception { + DIDLContent didl = new DIDLContent(); + didl.addContainer(createPlaylistRootContainer()); + return createBrowseResult(didl, 1, 1); + } + + private BrowseResult browsePlaylistRoot(long firstResult, long maxResults) throws Exception { + DIDLContent didl = new DIDLContent(); + List allPlaylists = playlistService.getAllPlaylists(); + List selectedPlaylists = Util.subList(allPlaylists, firstResult, maxResults); + for (Playlist playlist : selectedPlaylists) { + didl.addContainer(createPlaylistContainer(playlist)); + } + + return createBrowseResult(didl, selectedPlaylists.size(), allPlaylists.size()); + } + + private BrowseResult browsePlaylistMetadata(Playlist playlist) throws Exception { + DIDLContent didl = new DIDLContent(); + didl.addContainer(createPlaylistContainer(playlist)); + return createBrowseResult(didl, 1, 1); + } + + private BrowseResult browsePlaylist(Playlist playlist, long firstResult, long maxResults) throws Exception { + List allChildren = playlistService.getFilesInPlaylist(playlist.getId()); + List selectedChildren = Util.subList(allChildren, firstResult, maxResults); + + DIDLContent didl = new DIDLContent(); + for (MediaFile child : selectedChildren) { + addContainerOrItem(didl, child); + } + return createBrowseResult(didl, selectedChildren.size(), allChildren.size()); + } + + private BrowseResult browseRoot(long firstResult, long maxResults) throws Exception { + DIDLContent didl = new DIDLContent(); + List allFolders = settingsService.getAllMusicFolders(); + List selectedFolders = Util.subList(allFolders, firstResult, maxResults); + for (MusicFolder folder : selectedFolders) { + MediaFile mediaFile = mediaFileService.getMediaFile(folder.getPath()); + addContainerOrItem(didl, mediaFile); + } + + if (maxResults > selectedFolders.size()) { + didl.addContainer(createPlaylistRootContainer()); + } + + return createBrowseResult(didl, (int) didl.getCount(), allFolders.size() + 1); + } + + private BrowseResult browseMediaFileMetadata(MediaFile mediaFile) throws Exception { + DIDLContent didl = new DIDLContent(); + didl.addContainer(createContainer(mediaFile)); + return createBrowseResult(didl, 1, 1); + } + + private BrowseResult browseMediaFile(MediaFile mediaFile, long firstResult, long maxResults) throws Exception { + List allChildren = mediaFileService.getChildrenOf(mediaFile, true, true, true); + List selectedChildren = Util.subList(allChildren, firstResult, maxResults); + + DIDLContent didl = new DIDLContent(); + for (MediaFile child : selectedChildren) { + addContainerOrItem(didl, child); + } + return createBrowseResult(didl, selectedChildren.size(), allChildren.size()); + } + + private void addContainerOrItem(DIDLContent didl, MediaFile mediaFile) throws Exception { + if (mediaFile.isFile()) { + didl.addItem(createItem(mediaFile)); + } else { + didl.addContainer(createContainer(mediaFile)); + } + } + + private Item createItem(MediaFile song) throws Exception { + MediaFile parent = mediaFileService.getParentOf(song); + MusicTrack item = new MusicTrack(); + item.setId(String.valueOf(song.getId())); + item.setParentID(String.valueOf(parent.getId())); + item.setTitle(song.getTitle()); + item.setAlbum(song.getAlbumName()); + if (song.getArtist() != null) { + item.setArtists(new PersonWithRole[]{new PersonWithRole(song.getArtist())}); + } + Integer year = song.getYear(); + if (year != null) { + item.setDate(year + "-01-01"); + } + item.setOriginalTrackNumber(song.getTrackNumber()); + if (song.getGenre() != null) { + item.setGenres(new String[]{song.getGenre()}); + } + item.setResources(Arrays.asList(createResourceForSong(song))); + item.setDescription(song.getComment()); + item.addProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(getAlbumArtUrl(parent))); + + return item; + } + + private Container createContainer(MediaFile mediaFile) throws Exception { + Container container = mediaFile.isAlbum() ? createAlbumContainer(mediaFile) : new MusicAlbum(); + container.setId(CONTAINER_ID_FOLDER_PREFIX + mediaFile.getId()); + container.setTitle(mediaFile.getName()); + List children = mediaFileService.getChildrenOf(mediaFile, true, true, false); + container.setChildCount(children.size()); + + container.setParentID(CONTAINER_ID_ROOT); + if (!mediaFileService.isRoot(mediaFile)) { + MediaFile parent = mediaFileService.getParentOf(mediaFile); + if (parent != null) { + container.setParentID(String.valueOf(parent.getId())); + } + } + return container; + } + + private Container createAlbumContainer(MediaFile album) throws Exception { + MusicAlbum container = new MusicAlbum(); + container.setAlbumArtURIs(new URI[]{getAlbumArtUrl(album)}); + + // TODO: correct artist? + if (album.getArtist() != null) { + container.setArtists(new PersonWithRole[]{new PersonWithRole(album.getArtist())}); + } + container.setDescription(album.getComment()); + + return container; + } + + private Container createPlaylistRootContainer() { + Container container = new StorageFolder(); + container.setId(CONTAINER_ID_PLAYLIST_ROOT); + container.setTitle("Playlists"); + + List playlists = playlistService.getAllPlaylists(); + container.setChildCount(playlists.size()); + container.setParentID(CONTAINER_ID_ROOT); + return container; + } + + private Container createPlaylistContainer(Playlist playlist) { + PlaylistContainer container = new PlaylistContainer(); + container.setId(CONTAINER_ID_PLAYLIST_PREFIX + playlist.getId()); + container.setParentID(CONTAINER_ID_PLAYLIST_ROOT); + container.setTitle(playlist.getName()); + container.setDescription(playlist.getComment()); + container.setChildCount(playlistService.getFilesInPlaylist(playlist.getId()).size()); + + return container; + } + + private URI getAlbumArtUrl(MediaFile album) throws URISyntaxException { + return new URI(getBaseUrl() + "coverArt.view?id=" + album.getId() + "&size=" + CoverArtScheme.LARGE.getSize()); + } + + public void setMediaFileService(MediaFileService mediaFileService) { + this.mediaFileService = mediaFileService; + } + + public void setPlaylistService(PlaylistService playlistService) { + this.playlistService = playlistService; + } +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/MSMediaReceiverRegistrarService.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/MSMediaReceiverRegistrarService.java new file mode 100644 index 00000000..362e4ed9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/MSMediaReceiverRegistrarService.java @@ -0,0 +1,10 @@ +package net.sourceforge.subsonic.service.upnp; + +import org.fourthline.cling.support.xmicrosoft.AbstractMediaReceiverRegistrarService; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class MSMediaReceiverRegistrarService extends AbstractMediaReceiverRegistrarService { +} diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/NATPMPRouter.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/NATPMPRouter.java new file mode 100644 index 00000000..3099a0a9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.upnp; + +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/service/upnp/Router.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/Router.java new file mode 100644 index 00000000..13d8c64c --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.upnp; + +/** + * @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/service/upnp/SubsonicContentDirectory.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/SubsonicContentDirectory.java new file mode 100644 index 00000000..aff3c81d --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/service/upnp/SubsonicContentDirectory.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.service.upnp; + +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang.StringUtils; +import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService; +import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; +import org.fourthline.cling.support.contentdirectory.DIDLParser; +import org.fourthline.cling.support.model.BrowseResult; +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.SortCriterion; +import org.seamless.util.MimeType; + +import net.sourceforge.subsonic.domain.MediaFile; +import net.sourceforge.subsonic.domain.Player; +import net.sourceforge.subsonic.service.PlayerService; +import net.sourceforge.subsonic.service.SettingsService; +import net.sourceforge.subsonic.service.TranscodingService; +import net.sourceforge.subsonic.util.StringUtil; + +/** + * @author Sindre Mehus + * @version $Id: TagBasedContentDirectory.java 3739 2013-12-03 11:55:01Z sindre_mehus $ + */ +public abstract class SubsonicContentDirectory extends AbstractContentDirectoryService { + + protected static final String CONTAINER_ID_ROOT = "0"; + + protected SettingsService settingsService; + private PlayerService playerService; + private TranscodingService transcodingService; + + protected Res createResourceForSong(MediaFile song) { + Player player = playerService.getGuestPlayer(null); + String url = getBaseUrl() + "stream?id=" + song.getId() + "&player=" + player.getId(); + if (song.isVideo()) { + url += "&format=" + TranscodingService.FORMAT_RAW; + } + + String suffix = song.isVideo() ? FilenameUtils.getExtension(song.getPath()) : transcodingService.getSuffix(player, song, null); + String mimeTypeString = StringUtil.getMimeType(suffix); + MimeType mimeType = mimeTypeString == null ? null : MimeType.valueOf(mimeTypeString); + + Res res = new Res(mimeType, null, url); + res.setDuration(formatDuration(song.getDurationSeconds())); + return res; + } + + private String formatDuration(Integer seconds) { + if (seconds == null) { + return null; + } + + StringBuilder result = new StringBuilder(8); + + int hours = seconds / 3600; + seconds -= hours * 3600; + + int minutes = seconds / 60; + seconds -= minutes * 60; + + result.append(hours).append(':'); + if (minutes < 10) { + result.append('0'); + } + result.append(minutes).append(':'); + if (seconds < 10) { + result.append('0'); + } + result.append(seconds); + result.append(".0"); + + return result.toString(); + } + + protected String getBaseUrl() { + int port = settingsService.getPort(); + String contextPath = settingsService.getUrlRedirectContextPath(); + + // Note: Serving media and cover art with http (as opposed to https) works when using jetty and SubsonicDeployer. + StringBuilder url = new StringBuilder("http://") + .append(settingsService.getLocalIpAddress()) + .append(":") + .append(port) + .append("/"); + + if (StringUtils.isNotEmpty(contextPath)) { + url.append(contextPath).append("/"); + } + return url.toString(); + } + + protected BrowseResult createBrowseResult(DIDLContent didl, int count, int totalMatches) throws Exception { + return new BrowseResult(new DIDLParser().generate(didl), count, totalMatches); + } + + @Override + public BrowseResult search(String containerId, + String searchCriteria, String filter, + long firstResult, long maxResults, + SortCriterion[] orderBy) throws ContentDirectoryException { + // You can override this method to implement searching! + return super.search(containerId, searchCriteria, filter, firstResult, maxResults, orderBy); + } + + public void setPlayerService(PlayerService playerService) { + this.playerService = playerService; + } + + 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/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 . + + 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 String using JavaScript String rules. + *

+ * Escapes any values it finds into their JavaScript String form. + * Deals correctly with quotes and control-chars (tab, backslash, cr, ff, etc.) + *

+ * So a tab becomes the characters '\\' and + * 't'. + *

+ * The only difference between Java strings and JavaScript strings + * is that in JavaScript, a single quote must be escaped. + *

+ * Example: + *

+ * input string: He didn't say, "Stop!"
+ * output string: He didn\'t say, \"Stop!\"
+ * 
+ * + * @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 . + + 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. + *

+ * For instance: + *

    + *
  • format(918) returns "918 B".
  • + *
  • format(98765) returns "96 KB".
  • + *
  • format(1238476) returns "1.2 MB".
  • + *
+ * 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 . + + 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 . + + 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: + *
    + *
  • Parameter values are encoded as the hexadecimal representation of the UTF-8 bytes of the original string.
  • + *
  • Parameter names are prepended with the suffix "Utf8Hex"
  • + *
  • Note: Nothing is done with the parameter name or value if the value only contains ASCII alphanumeric characters.
  • + *
+ *

+ * (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 parameters = new ArrayList(); + + 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 . + + 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 . + + 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 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(); + 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..5bcca8b9 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/theme/SubsonicThemeSource.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 . + + 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; + +import net.sourceforge.subsonic.domain.Theme; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * 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 SettingsService settingsService; + private String basenamePrefix; + + @Override + protected MessageSource createMessageSource(String basename) { + ResourceBundleMessageSource messageSource = (ResourceBundleMessageSource) super.createMessageSource(basename); + + // Create parent theme recursively. + for (Theme theme : settingsService.getAvailableThemes()) { + if (basename.equals(basenamePrefix + theme.getId()) && theme.getParent() != null) { + String parent = basenamePrefix + theme.getParent(); + messageSource.setParentMessageSource(createMessageSource(parent)); + break; + } + } + return messageSource; + } + + @Override + public void setBasenamePrefix(String basenamePrefix) { + this.basenamePrefix = basenamePrefix; + super.setBasenamePrefix(basenamePrefix); + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} 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 . + + 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 . + + 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 . + + 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 . + + 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 . + + 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 extends LinkedList { + 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 true. + */ + 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..d7230cf2 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/FileUtil.java @@ -0,0 +1,190 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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("isFile", file) { + @Override + public Boolean execute() { + return file.isFile(); + } + }); + } + + public static boolean isDirectory(final File file) { + return timed(new FileTask("isDirectory", file) { + @Override + public Boolean execute() { + return file.isDirectory(); + } + }); + } + + public static boolean exists(final File file) { + return timed(new FileTask("exists", file) { + @Override + public Boolean execute() { + return file.exists(); + } + }); + } + + public static boolean exists(String path) { + return exists(new File(path)); + } + + public static long lastModified(final File file) { + return timed(new FileTask("lastModified", file) { + @Override + public Long execute() { + return file.lastModified(); + } + }); + } + + public static long length(final File file) { + return timed(new FileTask("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("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("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 timed(FileTask 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 { + + 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/HttpRange.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/HttpRange.java new file mode 100644 index 00000000..0213d769 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/HttpRange.java @@ -0,0 +1,121 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.apache.commons.lang.StringUtils; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class HttpRange { + + private static final Pattern PATTERN = Pattern.compile("bytes=(\\d+)-(\\d*)"); + private final Long firstBytePos; + private final Long lastBytePos; + + /** + * Parses the given string as a HTTP header byte range. See chapter 14.36.1 in RFC 2068 + * for details. + *

+ * 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 {@code null}. The method returns null if the syntax + * of the given range is not supported. + */ + public static HttpRange valueOf(String range) { + if (range == null) { + return null; + } + + 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 ? null : Long.parseLong(lastString); + + if (last != null && first > last) { + return null; + } + return new HttpRange(first, last); + } + return null; + } + + public HttpRange(long firstBytePos, Long lastBytePos) { + this.firstBytePos = firstBytePos; + this.lastBytePos = lastBytePos; + } + + /** + * @return The first byte position (inclusive) in the range. Never {@code null}. + */ + public Long getFirstBytePos() { + return firstBytePos; + } + + /** + * @return The last byte position (inclusive) in the range. Can be {@code null}. + */ + public Long getLastBytePos() { + return lastBytePos; + } + + /** + * @return Whether this is a closed range (both first and last byte position specified). + */ + public boolean isClosed() { + return firstBytePos != null && lastBytePos != null; + } + + /** + * @return The size in bytes if the range is closed, -1 otherwise. + */ + public long size() { + return isClosed() ? (lastBytePos - firstBytePos + 1) : -1; + } + + /** + * @return Returns whether the given byte position is within this range. + */ + public boolean contains(long pos) { + if (pos < firstBytePos) { + return false; + } + return lastBytePos == null || pos <= lastBytePos; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(firstBytePos).append('-'); + if (lastBytePos != null) { + builder.append(lastBytePos); + } + return builder.toString(); + } +} 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..58ea61e6 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Pair.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.io.Serializable; + +import org.apache.commons.lang.ObjectUtils; + +/** + * @author Sindre Mehus + */ +public class Pair implements Serializable { + + private final S first; + private final T second; + + public static Pair create(S first, T second) { + return new Pair(first, second); + } + + public Pair(S first, T second) { + this.first = first; + this.second = second; + } + + public S getFirst() { + return first; + } + + public T getSecond() { + return second; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Pair pair = (Pair) o; + + return ObjectUtils.equals(first, pair.first) && ObjectUtils.equals(second, pair.second); + } + + @Override + public int hashCode() { + return ObjectUtils.hashCode(first) * ObjectUtils.hashCode(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..f83da09a --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/StringUtil.java @@ -0,0 +1,548 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.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.URLDecoder; +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.IOUtils; +import org.apache.commons.lang.StringUtils; + +import net.sourceforge.subsonic.domain.UrlRedirectType; + +/** + * 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"}, + {"opus", "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"}, + {"ts", "video/MP2T"}, + {"webm", "video/webm"}, + + {"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. + *

+ * 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, + * application/octet-stream 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"; + } + + public static String getMimeType(String suffix, boolean sonos) { + String result = getMimeType(suffix); + + // Sonos doesn't work with "audio/mp4" but needs "audio/aac" for ALAC and AAC (in MP4 container) + return sonos && "audio/mp4".equals(result) ? "audio/aac" : result; + } + + public static String getSuffix(String mimeType) { + for (String[] map : MIME_TYPES) { + if (map[1].equalsIgnoreCase(mimeType)) { + return map[0]; + } + } + return null; + } + + /** + * Converts a byte-count to a formatted string suitable for display to the user. + * For instance: + *

    + *
  • format(918) returns "918 B".
  • + *
  • format(98765) returns "96 KB".
  • + *
  • format(1238476) returns "1.2 MB".
  • + *
+ * 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.
+ * For instance, the input "u2 rem "greatest hits"" will return an array with + * three elements: {"u2", "rem", "greatest hits"} + * + * @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 result = new ArrayList(); + 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 result = new ArrayList(); + 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 int 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; + } + + /** + * 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); + } + } + + /** + * URL-decodes the input value using UTF-8. + */ + public static String urlDecode(String s) { + try { + return URLDecoder.decode(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: + *

+ * + * getUrlFile("http://archive.ncsa.uiuc.edu:80/SDG/Software/Mosaic/Demo/url-primer.html") + * + *

+ * will return "url-primer.html". + * + * @param url The URL in question. + * @return The file part, or null 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; + } + } + + /** + * Rewrites an URL to make it accessible from remote clients. + */ + public static String rewriteRemoteUrl(String localUrl, boolean urlRedirectionEnabled, UrlRedirectType urlRedirectType, + String urlRedirectFrom, String urlRedirectCustomUrl, String urlRedirectContextPath, + String localIp, int localPort) { + try { + URLBuilder urlBuilder = new URLBuilder(localUrl); + if (urlRedirectionEnabled) { + if (urlRedirectType == UrlRedirectType.NORMAL) { + String subsonicHost = urlRedirectFrom + ".subsonic.org"; + urlBuilder.setHost(subsonicHost); + urlBuilder.setPort(80); + urlBuilder.setProtocol(URLBuilder.HTTP); + if (StringUtils.isNotBlank(urlRedirectContextPath)) { + urlBuilder.setFile(urlBuilder.getFile().replaceFirst("^/" + urlRedirectContextPath, "")); + } + + } else { + URL customUrl = new URL(urlRedirectCustomUrl); + urlBuilder.setProtocol(URLBuilder.HTTP); + urlBuilder.setHost(customUrl.getHost()); + urlBuilder.setPort(localPort); + } + + } else { + urlBuilder.setProtocol(URLBuilder.HTTP); + urlBuilder.setHost(localIp); + urlBuilder.setPort(localPort); + } + + return urlBuilder.getURLAsString(); + + } catch (Exception e) { + return localUrl; + } + } + + /** + * 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; + } + + public static String removeMarkup(String s) { + if (s == null) { + return null; + } + return s.replaceAll("<.*?>", ""); + } +} \ No newline at end of file diff --git a/subsonic-main/src/main/java/net/sourceforge/subsonic/util/URLBuilder.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/URLBuilder.java new file mode 100644 index 00000000..09370e35 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/URLBuilder.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 . + * + * Copyright 2014 (C) Sindre Mehus + */ + +package net.sourceforge.subsonic.util; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * @author Sindre Mehus + * @version $Id$ + */ +public class URLBuilder { + + public static String HTTP = "http"; + public static String HTTPS = "https"; + + private String protocol; + private String host; + private int port; + private String file; + + public URLBuilder(URL url) { + this.protocol = url.getProtocol(); + this.host = url.getHost(); + this.port = url.getPort(); + this.file = url.getFile(); + } + + public URLBuilder(String url) throws MalformedURLException { + this(new URL(url)); + } + + public void setProtocol(String protocol) { + this.protocol = protocol; + } + + public void setHost(String host) { + this.host = host; + } + + public void setPort(int port) { + this.port = port; + } + + public void setFile(String file) { + this.file = file; + } + + public String getProtocol() { + return protocol; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public String getFile() { + return file; + } + + public URL getURL() { + try { + return new URL(protocol, host, port, file); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + public String getURLAsString() { + return getURL().toString(); + } +} 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..6b644e7e --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/util/Util.java @@ -0,0 +1,202 @@ +/* + This file is part of Subsonic. + + Subsonic is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Subsonic is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Subsonic. If not, see . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.util; + +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import net.sourceforge.subsonic.Logger; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * 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. + *

+ * 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. Honours the "subsonic.host" system property. + *

+ * NOTE: For improved performance, use {@link SettingsService#getLocalIpAddress()} instead. + * + * @return The local IP, or the loopback address (127.0.0.1) if not found. + */ + public static String getLocalIpAddress() { + List ipAddresses = getLocalIpAddresses(); + String subsonicHost = System.getProperty("subsonic.host"); + if (subsonicHost != null && ipAddresses.contains(subsonicHost)) { + return subsonicHost; + } + return ipAddresses.get(0); + } + + private static List getLocalIpAddresses() { + List result = new ArrayList(); + + // Try the simple way first. + try { + InetAddress address = InetAddress.getLocalHost(); + if (!address.isLoopbackAddress()) { + result.add(address.getHostAddress()); + } + } catch (Throwable x) { + LOG.warn("Failed to resolve local IP address.", x); + } + + // Iterate through all network interfaces, looking for a suitable IP. + try { + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while (interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + Enumeration addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress addr = addresses.nextElement(); + if (addr instanceof Inet4Address && !addr.isLoopbackAddress()) { + result.add(addr.getHostAddress()); + } + } + } + } catch (Throwable x) { + LOG.warn("Failed to resolve local IP address.", x); + } + + if (result.isEmpty()) { + result.add("127.0.0.1"); + } + + return result; + } + + public static int randomInt(int min, int max) { + if (min >= max) { + return 0; + } + return min + RANDOM.nextInt(max - min); + } + + public static Iterable toIterable(final Enumeration e) { + return new Iterable() { + public Iterator iterator() { + return toIterator(e); + } + }; + } + + public static Iterator toIterator(final Enumeration e) { + return new Iterator() { + public boolean hasNext() { + return e.hasMoreElements(); + } + + public T next() { + return (T) e.nextElement(); + } + + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + public static List subList(List list, long offset, long max) { + return list.subList((int) offset, Math.min(list.size(), (int) (offset + max))); + } + + public static List toIntegerList(int[] values) { + if (values == null) { + return Collections.emptyList(); + } + List result = new ArrayList(values.length); + for (int value : values) { + result.add(value); + } + return result; + } + + public static int[] toIntArray(List values) { + if (values == null) { + return new int[0]; + } + int[] result = new int[values.size()]; + for (int i = 0; i < result.length; i++) { + result[i] = values.get(i); + } + return result; + } +} \ No newline at end of file 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 . + + 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/PremiumSettingsValidator.java b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PremiumSettingsValidator.java new file mode 100644 index 00000000..c1781da4 --- /dev/null +++ b/subsonic-main/src/main/java/net/sourceforge/subsonic/validator/PremiumSettingsValidator.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 . + + Copyright 2009 (C) Sindre Mehus + */ +package net.sourceforge.subsonic.validator; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +import net.sourceforge.subsonic.command.PremiumSettingsCommand; +import net.sourceforge.subsonic.controller.PremiumSettingsController; +import net.sourceforge.subsonic.service.SettingsService; + +/** + * Validator for {@link PremiumSettingsController}. + * + * @author Sindre Mehus + */ +public class PremiumSettingsValidator implements Validator { + private SettingsService settingsService; + + public boolean supports(Class clazz) { + return clazz.equals(PremiumSettingsCommand.class); + } + + public void validate(Object obj, Errors errors) { + PremiumSettingsCommand command = (PremiumSettingsCommand) obj; + + if (!settingsService.isLicenseValid(command.getLicenseInfo().getLicenseEmail(), command.getLicenseCode())) { + command.setSubmissionError(true); + errors.rejectValue("licenseCode", "premium.invalidlicense"); + } + } + + public void setSettingsService(SettingsService settingsService) { + this.settingsService = settingsService; + } +} 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..87a2b0f7 --- /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 . + + 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.isNewUser()) { + 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.isNewUser() || 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 100755 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. + *

+ * 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. + *

+ * The first row usually contains the names of the columns. + *

+ * 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 100755 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 %hh sequences to single characters, and + * convert plus to space. + * @param string A string that may contain + * + (plus) and + * %hh 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 100755 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 100755 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 + *

{
+     *    Method: "POST" (for example),
+     *    "Request-URI": "/" (for example),
+     *    "HTTP-Version": "HTTP/1.1" (for example)
+     * }
+ * A response header will contain + *
{
+     *    "HTTP-Version": "HTTP/1.1" (for example),
+     *    "Status-Code": "200" (for example),
+     *    "Reason-Phrase": "OK" (for example)
+     * }
+ * In addition, the other parameters in the header will be captured, using + * the HTTP field names as JSON names, so that
+     *    Date: Sun, 26 May 2002 18:06:04 GMT
+     *    Cookie: Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s
+     *    Cache-Control: no-cache
+ * become + *
{...
+     *    Date: "Sun, 26 May 2002 18:06:04 GMT",
+     *    Cookie: "Q=q2=PPEAsg--; B=677gi6ouf29bn&b=2&f=s",
+     *    "Cache-Control": "no-cache",
+     * ...}
+ * 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 + *
{
+     *    Method: "POST" (for example),
+     *    "Request-URI": "/" (for example),
+     *    "HTTP-Version": "HTTP/1.1" (for example)
+     * }
+ * A response header must contain + *
{
+     *    "HTTP-Version": "HTTP/1.1" (for example),
+     *    "Status-Code": "200" (for example),
+     *    "Reason-Phrase": "OK" (for example)
+     * }
+ * 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 100755 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 get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

+ * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

+ * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing bracket.
  • + *
  • The null value will be inserted when there + * is , (comma) elision.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • 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: + * { } [ ] / \ : , = ; # and if they do not look like numbers + * and if they are not the reserved words true, + * false, or null.
  • + *
  • Values can be separated by ; (semicolon) as + * well as by , (comma).
  • + *
+ + * @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 + * [ (left bracket) + * and ends with ] (right bracket). + * @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 + * separator 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. + *

+ * 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 [ (left bracket) and ending + * with ] (right bracket). + * @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. + *

+ * 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 100755 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 "); + } + 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 ' 0); + } + } else if (token == XML.QUEST) { + +// "); + } 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 <[ [ ]]> 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 <[ [ ]]> 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 <[ [ ]]> 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 <[ [ ]]> 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 = 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 '); + } 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 get and opt methods for + * accessing the values by name, and put methods for adding or + * replacing values by name. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the JSONObject.NULL + * 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 + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. + * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt 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. + *

+ * The put methods add or replace values in an object. For example, + *

myString = new JSONObject().put("JSON", "Hello, World!").toString();
+ * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. + * The constructors are more forgiving in the texts they will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • 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: + * { } [ ] / \ : , = ; # and if they do not look like numbers + * and if they are not the reserved words true, + * false, or null.
  • + *
  • Keys can be followed by = or => as well as + * by :.
  • + *
  • Values can be followed by ; (semicolon) as + * well as by , (comma).
  • + *
+ * @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 + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + 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 "get" or "is" 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 "get" or "is" 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 "getName", and + * if the result of calling object.getName() is "Larry Fine", + * then the JSONObject will contain "name": "Larry Fine". + * + * @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 { (left brace) and ending + * with } (right brace). + * @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 = '\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. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable + * representation of the object, beginning + * with { (left brace) and ending + * with } (right brace). + */ + 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. + *

+ * 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 { (left brace) and ending + * with } (right brace). + * @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. + *

+ * 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 { (left brace) and ending + * with } (right brace). + * @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. + * + *

+ * 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 { (left brace) and ending + * with } (right brace). + * @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. + *

+ * 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 { (left brace) and ending + * with } (right brace). + * @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. + *

+ * 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 JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), + * and JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString { + /** + * The toJSONString 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 100755 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. + *

+ * A JSONStringer instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting cascade style. For example,

+ * myString = new JSONStringer()
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject()
+ *     .toString();
which produces the string
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * 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. + *

+ * 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 null if there was a + * problem in the construction of the JSON text (such as the calls to + * array were not properly balanced with calls to + * endArray). + * @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 + * " (double quote) or + * ' (single quote). + * @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 100755 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. + *

+ * A JSONWriter instance provides a value method for appending + * values to the + * text, and a key + * method for adding keys before values in objects. There are array + * and endArray methods that make and bound array values, and + * object and endObject methods which make and bound + * object values. All of these methods return the JSONWriter instance, + * permitting a cascade style. For example,

+ * new JSONWriter(myWriter)
+ *     .object()
+ *         .key("JSON")
+ *         .value("Hello, World!")
+ *     .endObject();
which writes
+ * {"JSON":"Hello, World!"}
+ *

+ * The first method called must be array or object. + * 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. + *

+ * 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 + * endArray will be appended to this array. The + * endArray 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 + * array. + * @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 + * object. + * @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 + * endObject will be appended to this object. The + * endObject 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 true or the value + * false. + * @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: + *

+     * & (ampersand) is replaced by &amp;
+     * < (less than) is replaced by &lt;
+     * > (greater than) is replaced by &gt;
+     * " (double quote) is replaced by &quot;
+     * 
+ * @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(); + +// "); + 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 ' 0); + return false; + } else if (token == QUEST) { + +// "); + return false; + } else if (token == SLASH) { + +// Close 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 + * <[ [ ]]> 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 + + 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("'); + } else { + sb.append(toString(value, key)); + } + } + } else if ("".equals(value)) { + sb.append('<'); + sb.append(key); + sb.append("/>"); + +// Emit a new tag + + } else { + sb.append(toString(value, key)); + } + } + if (tagName != null) { + +// Emit the close tag + + 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 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 + ""; + } + } + } +} \ 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 ]]>. + * @throws JSONException If the ]]> 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: + * & ' > < ". + * @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 (< > / = ! ?) 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: / > = ! ? 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; + } + } + } +} diff --git a/subsonic-main/src/main/resources/ehcache.xml b/subsonic-main/src/main/resources/ehcache.xml new file mode 100644 index 00000000..ea04b885 --- /dev/null +++ b/subsonic-main/src/main/resources/ehcache.xml @@ -0,0 +1,286 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/subsonic-main/src/main/resources/log4j.properties b/subsonic-main/src/main/resources/log4j.properties new file mode 100644 index 00000000..4908ba27 --- /dev/null +++ b/subsonic-main/src/main/resources/log4j.properties @@ -0,0 +1,9 @@ +# Set root logger level to WARN and its only appender to A1. +log4j.rootLogger=WARN, A1 + +# A1 is set to be a ConsoleAppender. +log4j.appender.A1=org.apache.log4j.ConsoleAppender + +# A1 uses PatternLayout. +log4j.appender.A1.layout=org.apache.log4j.PatternLayout +log4j.appender.A1.layout.ConversionPattern=[%d{ISO8601}] %-5p %c - %m%n diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpg b/subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpg new file mode 100644 index 00000000..53f3fc64 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/controller/default_cover.jpg differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.png new file mode 100644 index 00000000..5ba5254a Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/All-Caps.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.png new file mode 100644 index 00000000..3d9f3f31 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Army-Officer.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.png new file mode 100644 index 00000000..6663d48d Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Beatnik.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.png new file mode 100644 index 00000000..b42954e9 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Clown.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.png new file mode 100644 index 00000000..08f58b83 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Commie-Pinko.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.png new file mode 100644 index 00000000..ea3a0b62 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Cool.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.png new file mode 100644 index 00000000..505c259e Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Drum.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.png new file mode 100644 index 00000000..d964b959 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Engineer.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.png new file mode 100644 index 00000000..05f5bb63 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Fire-Guitar.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.png new file mode 100644 index 00000000..1a8a528c Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Footballer.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.png new file mode 100644 index 00000000..7d25ea69 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Formal.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.png new file mode 100644 index 00000000..ccb1bf7f Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Forum-Flirt.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.png new file mode 100644 index 00000000..400b6196 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Gamer.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.png new file mode 100644 index 00000000..9ed06785 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Green-Boy.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.png new file mode 100644 index 00000000..90cc14de Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Headphones.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.png new file mode 100644 index 00000000..0c13e4f7 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Hopelessly-Addicted.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.png new file mode 100644 index 00000000..fd99ffdb Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Jekyll-And-Hyde.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.png new file mode 100644 index 00000000..9d5512ac Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Joker.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.png new file mode 100644 index 00000000..3bca8892 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Laugh.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.png new file mode 100644 index 00000000..0f85bf5c Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Linux-Zealot.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.png new file mode 100644 index 00000000..d5555c06 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Lurker.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.png new file mode 100644 index 00000000..78c498fe Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mac-Zealot.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.png new file mode 100644 index 00000000..0da7b6d3 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Mic.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.png new file mode 100644 index 00000000..dc4a5d71 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Moderator.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.png new file mode 100644 index 00000000..3bba4bcd Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Newbie.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.png new file mode 100644 index 00000000..211162d2 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/No-Dissent.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.png new file mode 100644 index 00000000..c47bf0af Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Performer.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.png new file mode 100644 index 00000000..4a67bead Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Push-My-Button.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.png new file mode 100644 index 00000000..1e520750 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ray-Of-Sunshine.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.png new file mode 100644 index 00000000..ec1716c5 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-1.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.png new file mode 100644 index 00000000..0ad9d241 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-2.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.png new file mode 100644 index 00000000..dd36bf0e Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-3.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.png new file mode 100644 index 00000000..d3a58f0b Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Red-Hot-Chili-Peppers-4.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.png new file mode 100644 index 00000000..bb7d377b Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Ringmaster.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.png new file mode 100644 index 00000000..e4d45068 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Rumor-Junkie.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.png new file mode 100644 index 00000000..b1373f34 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Sozzled-Surfer.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.png new file mode 100644 index 00000000..a9266ceb Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Statistician.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.png new file mode 100644 index 00000000..24cb3a04 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Study.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.png new file mode 100644 index 00000000..9dd9d0f0 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Tech-Support.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.png new file mode 100644 index 00000000..8b20b86b Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Guru.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.png new file mode 100644 index 00000000..d3381cf5 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/The-Referee.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.png new file mode 100644 index 00000000..0b7a621d Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Troll.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.png new file mode 100644 index 00000000..bcd25f1f Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Turntable.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.png new file mode 100644 index 00000000..6073d700 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Uptight.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.png new file mode 100644 index 00000000..9f448824 Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Vinyl.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.png b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.png new file mode 100644 index 00000000..070e5a3e Binary files /dev/null and b/subsonic-main/src/main/resources/net/sourceforge/subsonic/dao/schema/Windows-Zealot.png differ diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties new file mode 100644 index 00000000..cfb84548 --- /dev/null +++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_bg.properties @@ -0,0 +1,664 @@ +# +# Bulgarian localization. +# Author: Ivan Achev +# + +common.home = \u041D\u0430\u0447\u0430\u043B\u043E +common.back = \u041E\u0431\u0440\u0430\u0442\u043D\u043E +common.help = \u041F\u043E\u043C\u043E\u0449 +common.play = \u041F\u0443\u0441\u043D\u0438 +common.add = \u0414\u043E\u0431\u0430\u0432\u0438 +common.download = \u0421\u0432\u0430\u043B\u0438 +common.close = \u0417\u0430\u0442\u0432\u043E\u0440\u0438 +common.refresh = \u041E\u0431\u043D\u043E\u0432\u0438 +common.next = \u0421\u043B\u0435\u0434\u0432\u0430\u0449 +common.previous = \u041F\u0440\u0435\u0434\u0438\u0448\u0435\u043D +common.more = \u041E\u0449\u0435 +common.ok = OK +common.cancel = \u041E\u0442\u043C\u0435\u043D\u0438 +common.save = \u0417\u0430\u043F\u0430\u0437\u0438 +common.create = \u0421\u044A\u0437\u0434\u0430\u0439 +common.delete = \u0418\u0437\u0442\u0440\u0438\u0439 +common.unknown = (\u041D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u0435\u043D) +common.default = (\u041F\u043E \u043F\u043E\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043D\u0435) + +# login.jsp +login.username = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +login.password = \u041F\u0430\u0440\u043E\u043B\u0430 +login.login = \u0412\u0445\u043E\u0434 +login.remember = \u0417\u0430\u043F\u043E\u043C\u043D\u0438 \u043C\u0435 +login.logout = \u0418\u0437\u043B\u044F\u0437\u043E\u0445\u0442\u0435 \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u043E\u0442 \u0430\u043A\u0430\u0443\u043D\u0442\u0430. +login.error = \u0413\u0440\u0435\u0448\u0435\u043D \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B \u0438\u043B\u0438 \u043F\u0430\u0440\u043E\u043B\u0430. +login.insecure = {0} \u043D\u0435 \u0435 \u0437\u0430\u0449\u0438\u0442\u0435\u043D. \u041C\u043E\u043B\u044F \u0432\u043B\u0435\u0437\u0442\u0435 \u0441 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B "admin"
\u0438 \u043F\u0430\u0440\u043E\u043B\u0430 "admin", \u0438\u043B\u0438 \u0449\u0440\u0430\u043A\u043D\u0435\u0442\u0435 \u0442\u0443\u043A. \u0421\u043B\u0435\u0434 \u0442\u043E\u0432\u0430 \u0441\u043C\u0435\u043D\u0435\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u0430\u0442\u0430 \u043D\u0435\u0437\u0430\u0431\u0430\u0432\u043D\u043E. + +# accessDenied.jsp +accessDenied.title = \u0414\u043E\u0441\u0442\u044A\u043F \u043E\u0442\u043A\u0430\u0437\u0430\u043D +accessDenied.text = \u0421\u044A\u0436\u0430\u043B\u044F\u0432\u0430\u043C\u0435, \u043D\u044F\u043C\u0430\u0442\u0435 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0438\u0437\u0432\u044A\u0440\u0448\u0438\u0442\u0435 \u0442\u043E\u0432\u0430 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0435. + +# top.jsp +top.home = \u041D\u0430\u0447\u0430\u043B\u043E +top.now_playing = \u041F\u043B\u0435\u044A\u0440 +top.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 +top.status = \u0421\u0442\u0430\u0442\u0443\u0441 +top.podcast = \u041F\u043E\u0434\u043A\u0430\u0441\u0442 +top.more = \u041E\u0449\u0435 +top.help = \u041E\u0442\u043D\u043E\u0441\u043D\u043E +top.search = \u0422\u044A\u0440\u0441\u0435\u043D\u0435 +top.upgrade = \u041D\u0430\u043B\u0438\u0447\u043D\u0430 \u0435 \u043D\u043E\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044F. \u0421\u0432\u0430\u043B\u0438 {0} {1} \ + \u0442\u0443\u043A. +top.missing = \u041D\u044F\u043C\u0430 \u043D\u0430\u043B\u0438\u0447\u043D\u0438 \u043F\u0430\u043F\u043A\u0438 \u0441 \u043C\u0443\u0437\u0438\u043A\u0430. \u041C\u043E\u043B\u044F \u043F\u0440\u043E\u043C\u0435\u043D\u0435\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438\u0442\u0435. +top.logout = \u0418\u0437\u043B\u0435\u0437 {0} + +# left.jsp +left.statistics = {0} \u0438\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B\u0438
\ + {1} \u0430\u043B\u0431\u0443\u043C\u0438
\ + {2} \u043F\u0435\u0441\u043D\u0438
\ + {3} (~ {4} \u0447\u0430\u0441\u0430) +left.shortcut = \u0412\u0440\u044A\u0437\u043A\u0438 +left.radio = Internet TV/\u0420\u0430\u0434\u0438\u043E +left.allfolders = \u0412\u0441\u0438\u0447\u043A\u0438 \u043F\u0430\u043F\u043A\u0438 + +# playlist.jsp +playlist.stop = \u0421\u043F\u0440\u0438 +playlist.start = \u041F\u0443\u0441\u043D\u0438 +playlist.confirmclear = \u0418\u0437\u0442\u0440\u0438\u0432\u0430\u043D\u0435 \u043D\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430\u0442\u0430? +playlist.clear = \u0418\u0437\u0447\u0438\u0441\u0442\u0438 +playlist.shuffle = \u0420\u0430\u0437\u0431\u044A\u0440\u043A\u0430\u0439 +playlist.repeat_on = \u041F\u043E\u0432\u0442\u043E\u0440\u0435\u043D\u0438\u0435 +playlist.repeat_off = \u0411\u0435\u0437 \u043F\u043E\u0432\u0442\u043E\u0440\u0435\u043D\u0438\u0435 +playlist.undo = \u041E\u0442\u043C\u0435\u043D\u0438 +playlist.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 +playlist.more = \u041E\u0449\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F +playlist.more.playlist = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +playlist.more.sortbytrack = \u0421\u043E\u0440\u0442\u0438\u0440\u0430\u043D\u0435 \u043F\u043E \u043F\u0435\u0441\u0435\u043D +playlist.more.sortbyartist = \u0421\u043E\u0440\u0442\u0438\u0440\u0430\u043D\u0435 \u043F\u043E \u0438\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B +playlist.more.sortbyalbum = \u0421\u043E\u0440\u0442\u0438\u0440\u0430\u043D\u0435 \u043F\u043E \u0430\u043B\u0431\u0443\u043C +playlist.more.selection = \u041C\u0430\u0440\u043A\u0438\u0440\u0430\u043D\u0438 \u043F\u0435\u0441\u043D\u0438 +playlist.more.selectall = \u041C\u0430\u0440\u043A\u0438\u0440\u0430\u0439 \u0432\u0441\u0438\u0447\u043A\u0438 +playlist.more.selectnone = \u041E\u0442\u043C\u0430\u0440\u043A\u0438\u0440\u0430\u0439 +playlist.getflash = \u0421\u0432\u0430\u043B\u0435\u0442\u0435 \u0441\u0438 Flash \u043F\u043B\u0435\u044A\u0440 +playlist.save = \u0417\u0430\u043F\u0430\u0437\u0438 +playlist.append = \u0414\u043E\u0431\u0430\u0432\u0438 \u0432 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +playlist.remove = \u041F\u0440\u0435\u043C\u0430\u0445\u043D\u0438 +playlist.up = \u041D\u0430\u0433\u043E\u0440\u0435 +playlist.down = \u041D\u0430\u0434\u043E\u043B\u0443 +playlist.empty = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430\u0442\u0430 \u0435 \u043F\u0440\u0430\u0437\u043D\u0430 + +# videoPlayer.jsp +videoPlayer.getflash = \u041C\u043E\u043B\u044F \u0438\u043D\u0441\u0442\u0430\u043B\u0438\u0440\u0430\u0439\u0442\u0435 \u0441\u0438 Flash \u043F\u043B\u0435\u044A\u0440 +videoPlayer.popout = \u041E\u0442\u0432\u043E\u0440\u0438 \u0432 \u043D\u043E\u0432 \u043F\u0440\u043E\u0437\u043E\u0440\u0435\u0446 + +# status.jsp +status.title = \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430 +status.type = \u0422\u0438\u043F +status.stream = \u041F\u043E\u0442\u043E\u043A +status.download = \u0421\u0432\u0430\u043B\u0435\u043D\u0438 +status.upload = \u041A\u0430\u0447\u0435\u043D\u0438 +status.player = \u041F\u043B\u0435\u044A\u0440 +status.user = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +status.current = \u0422\u0435\u043A\u0443\u0449 \u0444\u0430\u0439\u043B +status.transmitted = \u0418\u0437\u043F\u0440\u0430\u0442\u0435\u043D\u0438 +status.bitrate = \u0411\u0438\u0442\u0440\u0435\u0439\u0442 (Kbps) + +# search.jsp +search.title = \u0422\u044A\u0440\u0441\u0435\u043D\u0435 +search.query = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B, \u0430\u043B\u0431\u0443\u043C \u0438\u043B\u0438 \u0438\u043C\u0435 \u043D\u0430 \u043F\u0435\u0441\u0435\u043D +search.search = \u0422\u044A\u0440\u0441\u0438 +search.index = \u0412 \u043C\u043E\u043C\u0435\u043D\u0442\u0430 \u0441\u0435 \u0438\u0437\u0432\u044A\u0440\u0448\u0432\u0430 \u0438\u043D\u0434\u0435\u043A\u0441\u0438\u0440\u0430\u043D\u0435 \u043E\u0442 \u0442\u044A\u0440\u0441\u0430\u0447\u043A\u0430\u0442\u0430. \u041C\u043E\u043B\u044F \u043E\u043F\u0438\u0442\u0430\u0439\u0442\u0435 \u043E\u0442\u043D\u043E\u0432\u043E \u043F\u043E-\u043A\u044A\u0441\u043D\u043E. +search.hits.none = \u041D\u044F\u043C\u0430 \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438 \u0441\u044A\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u044F. +search.hits.more = \u041E\u0449\u0435 +search.hits.artists = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B\u0438 +search.hits.albums = \u0410\u043B\u0431\u0443\u043C\u0438 +search.hits.songs = \u041F\u0435\u0441\u043D\u0438 + +# gettingStarted.jsp +gettingStarted.title = \u0414\u0430 \u0437\u0430\u043F\u043E\u0447\u0432\u0430\u043C\u0435 +gettingStarted.text =

\u0414\u043E\u0431\u0440\u0435 \u0434\u043E\u0448\u043B\u0438 \u0432 Subsonic! \u0422\u0440\u044F\u0431\u0432\u0430\u0442 \u0441\u0430\u043C\u043E \u043E\u0449\u0435 \u043D\u044F\u043A\u043E\u043B\u043A\u043E \u0431\u044A\u0440\u0437\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438, \u043F\u0440\u043E\u0441\u0442\u043E \u0441\u043B\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0441\u0442\u044A\u043F\u043A\u0438\u0442\u0435 \u043E\u043F\u0438\u0441\u0430\u043D\u0438 \u043F\u043E-\u0434\u043E\u043B\u0443.
\ + \u041D\u0430\u0442\u0438\u0441\u043D\u0435\u0442\u0435 \u0431\u0443\u0442\u043E\u043D\u0430 "\u041D\u0430\u0447\u0430\u043B\u043E" \u0432 \u043B\u0435\u043D\u0442\u0430\u0442\u0430 \u0433\u043E\u0440\u0435, \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0432\u044A\u0440\u043D\u0435\u0442\u0435 \u043F\u043E \u0432\u0441\u044F\u043A\u043E \u0432\u0440\u0435\u043C\u0435 \u043A\u044A\u043C \u0442\u043E\u0437\u0438 \u0435\u043A\u0440\u0430\u043D.

\ +

\u0417\u0430 \u043F\u043E\u0432\u0435\u0447\u0435 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F, \u043C\u043E\u043B\u044F \u043F\u0440\u043E\u0447\u0435\u0442\u0435\u0442\u0435 \u0420\u044A\u043A\u043E\u0432\u043E\u0434\u0441\u0442\u0432\u043E\u0442\u043E \u0437\u0430 \u043D\u043E\u0432\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438.

+gettingStarted.step1.title = \u041F\u0440\u043E\u043C\u044F\u043D\u0430 \u043D\u0430 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0441\u043A\u0430\u0442\u0430 \u043F\u0430\u0440\u043E\u043B\u0430. +gettingStarted.step1.text = \u0417\u0430\u0449\u0438\u0442\u0435\u0442\u0435 \u0432\u0430\u0448\u0438\u044F\u0442 \u0441\u044A\u0440\u0432\u044A\u0440 \u043A\u0430\u0442\u043E \u0441\u043C\u0435\u043D\u0438\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u0430\u0442\u0430 \u043F\u043E \u043F\u043E\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043D\u0435 \u0441 \u0432\u0430\u0448\u0430 \u043F\u0430\u0440\u043E\u043B\u0430 \u0437\u0430 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440\u0441\u043A\u0438\u044F \u043F\u0440\u043E\u0444\u0438\u043B. \ + \u041C\u043E\u0436\u0435\u0442\u0435 \u0441\u044A\u0449\u043E \u0442\u0430\u043A\u0430 \u0434\u0430 \u0441\u044A\u0437\u0434\u0430\u0432\u0430\u0442\u0435 \u043D\u043E\u0432\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u0438 \u043F\u0440\u043E\u0444\u0438\u043B\u0438 \u0441 \u0440\u0430\u0437\u043B\u0438\u0447\u043D\u0438 \u043F\u0440\u0430\u0432\u0430. +gettingStarted.step2.title = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043D\u0430 \u043F\u0430\u043F\u043A\u0438\u0442\u0435 \u0441 \u043C\u0443\u0437\u0438\u043A\u0430. +gettingStarted.step2.text = \u0422\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u043A\u044A\u0434\u0435 \u0441\u0435 \u043D\u0430\u043C\u0438\u0440\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u043C\u0443\u0437\u0438\u043A\u0430. +gettingStarted.step3.title = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043D\u0430 \u043C\u0440\u0435\u0436\u0430\u0442\u0430. +gettingStarted.step3.text = \u041D\u044F\u043A\u043E\u0438 \u043F\u043E\u043B\u0435\u0437\u043D\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0430\u043A\u043E \u0438\u0441\u043A\u0430\u0442\u0435 \u0434\u0430 \u0441\u043B\u0443\u0448\u0430\u0442\u0435 \u0432\u0430\u0448\u0430\u0442\u0430 \u043C\u0443\u0437\u0438\u043A\u0430 \u0432 \u0418\u043D\u0442\u0435\u0440\u043D\u0435\u0442, \u043A\u044A\u0434\u0435\u0442\u043E \u0438 \u0434\u0430 \u0441\u0435 \u043D\u0430\u043C\u0438\u0440\u0430\u0442\u0435 \ + \u0438\u043B\u0438 \u0434\u0430 \u044F \u0441\u043F\u043E\u0434\u0435\u043B\u044F\u0442\u0435 \u0441 \u043F\u0440\u0438\u044F\u0442\u0435\u043B\u0438 \u0438 \u0441\u0435\u043C\u0435\u0439\u0441\u0442\u0432\u043E\u0442\u043E. \u0412\u0437\u0435\u043C\u0435\u0442\u0435 \u0432\u0430\u0448 \u043B\u0438\u0447\u0435\u043D \u0432\u0430\u0448\u0435\u0442\u043E\u0438\u043C\u0435.subsonic.org \ + \u0438\u043D\u0442\u0435\u0440\u043D\u0435\u0442 \u0430\u0434\u0440\u0435\u0441. +gettingStarted.hide = \u041D\u0435 \u043F\u043E\u043A\u0430\u0437\u0432\u0430\u0439 \u0442\u043E\u0432\u0430 \u043F\u043E\u0432\u0435\u0447\u0435 +gettingStarted.hidealert = \u0417\u0430 \u0434\u0430 \u043F\u043E\u043A\u0430\u0436\u0435\u0442\u0435 \u0442\u0430\u0437\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 \u043E\u0442\u043D\u043E\u0432\u043E, \u043E\u0442\u0432\u043E\u0440\u0435\u0442\u0435 \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 > \u041E\u0431\u0449\u0438. + +# home.jsp +home.random.title = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u0438 +home.newest.title = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u0435\u043D\u0438 +home.highest.title = \u041F\u043E \u0440\u0435\u0439\u0442\u0438\u043D\u0433 +home.frequent.title = \u041D\u0430\u0439-\u0441\u043B\u0443\u0448\u0430\u043D\u0438 +home.recent.title = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u043E \u0441\u043B\u0443\u0448\u0430\u043D\u0438 +home.users.title = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438 +home.random.text = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u0438 \u0430\u043B\u0431\u0443\u043C\u0438 +home.newest.text = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u043E \u0434\u043E\u0431\u0430\u0432\u0435\u043D\u0438 \u0430\u043B\u0431\u0443\u043C\u0438 +home.highest.text = \u041D\u0430\u0439-\u0432\u0438\u0441\u043E\u043A\u043E \u043E\u0446\u0435\u043D\u0435\u043D\u0438 \u0430\u043B\u0431\u0443\u043C\u0438 +home.frequent.text = \u041D\u0430\u0439-\u0447\u0435\u0441\u0442\u043E \u0441\u043B\u0443\u0448\u0430\u043D\u0438 \u0430\u043B\u0431\u0443\u043C\u0438 +home.recent.text = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u043E \u0441\u043B\u0443\u0448\u0430\u043D\u0438 \u0430\u043B\u0431\u0443\u043C\u0438 +home.users.text = \u0421\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043A\u0430 \u0437\u0430 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438\u0442\u0435 +home.scan = \u0422\u0430\u0437\u0438 \u043F\u0430\u043F\u043A\u0430 \u0441\u0435 \u0441\u043A\u0430\u043D\u0438\u0440\u0430 \u0432 \u043C\u043E\u043C\u0435\u043D\u0442\u0430. \u041D\u044F\u043A\u043E\u0438 \u0444\u0443\u043D\u043A\u0446\u0438\u0438 \u043D\u0435 \u0441\u0430 \u0434\u043E\u0441\u0442\u044A\u043F\u043D\u0438 \u043E\u0449\u0435. +home.albums = \u0410\u043B\u0431\u0443\u043C\u0438 {0} - {1} +home.playcount = \u041F\u0440\u043E\u0441\u043B\u0443\u0448\u0430\u043D\u0438 {0} \u043F\u0435\u0441\u043D\u0438 +home.lastplayed = \u0421\u043B\u0443\u0448\u0430\u043D\u0430 {0} +home.created = \u0421\u044A\u0437\u0434\u0430\u0434\u0435\u043D {0} +home.chart.total = \u041E\u0431\u0449\u043E (MB) +home.chart.stream = \u041F\u043E\u0442\u043E\u043A (MB) +home.chart.download = \u0421\u0432\u0430\u043B\u0435\u043D\u0438 (MB) +home.chart.upload = \u041A\u0430\u0447\u0435\u043D\u0438 (MB) + +# more.jsp +more.title = \u041E\u0449\u0435 \u0444\u0443\u043D\u043A\u0446\u0438\u0438 +more.random.title = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +more.random.text = \u0421\u044A\u0437\u0434\u0430\u0432\u0430\u043D\u0435 \u043D\u0430 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 \u0441 +more.random.songs = {0} \u043F\u0435\u0441\u043D\u0438 +more.random.auto = \u041E\u0449\u0435 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u0438 \u043F\u0435\u0441\u043D\u0438, \u043A\u043E\u0433\u0430\u0442\u043E \u0441\u0432\u044A\u0440\u0448\u0430\u0442 \u043F\u0435\u0441\u043D\u0438\u0442\u0435 \u0432 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430\u0442\u0430. +more.random.ok = OK +more.random.genre = \u043E\u0442 \u0436\u0430\u043D\u0440 +more.random.anygenre = \u0411\u0435\u0437 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 +more.random.year = \u0438 \u0433\u043E\u0434\u0438\u043D\u0430 +more.random.anyyear = \u0411\u0435\u0437 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 +more.random.folder = \u043E\u0442 \u043F\u0430\u043F\u043A\u0430 +more.random.anyfolder = \u0411\u0435\u0437 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435 +more.apps.title = Subsonic \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F +more.apps.text =

Subsonic \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F\u0442\u0430 \u0441\u0430 \u043F\u0440\u0435\u0434\u043D\u0430\u0437\u043D\u0430\u0447\u0435\u043D\u0438 \u0437\u0430 Android, iPhone, \ + Windows Phone \u0438 AIR.

+more.mobile.title = \u041C\u043E\u0431\u0438\u043B\u043D\u0438 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430 +more.mobile.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0432\u0430\u0442\u0435 {0} \u043E\u0442 \u0432\u0441\u0435\u043A\u0438 WAP-\u0441\u044A\u0432\u043C\u0435\u0441\u0442\u0438\u043C \u043C\u043E\u0431\u0438\u043B\u0435\u043D \u0442\u0435\u043B\u0435\u0444\u043E\u043D \u0438\u043B\u0438 PDA.
\ + \u041F\u0440\u043E\u0441\u0442\u043E \u043F\u043E\u0441\u0435\u0442\u0435\u0442\u0435 \u0441\u043B\u0435\u0434\u043D\u0438\u044F URL \u0430\u0434\u0440\u0435\u0441 \u043E\u0442 \u0432\u0430\u0448\u0438\u044F \u0442\u0435\u043B\u0435\u0444\u043E\u043D: http://yourhostname/wap

\ +

\u0422\u043E\u0432\u0430 \u0438\u0437\u0438\u0441\u043A\u0432\u0430 \u043D\u0430\u043B\u0438\u0447\u0435\u043D \u0434\u043E\u0441\u0442\u044A\u043F \u0434\u043E \u0432\u0430\u0448\u0438\u044F \u0441\u044A\u0440\u0432\u044A\u0440 \u043F\u043E \u0418\u043D\u0442\u0435\u0440\u043D\u0435\u0442.

+more.podcast.title = \u041F\u043E\u0434\u043A\u0430\u0441\u0442\u0438\u043D\u0433 +more.podcast.text =

\u0417\u0430\u043F\u0430\u0437\u0435\u043D\u0438\u0442\u0435 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0438 \u0441\u0430 \u0434\u043E\u0441\u0442\u044A\u043F\u043D\u0438 \u0438 \u043A\u0430\u0442\u043E \u043F\u043E\u0434\u043A\u0430\u0441\u0442.
\ + \u0418\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0439\u0442\u0435 \u0441\u043B\u0435\u0434\u043D\u0438\u044F \u0430\u0434\u0440\u0435\u0441 \u0432\u044A\u0432 \u0432\u0430\u0448\u0435\u0442\u043E \u043F\u043E\u0434\u043A\u0430\u0441\u0442 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u043E: http://yourhostname/podcast, \ + \u0438\u043B\u0438 \u0449\u0440\u0430\u043A\u043D\u0435\u0442\u0435 \u0442\u0443\u043A.

+more.upload.title = \u041A\u0430\u0447\u0432\u0430\u043D\u0435 \u043D\u0430 \u0444\u0430\u0439\u043B +more.upload.source = \u0418\u0437\u0431\u043E\u0440 \u043D\u0430 \u0444\u0430\u0439\u043B +more.upload.target = \u041A\u0430\u0447\u0432\u0430\u043D\u0435 \u0432 +more.upload.browse = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 +more.upload.ok = \u041A\u0430\u0447\u0438 +more.upload.unzip = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u0440\u0430\u0437\u0430\u0440\u0445\u0438\u0432\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 zip-\u0444\u0430\u0439\u043B\u043E\u0432\u0435. +more.upload.progress = % \u0437\u0430\u0432\u044A\u0440\u0448\u0435\u043D\u0438. \u041C\u043E\u043B\u044F \u0438\u0437\u0447\u0430\u043A\u0430\u0439\u0442\u0435... + +# upload.jsp +upload.title = \u041A\u0430\u0447\u0432\u0430\u043D\u0435 \u043D\u0430 \u0444\u0430\u0439\u043B +upload.success = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u043A\u0430\u0447\u0435\u043D {0} +upload.empty = \u041D\u044F\u043C\u0430 \u0438\u0437\u0431\u0440\u0430\u043D\u0438 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0437\u0430 \u043A\u0430\u0447\u0432\u0430\u043D\u0435. +upload.failed = \u041A\u0430\u0447\u0432\u0430\u043D\u0435\u0442\u043E \u0435 \u043D\u0435\u0443\u0441\u043F\u0435\u0448\u043D\u043E \u043F\u043E\u0440\u0430\u0434\u0438 \u0441\u043B\u0435\u0434\u043D\u0430\u0442\u0430 \u0433\u0440\u0435\u0448\u043A\u0430:
"{0}" +upload.unzipped = \u0420\u0430\u0437\u0430\u0440\u0445\u0438\u0432\u0438\u0440\u0430\u043D {0} + +# help.jsp +help.title = \u0418\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F \u0437\u0430 {0} +help.upgrade = \u0412\u043D\u0438\u043C\u0430\u043D\u0438\u0435! \u041D\u0430\u043B\u0438\u0447\u043D\u0430 \u0435 \u043D\u043E\u0432\u0430 \u0432\u0435\u0440\u0441\u0438\u044F. \u0421\u0432\u0430\u043B\u0438 \u043E\u0442 {0} {1} \ + \u0442\u0443\u043A. +help.version.title = \u0412\u0435\u0440\u0441\u0438\u044F +help.builddate.title = \u041E\u0442 \u0434\u0430\u0442\u0430 +help.server.title = \u0421\u044A\u0440\u0432\u044A\u0440 +help.license.title = \u0423\u0441\u043B\u043E\u0432\u0438\u044F \u0437\u0430 \u043F\u043E\u043B\u0437\u0432\u0430\u043D\u0435 +help.license.text = {0} \u0435 \u0431\u0435\u0437\u043F\u043B\u0430\u0442\u0435\u043D \u0441\u043E\u0444\u0442\u0443\u0435\u0440, \u0440\u0430\u0437\u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u044F\u0432\u0430\u043D \u0447\u0440\u0435\u0437 GPL \u043B\u0438\u0446\u0435\u043D\u0437 \u0441 \u043E\u0442\u0432\u043E\u0440\u0435\u043D \u043A\u043E\u0434. \ + {0} \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u043B\u0438\u0446\u0435\u043D\u0437\u0438\u0440\u0430\u043D\u0438 \u0431\u0438\u0431\u043B\u0438\u043E\u0442\u0435\u043A\u0438 \u043D\u0430 \u0442\u0440\u0435\u0442\u0438 \u0441\u0442\u0440\u0430\u043D\u0438. \u041C\u043E\u043B\u044F \u043E\u0431\u044A\u0440\u043D\u0435\u0442\u0435 \u0432\u043D\u0438\u043C\u0430\u043D\u0438\u0435, \u0447\u0435 {0} \u043D\u0435 \u0435 \ + \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \u0437\u0430 \u043D\u0435\u043B\u0435\u0433\u0430\u043B\u043D\u043E \u0440\u0430\u0437\u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0435\u043D\u0438\u0435 \u043D\u0430 \u0437\u0430\u0449\u0438\u0442\u0435\u043D\u0438 \u0441 \u0430\u0432\u0442\u043E\u0440\u0441\u043A\u043E \u043F\u0440\u0430\u0432\u043E \u043C\u0430\u0442\u0435\u0440\u0438\u0430\u043B\u0438. \u0412\u0438\u043D\u0430\u0433\u0438 \u0441\u043F\u0430\u0437\u0432\u0430\u0439\u0442\u0435 \u0437\u0430\u043A\u043E\u043D\u0438\u0442\u0435, \u0441\u043F\u0435\u0446\u0438\u0444\u0438\u0447\u043D\u0438 \u0437\u0430 \u0432\u0430\u0448\u0430\u0442\u0430 \u0434\u044A\u0440\u0436\u0430\u0432\u0430. +help.homepage.title = \u041E\u0444\u0438\u0446\u0438\u0430\u043B\u0435\u043D \u0441\u0430\u0439\u0442 +help.forum.title = \u0424\u043E\u0440\u0443\u043C +help.shop.title = \u041C\u0430\u0433\u0430\u0437\u0438\u043D +help.contact.title = \u041A\u043E\u043D\u0442\u0430\u043A\u0442 +help.contact.text = {0} \u0435 \u0441\u044A\u0437\u0434\u0430\u0434\u0435\u043D \u0438 \u0441\u0435 \u043F\u043E\u0434\u0434\u044A\u0440\u0436\u0430 \u043E\u0442 Sindre Mehus \ + (sindre@activeobjects.no). \ + \u0410\u043A\u043E \u0438\u043C\u0430\u0442\u0435 \u0432\u044A\u043F\u0440\u043E\u0441\u0438, \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0438 \u0438\u043B\u0438 \u043F\u0440\u0435\u0434\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0437\u0430 \u043F\u043E\u0434\u043E\u0431\u0440\u0435\u043D\u0438\u044F, \u043C\u043E\u043B\u044F \u043F\u043E\u0441\u0435\u0442\u0435\u0442\u0435 \ + Subsonic \u0424\u043E\u0440\u0443\u043C. +help.log = \u041B\u043E\u0433 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 +help.logfile = \u0426\u0435\u043B\u0438\u044F\u0442 \u043B\u043E\u0433 \u0435 \u0437\u0430\u043F\u0430\u0437\u0435\u043D \u0432 {0}. + +# settingsHeader.jsp +settingsheader.title = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 +settingsheader.general = \u041E\u0431\u0449\u0438 +settingsheader.advanced = \u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043D\u0438 +settingsheader.personal = \u0412\u0438\u0434 +settingsheader.musicFolder = \u041F\u0430\u043F\u043A\u0438 \u0441 \u043C\u0443\u0437\u0438\u043A\u0430 +settingsheader.internetRadio = Internet TV/\u0420\u0430\u0434\u0438\u043E +settingsheader.podcast = \u041F\u043E\u0434\u043A\u0430\u0441\u0442 +settingsheader.player = \u041F\u043B\u0435\u044A\u0440\u0438 +settingsheader.network = \u041C\u0440\u0435\u0436\u0430 +settingsheader.transcoding = \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 +settingsheader.user = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438 +settingsheader.search = \u0422\u044A\u0440\u0441\u0430\u0447\u043A\u0430 +settingsheader.coverArt = \u041E\u0431\u043B\u043E\u0436\u043A\u0438 +settingsheader.password = \u041F\u0430\u0440\u043E\u043B\u0430 + +# generalSettings.jsp +generalsettings.playlistfolder = \u041F\u0430\u043F\u043A\u0430 \u0437\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0438 +generalsettings.musicmask = \u0424\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u043C\u0443\u0437\u0438\u043A\u0430 +generalsettings.videomask = \u0424\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u0432\u0438\u0434\u0435\u043E +generalsettings.coverartmask = \u0424\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0438 +generalsettings.index = \u0418\u043D\u0434\u0435\u043A\u0441 +generalsettings.ignoredarticles = \u0421\u0438\u043C\u0432\u043E\u043B\u0438 \u0437\u0430 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u0430\u043D\u0435 +generalsettings.shortcuts = \u0412\u0440\u044A\u0437\u043A\u0438 +generalsettings.showgettingstarted = \u041F\u043E\u043A\u0430\u0437\u0432\u0430\u0439 "\u0414\u0430 \u0437\u0430\u043F\u043E\u0447\u0432\u0430\u043C\u0435" \u043F\u0440\u0438 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u043D\u0435 +generalsettings.welcometitle = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043E \u0437\u0430\u0433\u043B\u0430\u0432\u0438\u0435 +generalsettings.welcomesubtitle = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043E \u043F\u043E\u0434\u0437\u0430\u0433\u043B\u0430\u0432\u0438\u0435 +generalsettings.welcomemessage = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043E \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435 +generalsettings.loginmessage = \u0421\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u0432\u0445\u043E\u0434 +generalsettings.language = \u0421\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u0435\u043D \u0435\u0437\u0438\u043A +generalsettings.theme = \u0421\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u043D\u0430 \u0442\u0435\u043C\u0430 + +# advancedSettings.jsp +advancedsettings.downsamplecommand = \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0437\u0430 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 +advancedsettings.coverartlimit = \u041B\u0438\u043C\u0438\u0442 \u0437\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0438
(0 = \u0411\u0435\u0437 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435)
+advancedsettings.downloadlimit = \u041B\u0438\u043C\u0438\u0442 \u0437\u0430 \u0441\u0432\u0430\u043B\u044F\u043D\u0435 (Kbps)
(0 = \u0411\u0435\u0437 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435)
+advancedsettings.uploadlimit = \u041B\u0438\u043C\u0438\u0442 \u0437\u0430 \u043A\u0430\u0447\u0432\u0430\u043D\u0435 (Kbps)
(0 = \u0411\u0435\u0437 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435)
+advancedsettings.streamport = \u041F\u043E\u0440\u0442 \u0437\u0430 \u043D\u0435\u043A\u0440\u0438\u043F\u0442\u0438\u0440\u0430\u043D (SSL) \u043F\u043E\u0442\u043E\u043A
(0 = \u0418\u0437\u043A\u043B\u044E\u0447\u0435\u043D)
+advancedsettings.ldapenabled = \u0412\u043A\u043B\u044E\u0447\u0438 LDAP \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 +advancedsettings.ldapurl = LDAP URL +advancedsettings.ldapsearchfilter = LDAP \u0444\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u0442\u044A\u0440\u0441\u0435\u043D\u0435 +advancedsettings.ldapmanagerdn = LDAP \u043C\u0435\u043D\u0438\u0434\u0436\u044A\u0440 DN
(\u041D\u0435\u0437\u0430\u0434\u044A\u043B\u0436\u0438\u0442\u0435\u043B\u043D\u043E)
+advancedsettings.ldapmanagerpassword = \u041F\u0430\u0440\u043E\u043B\u0430 +advancedsettings.ldapautoshadowing = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u0441\u044A\u0437\u0434\u0430\u0432\u0430\u0439 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438 \u0432 {0} + +# personalSettings.jsp +personalsettings.title = \u0412\u044A\u043D\u0448\u0435\u043D \u0432\u0438\u0434 \u0437\u0430 {0} +personalsettings.language = \u0415\u0437\u0438\u043A +personalsettings.theme = \u0422\u0435\u043C\u0430 +personalsettings.display = \u041F\u043E\u043A\u0430\u0437\u0432\u0430\u0439 +personalsettings.browse = \u041E\u0441\u043D\u043E\u0432\u0435\u043D +personalsettings.playlist = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +personalsettings.tracknumber = \u041F\u0435\u0441\u0435\u043D # +personalsettings.artist = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B +personalsettings.album = \u0410\u043B\u0431\u0443\u043C +personalsettings.genre = \u0416\u0430\u043D\u0440 +personalsettings.year = \u0413\u043E\u0434\u0438\u043D\u0430 +personalsettings.bitrate = \u0411\u0438\u0442\u0440\u0435\u0439\u0442 +personalsettings.duration = \u041F\u0440\u043E\u0434\u044A\u043B\u0436\u0438\u0442\u0435\u043B\u043D\u043E\u0441\u0442 +personalsettings.format = \u0424\u043E\u0440\u043C\u0430\u0442 +personalsettings.filesize = \u0420\u0430\u0437\u043C\u0435\u0440 +personalsettings.captioncutoff = \u0417\u0430\u0433\u043B\u0430\u0432\u0438\u044F (\u0431\u0440\u043E\u0439 \u0431\u0443\u043A\u0432\u0438)) +personalsettings.partymode = \u041E\u043B\u0435\u043A\u043E\u0442\u0435\u043D \u0440\u0435\u0436\u0438\u043C +personalsettings.shownowplaying = \u041F\u043E\u043A\u0430\u0437\u0432\u0430\u0439 \u043A\u0430\u043A\u0432\u043E \u0441\u043B\u0443\u0448\u0430\u0442 \u0434\u0440\u0443\u0433\u0438\u0442\u0435 +personalsettings.nowplayingallowed = \u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u043D\u0430 \u043E\u0441\u0442\u0430\u043D\u0430\u043B\u0438\u0442\u0435 \u0434\u0430 \u0432\u0438\u0436\u0434\u0430\u0442 \u043A\u0430\u043A\u0432\u043E \u0441\u043B\u0443\u0448\u0430\u043C \u0430\u0437 +personalsettings.showchat = \u041F\u043E\u043A\u0430\u0437\u0432\u0430\u0439 \u0447\u0430\u0442 \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u044F +personalsettings.finalversionnotification = \u0423\u0432\u0435\u0434\u043E\u043C\u044F\u0432\u0430\u0439 \u043C\u0435 \u0437\u0430 \u043D\u043E\u0432\u0438 \u0432\u0435\u0440\u0441\u0438\u0438 +personalsettings.betaversionnotification = \u0423\u0432\u0435\u0434\u043E\u043C\u044F\u0432\u0430\u0439 \u043C\u0435 \u0437\u0430 \u043D\u043E\u0432\u0438 \u0431\u0435\u0442\u0430 \u0432\u0435\u0440\u0441\u0438\u0438 +personalsettings.lastfmenabled = \u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u0439 \u0442\u043E\u0432\u0430, \u043A\u043E\u0435\u0442\u043E \u0441\u043B\u0443\u0448\u0430\u043C \u0432 Last.fm +personalsettings.lastfmusername = Last.fm \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +personalsettings.lastfmpassword = Last.fm \u043F\u0430\u0440\u043E\u043B\u0430 +personalsettings.avatar.title = \u041B\u0438\u0447\u043D\u0430 \u0441\u043D\u0438\u043C\u043A\u0430 +personalsettings.avatar.none = \u0411\u0435\u0437 \u0441\u043D\u0438\u043C\u043A\u0430 +personalsettings.avatar.custom = \u041C\u043E\u044F \u0441\u043D\u0438\u043C\u043A\u0430 +personalsettings.avatar.changecustom = \u0418\u0437\u0431\u043E\u0440 \u043D\u0430 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D\u0430 \u0441\u043D\u0438\u043C\u043A\u0430 +personalsettings.avatar.upload = \u041A\u0430\u0447\u0438 +personalsettings.avatar.courtesy = \u0418\u043A\u043E\u043D\u043A\u0438\u0442\u0435 \u0441\u0430 \u043F\u0440\u0435\u0434\u043E\u0441\u0442\u0430\u0432\u0435\u043D\u0438 \u043E\u0442 Afterglow, \ + Aha-Soft, \ + Icons-Land, and \ + Iconshock + +# avatarUploadResult.jsp +avataruploadresult.title = \u041F\u0440\u043E\u043C\u044F\u043D\u0430 \u043D\u0430 \u043B\u0438\u0447\u043D\u0430\u0442\u0430 \u0441\u043D\u0438\u043C\u043A\u0430 +avataruploadresult.success = \u0423\u0441\u043F\u0435\u0448\u043D\u043E \u043A\u0430\u0447\u0435\u043D\u0430 \u0441\u043D\u0438\u043C\u043A\u0430 "{0}". +avataruploadresult.failure = \u041A\u0430\u0447\u0432\u0430\u043D\u0435\u0442\u043E \u0435 \u043D\u0435\u0443\u0441\u043F\u0435\u0448\u043D\u043E. \u0412\u0438\u0436\u0442\u0435 \u0434\u043E\u043A\u043B\u0430\u0434\u0430 \u0437\u0430 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438. + +# passwordSettings.jsp +passwordsettings.title = \u0421\u043C\u044F\u043D\u0430 \u043D\u0430 \u043F\u0430\u0440\u043E\u043B\u0430 \u043D\u0430 {0} + +# musicFolderSettings.jsp +musicfoldersettings.path = \u041F\u0430\u043F\u043A\u0430 +musicfoldersettings.name = \u0418\u043C\u0435 +musicfoldersettings.enabled = \u0410\u043A\u0442\u0438\u0432\u043D\u0430 +musicfoldersettings.add = \u0414\u043E\u0431\u0430\u0432\u0438 \u043F\u0430\u043F\u043A\u0430 +musicfoldersettings.nopath = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u043F\u0430\u043F\u043A\u0430. + +# networkSettings.jsp +networksettings.text = \u0418\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0439\u0442\u0435 \u043E\u043F\u0446\u0438\u0438\u0442\u0435 \u043F\u043E-\u0434\u043E\u043B\u0443 \u0437\u0430 \u0434\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u0435 \u043D\u0430\u0447\u0438\u043D\u0430 \u0437\u0430 \u0434\u043E\u0441\u0442\u044A\u043F \u0434\u043E \u0432\u0430\u0448\u0438\u044F Subsonic \u0441\u044A\u0440\u0432\u044A\u0440 \u043F\u0440\u0435\u0437 Internet.
\ + \u0410\u043A\u043E \u0438\u043C\u0430\u0442\u0435 \u043F\u0440\u043E\u0431\u043B\u0435\u043C\u0438, \u043C\u043E\u043B\u044F \u043F\u0440\u043E\u0447\u0435\u0442\u0435\u0442\u0435 \u0420\u044A\u043A\u043E\u0432\u043E\u0434\u0441\u0442\u0432\u043E\u0442\u043E \u0437\u0430 \u043D\u043E\u0432\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438. +networksettings.portforwardingenabled = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u0432\u0430\u0448\u0438\u044F \u0440\u0443\u0442\u0435\u0440 \u0434\u0430 \u0440\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430 \u0432\u0445\u043E\u0434\u044F\u0449\u0438 \u0432\u0440\u044A\u0437\u043A\u0438 \u043A\u044A\u043C Subsonic (\u0447\u0440\u0435\u0437 UPnP \u0438\u043B\u0438 NAT-PMP \u043F\u0440\u0435\u043D\u0430\u0441\u043E\u0447\u0432\u0430\u043D\u0435 \u043D\u0430 \u043F\u043E\u0440\u0442\u0430). +networksettings.portforwardinghelp = \u0410\u043A\u043E \u0440\u0443\u0442\u0435\u0440\u0430 \u0432\u0438 \u043D\u0435 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043D \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E, \u043C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043D\u0430\u043F\u0440\u0430\u0432\u0438\u0442\u0435 \u0442\u043E\u0432\u0430 \u0440\u044A\u0447\u043D\u043E. \ + \u0421\u043B\u0435\u0434\u0432\u0430\u0439\u0442\u0435 \u0438\u043D\u0441\u0442\u0440\u0443\u043A\u0446\u0438\u0438\u0442\u0435 \u043E\u0442 portforward.com. \ + \u0422\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043F\u0440\u0435\u043D\u0430\u0441\u043E\u0447\u0438\u0442\u0435 \u043F\u043E\u0440\u0442 {0} \u043A\u044A\u043C \u043A\u043E\u043C\u043F\u044E\u0442\u044A\u0440\u0430 \u0441 \u0438\u043D\u0441\u0442\u0430\u043B\u0438\u0440\u0430\u043D Subsonic \u0441\u044A\u0440\u0432\u044A\u0440. +networksettings.urlredirectionenabled = \u0418\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0439\u0442\u0435 \u043B\u0435\u0441\u043D\u043E \u0437\u0430\u043F\u043E\u043C\u043D\u044F\u0449 \u0441\u0435 \u0430\u0434\u0440\u0435\u0441 \u0437\u0430 \u0434\u043E\u0441\u0442\u044A\u043F \u0434\u043E \u0432\u0430\u0448\u0438\u044F\u0442 \u0441\u044A\u0440\u0432\u044A\u0440. +networksettings.status = \u0421\u0442\u0430\u0442\u0443\u0441: + +# transcodingSettings.jsp +transcodingsettings.name = \u0418\u043C\u0435 +transcodingsettings.sourceformat = \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 \u043E\u0442 +transcodingsettings.targetformat = \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 \u0432 +transcodingsettings.step1 = \u0421\u0442\u044A\u043F\u043A\u0430 1 +transcodingsettings.step2 = \u0421\u0442\u044A\u043F\u043A\u0430 2 +transcodingsettings.step3 = \u0421\u0442\u044A\u043F\u043A\u0430 3 +transcodingsettings.defaultactive = \u041F\u043E \u043F\u043E\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043D\u0435 +transcodingsettings.add = \u0414\u043E\u0431\u0430\u0432\u0438 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 +transcodingsettings.recommended = \u041F\u0440\u0435\u043F\u043E\u0440\u044A\u0447\u0438\u0442\u0435\u043B\u043D\u0430 \u043A\u043E\u043D\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044F +transcodingsettings.noname = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u0438\u043C\u0435. +transcodingsettings.nosourceformat = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u0444\u043E\u0440\u043C\u0430\u0442\u0430 \u043E\u0442 \u043A\u043E\u0439\u0442\u043E \u0449\u0435 \u0441\u0435 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430. +transcodingsettings.notargetformat = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u0444\u043E\u0440\u043C\u0430\u0442\u0430 \u043A\u044A\u043C \u043A\u043E\u0439\u0442\u043E \u0449\u0435 \u0441\u0435 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430. +transcodingsettings.nostep1 = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u043F\u043E\u043D\u0435 \u0435\u0434\u043D\u0430 \u0441\u0442\u044A\u043F\u043A\u0430 \u0437\u0430 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435. +transcodingsettings.info =

(%s = \u0424\u0430\u0439\u043B\u044A\u0442 \u0437\u0430 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435, %b = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u0435\u043D \u0431\u0438\u0442\u0440\u0435\u0439\u0442 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430, %t = \u041F\u0435\u0441\u0435\u043D, %a = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B, %l = \u0410\u043B\u0431\u0443\u043C)

\ +

\u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435\u0442\u043E \u0435 \u043F\u0440\u043E\u0446\u0435\u0441 \u043D\u0430 \u043F\u0440\u0435\u0432\u0440\u044A\u0449\u0430\u043D\u0435 \u043D\u0430 \u0435\u0434\u0438\u043D \u043C\u0435\u0434\u0438\u0435\u043D \u0444\u043E\u0440\u043C\u0430\u0442 \u0432 \u0434\u0440\u0443\u0433. \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435\u0442\u043E \u043F\u0440\u0438 {1} \ + \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0432\u0430 \u043F\u043E\u0442\u043E\u0447\u043D\u043E \u0438\u0437\u043B\u044A\u0447\u0432\u0430\u043D\u0435 \u043D\u0430 \u043C\u0435\u0434\u0438\u0439\u043D\u0438 \u0444\u043E\u0440\u043C\u0430\u0442\u0438, \u043A\u043E\u0438\u0442\u043E \u043D\u043E\u0440\u043C\u0430\u043B\u043D\u043E \u043D\u0435 \u0441\u0430 \u043F\u0440\u0438\u0433\u043E\u0434\u0435\u043D\u0438 \u0437\u0430 \u0442\u043E\u0432\u0430. \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435\u0442\u043E \u0441\u0435 \u0438\u0437\u0432\u044A\u0440\u0448\u0432\u0430 \u043F\u043E \u0432\u0440\u0435\u043C\u0435 \u043D\u0430 \u0441\u0430\u043C\u043E\u0442\u043E \u0441\u043B\u0443\u0448\u0430\u043D\u0435 \u0438 \u0437\u0430\u0442\u043E\u0432\u0430 \ + \u043D\u0435 \u0438\u0437\u0438\u0441\u043A\u0432\u0430 \u0434\u043E\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B\u043D\u043E \u0434\u0438\u0441\u043A\u043E\u0432\u043E \u043F\u0440\u043E\u0441\u0442\u0440\u0430\u043D\u0441\u0442\u0432\u043E.

\ +

\u0424\u0430\u043A\u0442\u0438\u0447\u0435\u0441\u043A\u043E\u0442\u043E \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 \u0441\u0435 \u0438\u0437\u0432\u044A\u0440\u0448\u0432\u0430 \u043E\u0442 \u043A\u043E\u043D\u0437\u043E\u043B\u043D\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u043D\u0430 \u0442\u0440\u0435\u0442\u0438 \u0441\u0442\u0440\u0430\u043D\u0438, \u043A\u043E\u0438\u0442\u043E \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0441\u0430 \u0438\u043D\u0441\u0442\u0430\u043B\u0438\u0440\u0430\u043D\u0438 \u0432 {0}. \ + \u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u0449 \u043F\u0430\u043A\u0435\u0442 \u0437\u0430 Windows \ + \u0435 \u0434\u043E\u0441\u0442\u044A\u043F\u0435\u043D \u0442\u0443\u043A. \u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0434\u043E\u0431\u0430\u0432\u0438\u0442\u0435 \u0432\u0430\u0448 \u0441\u043E\u0431\u0441\u0442\u0432\u0435\u043D \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u043E\u0440, \u0430\u043A\u043E \u0438\u0437\u043F\u044A\u043B\u043D\u044F\u0432\u0430 \ + \u0441\u043B\u0435\u0434\u043D\u0438\u0442\u0435 \u0438\u0437\u0438\u0441\u043A\u0432\u0430\u043D\u0438\u044F: \ +

    \ +
  • \u0422\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043F\u0440\u0438\u0442\u0435\u0436\u0430\u0432\u0430 \u043A\u043E\u043D\u0437\u043E\u043B\u0435\u043D \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441.
  • \ +
  • \u0422\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0438\u0437\u043F\u0440\u0430\u0449\u0430 \u0440\u0435\u0437\u0443\u043B\u0442\u0430\u0442\u0430 \u043A\u044A\u043C stdout.
  • \ +
  • \u0410\u043A\u043E \u0441\u0435 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u0432 \u0441\u0442\u044A\u043F\u043A\u0438 2 \u0438 3, \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u043F\u043E\u043B\u0443\u0447\u0430\u0432\u0430 \u0434\u0430\u043D\u043D\u0438 \u043E\u0442 stdin.
  • \ +
\ +

\ +

\u041A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435\u0442\u043E \u0441\u0435 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0430 \u0437\u0430 \u0432\u0441\u0435\u043A\u0438 \u0435\u0434\u0438\u043D \u043F\u043B\u0435\u044A\u0440 \u043F\u043E\u043E\u0442\u0434\u0435\u043B\u043D\u043E, \u043E\u0442 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430. \u0410\u043A\u043E \u0435 \u043C\u0430\u0440\u043A\u0438\u0440\u0430\u043D\u043E "\u041F\u043E \u043F\u043E\u0434\u0440\u0430\u0437\u0431\u0438\u0440\u0430\u043D\u0435", \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435\u0442\u043E \ + \u0435 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u0432\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u0437\u0430 \u0432\u0441\u0438\u0447\u043A\u0438 \u043D\u043E\u0432\u0438 \u043F\u043B\u0435\u044A\u0440\u0438.

+ +# internetRadioSettings.jsp +internetradiosettings.streamurl = URL \u0430\u0434\u0440\u0435\u0441 +internetradiosettings.homepageurl = \u041E\u0444\u0438\u0446\u0438\u0430\u043B\u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430 +internetradiosettings.name = \u0418\u043C\u0435 +internetradiosettings.enabled = \u0412\u043A\u043B\u044E\u0447\u0435\u043D +internetradiosettings.add = \u0414\u043E\u0431\u0430\u0432\u044F\u043D\u0435 \u043D\u0430 Internet TV/\u0420\u0430\u0434\u0438\u043E +internetradiosettings.nourl = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 URL \u0430\u0434\u0440\u0435\u0441. +internetradiosettings.noname = \u041C\u043E\u043B\u044F \u043F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u0438\u043C\u0435. + +# podcastSettings.jsp +podcastsettings.update = \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0437\u0430 \u043D\u043E\u0432\u0438 \u0435\u043F\u0438\u0437\u043E\u0434\u0438 +podcastsettings.keep = \u0417\u0430\u043F\u0430\u0437\u0438 +podcastsettings.keep.all = \u0412\u0441\u0438\u0447\u043A\u0438 \u0435\u043F\u0438\u0437\u043E\u0434\u0438 +podcastsettings.keep.one = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u044F\u0442 \u0435\u043F\u0438\u0437\u043E\u0434 +podcastsettings.keep.many = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0442\u0435 {0} \u0435\u043F\u0438\u0437\u043E\u0434\u0430 +podcastsettings.download = \u041A\u043E\u0433\u0430\u0442\u043E \u0438\u043C\u0430 \u043D\u043E\u0432\u0438 \u0435\u043F\u0438\u0437\u043E\u0434\u0438 +podcastsettings.download.all = \u0421\u0432\u0430\u043B\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 +podcastsettings.download.one = \u0421\u0432\u0430\u043B\u0438 \u0441\u0430\u043C\u043E \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u044F +podcastsettings.download.many = \u0421\u0432\u0430\u043B\u0438 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0438\u0442\u0435 {0} \u0435\u043F\u0438\u0437\u043E\u0434\u0430 +podcastsettings.download.none = \u041D\u0435 \u0441\u0432\u0430\u043B\u044F\u0439 +podcastsettings.interval.manually = \u0420\u044A\u0447\u043D\u043E +podcastsettings.interval.hourly = \u0412\u0441\u0435\u043A\u0438 \u0447\u0430\u0441 +podcastsettings.interval.daily = \u0412\u0441\u0435\u043A\u0438 \u0434\u0435\u043D +podcastsettings.interval.weekly = \u0412\u0441\u044F\u043A\u0430 \u0441\u0435\u0434\u043C\u0438\u0446\u0430 +podcastsettings.folder = \u0421\u044A\u0445\u0440\u0430\u043D\u0438 \u043F\u043E\u0434\u043A\u0430\u0441\u0442\u0430 \u0432 + +# playerSettings.jsp +playersettings.noplayers = \u041D\u044F\u043C\u0430 \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438 \u043F\u043B\u0435\u044A\u0440\u0438. +playersettings.type = \u0412\u0438\u0434 +playersettings.lastseen = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u043E +playersettings.title = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043F\u043B\u0435\u044A\u0440 + +playersettings.technology.web.title = \u0423\u0435\u0431 \u043F\u043B\u0435\u044A\u0440 +playersettings.technology.external.title = \u0412\u044A\u043D\u0448\u0435\u043D \u043F\u043B\u0435\u044A\u0440 +playersettings.technology.external_with_playlist.title = \u0412\u044A\u043D\u0448\u0435\u043D \u043F\u043B\u0435\u044A\u0440 \u0441 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +playersettings.technology.jukebox.title = \u0414\u0436\u0443\u0431\u043E\u043A\u0441 +playersettings.technology.web.text = \u0421\u043B\u0443\u0448\u0430\u0439\u0442\u0435 \u043C\u0443\u0437\u0438\u043A\u0430\u0442\u0430 \u0434\u0438\u0440\u0435\u043A\u0442\u043D\u043E \u0432 \u0431\u0440\u0430\u0443\u0437\u044A\u0440\u0430, \u0447\u0440\u0435\u0437 \u0438\u043D\u0442\u0435\u0433\u0440\u0438\u0440\u0430\u043D\u0438\u044F \u0444\u043B\u0430\u0448 \u043F\u043B\u0435\u044A\u0440. +playersettings.technology.external.text = \u0421\u043B\u0443\u0448\u0430\u0439\u0442\u0435 \u043C\u0443\u0437\u0438\u043A\u0430\u0442\u0430 \u0432 \u043B\u044E\u0431\u0438\u043C\u0438\u044F \u0441\u0438 \u043F\u043B\u0435\u0439\u044A\u0440, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u0432 WinAmp \u0438\u043B\u0438 Windows Media Player. +playersettings.technology.external_with_playlist.text = \u0421\u044A\u0449\u043E \u043A\u0430\u0442\u043E \u043F\u043E-\u0433\u043E\u0440\u0435, \u043D\u043E \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430\u0442\u0430 \u0441\u0435 \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0432\u0430 \u043E\u0442 \u043F\u043B\u0435\u044A\u0440\u0430, \ + \u0430 \u043D\u0435 \u043E\u0442 Subsonic \u0441\u044A\u0440\u0432\u044A\u0440\u0430. \u0412 \u0442\u043E\u0437\u0438 \u0440\u0435\u0436\u0438\u043C \u0435 \u0432\u044A\u0437\u043C\u043E\u0436\u043D\u043E \u043D\u0430\u043A\u044A\u0441\u0432\u0430\u043D\u0435 \u043D\u0430 \u043F\u0435\u0441\u043D\u0438\u0442\u0435. +playersettings.technology.jukebox.text = \u0421\u043B\u0443\u0448\u0430\u0439\u0442\u0435 \u043C\u0443\u0437\u0438\u043A\u0430\u0442\u0430 \u0434\u0438\u0440\u0435\u043A\u0442\u043D\u043E \u0447\u0440\u0435\u0437 \u0430\u0443\u0434\u0438\u043E \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435\u0442\u043E \u043D\u0430 Subsonic \u0441\u044A\u0440\u0432\u044A\u0440\u0430. (\u0421\u0430\u043C\u043E \u0437\u0430 \u043E\u0442\u043E\u0440\u0438\u0437\u0438\u0440\u0430\u043D\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438). +playersettings.name = \u0418\u043C\u0435 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430 +playersettings.coverartsize = \u0420\u0430\u0437\u043C\u0435\u0440 \u043D\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0438\u0442\u0435 +playersettings.maxbitrate = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u0435\u043D \u0431\u0438\u0442\u0440\u0435\u0439\u0442 +playersettings.coverart.off = \u0418\u0437\u043A\u043B\u044E\u0447\u0435\u043D\u043E +playersettings.coverart.small = \u041C\u0430\u043B\u043A\u0438 +playersettings.coverart.medium = \u0421\u0440\u0435\u0434\u043D\u0438 +playersettings.coverart.large = \u0413\u043E\u043B\u0435\u043C\u0438 +playersettings.notranscoder = Notice: \u041D\u0435 \u0435 \u0438\u043D\u0441\u0442\u0430\u043B\u0438\u0440\u0430\u043D ffmpeg.
\u041D\u0430\u0442\u0438\u0441\u043D\u0435\u0442\u0435 \u0431\u0443\u0442\u043E\u043D\u0430 \u041F\u043E\u043C\u043E\u0449 \u0437\u0430 \u043F\u043E\u0432\u0435\u0447\u0435 \u0438\u043D\u0444\u043E\u0440\u043C\u0430\u0446\u0438\u044F. +playersettings.autocontrol = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u043A\u043E\u043D\u0442\u0440\u043E\u043B\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430 +playersettings.dynamicip = \u041F\u043B\u0435\u044A\u0440\u044A\u0442 \u0438\u043C\u0430 \u0434\u0438\u043D\u0430\u043C\u0438\u0447\u0435\u043D IP \u0430\u0434\u0440\u0435\u0441 +playersettings.transcodings = \u0410\u043A\u0442\u0438\u0432\u043D\u043E \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 +playersettings.ok = \u0417\u0430\u043F\u0430\u0437\u0438 +playersettings.forget = \u0418\u0437\u0442\u0440\u0438\u0439 \u043F\u043B\u0435\u044A\u0440\u0430 +playersettings.clone = \u0414\u0443\u0431\u043B\u0438\u0440\u0430\u0439 \u043F\u043B\u0435\u044A\u0440\u0430 + +# userSettings.jsp +usersettings.title = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +usersettings.newuser = \u041D\u043E\u0432 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +usersettings.admin = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0435 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0430\u0442\u043E\u0440 +usersettings.settings = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u043C\u0435\u043D\u044F \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0438 \u043F\u0430\u0440\u043E\u043B\u0430 +usersettings.stream = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u043B\u0443\u0448\u0430 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 +usersettings.jukebox = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u043B\u0443\u0448\u0430 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0432 \u0440\u0435\u0436\u0438\u043C \u0414\u0436\u0443\u0431\u043E\u043A\u0441 +usersettings.download = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u0432\u0430\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432\u0435 +usersettings.upload = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u043A\u0430\u0447\u0432\u0430 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 +usersettings.share = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u043F\u043E\u0434\u0435\u043B\u044F \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0441 \u0432\u0441\u0435\u043A\u0438 +usersettings.coverart = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u043C\u0435\u043D\u044F \u043E\u0431\u043B\u043E\u0436\u043A\u0438 \u0438 \u0442\u0430\u0433\u043E\u0432\u0435 \u043D\u0430 \u043F\u0435\u0441\u043D\u0438\u0442\u0435 +usersettings.comment= \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0441\u044A\u0437\u0434\u0430\u0432\u0430 \u0438 \u0440\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0430 \u043A\u043E\u043C\u0435\u043D\u0442\u0430\u0440\u0438 \u0438 \u0440\u0435\u0439\u0442\u0438\u043D\u0433\u0438 +usersettings.podcast= \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F\u0442 \u0438\u043C\u0430 \u043F\u0440\u0430\u0432\u043E \u0434\u0430 \u0430\u0434\u043C\u0438\u043D\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u041F\u043E\u0434\u043A\u0430\u0441\u0442\u0438\u0442\u0435 +usersettings.username = \u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +usersettings.email = Email +usersettings.changepassword = \u0421\u043C\u044F\u043D\u0430 \u043D\u0430 \u043F\u0430\u0440\u043E\u043B\u0430 +usersettings.password = \u041F\u0430\u0440\u043E\u043B\u0430 +usersettings.newpassword = \u041D\u043E\u0432\u0430 \u043F\u0430\u0440\u043E\u043B\u0430 +usersettings.confirmpassword = \u041F\u043E\u0432\u0442\u043E\u0440\u0435\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u0430\u0442\u0430 +usersettings.delete = \u0418\u0437\u0442\u0440\u0438\u0439 \u0442\u043E\u0437\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B +usersettings.ldap = \u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u043D\u0430 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u044F \u0447\u0440\u0435\u0437 LDAP +usersettings.nousername = \u0422\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0432\u044A\u0432\u0435\u0434\u0435\u0442\u0435 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u043C\u0435. +usersettings.noemail= \u041D\u0435\u0432\u0430\u043B\u0438\u0434\u0435\u043D email \u0430\u0434\u0440\u0435\u0441. +usersettings.useralreadyexists = \u0418\u043C\u0430 \u0432\u0435\u0447\u0435 \u0442\u0430\u043A\u044A\u0432 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B. +usersettings.nopassword = \u0422\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0432\u044A\u0432\u0435\u0434\u0435\u0442\u0435 \u043F\u0430\u0440\u043E\u043B\u0430. +usersettings.wrongpassword = \u041F\u0430\u0440\u043E\u043B\u0438\u0442\u0435 \u043D\u0435 \u0441\u044A\u0432\u043F\u0430\u0434\u0430\u0442. +usersettings.ldapdisabled = LDAP \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0430\u0442\u0430 \u043D\u0435 \u0435 \u0430\u043A\u0442\u0438\u0432\u0438\u0440\u0430\u043D\u0430. \u0412\u0438\u0436\u0442\u0435 \u0420\u0430\u0437\u0448\u0438\u0440\u0435\u043D\u0438\u0442\u0435 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438. +usersettings.passwordnotsupportedforldap = \u041D\u0435 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0441\u0435 \u0441\u044A\u0437\u0434\u0430\u0432\u0430 \u0438\u043B\u0438 \u0441\u043C\u0435\u043D\u044F \u043F\u0430\u0440\u043E\u043B\u0430 \u0437\u0430 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438 \u0441 \u043F\u043E\u0442\u0432\u044A\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u0447\u0440\u0435\u0437 LDAP. +usersettings.ok = \u041F\u0430\u0440\u043E\u043B\u0430\u0442\u0430 \u0435 \u0443\u0441\u043F\u0435\u0448\u043D\u043E \u0441\u043C\u0435\u043D\u0435\u043D\u0430 \u0437\u0430 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B {0}. + +# musicFolderSettings.jsp +musicfoldersettings.interval.never = \u041D\u0438\u043A\u043E\u0433\u0430 +musicfoldersettings.interval.one = \u0412\u0441\u0435\u043A\u0438 \u0434\u0435\u043D +musicfoldersettings.interval.many = \u0412\u0441\u0435\u043A\u0438 {0} \u0434\u043D\u0438 +musicfoldersettings.hour = \u0432 {0}:00 + +# main.jsp +main.up = \u041D\u0430\u0433\u043E\u0440\u0435 +main.playall = \u041F\u0443\u0441\u043D\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 +main.playrandom = \u041F\u0443\u0441\u043D\u0438 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u0438 +main.addall = \u0414\u043E\u0431\u0430\u0432\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 +main.downloadall = \u0421\u0432\u0430\u043B\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 +main.tags = \u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0430\u0439 \u0442\u0430\u0433\u043E\u0432\u0435\u0442\u0435 +main.playcount = \u0421\u043B\u0443\u0448\u0430\u043D\u043E {0} \u043F\u044A\u0442\u0438. +main.lastplayed = \u041F\u043E\u0441\u043B\u0435\u0434\u043D\u043E \u043D\u0430 {0}. +main.comment = \u041A\u043E\u043C\u0435\u043D\u0442\u0438\u0440\u0430\u0439 +main.wiki = \ + \ + \ + \ + \ +
__text__\u041F\u043E\u0434\u0447\u0435\u0440\u0442\u0430\u043D \u0442\u0435\u043A\u0441\u0442 \\\\ \u041D\u043E\u0432 \u0440\u0435\u0434
~~text~~\u0428\u0440\u0438\u0444\u0442 Italic (empty line) \u041D\u043E\u0432 \u043F\u0430\u0440\u0430\u0433\u0440\u0430\u0444
* text \u0421\u043F\u0438\u0441\u044A\u043A http://foo.com/ \u0412\u0440\u044A\u0437\u043A\u0430
1. text \u0421\u043F\u0438\u0441\u044A\u043A \u0441 \u043F\u043E\u0440\u0435\u0434\u043D\u0438 \u0446\u0438\u0444\u0440\u0438{link:Foo|http://foo.com}\u0412\u0440\u044A\u0437\u043A\u0430 \u0441\u044A\u0441 \u0437\u0430\u0433\u043B\u0430\u0432\u0438\u0435
+main.sharealbum = \u0421\u043F\u043E\u0434\u0435\u043B\u0438 +main.more = \u041E\u0449\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F... +main.more.share = \u0421\u043F\u043E\u0434\u0435\u043B\u0438 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 \u043F\u0435\u0441\u043D\u0438 +main.nowplaying = \u0421\u043B\u0443\u0448\u0430\u0442\u0435 +main.lyrics = \u0422\u0435\u043A\u0441\u0442 +main.minutesago = \u043C\u0438\u043D\u0443\u0442\u0438 \u043D\u0430\u0437\u0430\u0434 +main.chat = \u0427\u0430\u0442 \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u044F +main.message = \u041D\u0430\u043F\u0438\u0448\u0435\u0442\u0435 \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435 +main.clearchat = \u0418\u0437\u0447\u0438\u0441\u0442\u0435\u0442\u0435 \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u044F\u0442\u0430 + +# rating.jsp +rating.rating = \u0420\u0435\u0439\u0442\u0438\u043D\u0433 +rating.clearrating = \u0418\u0437\u0442\u0440\u0438\u0439 \u0440\u0435\u0439\u0442\u0438\u043D\u0433\u0430 + +# coverArt.jsp +coverart.change = \u041F\u0440\u043E\u043C\u0435\u043D\u0438 +coverart.zoom = \u0423\u0432\u0435\u043B\u0438\u0447\u0438 + +# allmusic.jsp +allmusic.text = \u0422\u044A\u0440\u0441\u0435\u043D\u0435 \u043D\u0430 \u0430\u043B\u0431\u0443\u043C\u0430 {0} \u0432 allmusic.com - \u041C\u043E\u043B\u044F \u0438\u0437\u0447\u0430\u043A\u0430\u0439\u0442\u0435. + +# changeCoverArt.jsp +changecoverart.title = \u041F\u0440\u043E\u043C\u044F\u043D\u0430 \u043D\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0430\u0442\u0430 +changecoverart.address = \u0418\u043B\u0438 \u0432\u044A\u0432\u0435\u0434\u0435\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u0434\u043E \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u0442\u043E +changecoverart.artist = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B +changecoverart.album = \u0410\u043B\u0431\u0443\u043C +changecoverart.search = \u0422\u044A\u0440\u0441\u0435\u043D\u0435 \u0432 Google +changecoverart.wait = \u041C\u043E\u043B\u044F \u0438\u0437\u0447\u0430\u043A\u0430\u0439\u0442\u0435... +changecoverart.success = \u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u0442\u043E \u0435 \u0441\u0432\u0430\u043B\u0435\u043D\u043E \u0443\u0441\u043F\u0435\u0448\u043D\u043E. +changecoverart.error = \u0421\u0432\u0430\u043B\u044F\u043D\u0435\u0442\u043E \u043D\u0430 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435\u0442\u043E \u0435 \u043D\u0435\u0443\u0441\u043F\u0435\u0448\u043D\u043E. +changecoverart.noimagesfound = \u041D\u044F\u043C\u0430 \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0438 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F. + +# changeCoverArtConfirm.jsp +changeCoverArtConfirm.failed = \u041D\u0435\u0443\u0441\u043F\u0435\u0448\u043D\u0430 \u0441\u043C\u044F\u043D\u0430 \u043D\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0430:
"{0}" + +# editTags.jsp +edittags.title = \u0420\u0435\u0434\u0430\u043A\u0442\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u0442\u0430\u0433\u043E\u0432\u0435 +edittags.file = \u0424\u0430\u0439\u043B +edittags.track = \u041F\u0435\u0441\u0435\u043D +edittags.songtitle = \u0417\u0430\u0433\u043B\u0430\u0432\u0438\u0435 +edittags.artist = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B +edittags.album = \u0410\u043B\u0431\u0443\u043C +edittags.year = \u0413\u043E\u0434\u0438\u043D\u0430 +edittags.genre = \u0416\u0430\u043D\u0440 +edittags.status = \u0421\u0442\u0430\u0442\u0443\u0441 +edittags.suggest = \u041F\u043E\u0434\u0441\u043A\u0430\u0436\u0438 +edittags.reset = \u0418\u0437\u0447\u0438\u0441\u0442\u0438 +edittags.suggest.short = S +edittags.reset.short = R +edittags.set = \u0417\u0430\u043F\u0430\u0437\u0438 +edittags.working = \u041E\u0431\u0440\u0430\u0431\u043E\u0442\u0432\u0430\u043D\u0435 +edittags.updated = \u041E\u0431\u043D\u043E\u0432\u0435\u043D +edittags.skipped = \u041F\u0440\u043E\u043F\u0443\u0441\u043D\u0430\u0442 +edittags.error = \u0413\u0440\u0435\u0448\u043A\u0430 + +# share.jsp +share.title = \u0421\u043F\u043E\u0434\u0435\u043B\u044F\u043D\u0435 + +# podcastReceiver.jsp +podcastreceiver.title = \u041F\u043E\u0434\u043A\u0430\u0441\u0442 +podcastreceiver.expandall = \u041F\u043E\u043A\u0430\u0436\u0438 \u0435\u043F\u0438\u0437\u043E\u0434\u0438\u0442\u0435 +podcastreceiver.collapseall = \u0421\u043A\u0440\u0438\u0439 \u0435\u043F\u0438\u0437\u043E\u0434\u0438\u0442\u0435 +podcastreceiver.status.new = \u041D\u043E\u0432 +podcastreceiver.status.downloading = \u0421\u0432\u0430\u043B\u044F\u043D\u0435 +podcastreceiver.status.completed = \u0417\u0430\u0432\u044A\u0440\u0448\u0435\u043D\u043E +podcastreceiver.status.error = \u0413\u0440\u0435\u0448\u043A\u0430 +podcastreceiver.status.deleted = \u0418\u0437\u0442\u0440\u0438\u0442\u043E +podcastreceiver.status.skipped = \u041F\u0440\u043E\u043F\u0443\u0441\u043D\u0430\u0442\u043E +podcastreceiver.downloadselected= \u0421\u0432\u0430\u043B\u0438 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 +podcastreceiver.deleteselected= \u0418\u0437\u0442\u0440\u0438\u0439 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435 +podcastreceiver.confirmdelete= \u0418\u0437\u0442\u0440\u0438\u0432\u0430\u043D\u0435 \u043D\u0430 \u0438\u0437\u0431\u0440\u0430\u043D\u0438\u0442\u0435? +podcastreceiver.check = \u041F\u0440\u043E\u0432\u0435\u0440\u0438 \u0437\u0430 \u043D\u043E\u0432\u0438 \u0435\u043F\u0438\u0437\u043E\u0434\u0438 +podcastreceiver.refresh = \u041F\u0440\u0435\u0437\u0430\u0440\u0435\u0434\u0438 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430\u0442\u0430 +podcastreceiver.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043D\u0430 \u043F\u043E\u0434\u043A\u0430\u0441\u0442\u0430 +podcastreceiver.subscribe = \u0410\u0431\u043E\u043D\u0430\u043C\u0435\u043D\u0442 \u0437\u0430 \u043F\u043E\u0434\u043A\u0430\u0441\u0442 + +# lyrics.jsp +lyrics.title = \u0422\u0435\u043A\u0441\u0442 \u043D\u0430 \u043F\u0435\u0441\u0435\u043D\u0442\u0430 +lyrics.artist = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B +lyrics.song = \u041F\u0435\u0441\u0435\u043D +lyrics.search = \u0422\u044A\u0440\u0441\u0438 +lyrics.wait = \u0422\u044A\u0440\u0441\u0435\u043D\u0435 \u0437\u0430 \u0442\u0435\u043A\u0441\u0442\u043E\u0432\u0435, \u043C\u043E\u043B\u044F \u0438\u0437\u0447\u0430\u043A\u0430\u0439\u0442\u0435... +lyrics.courtesy = (\u0422\u0435\u043A\u0441\u0442\u043E\u0432\u0435 \u043E\u0442 chartlyrics.com) +lyrics.nolyricsfound = \u041D\u044F\u043C\u0430 \u043D\u0430\u043C\u0435\u0440\u0435\u043D \u0442\u0435\u043A\u0441\u0442. + +# helpPopup.jsp +helppopup.title = {0} \u041F\u043E\u043C\u043E\u0449 +helppopup.cover.title = \u0420\u0430\u0437\u043C\u0435\u0440 \u043D\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0430\u0442\u0430 +helppopup.cover.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0442\u0435 \u0440\u0430\u0437\u043C\u0435\u0440\u0430 \u043D\u0430 \u043F\u043E\u043A\u0430\u0437\u0432\u0430\u043D\u0438\u0442\u0435 \u043E\u0431\u043B\u043E\u0436\u043A\u0438, \u043A\u0430\u043A\u0442\u043E \u0438 \u043D\u0430\u043F\u044A\u043B\u043D\u043E \u0434\u0430 \u0438\u0437\u043A\u043B\u044E\u0447\u0438\u0442\u0435 \u0442\u0430\u0437\u0438 \u043E\u043F\u0446\u0438\u044F.

+helppopup.transcode.title = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u0435\u043D \u0431\u0438\u0442\u0440\u0435\u0439\u0442 +helppopup.transcode.text =

\u0410\u043A\u043E \u0438\u043C\u0430\u0442\u0435 \u043E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u043D\u0430 \u0442\u0440\u0430\u0444\u0438\u043A\u0430, \u043C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u0438\u0442\u0435 \u0433\u043E\u0440\u043D\u0430 \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0437\u0430 \u0431\u0438\u0442\u0440\u0435\u0439\u0442\u0430 \u043D\u0430 \u043C\u0443\u0437\u0438\u043A\u0430\u043B\u043D\u0438\u044F \u043F\u043E\u0442\u043E\u043A. \ + \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0430\u043A\u043E \u043E\u0440\u0438\u0433\u0438\u043D\u0430\u043B\u043D\u0438\u0442\u0435 \u0432\u0438 mp3 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0441\u0430 \u0441 \u0431\u0438\u0442\u0440\u0435\u0439\u0442 256 Kbps (\u043A\u0438\u043B\u043E\u0431\u0438\u0442\u0430 \u0432 \u0441\u0435\u043A\u0443\u043D\u0434\u0430), \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u043D\u0435 \u043D\u0430 \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u0435\u043D \u0431\u0438\u0442\u0440\u0435\u0439\u0442 \ + 128 \u0449\u0435 \u043D\u0430\u043A\u0430\u0440\u0430 {0} \u0434\u0430 \u043F\u0440\u0435\u043E\u0431\u0440\u0430\u0437\u0443\u0432\u0430 \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u043C\u0443\u0437\u0438\u043A\u0430\u0442\u0430 \u043E\u0442 256 \u043D\u0430 128 Kbps.

+helppopup.playlistfolder.title = \u041F\u0430\u043F\u043A\u0430 \u0437\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0438 +helppopup.playlistfolder.text =

\u0422\u0443\u043A \u043F\u043E\u0441\u043E\u0447\u0432\u0430\u0442\u0435 \u043F\u0430\u043F\u043A\u0430\u0442\u0430, \u043A\u044A\u0434\u0435\u0442\u043E \u0449\u0435 \u0441\u0435 \u0441\u044A\u0445\u0440\u0430\u043D\u044F\u0432\u0430\u0442 \u0432\u0430\u0448\u0438\u0442\u0435 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0438.

+helppopup.musicmask.title = \u0424\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u043C\u0443\u0437\u0438\u043A\u0430 +helppopup.musicmask.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u043A\u043E\u0438 \u0442\u0438\u043F\u043E\u0432\u0435 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0449\u0435 \u0441\u0435 \u0440\u0430\u0437\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u0442 \u043A\u0430\u0442\u043E \u043C\u0443\u0437\u0438\u043A\u0430\u043B\u043D\u0438.

+helppopup.videomask.title = \u0424\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u0432\u0438\u0434\u0435\u043E +helppopup.videomask.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u043A\u043E\u0438 \u0442\u0438\u043F\u043E\u0432\u0435 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0449\u0435 \u0441\u0435 \u0440\u0430\u0437\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u0442 \u043A\u0430\u0442\u043E \u0432\u0438\u0434\u0435\u043E.

+helppopup.coverartmask.title = \u0424\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0438 +helppopup.coverartmask.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u043A\u043E\u0438 \u0442\u0438\u043F\u043E\u0432\u0435 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0449\u0435 \u0441\u0435 \u0440\u0430\u0437\u043F\u043E\u0437\u043D\u0430\u0432\u0430\u0442 \u043A\u0430\u0442\u043E \u043F\u043E\u0434\u0445\u043E\u0434\u044F\u0449\u0438 \u0437\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0438, \u043A\u043E\u0433\u0430\u0442\u043E \u0440\u0430\u0437\u0433\u043B\u0435\u0436\u0434\u0430\u0442\u0435 \u0441\u044A\u0434\u044A\u0440\u0436\u0430\u043D\u0438\u0435\u0442\u043E \u043D\u0430 \u0434\u0430\u0434\u0435\u043D\u0430 \u043F\u0430\u043F\u043A\u0430.

+helppopup.downsamplecommand.title = \u041A\u043E\u043C\u0430\u043D\u0434\u0430 \u0437\u0430 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430\u043D\u0435 +helppopup.downsamplecommand.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u043A\u0430\u043A\u0432\u0430 \u043A\u043E\u043C\u0430\u043D\u0434\u0430 \u0434\u0430 \u0441\u0435 \u0438\u0437\u043F\u044A\u043B\u043D\u044F\u0432\u0430, \u043A\u043E\u0433\u0430\u0442\u043E \u0441\u0435 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430 \u043A\u044A\u043C \u043F\u043E-\u043D\u0438\u0441\u044A\u043A \u0431\u0438\u0442\u0440\u0435\u0439\u0442.

\ +

(%s = \u0424\u0430\u0439\u043B\u044A\u0442, \u043A\u043E\u0439\u0442\u043E \u0449\u0435 \u0441\u0435 \u043A\u043E\u043D\u0432\u0435\u0440\u0442\u0438\u0440\u0430, %b = \u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u0435\u043D \u0431\u0438\u0442\u0440\u0435\u0439\u0442 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430, %t = \u0417\u0430\u0433\u043B\u0430\u0432\u0438\u0435, %a = \u0418\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B, %l = \u0410\u043B\u0431\u0443\u043C)

+helppopup.index.title = \u0418\u043D\u0434\u0435\u043A\u0441 +helppopup.index.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F\u0442\u0435 \u043A\u0430\u043A \u0434\u0430 \u0438\u0437\u0433\u043B\u0435\u0436\u0434\u0430 \u0438\u043D\u0434\u0435\u043A\u0441\u0430 (\u043D\u0430\u043C\u0438\u0440\u0430\u0449 \u0441\u0435 \u0432\u043B\u044F\u0432\u043E \u043D\u0430 \u0435\u043A\u0440\u0430\u043D\u0430) . \u0424\u0430\u0439\u043B\u043E\u0432\u0435\u0442\u0435 \u0438 \u043F\u0430\u043F\u043A\u0438\u0442\u0435, \ + \u043A\u043E\u0438\u0442\u043E \u0441\u0430 \u0432 \u043E\u0441\u043D\u043E\u0432\u043D\u0430\u0442\u0430 \u043C\u0443\u0437\u0438\u043A\u0430\u043B\u043D\u0430 \u043F\u0430\u043F\u043A\u0430, \u043C\u043E\u0433\u0430\u0442 \u043B\u0435\u0441\u043D\u043E \u0434\u0430 \u0431\u044A\u0434\u0430\u0442 \u0440\u0430\u0437\u0433\u043B\u0435\u0436\u0434\u0430\u043D\u0438 \u0447\u0440\u0435\u0437 \u0442\u043E\u0437\u0438 \u0438\u043D\u0434\u0435\u043A\u0441.

\ +

\u041F\u0440\u0435\u0434\u0441\u0442\u0430\u0432\u043B\u044F\u0432\u0430 \u0441\u043F\u0438\u0441\u044A\u043A \u043D\u0430 \u043E\u0442\u0434\u0435\u043B\u043D\u0438\u0442\u0435 \u0435\u043B\u0435\u043C\u0435\u043D\u0442\u0438 \u043D\u0430 \u0438\u043D\u0434\u0435\u043A\u0441\u0430. \u041E\u0431\u0438\u043A\u043D\u043E\u0432\u0435\u043D\u043E, \u0432\u0441\u0435\u043A\u0438 \u0435\u0434\u0438\u043D \u0435\u043B\u0435\u043C\u0435\u043D\u0442 \u043E\u0442 \u0441\u043F\u0438\u0441\u044A\u043A\u0430 \u0435 \u043F\u0440\u043E\u0441\u0442\u043E \u0435\u0434\u043D\u0430 \u0431\u0443\u043A\u0432\u0430, \ + \u043D\u043E \u0432\u0438\u0435 \u043C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u0438 \u043A\u043E\u043C\u0431\u0438\u043D\u0430\u0446\u0438\u044F \u043E\u0442 \u043D\u044F\u043A\u043E\u043B\u043A\u043E \u0441\u0438\u043C\u0432\u043E\u043B\u0430. \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \u0430\u043A\u043E \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 The \u0442\u043E \u0442\u043E\u0437\u0438 \u043B\u0438\u043D\u043A \u0449\u0435 \u043E\u0442\u0432\u0430\u0440\u044F \u0432\u0441\u0438\u0447\u043A\u0438 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0438 \ + \u043F\u0430\u043F\u043A\u0438 \u0437\u0430\u043F\u043E\u0447\u0432\u0430\u0449\u0438 \u0441 "The".

\ +

\u041C\u043E\u0436\u0435\u0442\u0435 \u0441\u044A\u0449\u043E \u0442\u0430\u043A\u0430 \u0434\u0430 \u0441\u044A\u0437\u0434\u0430\u0434\u0435\u0442\u0435 \u0433\u0440\u0443\u043F\u0430 \u043E\u0442 \u0441\u0438\u043C\u0432\u043E\u043B\u0438, \u043F\u043E\u0441\u0442\u0430\u0432\u0435\u043D\u0438 \u0432 \u0441\u043A\u043E\u0431\u0438. \u041D\u0430\u043F\u0440\u0438\u043C\u0435\u0440, \u0430\u043A\u043E \u0437\u0430\u0434\u0430\u0434\u0435\u0442\u0435 \ + A-E(ABCDE) \u0449\u0435 \u0441\u0435 \u043F\u043E\u043A\u0430\u0437\u0432\u0430 \u043A\u0430\u0442\u043E A-E, \u0430 \u043B\u0438\u043D\u043A\u0430 \u0449\u0435 \u043E\u0442\u0432\u0430\u0440\u044F \u0432\u0441\u0438\u0447\u043A\u0438 \u0444\u0430\u0439\u043B\u043E\u0432\u0435 \u0438 \u043F\u0430\u043F\u043A\u0438 \u0437\u0430\u043F\u043E\u0447\u0432\u0430\u0449\u0438 \u0441 \ + A, B, C, D \u0438\u043B\u0438 E. \u0422\u043E\u0432\u0430 \u0435 \u043F\u0440\u0430\u043A\u0442\u0438\u0447\u043D\u043E \u0437\u0430 \u0433\u0440\u0443\u043F\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u043F\u043E-\u0440\u044F\u0434\u043A\u043E \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u043D\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u0438 \u0438 \u0431\u0443\u043A\u0432\u0438 (\u043A\u0430\u0442\u043E X, Y \u0438 Z), \u0438\u043B\u0438 \ + \u0437\u0430 \u0433\u0440\u0443\u043F\u0438\u0440\u0430\u043D\u0435 \u043D\u0430 \u0441\u0438\u043C\u0432\u043E\u043B\u0438 \u0441 \u0443\u0434\u0430\u0440\u0435\u043D\u0438\u044F (\u043A\u0430\u0442\u043E A, \u00C0 \u0438 \u00C1)

\ +

\u0424\u0430\u0439\u043B\u043E\u0432\u0435\u0442\u0435 \u0438 \u043F\u0430\u043F\u043A\u0438\u0442\u0435, \u043A\u043E\u0438\u0442\u043E \u043D\u0435 \u0441\u0430 \u0447\u0430\u0441\u0442 \u043E\u0442 \u0434\u0430\u0434\u0435\u043D\u0430 \u0433\u0440\u0443\u043F\u0430 \u0432 \u0438\u043D\u0434\u0435\u043A\u0441\u0430, \u0449\u0435 \u0431\u044A\u0434\u0430\u0442 \u043F\u043E\u0441\u0442\u0430\u0432\u0435\u043D\u0438 \u043F\u043E\u0434 \u043E\u0431\u0449\u0430 \u0433\u0440\u0443\u043F\u0430 "#".

+helppopup.ignoredarticles.title = \u0418\u0433\u043D\u043E\u0440\u0438\u0440\u0430\u043D\u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u0438 +helppopup.ignoredarticles.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 \u0441\u043F\u0438\u0441\u044A\u043A \u043E\u0442 \u0431\u0443\u043A\u0432\u0438 \u0438 \u0441\u0438\u043C\u0432\u043E\u043B\u0438 (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "The"), \u043A\u043E\u0438\u0442\u043E \u0434\u0430 \u0431\u044A\u0434\u0430\u0442 \u0438\u0433\u043D\u043E\u0440\u0438\u0440\u0430\u043D\u0438 \u043F\u0440\u0438 \u0441\u044A\u0437\u0434\u0430\u0432\u0430\u043D\u0435 \u043D\u0430 \u0438\u043D\u0434\u0435\u043A\u0441\u0430.

+helppopup.shortcuts.title = \u0412\u0440\u044A\u0437\u043A\u0438 +helppopup.shortcuts.text =

\u0421\u043F\u0438\u0441\u044A\u043A \u043E\u0442 \u043E\u0442\u0434\u0435\u043B\u043D\u0438 \u043F\u0430\u043F\u043A\u0438 \u043A\u044A\u043C \u043A\u043E\u0438\u0442\u043E \u0434\u0430 \u0431\u044A\u0434\u0430\u0442 \u0441\u044A\u0437\u0434\u0430\u0434\u0435\u043D\u0438 \u0432\u0440\u044A\u0437\u043A\u0438. \u0418\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0439\u0442\u0435 \u043A\u0430\u0432\u0438\u0447\u043A\u0438 \u0437\u0430 \u0434\u0430 \u0433\u0440\u0443\u043F\u0438\u0440\u0430\u0442\u0435 \u0434\u0443\u043C\u0438, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440:

\ +

\u041D\u043E\u0432\u0438 \u041F\u043E\u043B\u0443\u0447\u0435\u043D\u0438 "\u041F\u0435\u0441\u043D\u0438 \u043E\u0442 \u0444\u0438\u043B\u043C\u0438"

+helppopup.language.title = \u0415\u0437\u0438\u043A +helppopup.language.text =

\u0422\u0443\u043A \u043C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043A\u0430\u043A\u044A\u0432 \u0435\u0437\u0438\u043A \u0434\u0430 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0442\u0435.

+helppopup.visibility.title = \u0412\u044A\u043D\u0448\u0435\u043D \u0432\u0438\u0434 +helppopup.visibility.text =

\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043A\u0430\u043A\u0432\u0438 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0434\u0430 \u0431\u044A\u0434\u0430\u0442 \u043F\u043E\u043A\u0430\u0437\u0432\u0430\u043D\u0438 \u0437\u0430 \u0432\u0441\u044F\u043A\u0430 \u043F\u0435\u0441\u0435\u043D, \u0430 \u0442\u0430\u043A\u0430 \u0441\u044A\u0449\u043E \u0441\u043B\u0435\u0434 \u043A\u043E\u043B\u043A\u043E \u0431\u0443\u043A\u0432\u0438 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0441\u044A\u043A\u0440\u0430\u0449\u0430\u0432\u0430\u043D\u043E \u0437\u0430\u0433\u043B\u0430\u0432\u0438\u0435\u0442\u043E. \u041F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \ + \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u043D\u0438\u044F \u0431\u0440\u043E\u0439 \u0441\u0438\u043C\u0432\u043E\u043B\u0438 \u0437\u0430 \u0437\u0430\u0433\u043B\u0430\u0432\u0438\u0435 \u043D\u0430 \u043F\u0435\u0441\u0435\u043D, \u0430\u043B\u0431\u0443\u043C \u0438 \u0438\u0437\u043F\u044A\u043B\u043D\u0438\u0442\u0435\u043B.

+helppopup.partymode.title = \u041E\u043B\u0435\u043A\u043E\u0442\u0435\u043D \u0440\u0435\u0436\u0438\u043C +helppopup.partymode.text =

\u041A\u043E\u0433\u0430\u0442\u043E \u0435 \u0432\u043A\u043B\u044E\u0447\u0435\u043D \u0442\u043E\u0437\u0438 \u0440\u0435\u0436\u0438\u043C, \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u0438\u044F \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u0435 \u043E\u043F\u0440\u043E\u0441\u0442\u0435\u043D \u0438 \u043F\u043E-\u043B\u0435\u0441\u0435\u043D \u0437\u0430 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u043D\u0435 \u043E\u0442 \u043D\u0435\u043E\u043F\u0438\u0442\u043D\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438. \ + \u041D\u0430 \u043F\u0440\u0430\u043A\u0442\u0438\u043A\u0430 \u0441\u0435 \u0438\u0437\u0431\u044F\u0433\u0432\u0430 \u0441\u043B\u0443\u0447\u0430\u0439\u043D\u043E \u043E\u0431\u044A\u0440\u043A\u0432\u0430\u043D\u0435 \u043D\u0430 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0438\u0442\u0435.

+helppopup.theme.title = \u0422\u0435\u043C\u0430 +helppopup.theme.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043A\u0430\u043A\u0432\u0430 \u0442\u0435\u043C\u0430 \u0434\u0430 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0442\u0435 \u0437\u0430 \u0438\u043D\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0430. \u0422\u0435\u043C\u0430\u0442\u0430 \u043E\u043F\u0440\u0435\u0434\u0435\u043B\u044F \u0432\u044A\u043D\u0448\u043D\u0438\u044F \u0432\u0438\u0434 \u043D\u0430 {0} \u043F\u043E \u043E\u0442\u043D\u043E\u0448\u0435\u043D\u0438\u0435 \u043D\u0430 \u0446\u0432\u0435\u0442\u043E\u0432\u0435, \u0448\u0440\u0438\u0444\u0442\u043E\u0432\u0435, \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u0438.\u0442.\u043D

+helppopup.welcomemessage.title = \u041F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0435\u043D\u043E \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435 +helppopup.welcomemessage.text =

\u0421\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435\u0442\u043E, \u043A\u043E\u0435\u0442\u043E \u0441\u0435 \u043F\u043E\u043A\u0430\u0437\u0432\u0430 \u0432 \u043D\u0430\u0447\u0430\u043B\u043D\u0430\u0442\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430.

+helppopup.loginmessage.title = \u0421\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u0432\u043B\u0438\u0437\u0430\u043D\u0435 +helppopup.loginmessage.text =

\u0422\u043E\u0432\u0430 \u0441\u044A\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0441\u0435 \u043F\u043E\u043A\u0430\u0437\u0432\u0430 \u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430\u0442\u0430 \u0437\u0430 \u0432\u043B\u0438\u0437\u0430\u043D\u0435 \u0432 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u0438\u044F \u043F\u0440\u043E\u0444\u0438\u043B.

+helppopup.coverartlimit.title = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u0437\u0430 \u043E\u0431\u043B\u043E\u0436\u043A\u0438 +helppopup.coverartlimit.text =

\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u043D\u0438\u044F\u0442 \u0431\u0440\u043E\u0439 \u043E\u0431\u043B\u043E\u0436\u043A\u0438, \u043A\u043E\u0438\u0442\u043E \u0434\u0430 \u0441\u0435 \u043F\u043E\u043A\u0430\u0437\u0432\u0430\u0442 \u043D\u0430 \u0435\u0434\u043D\u0430 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u0430.

+helppopup.downloadlimit.title = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u0441\u0432\u0430\u043B\u044F\u043D\u0435 +helppopup.downloadlimit.text =

\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u043D\u0430 \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0437\u0430 \u0442\u043E\u0432\u0430 \u043A\u0430\u043A\u0432\u0430 \u0447\u0430\u0441\u0442 \u043E\u0442 \u0442\u0440\u0430\u0444\u0438\u043A\u0430 \u0449\u0435 \u0441\u0435 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u0437\u0430 \u0441\u0432\u0430\u043B\u044F\u043D\u0435 \u043D\u0430 \u0444\u0430\u0439\u043B\u043E\u0432\u0435\u0442\u0435.

+helppopup.uploadlimit.title = \u041E\u0433\u0440\u0430\u043D\u0438\u0447\u0435\u043D\u0438\u0435 \u043F\u0440\u0438 \u043A\u0430\u0447\u0432\u0430\u043D\u0435 +helppopup.uploadlimit.text =

\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u043D\u0430 \u0433\u0440\u0430\u043D\u0438\u0446\u0430 \u0437\u0430 \u0442\u043E\u0432\u0430 \u043A\u0430\u043A\u0432\u0430 \u0447\u0430\u0441\u0442 \u043E\u0442 \u0442\u0440\u0430\u0444\u0438\u043A\u0430 \u0449\u0435 \u0441\u0435 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u0437\u0430 \u043A\u0430\u0447\u0432\u0430\u043D\u0435 \u043D\u0430 \u0444\u0430\u0439\u043B\u043E\u0432\u0435\u0442\u0435.

+helppopup.streamport.title = \u041F\u043E\u0440\u0442 \u0437\u0430 \u043D\u0435\u043A\u0440\u0438\u043F\u0442\u0438\u0440\u0430\u043D (SSL) \u043F\u043E\u0442\u043E\u043A +helppopup.streamport.text =

\u0422\u0430\u0437\u0438 \u043E\u043F\u0446\u0438\u044F \u0435 \u0432\u044A\u0437\u043C\u043E\u0436\u043D\u0430 \u0441\u0430\u043C\u043E \u0430\u043A\u043E \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0442\u0435 {0} \u043D\u0430 \u0441\u044A\u0440\u0432\u044A\u0440 \u0441\u044A\u0441 SSL (HTTPS).

\u041D\u044F\u043A\u043E\u0438 \u043F\u043B\u0435\u044A\u0440\u0438 \ + (\u043A\u0430\u0442\u043E Winamp) \u043D\u0435 \u043F\u043E\u0434\u0434\u044A\u0440\u0436\u0430\u0442 \u043F\u043E\u0442\u043E\u0447\u043D\u043E \u0430\u0443\u0434\u0438\u043E \u043F\u043E\u0434 SSL. \u041F\u043E\u0441\u043E\u0447\u0435\u0442\u0435 \u043D\u043E\u043C\u0435\u0440\u0430 \u043D\u0430 \u043F\u043E\u0440\u0442\u0430 \u0437\u0430 \u0441\u0442\u0430\u043D\u0434\u0430\u0440\u0442\u0435\u043D http (\u043E\u0431\u0438\u043A\u043D\u043E\u0432\u0435\u043D\u043E \u0435 80 \ + \u0438\u043B\u0438 4040) \u0430\u043A\u043E \u043D\u0435 \u0436\u0435\u043B\u0430\u0435\u0442\u0435 \u043F\u043E\u0442\u043E\u043A\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u0438\u0437\u043B\u044A\u0447\u0432\u0430\u043D \u043F\u043E\u0434 SSL. \u0412 \u0442\u043E\u0437\u0438 \u0441\u043B\u0443\u0447\u0430\u0439, \u043F\u043E\u0442\u043E\u043A\u0430 \u043D\u044F\u043C\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 \u043A\u0440\u0438\u043F\u0442\u0438\u0440\u0430\u043D.

+helppopup.ldap.title = LDAP \u043F\u043E\u0442\u0432\u044A\u0440\u0436\u0434\u0430\u0432\u0430\u043D\u0435 +helppopup.ldap.text =

\u041F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438\u0442\u0435 \u043C\u043E\u0433\u0430\u0442 \u0434\u0430 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u0442 \u043F\u0440\u0438 \u0432\u0445\u043E\u0434 \u043F\u043E\u0442\u0432\u044A\u0440\u0436\u0434\u0435\u043D\u0438\u0435 \u043E\u0442 \u0432\u044A\u043D\u0448\u0435\u043D LDAP \u0441\u044A\u0440\u0432\u044A\u0440 (\u0432\u043A\u043B\u044E\u0447\u0438\u0442\u0435\u043B\u043D\u043E Windows Active Directory). \ + \u041A\u043E\u0433\u0430\u0442\u043E LDAP-\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043D\u0438 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438 \u0432\u043B\u0438\u0437\u0430\u0442 \u0432 \u043F\u0440\u043E\u0444\u0438\u043B\u0430 \u0441\u0438 \u0432 {0}, \u0442\u044F\u0445\u043D\u043E\u0442\u043E \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u043C\u0435 \u0438 \u043F\u0430\u0440\u043E\u043B\u0430 \u0441\u0435 \u043F\u0440\u043E\u0432\u0435\u0440\u044F\u0432\u0430\u0442 \u043E\u0442 \u0432\u044A\u043D\u0448\u0435\u043D \u0441\u044A\u0440\u0432\u044A\u0440, \u0430 \u043D\u0435 \u043E\u0442 {0} .

+helppopup.ldapurl.title = LDAP URL +helppopup.ldapurl.text =

URL \u0430\u0434\u0440\u0435\u0441\u0430 \u043D\u0430 LDAP \u0441\u044A\u0440\u0432\u044A\u0440\u0430. \u041F\u0440\u043E\u0442\u043E\u043A\u043E\u043B\u044A\u0442 \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u0431\u044A\u0434\u0435 ldap:// \u0438\u043B\u0438 ldaps:// \ + (\u0437\u0430 LDAP \u043F\u043E\u0434 SSL). \u0412\u0438\u0436\u0442\u0435 \u0442\u0443\u043A \ + \u0437\u0430 \u043F\u043E\u0432\u0435\u0447\u0435 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438.

+helppopup.ldapsearchfilter.title = LDAP \u0444\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u0442\u044A\u0440\u0441\u0435\u043D\u0435 +helppopup.ldapsearchfilter.text =

\u0424\u0438\u043B\u0442\u044A\u0440, \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u043D \u0437\u0430 \u0442\u044A\u0440\u0441\u0435\u043D\u0435 \u043D\u0430 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438. \u0422\u043E\u0432\u0430 \u0435 LDAP \u0444\u0438\u043B\u0442\u044A\u0440 \u0437\u0430 \u0442\u044A\u0440\u0441\u0435\u043D\u0435 \ + (\u043A\u0430\u043A\u0442\u043E \u0435 \u043E\u043F\u0438\u0441\u0430\u043D \u0432 RFC 2254). \ + \u0428\u0430\u0431\u043B\u043E\u043D\u044A\u0442 "'{0'}" \u0441\u0435 \u0437\u0430\u043C\u0435\u0441\u0442\u0432\u0430 \u0441 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E\u0442\u043E \u0438\u043C\u0435, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: \ +

    \ +
  • (uid='{0'}) - \u0449\u0435 \u0442\u044A\u0440\u0441\u0438 \u0437\u0430 \u0441\u044A\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435 \u0441 \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u043C\u0435 \u0441\u043F\u043E\u0440\u0435\u0434 uid \u0444\u0430\u043A\u0442\u043E\u0440\u0430.
  • \ +
  • (sAMAccountName='{0'}) - \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430\u043D \u043F\u0440\u0435\u0434\u0438\u043C\u043D\u043E \u0437\u0430 \u0432\u0445\u043E\u0434 \u0432 Microsoft Active Directory.
  • \ +

+helppopup.ldapmanagerdn.title = LDAP \u043C\u0435\u043D\u0438\u0434\u0436\u044A\u0440 DN +helppopup.ldapmanagerdn.text =

\u0410\u043A\u043E LDAP \u0441\u044A\u0440\u0432\u044A\u0440\u0430 \u043D\u0435 \u043F\u043E\u0434\u0434\u044A\u0440\u0436\u0430 \u0430\u043D\u043E\u043D\u0438\u043C\u043D\u0430 \u0432\u0440\u044A\u0437\u043A\u0430, \u0442\u0440\u044F\u0431\u0432\u0430 \u0434\u0430 \u043F\u043E\u0441\u043E\u0447\u0438\u0442\u0435 DN \ + (Distinguished Name) \u0438 \u043F\u0430\u0440\u043E\u043B\u0430 \u043D\u0430 LDAP \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B \u0437\u0430 \u043E\u0441\u044A\u0449\u0435\u0441\u0442\u0432\u044F\u0432\u0430\u043D\u0435 \u043D\u0430 \u0432\u0440\u044A\u0437\u043A\u0430\u0442\u0430.

+helppopup.ldapautoshadowing.title = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u0441\u044A\u0437\u0434\u0430\u0432\u0430\u043D\u0435 \u043D\u0430 LDAP \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438 \u0432 {0} +helppopup.ldapautoshadowing.text =

\u0410\u043A\u043E \u0442\u0430\u0437\u0438 \u043E\u043F\u0446\u0438\u044F \u0435 \u043C\u0430\u0440\u043A\u0438\u0440\u0430\u043D\u0430, LDAP \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0438\u0442\u0435 \u043D\u0435 \u0435 \u043D\u0443\u0436\u043D\u043E \u0434\u0430 \u0431\u044A\u0434\u0430\u0442 \u0441\u044A\u0437\u0434\u0430\u0432\u0430\u043D\u0438 \u0440\u044A\u0447\u043D\u043E \u0432 {0} \u043F\u0440\u0435\u0434\u0438 \u0434\u0430 \u043C\u043E\u0433\u0430\u0442 \u0434\u0430 \u0432\u043B\u044F\u0437\u0430\u0442 \u0432 \u043F\u0440\u043E\u0444\u0438\u043B\u0430 \u0441\u0438.

\ +

\u0412\u043D\u0438\u043C\u0430\u043D\u0438\u0435! \u0422\u043E\u0432\u0430 \u043E\u0437\u043D\u0430\u0447\u0430\u0432\u0430, \u0447\u0435 \u0432\u0441\u0435\u043A\u0438 \u0435\u0434\u0438\u043D \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B \u0441 \u0432\u0430\u043B\u0438\u0434\u043D\u043E LDAP \u043F\u043E\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043B\u0441\u043A\u043E \u0438\u043C\u0435 \u0438 \u043F\u0430\u0440\u043E\u043B\u0430 \u043C\u043E\u0436\u0435 \u0434\u0430 \u0432\u043B\u0435\u0437\u0435 \u0432 {0}, \ + \u043A\u043E\u0435\u0442\u043E \u043C\u043E\u0436\u0435 \u0434\u0430 \u043D\u0435 \u0436\u0435\u043B\u0430\u0435\u0442\u0435.

+helppopup.playername.title = \u0417\u0430\u0433\u043B\u0430\u0432\u0438\u0435 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430 +helppopup.playername.text =

\u041C\u043E\u0436\u0435\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043B\u0435\u0441\u043D\u043E \u0437\u0430 \u0437\u0430\u043F\u043E\u043C\u043D\u044F\u043D\u0435 \u0438\u043C\u0435 \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 "\u0421\u043B\u0443\u0436\u0435\u0431\u0435\u043D" \u0438\u043B\u0438 "\u0414\u043E\u043C\u0430\u0448\u0435\u043D".

+helppopup.autocontrol.title = \u0410\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u043D \u043A\u043E\u043D\u0442\u0440\u043E\u043B \u043D\u0430 \u043F\u043B\u0435\u044A\u0440\u0430 +helppopup.autocontrol.text =

\u0410\u043A\u043E \u0435 \u0438\u0437\u0431\u0440\u0430\u043D\u0430 \u0442\u0430\u0437\u0438 \u043E\u043F\u0446\u0438\u044F, {0} \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u043D\u043E \u0449\u0435 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430 \u043F\u043B\u0435\u044A\u0440\u0430, \u043A\u043E\u0433\u0430\u0442\u043E \u043D\u0430\u0442\u0438\u0441\u043D\u0435\u0442\u0435 "\u041F\u0443\u0441\u043D\u0438" \ + \u0432 \u043F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430\u0442\u0430. \u0412 \u043F\u0440\u043E\u0442\u0438\u0432\u0435\u043D \u0441\u043B\u0443\u0447\u0430\u0439, \u0442\u0440\u044F\u0431\u0432\u0430 \u0440\u044A\u0447\u043D\u043E \u0434\u0430 \u0441\u0442\u0430\u0440\u0442\u0438\u0440\u0430\u0442\u0435 \u0438 \u043F\u0443\u0441\u043A\u0430\u0442\u0435 \u043F\u043B\u0435\u044A\u0440\u0430.

+helppopup.dynamicip.title = \u0414\u0438\u043D\u0430\u043C\u0438\u0447\u0435\u043D IP \u0430\u0434\u0440\u0435\u0441 +helppopup.dynamicip.text =

\u0418\u0437\u043A\u043B\u044E\u0447\u0435\u0442\u0435 \u0442\u0430\u0437\u0438 \u043E\u043F\u0446\u0438\u044F, \u0430\u043A\u043E \u043F\u043B\u0435\u044A\u0440\u0430 \u0438\u0437\u043F\u043E\u043B\u0437\u0432\u0430 \u0441\u0442\u0430\u0442\u0438\u0447\u0435\u043D IP \u0430\u0434\u0440\u0435\u0441.

+ +# wap/index.jsp +wap.index.missing = \u041D\u044F\u043C\u0430 \u043D\u0430\u043C\u0435\u0440\u0435\u043D\u0430 \u043C\u0443\u0437\u0438\u043A\u0430 +wap.index.playlist = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +wap.index.search = \u0422\u044A\u0440\u0441\u0438 +wap.index.settings = \u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 + +# wap/browse.jsp +wap.browse.playone = \u041F\u0443\u0441\u043D\u0438 +wap.browse.playall = \u041F\u0443\u0441\u043D\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 +wap.browse.addone = \u0414\u043E\u0431\u0430\u0432\u0438 +wap.browse.addall = \u0414\u043E\u0431\u0430\u0432\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 +wap.browse.downloadone = \u0421\u0432\u0430\u043B\u0438 +wap.browse.downloadall = \u0421\u0432\u0430\u043B\u0438 \u0432\u0441\u0438\u0447\u043A\u0438 + +# wap/playlist.jsp +wap.playlist.title = \u041F\u043B\u0435\u0439\u043B\u0438\u0441\u0442\u0430 +wap.playlist.noplayer = \u041D\u0435 \u0435 \u043D\u0430\u043C\u0435\u0440\u0435\u043D \u043F\u043B\u0435\u044A\u0440 +wap.playlist.clear = \u0418\u0437\u0447\u0438\u0441\u0442\u0438 +wap.playlist.load = \u0417\u0430\u0440\u0435\u0434\u0438 +wap.playlist.random = \u0421\u043B\u0443\u0447\u0430\u0439\u043D\u0438 +wap.playlist.play = \u041F\u0443\u0441\u043D\u0438 \u043D\u0430 \u0442\u0435\u043B\u0435\u0444\u043E\u043D\u0430 + +# wap/search.jsp +wap.search.title = \u0422\u044A\u0440\u0441\u0435\u043D\u0435 + +# wap/searchResult.jsp +wap.searchresult.index = \u0418\u043D\u0434\u0435\u043A\u0441\u044A\u0442 \u043D\u0430 \u0442\u044A\u0440\u0441\u0430\u0447\u043A\u0430\u0442\u0430 \u0441\u0435 \u0441\u044A\u0437\u0434\u0430\u0432\u0430 \u0432 \u043C\u043E\u043C\u0435\u043D\u0442\u0430. \u041C\u043E\u043B\u044F \u043E\u043F\u0438\u0442\u0430\u0439\u0442\u0435 \u043F\u043E-\u043A\u044A\u0441\u043D\u043E. + +# wap/settings.jsp +wap.settings.selectplayer = \u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043F\u043B\u0435\u044A\u0440 +wap.settings.allplayers = \u0412\u0441\u0438\u0447\u043A\u0438 \ No newline at end of file diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties new file mode 100644 index 00000000..1e34fd0e --- /dev/null +++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_ca.properties @@ -0,0 +1,713 @@ +# +# Catalan localization. +# Author: Josep Santalo Jordana (jsantajor at gmail.com) +# + +common.home = Inici +common.back = Enrere +common.help = Ajuda +common.play = Reproduir +common.add = Afegir +common.download = Descarregar +common.close = Tancar +common.refresh = Actualitzar +common.next = Seg\u00FCent +common.previous = Anterior +common.more = M\u00E9s +common.ok = OK +common.cancel = Cancel\u00B7la +common.save = Guardar +common.create = Crear +common.delete = Esborrar +common.unknown = (Desconegut) +common.default = (Predeterminat) + +# login.jsp +login.username = Usuari +login.password = Contrasenya +login.login = Iniciar sessi\u00F3 +login.remember = Recordat +login.logout = Desconnexi\u00F3 correcta. +login.error = Usuari o contrasenya incorrecta. +login.insecure = {0} no \u00E9s segur. Si us plau inicieu sessi\u00F3 amb usuari
i contrasenya "admin", o cliqui aqu\u00ED. Tot seguit, canvi\u00EF la contrasenya el m\u00E9s r\u00E0pid possible. + +# accessDenied.jsp +accessDenied.title = Acc\u00E9s denegat +accessDenied.text = Vost\u00E8 no est\u00E0 autoritzat a realitzar aquesta operaci\u00F3. + +# top.jsp +top.home = Inici +top.now_playing = Reproduint +top.settings = Configuraci\u00F3 +top.status = Estat +top.podcast = Podcast +top.more = M\u00E9s +top.help = Ajuda +top.search = Buscar +top.upgrade = Una nova versi\u00F3 est\u00E0 disponible. Descarregar {0} {1} \ + aqu\u00ED. +top.missing = No s'ha trobat cap directori. Si us plau, canvi\u00EF la configuraci\u00F3. +top.logout = Desconnectar {0} + +# left.jsp +left.statistics = {0} artistes
\ + {1} \u00E0lbums
\ + {2} can\u00E7ons
\ + {3}
\ + {4} hores +left.shortcut = Acc\u00E9s directe +left.radio = Internet TV/radio +left.allfolders = Tots els directoris + +# playlist.jsp +playlist.stop = Parar +playlist.start = Reproduir +playlist.confirmclear = Realment vol netejar la llista? +playlist.clear = Netejar +playlist.shuffle = Mode aleatori +playlist.repeat_on = Desactivar repetir +playlist.repeat_off = Activar repetir +playlist.undo = Desfer +playlist.settings = Configuraci\u00F3 +playlist.more = M\u00E9s accions... +playlist.more.playlist = Llista de reproducci\u00F3 +playlist.more.sortbytrack = Ordenar per pista +playlist.more.sortbyartist = Ordenar per artista +playlist.more.sortbyalbum = Ordenar per \u00E0lbum +playlist.more.selection = Can\u00E7ons seleccionades +playlist.more.selectall = Seleccionar-ho tot +playlist.more.selectnone = Esborrar selecci\u00F3 +playlist.getflash = Get Flash player +playlist.save = Guardar +playlist.append = Afegir a la llista de reproducci\u00F3 +playlist.remove = Esborrar +playlist.up = Amunt +playlist.down = Avall +playlist.empty = La llista de reproducci\u00F3 est\u00E0 buida + +# videoPlayer.jsp +videoPlayer.getflash = Si us plau, instal\u00B7li Flash Player +videoPlayer.popout = Obrir a una nova finestra + +# loadPlaylist.jsp +playlist.load.title = Carregar llista de reproducci\u00F3 +playlist.load.appendtitle = Afegir a la llista de reproducci\u00F3 +playlist.load.load = Carregar +playlist.load.append = Afegir +playlist.load.delete = Eliminar +playlist.load.confirm_delete = Realment vol eliminar la llista de reproducci\u00F3? +playlist.load.missing_folder = El directori de la llista de reproducci\u00F3 "{0}" no existeix. Si us plau, canvi\u00EF la configuraci\u00F3. +playlist.load.empty = No hi ha cap llista de reproducci\u00F3 disponible. + +# savePlaylist.jsp +playlist.save.title = Guardar la llista de reproducci\u00F3 +playlist.save.save = Guardar +playlist.save.name = Nom de la llista de reproducci\u00F3 +playlist.save.format = Format +playlist.save.missing_folder = El directori de la llista de reproducci\u00F3 "{0}" no existeix. Si us plau, canvi\u00EF la configuraci\u00F3. +playlist.save.noname = Si us plau, especifiqui el nom de la llista de reproducci\u00F3. + +# status.jsp +status.title = Estat +status.type = Tipus +status.stream = Stream +status.download = Descarregar +status.upload = Pujar +status.player = Oient +status.user = Usuari +status.current = Can\u00E7\u00F3 +status.transmitted = Transm\u00E8s +status.bitrate = Bitrate (Kbps) + +# search.jsp +search.title = Buscar +search.query = Artista, \u00E0lbum o nom de la can\u00E7\u00F3 +search.search = Buscar +search.index = L'\u00EDndex de cerca s'est\u00E0 creant en aquest moment. Si us plau torni-ho a intentar m\u00E9s tard. +search.hits.none = No s'han trobat resultats. +search.hits.more = M\u00E9s +search.hits.artists = Artistes +search.hits.albums = \u00C0lbums +search.hits.songs = Can\u00E7ons + +# gettingStarted.jsp +gettingStarted.title = Primers passos +gettingStarted.text =

Benvingut a Subsonic! Per tal de configurar el programa de la manera m\u00E9s r\u00E0pida possible, nom\u00E9s cal que segueixi els seg\u00FCent passos.
\ + Cliqui al bot\u00F3 d''Inici que hi ha a la barra superior per tal de tornar a aquesta p\u00E0gina.

\ +

Per a m\u00E9s informaci\u00F3, consulti la p\u00E0gina la guia Getting started.

+gettingStarted.step1.title = Canviar la contrasenya de l'administrador. +gettingStarted.step1.text = Per tal de fer m\u00E9s segur el seu servidor es recomana canviar la contrasenya per defecte de l''administrador. \ + Tamb\u00E9 pot crear comptes d'usuari nous amb diferents privilegis associats. +gettingStarted.step2.title = Configuri els directoris multim\u00E8dia. +gettingStarted.step2.text = Indiqui a Subsonic a on guarda els arxius de m\u00FAsica i de v\u00EDdeo. +gettingStarted.step3.title = Configuri els par\u00E0metres de xarxa. +gettingStarted.step3.text = Par\u00E0metres \u00FAtil per tal de gaudir de Subsonic remotament a trav\u00E9s de Internet, \ + o compartir-ho amb la fam\u00EDlia i amics. Aconsegueixi la seva adre\u00E7a personal el_seu_nom.subsonic.org. +gettingStarted.hide = No mostrar aquest missatge de nou +gettingStarted.hidealert = Per tal de tornar a mostrar aquest missatge, accedeixi a Configuraci\u00F3 > General. + +# home.jsp +home.random.title = Aleatori +home.newest.title = El m\u00E9s nou +home.highest.title = Els m\u00E9s valorats +home.frequent.title = Escoltats freq\u00FCentment +home.recent.title = Escoltats recentment +home.users.title = Usuaris +home.random.text = \u00C0lbums aleatoris +home.newest.text = \u00C0lbums afegits o modificats recentment +home.highest.text = \u00C0lbums m\u00E9s ben valorats +home.frequent.text = \u00C0lbums freq\u00FCentment escoltats +home.recent.text = \u00C0lbums recentment escoltats +home.users.text = Estad\u00EDstiques d'usuari +home.scan = El directori de m\u00FAsica s''est\u00E0 escanejant ara mateix. Encara no estan disponibles totes les caracter\u00EDstiques. +home.albums = \u00C0lbums {0} - {1} +home.playcount = Reprodu\u00EFts {0} vegades +home.lastplayed = Reprodu\u00EFts {0} +home.created = Modificats {0} +home.chart.total = Total (MB) +home.chart.stream = Streamed (MB) +home.chart.download = Descarregats (MB) +home.chart.upload = Pujats (MB) + +# more.jsp +more.title = M\u00E9s +more.random.title = Llista de reproducci\u00F3 aleat\u00F2ria +more.random.text = Crear llista de reproducci\u00F3 aleat\u00F2ria amb +more.random.songs = {0} can\u00E7ons +more.random.auto = Reprodueixi m\u00E9s can\u00E7ons de manera aleat\u00F2ria quan s''arribi al final de la llista de reproducci\u00F3. +more.random.ok = OK +more.random.genre = segons g\u00E8nere +more.random.anygenre = Qualsevol +more.random.year = i any +more.random.anyyear = Qualsevol +more.random.folder = i directori +more.random.anyfolder = Qualsevol +more.apps.title = Subsonic Apps +more.apps.text =

Hi ha aplicacions de Subsonic disponibles per a Android, iPhone, \ + Windows Phone 7 i AIR.

+more.mobile.title = Tel\u00E8fon m\u00F2bil +more.mobile.text =

Vost\u00E8 pot controlar {0} amb qualsevol m\u00F2bil que tingui el WAP activat o amb una PDA.
\ + Simplement ha d''accedir a la seg\u00FCent URL des del dispositiu: http://yourhostname/wap

\ +

Per tal que aix\u00F2 funcioni, necessita que el seu servidor si pugui accedir d\u00E9s de Internet.

+more.podcast.title = Podcast +more.podcast.text =

Llistes de reproducci\u00F3 emmagatzemades com a Podcasts.
\ + Usi la seg\u00FCent URL en el seu Podcast: http://yourhostname/podcast, \ + o b\u00E9 cliqui aqu\u00ED.

+more.upload.title = Pujar arxiu +more.upload.source = Seleccionar arxiu +more.upload.target = Pujar a +more.upload.browse = Escollir +more.upload.ok = Pujar +more.upload.unzip = Arxiu ZIP auto-descomprimible +more.upload.progress = % completat. Si us plau, esperi... + + +# upload.jsp +upload.title = Pujant arxiu +upload.success = Pujada completada {0} +upload.empty = No hi ha arxius per a pujar. +upload.failed = Ha fallat la pujada degut al seg\u00FCent error:
"{0}" +upload.unzipped = Descomprimit {0} + +# help.jsp +help.title = Quant a {0} +help.upgrade = Avis! Una nova versi\u00F3 est\u00E0 disponible.
Descarregar {0} {1} \ + aqu\u00ED. +help.version.title = Versi\u00F3 +help.builddate.title = Data de creaci\u00F3 +help.server.title = Server +help.license.title = Llic\u00E8ncia +help.license.text = {0} es software lliure distribu\u00EFt sota llic\u00E8ncia de codi obert GPL. \ + {0} usa llibreries de tercers sota les respectives llic\u00E8ncies. Si us plau, noti que {0} NO \ + \u00E9s una eina per a la distribuci\u00F3 il\u00B7legal de material amb copyright. Prengui atenci\u00F3 a les lleis espec\u00EDfiques del seu pa\u00EDs envers aquest punt. +help.homepage.title = P\u00E0gina web del projecte +help.forum.title = F\u00F2rum +help.shop.title = Merchandise +help.contact.title = Contacte +help.contact.text = {0} est\u00E0 desenvolupat i mantingut per Sindre Mehus \ + (sindre@activeobjects.no). \ + Si vost\u00E8 t\u00E9 alguna pregunta, comentari o suggeriment, si us plau visiti \ + Subsonic Forum. +help.log = Log +help.logfile = El log complet esta guardat a {0}. + +# settingsHeader.jsp +settingsheader.title = Configuracions +settingsheader.general = General +settingsheader.advanced = Avan\u00E7at +settingsheader.personal = Aparen\u00E7a +settingsheader.musicFolder = Directoris de m\u00FAsica +settingsheader.internetRadio = Internet TV/radio +settingsheader.podcast = Podcast +settingsheader.player = Oients +settingsheader.share = Shared media +settingsheader.network = Xarxa +settingsheader.transcoding = Canviar format +settingsheader.user = Usuaris +settingsheader.search = Buscar +settingsheader.coverArt = Car\u00E0tula +settingsheader.password = Contrasenya + +# generalSettings.jsp +generalsettings.playlistfolder = Directori de la llista de reproducci\u00F3 +generalsettings.musicmask = Arxius de m\u00FAsica +generalsettings.videomask = Arxius de v\u00EDdeo +generalsettings.coverartmask = Arxius de car\u00E0tula +generalsettings.index = \u00CDndex +generalsettings.ignoredarticles = Ignorar articles +generalsettings.shortcuts = Accessos directes +generalsettings.showgettingstarted = Mostrar "Primers passos" a l'inici +generalsettings.welcometitle = T\u00EDtol de benvinguda +generalsettings.welcomesubtitle = Subt\u00EDtol de benvinguda +generalsettings.welcomemessage = Missatge de benvinguda +generalsettings.loginmessage = Missatge d'inici de sessi\u00F3 +generalsettings.language = Idioma predeterminat +generalsettings.theme = Tema predeterminat + +# advancedSettings.jsp +advancedsettings.downsamplecommand = Comanda de disminuci\u00F3 de resoluci\u00F3 +advancedsettings.coverartlimit = L\u00EDmit de la mida de la car\u00E0tula
(0 = Sense l\u00EDmit)
+advancedsettings.downloadlimit = L\u00EDmit de baixada (Kbps)
(0 = Sense l\u00EDmit)
+advancedsettings.uploadlimit = L\u00EDmit de pujada (Kbps)
(0 = Sense l\u00EDmit)
+advancedsettings.streamport = N\u00FAmero del port SSL
(0 = Desactivat)
+advancedsettings.ldapenabled = Activar autenticaci\u00F3 LDAP +advancedsettings.ldapurl = LDAP URL +advancedsettings.ldapsearchfilter = Filtre de cerca LDAP +advancedsettings.ldapmanagerdn = Gestor LDAP DN
(Opcional)
+advancedsettings.ldapmanagerpassword = Contrasenya +advancedsettings.ldapautoshadowing = Crear usuaris de manera autom\u00E0tica {0} + +# personalSettings.jsp +personalsettings.title = Configuraci\u00F3 d'aparen\u00E7a per a {0} +personalsettings.language = Idioma +personalsettings.theme = Tema +personalsettings.display = Pantalla +personalsettings.browse = Navegador +personalsettings.playlist = Llista de reproducci\u00F3 +personalsettings.tracknumber = Pista # +personalsettings.artist = Artista +personalsettings.album = \u00C0lbum +personalsettings.genre = G\u00E8nere +personalsettings.year = Any +personalsettings.bitrate = Bit rate +personalsettings.duration = Duraci\u00F3 +personalsettings.format = Format +personalsettings.filesize = Mida de l'arxiu +personalsettings.captioncutoff = Car\u00E0cters visibles +personalsettings.partymode = Mode Festa +personalsettings.shownowplaying = Mostrar el que altres escolten +personalsettings.nowplayingallowed = Permetre als altres veure el que escolto +personalsettings.showchat = Motrar els missatges del Xat +personalsettings.finalversionnotification = Notifica'm sobre noves versions +personalsettings.betaversionnotification = Notifica'm sobre noves versions beta +personalsettings.lastfmenabled = Registrar el que estic reproduint a Last.fm +personalsettings.lastfmusername = Nom d'usuari de Last.fm +personalsettings.lastfmpassword = Contrasenya de Last.fm +personalsettings.avatar.title = Imatge personal +personalsettings.avatar.none = Sense imatge +personalsettings.avatar.custom = Imatge personalitzada +personalsettings.avatar.changecustom = Canviar la imatge personalitzada +personalsettings.avatar.upload = Pujar +personalsettings.avatar.courtesy = Icones cortesia de Afterglow, \ + Aha-Soft, \ + Icons-Land, i \ + Iconshock + +# avatarUploadResult.jsp +avataruploadresult.title = Canviar imatge personal +avataruploadresult.success = Imatge personal carregada correctament "{0}". +avataruploadresult.failure = S'ha produ\u00EFt un error al carregar la imatge. Vegi el log per a obtenir m\u00E9s detalls. + +# passwordSettings.jsp +passwordsettings.title = Canviar contrasenya per {0} + +# musicFolderSettings.jsp +musicfoldersettings.path = Directori +musicfoldersettings.name = Nom +musicfoldersettings.enabled = Activat +musicfoldersettings.add = Afegir directori multim\u00E8dia +musicfoldersettings.nopath = Si us plau, especifiqui un directori. +musicfoldersettings.notfound = Directori desconegut +musicfoldersettings.scan = Escanejar directoris multim\u00E8dia +musicfoldersettings.interval.never = Mai +musicfoldersettings.interval.one = Cada dia +musicfoldersettings.interval.many = Cada {0} dies +musicfoldersettings.hour = a les {0}:00 +musicfoldersettings.nowscanning = S'est\u00E0 realitzant l'escaneig dels directoris multim\u00E8dia. Aquest proc\u00E9s tardar\u00E0 uns quants minuts en funci\u00F3 de \ + la mida de la vostre biblioteca multim\u00E8dia. +musicfoldersettings.scannow = Escanejar ara els directoris multim\u00E8dia +musicfoldersettings.fastcache = Mode d'acc\u00E9s r\u00E0pid +musicfoldersettings.fastcache.description = Usi aquesta opci\u00F3 per tal de minimitzar l'acc\u00E9s a disc, per exemple si els arxius es troben a un disc de xarxa. \ + Note: Els canvis d'aquests arxius nom\u00E9s seran visibles despr\u00E9s del proc\u00E9s d'escaneig. (veure m\u00E9s amunt). + +musicfoldersettings.organizebyfolderstructure = Organitzar segons l'estructura dels directoris +musicfoldersettings.organizebyfolderstructure.description = Usi aquesta opci\u00F3 per tal de navegar per la seva biblioteca multim\u00E8dia usant l'estructura dels directoris enlloc dels ID3 tags artista/\u00E0lbum. + +# networkSettings.jsp +networksettings.text = Usi els seg\u00FCents par\u00E0metres per a controlar com s'accedeix al seu servidor Subsonic a trav\u00E9s de Internet.
\ + Si experimenta algun tipus de contratemps, visiti la guia de Primers passos. +networksettings.portforwardingenabled = Configuri el seu router de manera autom\u00E0tica per tal de permetre les connexions entrants a Subsonic (usant reenviament de ports UPnP o NAT-PMP). +networksettings.portforwardinghelp = Si el seu router no es pot configurar de manera autom\u00E0tica pot intentar configurar-lo de manera manual. \ + Pot intentar seguir les instruccions de portforward.com. \ + Ha de reenviar el port {0} a l'ordinador on s'est\u00E0 executant el servidor de Subsonic. +networksettings.urlredirectionenabled = Accedeixi al seu servidor a trav\u00E9s de Internet usant una direcci\u00F3 f\u00E0cil de recordar. +networksettings.status = Estat: + +# transcodingSettings.jsp +transcodingsettings.name = Nom +transcodingsettings.sourceformat = Convertir de +transcodingsettings.targetformat = Convertir a +transcodingsettings.step1 = Pas 1 +transcodingsettings.step2 = Pas 2 +transcodingsettings.step3 = Pas 3 +transcodingsettings.noname = Si us plau, especifiqui un nom. +transcodingsettings.nosourceformat = Si us plau, especifiqui un format des d'on convertir. +transcodingsettings.notargetformat = Si us plau, especifiqui un format a on convertir. +transcodingsettings.nostep1 = Si us plau, especifiqui com a m\u00EDnim un pas per a canviar de format. +transcodingsettings.info =

(%s = el format de l'arxiu que volem canviar, %b = Bitrate m\u00E0xima del reproductor)

\ +

El canvi de format d'un arxiu de so \u00E9s el pas d'una codificaci\u00F3 a una altra. El canvio de format de {1} \ + permet fer streaming de so que normalment no es podria dur a terme. El canvio de format es fa en temps de reproducci\u00F3 i no \ + necessita espai de disc extra.

\ +

El canvi de format es realitza mitjan\u00E7ant programes de l\u00EDnia de comandes de tercers els quals s'han de trobar instal\u00B7lats en {0}. \ + Un paquet de windows pel canvi de format \ + est\u00E0 disponible aqu\u00ED. Vost\u00E8 pot afegir el seu propi programa \ + si compleix els seg\u00FCent requisits: \ +

    \ +
  • Ha de tenir una interf\u00EDcie de l\u00EDnia de comandes.
  • \ +
  • Ha de ser capa\u00E7 d'enviar la sortida a stdout.
  • \ +
  • Si s'usa el pas 2 o 3 ha de ser capa\u00E7 de llegir l'entrada de stdin.
  • \ +
\ +

\ +

Cal remarcar que el canvio de codificaci\u00F3 s'activa en el reproductor des de la p\u00E0gina de configuraci\u00F3.

+ +# internetRadioSettings.jsp +internetradiosettings.streamurl = URL del stream +internetradiosettings.homepageurl = P\u00E0gina principal +internetradiosettings.name = Nom +internetradiosettings.enabled = Habilitat +internetradiosettings.add = Afegir Internet TV/radio +internetradiosettings.nourl = Especifiqui un URL. +internetradiosettings.noname = Especifiqui un nom. + +# podcastSettings.jsp +podcastsettings.update = Comprovar disponibilitat de nous episodis +podcastsettings.keep = Mantenir +podcastsettings.keep.all = Tots els episodis +podcastsettings.keep.one = Episodi m\u00E9s recent +podcastsettings.keep.many = \u00DAltim {0} episodi +podcastsettings.download = Quan nous episodis estiguin disponibles +podcastsettings.download.all = Descarregar-ho tot +podcastsettings.download.one = Descarregar el m\u00E9s recent +podcastsettings.download.many = Descarregar els \u00FAltims {0} episodis +podcastsettings.download.none = No facis res +podcastsettings.interval.manually = Manualment +podcastsettings.interval.hourly = Cada hora +podcastsettings.interval.daily = Cada dia +podcastsettings.interval.weekly = Cada setmana +podcastsettings.folder = Guardar els Podcasts a + +# playerSettings.jsp +playersettings.noplayers = No s'ha trobat cap oient. +playersettings.type = Tipus +playersettings.lastseen = \u00DAltim av\u00EDs +playersettings.title = Seleccioni un oient +playersettings.technology.web.title = Reproductor Web +playersettings.technology.external.title = Reproductor Extern +playersettings.technology.external_with_playlist.title = Reproductor Extern amb llista de reproducci\u00F3 +playersettings.technology.jukebox.title = Jukebox +playersettings.technology.web.text = Reprodueixi m\u00FAsica directament a un navegador web usant Flash player integrat. +playersettings.technology.external.text = Reprodueixi m\u00FAsica al seu reproductor preferit com ara WinAmp o Windows Media Player. +playersettings.technology.external_with_playlist.text = El mateix que el cas anterior per\u00F2 la llista de reproducci\u00F3 es controla pel reproductor enlloc \ + del servidor Subsonic. En aquest mode saltar-se can\u00E7ons \u00E9s possible. +playersettings.technology.jukebox.text = Reprodueixi m\u00FAsica directament a un aparell d''\u00E0udio de Subsonic. (Nom\u00E9s usuaris autoritzats). +playersettings.name = Nom de l'oient +playersettings.coverartsize = Mida de la car\u00E0tula +playersettings.maxbitrate = Bitrate m\u00E0xim +playersettings.coverart.off = Off +playersettings.coverart.small = Petit +playersettings.coverart.medium = Mitj\u00E0 +playersettings.coverart.large = Gran +playersettings.notranscoder = Not\u00EDcia: Sembla que ffmpeg no est\u00E0 instal\u00B7lat.
Cliqui el bot\u00F3 d'ajuda per a m\u00E9s informaci\u00F3. +playersettings.autocontrol = Controla el reproductor de manera autom\u00E0tica +playersettings.dynamicip = L'oient t\u00E9 IP din\u00E0mica +playersettings.transcodings = Canvi de format activat +playersettings.ok = Guardar +playersettings.forget = Esborrar oient +playersettings.clone = Copiar oient + +# shareSettings.jsp +sharesettings.name = Nom +sharesettings.owner = Compartit per +sharesettings.description = Descripci\u00F3 +sharesettings.visits = Visites +sharesettings.lastvisited = \u00DAltima visita +sharesettings.expires = Expira +sharesettings.files = Arxius compartits +sharesettings.expirein = Expira en +sharesettings.expirein.week = 1w +sharesettings.expirein.month = 1m +sharesettings.expirein.year = 1y +sharesettings.expirein.never = mai + +# userSettings.jsp +usersettings.title = Seleccionar usuari +usersettings.newuser = Nou usuari +usersettings.admin = L'usuari \u00E9s administrador +usersettings.settings = L'usuari pot canviar par\u00E0metres i la contrasenya +usersettings.stream = L'usuari pot reproduir arxius +usersettings.jukebox = L'usuari pot reproduir arxius en el mode jukebox +usersettings.download = L'usuari pot descarregar arxius +usersettings.upload = L'usuari pot pujar arxius al servidor +usersettings.share = L'usuari pot compartir arxius amb qualsevol +usersettings.coverart = L'usuari pot canviar car\u00E0tules i els tags +usersettings.comment= L'usuari pot crear i editar comentaris i qualificacions +usersettings.podcast= L'usuari pot administrar Podcasts +usersettings.username = Nom de l'usuari +usersettings.email = Email +usersettings.changepassword = Canviar contrasenya +usersettings.password = Contrasenya +usersettings.newpassword = Nova contrasenya +usersettings.confirmpassword = Confirmar contrasenya +usersettings.delete = Esborrar aquest usuari +usersettings.ldap = Autenticaci\u00F3 d'usuari a LDAP +usersettings.nousername = No s'ha trobat el nom d'usuari. +usersettings.noemail= Adre\u00E7a d'email inv\u00E0lida. +usersettings.useralreadyexists = L'usuari ja existeix. +usersettings.nopassword = Es necess\u00E0ria una contrasenya. +usersettings.wrongpassword = Las contrasenyes no s\u00F3n coincidents. +usersettings.ldapdisabled = Autenticaci\u00F3 LDAP deshabilitada. Vegi configuraci\u00F3 avan\u00E7ada. +usersettings.passwordnotsupportedforldap = No es permet la creaci\u00F3 o canvi de contrasenyes per a usuaris autenticats via LDAP. +usersettings.ok = L'usuari {0} ha canviat la contrasenya correctament. + +# main.jsp +main.up = Pujar +main.playall = Reproduir-ho tot +main.playrandom = Reproducci\u00F3 aleat\u00F2ria +main.addall = Afegir-ho tot +main.downloadall = Descarregar-ho tot +main.tags = Editar tags +main.playcount = Reprodu\u00EFt {0} cops. +main.lastplayed = \u00DAltima reproducci\u00F3 {0}. +main.comment = Comentari +main.wiki = \ + \ + \ + \ + \ +
__text__Bold text \\\\ Line break
~~text~~Italic text (empty line) New paragraph
* text List item http://foo.com/ Link
1. text Enumerated list item{link:Foo|http://foo.com}Named link
+main.sharealbum = Compartir +main.more = M\u00E9s accions... +main.more.selection = Can\u00E7ons seleccionades... +main.more.share = Compartir +main.nowplaying = Actualment en reproducci\u00F3 +main.lyrics = Lletres de can\u00E7ons +main.minutesago = minuts passats +main.chat = Missatges de Xat +main.scanning = Escanejant arxius: +main.message = Escriure un missatge +main.clearchat = Netejar missatges + +# rating.jsp +rating.rating = Qualificaci\u00F3 +rating.clearrating = Netejar qualificaci\u00F3 + +# coverArt.jsp +coverart.change = Canviar +coverart.zoom = Ampliar + +# allmusic.jsp +allmusic.text = Buscant l''\u00E0lbum {0} a allmusic.com - Si us plau esperi. + +# changeCoverArt.jsp +changecoverart.title = Canviar car\u00E0tula +changecoverart.address = Introdueixi la direcci\u00F3 de la imatge de la car\u00E0tula +changecoverart.artist = Artista +changecoverart.album = \u00C0lbum +changecoverart.search = Cercador d'Imatges de Google +changecoverart.wait = Esperi, si us plau... +changecoverart.success = La imatge ha estat descarregada correctament. +changecoverart.error = Error descarregant la imatge. +changecoverart.noimagesfound = No s'han trobat imatges. + +# changeCoverArtConfirm.jsp +changeCoverArtConfirm.failed = Ha fallat al canviar la car\u00E0tula:
"{0}" + +# editTags.jsp +edittags.title = Editar tags +edittags.file = Arxiu +edittags.track = Pista +edittags.songtitle = T\u00EDtol +edittags.artist = Artista +edittags.album = \u00C0lbum +edittags.year = Any +edittags.genre = Genere +edittags.status = Estat +edittags.suggest = Suggerir +edittags.reset = Reset +edittags.suggest.short = S +edittags.reset.short = R +edittags.set = Establir +edittags.working = Treballant +edittags.updated = Actualitzat +edittags.skipped = Om\u00E8s +edittags.error = Error + +# share.jsp +share.title = Share +share.warning =

IMPORTANT NOTICE!

Play fair – Don't share copyrighted material in any manner that violates the law.

+share.facebook = Share on Facebook +share.twitter = Share on Twitter +share.googleplus = Share on Google+ +share.link = Or share this with someone by sending them this link: {0} +share.disabled = To share your music with someone you must first register your own subsonic.org address.
\ + Please go to Settings > Network (administrative rights required). +share.manage = Manage my shared media + +# podcastReceiver.jsp +podcastreceiver.title = Receptor de Podcast +podcastreceiver.expandall = Mostrar cap\u00EDtols +podcastreceiver.collapseall = Amagar cap\u00EDtols +podcastreceiver.status.new = Nou +podcastreceiver.status.downloading = Descarregar +podcastreceiver.status.completed = Complet +podcastreceiver.status.error = Error +podcastreceiver.status.deleted = Esborrat +podcastreceiver.status.skipped = Om\u00E8s +podcastreceiver.downloadselected= Descarregar seleccionada +podcastreceiver.deleteselected= Borrat seleccionat +podcastreceiver.confirmdelete= Est\u00E0 segur de esborrar els Podcasts seleccionats? +podcastreceiver.check = Comprovar nous cap\u00EDtols +podcastreceiver.refresh = Actualitzar la p\u00E0gina +podcastreceiver.settings = Configuraci\u00F3 dels Podcast +podcastreceiver.subscribe = Subscriure's a un Podcast + +# lyrics.jsp +lyrics.title = Lletres de can\u00E7ons +lyrics.artist = Artista +lyrics.song = Can\u00E7\u00F3 +lyrics.search = Cerca +lyrics.wait = Buscant lletres de can\u00E7ons, esperi un moment... +lyrics.courtesy = (Lyrics by chartlyrics.com) +lyrics.nolyricsfound = No s'han trobat lletres de can\u00E7ons. + +# helpPopup.jsp +helppopup.title = Ajuda de {0} +helppopup.cover.title = Mida de la car\u00E0tula +helppopup.cover.text =

Permet especificar la mida de la car\u00E0tula visualitzada amb l'opci\u00F3 d'ocultar-la completament.

+helppopup.transcode.title = Bitrate m\u00E0xim +helppopup.transcode.text =

Si ho desitja, pot limitar el bitrate del stream de m\u00FAsica. \ + Per exemple, si el seu mp3 original est\u00E0 codificat a 256 Kbps(kbits per segon), configurar el bitrate m\u00E0xim \ + a 128 far\u00E0 que {0} es codifiqui la m\u00FAsica de 256 a 128 Kbps.

+helppopup.playlistfolder.title = Directori de la llista de reproducci\u00F3 +helppopup.playlistfolder.text =

Permet especificar el directori a on es troben les llistes de reproducci\u00F3.

+helppopup.musicmask.title = Arxius de m\u00FAsica +helppopup.musicmask.text =

Permet especificar els tipus d''arxius que s'haurien de recon\u00E8ixer com a m\u00FAsica quan naveguem pels directoris de m\u00FAsica.

+helppopup.videomask.title = Arxius de v\u00EDdeo +helppopup.videomask.text =

Permet especificar el tipus d''arxius que seran reconeguts com a v\u00EDdeo.

+helppopup.coverartmask.title = Arxius de car\u00E0tula +helppopup.coverartmask.text =

Permet especificar els tipus d''arxius que s'haurien de recon\u00E8ixer com a car\u00E0tules quan naveguem pels directoris de m\u00FAsica.

+helppopup.downsamplecommand.title = Comanda de Downsample +helppopup.downsamplecommand.text =

Permet especificar la comanda a executar quan s''hagi de fer un downsampling a bitrates m\u00E9s baixes.

\ +

(%s = L'arxiu a ser downsampled, %b = Bitrate Max del reproductor, %t = T\u00EDtol, %a = Artista, %l = \u00C0lbum)

+helppopup.index.title = \u00CDndex +helppopup.index.text =

Et permet especificar com hauria ser l''\u00EDndex que es troba a la part superior de la pantalla. D'aquesta manera \ + els arxius i directoris que es troben als directoris de m\u00FAsica configurats es poden accedir amb m\u00E9s facilitat.

\ +

La manera d''especificar els \u00EDndexs \u00E9s mitjan\u00E7ant una llista separada por espais en blanc. Normalment aquests \u00EDndex s\u00F3n d'un car\u00E0cter \ + per\u00F2 podem agrupar els que vulguem. Per exemple, a traves de l'\u00EDndex el podrem accedir als arxius i directoris \ + que comencin per "el".

\ +

Vost\u00E8 tamb\u00E9 pot crear un \u00EDndex que sigui un grup de car\u00E0cters d'\u00EDndexs. Per a fer-ho, els car\u00E0cters han d'estar entre par\u00E8ntesis. \ + Per exemple l'\u00EDndex A-E(ABCDE) es mostrar\u00E0 com a A-E i enlla\u00E7ar\u00E0 amb tots els arxius i directoris que comencin \ + per A, B, C, D o E. Aix\u00F2 pot ser \u00FAtil per tal d'agrupar els car\u00E0cters que es fan servir menys (com ara X, Y y Z), o \ + per tal d'agrupar els car\u00E0cters accentuats (com ara A, \u00C0 i \u00C1)

\ +

Els Arxius i directoris que no es trobin sota cap \u00EDndex s'assignaran autom\u00E0ticament al \u00EDndex "#".

+helppopup.ignoredarticles.title = Ignorar articles +helppopup.ignoredarticles.text =

Et permet especificar una llista d''articles(com "The","el","la") que seran ignorats en el moment de crear l'\u00EDndex.

+helppopup.shortcuts.title = Accessos directes +helppopup.shortcuts.text =

Una llista separada per espais dels directoris els quals es volen crear accessos directes. Faci servir cometes per a agrupar paraules com per exemple:

\ +

Nou Incoming "M\u00FAsica electr\u00F2nica"

+helppopup.language.title = Idioma +helppopup.language.text =

Et permet especificar l''idioma que es vol fer servir.

+helppopup.visibility.title = Visibilitat +helppopup.visibility.text =

Seleccioni els detalls que vol que es mostrin amb la can\u00E7\u00F3 i quants car\u00E0cters es poden visualitzar. Aquest \u00E9s el m\u00E0xim \ + de car\u00E0cters que es podran visualitzar al t\u00EDtol d'una can\u00E7\u00F3, d'un \u00E0lbum o al nom d'un artista.

+helppopup.partymode.title = Mode Festa +helppopup.partymode.text =

Quan el mode festa s'activa, la interf\u00EDcie d'usuari es simplifica per ser usat per usuaris sense experi\u00E8ncia. \ + M\u00E9s concretament, s'activa un sistema per evitar l'eliminaci\u00F3 per error de llistes de reproducci\u00F3.

+helppopup.theme.title = Tema +helppopup.theme.text =

Et permet seleccionar el tema que es vol fer servir. Un tema defineix l'aparen\u00E7a(colors, fonts, imatges...) de {0}.

+helppopup.welcomemessage.title = Missatge de benvinguda +helppopup.welcomemessage.text =

El missatge que es mostra a l''inici de la p\u00E0gina.

+helppopup.loginmessage.title = Missatges de Login +helppopup.loginmessage.text =

El missatge que es mostra a la p\u00E0gina de login.

+helppopup.coverartlimit.title = L\u00EDmit de car\u00E0tules +helppopup.coverartlimit.text =

N\u00FAmero m\u00E0xim de car\u00E0tules que es mostren a una p\u00E0gina.

+helppopup.downloadlimit.title = L\u00EDmit de descarrega +helppopup.downloadlimit.text =

Especifica l'ample de banda que es far\u00E0 servir per a descarregar els arxius.

+helppopup.uploadlimit.title = L\u00EDmit de pujada +helppopup.uploadlimit.text =

Especifica l'ample de banda que es far\u00E0 servir per a pujar els arxius.

+helppopup.streamport.title = N\u00FAmero de port SSL +helppopup.streamport.text =

Aquesta opci\u00F3 nom\u00E9s \u00E9s rellevant si s'usa {0} a un servidor amb SSL (HTTPS).

Alguns reproductors \ + (com ara Winamp) no suporten streaming sobre SSL per tant haurem d'especificar un port alternatiu http(normalment 80 \ + o 4040) per a aquests. Els streams no s'encriptaran.

+helppopup.ldap.title = Autenticaci\u00F3 LDAP +helppopup.ldap.text =

L''Usurari pot ser autenticat mitjan\u00E7ant un servidor LDAP extern (Incloent Windows Active Directory). \ + Quan l'autenticaci\u00F3 LDAP est\u00E0 activada, el nom d'usuari i la contrasenya s\u00F3n revisades pel servidor extern i no per part de {0}.

+helppopup.ldapurl.title = LDAP URL +helppopup.ldapurl.text =

URL del servidor LDAP. El protocol ha de ser tant ldap:// com ldaps:// \ + (per LDAP over SSL). Per a m\u00E9s informaci\u00F3 accedir al seg\u00FCent link.

+ +helppopup.ldapsearchfilter.title = Filtre de cerca LDAP +helppopup.ldapsearchfilter.text =

Expressi\u00F3 usada pel filtre de cerca. Aquest \u00E9s un filtre de cerca LDAP \ + (definit com al RFC 2254). \ + El comportament "'{0'}" \u00E9s substitu\u00EFt pel nom d''usuari, per exemple: \ +

    \ +
  • (uid='{0'}) - aquest cercar\u00E0 el nom d''usuari a l''atribut uid.
  • \ +
  • (sAMAccountName='{0'}) - t\u00EDpicament usat per la autenticaci\u00F3 a Microsoft Active Directory.
  • \ +

+helppopup.ldapmanagerdn.title = LDAP manager DN +helppopup.ldapmanagerdn.text =

Si el servidor LDAP no suporta connexi\u00F3 an\u00F2nima, s'haur\u00E0 d''especificar el DN \ + (Distinguished Nom) i la contrasenya per l'usuari LDAP que s'usar\u00E0.

+helppopup.ldapautoshadowing.title = Crear de manera autom\u00E0tica usuaris LDAP a {0} +helppopup.ldapautoshadowing.text =

Amb aquesta opci\u00F3 seleccionada, els usuaris LDAP no cal que hagin hagut de crear-se de manera manual a {0} abans d''autenticar-se.

\ +

NOTE! Aix\u00F2 implica que qualsevol usuari amb un nom d'usuari i una contrasenya LDAP v\u00E0lids pot autenticar-se de manera \ + correcta a {0}.

+helppopup.playername.title = Nom de l'oient +helppopup.playername.text =

Permet especificar un nom per l''oient(usuari@ip).

+helppopup.autocontrol.title = Controla el reproductor de manera autom\u00E0tica +helppopup.autocontrol.text =

Si selecciona aquesta opci\u00F3 {0} executar\u00E0 el reproductor en el moment que vost\u00E8 cliqui a "Reproduir"\ + a la llista de reproducci\u00F3. Si no, haur\u00E0 d'executar i connectar el reproductor.

+helppopup.dynamicip.title = IP din\u00E0mica +helppopup.dynamicip.text =

Deshabiliti aquesta opci\u00F3 si l''oient usa una IP est\u00E0tica.

+ +# wap/index.jsp +wap.index.missing = No s'ha trobat m\u00FAsica +wap.index.playlist = Llista de reproducci\u00F3 +wap.index.search = Buscar +wap.index.settings = Configuraci\u00F3 + +# wap/browse.jsp +wap.browse.playone = Reprodueixi la can\u00E7\u00F3 +wap.browse.playall = Reprodueixi-ho tot +wap.browse.addone = Afegeixi la can\u00E7\u00F3 +wap.browse.addall = Afegeixi-ho tot +wap.browse.downloadone = Descarregar can\u00E7\u00F3 +wap.browse.downloadall = Descarregar-ho tot + +# wap/playlist.jsp +wap.playlist.title = Llista de reproducci\u00F3 +wap.playlist.noplayer = Cap reproductor connectat +wap.playlist.clear = Netejar +wap.playlist.load = Carregar +wap.playlist.random = Random +wap.playlist.play = Reproduir al dispositiu m\u00F2bil + +# wap/search.jsp +wap.search.title = Buscar + +# wap/searchResult.jsp +wap.searchresult.index = L'\u00EDndex de cerca s'est\u00E0 creant en aquests moments. Si us plau, intenti-ho de nou m\u00E9s tard. + +# wap/settings.jsp +wap.settings.selectplayer = Seleccioni un reproductor +wap.settings.allplayers = Tot + diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties new file mode 100644 index 00000000..aea2f10d --- /dev/null +++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_cs.properties @@ -0,0 +1,816 @@ +# +# Czech localization. +# Author: Robert Ilyk (rilyk@volny.cz), Trottel (trottel09@gmail.com) +# Last update: 23-05-2014 +# + +common.home = Dom\u016F +common.back = Zp\u011Bt +common.help = N\u00E1pov\u011Bda +common.play = P\u0159ehr\u00E1t +common.add = P\u0159idat +common.download = St\u00E1hnout +common.close = Zav\u0159\u00EDt +common.refresh = Obnovit +common.next = Dal\u0161\u00ED +common.previous = P\u0159edchoz\u00ED +common.more = V\u00EDce +common.ok = OK +common.cancel = Zru\u0161it +common.save = Ulo\u017Eit +common.create = Vytvo\u0159it +common.delete = Odstranit +common.edit = Upravit +common.confirm = Potvr\u010Fte +common.unknown = (Nezn\u00E1m\u00FD) +common.default = (V\u00FDchoz\u00ED) +common.settingssaved = Nastaven\u00ED byla ulo\u017Eena. +common.trialexpired = Zku\u0161ebn\u00ED doba vypr\u0161ela {0}. Z\u00EDskejte Subsonic Premium pro pou\u017Eit\u00ED t\u00E9to funkce. +common.trialnotexpired = Tato funkce je dostupn\u00E1 do {0}. Pot\u00E9 mus\u00EDte z\u00EDskat Subsonic Premium. + +# login.jsp +login.username = U\u017Eivatelsk\u00E9 jm\u00E9no +login.password = Heslo +login.login = P\u0159ihl\u00E1sit +login.remember = Pamatovat si m\u011B +login.logout = Nyn\u00ED jste odhl\u00E1\u0161eni. +login.error = \u0160patn\u00E9 u\u017Eivatelsk\u00E9 jm\u00E9no nebo heslo. +login.insecure = \u00DA\u010Det {0} nen\u00ED zabazpe\u010Den\u00FD. P\u0159ihlaste s u\u017Eivatelsk\u00FDm jm\u00E9nem a heslem "admin",
nebo klikn\u011Bte sem. Pak okam\u017Eit\u011B zm\u011B\u0148te heslo. +login.recover = Zapomn\u011Bli jste sv\u00E9 heslo? + +# recover.jsp +recover.title = Zapomn\u011Bli jste sv\u00E9 heslo? +recover.text = Pro obnoven\u00ED hesla zadejte n\u00ED\u017Ee sv\u00E9 u\u017Eivatelsk\u00E9 jm\u00E9no nebo e-mailovou adresu. +recover.username = U\u017Eivatelsk\u00E9 jm\u00E9no nebo e-mailov\u00E1 adresa +recover.send = Obnovit heslo a poslat mi ho +recover.success = Va\u0161e heslo bylo obnoveno a posl\u00E1no na {0}. +recover.error.usernotfound = Bohu\u017Eel, u\u017Eivatel nebyl nalezen. +recover.error.noemail = Bohu\u017Eel, pro tohoto u\u017Eivatele nen\u00ED registrov\u00E1na \u017E\u00E1dn\u00E1 e-mailov\u00E1 adresa. +recover.error.sendfailed = Nepoda\u0159ilo se poslat e-mail, zkuste to znovu pozd\u011Bji. +recover.error.invalidcaptcha = Bohu\u017Eel, byla zad\u00E1na \u0161patn\u00E1 slova CAPTCHA, zkuste to znovu pozd\u011Bji. + +# accessDenied.jsp +accessDenied.title = P\u0159\u00EDstup byl odep\u0159en +accessDenied.text = Bohu\u017Eel, nem\u00E1te opr\u00E1vn\u011Bn\u00ED k proveden\u00ED po\u017Eadovan\u00E9 operace. +# notFound.jsp +notFound.title = Nenalezeno +notFound.text =

Bohu\u017Eel, nepoda\u0159ilo se nal\u00E9zt to, co hled\u00E1te.

Zkuste znovu na\u010D\u00EDst str\u00E1nku. Pokud to nepom\u016F\u017Ee, \ + zkuste znovu prohledat slo\u017Eky m\u00E9di\u00ED.

+notFound.reload = Znovu na\u010D\u00EDst str\u00E1nku +notFound.scan = Nastaven\u00ED slo\u017Eek m\u00E9di\u00ED + +# top.jsp +top.home = Dom\u016F +top.now_playing = P\u0159ehr\u00E1v\u00E1n\u00ED +top.starred = Obl\u00EDben\u00E9 +top.settings = Nastaven\u00ED +top.status = Stav +top.podcast = Podcasty +top.more = V\u00EDce +top.help = N\u00E1pov\u011Bda +top.search = Hledat +top.upgrade = Je dostupn\u00E1 nov\u00E1 verze. St\u00E1hnout {0} {1} \ + zde. +top.missing = Nebyly nalezeny \u017E\u00E1dn\u00E9 slo\u017Eky m\u00E9di\u00ED. Zm\u011B\u0148te nastaven\u00ED. +top.logout = Odhl\u00E1sit u\u017Eivatele {0} +top.getpremium = Z\u00EDskat Subsonic Premium +top.gotpremium = Subsonic Premium +top.trialdaysleft = Zb\u00FDv\u00E1 {0} dn\u016F zku\u0161ebn\u00ED doby + +# left.jsp +left.scanning = Prohled\u00E1v\u00E1n\u00ED slo\u017Eek m\u00E9di\u00ED... +left.statistics = {0} interpret\u016F
\ + {1} alb
\ + {2} skladeb
\ + {3}
\ + {4}  hodin +left.shortcut = Z\u00E1stupci +left.playlists = Seznamy stop +left.radio = Internetov\u00E9 TV a r\u00E1dia +left.allfolders = V\u0161echny slo\u017Eky +left.showallplaylists = Zobrazit v\u0161e +left.createplaylist = Vytvo\u0159it nov\u00FD seznam stop +left.importplaylist = Importovat seznam stop + +# playQueue.jsp +playlist.stop = Zastavit +playlist.start = P\u0159ehr\u00E1t +playlist.confirmclear = Opravdu vy\u010Distit frontu p\u0159ehr\u00E1v\u00E1n\u00ED? +playlist.clear = Vy\u010Distit +playlist.shuffle = N\u00E1hodn\u011B +playlist.repeat_on = Opakov\u00E1n\u00ED zapnuto +playlist.repeat_off = Opakov\u00E1n\u00ED vypnuto +playlist.undo = Zp\u011Bt +playlist.settings = Nastaven\u00ED +playlist.more = V\u00EDce akc\u00ED... +playlist.more.playlist = Fronta p\u0159ehr\u00E1v\u00E1n\u00ED +playlist.more.sortbytrack = Se\u0159adit podle stopy +playlist.more.sortbyartist = Se\u0159adit podle interpreta +playlist.more.sortbyalbum = Se\u0159adit podle alba +playlist.more.selection = Vybran\u00E9 skladby +playlist.more.selectall = Vybrat v\u0161e +playlist.more.selectnone = Nevybrat nic +playlist.getflash = Z\u00EDskat Flash player +playlist.save = Ulo\u017Eit jako seznam stop +playlist.append = P\u0159idat do seznamu stop +playlist.remove = Odebrat +playlist.up = Nahoru +playlist.down = Dol\u016F +playlist.empty = Fronta p\u0159ehr\u00E1v\u00E1n\u00ED je pr\u00E1zdn\u00E1 +playlist.toast.appendtoplaylist = Seznam stop aktualizov\u00E1n. +playlist.toast.saveasplaylist = Seznam stop ulo\u017Een. + +# playlist.jsp +playlist2.created = Vytvo\u0159eno u\u017Eivatelem {0} {1} +playlist2.songs = skladeb +playlist2.shared = Viditeln\u00FD pro ostatn\u00ED +playlist2.notshared = Nen\u00ED viditeln\u00FD pro ostatn\u00ED +playlist2.name = N\u00E1zev seznamu stop +playlist2.comment = Pozn\u00E1mka k seznamu stop +playlist2.public = Ziditeln\u011Bte tento seznam stop pro ostatn\u00ED u\u017Eivatele. +playlist2.confirmdelete = Opravdu chcete odstranit tento seznam stop? +playlist2.empty = Seznam stop je pr\u00E1zdn\u00FD +playlist2.export = Exportovat + +# importPlaylist.jsp +importPlaylist.title = Importovat seznam stop +importPlaylist.text = Vyberte seznam stop k importu (m3u, pls, xspf) +importPlaylist.success = Seznam stop "{0}" byl \u00FAsp\u011B\u0161n\u011B importov\u00E1n. +importPlaylist.error = Nepoda\u0159ilo se importovat seznam stop. {0} + +# videoPlayer.jsp +videoPlayer.getflash = Naistalujte si Flash Player +videoPlayer.popout = Otev\u0159\u00EDt v nov\u00E9m okn\u011B + +# status.jsp +status.title = Stav +status.type = Typ +status.stream = Streamov\u00E1n\u00ED +status.download = St\u00E1hov\u00E1n\u00ED +status.upload = Nahr\u00E1v\u00E1n\u00ED +status.player = P\u0159ehr\u00E1va\u010D +status.user = U\u017Eivatel +status.current = Aktu\u00E1ln\u00ED soubor +status.transmitted = P\u0159eneseno +status.bitrate = P\u0159enosov\u00E1 rychlost (Kb/s) + +# starred.jsp +starred.title = M\u00E9 obl\u00EDben\u00E9 polo\u017Eky +starred.empty = Klikn\u011Bte na ikonu hv\u011Bzdy pro ozna\u010Den\u00ED sv\u00FDch obl\u00EDben\u00FDch interpret\u016F, alb a skladeb. + +# search.jsp +search.title = Hledat +search.query = Interpret, album nebo n\u00E1zev skladby +search.search = Hledat +search.index = Rejst\u0159\u00EDk vyhled\u00E1v\u00E1n\u00ED se pr\u00E1v\u011B vytv\u00E1\u0159\u00ED. Zkuste to znovu pozd\u011Bji. +search.hits.none = Nebyly nalezeny \u017E\u00E1dn\u00E9 shody +search.hits.more = V\u00EDce +search.hits.artists = Interpreti +search.hits.albums = Alba +search.hits.songs = Skladby + +# gettingStarted.jsp +gettingStarted.title = Za\u010D\u00EDn\u00E1me +gettingStarted.text =

V\u00EDt\u00E1 v\u00E1s Subsonic! Nastaven\u00ED bude provedeno b\u011Bhem p\u00E1r okam\u017Eik\u016F, postupujte podle n\u00ED\u017Ee uveden\u00FDch jednoduch\u00FDch krok\u016F.
\ + Pro n\u00E1vrat na tuto obrazovku klikn\u011Bte na tla\u010D\u00EDtko "Dom\u016F" v n\u00E1strojov\u00E9 li\u0161t\u011B naho\u0159e.

\ +

Pro v\u00EDce informac\u00ED si p\u0159e\u010Dt\u011Bte p\u0159\u00EDru\u010Dku Za\u010D\u00EDn\u00E1me (v anglick\u00E9m jazyce).

+gettingStarted.root = Upozorn\u011Bn\u00ED! Proces Subsonic b\u011B\u017E\u00ED jako u\u017Eivatel root. Uva\u017Eujte o \ + zm\u011Bn\u011B. +gettingStarted.step1.title = Zm\u011B\u0148te heslo spr\u00E1vce. +gettingStarted.step1.text = Zabezpe\u010Dte si sv\u016Fj server zm\u011Bnou v\u00FDchoz\u00EDho hesla pro \u00FA\u010Det administr\u00E1tora. \ + M\u016F\u017Eete tak\u00E9 vytvo\u0159it nov\u00E9 \u00FA\u010Dty u\u017Eivatel\u016F s rozd\u00EDln\u00FDmi opr\u00E1vn\u011Bn\u00EDmi. +gettingStarted.step2.title = Nastavte slo\u017Eky m\u00E9di\u00ED. +gettingStarted.step2.text = Ur\u010Dete slo\u017Eky, kde m\u00E1te svou hudbu a videa. +gettingStarted.step3.title = Nastavte s\u00ED\u0165. +gettingStarted.step3.text = N\u011Bkter\u00E1 u\u017Eite\u010Dn\u00E1 nastaven\u00ED, pokud si chcete u\u017E\u00EDvat svou hudbu z internetu, \ + nebo ji sd\u00EDlet s rodinou a p\u0159\u00E1teli. Z\u00EDskejte svou soukromou adresu \ + vasejmeno.subsonic.org. +gettingStarted.hide = Toto znovu nezobrazovat +gettingStarted.hidealert = Pro op\u011Btovn\u00E9 zobrazen\u00ED t\u00E9to obrazovky p\u0159ejd\u011Bte do Nastaven\u00ED > Obecn\u00E9. + +# home.jsp +home.random.title = N\u00E1hodn\u011B +home.alphabetical.title = V\u0161e +home.newest.title = Naposledy p\u0159idan\u00E9 +home.starred.title = Obl\u00EDben\u00E9 +home.highest.title = Nejl\u00E9pe hodnocen\u00E9 +home.frequent.title = Nej\u010Dast\u011Bji p\u0159ehr\u00E1van\u00E9 +home.recent.title = Naposledy p\u0159ehr\u00E1van\u00E9 +home.decade.title = Podle desetilet\u00ED +home.genre.title = Podle \u017E\u00E1nru +home.users.title = U\u017Eivatel\u00E9 +home.random.text = N\u00E1hodn\u00E1 alba +home.alphabetical.text = V\u0161echna alba +home.newest.text = Naposledy p\u0159idan\u00E1 alba +home.starred.text = V\u00E1mi obl\u00EDben\u00E1 alba +home.highest.text = Nejl\u00E9pe hodnocen\u00E1 alba +home.frequent.text = Nej\u010Dast\u011Bji p\u0159ehr\u00E1van\u00E1 alba +home.recent.text = Naposledy prehr\u00E1van\u00E1 alba +home.decade.text = Desetilet\u00ED +home.genre.text = \u017D\u00E1nr +home.users.text = U\u017Eivatelsk\u00E1 statistika +home.scan = Slo\u017Eka m\u00E9di\u00ED se pr\u00E1v\u011B prohled\u00E1van\u00E1. N\u011Bkter\u00E9 funkce nejsou dostupn\u00E9. +home.albums = Alba {0}–{1} +home.playcount = P\u0159ehr\u00E1no {0} skladeb +home.lastplayed = P\u0159ehr\u00E1no {0} +home.created = Vytvo\u0159eno {0} +home.chart.total = Celkem (MB) +home.chart.stream = Streamov\u00E1no (MB) +home.chart.download = Sta\u017Eeno (MB) +home.chart.upload = Nahr\u00E1no (MB) + +# more.jsp +more.title = V\u00EDce +more.random.title = N\u00E1hodn\u00E1 fronta p\u0159ehr\u00E1v\u00E1n\u00ED +more.random.text = Vytvo\u0159it n\u00E1hodnou frontu p\u0159ehr\u00E1v\u00E1n\u00ED z +more.random.songs = {0} skladeb +more.random.auto = P\u0159ehr\u00E1t v\u00EDce n\u00E1hodn\u00FDch skladeb po dosa\u017Een\u00ED konce fronty p\u0159ehr\u00E1v\u00E1n\u00ED. +more.random.ok = OK +more.random.genre = ze \u017E\u00E1nru +more.random.anygenre = Jak\u00FDkoliv +more.random.year = a roku +more.random.anyyear = Jak\u00FDkoliv +more.random.folder = ve slo\u017Ece +more.random.anyfolder = Jak\u00E1koliv +more.apps.title = Aplikace Subsonicu +more.apps.text =

Vyzkou\u0161ejte neust\u00E1le rostouc\u00ED seznam aplikac\u00ED Subsonicu. \ + Poskytuj\u00ED z\u00E1bavu a alternativn\u00ED zp\u016Fsoby, jak si u\u017E\u00EDt svou sb\u00EDrku m\u00E9di\u00ED - bez ohledu na to, kde se nach\u00E1z\u00EDte. \ + Aplikace jsou dostupn\u00E9 pro Android, iPhone, Windows Phone, BlackBerry, Roku a spoustu dal\u0161\u00EDch.

+more.minisub.title = MiniSub +more.minisub.text =

MiniSub je HTML5 minip\u0159ehr\u00E1va\u010D pro Subsonic. \ + Je sou\u010D\u00E1st\u00ED webov\u00E9 aplikace Subsonicu, pro jeho spu\u0161t\u011Bn\u00ED klikn\u011Bte sem, \ + nebo nav\u0161tivte str\u00E1nku GitHub pro z\u00EDsk\u00E1n\u00ED nejnov\u011Bj\u0161\u00ED verze. \ + Je tak\u00E9 dostupn\u00FD jako aplikace Chrome.

+more.mobile.title = Mobiln\u00ED telefon +more.mobile.text =

{0} m\u016F\u017Eete ovl\u00E1dat prost\u0159ednictv\u00EDm libovoln\u00E9ho telefonu nebo PDA vybaven\u00E9ho technologi\u00ED WAP.
\ + Jednodu\u0161e nav\u0161tivte n\u00E1sleduj\u00EDc\u00ED adresu ze sv\u00E9ho telefonu: http://yourhostname/wap

\ +

To vy\u017Eaduje, aby v\u00E1\u0161 server byl dostupn\u00FD z interneru.

+more.podcast.title = Podcast +more.podcast.text =

Ulo\u017Een\u00E9 seznamy stop jsou dostupn\u00E9 jako Podscasty
\ + Pou\u017Eijte n\u00E1sleduj\u00EDc\u00ED adresu pro p\u0159id\u00E1n\u00ED Podcastu: http://yourhostname/podcast, \ + nebo klikn\u011Bte sem.

+more.upload.title = Nahr\u00E1t soubor +more.upload.source = Vybrat soubor +more.upload.target = Nahr\u00E1t do +more.upload.browse = Vybrat +more.upload.ok = Nahr\u00E1t +more.upload.unzip = Automaticky rozbalit soubory ZIP. +more.upload.progress = % dokon\u010Deno. \u010Cekejte... + +# upload.jsp +upload.title = Nahr\u00E1v\u00E1n\u00ED souboru +upload.success = Soubor {0} byl \u00FAsp\u011B\u0161n\u011B nahr\u00E1n +upload.empty = \u017D\u00E1dn\u00FD soubor k nahr\u00E1n\u00ED. +upload.failed = Nahr\u00E1v\u00E1n\u00ED selhalo s n\u00E1sleduj\u00EDc\u00ED chybou:
"{0}" +upload.unzipped = Rozbaleno {0} + +# help.jsp +help.title = O aplikaci {0} +help.upgrade = Pozor! Je dostupn\u00E1 nov\u00E1 verze. St\u00E1hnout {0} {1} \ + zde. +help.premium.title = Licence +help.premium.expires = (vypr\u0161\u00ED {0}) +help.premium.upgrade = Upgradujte na Subsonic Premium, abyste si u\u017Eili spoustu funkc\u00ED nav\u00EDc! +help.premium.expired = (Licence vypr\u0161ela {0}) +help.version.title = Verze +help.builddate.title = Datum sestaven\u00ED +help.server.title = Server +help.license.title = Podm\u00EDnky pou\u017Eit\u00ED +help.license.text = {0} je software distribuovan\u00FD pod GPL open-source licenc\u00ED. \ + {0} pou\u017E\u00EDv\u00E1 licencovan\u00E9 knihovny t\u0159et\u00EDch stran. Uv\u011Bdomte si, \u017Ee {0} nen\u00ED \ + n\u00E1stroj pro neleg\u00E1ln\u00ED distribuci materi\u00E1lu chr\u00E1n\u011Bn\u00E9ho autorsk\u00FDmi pr\u00E1vy. V\u017Edy v\u011Bnujte pozornost a dodr\u017Eujte p\u0159\u00EDslu\u0161n\u00E9 z\u00E1kony va\u0161\u00ED zem\u011B. +help.homepage.title = Domovsk\u00E1 str\u00E1nka +help.forum.title = F\u00F3rum +help.shop.title = Obchod +help.contact.title = Kontakt +help.contact.text = {0} je vyv\u00EDjen a udr\u017Eov\u00E1n Sindrem Mehusem \ + (sindre@activeobjects.no). \ + Jestli\u017Ee m\u00E1te jak\u00FDkoliv dotaz, p\u0159ipom\u00EDnku nebo n\u00E1m\u011Bt na zlep\u0161en\u00ED, nav\u0161tivte \ + f\u00F3rum Subsonicu. +help.log = Protokol +help.logfile = Kompletn\u00ED protokol je ulo\u017Een v {0}. + +# settingsHeader.jsp +settingsheader.title = Nastaven\u00ED +settingsheader.general = Obecn\u00E9 +settingsheader.advanced = Roz\u0161\u00ED\u0159en\u00E9 +settingsheader.personal = Osobn\u00ED +settingsheader.musicFolder = Slo\u017Eky m\u00E9di\u00ED +settingsheader.internetRadio = Internetov\u00E9 TV a r\u00E1dia +settingsheader.podcast = Podcasty +settingsheader.player = P\u0159ehr\u00E1va\u010De +settingsheader.dlna = DLNA +settingsheader.share = Sd\u00EDlen\u00E1 m\u00E9dia +settingsheader.network = S\u00ED\u0165 +settingsheader.transcoding = P\u0159ek\u00F3dov\u00E1n\u00ED +settingsheader.user = U\u017Eivatel\u00E9 +settingsheader.search = Vyhled\u00E1v\u00E1n\u00ED a mezipam\u011B\u0165 +settingsheader.coverArt = Obaly alb +settingsheader.password = Heslo + +# generalSettings.jsp +generalsettings.playlistfolder = Importovat seznam stop z +generalsettings.musicmask = Hudebn\u00ED soubory +generalsettings.videomask = Soubory vide\u00ED +generalsettings.coverartmask = Soubory obal\u016F alb +generalsettings.index = Rejst\u0159\u00EDk +generalsettings.ignoredarticles = Ignorovan\u00E9 \u010Dleny +generalsettings.shortcuts = Z\u00E1stupci +generalsettings.sortalbumsbyyear = Se\u0159adit alba podle roku +generalsettings.showgettingstarted = Po spu\u0161t\u011Bn\u00ED zobrazit obrazovku "Za\u010D\u00EDn\u00E1me" +generalsettings.welcometitle = Uv\u00EDtac\u00ED nadpis +generalsettings.welcomesubtitle = Uv\u00EDtac\u00ED podnadpis +generalsettings.welcomemessage = Uv\u00EDtac\u00ED zpr\u00E1va +generalsettings.loginmessage = P\u0159ihla\u0161ovac\u00ED zpr\u00E1va +generalsettings.language = V\u00FDchoz\u00ED jazyk +generalsettings.theme = V\u00FDchoz\u00ED motiv + +# advancedSettings.jsp +advancedsettings.downsamplecommand = P\u0159\u00EDkaz p\u0159evzorkov\u00E1n\u00ED +advancedsettings.hlscommand = P\u0159\u00EDkaz \u017Eiv\u00E9ho streamov\u00E1n\u00ED HTTP +advancedsettings.coverartlimit = Omezen\u00ED obal\u016F alb
(0 = Neomezeno)
+advancedsettings.downloadlimit = Omezen\u00ED stahov\u00E1n\u00ED (Kb/s)
(0 = Neomezeno)
+advancedsettings.uploadlimit = Omezen\u00ED nahr\u00E1v\u00E1n\u00ED (Kb/s)
(0 = Neomezeno)
+advancedsettings.streamport = Port streamov\u00E1n\u00ED bez SSL
(0 = Zak\u00E1z\u00E1no)
+advancedsettings.ldapenabled = Povolit ov\u011B\u0159ov\u00E1n\u00ED LDAP +advancedsettings.ldapurl = URL adresa LDAP +advancedsettings.ldapsearchfilter = Filtr vyhled\u00E1v\u00E1n\u00ED LDAP +advancedsettings.ldapmanagerdn = Spr\u00E1vce DN LDAP
(Voliteln\u00E9)
+advancedsettings.ldapmanagerpassword = Heslo +advancedsettings.ldapautoshadowing = Automaticky vytvo\u0159it u\u017Eivatele v {0} + +# personalSettings.jsp +personalsettings.title = Osobn\u00ED nastaven\u00ED u\u017Eivatele {0} +personalsettings.language = Jazyk +personalsettings.theme = Motiv +personalsettings.display = Zobrazen\u00ED +personalsettings.browse = Proch\u00E1zet +personalsettings.playlist = Seznam stop +personalsettings.tracknumber = \u010C\u00EDslo skladby +personalsettings.artist = Interpret +personalsettings.album = Album +personalsettings.genre = \u017D\u00E1nr +personalsettings.year = Rok +personalsettings.bitrate = P\u0159enosov\u00E1 rychlost +personalsettings.duration = D\u00E9lka +personalsettings.format = Form\u00E1t +personalsettings.filesize = Velikost souboru +personalsettings.captioncutoff = Zkr\u00E1cen\u00ED titulku +personalsettings.partymode = Zjednodu\u0161en\u00E9 rozhran\u00ED +personalsettings.shownowplaying = Uk\u00E1zat, co p\u0159ehr\u00E1vaj\u00ED ostatn\u00ED +personalsettings.nowplayingallowed = Uk\u00E1zat ostatn\u00EDm, co p\u0159ehr\u00E1v\u00E1m j\u00E1 +personalsettings.showchat = Zobrazit zpr\u00E1vy chatu +personalsettings.finalversionnotification = Upozornit m\u011B na nov\u00E9 verze +personalsettings.betaversionnotification = Upozornit m\u011B na nov\u00E9 betaverze +personalsettings.lastfmenabled = Informovat Last.fm o tom, co p\u0159ehr\u00E1v\u00E1m +personalsettings.lastfmusername = U\u017Eivatel Last.fm +personalsettings.lastfmpassword = Heslo Last.fm +personalsettings.avatar.title = Osobn\u00ED obr\u00E1zek +personalsettings.avatar.none = Bez obr\u00E1zku +personalsettings.avatar.custom = Vlastn\u00ED obr\u00E1zek +personalsettings.avatar.changecustom = Zm\u011Bnit vlastn\u00ED obr\u00E1zek +personalsettings.avatar.upload = Nahr\u00E1t +personalsettings.avatar.courtesy = Ikony jsou k dispozici se svolen\u00EDm Afterglow, \ + Aha-Soft, \ + Icons-Land a \ + Iconshock + +# avatarUploadResult.jsp +avataruploadresult.title = Zm\u011Bnit osobn\u00ED obr\u00E1zek +avataruploadresult.success = Osobn\u00ED obr\u00E1zek "{0}" byl \u00FAsp\u011B\u0161n\u011B nahr\u00E1n. +avataruploadresult.failure = Nepoda\u0159ilo se nahr\u00E1t osobn\u00ED obr\u00E1zek. Pro v\u00EDce informac\u00ED si prohl\u00E9dn\u011Bte protokol. + +# passwordSettings.jsp +passwordsettings.title = Zm\u011Bnit heslo u\u017Eivatele {0} + +# musicFolderSettings.jsp +musicfoldersettings.path = Slo\u017Eka +musicfoldersettings.name = N\u00E1zev +musicfoldersettings.enabled = Povolena +musicfoldersettings.add = P\u0159idat slo\u017Eku m\u00E9di\u00ED +musicfoldersettings.nopath = Zadejte slo\u017Eku. +musicfoldersettings.notfound = Slo\u017Ekas nebyla nalezena +musicfoldersettings.scan = Prohled\u00E1vat slo\u017Eky m\u00E9di\u00ED +musicfoldersettings.interval.never = Nikdy +musicfoldersettings.interval.one = Ka\u017Ed\u00FD den +musicfoldersettings.interval.many = Ka\u017Ed\u00FDch {0} dn\u00ED +musicfoldersettings.hour = v {0}:00 +musicfoldersettings.nowscanning = Slo\u017Eky m\u00E9di\u00ED se nyn\u00ED prohled\u00E1vaj\u00ED. Tato operace m\u016F\u017Ee trvat n\u011Bkolik minut, v z\u00E1vislosti \ + na velikosti va\u0161\u00ED knihovny m\u00E9di\u00ED. +musicfoldersettings.scannow = Prohledat nyn\u00ED slo\u017Eky m\u00E9di\u00ED +musicfoldersettings.fastcache = Re\u017Eim rychl\u00E9ho p\u0159\u00EDstupu +musicfoldersettings.fastcache.description = Pou\u017Eijte tuto volbu pro minimalizaci p\u0159\u00EDstupu k disku, nap\u0159\u00EDklad pokud jsou va\u0161e soubory m\u00E9di\u00ED na s\u00ED\u0165ov\u00E9m disku. \ + Pozn\u00E1mka: Zm\u011Bny soubor\u016F budou viditeln\u00E9 a\u017E po prohled\u00E1n\u00ED slo\u017Eek m\u00E9di\u00ED. +musicfoldersettings.expunge = Vy\u010Distit datab\u00E1zi +musicfoldersettings.expunge.description = Subsonic ukl\u00E1d\u00E1 informace o v\u0161ech minul\u00FDch souborech m\u00E9di\u00ED. Vy\u010Di\u0161t\u011Bn\u00EDm datab\u00E1ze budou trvale odebr\u00E1ny informace \ + o souborech, kter\u00E9 ji\u017E nejsou ve va\u0161\u00ED sb\u00EDrce m\u00E9di\u00ED. +musicfoldersettings.organizebyfolderstructure = Organizovat podle structury slo\u017Eek +musicfoldersettings.organizebyfolderstructure.description = Tuto volbu pou\u017Eijte pro proch\u00E1zen\u00ED knihovny m\u00E9di\u00ED pomoc\u00ED adres\u00E1\u0159ov\u00E9 struktury ne\u017E pomoc\u00ED informac\u00ED o interpretech nebo albech v ID3 taz\u00EDch. + +# networkSettings.jsp +networksettings.text = Pomoc\u00ED n\u00ED\u017Ee uveden\u00FDch nastaven\u00ED ur\u010Dete, jak p\u0159istupovat k va\u0161emu serveru Subsonic z internetu.
\ + Pokud naraz\u00EDte na pot\u00ED\u017Ee, p\u0159e\u010Dt\u011Bte si p\u0159\u00EDru\u010Dku Za\u010D\u00EDn\u00E1me. +networksettings.portforwardingenabled = Automaticky nakonfigurovat router tak, aby umo\u017Enil p\u0159\u00EDchoz\u00ED p\u0159ipojen\u00ED k Subsonicu (pomoc\u00ED UPnP nebo p\u0159esm\u011Brov\u00E1n\u00ED port\u016F NAT-PMP). +networksettings.portforwardinghelp = Pokud v\u00E1\u0161 router nem\u016F\u017Ee b\u00FDt nakonfigurov\u00E1n automaticky, m\u016F\u017Eete ho nastavit ru\u010Dn\u011B. \ + Postupujte podle pokyn\u016F na portforward.com. \ + Mus\u00EDte p\u0159esm\u011Brovat port {0} na po\u010D\u00EDta\u010D se spu\u0161t\u011Bn\u00FDm serverem Subsonic. +networksettings.urlredirectionenabled = P\u0159istupovat k serveru z internetu pomoc\u00ED snadno zapamatovateln\u00E9 adresy. +networksettings.status = Stav: + +# transcodingSettings.jsp +transcodingsettings.name = N\u00E1zev +transcodingsettings.sourceformat = P\u0159ev\u00E9st z +transcodingsettings.targetformat = P\u0159ev\u00E9st na +transcodingsettings.step1 = Krok 1 +transcodingsettings.step2 = Krok 2 +transcodingsettings.step3 = Krok 3 +transcodingsettings.add = P\u0159idat p\u0159ek\u00F3dov\u00E1n\u00ED +transcodingsettings.defaultactive = Povolit toto p\u0159ek\u00F3dov\u00E1n\u00ED pro v\u0161echny existuj\u00EDc\u00ED a nov\u00E9 p\u0159ehr\u00E1va\u010De. +transcodingsettings.recommended = Doporu\u010Den\u00E1 konfigurace +transcodingsettings.noname = Zadejte n\u00E1zev. +transcodingsettings.nosourceformat = Zadejte form\u00E1t, ze kter\u00E9ho p\u0159ev\u00E1d\u011Bt +transcodingsettings.notargetformat = Zadejte form\u00E1t, do kter\u00E9ho p\u0159ev\u00E1d\u011Bt +transcodingsettings.nostep1 = Zadejte alespo\u0148 jeden krok p\u0159ek\u00F3dov\u00E1n\u00ED +transcodingsettings.info =

(%s = Soubor, kter\u00FD m\u00E1 b\u00FDt p\u0159ek\u00F3dov\u00E1n, %b = Maxim\u00E1ln\u00ED p\u0159enosov\u00E1 rychlost p\u0159ehr\u00E1va\u010De, %t = N\u00E1zev, %a = Interpret, %l = Album)

\ +

P\u0159ek\u00F3dov\u00E1n\u00ED je proces p\u0159evodu z jednoho form\u00E1tu m\u00E9dia do jin\u00E9ho. Modul p\u0159ek\u00F3dov\u00E1n\u00ED {1}u \ + umo\u017E\u0148uje streamov\u00E1n\u00ED medi\u00ED, kter\u00E1 jinak nejsou streamovateln\u00E1. P\u0159ek\u00F3dov\u00E1n\u00ED prob\u00EDh\u00E1 v re\u00E1ln\u00E9m \u010Dase a nevy\u017Eaduje \ + jak\u00E9koliv vyu\u017Eit\u00ED disku.

\ +

Vlastn\u00ED p\u0159ek\u00F3dov\u00E1n\u00ED je realizov\u00E1no programy p\u0159\u00EDkazov\u00E9ho \u0159\u00E1dku t\u0159et\u00EDch stran, kter\u00E9 mus\u00ED b\u00FDt nainstalov\u00E1ny ve slo\u017Ece {0}. \ + M\u016F\u017Eete tak\u00E9 p\u0159idat vlastn\u00ED p\u0159evodn\u00EDky, kter\u00E9 spl\u0148uj\u00ED n\u00E1sleduj\u00EDc\u00ED po\u017Eadavky: \ +

    \ +
  • Mus\u00ED m\u00EDt rozhran\u00ED p\u0159\u00EDkazov\u00E9ho \u0159\u00E1dku.
  • \ +
  • Mus\u00ED b\u00FDt schopny odeslat v\u00FDstup na stdout.
  • \ +
  • P\u0159i pou\u017Eit\u00ED v kroku 2 mus\u00ED b\u00FDt schopny na\u010D\u00EDst vstup ze stdin.
  • \ +
\ +

\ +

P\u0159ek\u00F3dov\u00E1n\u00ED je aktivov\u00E1no na z\u00E1klad\u011B p\u0159ehr\u00E1va\u010De v Nastaven\u00ED > P\u0159ehr\u00E1va\u010De.

+ +# internetRadioSettings.jsp +internetradiosettings.streamurl = URL streamov\u00E1n\u00ED +internetradiosettings.homepageurl = Domovsk\u00E1 str\u00E1nka +internetradiosettings.name = N\u00E1zev +internetradiosettings.enabled = Povoleno +internetradiosettings.add = P\u0159idat Internetovou TV nebo r\u00E1dio +internetradiosettings.nourl = Zadejte URL. +internetradiosettings.noname = Zadejte n\u00E1zev. + +# podcastSettings.jsp +podcastsettings.update = Zjistit nov\u00E9 epizody +podcastsettings.keep = Zachovat +podcastsettings.keep.all = V\u0161echny epizody +podcastsettings.keep.one = Nejnov\u011Bj\u0161\u00ED epizody +podcastsettings.keep.many = Posledn\u00EDch {0} epizod +podcastsettings.download = Jestli\u017Ee jsou dostupn\u00E9 nov\u00E9 epizody +podcastsettings.download.all = St\u00E1hnout v\u0161echny +podcastsettings.download.one = St\u00E1hnout nejnov\u011Bj\u0161\u00ED +podcastsettings.download.many = St\u00E1hnout posledn\u00EDch {0} epizod +podcastsettings.download.none = Ned\u011Blat nic +podcastsettings.interval.manually = Ru\u010Dn\u011B +podcastsettings.interval.hourly = Ka\u017Edou hodinu +podcastsettings.interval.daily = Ka\u017Ed\u00FD den +podcastsettings.interval.weekly = Ka\u017Ed\u00FD t\u00FDden +podcastsettings.folder = Ulo\u017Eit podcast do + +# playerSettings.jsp +playersettings.noplayers = Nebyly nalezeny \u017E\u00E1dn\u00E9 p\u0159ehr\u00E1va\u010De. +playersettings.type = Typ +playersettings.lastseen = Naposledy zhl\u00E9dnut\u00E9 +playersettings.title = V\u00FDb\u011Br p\u0159ehr\u00E1va\u010De +playersettings.technology.web.title = Webov\u00FD p\u0159ehr\u00E1va\u010D +playersettings.technology.external.title = Extern\u00ED p\u0159ehr\u00E1va\u010D +playersettings.technology.external_with_playlist.title = Extern\u00ED p\u0159ehr\u00E1va\u010D se seznamem stop +playersettings.technology.jukebox.title = Jukebox +playersettings.technology.web.text = P\u0159ehr\u00E1v\u00E1 hudbu p\u0159\u00EDmo v prohl\u00ED\u017Ee\u010Di pomoc\u00ED integrovan\u00E9ho p\u0159ehr\u00E1va\u010De Flash player. +playersettings.technology.external.text = P\u0159ehr\u00E1v\u00E1 hudbu ve va\u0161em obl\u00EDben\u00E9m p\u0159ehr\u00E1va\u010Di jako je WinAmp nebo Windows Media Player. +playersettings.technology.external_with_playlist.text = Stejn\u011B jako p\u0159edchoz\u00ED, ale seznam stop je spravov\u00E1n p\u0159ehr\u00E1va\u010Dem \ + a ne serverem Subsonic. V tomto re\u017Eimu je mo\u017En\u00E9 p\u0159eskakovat uvnit\u0159 skladeb. +playersettings.technology.jukebox.text = P\u0159ehr\u00E1v\u00E1 hudbu p\u0159\u00EDmo na zvukov\u00E9m za\u0159\u00EDzen\u00ED serveru Subsonic (Pouze schv\u00E1len\u00ED u\u017Eivatel\u00E9). +playersettings.name = N\u00E1zev p\u0159ehr\u00E1va\u010De +playersettings.coverartsize = Velikost obalu alba +playersettings.maxbitrate = Maxim\u00E1ln\u00ED p\u0159enosov\u00E1 rychlost +playersettings.coverart.off = Vypnuto +playersettings.coverart.small = Mal\u00FD +playersettings.coverart.medium = St\u0159edn\u00ED +playersettings.coverart.large = Velk\u00FD +playersettings.notranscoder = Pozn\u00E1mka: Zd\u00E1 se, \u017Ee nejsou nainstalov\u00E1ny p\u0159evodn\u00EDky.
Pro v\u00EDce informac\u00ED klikn\u011Bte na tla\u010D\u00EDtko N\u00E1pov\u011Bda. +playersettings.autocontrol = Automatick\u00E9 ovl\u00E1d\u00E1n\u00ED p\u0159ehr\u00E1va\u010De +playersettings.dynamicip = P\u0159ehr\u00E1va\u010D m\u00E1 dynamickou adresu IP +playersettings.transcodings = Aktivn\u00ED p\u0159ek\u00F3dov\u00E1n\u00ED +playersettings.ok = Ulo\u017Eit +playersettings.forget = Odstranit p\u0159ehr\u00E1va\u010D +playersettings.clone = Klonovat p\u0159ehr\u00E1va\u010D + +dlnasettings.enabled = Povolit server DLNA +dlnasettings.description = Pou\u017Eijte tuto volbu k zapnut\u00ED medi\u00E1ln\u00EDho serveru DLNA nebo UPnP v Subsonicu \ + a streamujte sv\u00E1 m\u00E9dia do kompatibiln\u00EDch p\u0159ehr\u00E1va\u010D\u016F DLNA. + +# shareSettings.jsp +sharesettings.name = N\u00E1zev +sharesettings.owner = Sd\u00EDleno u\u017Eivatelem +sharesettings.description = Popis +sharesettings.visits = Nav\u0161t\u011Bv +sharesettings.lastvisited = Naposledy nav\u0161t\u00EDveno +sharesettings.expires = Vypr\u0161\u00ED +sharesettings.files = Sd\u00EDlen\u00E9 soubory +sharesettings.expirein = Vypr\u0161\u00ED za +sharesettings.expirein.week = t\u00FDden +sharesettings.expirein.month = m\u011Bs\u00EDc +sharesettings.expirein.year = rok +sharesettings.expirein.never = nikdy + +# userSettings.jsp +usersettings.title = Vybrat u\u017Eivatele +usersettings.newuser = Nov\u00FD u\u017Eivatel +usersettings.admin = U\u017Eivatel je administr\u00E1tor +usersettings.settings = U\u017Eivatel m\u00E1 povoleno m\u011Bnit nastaven\u00ED a heslo +usersettings.stream = U\u017Eivatel m\u00E1 povoleno p\u0159ehr\u00E1vat soubory +usersettings.jukebox = U\u017Eivatel m\u00E1 povoleno p\u0159ehr\u00E1vat soubory v re\u017Eimu Jukebox +usersettings.download = U\u017Eivatel m\u00E1 povoleno stahovat soubory +usersettings.upload = U\u017Eivatel m\u00E1 povoleno nahr\u00E1vat soubory +usersettings.share = U\u017Eivatel m\u00E1 povoleno sd\u00EDlet soubory s k\u00FDmkoliv +usersettings.coverart = U\u017Eivatel m\u00E1 povoleno m\u011Bnit obaly alb a \u0161t\u00EDtky +usersettings.comment= U\u017Eivatel m\u00E1 povoleno vytv\u00E1\u0159et a upravovat koment\u00E1\u0159e a hodnocen\u00ED +usersettings.podcast= U\u017Eivatel m\u00E1 povoleno spravovat Podcasty +usersettings.username = U\u017Eivatelsk\u00E9 jm\u00E9no +usersettings.email = E-mail +usersettings.changepassword = Zm\u011Bnit heslo +usersettings.password = Heslo +usersettings.newpassword = Nov\u00E9 heslo +usersettings.confirmpassword = Potvrdit heslo +usersettings.delete = Odstranit tohoto u\u017Eivatele +usersettings.ldap = Ov\u011B\u0159it u\u017Eivatele pomoc\u00ED LDAP +usersettings.nousername = Chyb\u00ED u\u017Eivatelsk\u00E9 jm\u00E9no. +usersettings.noemail= Neplatn\u00E1 e-mailov\u00E1 adresa. +usersettings.useralreadyexists = U\u017Eivatel ji\u017E existuje. +usersettings.nopassword = Je vy\u017Eadov\u00E1no heslo. +usersettings.wrongpassword = Hesla se neshoduj\u00ED. +usersettings.ldapdisabled = Ove\u0159ov\u00E1n\u00ED pomoc\u00ED LDAP nen\u00ED povoleno. Zkontrolujte roz\u0161\u00ED\u0159en\u00E9 nastaven\u00ED. +usersettings.passwordnotsupportedforldap = Nen\u00ED mo\u017En\u00E9 nastavit nebo zm\u011Bnit heslo pro u\u017Eivatele s ov\u011B\u0159ov\u00E1n\u00ED pomoc\u00ED LDAP. +usersettings.ok = Heslo u\u017Eivatele {0} bylo \u00FAsp\u011B\u0161n\u011B zm\u011Bn\u011Bno. + +# main.jsp +main.up = Nahoru +main.playall = P\u0159ehr\u00E1t v\u0161e +main.playrandom = P\u0159ehr\u00E1t n\u00E1hodn\u011B +main.addall = P\u0159idat v\u0161e +main.downloadall = St\u00E1hnout v\u0161e +main.tags = Upravit \u0161t\u00EDtky +main.playcount = P\u0159ehr\u00E1no {0} kr\u00E1t. +main.lastplayed = Naposledy p\u0159ehr\u00E1no {0}. +main.comment = Koment\u00E1\u0159 +main.wiki = \ + \ + \ + \ + \ +
__text__Tu\u010Dn\u00E9 \\\\ Nov\u00FD \u0159\u00E1dek
~~text~~Kurz\u00EDva (pr\u00E1zdn\u00FD \u0159\u00E1dek) Nov\u00FD odstavec
* text V\u00FDpis polo\u017Eek http://foo.com/ Odkaz
1. text \u010C\u00EDslovan\u00FD v\u00FDpis polo\u017Eek{link:Foo|http://foo.com}Pojmenovan\u00FD odkaz
+main.sharealbum = Sd\u00EDlet +main.more = V\u00EDce akc\u00ED... +main.more.selection = Vybran\u00E9 skladby... +main.more.share = Sd\u00EDlet +main.premium = Z\u00EDskat Subsonic Premium
(a odebrat tuto reklamu) +main.nowplaying = P\u0159ehr\u00E1v\u00E1n\u00ED +main.lyrics = Texty +main.minutesago = minut zp\u011Bt +main.chat = Zpr\u00E1vy chatu +main.scanning = Prohled\u00E1v\u00E1n\u00ED soubor\u016F: +main.message = Napsat zpr\u00E1vu +main.clearchat = Vymazat zpr\u00E1vy +main.addtoplaylist.title = P\u0159idat do seznamu stop +main.addtoplaylist.text = P\u0159idat vybran\u00E9 skladby do tohoto seznamu stop: +main.addnext = P\u0159ehr\u00E1t dal\u0161\u00ED +main.addlast = P\u0159ehr\u00E1t posledn\u00ED + +# rating.jsp +rating.rating = Hodnocen\u00ED +rating.clearrating = Vymazat hodnocen\u00ED + +# coverArt.jsp +coverart.change = Zm\u011Bnit +coverart.zoom = Zv\u011Bt\u0161it + +# allmusic.jsp +allmusic.text = Vyhled\u00E1v\u00E1n\u00ED alba {0} na allmusic.com - \u010Cekejte. + +# changeCoverArt.jsp +changecoverart.title = Zm\u011Bnit obal alba +changecoverart.address = Nebo zadejte adresu obr\u00E1zku +changecoverart.artist = Interpret +changecoverart.album = Album +changecoverart.search = Hled\u00E1n\u00ED obr\u00E1zk\u016F na Google +changecoverart.wait = \u010Cekejte... +changecoverart.success = Obr\u00E1zek byl \u00FAsp\u011B\u0161n\u011B sta\u017Een. +changecoverart.error = Nelze st\u00E1hnout obr\u00E1zek. +changecoverart.noimagesfound = Nebyly nalezeny \u017E\u00E1dn\u00E9 obr\u00E1zky. + +# changeCoverArtConfirm.jsp +changeCoverArtConfirm.failed = Nelze zm\u011Bnit obal alba:
"{0}" + +# editTags.jsp +edittags.title = Upravit \u0161t\u00EDtky +edittags.file = Soubor +edittags.track = Stopa +edittags.songtitle = N\u00E1zev +edittags.artist = Interpret +edittags.album = Album +edittags.year = Rok +edittags.genre = \u017D\u00E1nr +edittags.status = Stav +edittags.suggest = Navrhnout +edittags.reset = Resetovat +edittags.suggest.short = N +edittags.reset.short = R +edittags.set = Nastavit +edittags.working = Spu\u0161t\u011Bno +edittags.updated = Aktualizov\u00E1no +edittags.skipped = Vynech\u00E1no +edittags.error = Chyba + +# share.jsp +share.title = Sd\u00EDlet +share.warning =

D\u016ELE\u017DIT\u00C9 UPOZORN\u011AN\u00CD!

Bu\u010Fte f\u00E9rov\u00ED a nesd\u00EDlejte materi\u00E1l chr\u00E1n\u011Bn\u00FD autorsk\u00FDmi pr\u00E1vy jak\u00FDmkoliv zp\u016Fsobem, kter\u00FD poru\u0161uje z\u00E1kon.

+share.facebook = Sd\u00EDlet na Facebooku +share.twitter = Sd\u00EDlet na Twiteru +share.googleplus = Sd\u00EDlet na Google+ +share.link = Nebo sd\u00EDlejte s k\u00FDmkoliv posl\u00E1n\u00EDm tohoto odkazu: {0} +share.disabled = Abyste s n\u011Bk\u00FDm mohli sd\u00EDlet svou hudbu, mus\u00EDte si nejd\u0159\u00EDv zaregistrovat vlastn\u00ED adresu na subsonic.org.
\ + P\u0159ejd\u011Bte na Nastaven\u00ED > S\u00ED\u0165 (jsou vy\u017Eadov\u00E1na opr\u00E1vn\u011Bn\u00ED administr\u00E1tora). +share.manage = Spravovat m\u00E1 sd\u00EDlen\u00E1 m\u00E9dia + +# premium.jsp +premium.title = Subsonic Premium +premium.invalidlicense = Neplatn\u00FD licen\u010Dn\u00ED kl\u00ED\u010D. +premium.text =

Upgradujte na Subsonic Premium, abyste si u\u017Eili tyto dal\u0161\u00ED funkce:

\ +
    \ +
  • Aplikace pro Android, iPhone, Blackberry a Windows Phone*.
  • \ +
  • Aplikace pro Mac, Windows, Chrome, Roku a spoustu dal\u0161\u00EDch*.
  • \ +
  • Streamov\u00E1n\u00ED videa.
  • \ +
  • P\u0159ij\u00EDma\u010D Podcast\u016F.
  • \ +
  • Va\u0161e osobn\u00ED serverov\u00E1 adresa: vasejmeno.subsonic.org (viz Nastaven\u00ED > S\u00ED\u0165).
  • \ +
  • P\u0159ehr\u00E1v\u00E1n\u00ED sv\u00FDch m\u00E9di\u00ED na kompatibiln\u00EDch za\u0159\u00EDzen\u00EDch DLNA a UPnP.
  • \ +
  • Sd\u00EDlen\u00ED sv\u00FDch m\u00E9di\u00ED na Facebooku, Twitteru, Google+.
  • \ +
  • \u017D\u00E1dn\u00E9 reklamy ve webov\u00E9m rozhran\u00ED.
  • \ +
  • Ostatn\u00ED funkce, kter\u00E9 budou spu\u0161t\u011Bny pozd\u011Bji.
  • \ +
\ +

* N\u011Bkter\u00E9 aplikace mus\u00ED b\u00FDt zakoupeny samostatn\u011B.

+premium.getpremium = Z\u00EDskat Subsonic Premium +premium.licensed = M\u00E1te platnou licenci Subsonic Premium! +premium.licensedexpires = Va\u0161e licence Subsonic Premium je platn\u00E1 do {0} +premium.licensedexpired = Va\u0161e licence Subsonic Premium vypr\u0161ela {0} +premium.licensedto = Licence je registrov\u00E1na na u\u017Eivatele {0}. +premium.forcechange = Registrovat jin\u00FD licen\u010Dn\u00ED kl\u00ED\u010D +premium.register = P\u0159i upgradu na Subsonic Premium obdr\u017E\u00EDte licen\u010Dn\u00ED kl\u00ED\u010D e-mailem. Zaregistrujte se n\u00ED\u017Ee. +premium.resend = Ztracen\u00FD licen\u010Dn\u00ED kl\u00ED\u010D? Poslat ho znovu. +premium.register.email = E-mail +premium.register.license = Licen\u010Dn\u00ED kl\u00ED\u010D + +# podcastReceiver.jsp +podcastreceiver.title = P\u0159ij\u00EDma\u010D Podcast\u016F +podcastreceiver.expandall = Zobrazit epizody +podcastreceiver.collapseall = Skr\u00FDt epizody +podcastreceiver.status.new = Nov\u00E9 +podcastreceiver.status.downloading = Stahov\u00E1n\u00ED +podcastreceiver.status.completed = Dokon\u010Deno +podcastreceiver.status.error = Chyba +podcastreceiver.status.deleted = Odstran\u011Bno +podcastreceiver.status.skipped = Vynech\u00E1no +podcastreceiver.downloadselected= St\u00E1hnout vybran\u00E9 +podcastreceiver.deleteselected= Odstranit vybran\u00E9 +podcastreceiver.confirmdelete= Opravdu odstranit vybran\u00E9 Podcasty? +podcastreceiver.check = Zkontrolovat nov\u00E9 epizody +podcastreceiver.refresh = Obnovit str\u00E1nku +podcastreceiver.settings = Nastaven\u00ED Podsatu +podcastreceiver.subscribe = P\u0159ihl\u00E1sit se k odb\u011Bru Podcastu + +# lyrics.jsp +lyrics.title = Texty +lyrics.artist = Interpret +lyrics.song = Sklatba +lyrics.search = Hledat +lyrics.wait = Vyhled\u00E1v\u00E1n\u00ED text\u016F, \u010Dekejte... +lyrics.courtesy = (Text od chartlyrics.com) +lyrics.nolyricsfound = Nebyl nalezen \u017E\u00E1dn\u00FD text. +lyrics.trylater = Bohu\u017Eel, vyhled\u00E1vac\u00ED modul text\u016F povoluje pouze jedno vyhled\u00E1v\u00E1n\u00ED ka\u017Ed\u00FDch 20 sekund. Zkuste to znovu pozd\u011Bji. + +# helpPopup.jsp +helppopup.title = {0} N\u00E1pov\u011Bda +helppopup.cover.title = Velikost obalu alba +helppopup.cover.text =

Umo\u017E\u0148uje v\u00E1m zvolit velikost zobrazovan\u00E9ho obalu alba s volbou ho \u00FApln\u011B vypnout.

+helppopup.transcode.title = Maxim\u00E1ln\u00ED p\u0159enosov\u00E1 rychlost +helppopup.transcode.text =

Pokud m\u00E1te omezenou \u0161\u00ED\u0159ku p\u00E1sma, m\u016F\u017Eete nastavit horn\u00ED omezen\u00ED pro p\u0159enosovou rychlost streamovan\u00E9 hudby. \ + Nap\u0159\u00EDklad, pokud origin\u00E1ln\u00ED soubory mp3 maj\u00ED 256 Kb/s (kilobajt\u016F za sekundu), nastaven\u00ED maxim\u00E1ln\u00ED p\u0159enosov\u00E9 rychlosti \ + na 128 zp\u016Fsob\u00ED, \u017Ee {0} automaticky p\u0159evzorkuje hudbu z 256 na 128 Kb/s.

+helppopup.playlistfolder.title = Importovat seznam stop z +helppopup.playlistfolder.text =

Subsonic bude pravideln\u011B importovat seznamy stop ulo\u017Een\u00E9 v t\u00E9to slo\u017Ece.

+helppopup.musicmask.title = Hudebn\u00ED soubory +helppopup.musicmask.text =

Umo\u017E\u0148uje v\u00E1m zvolit typy soubor\u016F, kter\u00E9 by m\u011Bly b\u00FDt pova\u017Eov\u00E1ny za hudbu.

+helppopup.videomask.title = Soubory vide\u00ED +helppopup.videomask.text =

Umo\u017E\u0148uje v\u00E1m zvolit typy soubor\u016F, kter\u00E9 by m\u011Bly b\u00FDt pova\u017Eov\u00E1ny za video.

+helppopup.coverartmask.title = Sobory obal\u016F alb +helppopup.coverartmask.text =

Umo\u017E\u0148uje v\u00E1m zvolit typy soubor\u016F, kter\u00E9 by m\u011Bly b\u00FDt pova\u017Eov\u00E1ny za obaly alb p\u0159i proch\u00E1zen\u00ED slo\u017Eek m\u00E9di\u00ED.

+helppopup.downsamplecommand.title = P\u0159\u00EDkaz p\u0159evzorkov\u00E1n\u00ED +helppopup.downsamplecommand.text =

Umo\u017E\u0148uje v\u00E1m zvolit, kter\u00FD p\u0159\u00EDkaz bude spu\u0161t\u011Bn p\u0159i p\u0159evzorkov\u00E1n\u00ED na ni\u017E\u0161\u00ED p\u0159enosov\u00E9 rychlosti.

\ +

(%s = Soubor, kter\u00FD m\u00E1 b\u00FDt p\u0159evzorkov\u00E1n, %b = Maxim\u00E1ln\u00ED p\u0159enosov\u00E1 rychlost p\u0159ehr\u00E1va\u010De, %t = N\u00E1zev, %a = Interpret, %l = Album)

+helppopup.hlscommand.title = P\u0159\u00EDkaz \u017Eiv\u00E9ho streamov\u00E1n\u00ED HTTP +helppopup.hlscommand.text =

P\u0159\u00EDkaz pou\u017Eit\u00FD k vytvo\u0159en\u00ED \u00FAsek\u016F videa pro protokol HLS (HTTP Live Streaming) od Apple.

+helppopup.index.title = Rejst\u0159\u00EDk +helppopup.index.text =

Umo\u017E\u0148uje v\u00E1m zvolit, jak by m\u011Bl vypadat rejst\u0159\u00EDk (um\u00EDst\u011Bn\u00FD na lev\u00E9 stran\u011B obrazovky). Pomoc\u00ED tohoto rejst\u0159\u00EDku \ + lze snadno p\u0159istupovat k soubor\u016Fm a slo\u017Ek\u00E1m p\u0159\u00EDmo v ko\u0159enov\u00E9 slo\u017Ece m\u00E9di\u00ED.

\ +

Rejst\u0159\u00EDk je seznam polo\u017Eek odd\u011Blen\u00FDch mezerami. Obvykle je ka\u017Ed\u00E1 polo\u017Eka jednodu\u0161e jeden znak, \ + m\u016F\u017Eete ale tak\u00E9 zvolit v\u00EDce znak\u016F. Nap\u0159\u00EDklad polo\u017Eka The bude odkazovat na v\u0161echny soubory a \ + slo\u017Eky za\u010D\u00EDnaj\u00EDc\u00ED \u010Dlenem "The".

\ +

M\u016F\u017Eete tak\u00E9 vytvo\u0159it polo\u017Eku pomoc\u00ED skupiny znak\u016F rejst\u0159\u00EDku v z\u00E1vork\u00E1ch. Nap\u0159\u00EDklad polo\u017Eka \ + A-E(ABCDE) bude zobrazena jako A-E a bude odkazovat na v\u0161echny soubory a slo\u017Eky za\u010D\u00EDnaj\u00EDc\u00ED \ + A, B, C, D nebo E. Toto m\u016F\u017Ee b\u00FDt u\u017Eite\u010Dn\u00E9 pro seskupen\u00ED m\u00E9n\u011B pou\u017E\u00EDvan\u00FDch znak\u016F (jako je X, Y a Z), nebo \ + pro seskupen\u00ED znak\u016F s diakritikou (jako je A, \u00C0 a \u00C1)

\ +

Soubory a slo\u017Eky, kter\u00E9 nespadaj\u00ED pod \u017E\u00E1dnou polo\u017Eku rejst\u0159\u00EDku, budou um\u00EDst\u011Bny pod polo\u017Eku "#".

+helppopup.ignoredarticles.title = Ignorovan\u00E9 \u010Dleny +helppopup.ignoredarticles.text =

Umo\u017E\u0148uje v\u00E1m zvolit seznam \u010Dlen\u016F (jako je "The"), kter\u00E9 budou ignorov\u00E1ny p\u0159i vytv\u00E1\u0159en\u00ED rejst\u0159\u00EDku.

+helppopup.shortcuts.title = Z\u00E1stupci +helppopup.shortcuts.text =

Mezerou odd\u011Blen\u00FD seznam slo\u017Eek nejvy\u0161\u0161\u00ED \u00FArovn\u011B, pro kter\u00E9 vytvo\u0159it z\u00E1stupce. Pro seskupen\u00ED slov pou\u017Eijte uvozovky, nap\u0159\u00EDklad:

\ +

Nov\u011B ulo\u017Een\u00E9 "Zvukov\u00E9 stopy"

+helppopup.language.title = Jazyk +helppopup.language.text =

Umo\u017E\u0148uje v\u00E1m vybrat pou\u017Eit\u00FD jazyk.

+helppopup.visibility.title = Viditelnost +helppopup.visibility.text =

Vyberte, kter\u00E9 podrobnosti by m\u011Bly b\u00FDt zobrazeny pro ka\u017Edou skladbu a tak\u00E9 zkr\u00E1cen\u00ED titulku. To je maxim\u00E1ln\u00ED \ + zobrazen\u00FD po\u010Det znak\u016F pro n\u00E1zev skladby, alba a interpreta.

+helppopup.partymode.title = Zjednodu\u0161en\u00E9 rozhran\u00ED +helppopup.partymode.text =

Pokud je povoleno zjednodu\u0161en\u00E9 rozhran\u00ED, u\u017Eivatelsk\u00E9 rozhran\u00ED je zjednodu\u0161eno a snadn\u011Bji se ovl\u00E1d\u00E1 nezku\u0161en\u00FDm u\u017Eivatel\u016Fm. \ + Hlavn\u011B je zabr\u00E1n\u011Bno n\u00E1hodn\u00E9mu zfu\u0161ov\u00E1n\u00ED seznam\u016F stop.

+helppopup.theme.title = Motiv +helppopup.theme.text =

Umo\u017E\u0148uje v\u00E1m vybrat pou\u017Eit\u00FD motiv. Motiv ur\u010Duje vzhled a chov\u00E1n\u00ED {0}u z hlediska barev, p\u00EDsem, obr\u00E1zk\u016F atd.

+helppopup.welcomemessage.title = Uv\u00EDtac\u00ED zpr\u00E1va +helppopup.welcomemessage.text =

Zpr\u00E1va, kter\u00E1 je zobrazena na domovsk\u00E9 str\u00E1nce.

+helppopup.loginmessage.title = P\u0159ihla\u0161ovac\u00ED zpr\u00E1va +helppopup.loginmessage.text =

Zpr\u00E1va, kter\u00E1 je zobrazena na p\u0159ihla\u0161ovac\u00ED str\u00E1nce.

+helppopup.coverartlimit.title = Omezen\u00ED obal\u016F alb +helppopup.coverartlimit.text =

Maxim\u00E1ln\u00ED po\u010Det obr\u00E1zk\u016F obal\u016F alb zobrazen\u00FDch na jedn\u00E9 str\u00E1nce.

+helppopup.downloadlimit.title = Omezen\u00ED stahov\u00E1n\u00ED +helppopup.downloadlimit.text =

Horn\u00ED limit pou\u017Eit\u00E9 \u0161\u00ED\u0159ky p\u00E1sma pro stahov\u00E1n\u00ED soubor\u016F.

+helppopup.uploadlimit.title = Omezen\u00ED nahr\u00E1v\u00E1n\u00ED +helppopup.uploadlimit.text =

Horn\u00ED limit pou\u017Eit\u00E9 \u0161\u00ED\u0159ky p\u00E1sma pro nahr\u00E1v\u00E1n\u00ED soubor\u016F.

+helppopup.streamport.title = Port streamov\u00E1n\u00ED bez SSL +helppopup.streamport.text =

Tato volba je relevantn\u00ED, pouze pokud pou\u017E\u00EDv\u00E1te {0} na serveru s SSL (HTTPS).

N\u011Bkter\u00E9 p\u0159ehr\u00E1va\u010De \ + (jako je Winamp) neumo\u017E\u0148uj\u00ED streamov\u00E1n\u00ED prost\u0159ednictv\u00EDm SSL. Zvolte \u010D\u00EDslo portu pro standardn\u00ED HTTP (obvykle 80 \ + nebo 4040) pokud nechcete p\u0159en\u00E1\u0161et streamov\u00E1n\u00ED prost\u0159ednictv\u00EDm SSL. Pozor na to, \u017Ee streamov\u00E1n\u00ED nebude \u0161ifrov\u00E1no.

+helppopup.ldap.title = Ov\u011B\u0159ov\u00E1n\u00ED LDAP +helppopup.ldap.text =

U\u017Eivatel\u00E9 mohou b\u00FDt ov\u011B\u0159ov\u00E1ni extern\u00EDm serverem LDAP (v\u010Detn\u011B Windows Active Directory). \ + Kdy\u017E se u\u017Eivatel\u00E9 s povolen\u00FDm LDAP p\u0159ihla\u0161uj\u00ED do {0}u, u\u017Eivatelsk\u00E9 jm\u00E9no a heslo jsou zkontrolov\u00E1ny extern\u00EDm serverem, ne samotn\u00FDm {0}em.

+helppopup.ldapurl.title = URL adresa LDAP +helppopup.ldapurl.text =

URL adresa serveru LDAP. Protokol mus\u00ED b\u00FDt bu\u010F ldap:// nebo ldaps:// \ + (pro LDAP prost\u0159ednictv\u00EDm SSL). Zde \ + se do\u010Dtete podrobn\u011Bj\u0161\u00ED popis.

+helppopup.ldapsearchfilter.title = Filtr vyhled\u00E1v\u00E1n\u00ED LDAP +helppopup.ldapsearchfilter.text =

V\u00FDraz pro filtr pou\u017Eit\u00FD p\u0159i vyhled\u00E1v\u00E1n\u00ED u\u017Eivatele. Toto je filtr vyhled\u00E1v\u00E1n\u00ED LDAP \ + (jak je definov\u00E1n v RFC 2254). \ + Symbol "'{0'}" je nahrazen u\u017Eivatelsk\u00FDm jm\u00E9nem, nap\u0159\u00EDklad: \ +

    \ +
  • (uid='{0'}) - vyhled\u00E1 u\u017Eivatelsk\u00E9 jm\u00E9no podle atributu uid.
  • \ +
  • (sAMAccountName='{0'}) - obvykle se pou\u017E\u00EDv\u00E1 pro ov\u011B\u0159ov\u00E1n\u00ED v Microsoft Active Directory.
  • \ +

+helppopup.ldapmanagerdn.title = Spr\u00E1vce DN LDAP +helppopup.ldapmanagerdn.text =

Pokud server LDAP nepodporuje anonymn\u00ED p\u0159ipojen\u00ED, mus\u00EDte specifikovat DN \ + (Distinguished Name) a heslo u\u017Eivatele LDAP pou\u017Eit\u00E9 p\u0159i p\u0159ipojov\u00E1n\u00ED.

+helppopup.ldapautoshadowing.title = Automaticky vytv\u00E1\u0159et u\u017Eivatele LDAP v {0} +helppopup.ldapautoshadowing.text =

Pokud je tato volba vybr\u00E1na, u\u017Eivatel\u00E9 LDAP nemus\u00ED b\u00FDt ru\u010Dn\u011B vytv\u00E1\u0159eni v {0}u p\u0159ed p\u0159ihl\u00E1\u0161en\u00EDm.

\ +

POZN\u00C1MKA! To znamen\u00E1, \u017Ee se jak\u00FDkoliv u\u017Eivatel s platn\u00FDm u\u017Eivatelsk\u00FDm jm\u00E9nem a heslem LDAP se m\u016F\u017Ee p\u0159ihl\u00E1sit do {0}u, \ + co\u017E nemus\u00EDte cht\u00EDt.

+helppopup.playername.title = N\u00E1zev p\u0159ehr\u00E1va\u010De +helppopup.playername.text =

Umo\u017E\u0148uje v\u00E1m zvolit snadno zapamatovateln\u00FD n\u00E1zev p\u0159ehr\u00E1va\u010De, jako je "Pr\u00E1ce" nebo "Ob\u00FDvac\u00ED pokoj".

+helppopup.autocontrol.title = Automatick\u00E9 ovl\u00E1d\u00E1n\u00ED p\u0159ehr\u00E1va\u010De +helppopup.autocontrol.text =

Pokud je tato volba vybr\u00E1na, {0} automaticky spust\u00ED p\u0159ehr\u00E1va\u010D, kdy\u017E kliknete na "P\u0159ehr\u00E1t" \ + v seznamu stop. Jinak mus\u00EDte spustit p\u0159ehr\u00E1va\u010D sami.

+helppopup.dynamicip.title = Dynamick\u00E1 adresa IP +helppopup.dynamicip.text =

Vypn\u011Bte tuto volbu, pokud p\u0159ehr\u00E1va\u010D pou\u017E\u00EDv\u00E1 statickou adresu IP.

+ +# wap/index.jsp +wap.index.missing = Nebyla nalezena \u017E\u00E1dn\u00E1 hudba +wap.index.playlist = Seznam stop +wap.index.search = Hledat +wap.index.settings = Nastaven\u00ED + +# wap/browse.jsp +wap.browse.playone = P\u0159ehr\u00E1t skladbu +wap.browse.playall = P\u0159ehr\u00E1t v\u0161e +wap.browse.addone = P\u0159idat skladbu +wap.browse.addall = P\u0159idat v\u0161e +wap.browse.downloadone = St\u00E1hnout skladbu +wap.browse.downloadall = St\u00E1hnout v\u0161e + +# wap/playlist.jsp +wap.playlist.title = Seznam stop +wap.playlist.noplayer = Nen\u00ED p\u0159ipojen \u017E\u00E1dn\u00FD p\u0159ehr\u00E1va\u010D +wap.playlist.clear = Vy\u010Distit +wap.playlist.load = Na\u010D\u00EDst +wap.playlist.random = N\u00E1hodn\u011B +wap.playlist.play = P\u0159ehr\u00E1t na telefonu + +# wap/search.jsp +wap.search.title = Hledat + +# wap/searchResult.jsp +wap.searchresult.index = Rejst\u0159\u00EDk vyhled\u00E1v\u00E1n\u00ED se pr\u00E1v\u011B vytv\u00E1\u0159\u00ED. Zkuste to znovu pozd\u011Bji. + +# wap/settings.jsp +wap.settings.selectplayer = Vybrat p\u0159ehr\u00E1va\u010D +wap.settings.allplayers = V\u0161e diff --git a/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties new file mode 100644 index 00000000..c254f877 --- /dev/null +++ b/subsonic-main/src/main/resources/net/sourceforge/subsonic/i18n/ResourceBundle_da.properties @@ -0,0 +1,665 @@ + # Danish localization. + # Forfatter: Morten Hartvich + # + +common.home = Hjem +common.back = Tilbage +common.help = Hj\u00E6lp +common.play = Spil +common.add = Tilf\u00F8j +common.download = Download +common.close = Luk +common.refresh = opdat\u00E9r +common.next = N\u00E6ste +common.previous = Forrige +common.more = Mere +common.ok = OK +common.cancel = Annuller +common.save = Gem +common.create = Opret +common.delete = Slet +common.unknown = (Ukendt) +common.default = (Standard) + +# Login.jsp +login.username = Brugernavn +login.password = Password +login.login = Log in +login.remember = Husk mig +login.logout = Du er nu logget ud, Tak for bes\u00F8get. +login.error = Forkert brugernavn eller password +login.insecure = {0} er ikke sikret. Log ind med brugernavn og
password "admin", eller klik p\u00E5 her. Derefter \u00E6ndre adgangskode \u00F8jeblikkeligt. + +# AccessDenied.jsp +accessDenied.title = Ingen adgang +accessDenied.text = Du har desv\u00E6rre ikke tilladelse til at udf\u00F8re den \u00F8nskede handling. + +# Top.jsp +top.home = Forside +top.now_playing = Afspilning +top.settings = Indstillinger +top.status = Status +top.podcast = Podcast +top.more = Mere +top.help = Hj\u00E6lp +top.search = S\u00F8g +top.upgrade = En ny version er tilg\u00E6ngelig. Download {0} {1} \ + her. +top.missing = Ingen medie mapper fundet. Skal du \u00E6ndre indstillingerne. +top.logout = Log ud {0} + +# left.jsp +left.statistics = {0} artister
\ + {1} albums
\ + {2} sange
\ + {3} (~ {4} timer) +left.shortcut = Genveje +left.radio = Internet TV / radio +left.allfolders = Alle mapper + +# Playlist.jsp +playlist.stop = Stop +playlist.start = Spil +playlist.confirmclear = Slet spilleliste? +playlist.clear = Ryd +playlist.shuffle = Blande +playlist.repeat_on = Gentag er p\u00E5 +playlist.repeat_off = Gentag er slukket +playlist.undo = Fortryd +playlist.settings = Indstillinger +playlist.more = Flere handlinger ... +playlist.more.playlist = Spilleliste +playlist.more.sortbytrack = Sorter efter spor +playlist.more.sortbyartist = Sorter efter kunstner +playlist.more.sortbyalbum = Sortering album +playlist.more.selection = valgte sange +playlist.more.selectall = V\u00E6lg alle +playlist.more.selectnone = V\u00E6lg ingen +playlist.getflash = F\u00E5 Flash Player +playlist.save = Gem +playlist.append = Tilf\u00F8j til afspilningsliste +playlist.remove = Fjern +playlist.up = Up +playlist.down = Ned +playlist.empty = Spillelisten er tom + +# Status.jsp +status.title = Status +status.type = Type +status.stream = Stream +status.download = Download +status.upload = Upload +status.player = Afspiller +status.user = Bruger +status.current = Aktuelle fil +status.transmitted = Overf\u00F8rt +status.bitrate = Bitrate (Kbps) + +# Search.jsp +search.title = Title +search.search = S\u00F8g +search.index = S\u00F8geanmodningsparameteren indeks er i \u00F8jeblikket ved at blive oprettet. Pr\u00F8v igen senere. +search.hits.none = Ingen resultater fundet. + +# Home.jsp +home.random.title = Blandet +home.newest.title = Nyeste +home.highest.title = H\u00F8jeste karakter +home.frequent.title = Hyppigst spillet +home.recent.title = For nylig spillet +home.users.title = Brugere +home.random.text = Blandet album +home.newest.text = Senest tilf\u00F8jede eller opdateret albums +home.highest.text = H\u00F8jeste popul\u00E6re albums +home.frequent.text = Hyppigst spillet albums +home.recent.text = For nylig spillede albums +home.users.text = Brugerstatisik +home.scan = Mappen medie er i \u00F8jeblikket ved at blive scannet. Alle funktioner er endnu ikke tilg\u00E6ngelige. +home.albums = Albums {0} - {1} +home.playcount = Spillet {0} sange +home.lastplayed = Sidst spillet {0} +home.created = \u00C6ndret {0} +home.chart.total = Total (MB) +home.chart.stream = Streamede (MB) +home.chart.download = Downloaded (MB) +home.chart.upload = Uploaded (MB) + +# More.jsp +more.title = Mere +more.random.title = Tilf\u00E6ldige afspilningsliste +more.random.text = Opret tilf\u00E6ldig spilleliste med +more.random.songs = {0} sange +more.random.auto = Spil flere tilf\u00E6ldige sange n\u00E5r slutningen af afspilningslisten er n\u00E5et. +more.random.ok = OK +more.random.genre = fra genre +more.random.anygenre = Enhver +more.random.year = og \u00E5r +more.random.anyyear = Enhver +more.random.folder = i mappen +more.random.anyfolder = Enhver +more.apps.title = Subsonic Apps +more.apps.text =

Subsonic Apps er til \ + r\u00E5dighed for iPhone, Android og AIR.

+more.mobile.title = Mobiltelefon +more.mobile.text =

Du kan styre {0} fra en WAP-mobiltelefon eller PDA.
\ + Blot bes\u00F8ge f\u00F8lgende webadresse fra telefonen: http://yourhostname/wap

\ +

Dette kr\u00E6ver, at din server kan n\u00E5s fra internettet.

+more.podcast.title = Podcast +more.podcast.text =

Gemte spillelister er tilg\u00E6ngelige som Podcasts.
\ + Brug f\u00F8lgende URL i din Podcast receiver: http://yourhostname/podcast , \ + eller Klik her.

+more.upload.title = Upload fil +more.upload.source = V\u00E6lg fil +more.upload.target = Upload til +more.upload.browse = V\u00E6lg +more.upload.ok = Upload +more.upload.unzip = Automatisk udpakning af zip-fil. +more.upload.progress =% fuldf\u00F8rt. Vent venligst ... + +# Upload.jsp +upload.title = Overf\u00F8rer fil +upload.success = uploadet {0} +upload.empty = Ingen filer til overf\u00F8rsel. +upload.failed = Uploading mislykkedes med f\u00F8lgende fejl:
"{0}" +upload.unzipped = Udpakket {0} + +# Help.jsp +help.title = Om {0} +help.upgrade = Bem\u00E6rk! En ny version er tilg\u00E6ngelig. Download {0} {1} \ + her. +help.version.title = Version +help.builddate.title = Oprettelsesdato +help.server.title = Server +help.license.title = Licens +help.license.text = {0} er gratis software, der distribueres i henhold til GPL open-source licens. \ + {0} bruger licenseret tredjeparts biblioteker . +help.homepage.title = Hjemmeside +help.forum.title = Forum +help.shop.title = Merchandise +help.contact.title = Kontakt +help.contact.text = {0} er udviklet og vedligeholdes af Sindre Mehus \ + ( sindre@activeobjects.no ). \ + Hvis du har sp\u00F8rgsm\u00E5l, kommentarer eller forslag til forbedringer, kan du bes\u00F8ge \ + Subsonic Forum. +help.log = Log +help.logfile = Den komplette log er gemt i {0}. + +# SettingsHeader.jsp +settingsheader.title = Indstillinger +settingsheader.general = General +settingsheader.advanced = Avanceret +settingsheader.personal = Personlig +settingsheader.musicFolder = Medie mapper +settingsheader.internetRadio = Internet TV / radio +settingsheader.podcast = Podcast +settingsheader.player = Afspillere +settingsheader.share = Delt medie +settingsheader.network = Netv\u00E6rk +settingsheader.transcoding = Kodning +settingsheader.user = Brugere +settingsheader.search = S\u00F8g +settingsheader.coverArt = Cover +settingsheader.password = Password + +# GeneralSettings.jsp +generalsettings.playlistfolder = Spilleliste mappe +generalsettings.musicmask = Musik maske +generalsettings.videomask = Video maske +generalsettings.ignoredarticles = Ord som skal ignoreres +generalsettings.loginmessage = Logon meddelelse +generalsettings.coverartmask = Cover maske +generalsettings.index = Indeks +generalsettings.shortcuts = Genveje +generalsettings.showgettingstarted = Vis "Kom godt i gang" ved Login +generalsettings.welcometitle = Velkommen titel +generalsettings.welcomesubtitle = Velkommen undertitel +generalsettings.welcomemessage = Velkomstmeddelelse +generalsettings.language = Standard sprog +generalsettings.theme = Standard tema + +# AdvancedSettings.jsp +advancedsettings.downsamplecommand = Downsample kommando +advancedsettings.coverartlimit = Cover gr\u00E6nse
(0 = ubegr\u00E6nset) +advancedsettings.downloadlimit = Download gr\u00E6nse (Kbps)
(0 = ubegr\u00E6nset) +advancedsettings.uploadlimit = Upload gr\u00E6nse (Kbps)
(0 = ubegr\u00E6nset) +advancedsettings.streamport = Ikke-SSL stream port
(0 = Deaktiveret) +advancedsettings.ldapenabled = Aktiv\u00E9r LDAP-godkendelse +advancedsettings.ldapurl = LDAP URL +advancedsettings.ldapsearchfilter = LDAP s\u00F8gefilter +advancedsettings.ldapmanagerdn = LDAP manager DN
(valgfri) +advancedsettings.ldapmanagerpassword = Password +advancedsettings.ldapautoshadowing = Automatisk oprettet brugere i {0} + +# PersonalSettings.jsp +personalsettings.title = Personlige indstillinger for {0} +personalsettings.language = Sprog +personalsettings.theme = Tema +personalsettings.display = Display +personalsettings.browse = Gennemse +personalsettings.playlist = Spilleliste +personalsettings.tracknumber = Track # +personalsettings.artist = Kunstner +personalsettings.album = Album +personalsettings.genre = Genre +personalsettings.year = \u00C5r +personalsettings.bitrate = Bithastighed +personalsettings.duration = Varighed +personalsettings.format = Format +personalsettings.filesize = Filst\u00F8rrelse +personalsettings.captioncutoff = Caption cutoff +personalsettings.partymode = Fest indstilling +personalsettings.shownowplaying = Vis hvad andre spiller +personalsettings.nowplayingallowed = Lad andre se, hvad jeg spiller +personalsettings.finalversionnotification = Advis\u00E9r mig om nye versioner +personalsettings.betaversionnotification = Advis\u00E9r mig om nye beta-versioner +personalsettings.showchat = Vis chat meddelelse +personalsettings.lastfmenabled = Registrer hvad jeg spiller p\u00E5 Last.fm +personalsettings.lastfmusername = Last.fm brugernavn +personalsettings.lastfmpassword = Last.fm adgangskode +personalsettings.avatar.title = Personligt image +personalsettings.avatar.none = Intet image +personalsettings.avatar.custom = Tilpassede image +personalsettings.avatar.changecustom = Skift tilpassede image +personalsettings.avatar.upload = Upload +personalsettings.avatar.courtesy = Icons courtesy of Afterglow, \ + Aha-Soft, \ + Icons-Land, and \ + Iconshock + +# AvatarUploadResult.jsp +avataruploadresult.title = Skift personlig image +avataruploadresult.success = Uploadet personlig image "{0}". +avataruploadresult.failure = Kunne ikke uploade personlig image. Se log for yderligere oplysninger. + +# PasswordSettings.jsp +passwordsettings.title = Skift adgangskode til {0} + +# MusicFolderSettings.jsp +musicfoldersettings.path = Mappe +musicfoldersettings.name = Navn +musicfoldersettings.enabled = Aktiveret +musicfoldersettings.add = Tilf\u00F8j medie mappe +musicfoldersettings.nopath = Angiv en mappe. + +# TranscodingSettings.jsp +transcodingsettings.name = Navn +transcodingsettings.sourceformat = Konverter fra +transcodingsettings.targetformat = Konverter til +transcodingsettings.step1 = Trin 1 +transcodingsettings.step2 = Trin 2 +transcodingsettings.step3 = Trin 3 +transcodingsettings.defaultactive = Standard +transcodingsettings.add = Tilf\u00F8j kodning +transcodingsettings.noname = Angiv et navn. +transcodingsettings.nosourceformat = Angiv formatet til at konvertere fra. +transcodingsettings.notargetformat = Angiv formatet til at konvertere til. +transcodingsettings.nostep1 = Angiv mindst \u00E9t kodning skridt. +transcodingsettings.info =

(% s = Den fil, der skal omkodet,% b = Max bitrate for afspilleren)

\ +

Kodning er processen som konvertere fra \u00E9t medie format til et andet. {1}''s omkodning \ + giver mulighed for streaming af medier, der normalt ville ikke v\u00E6re mulige at streame. Denne omkodning er foretaget on-the-fly og kr\u00E6ver nogen diskaktivitet.

\ +

Den faktiske omkodning er udf\u00F8rt af tredjepart kommandolinje-programmer, som skal installeres i {0}. \ + En omkodning pakke til Windows \ + er tilg\u00E6ngelig her. Du kan tilf\u00F8je dine egne brugerdefinerede omkodninger og kaldet transcoder, hvis det \ + opfylder f\u00F8lgende krav: \ +

    \ +
  • Den skal have en kommandolinje-gr\u00E6nseflade.
  • \ +
  • Det skal kunne sende output til stdout.
  • \ +
  • Hvis der anvendes i trin 2 eller 3, skal den v\u00E6re i stand til at l\u00E6se input fra stdin.
  • \ +
\ +

\ +

Bem\u00E6rk, at omkodningen er aktiveret p\u00E5 en per-afspiller i ops\u00E6tningsmenuen af afspillere. Hvis "Standard" er markeret, vil omkodningen \ + aktiveres automatisk for nye afspillere.

+ +# internetRadioSettings.jsp +internetradiosettings.streamurl = Stream URL +internetradiosettings.homepageurl = Hjemmeside +internetradiosettings.name = Navn +internetradiosettings.enabled = Aktiveret +internetradiosettings.add = Tilf\u00F8j Internet TV / radio +internetradiosettings.nourl = Angiv en webadresse. +internetradiosettings.noname = Angiv et navn. + +# PodcastSettings.jsp +podcastsettings.update = Kontroller for nye episoder +podcastsettings.keep = Hold +podcastsettings.keep.all = Alle episoder +podcastsettings.keep.one = Nyeste episode +podcastsettings.keep.many = Seneste {0} episoder +podcastsettings.download = N\u00E5r nye episoder er tilg\u00E6ngelige +podcastsettings.download.all = Download alle +podcastsettings.download.one = Download den seneste en +podcastsettings.download.many = Download sidste {0} episoder +podcastsettings.download.none = Ingen +podcastsettings.interval.manually = Manuelt +podcastsettings.interval.hourly = Hver time +podcastsettings.interval.daily = Hver dag +podcastsettings.interval.weekly = Hver uge +podcastsettings.folder = Gem Podcasts i + +# PlayerSettings.jsp +playersettings.noplayers = Ingen spillere fundet. +playersettings.type = Type +playersettings.lastseen = Sidst set +playersettings.title = V\u00E6lg afspiller +playersettings.technology.web.title = Web afspiller +playersettings.technology.external.title = Eksterne afspiller +playersettings.technology.external_with_playlist.title = Eksterne afspiller med afspilningsliste +playersettings.technology.jukebox.title = Jukebox +playersettings.technology.web.text = Spil medie direkte i webbrowseren ved hj\u00E6lp af integreret Flash Player. +playersettings.technology.external.text = Spil medie i din foretrukne afspiller, s\u00E5som Winamp eller Windows Media Player. +playersettings.technology.external_with_playlist.text = Samme som ovenfor, men afspilningslisten forvaltes af afspilleren, snarere \ + end Subsonic serveren. I denne tilstand, springe inden sange er muligt. +playersettings.technology.jukebox.text = Spil medie direkte p\u00E5 lydenheden til Subsonic serveren. (Autoriserede brugere). +playersettings.name = Afspiller navn +playersettings.coverartsize = Cover st\u00F8rrelse +playersettings.maxbitrate = Max bitrate +playersettings.coverart.off = Off +playersettings.coverart.small = Small +playersettings.coverart.medium = Medium +playersettings.coverart.large = Large +playersettings.notranscoder = Meddelelse: ffmpeg synes ikke at v\u00E6re installeret.
Klik p\u00E5 knappen Hj\u00E6lp for yderligere oplysninger. +playersettings.autocontrol = Control afspilleren automatisk +playersettings.dynamicip = Afspiller har dynamisk IP-adresse +playersettings.transcodings = Aktive kodninger +playersettings.ok = Gem +playersettings.forget = Slet afspiller +playersettings.clone = Klon afspiller + +# NetworkSettings.jsp +networksettings.text = Brug indstillingerne nedenfor til at kontrollere, hvordan adgangen til din Subsonic server skal v\u00E6re over internettet. +networksettings.portforwardingenabled = Automatisk konfigurere routeren til at tillade indg\u00E5ende forbindelser til Subsonic (UPnP port forwarding). +networksettings.portforwardinghelp = Hvis din router ikke kan ops\u00E6ttes automatisk, skal du s\u00E6tte den op manuelt. \ + F\u00F8lg vejledningen p\u00E5 portforward.com . \ + Du skal forwarde port {0} til computeren, som k\u00F8rer Subsonic server. +networksettings.urlredirectionenabled = F\u00E5 adgang til din server over internettet ved hj\u00E6lp af en adresse, der er let at huske. +networksettings.status = Status: + +# shareSettings.jsp +sharesettings.name = Navn +sharesettings.owner = Delt af +sharesettings.description = Beskrivelse +sharesettings.visits = Antal bes\u00F8g +sharesettings.lastvisited = Seneste bes\u00F8g +sharesettings.expires = Udl\u00F8ber +sharesettings.files = Delte filer +sharesettings.expirein = Udl\u00F8ber om +sharesettings.expirein.week = 1u +sharesettings.expirein.month = 1m +sharesettings.expirein.year = 1\u00E5 +sharesettings.expirein.never = aldrig + +# UserSettings.jsp +usersettings.title = V\u00E6lg brugertype +usersettings.newuser = Ny bruger +usersettings.admin = Bruger er administrator +usersettings.settings = Brugeren har lov til at \u00E6ndre indstillinger og adgangskode +usersettings.stream = Bruger har lov til at afspille filer +usersettings.jukebox = Brugeren har lov til at afspille filer i jukebox mode +usersettings.download = Bruger har lov til at hente filer +usersettings.upload = Bruger har lov til at uploade filer +usersettings.share = Bruger har lov til at dele filer med alle +usersettings.coverart = Bruger har lov at \u00E6ndre Cover og tags +usersettings.comment = Bruger har lov til at oprette og redigere kommentarer og ratings +usersettings.podcast = Bruger har lov til at administrere Podcasts +usersettings.username = Brugernavn +usersettings.changepassword = Skift adgangskode +usersettings.password = Password +usersettings.newpassword = Ny adgangskode +usersettings.confirmpassword = Bekr\u00E6ft adgangskode +usersettings.delete = Slet denne bruger +usersettings.ldap = Godkend bruger i LDAP +usersettings.nousername = Intet brugernavn. +usersettings.useralreadyexists = Bruger eksisterer allerede. +usersettings.nopassword = Password er p\u00E5kr\u00E6vet. +usersettings.wrongpassword = Passwords ukendt. +usersettings.ldapdisabled = LDAP-godkendelse er ikke aktiveret. Se Avancerede indstillinger. +usersettings.passwordnotsupportedforldap = Kan ikke indstille eller \u00E6ndre adgangskode til LDAP-godkendte brugere. +usersettings.ok = Password blev \u00E6ndret for bruger {0}. + +# musicFolderSettings.jsp +musicfoldersettings.interval.never = aldrig +musicfoldersettings.interval.one = Hver dag +musicfoldersettings.interval.many = Hver {0} dage +musicfoldersettings.hour = p\u00E5 {0}: 00 + +# main.jsp +main.up = Niveau op +main.playall = Spil alt +main.playrandom = Spil blandet +main.addall = Tilf\u00F8j alt +main.downloadall = Download alle +main.tags = \u00C6ndre tags +main.playcount = Spillet {0} gange. +main.lastplayed = Sidst spillet {0}. +main.comment = Kommentar +main.wiki = \ + \ + \ + \ + \ +
__tekst__Fed tekst\\\\ Ny linje
~~tekst~~Kursiv tekst(tom linje) Nyt afsnit
* tekst Opstilling med punkttegn http://foo.com/ Link
1. tekst Opstilling med tal{link:Foo|http://foo.com}Navngivet link
+main.nowplaying = Spiller nu +main.sharealbum = Del +main.more.share = Del +main.lyrics = Tekster +main.minutesago = Minutter siden +main.message = Skriv en meddelelse +main.chat = Chat meddelelse +main.clearchat = Slet chat meddelelse + +# gettingsStarted.jsp +gettingStarted.title = Kom godt i gang +gettingStarted.text =

Velkommen til Subsonic! Som ops\u00E6ttes p\u00E5 ingen tid, skal du blot f\u00F8lge de grundl\u00E6ggende trin nedenfor.
\ + Klik p\u00E5 "Forside" knappen i v\u00E6rkt\u00F8jslinjen ovenfor for at komme tilbage til dette sk\u00E6rmbillede.

+gettingStarted.step1.title = Skift administrator adgangskode. +gettingStarted.step1.text = Sikre din server ved at \u00E6ndre standard password for administrator konto. \ + Du kan ogs\u00E5 oprette nye brugerkonti med forskellige rettigheder. +gettingStarted.step2.title = Indstil medie mapper. +gettingStarted.step2.text = Fort\u00E6l Subsonic hvor du opbevarer din medie. +gettingStarted.step3.title = Konfigurer netv\u00E6rksindstillinger. +gettingStarted.step3.text = Nogle nyttige indstillinger, hvis du vil nyde din medie over internettet, \ + eller dele det med familie og venner. F\u00E5 din personlige ditnavn.subsonic.org adresse. +gettingStarted.hide = Vis ikke denne igen +gettingStarted.hidealert = For at vise dette sk\u00E6rmbillede igen, skal du g\u00E5 til Indstillinger > Generel. + +# Rating.jsp +rating.rating = Karakter +rating.clearrating = Ryd karakter + +# CoverArt.jsp +coverart.change = Skift +coverart.zoom = Zoom + +# Allmusic.jsp +allmusic.text = S\u00F8gning efter album {0} p\u00E5 allmusic.com - Vent venligst. + +# ChangeCoverArt.jsp +changecoverart.title = Skift omslagsbilleder +changecoverart.address = Eller indtast image adresse +changecoverart.artist = Kunstner +changecoverart.album = Album +changecoverart.wait = Vent venligst ... +changecoverart.success = Image blev hentet. +changecoverart.error = Det lykkedes ikke at hente billedet. +changecoverart.noimagesfound = Ingen billeder fundet. + +# ChangeCoverArtConfirm.jsp +changeCoverArtConfirm.failed = Det lykkedes ikke at \u00E6ndre omslagsbilleder:
"{0}" + +# EditTags.jsp +edittags.title = Rediger tags +edittags.file = File +edittags.track = Spor +edittags.songtitle = Titel +edittags.artist = Kunstner +edittags.album = Album +edittags.year = \u00C5r +edittags.genre = Genre +edittags.status = Status +edittags.suggest = Forslag +edittags.reset = Nulstil +edittags.suggest.short = S +edittags.reset.short = R +edittags.set = Indstil +edittags.working = Arbejde +edittags.updated = Opdateret +edittags.skipped = Sprunget over +edittags.error = Fejl + +# share.jsp +share.title = Del +share.warning =

VIGTIG INFORMATION!

Undg\u00E5 at dele ophavsretligt beskyttet materiale p\u00E5 nogen m\u00E5de, der overtr\u00E6der loven.

+share.facebook = Del p\u00E5 Facebook +share.twitter = Del p\u00E5 Twitter +share.link = Eller dele dette med nogen, ved at sende dem dette link: {0} +share.disabled = Hvis du vil dele din medie med nogen, skal du f\u00F8rst registrere din egen subsonic.org address.
\ + G\u00E5 til Settings > Network (administrative rights required). +share.manage = Administrere mine delte medier + +# PodcastReceiver.jsp +podcastreceiver.title = Podcast receiver +podcastreceiver.expandall = Vis episoder +podcastreceiver.collapseall = Skjul episoder +podcastreceiver.status.new = Ny +podcastreceiver.status.downloading = Hentning +podcastreceiver.status.completed = Afsluttet +podcastreceiver.status.error = Fejl +podcastreceiver.status.deleted = Slettet +podcastreceiver.status.skipped = Sprunget over +podcastreceiver.downloadselected = Download udvalgte +podcastreceiver.deleteselected = Slet valgte +podcastreceiver.confirmdelete = Vil du slette valgte Podcasts? +podcastreceiver.check = Kontroller for nye episoder +podcastreceiver.refresh = Opdater side +podcastreceiver.settings = Podcast indstillinger +podcastreceiver.subscribe = Abonner p\u00E5 podcast + +# Lyrics.jsp +lyrics.title = Lyrics +lyrics.artist = Kunstner +lyrics.song = Sang +lyrics.search = S\u00F8g +lyrics.wait = S\u00F8ger efter sangtekster, vent venligst ... +lyrics.courtesy = (Lyrics ved chartlyrics.com ) +lyrics.nolyricsfound = Ingen sangtekster fundet. + +# HelpPopup.jsp +helppopup.title = {0} Hj\u00E6lp +helppopup.loginmessage.text =

Besked, der vises p\u00E5 loginsiden.

+helppopup.loginmessage.title = Login besked +helppopup.videomask.text =

Lader dig angive, hvilken type filer, der skal genkendes som video. +helppopup.videomask.title = Video maske +helppopup.cover.title = Cover st\u00F8rrelse +helppopup.cover.text =

Lader dig angive st\u00F8rrelsen af det viste Cover, med mulighed for at slukke for den helt.

+helppopup.transcode.title = Max bitrate +helppopup.transcode.text =

Hvis du har begr\u00E6nset b\u00E5ndbredde, kan du s\u00E6tte en \u00F8vre gr\u00E6nse for bithastighed af musikken str\u00F8mme. \ + For eksempel, hvis din oprindelige mp3 filer er kodet ved hj\u00E6lp af 256 Kbps (kilobit pr sekund), fasts\u00E6ttelse af max bitrate \ + til 128 vil g\u00F8re {0} automatisk resample musikken fra 256 til 128 Kbps.

+helppopup.playlistfolder.title = Playlist mappe +helppopup.playlistfolder.text =

Lader dig angive den mappe, hvor dine spillelister er beliggende.

+helppopup.musicmask.title = Music maske +helppopup.musicmask.text =

Lader dig angive den type filer, der skal anerkendes som medie, n\u00E5r du browser gennem medie mappe.

+helppopup.coverartmask.title = Cover maske +helppopup.coverartmask.text =

Lader dig angive den type filer, der skal anerkendes som Cover n\u00E5r du browser gennem medie mappe.

+helppopup.downsamplecommand.title = Downsample kommando +helppopup.downsamplecommand.text =

Lader dig angiver kommandoen til at udf\u00F8re, n\u00E5r downsampling til lavere bitrates.

\ +

(% s = Filen skal downsamplet,% b = Max bithastighed af afspilleren)

+helppopup.index.title = Indeks +helppopup.index.text =

Lader dig angive, hvordan indekset (placeret \u00F8verst p\u00E5 sk\u00E6rmen) skal se ud. Filer og mapper \ + direkte i roden medie mappe er nemt tilg\u00E6ngelige ved hj\u00E6lp af dette indeks.

\ +

Varespecifikationen er et rum-separeret liste over opslagsord. Normalt hver indrejse er blot et enkelt tegn, \ + men du kan ogs\u00E5 angive flere tegn. F.eks indrejse vil linke til alle filer og \ + mapper, der begynder med "I".

\ +

Du kan ogs\u00E5 oprette en post ved hj\u00E6lp af en gruppe af indeks tegn i parentes. F.eks indrejse \ + AE (ABCDE) vises som AE og link til alle filer og mapper, der begynder med enten \ + A, B, C, D eller E. Det kan v\u00E6re nyttigt til at samle de mindre hyppigt anvendte tegn (s\u00E5dan og X, Y og Z) eller \ + for gruppering accent tegn (s\u00E5som A, \u00C0 og \u00C1)

\ +

filer og mapper, der ikke er omfattet af et opslagsord vil blive placeret under opslagsord "#".

+helppopup.ignoredarticles.title = Ord som skal ignoreres +helppopup.ignoredarticles.text =

Lader dig angive en liste over artikler (s\u00E5som "De"), som vil blive ignoreret, n\u00E5r der skabes indekset.

+helppopup.shortcuts.title = Genveje +helppopup.shortcuts.text =

A space-separeret liste over \u00F8verste niveau mapper til at oprette genveje til. Brug anf\u00F8rselstegn til at gruppere ord, for eksempel:

\ +

New Indg\u00E5ende "lydspor"

+helppopup.language.title = Sprog +helppopup.language.text =

Lader dig v\u00E6lge sprog til brug.

+helppopup.visibility.title = Synlighed +helppopup.visibility.text =

V\u00E6lg, hvilke oplysninger der skal vises for hver sang, samt billedtekst cutoff. Dette er den maksimale \ + Antallet af tegn til at vise for sang titel, album og kunstner.

+helppopup.partymode.title = Fest indstilling +helppopup.partymode.text =

N\u00E5r fest indstilling er aktiveret, brugergr\u00E6nsefladen er forenklet og lettere at betjene for ikke-erfarne brugere. \ + Is\u00E6r utilsigtet Messing op afspilningslisten undg\u00E5s.

+helppopup.theme.title = Tema +helppopup.theme.text =

Lader dig v\u00E6lge tema til brug. Et tema definerer udseendet og fornemmelsen af {0} i form af farver, skrifter, billeder osv.

+helppopup.welcomemessage.title = Velkomstmeddelelse +helppopup.welcomemessage.text =

besked, der vises p\u00E5 hjemmesiden.

+helppopup.coverartlimit.title = Cover gr\u00E6nse +helppopup.coverartlimit.text =

Det maksimale antal Cover billeder skal vises p\u00E5 en enkelt side.

+helppopup.downloadlimit.title = Download gr\u00E6nse +helppopup.downloadlimit.text =

en \u00F8vre gr\u00E6nse for, hvor meget b\u00E5ndbredde vil blive brugt til at downloade filer.

+helppopup.uploadlimit.title = Upload gr\u00E6nse +helppopup.uploadlimit.text =

en \u00F8vre gr\u00E6nse for, hvor meget b\u00E5ndbredde vil blive brugt til at uploade filer.

+helppopup.streamport.title = Ikke-SSL stream havn +helppopup.streamport.text =

Denne valgmulighed er kun relevant, hvis du bruger {0} p\u00E5 en server med SSL (HTTPS).

Nogle spillere \ + (s\u00E5som Winamp) don''t st\u00F8tte streaming via SSL. Angiv portnummeret for regelm\u00E6ssig http (normalt 80 \ + eller 4040), hvis du don''t \u00F8nsker streams, der skal sendes via SSL. Bem\u00E6rk, at streams ikke vil v\u00E6re krypteret.

+helppopup.ldap.title = LDAP autentificering +helppopup.ldap.text =

Brugere kan blive bekr\u00E6ftet af en ekstern LDAP server (herunder Windows Active Directory). \ + N\u00E5r LDAP-aktiverede brugere logge p\u00E5 {0}, brugernavnet og adgangskoden er kontrolleret af den eksterne server, ikke af {0} selv.

+helppopup.ldapurl.title = LDAP URL +helppopup.ldapurl.text =

Webadressen p\u00E5 LDAP-serveren. Protokollen skal enten v\u00E6re ldap :// eller ldaps :// \ + (for LDAP over SSL). Se her \ + for en mere detaljeret beskrivelse.

+helppopup.ldapsearchfilter.title = LDAP s\u00F8gefilter +helppopup.ldapsearchfilter.text =

Filteret udtryk anvendes i brugernes s\u00F8gning. Dette er et LDAP s\u00F8gefilter \ + (som defineret i RFC 2254 ). \ + M\u00F8nsteret " '{0}" affattes brugernavn, for eksempel: \ +