/* * PS3 Media Server, for streaming any medias to your PS3. * Copyright (C) 2008 A.Brochard * * 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; version 2 * of the License only. * * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package net.pms; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import com.sun.jna.Platform; import com.sun.net.httpserver.HttpServer; import java.awt.*; import java.io.*; import java.net.BindException; import java.nio.charset.StandardCharsets; import java.security.AccessControlException; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.*; import java.util.List; import java.util.Map.Entry; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.LogManager; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.spi.IIORegistry; import javax.imageio.spi.ImageReaderSpi; import javax.imageio.spi.ImageWriterSpi; import javax.jmdns.JmDNS; import javax.swing.*; import net.pms.configuration.Build; import net.pms.configuration.DeviceConfiguration; import net.pms.configuration.NameFilter; import net.pms.configuration.PmsConfiguration; import net.pms.configuration.RendererConfiguration; import net.pms.database.Tables; import net.pms.dlna.*; import net.pms.dlna.virtual.MediaLibrary; import net.pms.encoders.Player; import net.pms.encoders.PlayerFactory; import net.pms.external.ExternalFactory; import net.pms.external.ExternalListener; import net.pms.formats.Format; import net.pms.formats.FormatFactory; import net.pms.io.*; import net.pms.logging.CacheLogger; import net.pms.logging.FrameAppender; import net.pms.logging.LoggingConfig; import net.pms.network.ChromecastMgr; import net.pms.network.HTTPServer; import net.pms.network.ProxyServer; import net.pms.network.UPNPHelper; import net.pms.newgui.*; import net.pms.remote.RemoteWeb; import net.pms.update.AutoUpdater; import net.pms.util.*; import org.apache.commons.configuration.ConfigurationException; import org.apache.commons.configuration.event.ConfigurationEvent; import org.apache.commons.configuration.event.ConfigurationListener; import org.apache.commons.lang.WordUtils; import org.fest.util.Files; import org.slf4j.ILoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class PMS { private static final String SCROLLBARS = "scrollbars"; private static final String NATIVELOOK = "nativelook"; private static final String CONSOLE = "console"; private static final String HEADLESS = "headless"; private static final String NOCONSOLE = "noconsole"; private static final String PROFILES = "profiles"; private static final String PROFILE = "^(?i)profile(?::|=)([^\"*<>?]+)$"; private static final String TRACE = "trace"; public static final String NAME = "Universal Media Server"; public static final String CROWDIN_LINK = "https://crowdin.com/project/universalmediaserver"; /** * @deprecated The version has moved to the resources/project.properties file. Use {@link #getVersion()} instead. */ @Deprecated public static String VERSION; private boolean ready = false; private static FileWatcher fileWatcher; private GlobalIdRepo globalRepo; public static final String AVS_SEPARATOR = "\1"; // (innot): The logger used for all logging. private static final Logger LOGGER = LoggerFactory.getLogger(PMS.class); // TODO(tcox): This shouldn't be static private static PmsConfiguration configuration; /** * Universally Unique Identifier used in the UPnP server. */ private String uuid; /** * Relative location of a context sensitive help page in the documentation * directory. */ private static String helpPage = "index.html"; private NameFilter filter; private JmDNS jmDNS; /** * Returns a pointer to the PMS GUI's main window. * @return {@link net.pms.newgui.IFrame} Main PMS window. */ public IFrame getFrame() { return frame; } /** * Returns the root folder for a given renderer. There could be the case * where a given media renderer needs a different root structure. * * @param renderer {@link net.pms.configuration.RendererConfiguration} * is the renderer for which to get the RootFolder structure. If <code>null</code>, * then the default renderer is used. * @return {@link net.pms.dlna.RootFolder} The root folder structure for a given renderer */ public RootFolder getRootFolder(RendererConfiguration renderer) { // something to do here for multiple directories views for each renderer if (renderer == null) { renderer = RendererConfiguration.getDefaultConf(); } return renderer.getRootFolder(); } /** * Pointer to a running PMS server. */ private static PMS instance = null; /** * Array of {@link net.pms.configuration.RendererConfiguration} that have * been found by UMS.<br><br> * * Important! If iteration is done on this list it's not thread safe unless * the iteration loop is enclosed by a <code>synchronized</code> block on * the <code>List itself</code>. */ private final List<RendererConfiguration> foundRenderers = Collections.synchronizedList(new ArrayList<RendererConfiguration>()); /** * The returned <code>List</code> itself is thread safe, but the objects * it's holding is not. Any looping/iterating of this <code>List</code> * MUST be enclosed in: * S<pre><code> * synchronized(getFoundRenderers()) { * ..code.. * } * </code></pre> * @return {@link #foundRenderers} */ public List<RendererConfiguration> getFoundRenderers() { return foundRenderers; } /** * @deprecated Use {@link #setRendererFound(RendererConfiguration)} instead. */ @Deprecated public void setRendererfound(RendererConfiguration renderer) { setRendererFound(renderer); } /** * Adds a {@link net.pms.configuration.RendererConfiguration} to the list of media renderers found. * The list is being used, for example, to give the user a graphical representation of the found * media renderers. * * @param renderer {@link net.pms.configuration.RendererConfiguration} * @since 1.82.0 */ public void setRendererFound(RendererConfiguration renderer) { synchronized (foundRenderers) { if (!foundRenderers.contains(renderer) && !renderer.isFDSSDP()) { LOGGER.debug("Adding status button for " + renderer.getRendererName()); foundRenderers.add(renderer); frame.addRenderer(renderer); frame.setStatusCode(0, Messages.getString("PMS.18"), "icon-status-connected.png"); } } } public void updateRenderer(RendererConfiguration renderer) { LOGGER.debug("Updating status button for " + renderer.getRendererName()); frame.updateRenderer(renderer); } /** * HTTP server that serves the XML files needed by UPnP server and the media files. */ private HTTPServer server; /** * User friendly name for the server. */ private String serverName; // FIXME unused private ProxyServer proxyServer; public ProxyServer getProxy() { return proxyServer; } public ArrayList<Process> currentProcesses = new ArrayList<>(); private PMS() { } /** * {@link net.pms.newgui.IFrame} object that represents the PMS GUI. */ private IFrame frame; /** * Interface to Windows-specific functions, like Windows Registry. registry is set by {@link #init()}. * @see net.pms.io.WinUtils */ private SystemUtils registry; /** * @see net.pms.io.WinUtils */ public SystemUtils getRegistry() { return registry; } /** * Main resource database that supports search capabilities. Also known as media cache. * @see net.pms.dlna.DLNAMediaDatabase */ private DLNAMediaDatabase database; private Object databaseLock = new Object(); /** * Used to get the database. Needed in the case of the Xbox 360, that requires a database. * for its queries. * @return (DLNAMediaDatabase) a reference to the database. */ public DLNAMediaDatabase getDatabase() { synchronized (databaseLock) { if (database == null) { database = new DLNAMediaDatabase("medias"); database.init(false); } return database; } } private void displayBanner() throws IOException { LOGGER.debug(""); LOGGER.info("Starting " + PropertiesUtil.getProjectProperties().get("project.name") + " " + getVersion()); LOGGER.info("Based on PS3 Media Server by shagrath, copyright 2008-2014"); LOGGER.info("http://www.universalmediaserver.com"); LOGGER.info(""); String commitId = PropertiesUtil.getProjectProperties().get("git.commit.id"); String commitTime = PropertiesUtil.getProjectProperties().get("git.commit.time"); String shortCommitId = commitId.substring(0, 9); LOGGER.info("Build: " + shortCommitId + " (" + commitTime + ")"); // Log system properties logSystemInfo(); String cwd = new File("").getAbsolutePath(); LOGGER.info("Working directory: " + cwd); LOGGER.info("Temporary directory: " + configuration.getTempFolder()); /** * Verify the java.io.tmpdir is writable; JNA requires it. * Note: the configured tempFolder has already been checked, but it * may differ from the java.io.tmpdir so double check to be sure. */ File javaTmpdir = new File(System.getProperty("java.io.tmpdir")); if (!FileUtil.getFilePermissions(javaTmpdir).isWritable()) { LOGGER.error("The Java temp directory \"" + javaTmpdir.getAbsolutePath() + "\" is not writable by UMS"); LOGGER.error("Please make sure the directory is writable for user \"" + System.getProperty("user.name") + "\""); throw new IOException("Cannot write to Java temp directory: " + javaTmpdir.getAbsolutePath()); } LOGGER.info("Logging configuration file: " + LoggingConfig.getConfigFilePath()); HashMap<String, String> lfps = LoggingConfig.getLogFilePaths(); // Logfile name(s) and path(s) if (lfps != null && lfps.size() > 0) { if (lfps.size() == 1) { Entry<String, String> entry = lfps.entrySet().iterator().next(); if (entry.getKey().toLowerCase().equals("default.log")) { LOGGER.info("Logfile: {}", entry.getValue()); } else { LOGGER.info("{}: {}", entry.getKey(), entry.getValue()); } } else { LOGGER.info("Logging to multiple files:"); Iterator<Entry<String, String>> logsIterator = lfps.entrySet().iterator(); Entry<String, String> entry; while (logsIterator.hasNext()) { entry = logsIterator.next(); LOGGER.info("{}: {}", entry.getKey(), entry.getValue()); } } } String profilePath = configuration.getProfilePath(); String profileDirectoryPath = configuration.getProfileDirectory(); LOGGER.info(""); LOGGER.info("Profile directory: {}", profileDirectoryPath); try { // Don't use the {} syntax here as the check needs to be performed on every log level LOGGER.info("Profile directory permissions: " + FileUtil.getFilePermissions(profileDirectoryPath)); } catch (FileNotFoundException e) { LOGGER.warn("Profile directory not found: {}", e.getMessage()); } LOGGER.info("Profile configuration file: {}", profilePath); try { // Don't use the {} syntax here as the check needs to be performed on every log level LOGGER.info("Profile configuration file permissions: " + FileUtil.getFilePermissions(profilePath)); } catch (FileNotFoundException e) { LOGGER.warn("Profile configuration file not found: {}", e.getMessage()); } LOGGER.info("Profile name: {}", configuration.getProfileName()); LOGGER.info(""); if (configuration.useWebInterface()) { String webConfPath = configuration.getWebConfPath(); LOGGER.info("Web configuration file: {}", webConfPath); try { // Don't use the {} syntax here as the check needs to be performed on every log level LOGGER.info("Web configuration file permissions: " + FileUtil.getFilePermissions(webConfPath)); } catch (FileNotFoundException e) { LOGGER.warn("Web configuration file not found: {}", e.getMessage()); } LOGGER.info(""); } /** * Ensure the data directory is created. On Windows this is * usually done by the installer */ File dDir = new File(configuration.getDataDir()); if (!dDir.exists() && !dDir.mkdirs()) { LOGGER.error("Failed to create profile folder \"{}\"", configuration.getDataDir()); } dbgPack = new DbgPacker(); tfm = new TempFileMgr(); try { filter = new NameFilter(); } catch (ConfigurationException e) { filter = null; } // This should be removed soon OpenSubtitle.convert(); // Start this here to let the converison work tfm.schedule(); } /** * Initialization procedure for UMS. * * @return <code>true</code> if the server has been initialized correctly. * <code>false</code> if initialization was aborted. * @throws Exception */ private boolean init() throws Exception { // Show the language selection dialog before displayBanner(); if ( !isHeadless() && (configuration.getLanguageRawString() == null || !Languages.isValid(configuration.getLanguageRawString())) ) { LanguageSelection languageDialog = new LanguageSelection(null, PMS.getLocale(), false); if (languageDialog != null) { languageDialog.show(); if (languageDialog.isAborted()) { return false; } } } // Call this as early as possible displayBanner(); // Initialize database Tables.checkTables(); // Log registered ImageIO plugins if (LOGGER.isTraceEnabled()) { LOGGER.trace(""); LOGGER.trace("Registered ImageIO reader classes:"); Iterator<ImageReaderSpi> readerIterator = IIORegistry.getDefaultInstance().getServiceProviders(ImageReaderSpi.class, true); while (readerIterator.hasNext()) { ImageReaderSpi reader = readerIterator.next(); LOGGER.trace("Reader class: {}", reader.getPluginClassName()); } LOGGER.trace(""); LOGGER.trace("Registered ImageIO writer classes:"); Iterator<ImageWriterSpi> writerIterator = IIORegistry.getDefaultInstance().getServiceProviders(ImageWriterSpi.class, true); while (writerIterator.hasNext()) { ImageWriterSpi writer = writerIterator.next(); LOGGER.trace("Writer class: {}", writer.getPluginClassName()); } LOGGER.trace(""); } // Wizard if (configuration.isRunWizard() && !isHeadless()) { // Ask the user if they want to run the wizard int whetherToRunWizard = JOptionPane.showConfirmDialog( null, Messages.getString("Wizard.1"), Messages.getString("Dialog.Question"), JOptionPane.YES_NO_OPTION ); if (whetherToRunWizard == JOptionPane.YES_OPTION) { // The user has chosen to run the wizard // Total number of questions int numberOfQuestions = 3; // The current question number int currentQuestionNumber = 1; // Ask if they want UMS to start minimized int whetherToStartMinimized = JOptionPane.showConfirmDialog( null, Messages.getString("Wizard.3"), Messages.getString("Wizard.2") + " " + (currentQuestionNumber++) + " " + Messages.getString("Wizard.4") + " " + numberOfQuestions, JOptionPane.YES_NO_OPTION ); if (whetherToStartMinimized == JOptionPane.YES_OPTION) { configuration.setMinimized(true); save(); } else if (whetherToStartMinimized == JOptionPane.NO_OPTION) { configuration.setMinimized(false); save(); } // Ask if their network is wired, etc. Object[] options = { Messages.getString("Wizard.8"), Messages.getString("Wizard.9"), Messages.getString("Wizard.10") }; int networkType = JOptionPane.showOptionDialog( null, Messages.getString("Wizard.7"), Messages.getString("Wizard.2") + " " + (currentQuestionNumber++) + " " + Messages.getString("Wizard.4") + " " + numberOfQuestions, JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1] ); switch (networkType) { case JOptionPane.YES_OPTION: // Wired (Gigabit) configuration.setMaximumBitrate("0"); configuration.setMPEG2MainSettings("Automatic (Wired)"); configuration.setx264ConstantRateFactor("Automatic (Wired)"); save(); break; case JOptionPane.NO_OPTION: // Wired (100 Megabit) configuration.setMaximumBitrate("90"); configuration.setMPEG2MainSettings("Automatic (Wired)"); configuration.setx264ConstantRateFactor("Automatic (Wired)"); save(); break; case JOptionPane.CANCEL_OPTION: // Wireless configuration.setMaximumBitrate("30"); configuration.setMPEG2MainSettings("Automatic (Wireless)"); configuration.setx264ConstantRateFactor("Automatic (Wireless)"); save(); break; default: break; } // Ask if they want to hide advanced options int whetherToHideAdvancedOptions = JOptionPane.showConfirmDialog( null, Messages.getString("Wizard.11"), Messages.getString("Wizard.2") + " " + (currentQuestionNumber++) + " " + Messages.getString("Wizard.4") + " " + numberOfQuestions, JOptionPane.YES_NO_OPTION ); if (whetherToHideAdvancedOptions == JOptionPane.YES_OPTION) { configuration.setHideAdvancedOptions(true); save(); } else if (whetherToHideAdvancedOptions == JOptionPane.NO_OPTION) { configuration.setHideAdvancedOptions(false); save(); } JOptionPane.showMessageDialog( null, Messages.getString("Wizard.13"), Messages.getString("Wizard.12"), JOptionPane.INFORMATION_MESSAGE ); configuration.setRunWizard(false); save(); } else if (whetherToRunWizard == JOptionPane.NO_OPTION) { // The user has chosen to not run the wizard // Do not ask them again configuration.setRunWizard(false); save(); } } // Splash Splash splash = null; if (!isHeadless()) { splash = new Splash(configuration); } // The public VERSION field is deprecated. // This is a temporary fix for backwards compatibility VERSION = getVersion(); fileWatcher = new FileWatcher(); globalRepo = new GlobalIdRepo(); AutoUpdater autoUpdater = null; if (Build.isUpdatable()) { String serverURL = Build.getUpdateServerURL(); autoUpdater = new AutoUpdater(serverURL, getVersion()); } registry = createSystemUtils(); if (!isHeadless()) { frame = new LooksFrame(autoUpdater, configuration); } else { LOGGER.info("Graphics environment not available or headless mode is forced"); LOGGER.info("Switching to console mode"); frame = new DummyFrame(); } if (splash != null) { splash.dispose(); } /* * we're here: * * main() -> createInstance() -> init() * * which means we haven't created the instance returned by get() * yet, so the frame appender can't access the frame in the * standard way i.e. PMS.get().getFrame(). we solve it by * inverting control ("don't call us; we'll call you") i.e. * we notify the appender when the frame is ready rather than * e.g. making getFrame() static and requiring the frame * appender to poll it. * * XXX an event bus (e.g. MBassador or Guava EventBus * (if they fix the memory-leak issue)) notification * would be cleaner and could support other lifecycle * notifications (see above). */ FrameAppender.setFrame(frame); configuration.addConfigurationListener(new ConfigurationListener() { @Override public void configurationChanged(ConfigurationEvent event) { if ((!event.isBeforeUpdate()) && PmsConfiguration.NEED_RELOAD_FLAGS.contains(event.getPropertyName())) { frame.setReloadable(true); } } }); // Web stuff if (configuration.useWebInterface()) { web = new RemoteWeb(configuration.getWebPort()); } // init Credentials credMgr = new CredMgr(configuration.getCredFile()); // init dbs keysDb = new UmsKeysDb(); infoDb = new InfoDb(); codes = new CodeDb(); masterCode = null; RendererConfiguration.loadRendererConfigurations(configuration); // Now that renderer confs are all loaded, we can start searching for renderers UPNPHelper.getInstance().init(); // launch ChromecastMgr jmDNS = null; launchJmDNSRenderers(); OutputParams outputParams = new OutputParams(configuration); // Prevent unwanted GUI buffer artifacts (and runaway timers) outputParams.hidebuffer = true; // Make sure buffer is destroyed outputParams.cleanup = true; // Initialize MPlayer and FFmpeg to let them generate fontconfig cache/s if (!configuration.isDisableSubtitles()) { LOGGER.info("Checking the fontconfig cache in the background, this can take two minutes or so."); ProcessWrapperImpl mplayer = new ProcessWrapperImpl(new String[]{configuration.getMplayerPath(), "dummy"}, outputParams); mplayer.runInNewThread(); /** * Note: Different versions of fontconfig and bitness require * different caches, which is why here we ask FFmpeg (64-bit * if possible) to create a cache. * This should result in all of the necessary caches being built. */ if (!Platform.isWindows() || Platform.is64Bit()) { ProcessWrapperImpl ffmpeg = new ProcessWrapperImpl(new String[]{configuration.getFfmpegPath(), "-y", "-f", "lavfi", "-i", "nullsrc=s=720x480:d=1:r=1", "-vf", "ass=DummyInput.ass", "-target", "ntsc-dvd", "-"}, outputParams); ffmpeg.runInNewThread(); } } frame.setStatusCode(0, Messages.getString("PMS.130"), "icon-status-connecting.png"); // Check the existence of VSFilter / DirectVobSub if (registry.isAvis() && registry.getAvsPluginsDir() != null) { LOGGER.debug("AviSynth plugins directory: " + registry.getAvsPluginsDir().getAbsolutePath()); File vsFilterDLL = new File(registry.getAvsPluginsDir(), "VSFilter.dll"); if (vsFilterDLL.exists()) { LOGGER.debug("VSFilter / DirectVobSub was found in the AviSynth plugins directory."); } else { File vsFilterDLL2 = new File(registry.getKLiteFiltersDir(), "vsfilter.dll"); if (vsFilterDLL2.exists()) { LOGGER.debug("VSFilter / DirectVobSub was found in the K-Lite Codec Pack filters directory."); } else { LOGGER.info("VSFilter / DirectVobSub was not found. This can cause problems when trying to play subtitled videos with AviSynth."); } } } // Check if VLC is found String vlcVersion = registry.getVlcVersion(); String vlcPath = registry.getVlcPath(); if (vlcVersion != null && vlcPath != null) { LOGGER.info("Found VLC version " + vlcVersion + " at: " + vlcPath); Version vlc = new Version(vlcVersion); Version requiredVersion = new Version("2.0.2"); if (vlc.compareTo(requiredVersion) <= 0) { LOGGER.error("Only VLC versions 2.0.2 and above are supported"); } } // Check if Kerio is installed if (registry.isKerioFirewall()) { LOGGER.info("Detected Kerio firewall"); } // Force use of specific DVR-MS muxer when it's installed in the right place File dvrsMsffmpegmuxer = new File("win32/dvrms/ffmpeg_MPGMUX.exe"); if (dvrsMsffmpegmuxer.exists()) { configuration.setFfmpegAlternativePath(dvrsMsffmpegmuxer.getAbsolutePath()); } // Disable jaudiotagger logging LogManager.getLogManager().readConfiguration(new ByteArrayInputStream("org.jaudiotagger.level=OFF".getBytes(StandardCharsets.US_ASCII))); // Wrap System.err System.setErr(new PrintStream(new SystemErrWrapper(), true, StandardCharsets.UTF_8.name())); server = new HTTPServer(configuration.getServerPort()); /* * XXX: keep this here (i.e. after registerExtensions and before registerPlayers) so that plugins * can register custom players correctly (e.g. in the GUI) and/or add/replace custom formats * * XXX: if a plugin requires initialization/notification even earlier than * this, then a new external listener implementing a new callback should be added * e.g. StartupListener.registeredExtensions() */ try { ExternalFactory.lookup(); } catch (Exception e) { LOGGER.error("Error loading plugins", e); } // Initialize a player factory to register all players PlayerFactory.initialize(); // Instantiate listeners that require registered players. ExternalFactory.instantiateLateListeners(); // a static block in Player doesn't work (i.e. is called too late). // this must always be called *after* the plugins have loaded. // here's as good a place as any Player.initializeFinalizeTranscoderArgsListeners(); // Any plugin-defined players are now registered, create the gui view. frame.addEngines(); // To make the credentials stuff work cross plugins read credentials // file AFTER plugins are started if (!isHeadless()) { // but only if we got a GUI of course ((LooksFrame)frame).getPt().init(); } boolean binding = false; try { binding = server.start(); } catch (BindException b) { LOGGER.error("FATAL ERROR: Unable to bind on port: " + configuration.getServerPort() + ", because: " + b.getMessage()); LOGGER.info("Maybe another process is running or the hostname is wrong."); } new Thread("Connection Checker") { @Override public void run() { try { Thread.sleep(7000); } catch (InterruptedException e) { } if (foundRenderers.isEmpty()) { frame.setStatusCode(0, Messages.getString("PMS.0"), "icon-status-notconnected.png"); } else { frame.setStatusCode(0, Messages.getString("PMS.18"), "icon-status-connected.png"); } } }.start(); if (!binding) { return false; } if (web != null && web.getServer() != null) { LOGGER.info("WEB interface is available at: " + web.getUrl()); } // initialize the cache if (configuration.getUseCache()) { mediaLibrary = new MediaLibrary(); LOGGER.info("A tiny cache admin interface is available at: http://" + server.getHost() + ":" + server.getPort() + "/console/home"); } // XXX: this must be called: // a) *after* loading plugins i.e. plugins register root folders then RootFolder.discoverChildren adds them // b) *after* mediaLibrary is initialized, if enabled (above) getRootFolder(RendererConfiguration.getDefaultConf()); frame.serverReady(); ready = true; // UPNPHelper.sendByeBye(); Runtime.getRuntime().addShutdownHook(new Thread("UMS Shutdown") { @Override public void run() { try { for (ExternalListener l : ExternalFactory.getExternalListeners()) { l.shutdown(); } UPNPHelper.shutDownListener(); UPNPHelper.sendByeBye(); LOGGER.debug("Forcing shutdown of all active processes"); for (Process p : currentProcesses) { try { p.exitValue(); } catch (IllegalThreadStateException ise) { LOGGER.trace("Forcing shutdown of process: " + p); ProcessUtil.destroy(p); } } get().getServer().stop(); Thread.sleep(500); } catch (InterruptedException e) { LOGGER.debug("Caught exception", e); } LOGGER.info("Stopping " + PropertiesUtil.getProjectProperties().get("project.name") + " " + getVersion()); /** * Stopping logging gracefully (flushing logs) * No logging is available after this point */ ILoggerFactory iLoggerContext = LoggerFactory.getILoggerFactory(); if (iLoggerContext instanceof LoggerContext) { ((LoggerContext) iLoggerContext).stop(); } else { LOGGER.error("Unable to shut down logging gracefully"); } } }); configuration.setAutoSave(); UPNPHelper.sendByeBye(); LOGGER.trace("Waiting 250 milliseconds..."); Thread.sleep(250); UPNPHelper.sendAlive(); LOGGER.trace("Waiting 250 milliseconds..."); Thread.sleep(250); UPNPHelper.listen(); return true; } private MediaLibrary mediaLibrary; /** * Returns the MediaLibrary used by PMS. * @return (MediaLibrary) Used mediaLibrary, if any. null if none is in use. */ public MediaLibrary getLibrary() { return mediaLibrary; } private SystemUtils createSystemUtils() { if (Platform.isWindows()) { return new WinUtils(); } else { if (Platform.isMac()) { return new MacSystemUtils(); } else { if (Platform.isSolaris()) { return new SolarisUtils(); } else { return new BasicSystemUtils(); } } } } /** * @deprecated Use {@link #getSharedFoldersArray()} instead. */ @Deprecated public File[] getFoldersConf(boolean log) { return getSharedFoldersArray(false, getConfiguration()); } /** * @deprecated Use {@link #getSharedFoldersArray()} instead. */ @Deprecated public File[] getFoldersConf() { return getSharedFoldersArray(false, getConfiguration()); } /** * Transforms a comma-separated list of directory entries into an array of {@link String}. * Checks that the directory exists and is a valid directory. * * @return {@link java.io.File}[] Array of directories. */ public File[] getSharedFoldersArray(boolean monitored) { return getSharedFoldersArray(monitored, null, getConfiguration()); } public File[] getSharedFoldersArray(boolean monitored, PmsConfiguration configuration) { return getSharedFoldersArray(monitored, null, configuration); } public File[] getSharedFoldersArray(boolean monitored, ArrayList<String> tags, PmsConfiguration configuration) { String folders; if (monitored) { folders = configuration.getFoldersMonitored(); } else { folders = configuration.getFolders(tags); } if (folders == null || folders.length() == 0) { return null; } ArrayList<File> directories = new ArrayList<>(); String[] foldersArray = folders.split(","); for (String folder : foldersArray) { folder = folder.trim(); // unescape embedded commas. note: backslashing isn't safe as it conflicts with // Windows path separators: // http://ps3mediaserver.org/forum/viewtopic.php?f=14&t=8883&start=250#p43520 folder = folder.replaceAll(",", ","); // this is called *way* too often // so log it so we can fix it. LOGGER.info("Checking shared folder: " + folder); File file = new File(folder); if (file.exists()) { if (!file.isDirectory()) { LOGGER.warn( "The file \"{}\" is not a folder! Please remove it from your shared folders list on the \"{}\" tab or in the configuration file.", folder, Messages.getString("LooksFrame.22") ); } } else { LOGGER.warn( "The folder \"{}\" does not exist. Please remove it from your shared folders list on the \"{}\" tab or in the configuration file.", folder, Messages.getString("LooksFrame.22") ); } // add the file even if there are problems so that the user can update the shared folders as required. directories.add(file); } File f[] = new File[directories.size()]; directories.toArray(f); return f; } /** * Restarts the server. The trigger is either a button on the main PMS window or via * an action item. */ // XXX: don't try to optimize this by reusing the same server instance. // see the comment above HTTPServer.stop() public void reset() { TaskRunner.getInstance().submitNamed("restart", true, new Runnable() { @Override public void run() { try { LOGGER.trace("Waiting 1 second..."); UPNPHelper.sendByeBye(); server.stop(); server = null; RendererConfiguration.resetAllRenderers(); try { Thread.sleep(1000); } catch (InterruptedException e) { LOGGER.trace("Caught exception", e); } server = new HTTPServer(configuration.getServerPort()); server.start(); UPNPHelper.sendAlive(); frame.setReloadable(false); } catch (IOException e) { LOGGER.error("error during restart :" +e.getMessage(), e); } } }); } // Cannot remove these methods because of backwards compatibility; // none of the PMS code uses it, but some plugins still do. /** * @deprecated Use the SLF4J logging API instead. * Adds a message to the debug stream, or {@link System#out} in case the * debug stream has not been set up yet. * @param msg {@link String} to be added to the debug stream. */ @Deprecated public static void debug(String msg) { LOGGER.trace(msg); } /** * @deprecated Use the SLF4J logging API instead. * Adds a message to the info stream. * @param msg {@link String} to be added to the info stream. */ @Deprecated public static void info(String msg) { LOGGER.debug(msg); } /** * @deprecated Use the SLF4J logging API instead. * Adds a message to the minimal stream. This stream is also * shown in the Trace tab. * @param msg {@link String} to be added to the minimal stream. */ @Deprecated public static void minimal(String msg) { LOGGER.info(msg); } /** * @deprecated Use the SLF4J logging API instead. * Adds a message to the error stream. This is usually called by * statements that are in a try/catch block. * @param msg {@link String} to be added to the error stream * @param t {@link Throwable} comes from an {@link Exception} */ @Deprecated public static void error(String msg, Throwable t) { LOGGER.error(msg, t); } /** * Creates a new random {@link #uuid}. These are used to uniquely identify the server to renderers (i.e. * renderers treat multiple servers with the same UUID as the same server). * @return {@link String} with an Universally Unique Identifier. */ // XXX don't use the MAC address to seed the UUID as it breaks multiple profiles: // http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&p=75542#p75542 public synchronized String usn() { if (uuid == null) { // Retrieve UUID from configuration uuid = getConfiguration().getUuid(); if (uuid == null) { uuid = UUID.randomUUID().toString(); LOGGER.info("Generated new random UUID: {}", uuid); // save the newly-generated UUID getConfiguration().setUuid(uuid); try { getConfiguration().save(); } catch (ConfigurationException e) { LOGGER.error("Failed to save configuration with new UUID", e); } } LOGGER.info("Using the following UUID configured in UMS.conf: {}", uuid); } return "uuid:" + uuid; } /** * Returns the user friendly name of the UPnP server. * @return {@link String} with the user friendly name. */ public String getServerName() { if (serverName == null) { StringBuilder sb = new StringBuilder(); sb.append(System.getProperty("os.name").replace(" ", "_")); sb.append('-'); sb.append(System.getProperty("os.arch").replace(" ", "_")); sb.append('-'); sb.append(System.getProperty("os.version").replace(" ", "_")); sb.append(", UPnP/1.0 DLNADOC/1.50, UMS/").append(getVersion()); serverName = sb.toString(); } return serverName; } /** * Returns the PMS instance. * @return {@link net.pms.PMS} */ public static PMS get() { // XXX when PMS is run as an application, the instance is initialized via the createInstance call in main(). // However, plugin tests may need access to a PMS instance without going // to the trouble of launching the PMS application, so we provide a fallback // initialization here. Either way, createInstance() should only be called once (see below) if (instance == null) { createInstance(); } return instance; } private synchronized static void createInstance() { assert instance == null; // this should only be called once instance = new PMS(); try { if (instance.init()) { LOGGER.info("{} is now available for renderers to find", PMS.NAME); } else { LOGGER.info("{} initialization was aborted", PMS.NAME); } } catch (Exception e) { LOGGER.error("A serious error occurred during {} initialization: {}", PMS.NAME, e.getMessage()); LOGGER.trace("", e); } } /** * @deprecated Use {@link net.pms.formats.FormatFactory#getAssociatedFormat(String)} * instead. * * @param filename * @return The format. */ @Deprecated public Format getAssociatedFormat(String filename) { return FormatFactory.getAssociatedFormat(filename); } public static void main(String args[]) { boolean displayProfileChooser = false; boolean denyHeadless = false; File profilePath = null; CacheLogger.startCaching(); // Make sure that no other versions of JNA found on the system is used System.setProperty("jna.nosys", "true"); // Set headless options if given as a system property when launching the JVM if (System.getProperty(CONSOLE, "").equalsIgnoreCase(Boolean.toString(true))) { forceHeadless(); } if (System.getProperty(NOCONSOLE, "").equalsIgnoreCase(Boolean.toString(true))) { denyHeadless = true; } if (args.length > 0) { Pattern pattern = Pattern.compile(PROFILE); for (String arg : args) { switch (arg) { case HEADLESS: case CONSOLE: forceHeadless(); break; case NATIVELOOK: System.setProperty(NATIVELOOK, Boolean.toString(true)); break; case SCROLLBARS: System.setProperty(SCROLLBARS, Boolean.toString(true)); break; case NOCONSOLE: denyHeadless = true; break; case PROFILES: displayProfileChooser = true; break; case TRACE: traceMode = 2; break; default: Matcher matcher = pattern.matcher(arg); if (matcher.find()) { profilePath = new File(matcher.group(1)); } break; } } } try { Toolkit.getDefaultToolkit(); } catch (AWTError t) { LOGGER.error("Toolkit error: " + t.getClass().getName() + ": " + t.getMessage()); forceHeadless(); } if (isHeadless() && denyHeadless) { System.err.println( "Either a graphics environment isn't available or headless " + "mode is forced, but \"noconsole\" is specified. " + PMS.NAME + " can't start, exiting." ); System.exit(1); } else if (!isHeadless()) { LooksFrame.initializeLookAndFeel(); } if (profilePath != null) { if (!FileUtil.isValidFileName(profilePath.getName())) { LOGGER.warn("Invalid file name in profile argument - using default profile"); } else if (!profilePath.exists()) { LOGGER.warn("Specified profile ({}) doesn't exist - using default profile", profilePath.getAbsolutePath()); } else { LOGGER.debug("Using specified profile: {}", profilePath.getAbsolutePath()); System.setProperty("ums.profile.path", profilePath.getAbsolutePath()); } } else if (!isHeadless() && displayProfileChooser) { ProfileChooser.display(); } try { setConfiguration(new PmsConfiguration()); assert getConfiguration() != null; /* Rename previous log file to .prev * Log file location is unknown at this point, it's finally decided during loadFile() below * but the file is also truncated at the same time, so we'll have to try a qualified guess * for the file location. */ // Set root level from configuration here so that logging is available during renameOldLogFile(); LoggingConfig.setRootLevel(Level.toLevel(getConfiguration().getRootLogLevel())); renameOldLogFile(); // Load the (optional) LogBack config file. // This has to be called after 'new PmsConfiguration' LoggingConfig.loadFile(); // Check TRACE mode if (traceMode == 2) { LoggingConfig.setRootLevel(Level.TRACE); LOGGER.debug("Forcing debug level to TRACE"); } else { // Remember whether logging level was TRACE/ALL at startup traceMode = LoggingConfig.getRootLevel().toInt() <= Level.TRACE_INT ? 1 : 0; } // Configure syslog unless in forced trace mode if (traceMode != 2 && configuration.getLoggingUseSyslog()) { LoggingConfig.setSyslog(); } // Configure log buffering if (traceMode != 2 && configuration.getLoggingBuffered()) { LoggingConfig.setBuffered(true); } else if (traceMode == 2) { // force unbuffered regardless of logback.xml if in forced trace mode LOGGER.debug("Forcing unbuffered verbose logging"); LoggingConfig.setBuffered(false); LoggingConfig.forceVerboseFileEncoder(); } // Write buffered messages to the log now that logger is configured CacheLogger.stopAndFlush(); LOGGER.debug(new Date().toString()); try { getConfiguration().initCred(); } catch (IOException e) { LOGGER.debug("Error initializing plugin credentials: {}", e); } if (getConfiguration().isRunSingleInstance()) { killOld(); } // Create the PMS instance returned by get() createInstance(); // Calls new() then init() } catch (ConfigurationException t) { String errorMessage = String.format( "Configuration error: %s: %s", t.getClass().getName(), t.getMessage() ); LOGGER.error(errorMessage); if (!isHeadless() && instance != null) { JOptionPane.showMessageDialog( (SwingUtilities.getWindowAncestor((Component) instance.getFrame())), errorMessage, Messages.getString("PMS.42"), JOptionPane.ERROR_MESSAGE ); } } } public HTTPServer getServer() { return server; } public HttpServer getWebServer() { return web == null ? null : web.getServer(); } public void save() { try { configuration.save(); } catch (ConfigurationException e) { LOGGER.error("Could not save configuration", e); } } public void storeFileInCache(File file, int formatType) { if ( getConfiguration().getUseCache() && !getDatabase().isDataExists(file.getAbsolutePath(), file.lastModified()) ) { try { getDatabase().insertOrUpdateData(file.getAbsolutePath(), file.lastModified(), formatType, null); } catch (SQLException e) { LOGGER.error("Database error while trying to store \"{}\" in the cache: {}", file.getName(), e.getMessage()); LOGGER.trace("", e); } } } /** * Retrieves the {@link net.pms.configuration.PmsConfiguration PmsConfiguration} object * that contains all configured settings for PMS. The object provides getters for all * configurable PMS settings. * * @return The configuration object */ public static PmsConfiguration getConfiguration() { return configuration; } /** * Retrieves the composite {@link net.pms.configuration.DeviceConfiguration DeviceConfiguration} object * that applies to this device, which acts as its {@link net.pms.configuration.PmsConfiguration PmsConfiguration}. * * This function should be used to resolve the relevant PmsConfiguration wherever the renderer * is known or can be determined. * * @param renderer The renderer configuration. * @return The DeviceConfiguration object, if any, or the global PmsConfiguration. */ public static PmsConfiguration getConfiguration(RendererConfiguration renderer) { return (renderer != null && (renderer instanceof DeviceConfiguration)) ? (DeviceConfiguration) renderer : configuration; } public static PmsConfiguration getConfiguration(OutputParams params) { return getConfiguration(params != null ? params.mediaRenderer : null); } // Note: this should be used only when no RendererConfiguration or OutputParams is available public static PmsConfiguration getConfiguration(DLNAResource dlna) { return getConfiguration(dlna != null ? dlna.getDefaultRenderer() : null); } /** * Sets the {@link net.pms.configuration.PmsConfiguration PmsConfiguration} object * that contains all configured settings for PMS. The object provides getters for all * configurable PMS settings. * * @param conf The configuration object. */ public static void setConfiguration(PmsConfiguration conf) { configuration = conf; } /** * Returns the project version for PMS. * * @return The project version. */ public static String getVersion() { return PropertiesUtil.getProjectProperties().get("project.version"); } /** * Returns whether the operating system is 64-bit or 32-bit. * * This will work with Windows and OS X but not necessarily with Linux * because when the OS is not Windows we are using Java's os.arch which * only detects the bitness of Java, not of the operating system. * * @return The bitness of the operating system. */ public static int getOSBitness() { int bitness = 32; if ( (System.getProperty("os.name").contains("Windows") && System.getenv("ProgramFiles(x86)") != null) || System.getProperty("os.arch").contains("64") ) { bitness = 64; } return bitness; } /** * Log system properties identifying Java, the OS and encoding and log * warnings where appropriate. */ private void logSystemInfo() { long memoryInMB = Runtime.getRuntime().maxMemory() / 1048576; LOGGER.info("Java: " + System.getProperty("java.vm.name") + " " + System.getProperty("java.version") + " " + System.getProperty("sun.arch.data.model") + "-bit" + " by " + System.getProperty("java.vendor")); LOGGER.info("OS: " + System.getProperty("os.name") + " " + getOSBitness() + "-bit " + System.getProperty("os.version")); LOGGER.info("Encoding: " + System.getProperty("file.encoding")); LOGGER.info("Memory: {} MB", memoryInMB); LOGGER.info("Language: " + WordUtils.capitalize(PMS.getLocale().getDisplayName(Locale.ENGLISH))); LOGGER.info(""); if (Platform.isMac()) { // The binaries shipped with the Mac OS X version of PMS are being // compiled against specific OS versions, making them incompatible // with older versions. Warn the user about this when necessary. String osVersion = System.getProperty("os.version"); // Split takes a regular expression, so escape the dot. String[] versionNumbers = osVersion.split("\\."); if (versionNumbers.length > 1) { try { int osVersionMinor = Integer.parseInt(versionNumbers[1]); if (osVersionMinor < 6) { LOGGER.warn("-----------------------------------------------------------------"); LOGGER.warn("WARNING!"); LOGGER.warn("UMS ships with binaries compiled for Mac OS X 10.6 or higher."); LOGGER.warn("You are running an older version of Mac OS X so UMS may not work!"); LOGGER.warn("More information in the FAQ:"); LOGGER.warn("http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=3507&p=66371#p66371"); LOGGER.warn("-----------------------------------------------------------------"); LOGGER.warn(""); } } catch (NumberFormatException e) { LOGGER.debug("Cannot parse minor os.version number"); } } } } /** * Try to rename old logfile to <filename>.prev */ private static void renameOldLogFile() { String fullLogFileName = configuration.getDefaultLogFilePath(); String newLogFileName = fullLogFileName + ".prev"; try { File logFile = new File(newLogFileName); if (logFile.exists()) { Files.delete(logFile); } logFile = new File(fullLogFileName); if (logFile.exists()) { File newFile = new File(newLogFileName); if (!logFile.renameTo(newFile)) { LOGGER.warn("Could not rename \"{}\" to \"{}\"",fullLogFileName,newLogFileName); } } } catch (Exception e) { LOGGER.warn("Could not rename \"{}\" to \"{}\": {}",fullLogFileName,newLogFileName,e); } } /** * Restart handling */ private static void killOld() { // Note: failure here doesn't necessarily mean we need admin rights, // only that we lack the required permission for these specific items. try { killProc(); } catch (AccessControlException e) { LOGGER.error( "Failed to check for already running instance: " + e.getMessage() + (Platform.isWindows() ? "\nUMS might need to run as an administrator to access the PID file" : "") ); } catch (FileNotFoundException e) { LOGGER.debug("PID file not found, cannot check for running process"); } catch ( IOException e) { LOGGER.error("Error killing old process: " + e); } try { dumpPid(); } catch (FileNotFoundException e) { LOGGER.error( "Failed to write PID file: "+ e.getMessage() + (Platform.isWindows() ? "\nUMS might need to run as an administrator to enforce single instance" : "") ); } catch (IOException e) { LOGGER.error("Error dumping PID " + e); } } /* * This method is only called for Windows OS'es, so specialized Windows charset handling is allowed */ private static boolean verifyPidName(String pid) throws IOException, IllegalAccessException { if (!Platform.isWindows()) { throw new IllegalAccessException("verifyPidName can only be called from Windows!"); } ProcessBuilder pb = new ProcessBuilder("tasklist", "/FI", "\"PID eq " + pid + "\"", "/V", "/NH", "/FO", "CSV"); pb.redirectErrorStream(true); Process p = pb.start(); String line; try (BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream(), "cp" + WinUtils.getOEMCP()))) { try { p.waitFor(); } catch (InterruptedException e) { in.close(); return false; } line = in.readLine(); } if (line == null) { return false; } // remove all " and convert to common case before splitting result on , String[] tmp = line.toLowerCase().replaceAll("\"", "").split(","); // if the line is too short we don't kill the process if (tmp.length < 9) { return false; } // check first and last, update since taskkill changed // also check 2nd last since we migh have ", POSSIBLY UNSTABLE" in there boolean ums = tmp[tmp.length - 1].contains("universal media server") || tmp[tmp.length - 2].contains("universal media server"); return tmp[0].equals("javaw.exe") && ums; } private static String pidFile() { return configuration.getDataFile("pms.pid"); } private static void killProc() throws AccessControlException, IOException{ ProcessBuilder pb = null; String pid; String pidFile = pidFile(); if (!FileUtil.getFilePermissions(pidFile).isReadable()) { throw new AccessControlException("Cannot read " + pidFile); } try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(pidFile), StandardCharsets.US_ASCII))) { pid = in.readLine(); } if (pid == null) { return; } if (Platform.isWindows()) { try { if (verifyPidName(pid)) { pb = new ProcessBuilder("taskkill", "/F", "/PID", pid, "/T"); } } catch (IllegalAccessException e) { // Impossible } } else if (Platform.isFreeBSD() || Platform.isLinux() || Platform.isOpenBSD() || Platform.isSolaris()) { pb = new ProcessBuilder("kill", "-9", pid); } if (pb == null) { return; } try { Process p = pb.start(); p.waitFor(); } catch (InterruptedException e) { LOGGER.trace("Got interrupted while trying to kill process by PID " + e); } } public static long getPID() { String processName = java.lang.management.ManagementFactory.getRuntimeMXBean().getName(); return Long.parseLong(processName.split("@")[0]); } private static void dumpPid() throws IOException { try (FileOutputStream out = new FileOutputStream(pidFile())) { long pid = getPID(); LOGGER.debug("Writing PID: " + pid); String data = String.valueOf(pid) + "\r\n"; out.write(data.getBytes(StandardCharsets.US_ASCII)); out.flush(); } } private DbgPacker dbgPack; public DbgPacker dbgPack() { return dbgPack; } private TempFileMgr tfm; public void addTempFile(File f) { tfm.add(f); } public void addTempFile(File f, int cleanTime) { tfm.add(f, cleanTime); } @Deprecated public void registerPlayer(Player player) { PlayerFactory.registerPlayer(player); } private static ReadWriteLock headlessLock = new ReentrantReadWriteLock(); private static Boolean headless = null; /** * Checks if UMS is running in headless (console) mode, since some Linux * distros seem to not use java.awt.GraphicsEnvironment.isHeadless() properly */ public static boolean isHeadless() { headlessLock.readLock().lock(); try { if (headless != null) { return headless; } } finally { headlessLock.readLock().unlock(); } headlessLock.writeLock().lock(); try { JDialog d = new JDialog(); d.dispose(); headless = false; return headless; } catch (NoClassDefFoundError | HeadlessException | InternalError e) { headless = true; return headless; } finally { headlessLock.writeLock().unlock(); } } /** * Forces UMS to run in headless (console) mode whether a graphics * environment is available or not. */ public static void forceHeadless() { headlessLock.writeLock().lock(); try { headless = true; } finally { headlessLock.writeLock().unlock(); } } private static Locale locale = null; private static ReadWriteLock localeLock = new ReentrantReadWriteLock(); /** * Gets UMS' current {@link Locale} to be used in any {@link Locale} * sensitive operations. If <code>null</code> the default {@link Locale} * is returned. */ public static Locale getLocale() { localeLock.readLock().lock(); try { if (locale != null) { return locale; } else { return Locale.getDefault(); } } finally { localeLock.readLock().unlock(); } } /** * Sets UMS' {@link Locale}. * @param aLocale the {@link Locale} to set */ public static void setLocale(Locale aLocale) { localeLock.writeLock().lock(); try { locale = (Locale) aLocale.clone(); Messages.setLocaleBundle(locale); } finally { localeLock.writeLock().unlock(); } } /** * Sets UMS' {@link Locale} with the same parameters as the * {@link Locale} class constructor. <code>null</code> values are * treated as empty strings. * * @param language An ISO 639 alpha-2 or alpha-3 language code, or a * language subtag up to 8 characters in length. See the * <code>Locale</code> class description about valid language values. * @param country An ISO 3166 alpha-2 country code or a UN M.49 * numeric-3 area code. See the <code>Locale</code> class description * about valid country values. * @param variant Any arbitrary value used to indicate a variation of a * <code>Locale</code>. See the <code>Locale</code> class description * for the details. */ public static void setLocale(String language, String country, String variant) { if (country == null) { country = ""; } if (variant == null) { variant = ""; } localeLock.writeLock().lock(); try { locale = new Locale(language, country, variant); } finally { localeLock.writeLock().unlock(); } } /** * Sets UMS' {@link Locale} with the same parameters as the * {@link Locale} class constructor. <code>null</code> values are * treated as empty strings. * * @param language An ISO 639 alpha-2 or alpha-3 language code, or a * language subtag up to 8 characters in length. See the * <code>Locale</code> class description about valid language values. * @param country An ISO 3166 alpha-2 country code or a UN M.49 * numeric-3 area code. See the <code>Locale</code> class description * about valid country values. */ public static void setLocale(String language, String country) { setLocale(language, country, ""); } /** * Sets UMS' {@link Locale} with the same parameters as the {@link Locale} * class constructor. <code>null</code> values are * treated as empty strings. * * @param language An ISO 639 alpha-2 or alpha-3 language code, or a * language subtag up to 8 characters in length. See the * <code>Locale</code> class description about valid language values. */ public static void setLocale(String language) { setLocale(language, "", ""); } private RemoteWeb web; public RemoteWeb getWebInterface() { return web; } /** * Sets the relative URL of a context sensitive help page located in the * documentation directory. * * @param page The help page. */ public static void setHelpPage(String page) { helpPage = page; } /** * Returns the relative URL of a context sensitive help page in the * documentation directory. * * @return The help page. */ public static String getHelpPage() { return helpPage; } /** * @deprecated Use {@link com.sun.jna.Platform#isWindows()} instead */ @Deprecated public boolean isWindows() { return Platform.isWindows(); } public static boolean filter(RendererConfiguration render, DLNAResource res) { NameFilter nf = instance.filter; if (nf == null || render == null) { return false; } ArrayList<String> tags = render.tags(); if (tags == null) { return false; } for (String tag : tags) { if (nf.filter(tag, res)) { return true; } } return false; } public static boolean isReady() { return get().ready; } public static GlobalIdRepo getGlobalRepo() { return get().globalRepo; } private InfoDb infoDb; private CodeDb codes; private CodeEnter masterCode; public void infoDbAdd(File f, String formattedName) { infoDb.backgroundAdd(f, formattedName); } public InfoDb infoDb() { return infoDb; } public CodeDb codeDb() { return codes; } public void setMasterCode(CodeEnter ce) { masterCode = ce; } public boolean masterCodeValid() { return (masterCode != null && masterCode.validCode(null)); } public static FileWatcher getFileWatcher() { return fileWatcher; } public static class DynamicPlaylist extends Playlist { private long start; private String savePath; public DynamicPlaylist(String name, String dir, int mode) { super(name, null, 0, mode); savePath = dir; start = 0; } @Override public void clear() { super.clear(); start = 0; } @Override public void save() { if (start == 0) { start = System.currentTimeMillis(); } Date d = new Date(start); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH_mm", Locale.US); list.save(new File(savePath, "dynamic_" + sdf.format(d) + ".ups")); } } private DynamicPlaylist dynamicPls; public Playlist getDynamicPls() { if (dynamicPls == null) { dynamicPls = new DynamicPlaylist(Messages.getString("PMS.146"), configuration.getDynamicPlsSavePath(), (configuration.isDynamicPlsAutoSave() ? Playlist.AUTOSAVE : 0) | Playlist.PERMANENT); } return dynamicPls; } private void launchJmDNSRenderers() { if (configuration.useChromecastExt()) { if (RendererConfiguration.getRendererConfigurationByName("Chromecast") != null) { try { startjmDNS(); new ChromecastMgr(jmDNS); } catch (Exception e) { LOGGER.debug("Can't create chromecast mgr"); } } else { LOGGER.info("No Chromecast renderer found. Please enable one and restart."); } } } private void startjmDNS() throws IOException{ if (jmDNS == null) { jmDNS = JmDNS.create(); } } private static int traceMode = 0; /** * Returns current trace mode state * * @return * 0 = Not started in trace mode<br> * 1 = Started in trace mode<br> * 2 = Forced to trace mode */ public static int getTraceMode() { return traceMode; } private CredMgr credMgr; public static CredMgr.Cred getCred(String owner) { return instance.credMgr.getCred(owner); } public static CredMgr.Cred getCred(String owner, String tag) { return instance.credMgr.getCred(owner, tag); } public static String getCredTag(String owner, String username) { return instance.credMgr.getTag(owner, username); } public static boolean verifyCred(String owner, String tag, String user, String pwd) { return instance.credMgr.verify(owner, tag, user, pwd); } private UmsKeysDb keysDb; public static String getKey(String key) { return instance.keysDb.get(key); } public static void setKey(String key, String val) { instance.keysDb.set(key, val); } }