/* This file is part of Subsonic. Subsonic is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Subsonic is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 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.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.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.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.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 = "CoverArtFileTypes"; private static final String KEY_COVER_ART_LIMIT = "CoverArtLimit"; private static final String KEY_WELCOME_TITLE = "WelcomeTitle"; private static final String KEY_WELCOME_SUBTITLE = "WelcomeSubtitle"; private static final String KEY_WELCOME_MESSAGE = "WelcomeMessage2"; private static final String KEY_LOGIN_MESSAGE = "LoginMessage"; private static final String KEY_LOCALE_LANGUAGE = "LocaleLanguage"; private static final String KEY_LOCALE_COUNTRY = "LocaleCountry"; private static final String KEY_LOCALE_VARIANT = "LocaleVariant"; private static final String KEY_THEME_ID = "Theme"; private static final String KEY_INDEX_CREATION_INTERVAL = "IndexCreationInterval"; private static final String KEY_INDEX_CREATION_HOUR = "IndexCreationHour"; private static final String KEY_FAST_CACHE_ENABLED = "FastCacheEnabled"; private static final String KEY_PODCAST_UPDATE_INTERVAL = "PodcastUpdateInterval"; private static final String KEY_PODCAST_FOLDER = "PodcastFolder"; private static final String KEY_PODCAST_EPISODE_RETENTION_COUNT = "PodcastEpisodeRetentionCount"; private static final String KEY_PODCAST_EPISODE_DOWNLOAD_COUNT = "PodcastEpisodeDownloadCount"; private static final String KEY_DOWNLOAD_BITRATE_LIMIT = "DownloadBitrateLimit"; private static final String KEY_UPLOAD_BITRATE_LIMIT = "UploadBitrateLimit"; private static final String KEY_STREAM_PORT = "StreamPort"; private static final String KEY_LICENSE_EMAIL = "LicenseEmail"; private static final String KEY_LICENSE_CODE = "LicenseCode"; private static final String KEY_LICENSE_DATE = "LicenseDate"; private static final String KEY_DOWNSAMPLING_COMMAND = "DownsamplingCommand3"; private static final String KEY_HLS_COMMAND = "HlsCommand2"; private static final String KEY_JUKEBOX_COMMAND = "JukeboxCommand"; private static final String KEY_REWRITE_URL = "RewriteUrl"; private static final String KEY_LDAP_ENABLED = "LdapEnabled"; private static final String KEY_LDAP_URL = "LdapUrl"; private static final String KEY_LDAP_MANAGER_DN = "LdapManagerDn"; private static final String KEY_LDAP_MANAGER_PASSWORD = "LdapManagerPassword"; private static final String KEY_LDAP_SEARCH_FILTER = "LdapSearchFilter"; private static final String KEY_LDAP_AUTO_SHADOWING = "LdapAutoShadowing"; private static final String KEY_GETTING_STARTED_ENABLED = "GettingStartedEnabled"; private static final String KEY_PORT_FORWARDING_ENABLED = "PortForwardingEnabled"; private static final String KEY_PORT = "Port"; private static final String KEY_HTTPS_PORT = "HttpsPort"; private static final String KEY_URL_REDIRECTION_ENABLED = "UrlRedirectionEnabled"; private static final String KEY_URL_REDIRECT_FROM = "UrlRedirectFrom"; private static final String KEY_URL_REDIRECT_CONTEXT_PATH = "UrlRedirectContextPath"; 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"; // Default values. private static final String DEFAULT_INDEX_STRING = "A B C D E F G H I J K L M N O P Q R S T U V W X-Z(XYZ)"; private static final String DEFAULT_IGNORED_ARTICLES = "The El La Los Las Le Les"; private static final String DEFAULT_SHORTCUTS = "New Incoming Podcast"; private static final String DEFAULT_PLAYLIST_FOLDER = Util.getDefaultPlaylistFolder(); private static final String DEFAULT_MUSIC_FILE_TYPES = "mp3 ogg oga aac m4a flac wav wma aif aiff ape mpc shn"; private static final String DEFAULT_VIDEO_FILE_TYPES = "flv avi mpg mpeg mp4 m4v mkv mov wmv ogv divx m2ts"; private static final String DEFAULT_COVER_ART_FILE_TYPES = "cover.jpg folder.jpg jpg jpeg gif png"; private static final int DEFAULT_COVER_ART_LIMIT = 50; private static final String DEFAULT_WELCOME_TITLE = "Welcome to Subsonic!"; private static final String DEFAULT_WELCOME_SUBTITLE = null; private static final String DEFAULT_WELCOME_MESSAGE = "__Welcome to Subsonic!__\n" + "\\\\ \\\\\n" + "Subsonic is a free, web-based media streamer, providing ubiquitous access to your music. \n" + "\\\\ \\\\\n" + "Use it to share your music with friends, or to listen to your own music while at work. You can stream to multiple " + "players simultaneously, for instance to one player in your kitchen and another in your living room.\n" + "\\\\ \\\\\n" + "To change or remove this message, log in with administrator rights and go to {link:Settings > General|generalSettings.view}."; private static final String DEFAULT_LOGIN_MESSAGE = null; private static final String DEFAULT_LOCALE_LANGUAGE = "en"; private static final String DEFAULT_LOCALE_COUNTRY = ""; private static final String DEFAULT_LOCALE_VARIANT = ""; private static final String DEFAULT_THEME_ID = "default"; private static final int DEFAULT_INDEX_CREATION_INTERVAL = 1; private static final int DEFAULT_INDEX_CREATION_HOUR = 3; private static final boolean DEFAULT_FAST_CACHE_ENABLED = false; private static final int DEFAULT_PODCAST_UPDATE_INTERVAL = 24; private static final String DEFAULT_PODCAST_FOLDER = Util.getDefaultPodcastFolder(); private static final int DEFAULT_PODCAST_EPISODE_RETENTION_COUNT = 10; private static final int DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT = 1; private static final long DEFAULT_DOWNLOAD_BITRATE_LIMIT = 0; private static final long DEFAULT_UPLOAD_BITRATE_LIMIT = 0; private static final long DEFAULT_STREAM_PORT = 0; private static final String DEFAULT_LICENSE_EMAIL = null; private static final String DEFAULT_LICENSE_CODE = null; private static final String DEFAULT_LICENSE_DATE = null; private static final String DEFAULT_DOWNSAMPLING_COMMAND = "ffmpeg -i %s -ab %bk -v 0 -f mp3 -"; private static final String DEFAULT_HLS_COMMAND = "ffmpeg -ss %o -t %d -i %s -async 1 -b %bk -s %wx%h -ar 44100 -ac 2 -v 0 -f mpegts -vcodec libx264 -preset superfast -acodec libmp3lame -threads 0 -"; private static final String DEFAULT_JUKEBOX_COMMAND = "ffmpeg -ss %o -i %s -v 0 -f au -"; private static final boolean DEFAULT_REWRITE_URL = true; private static final boolean DEFAULT_LDAP_ENABLED = false; private static final String DEFAULT_LDAP_URL = "ldap://host.domain.com:389/cn=Users,dc=domain,dc=com"; private static final String DEFAULT_LDAP_MANAGER_DN = null; private static final String DEFAULT_LDAP_MANAGER_PASSWORD = null; private static final String DEFAULT_LDAP_SEARCH_FILTER = "(sAMAccountName={0})"; private static final boolean DEFAULT_LDAP_AUTO_SHADOWING = false; private static final boolean DEFAULT_PORT_FORWARDING_ENABLED = false; private static final boolean DEFAULT_GETTING_STARTED_ENABLED = true; private static final int DEFAULT_PORT = 80; private static final int DEFAULT_HTTPS_PORT = 0; private static final boolean DEFAULT_URL_REDIRECTION_ENABLED = false; private static final String DEFAULT_URL_REDIRECT_FROM = "yourname"; private static final String DEFAULT_URL_REDIRECT_CONTEXT_PATH = null; private static final String DEFAULT_SERVER_ID = null; private static final long DEFAULT_SETTINGS_CHANGED = 0L; private static final boolean DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE = true; private static final boolean DEFAULT_SORT_ALBUMS_BY_YEAR = true; 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 = true; // Array of obsolete keys. Used to clean property file. private static final List OBSOLETE_KEYS = Arrays.asList("PortForwardingPublicPort", "PortForwardingLocalPort", "DownsamplingCommand", "DownsamplingCommand2", "AutoCoverBatch", "MusicMask", "VideoMask", "CoverArtMask, HlsCommand", "UrlRedirectTrialExpires", "VideoTrialExpires"); 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 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; 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.debug("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() { ServiceLocator.setSettingsService(this); scheduleLicenseValidation(); } public void save() { save(true); } public void save(boolean updateChangedDate) { if (updateChangedDate) { setProperty(KEY_SETTINGS_CHANGED, String.valueOf(System.currentTimeMillis())); } OutputStream out = null; try { out = new FileOutputStream(getPropertyFile()); properties.store(out, "Subsonic preferences. NOTE: This file is automatically generated."); } catch (Exception x) { LOG.error("Unable to write to property file.", x); } finally { IOUtils.closeQuietly(out); } } private File getPropertyFile() { return new File(getSubsonicHome(), "subsonic.properties"); } /** * Returns the Subsonic home directory. * * @return The Subsonic home directory, if it exists. * @throws RuntimeException If directory doesn't exist. */ public static synchronized File getSubsonicHome() { if (subsonicHome != null) { return subsonicHome; } File home; String overrideHome = System.getProperty("subsonic.home"); if (overrideHome != null) { home = new File(overrideHome); } else { boolean isWindows = System.getProperty("os.name", "Windows").toLowerCase().startsWith("windows"); home = isWindows ? SUBSONIC_HOME_WINDOWS : SUBSONIC_HOME_OTHER; } // Attempt to create home directory if it doesn't exist. if (!home.exists() || !home.isDirectory()) { boolean success = home.mkdirs(); if (success) { subsonicHome = home; } else { String message = "The directory " + home + " does not exist. Please create it and make it writable. " + "(You can override the directory location by specifying -Dsubsonic.home=... when " + "starting the servlet container.)"; System.err.println("ERROR: " + message); } } else { subsonicHome = home; } return home; } private boolean getBoolean(String key, boolean defaultValue) { return Boolean.valueOf(properties.getProperty(key, String.valueOf(defaultValue))); } private void setBoolean(String key, boolean value) { setProperty(key, String.valueOf(value)); } 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 getCoverArtLimit() { return Integer.parseInt(properties.getProperty(KEY_COVER_ART_LIMIT, "" + DEFAULT_COVER_ART_LIMIT)); } public void setCoverArtLimit(int limit) { setProperty(KEY_COVER_ART_LIMIT, "" + limit); } public String getWelcomeTitle() { return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_TITLE, DEFAULT_WELCOME_TITLE)); } public void setWelcomeTitle(String title) { setProperty(KEY_WELCOME_TITLE, title); } public String getWelcomeSubtitle() { return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_SUBTITLE, DEFAULT_WELCOME_SUBTITLE)); } public void setWelcomeSubtitle(String subtitle) { setProperty(KEY_WELCOME_SUBTITLE, subtitle); } public String getWelcomeMessage() { return StringUtils.trimToNull(properties.getProperty(KEY_WELCOME_MESSAGE, DEFAULT_WELCOME_MESSAGE)); } public void setWelcomeMessage(String message) { setProperty(KEY_WELCOME_MESSAGE, message); } public String getLoginMessage() { return StringUtils.trimToNull(properties.getProperty(KEY_LOGIN_MESSAGE, DEFAULT_LOGIN_MESSAGE)); } public void setLoginMessage(String message) { setProperty(KEY_LOGIN_MESSAGE, message); } /** * Returns the number of days between automatic index creation, of -1 if automatic index * creation is disabled. */ public int getIndexCreationInterval() { return Integer.parseInt(properties.getProperty(KEY_INDEX_CREATION_INTERVAL, "" + DEFAULT_INDEX_CREATION_INTERVAL)); } /** * Sets the number of days between automatic index creation, of -1 if automatic index * creation is disabled. */ public void setIndexCreationInterval(int days) { setProperty(KEY_INDEX_CREATION_INTERVAL, String.valueOf(days)); } /** * Returns the hour of day (0 - 23) when automatic index creation should run. */ public int getIndexCreationHour() { return Integer.parseInt(properties.getProperty(KEY_INDEX_CREATION_HOUR, String.valueOf(DEFAULT_INDEX_CREATION_HOUR))); } /** * Sets the hour of day (0 - 23) when automatic index creation should run. */ public void setIndexCreationHour(int hour) { setProperty(KEY_INDEX_CREATION_HOUR, String.valueOf(hour)); } public boolean isFastCacheEnabled() { return getBoolean(KEY_FAST_CACHE_ENABLED, DEFAULT_FAST_CACHE_ENABLED); } public void setFastCacheEnabled(boolean enabled) { setBoolean(KEY_FAST_CACHE_ENABLED, enabled); } /** * Returns the number of hours between Podcast updates, of -1 if automatic updates * are disabled. */ public int getPodcastUpdateInterval() { return Integer.parseInt(properties.getProperty(KEY_PODCAST_UPDATE_INTERVAL, String.valueOf(DEFAULT_PODCAST_UPDATE_INTERVAL))); } /** * Sets the number of hours between Podcast updates, of -1 if automatic updates * are disabled. */ public void setPodcastUpdateInterval(int hours) { setProperty(KEY_PODCAST_UPDATE_INTERVAL, String.valueOf(hours)); } /** * Returns the number of Podcast episodes to keep (-1 to keep all). */ public int getPodcastEpisodeRetentionCount() { return Integer.parseInt(properties.getProperty(KEY_PODCAST_EPISODE_RETENTION_COUNT, String.valueOf(DEFAULT_PODCAST_EPISODE_RETENTION_COUNT))); } /** * Sets the number of Podcast episodes to keep (-1 to keep all). */ public void setPodcastEpisodeRetentionCount(int count) { setProperty(KEY_PODCAST_EPISODE_RETENTION_COUNT, String.valueOf(count)); } /** * Returns the number of Podcast episodes to download (-1 to download all). */ public int getPodcastEpisodeDownloadCount() { return Integer.parseInt(properties.getProperty(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, String.valueOf(DEFAULT_PODCAST_EPISODE_DOWNLOAD_COUNT))); } /** * Sets the number of Podcast episodes to download (-1 to download all). */ public void setPodcastEpisodeDownloadCount(int count) { setProperty(KEY_PODCAST_EPISODE_DOWNLOAD_COUNT, String.valueOf(count)); } /** * Returns the Podcast download folder. */ public String getPodcastFolder() { return properties.getProperty(KEY_PODCAST_FOLDER, DEFAULT_PODCAST_FOLDER); } /** * Sets the Podcast download folder. */ public void setPodcastFolder(String folder) { setProperty(KEY_PODCAST_FOLDER, folder); } /** * @return The download bitrate limit in Kbit/s. Zero if unlimited. */ public long getDownloadBitrateLimit() { return Long.parseLong(properties.getProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + DEFAULT_DOWNLOAD_BITRATE_LIMIT)); } /** * @param limit The download bitrate limit in Kbit/s. Zero if unlimited. */ public void setDownloadBitrateLimit(long limit) { setProperty(KEY_DOWNLOAD_BITRATE_LIMIT, "" + limit); } /** * @return The upload bitrate limit in Kbit/s. Zero if unlimited. */ public long getUploadBitrateLimit() { return Long.parseLong(properties.getProperty(KEY_UPLOAD_BITRATE_LIMIT, "" + DEFAULT_UPLOAD_BITRATE_LIMIT)); } /** * @param limit The upload bitrate limit in Kbit/s. Zero if unlimited. */ public void setUploadBitrateLimit(long limit) { setProperty(KEY_UPLOAD_BITRATE_LIMIT, "" + limit); } /** * @return The non-SSL stream port. Zero if disabled. */ public int getStreamPort() { return Integer.parseInt(properties.getProperty(KEY_STREAM_PORT, "" + DEFAULT_STREAM_PORT)); } /** * @param port The non-SSL stream port. Zero if disabled. */ public void setStreamPort(int port) { setProperty(KEY_STREAM_PORT, "" + port); } public String getLicenseEmail() { return properties.getProperty(KEY_LICENSE_EMAIL, DEFAULT_LICENSE_EMAIL); } public void setLicenseEmail(String email) { setProperty(KEY_LICENSE_EMAIL, email); } public String getLicenseCode() { return properties.getProperty(KEY_LICENSE_CODE, DEFAULT_LICENSE_CODE); } public void setLicenseCode(String code) { setProperty(KEY_LICENSE_CODE, code); } public Date getLicenseDate() { String value = properties.getProperty(KEY_LICENSE_DATE, DEFAULT_LICENSE_DATE); return value == null ? null : new Date(Long.parseLong(value)); } public void setLicenseDate(Date date) { String value = (date == null ? null : String.valueOf(date.getTime())); setProperty(KEY_LICENSE_DATE, value); } public boolean isLicenseValid() { return true; } public boolean isLicenseValid(String email, String license) { return true; } 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 boolean isRewriteUrlEnabled() { return getBoolean(KEY_REWRITE_URL, DEFAULT_REWRITE_URL); } public void setRewriteUrlEnabled(boolean rewriteUrl) { setBoolean(KEY_REWRITE_URL, rewriteUrl); } public boolean isLdapEnabled() { return getBoolean(KEY_LDAP_ENABLED, DEFAULT_LDAP_ENABLED); } public void setLdapEnabled(boolean ldapEnabled) { setBoolean(KEY_LDAP_ENABLED, ldapEnabled); } public String getLdapUrl() { return properties.getProperty(KEY_LDAP_URL, DEFAULT_LDAP_URL); } public void setLdapUrl(String ldapUrl) { properties.setProperty(KEY_LDAP_URL, ldapUrl); } public String getLdapSearchFilter() { return properties.getProperty(KEY_LDAP_SEARCH_FILTER, DEFAULT_LDAP_SEARCH_FILTER); } public void setLdapSearchFilter(String ldapSearchFilter) { properties.setProperty(KEY_LDAP_SEARCH_FILTER, ldapSearchFilter); } public String getLdapManagerDn() { return properties.getProperty(KEY_LDAP_MANAGER_DN, DEFAULT_LDAP_MANAGER_DN); } public void setLdapManagerDn(String ldapManagerDn) { properties.setProperty(KEY_LDAP_MANAGER_DN, ldapManagerDn); } public String getLdapManagerPassword() { String s = properties.getProperty(KEY_LDAP_MANAGER_PASSWORD, DEFAULT_LDAP_MANAGER_PASSWORD); try { return StringUtil.utf8HexDecode(s); } catch (Exception x) { LOG.warn("Failed to decode LDAP manager password.", x); return s; } } public void setLdapManagerPassword(String ldapManagerPassword) { try { ldapManagerPassword = StringUtil.utf8HexEncode(ldapManagerPassword); } catch (Exception x) { LOG.warn("Failed to encode LDAP manager password.", x); } properties.setProperty(KEY_LDAP_MANAGER_PASSWORD, ldapManagerPassword); } public boolean isLdapAutoShadowing() { return getBoolean(KEY_LDAP_AUTO_SHADOWING, DEFAULT_LDAP_AUTO_SHADOWING); } public void setLdapAutoShadowing(boolean ldapAutoShadowing) { setBoolean(KEY_LDAP_AUTO_SHADOWING, ldapAutoShadowing); } public boolean isGettingStartedEnabled() { return getBoolean(KEY_GETTING_STARTED_ENABLED, DEFAULT_GETTING_STARTED_ENABLED); } public void setGettingStartedEnabled(boolean isGettingStartedEnabled) { setBoolean(KEY_GETTING_STARTED_ENABLED, isGettingStartedEnabled); } public boolean isPortForwardingEnabled() { return getBoolean(KEY_PORT_FORWARDING_ENABLED, DEFAULT_PORT_FORWARDING_ENABLED); } public void setPortForwardingEnabled(boolean isPortForwardingEnabled) { setBoolean(KEY_PORT_FORWARDING_ENABLED, isPortForwardingEnabled); } public int getPort() { return Integer.valueOf(properties.getProperty(KEY_PORT, String.valueOf(DEFAULT_PORT))); } public void setPort(int port) { setProperty(KEY_PORT, String.valueOf(port)); } public int getHttpsPort() { return Integer.valueOf(properties.getProperty(KEY_HTTPS_PORT, String.valueOf(DEFAULT_HTTPS_PORT))); } public void setHttpsPort(int httpsPort) { setProperty(KEY_HTTPS_PORT, String.valueOf(httpsPort)); } public boolean isUrlRedirectionEnabled() { return getBoolean(KEY_URL_REDIRECTION_ENABLED, DEFAULT_URL_REDIRECTION_ENABLED); } public void setUrlRedirectionEnabled(boolean isUrlRedirectionEnabled) { setBoolean(KEY_URL_REDIRECTION_ENABLED, isUrlRedirectionEnabled); } public String getUrlRedirectFrom() { return properties.getProperty(KEY_URL_REDIRECT_FROM, DEFAULT_URL_REDIRECT_FROM); } public void setUrlRedirectFrom(String urlRedirectFrom) { properties.setProperty(KEY_URL_REDIRECT_FROM, urlRedirectFrom); } public Date 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 getServerId() { return properties.getProperty(KEY_SERVER_ID, DEFAULT_SERVER_ID); } public void setServerId(String serverId) { properties.setProperty(KEY_SERVER_ID, serverId); } public long getSettingsChanged() { return Long.parseLong(properties.getProperty(KEY_SETTINGS_CHANGED, String.valueOf(DEFAULT_SETTINGS_CHANGED))); } public Date getLastScanned() { String lastScanned = properties.getProperty(KEY_LAST_SCANNED); return lastScanned == null ? null : new Date(Long.parseLong(lastScanned)); } public void setLastScanned(Date date) { if (date == null) { properties.remove(KEY_LAST_SCANNED); } else { properties.setProperty(KEY_LAST_SCANNED, String.valueOf(date.getTime())); } } public boolean isOrganizeByFolderStructure() { return getBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, DEFAULT_ORGANIZE_BY_FOLDER_STRUCTURE); } public void setOrganizeByFolderStructure(boolean b) { setBoolean(KEY_ORGANIZE_BY_FOLDER_STRUCTURE, b); } public boolean isSortAlbumsByYear() { return getBoolean(KEY_SORT_ALBUMS_BY_YEAR, DEFAULT_SORT_ALBUMS_BY_YEAR); } public void setSortAlbumsByYear(boolean b) { setBoolean(KEY_SORT_ALBUMS_BY_YEAR, b); } 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 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); cachedMusicFolders = null; } /** * Deletes the music folder with the given ID. * * @param id The ID of the music folder to delete. */ public void deleteMusicFolder(Integer id) { musicFolderDao.deleteMusicFolder(id); cachedMusicFolders = null; } /** * Updates the given music folder. * * @param musicFolder The music folder to update. */ public void updateMusicFolder(MusicFolder musicFolder) { musicFolderDao.updateMusicFolder(musicFolder); cachedMusicFolders = null; } /** * Returns all internet radio stations. Disabled stations are not returned. * * @return Possibly empty list of all internet radio stations. */ public List 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.setShowNowPlayingEnabled(true); settings.setShowChatEnabled(true); settings.setPartyModeEnabled(false); settings.setNowPlayingAllowed(true); settings.setLastFmEnabled(false); settings.setLastFmUsername(null); settings.setLastFmPassword(null); settings.setChanged(new Date()); UserSettings.Visibility playlist = settings.getPlaylistVisibility(); playlist.setCaptionCutoff(35); playlist.setArtistVisible(true); playlist.setAlbumVisible(true); playlist.setYearVisible(true); playlist.setDurationVisible(true); playlist.setBitRateVisible(true); playlist.setFormatVisible(true); playlist.setFileSizeVisible(true); UserSettings.Visibility main = settings.getMainVisibility(); main.setCaptionCutoff(35); main.setTrackNumberVisible(true); main.setArtistVisible(true); main.setDurationVisible(true); return settings; } /** * Updates settings for the given username. * * @param settings The user-specific settings. */ public void updateUserSettings(UserSettings settings) { userDao.updateUserSettings(settings); } /** * Returns all system avatars. * * @return All system avatars. */ public List 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); } 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(); licenseValidated = true; return; } public synchronized void scheduleLicenseValidation() { if (licenseValidationFuture != null) { licenseValidationFuture.cancel(true); } Runnable task = new Runnable() { public void run() { validateLicense(); } }; licenseValidationFuture = executor.scheduleWithFixedDelay(task, 0L, LICENSE_VALIDATION_DELAY_HOURS, TimeUnit.HOURS); } 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; } }