public static String renameForSorting(String filename) { if (PMS.getConfiguration().isPrettifyFilenames()) { // This chunk makes anime sort properly int squareBracketIndex; if (filename.substring(0, 1).matches("\\[")) { filename = filename.replaceAll("_", " "); squareBracketIndex = filename.indexOf(']'); if (squareBracketIndex != -1) { filename = filename.substring(squareBracketIndex + 1); if (filename.substring(0, 1).matches("\\s")) { filename = filename.substring(1); } } } // Replace periods with spaces filename = filename.replaceAll("\\.", " "); } if (PMS.getConfiguration().isIgnoreTheWordThe()) { // Remove "The" from the beginning of files filename = filename.replaceAll("^(?i)The[ .]", ""); } return filename; }
public static void addURLResolver(URLResolver res) { if (urlResolvers.contains(res)) { return; } if (urlResolvers.isEmpty()) { urlResolvers.add(res); return; } String[] tmp = PMS.getConfiguration().getURLResolveOrder(); if (tmp.length == 0) { // no order at all, just add it urlResolvers.add(res); return; } int id = -1; for (int i = 0; i < tmp.length; i++) { if (tmp[i].equalsIgnoreCase(res.name())) { id = i; break; } } if (id == -1) { // no order here, just add it urlResolvers.add(res); return; } if (id > urlResolvers.size()) { // add it last urlResolvers.add(res); return; } urlResolvers.add(id, res); }
@Override public JComponent config() { FormLayout layout = new FormLayout("left:pref, 3dlu, p, 3dlu, 0:grow", "p, 3dlu, p, 3dlu, 0:grow"); PanelBuilder builder = new PanelBuilder(layout); builder.setBorder(Borders.EMPTY_BORDER); builder.setOpaque(false); CellConstraints cc = new CellConstraints(); JComponent cmp = builder.addSeparator(Messages.getString("NetworkTab.5"), cc.xyw(1, 1, 5)); cmp = (JComponent) cmp.getComponent(0); cmp.setFont(cmp.getFont().deriveFont(Font.BOLD)); builder.addLabel(Messages.getString("FFMpegDVRMSRemux.0"), cc.xy(1, 3)); altffpath = new JTextField(PMS.getConfiguration().getFfmpegAlternativePath()); altffpath.addKeyListener( new KeyListener() { @Override public void keyPressed(KeyEvent e) {} @Override public void keyTyped(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) { PMS.getConfiguration().setFfmpegAlternativePath(altffpath.getText()); } }); builder.add(altffpath, cc.xyw(3, 3, 3)); return builder.getPanel(); }
/** * This method populates the supplied {@link OutputParams} object with the correct audio track * (aid) based on the MediaInfo metadata and PMS configuration settings. * * @param media The MediaInfo metadata for the file. * @param params The parameters to populate. */ public static void setAudioOutputParameters(DLNAMediaInfo media, OutputParams params) { PmsConfiguration configuration = PMS.getConfiguration(params); if (params.aid == null && media != null && media.getFirstAudioTrack() != null) { // check for preferred audio DLNAMediaAudio dtsTrack = null; StringTokenizer st = new StringTokenizer(configuration.getAudioLanguages(), ","); while (st.hasMoreTokens()) { String lang = st.nextToken().trim(); LOGGER.trace("Looking for an audio track with lang: " + lang); for (DLNAMediaAudio audio : media.getAudioTracksList()) { if (audio.matchCode(lang)) { params.aid = audio; LOGGER.trace("Matched audio track: " + audio); return; } if (dtsTrack == null && audio.isDTS()) { dtsTrack = audio; } } } // preferred audio not found, take a default audio track, dts first if available if (dtsTrack != null) { params.aid = dtsTrack; LOGGER.trace("Found priority audio track with DTS: " + dtsTrack); } else { params.aid = media.getAudioTracksList().get(0); LOGGER.trace("Chose a default audio track: " + params.aid); } } }
/** * Creates an InputStream based on a URL. This is used while accessing external resources like * online radio stations. * * @param u URL. * @param saveOnDisk If true, the file is first downloaded to the temporary folder. * @return InputStream that can be used for sending to the media renderer. * @throws IOException * @see #downloadAndSendBinary(String) */ protected static InputStream downloadAndSend(String u, boolean saveOnDisk) throws IOException { URL url = new URL(u); File f = null; if (saveOnDisk) { String host = url.getHost(); String hostName = convertURLToFileName(host); String fileName = url.getFile(); fileName = convertURLToFileName(fileName); File hostDir = new File(PMS.getConfiguration().getTempFolder(), hostName); if (!hostDir.isDirectory()) { if (!hostDir.mkdir()) { LOGGER.debug("Cannot create directory: {}", hostDir.getAbsolutePath()); } } f = new File(hostDir, fileName); if (f.exists()) { return new FileInputStream(f); } } byte[] content = downloadAndSendBinary(u, saveOnDisk, f); return new ByteArrayInputStream(content); }
public static boolean isSubtitlesExists(File file, DLNAMediaInfo media, boolean usecache) { boolean found = false; if (file.exists()) { found = browseFolderForSubtitles(file.getParentFile(), file, media, usecache); } String alternate = PMS.getConfiguration().getAlternateSubtitlesFolder(); if (isNotBlank(alternate)) { // https://code.google.com/p/ps3mediaserver/issues/detail?id=737#c5 File subFolder = new File(alternate); if (!subFolder.isAbsolute()) { subFolder = new File(file.getParent() + "/" + alternate); try { subFolder = subFolder.getCanonicalFile(); } catch (IOException e) { LOGGER.debug("Caught exception", e); } } if (subFolder.exists()) { found = found || browseFolderForSubtitles(subFolder, file, media, usecache); } } return found; }
@Override public ProcessWrapper launchTranscode( String fileName, DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException { params.maxBufferSize = PMS.getConfiguration().getMaxAudioBuffer(); params.waitbeforestart = 2000; params.manageFastStart(); String args[] = args(); if (params.mediaRenderer.isTranscodeToMP3()) { args = new String[] {"-f", "mp3", "-ar", "48000", "-ab", "320000"}; } if (params.mediaRenderer.isTranscodeToWAV()) { args = new String[] {"-f", "wav", "-ar", "48000"}; } if (params.mediaRenderer.isTranscodeAudioTo441()) { args[3] = "44100"; } if (!configuration.isAudioResample()) { args[2] = "-vn"; args[3] = "-vn"; } if (params.mediaRenderer.isTranscodeAudioTo441()) { args[3] = "44100"; } return getFFMpegTranscode(fileName, dlna, media, params, args); }
public static String getMsgString(String key, HttpExchange t) { if (PMS.getConfiguration().useWebLang()) { String lang = getFirstSupportedLanguage(t); if (!lang.isEmpty()) { return Messages.getString(key, Locale.forLanguageTag(lang)); } } return Messages.getString(key); }
public static boolean downloadText(InputStream in, File f) throws Exception { PmsConfiguration configuration = PMS.getConfiguration(); // String subtitleQuality = config.getMencoderVobsubSubtitleQuality(); String subcp = configuration.getMencoderSubCp(); OutputStreamWriter out = new OutputStreamWriter(new FileOutputStream(f), subcp); InputStreamReader inn = new InputStreamReader(in); char[] buf = new char[4096]; int len; while ((len = inn.read(buf)) != -1) out.write(buf, 0, len); out.flush(); out.close(); in.close(); return true; }
public void checkUnicode() { if (file != null && file.exists() && file.length() > 3) { FileInputStream fis = null; try { int is_file_unicode = 0; fis = new FileInputStream(file); int b1 = fis.read(); int b2 = fis.read(); int b3 = fis.read(); if (b1 == 255 && b2 == 254) { is_file_unicode = 1; } else if (b1 == 254 && b2 == 255) { is_file_unicode = 2; } else if (b1 == 239 && b2 == 187 && b3 == 191) { is_file_utf8 = true; } // MPlayer doesn't handle UTF-16 encoded subs if (is_file_unicode > 0) { is_file_utf8 = true; utf8_file = new File(PMS.getConfiguration().getTempFolder(), "utf8_" + file.getName()); if (!utf8_file.exists()) { InputStreamReader r = new InputStreamReader( new FileInputStream(file), is_file_unicode == 1 ? "UTF-16" : "UTF-16BE"); OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(utf8_file), "UTF-8"); int c; while ((c = r.read()) != -1) { osw.write(c); } osw.close(); r.close(); } } } catch (IOException e) { LOGGER.error(null, e); } finally { if (fis != null) { try { fis.close(); } catch (IOException e) { LOGGER.debug("Caught exception", e); } } } } }
protected ProcessWrapperImpl getFFMpegTranscode( String fileName, DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException { PmsConfiguration configuration = PMS.getConfiguration(); String ffmpegAlternativePath = configuration.getFfmpegAlternativePath(); List<String> cmdList = new ArrayList<String>(); if (ffmpegAlternativePath != null && ffmpegAlternativePath.length() > 0) { cmdList.add(ffmpegAlternativePath); } else { cmdList.add(executable()); } if (params.timeseek > 0) { cmdList.add("-ss"); cmdList.add("" + params.timeseek); } cmdList.add("-i"); cmdList.add(fileName); for (String arg : args()) { cmdList.add(arg); } String[] ffmpegSettings = StringUtils.split(configuration.getFfmpegSettings()); if (ffmpegSettings != null) { for (String option : ffmpegSettings) { cmdList.add(option); } } cmdList.add("pipe:"); String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); cmdArray = finalizeTranscoderArgs(this, fileName, dlna, media, params, cmdArray); ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params); pw.runInNewThread(); return pw; }
private static String addSubs(String rUrl, String sub) { rUrl = append(rUrl, "&subs=", escape(sub)); // -spuaa 3 -subcp ISO-8859-10 -subfont C:\Windows\Fonts\Arial.ttf -subfont-text-scale 2 // -subfont-outline 1 -subfont-blur 1 -subpos 90 -quiet -quiet -sid 100 -fps 25 -ofps 25 -sub // C:\downloads\Kings Speech.srt -lavdopts fast -mc 0 -noskip -af lavcresample=48000 -srate // 48000 -o \\.\pipe\mencoder1299956406082 PmsConfiguration configuration = PMS.getConfiguration(); // String subtitleQuality = config.getMencoderVobsubSubtitleQuality(); String subcp = configuration.getMencoderSubCp(); rUrl = append(rUrl, "&subcp=", escape(subcp)); rUrl = append(rUrl, "&subtext=", escape(configuration.getMencoderNoAssScale())); rUrl = append(rUrl, "&subout=", escape(configuration.getMencoderNoAssOutline())); rUrl = append(rUrl, "&subblur=", escape(configuration.getMencoderNoAssBlur())); int subpos = 1; try { subpos = Integer.parseInt(configuration.getMencoderNoAssSubPos()); } catch (NumberFormatException n) { } rUrl = append(rUrl, "&subpos=", String.valueOf(100 - subpos)); // rUrl=append(rUrl,"&subdelay=","20000"); return rUrl; }
public static String servePage(String resource) { StringBuilder sb = new StringBuilder(); sb.append( "<html><head><title>" + PropertiesUtil.getProjectProperties().get("project.name") + " HTML Console</title></head><body>"); DLNAMediaDatabase database = PMS.get().getDatabase(); PmsConfiguration configuration = PMS.getConfiguration(); if (resource.equals("compact") && configuration.getUseCache()) { database.compact(); sb.append("<p align=center><b>Database compacted!</b></p><br>"); } if (resource.equals("scan") && configuration.getUseCache()) { if (!database.isScanLibraryRunning()) { database.scanLibrary(); } if (database.isScanLibraryRunning()) { sb.append( "<p align=center><b>Scan in progress! you can also <a href=\"stop\">stop it</a></b></p><br>"); } } if (resource.equals("stop") && configuration.getUseCache() && database.isScanLibraryRunning()) { sb.append("<p align=center><b>Scan stopped!</b></p><br>"); } sb.append( "<p align=center><img src='/images/thumbnail-256.png'><br>" + PropertiesUtil.getProjectProperties().get("project.name") + " HTML console<br><br>Menu:<br>"); sb.append("<a href=\"home\">Home</a><br>"); sb.append("<a href=\"scan\">Scan folders</a><br>"); sb.append("<a href=\"compact\">Shrink cache database (not recommended)</a>"); sb.append("</p></body></html>"); return sb.toString(); }
private static int getHW(int cfgVal, int id, int def) { if (cfgVal != 0) { // if we have a value cfg return that return cfgVal; } String s = PMS.getConfiguration().getWebSize(); if (StringUtils.isEmpty(s)) { // no size string return default return def; } String[] tmp = s.split("x", 2); if (tmp.length < 2) { // bad format resort to default return def; } try { // pick whatever we got return Integer.parseInt(tmp[id]); } catch (NumberFormatException e) { // bad format (again) resort to default return def; } }
/** * This class provides methods for creating and maintaining the database where media information is * stored. Scanning media and interpreting the data is intensive, so the database is used to cache * scanned information to be reused later. */ public class DLNAMediaDatabase implements Runnable { private static final Logger LOGGER = LoggerFactory.getLogger(DLNAMediaDatabase.class); private static final PmsConfiguration configuration = PMS.getConfiguration(); private String url; private String dbDir; private String dbName; public static final String NONAME = "###"; private Thread scanner; private JdbcConnectionPool cp; private int dbCount; /** * The database version should be incremented when we change anything to do with the database * since the last released version. */ private final String latestVersion = "7"; // Database column sizes private final int SIZE_CODECV = 32; private final int SIZE_FRAMERATE = 32; private final int SIZE_ASPECTRATIO_DVDISO = 32; private final int SIZE_ASPECTRATIO_CONTAINER = 5; private final int SIZE_ASPECTRATIO_VIDEOTRACK = 5; private final int SIZE_AVC_LEVEL = 3; private final int SIZE_CONTAINER = 32; private final int SIZE_MATRIX_COEFFICIENTS = 16; private final int SIZE_MODEL = 128; private final int SIZE_MUXINGMODE = 32; private final int SIZE_FRAMERATE_MODE = 16; private final int SIZE_STEREOSCOPY = 255; private final int SIZE_LANG = 3; private final int SIZE_TITLE = 255; private final int SIZE_SAMPLEFREQ = 16; private final int SIZE_CODECA = 32; private final int SIZE_ALBUM = 255; private final int SIZE_ARTIST = 255; private final int SIZE_SONGNAME = 255; private final int SIZE_GENRE = 64; public DLNAMediaDatabase(String name) { dbName = name; File profileDirectory = new File(configuration.getProfileDirectory()); dbDir = new File( profileDirectory.isDirectory() ? configuration.getProfileDirectory() : null, "database") .getAbsolutePath(); url = Constants.START_URL + dbDir + File.separator + dbName; LOGGER.debug("Using database URL: " + url); LOGGER.info("Using database located at: " + dbDir); try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException e) { LOGGER.error(null, e); } JdbcDataSource ds = new JdbcDataSource(); ds.setURL(url); ds.setUser("sa"); ds.setPassword(""); cp = JdbcConnectionPool.create(ds); } /** * Gets the name of the database file * * @return The filename */ public String getDatabaseFilename() { if (dbName == null || dbDir == null) { return null; } else { return dbDir + File.separator + dbName; } } /** * Gets a new connection from the connection pool if one is available. If not waits for a free * slot until timeout.<br> * <br> * <strong>Important: Every connection must be closed after use</strong> * * @return the new connection * @throws SQLException */ public Connection getConnection() throws SQLException { return cp.getConnection(); } public void init(boolean force) { dbCount = -1; String version = null; Connection conn = null; ResultSet rs = null; Statement stmt = null; try { conn = getConnection(); } catch (SQLException se) { final File dbFile = new File(dbDir + File.separator + dbName + ".data.db"); final File dbDirectory = new File(dbDir); if (dbFile.exists() || (se.getErrorCode() == 90048)) { // Cache is corrupt or a wrong version, so delete it FileUtils.deleteQuietly(dbDirectory); if (!dbDirectory.exists()) { LOGGER.info( "The database has been deleted because it was corrupt or had the wrong version"); } else { if (!net.pms.PMS.isHeadless()) { JOptionPane.showMessageDialog( SwingUtilities.getWindowAncestor((Component) PMS.get().getFrame()), String.format(Messages.getString("DLNAMediaDatabase.5"), dbDir), Messages.getString("Dialog.Error"), JOptionPane.ERROR_MESSAGE); } LOGGER.error( "Damaged cache can't be deleted. Stop the program and delete the folder \"" + dbDir + "\" manually"); PMS.get().getRootFolder(null).stopScan(); configuration.setUseCache(false); return; } } else { LOGGER.error("Database connection error: " + se.getMessage()); LOGGER.trace("", se); RootFolder rootFolder = PMS.get().getRootFolder(null); if (rootFolder != null) { rootFolder.stopScan(); } configuration.setUseCache(false); return; } } finally { close(conn); } try { conn = getConnection(); stmt = conn.createStatement(); rs = stmt.executeQuery("SELECT count(*) FROM FILES"); if (rs.next()) { dbCount = rs.getInt(1); } rs.close(); stmt.close(); stmt = conn.createStatement(); rs = stmt.executeQuery("SELECT VALUE FROM METADATA WHERE KEY = 'VERSION'"); if (rs.next()) { version = rs.getString(1); } } catch (SQLException se) { if (se.getErrorCode() != 42102) { // Don't log exception "Table "FILES" not found" which will be corrected in // following step LOGGER.error(null, se); } } finally { close(rs); close(stmt); close(conn); } // Recreate database if it is not the latest version. boolean force_reinit = !latestVersion.equals(version); if (force || dbCount == -1 || force_reinit) { LOGGER.debug("Database will be (re)initialized"); try { conn = getConnection(); executeUpdate(conn, "DROP TABLE FILES"); executeUpdate(conn, "DROP TABLE METADATA"); executeUpdate(conn, "DROP TABLE REGEXP_RULES"); executeUpdate(conn, "DROP TABLE AUDIOTRACKS"); executeUpdate(conn, "DROP TABLE SUBTRACKS"); } catch (SQLException se) { if (se.getErrorCode() != 42102) { // Don't log exception "Table "FILES" not found" which will be corrected in // following step LOGGER.error(null, se); } } try { StringBuilder sb = new StringBuilder(); sb.append("CREATE TABLE FILES ("); sb.append(" ID INT AUTO_INCREMENT"); sb.append(", FILENAME VARCHAR2(1024) NOT NULL"); sb.append(", MODIFIED TIMESTAMP NOT NULL"); sb.append(", TYPE INT"); sb.append(", DURATION DOUBLE"); sb.append(", BITRATE INT"); sb.append(", WIDTH INT"); sb.append(", HEIGHT INT"); sb.append(", SIZE NUMERIC"); sb.append(", CODECV VARCHAR2(").append(SIZE_CODECV).append(')'); sb.append(", FRAMERATE VARCHAR2(").append(SIZE_FRAMERATE).append(')'); sb.append(", ASPECT VARCHAR2(") .append(SIZE_ASPECTRATIO_DVDISO) .append(')'); sb.append(", ASPECTRATIOCONTAINER VARCHAR2(") .append(SIZE_ASPECTRATIO_CONTAINER) .append(')'); sb.append(", ASPECTRATIOVIDEOTRACK VARCHAR2(") .append(SIZE_ASPECTRATIO_VIDEOTRACK) .append(')'); sb.append(", REFRAMES TINYINT"); sb.append(", AVCLEVEL VARCHAR2(").append(SIZE_AVC_LEVEL).append(')'); sb.append(", BITSPERPIXEL INT"); sb.append(", THUMB BINARY"); sb.append(", CONTAINER VARCHAR2(").append(SIZE_CONTAINER).append(')'); sb.append(", MODEL VARCHAR2(").append(SIZE_MODEL).append(')'); sb.append(", EXPOSURE INT"); sb.append(", ORIENTATION INT"); sb.append(", ISO INT"); sb.append(", MUXINGMODE VARCHAR2(").append(SIZE_MUXINGMODE).append(')'); sb.append(", FRAMERATEMODE VARCHAR2(").append(SIZE_FRAMERATE_MODE).append(')'); sb.append(", STEREOSCOPY VARCHAR2(").append(SIZE_STEREOSCOPY).append(')'); sb.append(", MATRIXCOEFFICIENTS VARCHAR2(") .append(SIZE_MATRIX_COEFFICIENTS) .append(')'); sb.append(", TITLECONTAINER VARCHAR2(").append(SIZE_TITLE).append(')'); sb.append(", TITLEVIDEOTRACK VARCHAR2(").append(SIZE_TITLE).append(')'); sb.append(", VIDEOTRACKCOUNT INT"); sb.append(", IMAGECOUNT INT"); sb.append(", BITDEPTH INT"); sb.append(", constraint PK1 primary key (FILENAME, MODIFIED, ID))"); executeUpdate(conn, sb.toString()); sb = new StringBuilder(); sb.append("CREATE TABLE AUDIOTRACKS ("); sb.append(" FILEID INT NOT NULL"); sb.append(", ID INT NOT NULL"); sb.append(", LANG VARCHAR2(").append(SIZE_LANG).append(')'); sb.append(", TITLE VARCHAR2(").append(SIZE_TITLE).append(')'); sb.append(", NRAUDIOCHANNELS NUMERIC"); sb.append(", SAMPLEFREQ VARCHAR2(").append(SIZE_SAMPLEFREQ).append(')'); sb.append(", CODECA VARCHAR2(").append(SIZE_CODECA).append(')'); sb.append(", BITSPERSAMPLE INT"); sb.append(", ALBUM VARCHAR2(").append(SIZE_ALBUM).append(')'); sb.append(", ARTIST VARCHAR2(").append(SIZE_ARTIST).append(')'); sb.append(", SONGNAME VARCHAR2(").append(SIZE_SONGNAME).append(')'); sb.append(", GENRE VARCHAR2(").append(SIZE_GENRE).append(')'); sb.append(", YEAR INT"); sb.append(", TRACK INT"); sb.append(", DELAY INT"); sb.append(", MUXINGMODE VARCHAR2(").append(SIZE_MUXINGMODE).append(')'); sb.append(", BITRATE INT"); sb.append(", constraint PKAUDIO primary key (FILEID, ID))"); executeUpdate(conn, sb.toString()); sb = new StringBuilder(); sb.append("CREATE TABLE SUBTRACKS ("); sb.append(" FILEID INT NOT NULL"); sb.append(", ID INT NOT NULL"); sb.append(", LANG VARCHAR2(").append(SIZE_LANG).append(')'); sb.append(", TITLE VARCHAR2(").append(SIZE_TITLE).append(')'); sb.append(", TYPE INT"); sb.append(", constraint PKSUB primary key (FILEID, ID))"); executeUpdate(conn, sb.toString()); executeUpdate( conn, "CREATE TABLE METADATA (KEY VARCHAR2(255) NOT NULL, VALUE VARCHAR2(255) NOT NULL)"); executeUpdate(conn, "INSERT INTO METADATA VALUES ('VERSION', '" + latestVersion + "')"); executeUpdate(conn, "CREATE INDEX IDXARTIST on AUDIOTRACKS (ARTIST asc);"); executeUpdate(conn, "CREATE INDEX IDXALBUM on AUDIOTRACKS (ALBUM asc);"); executeUpdate(conn, "CREATE INDEX IDXGENRE on AUDIOTRACKS (GENRE asc);"); executeUpdate(conn, "CREATE INDEX IDXYEAR on AUDIOTRACKS (YEAR asc);"); executeUpdate( conn, "CREATE TABLE REGEXP_RULES ( ID VARCHAR2(255) PRIMARY KEY, RULE VARCHAR2(255), ORDR NUMERIC);"); executeUpdate(conn, "INSERT INTO REGEXP_RULES VALUES ( '###', '(?i)^\\W.+', 0 );"); executeUpdate(conn, "INSERT INTO REGEXP_RULES VALUES ( '0-9', '(?i)^\\d.+', 1 );"); // Retrieve the alphabet property value and split it String[] chars = Messages.getString("DLNAMediaDatabase.1").split(","); for (int i = 0; i < chars.length; i++) { // Create regexp rules for characters with a sort order based on the property value executeUpdate( conn, "INSERT INTO REGEXP_RULES VALUES ( '" + chars[i] + "', '(?i)^" + chars[i] + ".+', " + (i + 2) + " );"); } LOGGER.debug("Database initialized"); } catch (SQLException se) { LOGGER.info("Error in table creation: " + se.getMessage()); } finally { close(conn); } } else { LOGGER.debug("Database file count: " + dbCount); LOGGER.debug("Database version: " + latestVersion); } } private void executeUpdate(Connection conn, String sql) throws SQLException { if (conn != null) { try (Statement stmt = conn.createStatement()) { stmt.executeUpdate(sql); } } } public boolean isDataExists(String name, long modified) { boolean found = false; Connection conn = null; ResultSet rs = null; PreparedStatement stmt = null; try { conn = getConnection(); stmt = conn.prepareStatement("SELECT * FROM FILES WHERE FILENAME = ? AND MODIFIED = ?"); stmt.setString(1, name); stmt.setTimestamp(2, new Timestamp(modified)); rs = stmt.executeQuery(); while (rs.next()) { found = true; } } catch (SQLException se) { LOGGER.error(null, se); return false; } finally { close(rs); close(stmt); close(conn); } return found; } public ArrayList<DLNAMediaInfo> getData(String name, long modified) { ArrayList<DLNAMediaInfo> list = new ArrayList<>(); Connection conn = null; ResultSet rs = null; PreparedStatement stmt = null; try { conn = getConnection(); stmt = conn.prepareStatement("SELECT * FROM FILES WHERE FILENAME = ? AND MODIFIED = ?"); stmt.setString(1, name); stmt.setTimestamp(2, new Timestamp(modified)); rs = stmt.executeQuery(); while (rs.next()) { DLNAMediaInfo media = new DLNAMediaInfo(); int id = rs.getInt("ID"); media.setDuration(toDouble(rs, "DURATION")); media.setBitrate(rs.getInt("BITRATE")); media.setWidth(rs.getInt("WIDTH")); media.setHeight(rs.getInt("HEIGHT")); media.setSize(rs.getLong("SIZE")); media.setCodecV(rs.getString("CODECV")); media.setFrameRate(rs.getString("FRAMERATE")); media.setAspectRatioDvdIso(rs.getString("ASPECT")); media.setAspectRatioContainer(rs.getString("ASPECTRATIOCONTAINER")); media.setAspectRatioVideoTrack(rs.getString("ASPECTRATIOVIDEOTRACK")); media.setReferenceFrameCount(rs.getByte("REFRAMES")); media.setAvcLevel(rs.getString("AVCLEVEL")); media.setBitsPerPixel(rs.getInt("BITSPERPIXEL")); media.setThumb(rs.getBytes("THUMB")); media.setContainer(rs.getString("CONTAINER")); media.setModel(rs.getString("MODEL")); if (media.getModel() != null && !FormatConfiguration.JPG.equals(media.getContainer())) { media.setExtrasAsString(media.getModel()); } media.setExposure(rs.getInt("EXPOSURE")); media.setOrientation(rs.getInt("ORIENTATION")); media.setIso(rs.getInt("ISO")); media.setMuxingMode(rs.getString("MUXINGMODE")); media.setFrameRateMode(rs.getString("FRAMERATEMODE")); media.setStereoscopy(rs.getString("STEREOSCOPY")); media.setMatrixCoefficients(rs.getString("MATRIXCOEFFICIENTS")); media.setFileTitleFromMetadata(rs.getString("TITLECONTAINER")); media.setVideoTrackTitleFromMetadata(rs.getString("TITLEVIDEOTRACK")); media.setVideoTrackCount(rs.getInt("VIDEOTRACKCOUNT")); media.setImageCount(rs.getInt("IMAGECOUNT")); media.setVideoBitDepth(rs.getInt("BITDEPTH")); media.setMediaparsed(true); ResultSet subrs; try (PreparedStatement audios = conn.prepareStatement("SELECT * FROM AUDIOTRACKS WHERE FILEID = ?")) { audios.setInt(1, id); subrs = audios.executeQuery(); while (subrs.next()) { DLNAMediaAudio audio = new DLNAMediaAudio(); audio.setId(subrs.getInt("ID")); audio.setLang(subrs.getString("LANG")); audio.setAudioTrackTitleFromMetadata(subrs.getString("TITLE")); audio.getAudioProperties().setNumberOfChannels(subrs.getInt("NRAUDIOCHANNELS")); audio.setSampleFrequency(subrs.getString("SAMPLEFREQ")); audio.setCodecA(subrs.getString("CODECA")); audio.setBitsperSample(subrs.getInt("BITSPERSAMPLE")); audio.setAlbum(subrs.getString("ALBUM")); audio.setArtist(subrs.getString("ARTIST")); audio.setSongname(subrs.getString("SONGNAME")); audio.setGenre(subrs.getString("GENRE")); audio.setYear(subrs.getInt("YEAR")); audio.setTrack(subrs.getInt("TRACK")); audio.getAudioProperties().setAudioDelay(subrs.getInt("DELAY")); audio.setMuxingModeAudio(subrs.getString("MUXINGMODE")); audio.setBitRate(subrs.getInt("BITRATE")); media.getAudioTracksList().add(audio); } subrs.close(); } try (PreparedStatement subs = conn.prepareStatement("SELECT * FROM SUBTRACKS WHERE FILEID = ?")) { subs.setInt(1, id); subrs = subs.executeQuery(); while (subrs.next()) { DLNAMediaSubtitle sub = new DLNAMediaSubtitle(); sub.setId(subrs.getInt("ID")); sub.setLang(subrs.getString("LANG")); sub.setSubtitlesTrackTitleFromMetadata(subrs.getString("TITLE")); sub.setType(SubtitleType.valueOfStableIndex(subrs.getInt("TYPE"))); media.getSubtitleTracksList().add(sub); } subrs.close(); } list.add(media); } } catch (SQLException se) { LOGGER.error(null, se); return null; } finally { close(rs); close(stmt); close(conn); } return list; } private Double toDouble(ResultSet rs, String column) throws SQLException { Object obj = rs.getObject(column); if (obj instanceof Double) { return (Double) obj; } return null; } public synchronized void insertData(String name, long modified, int type, DLNAMediaInfo media) { Connection conn = null; PreparedStatement ps = null; try { conn = getConnection(); ps = conn.prepareStatement( "INSERT INTO FILES(FILENAME, MODIFIED, TYPE, DURATION, BITRATE, WIDTH, HEIGHT, SIZE, CODECV, " + "FRAMERATE, ASPECT, ASPECTRATIOCONTAINER, ASPECTRATIOVIDEOTRACK, REFRAMES, AVCLEVEL, BITSPERPIXEL, " + "THUMB, CONTAINER, MODEL, EXPOSURE, ORIENTATION, ISO, MUXINGMODE, FRAMERATEMODE, STEREOSCOPY, " + "MATRIXCOEFFICIENTS, TITLECONTAINER, TITLEVIDEOTRACK, VIDEOTRACKCOUNT, IMAGECOUNT, BITDEPTH) VALUES " + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); ps.setString(1, name); ps.setTimestamp(2, new Timestamp(modified)); ps.setInt(3, type); if (media != null) { if (media.getDuration() != null) { ps.setDouble(4, media.getDurationInSeconds()); } else { ps.setNull(4, Types.DOUBLE); } int databaseBitrate = 0; if (type != Format.IMAGE) { databaseBitrate = media.getBitrate(); if (databaseBitrate == 0) { LOGGER.debug("Could not parse the bitrate from: " + name); } } ps.setInt(5, databaseBitrate); ps.setInt(6, media.getWidth()); ps.setInt(7, media.getHeight()); ps.setLong(8, media.getSize()); ps.setString(9, left(media.getCodecV(), SIZE_CODECV)); ps.setString(10, left(media.getFrameRate(), SIZE_FRAMERATE)); ps.setString(11, left(media.getAspectRatioDvdIso(), SIZE_ASPECTRATIO_DVDISO)); ps.setString(12, left(media.getAspectRatioContainer(), SIZE_ASPECTRATIO_CONTAINER)); ps.setString(13, left(media.getAspectRatioVideoTrack(), SIZE_ASPECTRATIO_VIDEOTRACK)); ps.setByte(14, media.getReferenceFrameCount()); ps.setString(15, left(media.getAvcLevel(), SIZE_AVC_LEVEL)); ps.setInt(16, media.getBitsPerPixel()); ps.setBytes(17, media.getThumb()); ps.setString(18, left(media.getContainer(), SIZE_CONTAINER)); if (media.getExtras() != null) { ps.setString(19, left(media.getExtrasAsString(), SIZE_MODEL)); } else { ps.setString(19, left(media.getModel(), SIZE_MODEL)); } ps.setInt(20, media.getExposure()); ps.setInt(21, media.getOrientation()); ps.setInt(22, media.getIso()); ps.setString(23, left(media.getMuxingModeAudio(), SIZE_MUXINGMODE)); ps.setString(24, left(media.getFrameRateMode(), SIZE_FRAMERATE_MODE)); ps.setString(25, left(media.getStereoscopy(), SIZE_STEREOSCOPY)); ps.setString(26, left(media.getMatrixCoefficients(), SIZE_MATRIX_COEFFICIENTS)); ps.setString(27, left(media.getFileTitleFromMetadata(), SIZE_TITLE)); ps.setString(28, left(media.getVideoTrackTitleFromMetadata(), SIZE_TITLE)); ps.setInt(29, media.getVideoTrackCount()); ps.setInt(30, media.getImageCount()); ps.setInt(31, media.getVideoBitDepth()); } else { ps.setString(4, null); ps.setInt(5, 0); ps.setInt(6, 0); ps.setInt(7, 0); ps.setLong(8, 0); ps.setString(9, null); ps.setString(10, null); ps.setString(11, null); ps.setString(12, null); ps.setString(13, null); ps.setByte(14, (byte) -1); ps.setString(15, null); ps.setInt(16, 0); ps.setBytes(17, null); ps.setString(18, null); ps.setString(19, null); ps.setInt(20, 0); ps.setInt(21, 0); ps.setInt(22, 0); ps.setString(23, null); ps.setString(24, null); ps.setString(25, null); ps.setString(26, null); ps.setString(27, null); ps.setString(28, null); ps.setInt(29, 0); ps.setInt(30, 0); ps.setInt(31, 0); } ps.executeUpdate(); int id; try (ResultSet rs = ps.getGeneratedKeys()) { id = -1; while (rs.next()) { id = rs.getInt(1); } } if (media != null && id > -1) { PreparedStatement insert = null; if (media.getAudioTracksList().size() > 0) { insert = conn.prepareStatement( "INSERT INTO AUDIOTRACKS VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); for (DLNAMediaAudio audio : media.getAudioTracksList()) { insert.clearParameters(); insert.setInt(1, id); insert.setInt(2, audio.getId()); insert.setString(3, left(audio.getLang(), SIZE_LANG)); insert.setString(4, left(audio.getAudioTrackTitleFromMetadata(), SIZE_TITLE)); insert.setInt(5, audio.getAudioProperties().getNumberOfChannels()); insert.setString(6, left(audio.getSampleFrequency(), SIZE_SAMPLEFREQ)); insert.setString(7, left(audio.getCodecA(), SIZE_CODECA)); insert.setInt(8, audio.getBitsperSample()); insert.setString(9, left(trimToEmpty(audio.getAlbum()), SIZE_ALBUM)); insert.setString(10, left(trimToEmpty(audio.getArtist()), SIZE_ARTIST)); insert.setString(11, left(trimToEmpty(audio.getSongname()), SIZE_SONGNAME)); insert.setString(12, left(trimToEmpty(audio.getGenre()), SIZE_GENRE)); insert.setInt(13, audio.getYear()); insert.setInt(14, audio.getTrack()); insert.setInt(15, audio.getAudioProperties().getAudioDelay()); insert.setString(16, left(trimToEmpty(audio.getMuxingModeAudio()), SIZE_MUXINGMODE)); insert.setInt(17, audio.getBitRate()); try { insert.executeUpdate(); } catch (SQLException e) { if (e.getErrorCode() == 23505) { LOGGER.debug( "A duplicate key error occurred while trying to store the following file's audio information in the database: " + name); } else { LOGGER.debug( "An error occurred while trying to store the following file's audio information in the database: " + name); } LOGGER.debug("The error given by jdbc was: " + e); } } } if (media.getSubtitleTracksList().size() > 0) { insert = conn.prepareStatement("INSERT INTO SUBTRACKS VALUES (?, ?, ?, ?, ?)"); for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) { if (sub.getExternalFile() == null) { // no save of external subtitles insert.clearParameters(); insert.setInt(1, id); insert.setInt(2, sub.getId()); insert.setString(3, left(sub.getLang(), SIZE_LANG)); insert.setString(4, left(sub.getSubtitlesTrackTitleFromMetadata(), SIZE_TITLE)); insert.setInt(5, sub.getType().getStableIndex()); try { insert.executeUpdate(); } catch (SQLException e) { if (e.getErrorCode() == 23505) { LOGGER.debug( "A duplicate key error occurred while trying to store the following file's subtitle information in the database: " + name); } else { LOGGER.debug( "An error occurred while trying to store the following file's subtitle information in the database: " + name); } LOGGER.debug("The error given by jdbc was: " + e); } } } } close(insert); } } catch (SQLException se) { if (se.getErrorCode() == 23505) { LOGGER.debug( "Duplicate key while inserting this entry: " + name + " into the database: " + se.getMessage()); } else { LOGGER.error(null, se); } } finally { close(ps); close(conn); } } public synchronized void updateThumbnail( String name, long modified, int type, DLNAMediaInfo media) { Connection conn = null; PreparedStatement ps = null; try { conn = getConnection(); ps = conn.prepareStatement("UPDATE FILES SET THUMB = ? WHERE FILENAME = ? AND MODIFIED = ?"); ps.setString(2, name); ps.setTimestamp(3, new Timestamp(modified)); if (media != null) { ps.setBytes(1, media.getThumb()); } else { ps.setNull(1, Types.BINARY); } ps.executeUpdate(); } catch (SQLException se) { if (se.getErrorCode() == 23001) { LOGGER.debug( "Duplicate key while inserting this entry: " + name + " into the database: " + se.getMessage()); } else { LOGGER.error(null, se); } } finally { close(ps); close(conn); } } public ArrayList<String> getStrings(String sql) { ArrayList<String> list = new ArrayList<>(); Connection conn = null; ResultSet rs = null; PreparedStatement ps = null; try { conn = getConnection(); ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()) { String str = rs.getString(1); if (isBlank(str)) { if (!list.contains(NONAME)) { list.add(NONAME); } } else if (!list.contains(str)) { list.add(str); } } } catch (SQLException se) { LOGGER.error(null, se); return null; } finally { close(rs); close(ps); close(conn); } return list; } public void cleanup() { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = getConnection(); ps = conn.prepareStatement("SELECT COUNT(*) FROM FILES"); rs = ps.executeQuery(); dbCount = 0; if (rs.next()) { dbCount = rs.getInt(1); } rs.close(); ps.close(); PMS.get().getFrame().setStatusLine(Messages.getString("DLNAMediaDatabase.2") + " 0%"); int i = 0; int oldpercent = 0; if (dbCount > 0) { ps = conn.prepareStatement( "SELECT FILENAME, MODIFIED, ID FROM FILES", ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE); rs = ps.executeQuery(); while (rs.next()) { String filename = rs.getString("FILENAME"); long modified = rs.getTimestamp("MODIFIED").getTime(); File file = new File(filename); if (!file.exists() || file.lastModified() != modified) { rs.deleteRow(); } i++; int newpercent = i * 100 / dbCount; if (newpercent > oldpercent) { PMS.get() .getFrame() .setStatusLine(Messages.getString("DLNAMediaDatabase.2") + newpercent + "%"); oldpercent = newpercent; } } } } catch (SQLException se) { LOGGER.error(null, se); } finally { close(rs); close(ps); close(conn); } } public ArrayList<File> getFiles(String sql) { ArrayList<File> list = new ArrayList<>(); Connection conn = null; ResultSet rs = null; PreparedStatement ps = null; try { conn = getConnection(); ps = conn.prepareStatement( sql.toLowerCase().startsWith("select") ? sql : ("SELECT FILENAME, MODIFIED FROM FILES WHERE " + sql)); rs = ps.executeQuery(); while (rs.next()) { String filename = rs.getString("FILENAME"); long modified = rs.getTimestamp("MODIFIED").getTime(); File file = new File(filename); if (file.exists() && file.lastModified() == modified) { list.add(file); } } } catch (SQLException se) { LOGGER.error(null, se); return null; } finally { close(rs); close(ps); close(conn); } return list; } private void close(ResultSet rs) { try { if (rs != null) { rs.close(); } } catch (SQLException e) { LOGGER.error("error during closing:" + e.getMessage(), e); } } private void close(Statement ps) { try { if (ps != null) { ps.close(); } } catch (SQLException e) { LOGGER.error("error during closing:" + e.getMessage(), e); } } private void close(Connection conn) { try { if (conn != null) { conn.close(); } } catch (SQLException e) { LOGGER.error("error during closing:" + e.getMessage(), e); } } public boolean isScanLibraryRunning() { return scanner != null && scanner.isAlive(); } public void scanLibrary() { if (isScanLibraryRunning()) { LOGGER.info(Messages.getString("NetworkTab.70")); } else { scanner = new Thread(this, "Library Scanner"); scanner.start(); } } public void stopScanLibrary() { if (isScanLibraryRunning()) { PMS.get().getRootFolder(null).stopScan(); } } @Override public void run() { try { PMS.get().getRootFolder(null).scan(); } catch (Exception e) { LOGGER.error("Unhandled exception during library scan: {}", e.getMessage()); LOGGER.trace("", e); } } }
/** * Construct a proper HTTP response to a received request. After the response has been created, it * is sent and the resulting {@link ChannelFuture} object is returned. See <a * href="http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html">RFC-2616</a> for HTTP header * field definitions. * * @param output The {@link HttpResponse} object that will be used to construct the response. * @param e The {@link MessageEvent} object used to communicate with the client that sent the * request. * @param close Set to true to close the channel after sending the response. By default the * channel is not closed after sending. * @param startStopListenerDelegate The {@link StartStopListenerDelegate} object that is used to * notify plugins that the {@link DLNAResource} is about to start playing. * @return The {@link ChannelFuture} object via which the response was sent. * @throws IOException */ public ChannelFuture answer( HttpResponse output, MessageEvent e, final boolean close, final StartStopListenerDelegate startStopListenerDelegate) throws IOException { ChannelFuture future = null; long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit StringBuilder response = new StringBuilder(); DLNAResource dlna = null; boolean xbox = mediaRenderer.isXBOX(); // Samsung 2012 TVs have a problematic preceding slash that needs to be removed. if (argument.startsWith("/")) { LOGGER.trace("Stripping preceding slash from: " + argument); argument = argument.substring(1); } if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) { // Request to output a page to the HTLM console. output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/html"); response.append(HTMLConsole.servePage(argument.substring(8))); } else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) { // Request to retrieve a file // Extract the resource id from the argument string. String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/")); // Some clients escape the separators in their request, unescape them. id = id.replace("%24", "$"); // Retrieve the DLNAresource itself. List<DLNAResource> files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer); if (transferMode != null) { output.setHeader("TransferMode.DLNA.ORG", transferMode); } if (files.size() == 1) { // DNLAresource was found. dlna = files.get(0); String fileName = argument.substring(argument.lastIndexOf("/") + 1); if (fileName.startsWith("thumbnail0000")) { // This is a request for a thumbnail file. output.setHeader(HttpHeaders.Names.CONTENT_TYPE, dlna.getThumbnailContentType()); output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); if (mediaRenderer.isMediaParserV2()) { dlna.checkThumbnail(); } inputStream = dlna.getThumbnailInputStream(); } else if (fileName.indexOf("subtitle0000") > -1) { // This is a request for a subtitle file output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/plain"); output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT"); List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitlesCodes(); if (subs != null && !subs.isEmpty()) { // TODO: maybe loop subs to get the requested subtitle type instead of using the first // one DLNAMediaSubtitle sub = subs.get(0); inputStream = new java.io.FileInputStream(sub.getFile()); } } else { // This is a request for a regular file. // If range has not been initialized yet and the DLNAResource has its // own start and end defined, initialize range with those values before // requesting the input stream. Range.Time splitRange = dlna.getSplitRange(); if (range.getStart() == null && splitRange.getStart() != null) { range.setStart(splitRange.getStart()); } if (range.getEnd() == null && splitRange.getEnd() != null) { range.setEnd(splitRange.getEnd()); } inputStream = dlna.getInputStream( Range.create(lowRange, highRange, range.getStart(), range.getEnd()), mediaRenderer); // Some renderers (like Samsung devices) allow a custom header for a subtitle URL String subtitleHttpHeader = mediaRenderer.getSubtitleHttpHeader(); if (subtitleHttpHeader != null && !"".equals(subtitleHttpHeader)) { // Device allows a custom subtitle HTTP header; construct it List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitlesCodes(); if (subs != null && !subs.isEmpty()) { DLNAMediaSubtitle sub = subs.get(0); int type = sub.getType(); if (type < DLNAMediaSubtitle.subExtensions.length) { String strType = DLNAMediaSubtitle.subExtensions[type - 1]; String subtitleUrl = "http://" + PMS.get().getServer().getHost() + ':' + PMS.get().getServer().getPort() + "/get/" + id + "/subtitle0000." + strType; output.setHeader(subtitleHttpHeader, subtitleUrl); } } } String name = dlna.getDisplayName(mediaRenderer); if (inputStream == null) { // No inputStream indicates that transcoding / remuxing probably crashed. LOGGER.error("There is no inputstream to return for " + name); } else { // Notify plugins that the DLNAresource is about to start playing startStopListenerDelegate.start(dlna); // Try to determine the content type of the file String rendererMimeType = getRendererMimeType(dlna.mimeType(), mediaRenderer); if (rendererMimeType != null && !"".equals(rendererMimeType)) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, rendererMimeType); } final DLNAMediaInfo media = dlna.getMedia(); if (media != null) { if (StringUtils.isNotBlank(media.getContainer())) { name += " [container: " + media.getContainer() + "]"; } if (StringUtils.isNotBlank(media.getCodecV())) { name += " [video: " + media.getCodecV() + "]"; } } PMS.get().getFrame().setStatusLine("Serving " + name); // Response generation: // We use -1 for arithmetic convenience but don't send it as a value. // If Content-Length < 0 we omit it, for Content-Range we use '*' to signify // unspecified. boolean chunked = mediaRenderer.isChunkedTransfer(); // Determine the total size. Note: when transcoding the length is // not known in advance, so DLNAMediaInfo.TRANS_SIZE will be returned instead. long totalsize = dlna.length(mediaRenderer); if (chunked && totalsize == DLNAMediaInfo.TRANS_SIZE) { // In chunked mode we try to avoid arbitrary values. totalsize = -1; } long remaining = totalsize - lowRange; long requested = highRange - lowRange; if (requested != 0) { // Determine the range (i.e. smaller of known or requested bytes) long bytes = remaining > -1 ? remaining : inputStream.available(); if (requested > 0 && bytes > requested) { bytes = requested + 1; } // Calculate the corresponding highRange (this is usually redundant). highRange = lowRange + bytes - (bytes > 0 ? 1 : 0); LOGGER.trace( (chunked ? "Using chunked response. " : "") + "Sending " + bytes + " bytes."); output.setHeader( HttpHeaders.Names.CONTENT_RANGE, "bytes " + lowRange + "-" + (highRange > -1 ? highRange : "*") + "/" + (totalsize > -1 ? totalsize : "*")); // Content-Length refers to the current chunk size here, though in chunked // mode if the request is open-ended and totalsize is unknown we omit it. if (chunked && requested < 0 && totalsize < 0) { CLoverride = -1; } else { CLoverride = bytes; } } else { // Content-Length refers to the total remaining size of the stream here. CLoverride = remaining; } // Calculate the corresponding highRange (this is usually redundant). highRange = lowRange + CLoverride - (CLoverride > 0 ? 1 : 0); if (contentFeatures != null) { output.setHeader("ContentFeatures.DLNA.ORG", dlna.getDlnaContentFeatures()); } output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); } } } } else if ((method.equals("GET") || method.equals("HEAD")) && (argument.toLowerCase().endsWith(".png") || argument.toLowerCase().endsWith(".jpg") || argument.toLowerCase().endsWith(".jpeg"))) { if (argument.toLowerCase().endsWith(".png")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/png"); } else { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "image/jpeg"); } output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); output.setHeader(HttpHeaders.Names.EXPIRES, getFUTUREDATE() + " GMT"); inputStream = getResourceInputStream(argument); } else if ((method.equals("GET") || method.equals("HEAD")) && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); output.setHeader(HttpHeaders.Names.CACHE_CONTROL, "no-cache"); output.setHeader(HttpHeaders.Names.EXPIRES, "0"); output.setHeader(HttpHeaders.Names.ACCEPT_RANGES, "bytes"); output.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); inputStream = getResourceInputStream((argument.equals("description/fetch") ? "PMS.xml" : argument)); if (argument.equals("description/fetch")) { byte b[] = new byte[inputStream.available()]; inputStream.read(b); String s = new String(b); s = s.replace("[uuid]", PMS.get().usn()); // .substring(0, PMS.get().usn().length()-2)); String profileName = PMS.getConfiguration().getProfileName(); if (PMS.get().getServer().getHost() != null) { s = s.replace("[host]", PMS.get().getServer().getHost()); s = s.replace("[port]", "" + PMS.get().getServer().getPort()); } if (xbox) { LOGGER.debug("DLNA changes for Xbox 360"); s = s.replace( "Universal Media Server", "Universal Media Server [" + profileName + "] : Windows Media Connect"); s = s.replace( "<modelName>UMS</modelName>", "<modelName>Windows Media Connect</modelName>"); s = s.replace( "<serviceList>", "<serviceList>" + CRLF + "<service>" + CRLF + "<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>" + CRLF + "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>" + CRLF + "<SCPDURL>/upnp/mrr/scpd</SCPDURL>" + CRLF + "<controlURL>/upnp/mrr/control</controlURL>" + CRLF + "</service>" + CRLF); } else { s = s.replace("Universal Media Server", "Universal Media Server [" + profileName + "]"); } if (!mediaRenderer.isPS3()) { // hacky stuff. replace the png icon by a jpeg one. Like mpeg2 remux, // really need a proper format compatibility list by renderer s = s.replace("<mimetype>image/png</mimetype>", "<mimetype>image/jpeg</mimetype>"); s = s.replace("/images/thumbnail-256.png", "/images/thumbnail-120.jpg"); s = s.replace(">256<", ">120<"); } response.append(s); inputStream = null; } } else if (method.equals("POST") && (argument.contains("MS_MediaReceiverRegistrar_control") || argument.contains("mrr/control"))) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); if (soapaction != null && soapaction.contains("IsAuthorized")) { response.append(HTTPXMLHelper.XBOX_2); response.append(CRLF); } else if (soapaction != null && soapaction.contains("IsValidated")) { response.append(HTTPXMLHelper.XBOX_1); response.append(CRLF); } response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (method.equals("POST") && argument.endsWith("upnp/control/connection_manager")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); if (soapaction.indexOf("ConnectionManager:1#GetProtocolInfo") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.PROTOCOLINFO_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } } else if (method.equals("POST") && argument.endsWith("upnp/control/content_directory")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml; charset=\"utf-8\""); if (soapaction.indexOf("ContentDirectory:1#GetSystemUpdateID") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_HEADER); response.append(CRLF); response.append("<Id>" + DLNAResource.getSystemUpdateId() + "</Id>"); response.append(CRLF); response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction.indexOf("ContentDirectory:1#X_GetFeatureList") > -1) { // Added for Samsung 2012 TVs response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SAMSUNG_ERROR_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction.indexOf("ContentDirectory:1#GetSortCapabilities") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SORTCAPS_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction.indexOf("ContentDirectory:1#GetSearchCapabilities") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SEARCHCAPS_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction.contains("ContentDirectory:1#Browse") || soapaction.contains("ContentDirectory:1#Search")) { objectID = getEnclosingValue(content, "<ObjectID>", "</ObjectID>"); String containerID = null; if ((objectID == null || objectID.length() == 0) && xbox) { containerID = getEnclosingValue(content, "<ContainerID>", "</ContainerID>"); if (!containerID.contains("$")) { objectID = "0"; } else { objectID = containerID; containerID = null; } } Object sI = getEnclosingValue(content, "<StartingIndex>", "</StartingIndex>"); Object rC = getEnclosingValue(content, "<RequestedCount>", "</RequestedCount>"); browseFlag = getEnclosingValue(content, "<BrowseFlag>", "</BrowseFlag>"); if (sI != null) { startingIndex = Integer.parseInt(sI.toString()); } if (rC != null) { requestCount = Integer.parseInt(rC.toString()); } response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); if (soapaction.contains("ContentDirectory:1#Search")) { response.append(HTTPXMLHelper.SEARCHRESPONSE_HEADER); } else { response.append(HTTPXMLHelper.BROWSERESPONSE_HEADER); } response.append(CRLF); response.append(HTTPXMLHelper.RESULT_HEADER); response.append(HTTPXMLHelper.DIDL_HEADER); if (soapaction.contains("ContentDirectory:1#Search")) { browseFlag = "BrowseDirectChildren"; } // XBOX virtual containers ... d'oh! String searchCriteria = null; if (xbox && PMS.getConfiguration().getUseCache() && PMS.get().getLibrary() != null && containerID != null) { if (containerID.equals("7") && PMS.get().getLibrary().getAlbumFolder() != null) { objectID = PMS.get().getLibrary().getAlbumFolder().getResourceId(); } else if (containerID.equals("6") && PMS.get().getLibrary().getArtistFolder() != null) { objectID = PMS.get().getLibrary().getArtistFolder().getResourceId(); } else if (containerID.equals("5") && PMS.get().getLibrary().getGenreFolder() != null) { objectID = PMS.get().getLibrary().getGenreFolder().getResourceId(); } else if (containerID.equals("F") && PMS.get().getLibrary().getPlaylistFolder() != null) { objectID = PMS.get().getLibrary().getPlaylistFolder().getResourceId(); } else if (containerID.equals("4") && PMS.get().getLibrary().getAllFolder() != null) { objectID = PMS.get().getLibrary().getAllFolder().getResourceId(); } else if (containerID.equals("1")) { String artist = getEnclosingValue(content, "upnp:artist = "", "")"); if (artist != null) { objectID = PMS.get().getLibrary().getArtistFolder().getResourceId(); searchCriteria = artist; } } } List<DLNAResource> files = PMS.get() .getRootFolder(mediaRenderer) .getDLNAResources( objectID, browseFlag != null && browseFlag.equals("BrowseDirectChildren"), startingIndex, requestCount, mediaRenderer); if (searchCriteria != null && files != null) { for (int i = files.size() - 1; i >= 0; i--) { if (!files.get(i).getName().equals(searchCriteria)) { files.remove(i); } } if (files.size() > 0) { files = files.get(0).getChildren(); } } int minus = 0; if (files != null) { for (DLNAResource uf : files) { if (xbox && containerID != null) { uf.setFakeParentId(containerID); } if (uf.isCompatible(mediaRenderer) && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer))) { response.append(uf.toString(mediaRenderer)); } else { minus++; } } } response.append(HTTPXMLHelper.DIDL_FOOTER); response.append(HTTPXMLHelper.RESULT_FOOTER); response.append(CRLF); int filessize = 0; if (files != null) { filessize = files.size(); } response.append("<NumberReturned>").append(filessize - minus).append("</NumberReturned>"); response.append(CRLF); DLNAResource parentFolder = null; if (files != null && filessize > 0) { parentFolder = files.get(0).getParent(); } if (browseFlag != null && browseFlag.equals("BrowseDirectChildren") && mediaRenderer.isMediaParserV2() && mediaRenderer.isDLNATreeHack()) { // with the new parser, files are parsed and analyzed *before* creating the DLNA tree, // every 10 items (the ps3 asks 10 by 10), // so we do not know exactly the total number of items in the DLNA folder to send // (regular files, plus the #transcode folder, maybe the #imdb one, also files can be // invalidated and hidden if format is broken or encrypted, etc.). // let's send a fake total size to force the renderer to ask following items int totalCount = startingIndex + requestCount + 1; // returns 11 when 10 asked if (filessize - minus <= 0) // if no more elements, send the startingIndex { totalCount = startingIndex; } response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>"); } else if (browseFlag != null && browseFlag.equals("BrowseDirectChildren")) { response .append("<TotalMatches>") .append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus) .append("</TotalMatches>"); } else { // from upnp spec: If BrowseMetadata is specified in the BrowseFlags then // TotalMatches = 1 response.append("<TotalMatches>1</TotalMatches>"); } response.append(CRLF); response.append("<UpdateID>"); if (parentFolder != null) { response.append(parentFolder.getUpdateId()); } else { response.append("1"); } response.append("</UpdateID>"); response.append(CRLF); if (soapaction.contains("ContentDirectory:1#Search")) { response.append(HTTPXMLHelper.SEARCHRESPONSE_FOOTER); } else { response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER); } response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); // LOGGER.trace(response.toString()); } } else if (method.equals("SUBSCRIBE")) { output.setHeader("SID", PMS.get().usn()); output.setHeader("TIMEOUT", "Second-1800"); String cb = soapaction.replace("<", "").replace(">", ""); String faddr = cb.replace("http://", "").replace("/", ""); String addr = faddr.split(":")[0]; int port = Integer.parseInt(faddr.split(":")[1]); Socket sock = new Socket(addr, port); OutputStream out = sock.getOutputStream(); out.write(("NOTIFY /" + argument + " HTTP/1.1").getBytes()); out.write(CRLF.getBytes()); out.write(("SID: " + PMS.get().usn()).getBytes()); out.write(CRLF.getBytes()); out.write(("SEQ: " + 0).getBytes()); out.write(CRLF.getBytes()); out.write(("NT: upnp:event").getBytes()); out.write(CRLF.getBytes()); out.write(("NTS: upnp:propchange").getBytes()); out.write(CRLF.getBytes()); out.write(("HOST: " + faddr).getBytes()); out.write(CRLF.getBytes()); out.flush(); out.close(); if (argument.contains("connection_manager")) { response.append( HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ConnectionManager:1")); response.append(HTTPXMLHelper.eventProp("SinkProtocolInfo")); response.append(HTTPXMLHelper.eventProp("SourceProtocolInfo")); response.append(HTTPXMLHelper.eventProp("CurrentConnectionIDs")); response.append(HTTPXMLHelper.EVENT_FOOTER); } else if (argument.contains("content_directory")) { response.append( HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ContentDirectory:1")); response.append(HTTPXMLHelper.eventProp("TransferIDs")); response.append(HTTPXMLHelper.eventProp("ContainerUpdateIDs")); response.append( HTTPXMLHelper.eventProp("SystemUpdateID", "" + DLNAResource.getSystemUpdateId())); response.append(HTTPXMLHelper.EVENT_FOOTER); } } else if (method.equals("NOTIFY")) { output.setHeader(HttpHeaders.Names.CONTENT_TYPE, "text/xml"); output.setHeader("NT", "upnp:event"); output.setHeader("NTS", "upnp:propchange"); output.setHeader("SID", PMS.get().usn()); output.setHeader("SEQ", "0"); response.append("<e:propertyset xmlns:e=\"urn:schemas-upnp-org:event-1-0\">"); response.append("<e:property>"); response.append("<TransferIDs></TransferIDs>"); response.append("</e:property>"); response.append("<e:property>"); response.append("<ContainerUpdateIDs></ContainerUpdateIDs>"); response.append("</e:property>"); response.append("<e:property>"); response .append("<SystemUpdateID>") .append(DLNAResource.getSystemUpdateId()) .append("</SystemUpdateID>"); response.append("</e:property>"); response.append("</e:propertyset>"); } output.setHeader("Server", PMS.get().getServerName()); if (response.length() > 0) { // A response message was constructed; convert it to data ready to be sent. byte responseData[] = response.toString().getBytes("UTF-8"); output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + responseData.length); // HEAD requests only require headers to be set, no need to set contents. if (!method.equals("HEAD")) { // Not a HEAD request, so set the contents of the response. ChannelBuffer buf = ChannelBuffers.copiedBuffer(responseData); output.setContent(buf); } // Send the response to the client. future = e.getChannel().write(output); if (close) { // Close the channel after the response is sent. future.addListener(ChannelFutureListener.CLOSE); } } else if (inputStream != null) { // There is an input stream to send as a response. if (CLoverride > -2) { // Content-Length override has been set, send or omit as appropriate if (CLoverride > -1 && CLoverride != DLNAMediaInfo.TRANS_SIZE) { // Since PS3 firmware 2.50, it is wiser not to send an arbitrary Content-Length, // as the PS3 will display a network error and request the last seconds of the // transcoded video. Better to send no Content-Length at all. output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + CLoverride); } } else { int cl = inputStream.available(); LOGGER.trace("Available Content-Length: " + cl); output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + cl); } if (range.isStartOffsetAvailable() && dlna != null) { // Add timeseek information headers. String timeseekValue = DLNAMediaInfo.getDurationString(range.getStartOrZero()); String timetotalValue = dlna.getMedia().getDurationString(); String timeEndValue = range.isEndLimitAvailable() ? DLNAMediaInfo.getDurationString(range.getEnd()) : timetotalValue; output.setHeader( "TimeSeekRange.dlna.org", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue); output.setHeader( "X-Seek-Range", "npt=" + timeseekValue + "-" + timeEndValue + "/" + timetotalValue); } // Send the response headers to the client. future = e.getChannel().write(output); if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) { // Send the response body to the client in chunks. ChannelFuture chunkWriteFuture = e.getChannel().write(new ChunkedStream(inputStream, BUFFER_SIZE)); // Add a listener to clean up after sending the entire response body. chunkWriteFuture.addListener( new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) { try { PMS.get().getRegistry().reenableGoToSleep(); inputStream.close(); } catch (IOException e) { LOGGER.debug("Caught exception", e); } // Always close the channel after the response is sent because of // a freeze at the end of video when the channel is not closed. future.getChannel().close(); startStopListenerDelegate.stop(); } }); } else { // HEAD method is being used, so simply clean up after the response was sent. try { PMS.get().getRegistry().reenableGoToSleep(); inputStream.close(); } catch (IOException ioe) { LOGGER.debug("Caught exception", ioe); } if (close) { // Close the channel after the response is sent future.addListener(ChannelFutureListener.CLOSE); } startStopListenerDelegate.stop(); } } else { // No response data and no input stream. Seems we are merely serving up headers. if (lowRange > 0 && highRange > 0) { // FIXME: There is no content, so why set a length? output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "" + (highRange - lowRange + 1)); } else { output.setHeader(HttpHeaders.Names.CONTENT_LENGTH, "0"); } // Send the response headers to the client. future = e.getChannel().write(output); if (close) { // Close the channel after the response is sent. future.addListener(ChannelFutureListener.CLOSE); } } // Log trace information Iterator<String> it = output.getHeaderNames().iterator(); while (it.hasNext()) { String headerName = it.next(); LOGGER.trace("Sent to socket: " + headerName + ": " + output.getHeader(headerName)); } return future; }
/** * Circular memory buffer that can be used as {@link java.io.OutputStream OutputStream} and provides * methods that can read data from the memory buffer using an {@link java.io.InputStream * InputStream}. The name of this class is a bit misleading, as there is typically no file involved * in the process at all. Instead, the buffer is typically used to hold data piped by a transcoding * process in one thread until a request for data comes in from another thread. * * @see net.pms.io.ProcessWrapperImpl * @see net.pms.network.Request Request * @see net.pms.network.RequestV2 RequestV2 */ public class BufferedOutputFileImpl extends OutputStream implements BufferedOutputFile { private static final Logger LOGGER = LoggerFactory.getLogger(BufferedOutputFileImpl.class); private static final PmsConfiguration configuration = PMS.getConfiguration(); /** Initial size for the buffer in bytes. The current value is 50MB. */ private static final int INITIAL_BUFFER_SIZE = 50000000; /** Amount of extra bytes to increase the initial buffer with when memory allocation fails. */ private static final int MARGIN_LARGE = 20000000; private static final int MARGIN_MEDIUM = 2000000; private static final int MARGIN_SMALL = 600000; private static final int CHECK_INTERVAL = 500; private static final int CHECK_END_OF_PROCESS = 2500; // must be superior to CHECK_INTERVAL private int minMemorySize; private int maxMemorySize; private int bufferOverflowWarning; private boolean eof; private long writeCount; private byte buffer[]; private boolean forcefirst = (configuration.getTrancodeBlocksMultipleConnections() && configuration.getTrancodeKeepFirstConnections()); private ArrayList<WaitBufferedInputStream> inputStreams; private ProcessWrapper attachedThread; private int secondread_minsize; private Timer timer; private boolean shiftScr; private FileOutputStream debugOutput = null; private boolean buffered = false; private DecimalFormat formatter = new DecimalFormat("#,###"); private double timeseek; private double timeend; private long packetpos = 0; /** * Try to increase the size of a memory buffer, while retaining its contents. The provided new * size is considered to be a request, it is scaled down when an OutOfMemory error occurs. There * is no guarantee about the exact length of the returned byte array, only that it is greater than * or equal to the original buffer size. When null is passed as an argument, a fresh buffer will * be allocated. Copying one byte array to another is a costly operation, both in memory usage and * performance. It is best to avoid using this method. * * @param buffer The byte array to resize, null is allowed. * @param newSize The requested final size. Should be greater than the original size or the * original buffer will be returned. * @return The resized byte array. */ private byte[] growBuffer(byte[] buffer, int newSize) { byte[] copy; if (buffer == null) { // Temporary empty array to avoid null tests in the code below buffer = new byte[0]; } if (newSize <= buffer.length) { // Cannot shrink the original return buffer; } try { // Try to allocate the requested new size copy = new byte[newSize]; } catch (OutOfMemoryError e) { if (buffer.length == 0) { LOGGER.error("Cannot initialize buffer to " + formatter.format(newSize) + " bytes.", e); } else { LOGGER.warn( "Cannot grow buffer size from " + formatter.format(buffer.length) + " bytes to " + formatter.format(newSize) + " bytes.", e); } // Could not allocate the requested new size, use 30% of free memory instead. // Rationale behind using 30%: multiple threads are running at the same time, // we do not want one thread's memory usage to suffocate the others. // Using maxMemory() to ignore the initial Java heap space size that freeMemory() // takes into account. // See http://javarevisited.blogspot.com/2011/05/java-heap-space-memory-size-jvm.html long realisticSize = Runtime.getRuntime().maxMemory() * 3 / 10; if (realisticSize < buffer.length) { // A copy would be smaller in size, shrinking instead of growing the buffer. // Better to return the original and retain its size. return buffer; } else { try { // Try to allocate the realistic alternative size copy = new byte[(int) realisticSize]; } catch (OutOfMemoryError e2) { LOGGER.error( "Cannot grow buffer size from " + formatter.format(buffer.length) + " bytes to " + formatter.format(realisticSize) + " bytes either.", e2); LOGGER.error("freeMemory: " + formatter.format(Runtime.getRuntime().freeMemory())); LOGGER.error("totalMemory: " + formatter.format(Runtime.getRuntime().totalMemory())); LOGGER.error("maxMemory: " + formatter.format(Runtime.getRuntime().maxMemory())); // Cannot allocate memory, no other option than to return the original. return buffer; } } } if (buffer.length == 0) { LOGGER.info( "Successfully initialized buffer to " + formatter.format(copy.length) + " bytes."); } else { try { System.arraycopy(buffer, 0, copy, 0, buffer.length); LOGGER.info( "Successfully grown buffer from " + formatter.format(buffer.length) + " bytes to " + formatter.format(copy.length) + " bytes."); } catch (Exception ex) { LOGGER.error("Cannot grow buffer size, error copying buffer contents.", ex); } } return copy; } /** * Constructor to create a memory buffer based on settings that are passed on. Will also start up * a timer task to display buffer size and usage in the PMS main screen. * * @param params {@link OutputParams} object that contains preferences for the buffers dimensions * and behavior. */ public BufferedOutputFileImpl(OutputParams params) { this.minMemorySize = (int) (1048576 * params.minBufferSize); this.maxMemorySize = (int) (1048576 * params.maxBufferSize); // FIXME: Better to relate margin directly to maxMemorySize instead of using arbitrary fixed // values int margin = MARGIN_LARGE; // Issue 220: extends to 20Mb : readCount is wrongly set cause of the ps3's // 2nd request with a range like 44-xxx, causing the end of buffer margin to be first sent if (this.maxMemorySize < margin) { // for thumbnails / small buffer usage margin = MARGIN_MEDIUM; // margin must be superior to the buffer size of OutputBufferConsumer or // direct buffer size from WindowsNamedPipe class if (this.maxMemorySize < margin) { margin = MARGIN_SMALL; } } this.bufferOverflowWarning = this.maxMemorySize - margin; this.secondread_minsize = params.secondread_minsize; this.timeseek = params.timeseek; this.timeend = params.timeend; this.shiftScr = params.shift_scr; if ((maxMemorySize > INITIAL_BUFFER_SIZE) && !configuration.initBufferMax()) { // Try to limit memory usage a bit. // Start with a modest allocation initially, grow to max when needed later. buffer = growBuffer(null, INITIAL_BUFFER_SIZE); } else { buffer = growBuffer(null, maxMemorySize); } if (buffer.length == 0) { // Cannot transcode without a buffer LOGGER.info("FATAL ERROR: OutOfMemory / dumping stats"); LOGGER.trace("freeMemory: " + Runtime.getRuntime().freeMemory()); LOGGER.trace("totalMemory: " + Runtime.getRuntime().totalMemory()); LOGGER.trace("maxMemory: " + Runtime.getRuntime().maxMemory()); System.exit(1); } inputStreams = new ArrayList<WaitBufferedInputStream>(); timer = new Timer(); if (params.maxBufferSize > 15 && !params.hidebuffer) { timer.schedule( new TimerTask() { @Override public void run() { long rc = 0; if (getCurrentInputStream() != null) { rc = getCurrentInputStream().getReadCount(); PMS.get().getFrame().setReadValue(rc, ""); } long space = (writeCount - rc); LOGGER.trace( "buffered: " + formatter.format(space) + " bytes / inputs: " + inputStreams.size()); // There are 1048576 bytes in a megabyte long bufferInMBs = space / 1048576; PMS.get() .getFrame() .setValue( (int) (100 * space / maxMemorySize), formatter.format(bufferInMBs) + " " + Messages.getString("StatusTab.12")); } }, 0, 2000); } } @Override public void close() throws IOException { LOGGER.trace("EOF"); eof = true; } @Override public WaitBufferedInputStream getCurrentInputStream() { WaitBufferedInputStream wai = null; if (inputStreams.size() > 0) { try { wai = forcefirst ? inputStreams.get(0) : inputStreams.get(inputStreams.size() - 1); } catch (IndexOutOfBoundsException e) { // this should never happen unless there's a concurrency issue, // so log it if it does LOGGER.error("Unexpected input stream removal", e); } } return wai; } @Override public InputStream getInputStream(long newReadPosition) { if (attachedThread != null) { attachedThread.setReadyToStop(false); } WaitBufferedInputStream atominputStream; if (!configuration.getTrancodeBlocksMultipleConnections() || getCurrentInputStream() == null) { atominputStream = new WaitBufferedInputStream(this); inputStreams.add(atominputStream); } else { if (configuration.getTrancodeKeepFirstConnections()) { LOGGER.debug( "BufferedOutputFile is already attached to an InputStream: " + getCurrentInputStream()); } else { // Ditlew - fixes the above (the above iterator breaks on items getting close, cause they // will remove them self from the arraylist) while (inputStreams.size() > 0) { try { inputStreams.get(0).close(); } catch (IOException e) { LOGGER.error("Error: ", e); } } inputStreams.clear(); atominputStream = new WaitBufferedInputStream(this); inputStreams.add(atominputStream); LOGGER.debug("Reassign inputstream: " + getCurrentInputStream()); } return null; } if (newReadPosition > 0) { LOGGER.debug("Setting InputStream new position to: " + formatter.format(newReadPosition)); atominputStream.setReadCount(newReadPosition); } return atominputStream; } @Override public long getWriteCount() { return writeCount; } @Override public void write(byte b[], int off, int len) throws IOException { if (debugOutput != null) { debugOutput.write(b, off, len); debugOutput.flush(); } WaitBufferedInputStream input = getCurrentInputStream(); // LOGGER.trace("write(" + b.length + ", " + off + ", " + len + "), writeCount = " + writeCount // + ", readCount = " + (input != null ? input.getReadCount() : "null")); while ((input != null && (writeCount - input.getReadCount() > bufferOverflowWarning)) || (input == null && writeCount > bufferOverflowWarning)) { try { Thread.sleep(CHECK_INTERVAL); } catch (InterruptedException e) { } input = getCurrentInputStream(); } if (buffer != null) { int mb = (int) (writeCount % maxMemorySize); if (mb >= buffer.length - (len - off)) { if (buffer.length == INITIAL_BUFFER_SIZE) { // Initial buffer size was not big enough, try to increase it buffer = growBuffer(buffer, maxMemorySize); } // FIXME: This smells like 2x System.arraycopy()! int s = (len - off); for (int i = 0; i < s; i++) { buffer[modulo(mb + i, buffer.length)] = b[off + i]; } } else { System.arraycopy(b, off, buffer, mb, (len - off)); if ((len - off) > 0) { buffered = true; } } // Ditlew - WDTV Live if (timeseek > 0 && writeCount > 10) { for (int i = 0; i < len; i++) { if (buffer != null && shiftScr) { shiftSCRByTimeSeek(mb + i, (int) timeseek); // Ditlew - update any SCR headers } // shiftGOPByTimeSeek(mb+i, (int)timeseek); // Ditlew - update any GOP headers - Not // needed for WDTV Live } } writeCount += len - off; if (timeseek > 0 && timeend == 0) { int packetLength = 6; // minimum to get packet size while (packetpos + packetLength < writeCount && buffer != null) { int packetposMB = (int) (packetpos % maxMemorySize); int streamPos = 0; if (buffer[modulo(packetposMB, buffer.length)] == 71) { // TS packetLength = 188; streamPos = 4; // adaptation field if ((buffer[modulo(packetposMB + 3, buffer.length)] & 0x20) == 0x20) { streamPos += 1 + ((buffer[modulo(packetposMB + 4, buffer.length)] + 256) % 256); } if (streamPos == 188) { streamPos = -1; } } else if (buffer[modulo(packetposMB + 3, buffer.length)] == -70) { // BA packetLength = 14; streamPos = -1; } else { packetLength = 6 + (((buffer[modulo(packetposMB + 4, buffer.length)] + 256) % 256)) * 256 + ((buffer[modulo(packetposMB + 5, buffer.length)] + 256) % 256); } if (streamPos != -1) { mb = packetposMB + streamPos + 18; if (!shiftVideo(mb, true)) { mb = mb - 5; shiftAudio(mb, true); } } packetpos += packetLength; } } } } /** * Determine a modulo value that is guaranteed to be zero or positive, as opposed to the standard * Java % operator which can return a negative value. * * @param number Number to divide * @param divisor Number that is used to divide * @return The rest value of the division. */ private int modulo(int number, int divisor) { if (number >= 0) { return number % divisor; } return ((number % divisor) + divisor) % divisor; } @Override public void write(int b) throws IOException { boolean bb = b % 100000 == 0; WaitBufferedInputStream input = getCurrentInputStream(); while (bb && ((input != null && (writeCount - input.getReadCount() > bufferOverflowWarning)) || (input == null && writeCount == bufferOverflowWarning))) { try { Thread.sleep(CHECK_INTERVAL); // LOGGER.trace("BufferedOutputFile Full"); } catch (InterruptedException e) { } input = getCurrentInputStream(); } int mb = (int) (writeCount++ % maxMemorySize); if (buffer != null) { buffer[mb] = (byte) b; buffered = true; if (writeCount == INITIAL_BUFFER_SIZE) { buffer = growBuffer(buffer, maxMemorySize); } if (timeseek > 0 && writeCount > 19) { shiftByTimeSeek(mb, mb <= 20); } // Ditlew - WDTV Live - update any SCR headers if (timeseek > 0 && writeCount > 10) { shiftSCRByTimeSeek(mb, (int) timeseek); } } } // Ditlew - Modify SCR private void shiftSCRByTimeSeek(int buffer_index, int offset_sec) { int m9 = modulo(buffer_index - 9, buffer.length); int m8 = modulo(buffer_index - 8, buffer.length); int m7 = modulo(buffer_index - 7, buffer.length); int m6 = modulo(buffer_index - 6, buffer.length); int m5 = modulo(buffer_index - 5, buffer.length); int m4 = modulo(buffer_index - 4, buffer.length); int m3 = modulo(buffer_index - 3, buffer.length); int m2 = modulo(buffer_index - 2, buffer.length); int m1 = modulo(buffer_index - 1, buffer.length); int m0 = modulo(buffer_index, buffer.length); // SCR if (buffer[m9] == 0 && buffer[m8] == 0 && buffer[m7] == 1 && buffer[m6] == -70 && // 0xBA - Java/PMS wants -70 // control bits !((buffer[m5] & 128) == 128) && ((buffer[m5] & 64) == 64) && ((buffer[m5] & 4) == 4) && ((buffer[m3] & 4) == 4) && ((buffer[m1] & 4) == 4) && ((buffer[m0] & 1) == 1)) { long scr_32_30 = ((buffer[m5] & 56) >> 3); long scr_29_15 = ((buffer[m5] & 3) << 13) + (buffer[m4] << 5) + ((buffer[m3] & 248) >> 3); long scr_14_00 = ((buffer[m3] & 3) << 13) + (buffer[m2] << 5) + ((buffer[m1] & 248) >> 3); long scr = (scr_32_30 << 30) + (scr_29_15 << 15) + scr_14_00; long scr_new = scr + (90000L * offset_sec); long scr_32_30_new = (scr_new & 7516192768L) >> 30; // 111000000000000000000000000000000 long scr_29_15_new = (scr_new & 1073709056L) >> 15; // 000111111111111111000000000000000 long scr_14_00_new = (scr_new & 32767L); // 000000000000000000111111111111111 // scr_32_30_new buffer[m5] = (byte) ((buffer[m5] & 199) + ((scr_32_30_new << 3) & 56)); // 11000111 // scr_29_15_new buffer[m5] = (byte) ((buffer[m5] & 252) + ((scr_29_15_new >> 13) & 3)); // 00000011 buffer[m4] = (byte) (scr_29_15_new >> 5); // 11111111 buffer[m3] = (byte) ((buffer[m3] & 7) + ((scr_29_15_new << 3) & 248)); // 11111000 // scr_14_00_new buffer[m3] = (byte) ((buffer[m3] & 252) + ((scr_14_00_new >> 13) & 3)); // 00000011 buffer[m2] = (byte) (scr_14_00_new >> 5); // 11111111 buffer[m1] = (byte) ((buffer[m1] & 7) + ((scr_14_00_new << 3) & 248)); // 11111000 // Debug // LOGGER.trace("Ditlew - SCR "+scr+" ("+(int)(scr/90000)+") -> "+scr_new+" // ("+(int)(scr_new/90000)+") "+offset_sec+" secs"); } } // Ditlew - Modify GOP @SuppressWarnings("unused") private void shiftGOPByTimeSeek(int buffer_index, int offset_sec) { int m7 = modulo(buffer_index - 7, buffer.length); int m6 = modulo(buffer_index - 6, buffer.length); int m5 = modulo(buffer_index - 5, buffer.length); int m4 = modulo(buffer_index - 4, buffer.length); int m3 = modulo(buffer_index - 3, buffer.length); int m2 = modulo(buffer_index - 2, buffer.length); int m1 = modulo(buffer_index - 1, buffer.length); int m0 = modulo(buffer_index, buffer.length); // check if valid gop if (buffer[m7] == 0 && buffer[m6] == 0 && buffer[m5] == 1 && buffer[m4] == -72 && // 0xB8 - Java/PMS wants -72 // control bits ((buffer[m2] & 0x08) == 0x08) && ((buffer[m0] & 31) == 0) && // of interest !((buffer[m3] & 128) == 128) && // not drop frm !((buffer[m0] & 16) == 16) // not broken ) { // org timecode byte h = (byte) ((buffer[m3] & 124) >> 2); byte m = (byte) (((buffer[m3] & 3) << 4) + ((buffer[m2] & 240) >> 4)); byte s = (byte) (((buffer[m2] & 7) << 3) + ((buffer[m1] & 224) >> 5)); // updated offset int _offset = s + m * 60 + h * 60 + offset_sec; // new timecode byte _h = (byte) ((_offset / 3600) % 24); byte _m = (byte) ((_offset / 60) % 60); byte _s = (byte) (_offset % 60); // update gop // h - ok buffer[m3] = (byte) ((buffer[m3] & 131) + (_h << 2)); // 10000011 // m - ok buffer[m3] = (byte) ((buffer[m3] & 252) + (_m >> 4)); // 11111100 buffer[m2] = (byte) ((buffer[m2] & 15) + (_m << 4)); // 00001111 // s - ok buffer[m2] = (byte) ((buffer[m2] & 248) + (_s >> 3)); // 11111000 buffer[m1] = (byte) ((buffer[m1] & 31) + (_s << 5)); // 00011111 // Debug // LOGGER.trace("Ditlew - GOP "+h+":"+m+":"+s+" -> "+_h+":"+_m+":"+_s+" "+offset_sec+" // secs"); } } private void shiftByTimeSeek(int mb, boolean mod) { shiftVideo(mb, mod); shiftAudio(mb, mod); } private boolean shiftAudio(int mb, boolean mod) { boolean bb = (!mod && (buffer[mb - 10] == -67 || buffer[mb - 10] == -64) && buffer[mb - 11] == 1 && buffer[mb - 12] == 0 && buffer[mb - 13] == 0 && /*(buffer[mb-7]&128)==128 &&*/ (buffer[mb - 6] & 128) == 128 /*buffer[mb-6] == -128*/) || (mod && (buffer[modulo(mb - 10, buffer.length)] == -67 || buffer[modulo(mb - 10, buffer.length)] == -64) && buffer[modulo(mb - 11, buffer.length)] == 1 && buffer[modulo(mb - 12, buffer.length)] == 0 && buffer[modulo(mb - 13, buffer.length)] == 0 && /*(buffer[modulo(mb-7)]&128)==128 && */ (buffer[modulo(mb - 6, buffer.length)] & 128) == 128 /*buffer[modulo(mb-6, buffer.length)] == -128*/); if (bb) { int pts = (((((buffer[modulo(mb - 3, buffer.length)] & 0xff) << 8) + (buffer[modulo(mb - 2, buffer.length)] & 0xff)) >> 1) << 15) + ((((buffer[modulo(mb - 1, buffer.length)] & 0xff) << 8) + (buffer[modulo(mb, buffer.length)] & 0xff)) >> 1); pts += (int) (timeseek * 90000); setTS(pts, mb, mod); return true; } return false; } private boolean shiftVideo(int mb, boolean mod) { boolean bb = (!mod && (buffer[mb - 15] == -32 || buffer[mb - 15] == -3) && buffer[mb - 16] == 1 && buffer[mb - 17] == 0 && buffer[mb - 18] == 0 && (buffer[mb - 11] & 128) == 128 && (buffer[mb - 9] & 32) == 32) || (mod && (buffer[modulo(mb - 15, buffer.length)] == -32 || buffer[modulo(mb - 15, buffer.length)] == -3) && buffer[modulo(mb - 16, buffer.length)] == 1 && buffer[modulo(mb - 17, buffer.length)] == 0 && buffer[modulo(mb - 18, buffer.length)] == 0 && (buffer[modulo(mb - 11, buffer.length)] & 128) == 128 && (buffer[modulo(mb - 9, buffer.length)] & 32) == 32); if (bb) { // check EO or FD (tsMuxeR) int pts = getTS(mb - 5, mod); int dts = 0; boolean dts_present = (buffer[modulo(mb - 11, buffer.length)] & 64) == 64; if (dts_present) { if ((buffer[modulo(mb - 4, buffer.length)] & 15) == 15) { dts = (((((255 - (buffer[modulo(mb - 3, buffer.length)] & 0xff)) << 8) + (255 - (buffer[modulo(mb - 2, buffer.length)] & 0xff))) >> 1) << 15) + ((((255 - (buffer[modulo(mb - 1, buffer.length)] & 0xff)) << 8) + (255 - (buffer[modulo(mb, buffer.length)] & 0xff))) >> 1); dts = -dts; } else { dts = getTS(mb, mod); } } int ts = (int) (timeseek * 90000); if (mb == 50 && writeCount < maxMemorySize) { dts--; } pts += ts; setTS(pts, mb - 5, mod); if (dts_present) { if (dts < 0) { buffer[modulo(mb - 4, buffer.length)] = 17; } dts += ts; setTS(dts, mb, mod); } return true; } return false; } private int getTS(int mb, boolean modulo) { int m3 = mb - 3; int m2 = mb - 2; int m1 = mb - 1; int m0 = mb; if (modulo) { m3 = modulo(m3, buffer.length); m2 = modulo(m2, buffer.length); m1 = modulo(m1, buffer.length); m0 = modulo(m0, buffer.length); } return (((((buffer[m3] & 0xff) << 8) + (buffer[m2] & 0xff)) >> 1) << 15) + ((((buffer[m1] & 0xff) << 8) + (buffer[m0] & 0xff)) >> 1); } private void setTS(int ts, int mb, boolean modulo) { int m3 = mb - 3; int m2 = mb - 2; int m1 = mb - 1; int m0 = mb; if (modulo) { m3 = modulo(m3, buffer.length); m2 = modulo(m2, buffer.length); m1 = modulo(m1, buffer.length); m0 = modulo(m0, buffer.length); } int pts_low = ts & 32767; int pts_high = (ts >> 15) & 32767; int pts_left_low = 1 + (pts_low << 1); int pts_left_high = 1 + (pts_high << 1); buffer[m3] = (byte) ((pts_left_high & 65280) >> 8); buffer[m2] = (byte) (pts_left_high & 255); buffer[m1] = (byte) ((pts_left_low & 65280) >> 8); buffer[m0] = (byte) (pts_left_low & 255); } @Override public int read(boolean firstRead, long readCount, byte buf[], int off, int len) { if (readCount > INITIAL_BUFFER_SIZE && readCount < maxMemorySize) { int newMargin = maxMemorySize - MARGIN_MEDIUM; if (bufferOverflowWarning != newMargin) { LOGGER.debug("Setting margin to 2Mb"); } this.bufferOverflowWarning = newMargin; } if (eof && readCount >= writeCount) { return -1; } int c = 0; int minBufferS = firstRead ? minMemorySize : secondread_minsize; while (writeCount - readCount <= minBufferS && !eof && c < 15) { if (c == 0) { LOGGER.trace("Suspend Read: readCount=" + readCount + " / writeCount=" + writeCount); } c++; try { Thread.sleep(CHECK_INTERVAL); } catch (InterruptedException e) { } } if (attachedThread != null) { attachedThread.setReadyToStop(false); } if (c > 0) { LOGGER.trace("Resume Read: readCount=" + readCount + " / writeCount=" + writeCount); } if (buffer == null || !buffered) { return -1; } int mb = (int) (readCount % maxMemorySize); int endOF = buffer.length; int cut = 0; if (eof && (writeCount - readCount) < len) { cut = (int) (len - (writeCount - readCount)); } if (mb >= endOF - len) { try { System.arraycopy(buffer, mb, buf, off, endOF - mb - cut); } catch (ArrayIndexOutOfBoundsException ex) { LOGGER.error("Something went wrong with the buffer.", ex); LOGGER.error("buffer.length: " + formatter.format(buffer.length) + " bytes."); LOGGER.error("mb: " + mb); LOGGER.error("buf.length: " + formatter.format(buf.length) + " bytes."); LOGGER.error("off: " + off); LOGGER.error("endOF - mb - cut: " + (endOF - mb - cut)); } return endOF - mb; } else { System.arraycopy(buffer, mb, buf, off, len - cut); return len; } } @Override public int read(boolean firstRead, long readCount) { if (readCount > INITIAL_BUFFER_SIZE && readCount < maxMemorySize) { int newMargin = maxMemorySize - MARGIN_MEDIUM; if (bufferOverflowWarning != newMargin) { LOGGER.debug("Setting margin to 2Mb"); } this.bufferOverflowWarning = newMargin; } if (eof && readCount >= writeCount) { return -1; } int c = 0; int minBufferS = firstRead ? minMemorySize : secondread_minsize; while (writeCount - readCount <= minBufferS && !eof && c < 15) { if (c == 0) { LOGGER.trace("Suspend Read: readCount=" + readCount + " / writeCount=" + writeCount); } c++; try { Thread.sleep(CHECK_INTERVAL); } catch (InterruptedException e) { } } if (attachedThread != null) { attachedThread.setReadyToStop(false); } if (c > 0) { LOGGER.trace("Resume Read: readCount=" + readCount + " / writeCount=" + writeCount); } if (buffer == null || !buffered) { return -1; } try { return 0xff & buffer[(int) (readCount % maxMemorySize)]; } catch (ArrayIndexOutOfBoundsException ex) { LOGGER.error("Buffer read ArrayIndexOutOfBoundsException error.", ex); LOGGER.error("buffer.length: " + formatter.format(buffer.length) + " bytes."); LOGGER.error("readCount: \"" + readCount + "\""); LOGGER.error("maxMemorySize: \"" + maxMemorySize + "\""); return -1; } } @Override public void attachThread(ProcessWrapper thread) { if (attachedThread != null) { throw new RuntimeException( "BufferedOutputFile is already attached to a Thread: " + attachedThread); } LOGGER.debug("Attaching thread: " + thread); attachedThread = thread; } @Override public void removeInputStream(WaitBufferedInputStream inputStream) { inputStreams.remove(inputStream); } @Override public void detachInputStream() { PMS.get().getFrame().setReadValue(0, ""); if (attachedThread != null) { attachedThread.setReadyToStop(true); } Runnable checkEnd = new Runnable() { @Override public void run() { try { Thread.sleep(CHECK_END_OF_PROCESS); } catch (InterruptedException e) { LOGGER.error(null, e); } if (attachedThread != null && attachedThread.isReadyToStop()) { if (!attachedThread.isDestroyed()) { attachedThread.stopProcess(); } reset(); } } }; new Thread(checkEnd, "Buffered IO End Checker").start(); } @Override public synchronized void reset() { if (debugOutput != null) { try { debugOutput.close(); } catch (IOException ex) { LOGGER.warn("Caught exception", ex); } } timer.cancel(); if (buffer != null) { LOGGER.info("Destroying buffer"); buffer = null; } buffered = false; if (maxMemorySize != 1048576) { PMS.get().getFrame().setValue(0, Messages.getString("StatusTab.5")); } } }
/** * Returns the filename after being "prettified", which involves attempting to strip away certain * things like information about the quality, resolution, codecs, release groups, fansubbers, * etc., replacing periods with spaces, and various other things to produce a more "pretty" and * standardized filename. * * @param f The filename * @param file The file to possibly be used by the InfoDb * @return The prettified filename */ public static String getFileNameWithRewriting(String f, File file) { String fileNameWithoutExtension; String formattedName; String formattedNameTemp; String searchFormattedName; // These are false unless we recognize that we could use some info on the video from IMDB boolean isEpisodeToLookup = false; boolean isMovieToLookup = false; // Remove file extension fileNameWithoutExtension = getFileNameWithoutExtension(f); formattedName = fileNameWithoutExtension; searchFormattedName = ""; String commonFileEnds = "[\\s\\.]AC3.*|[\\s\\.]REPACK.*|[\\s\\.]480p.*|[\\s\\.]720p.*|[\\s\\.]m-720p.*|[\\s\\.]900p.*|[\\s\\.]1080p.*|[\\s\\.]HDTV.*|[\\s\\.]DSR.*|[\\s\\.]PDTV.*|[\\s\\.]WS.*|[\\s\\.]HQ.*|[\\s\\.]DVDRip.*|[\\s\\.]TVRiP.*|[\\s\\.]BDRip.*|[\\s\\.]WEBRip.*|[\\s\\.]BluRay.*|[\\s\\.]Blu-ray.*|[\\s\\.]SUBBED.*|[\\s\\.]x264.*|[\\s\\.]Dual[\\s\\.]Audio.*|[\\s\\.]HSBS.*|[\\s\\.]H-SBS.*|[\\s\\.]RERiP.*|[\\s\\.]DIRFIX.*|[\\s\\.]READNFO.*|[\\s\\.]60FPS.*"; String commonFileEndsMatch = ".*[\\s\\.]AC3.*|.*[\\s\\.]REPACK.*|.*[\\s\\.]480p.*|.*[\\s\\.]720p.*|.*[\\s\\.]m-720p.*|.*[\\s\\.]900p.*|.*[\\s\\.]1080p.*|.*[\\s\\.]HDTV.*|.*[\\s\\.]DSR.*|.*[\\s\\.]PDTV.*|.*[\\s\\.]WS.*|.*[\\s\\.]HQ.*|.*[\\s\\.]DVDRip.*|.*[\\s\\.]TVRiP.*|.*[\\s\\.]BDRip.*|.*[\\s\\.]WEBRip.*|.*[\\s\\.]BluRay.*|.*[\\s\\.]Blu-ray.*|.*[\\s\\.]SUBBED.*|.*[\\s\\.]x264.*|.*[\\s\\.]Dual[\\s\\.]Audio.*|.*[\\s\\.]HSBS.*|.*[\\s\\.]H-SBS.*|.*[\\s\\.]RERiP.*|.*[\\s\\.]DIRFIX.*|.*[\\s\\.]READNFO.*|.*[\\s\\.]60FPS.*"; String commonFileEndsCaseSensitive = "[\\s\\.]PROPER[\\s\\.].*|[\\s\\.]iNTERNAL[\\s\\.].*|[\\s\\.]LIMITED[\\s\\.].*|[\\s\\.]LiMiTED[\\s\\.].*|[\\s\\.]FESTiVAL[\\s\\.].*|[\\s\\.]NORDIC[\\s\\.].*|[\\s\\.]REAL[\\s\\.].*|[\\s\\.]SUBBED[\\s\\.].*|[\\s\\.]RETAIL[\\s\\.].*"; String commonFileMiddle = "(?i)(Special[\\s\\.]Edition|Unrated|Final[\\s\\.]Cut|Remastered|Extended[\\s\\.]Cut|Extended|IMAX[\\s\\.]Edition)"; if (formattedName.matches(".*[sS]0\\d[eE]\\d\\d[eE]\\d\\d.*")) { // This matches scene and most p2p TV episodes within the first 9 seasons that are double or // triple episodes // Rename the season/episode numbers. For example, "S01E01" changes to " - 101" // Then strip the end of the episode if it does not have the episode name in the title formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEnds + ")", " - $1$2$3-$1$4$5"); formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")", " - $1$2$3-$1$4$5"); // If it matches this then it didn't match the previous one, which means there is probably an // episode title in the filename formattedNameTemp = formattedName.replaceAll( "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3-$1$4$5 - "); if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); // Capitalize the first letter of each word if the string contains no capital letters if (formattedName.equals(formattedName.toLowerCase())) { formattedName = StringUtils.capitaliseAllWords(formattedName); } } else if (formattedName.matches(".*[sS][1-9]\\d[eE]\\d\\d[eE]\\d\\d.*")) { // This matches scene and most p2p TV episodes after their first 9 seasons that are double // episodes // Rename the season/episode numbers. For example, "S11E01" changes to " - 1101" formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEnds + ")", " - $1$2$3-$1$4$5"); formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")", " - $1$2$3-$1$4$5"); // If it matches this then it didn't match the previous one, which means there is probably an // episode title in the filename formattedNameTemp = formattedName.replaceAll( "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3-$1$4$5 - "); if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); // Capitalize the first letter of each word if the string contains no capital letters if (formattedName.equals(formattedName.toLowerCase())) { formattedName = StringUtils.capitaliseAllWords(formattedName); } } else if (formattedName.matches(".*[sS]0\\d[eE]\\d\\d.*")) { // This matches scene and most p2p TV episodes within the first 9 seasons // Rename the season/episode numbers. For example, "S01E01" changes to " - 101" // Then strip the end of the episode if it does not have the episode name in the title formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)(" + commonFileEnds + ")", " - $1$2$3"); formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")", " - $1$2$3"); // If it matches this then it didn't match the previous one, which means there is probably an // episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.]S0(\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3 - "); if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); // Capitalize the first letter of each word if the string contains no capital letters if (formattedName.equals(formattedName.toLowerCase())) { formattedName = StringUtils.capitaliseAllWords(formattedName); } } else if (formattedName.matches(".*[sS][1-9]\\d[eE]\\d\\d.*")) { // This matches scene and most p2p TV episodes after their first 9 seasons // Rename the season/episode numbers. For example, "S11E01" changes to " - 1101" formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)(" + commonFileEnds + ")", " - $1$2$3"); formattedName = formattedName.replaceAll( "(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)(" + commonFileEndsCaseSensitive + ")", " - $1$2$3"); // If it matches this then it didn't match the previous one, which means there is probably an // episode title in the filename formattedNameTemp = formattedName.replaceAll("(?i)[\\s\\.]S([1-9]\\d)E(\\d)(\\d)[\\s\\.]", " - $1$2$3 - "); if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); // Capitalize the first letter of each word if the string contains no capital letters if (formattedName.equals(formattedName.toLowerCase())) { formattedName = StringUtils.capitaliseAllWords(formattedName); } } else if (formattedName.matches( ".*[\\s\\.](19|20)\\d\\d[\\s\\.][0-1]\\d[\\s\\.][0-3]\\d[\\s\\.].*")) { // This matches scene and most p2p TV episodes that release several times per week // Rename the date. For example, "2013.03.18" changes to " - 2013/03/18" formattedName = formattedName.replaceAll( "(?i)[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)(" + commonFileEnds + ")", " - $1$2/$3/$4"); // If it matches this then it didn't match the previous one, which means there is probably an // episode title in the filename formattedNameTemp = formattedName.replaceAll( "(?i)[\\s\\.](19|20)(\\d\\d)[\\s\\.]([0-1]\\d)[\\s\\.]([0-3]\\d)[\\s\\.]", " - $1$2/$3/$4 - "); if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.equals(formattedNameTemp)) { isEpisodeToLookup = true; } // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedNameTemp.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); // Capitalize the first letter of each word if the string contains no capital letters if (formattedName.equals(formattedName.toLowerCase())) { formattedName = StringUtils.capitaliseAllWords(formattedName); } } else if (formattedName.matches(".*[\\s\\.](19|20)\\d\\d[\\s\\.].*")) { // This matches scene and most p2p movies // Rename the year. For example, "2013" changes to " (2013)" formattedName = formattedName.replaceAll("[\\s\\.](19|20)(\\d\\d)", " ($1$2)"); // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); formattedName = formattedName.replaceAll(commonFileMiddle, "($1)"); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); } else if (formattedName.matches(".*\\[(19|20)\\d\\d\\].*")) { // This matches rarer types of movies // Rename the year. For example, "2013" changes to " (2013)" formattedName = formattedName.replaceAll("(?i)\\[(19|20)(\\d\\d)\\].*", " ($1$2)"); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); } else if (formattedName.matches(".*\\((19|20)\\d\\d\\).*")) { // This matches rarer types of movies // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); } else if (formattedName.matches(".*\\((19|20)\\d\\d\\).*")) { // This matches rarer types of movies // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); } else if (formattedName.matches(commonFileEndsMatch)) { // This is probably a movie that doesn't specify a year isMovieToLookup = true; // Remove stuff at the end of the filename like release group, quality, source, etc. formattedName = formattedName.replaceAll("(?i)" + commonFileEnds, ""); formattedName = formattedName.replaceAll(commonFileEndsCaseSensitive, ""); formattedName = formattedName.replaceAll(commonFileMiddle, "($1)"); // Replace periods with spaces formattedName = formattedName.replaceAll("\\.", " "); } else if (formattedName.matches(".*\\[[0-9a-zA-Z]{8}\\]$")) { // This matches anime with a hash at the end of the name // Remove underscores formattedName = formattedName.replaceAll("_", " "); // Remove stuff at the end of the filename like hash, quality, source, etc. formattedName = formattedName.replaceAll( "(?i)\\s\\(1280x720.*|\\s\\(1920x1080.*|\\s\\(720x400.*|\\[720p.*|\\[1080p.*|\\[480p.*|\\s\\(BD.*|\\s\\[Blu-Ray.*|\\s\\[DVD.*|\\.DVD.*|\\[[0-9a-zA-Z]{8}\\]$|\\[h264.*|R1DVD.*|\\[BD.*", ""); // Remove group name from the beginning of the filename if (!"".equals(formattedName)) { if (formattedName.substring(0, 1).matches("\\[")) { int closingBracketIndex = formattedName.indexOf(']'); if (closingBracketIndex != -1) { formattedName = formattedName.substring(closingBracketIndex + 1); } if (formattedName.substring(0, 1).matches("\\s")) { formattedName = formattedName.substring(1); } } } else { formattedName = fileNameWithoutExtension; } if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.substring(formattedName.length() - 3).matches("[\\s\\._]\\d\\d")) { isEpisodeToLookup = true; searchFormattedName = formattedName.substring(0, formattedName.length() - 2) + "S01E" + formattedName.substring(formattedName.length() - 2); } } else if (formattedName.matches( ".*\\[BD\\].*|.*\\[720p\\].*|.*\\[1080p\\].*|.*\\[480p\\].*|.*\\[Blu-Ray.*|.*\\[h264.*")) { // This matches anime without a hash in the name // Remove underscores formattedName = formattedName.replaceAll("_", " "); // Remove stuff at the end of the filename like hash, quality, source, etc. formattedName = formattedName.replaceAll( "(?i)\\[BD\\].*|\\[720p.*|\\[1080p.*|\\[480p.*|\\[Blu-Ray.*\\[h264.*", ""); // Remove group name from the beginning of the filename if (!"".equals(formattedName)) { if (formattedName.substring(0, 1).matches("\\[")) { int closingBracketIndex = formattedName.indexOf(']'); if (closingBracketIndex != -1) { formattedName = formattedName.substring(closingBracketIndex + 1); } if (formattedName.substring(0, 1).matches("\\s")) { formattedName = formattedName.substring(1); } } } else { formattedName = fileNameWithoutExtension; } if (PMS.getConfiguration().isUseInfoFromIMDB() && formattedName.substring(formattedName.length() - 3).matches("[\\s\\._]\\d\\d")) { isEpisodeToLookup = true; searchFormattedName = formattedName.substring(0, formattedName.length() - 2) + "S01E" + formattedName.substring(formattedName.length() - 2); } } // Add episode name (if not there) if (isEpisodeToLookup || isMovieToLookup) { InfoDb.InfoDbData info = PMS.get().infoDb().get(file); if (info == null) { PMS.get().infoDbAdd(file, searchFormattedName); } else if (isEpisodeToLookup && StringUtils.isNotEmpty(info.ep_name)) { formattedName += " - " + info.ep_name; } else if (isMovieToLookup && StringUtils.isNotEmpty(info.year)) { formattedName += " (" + info.year + ")"; } } return formattedName; }
@Override public void handle(HttpExchange t) throws IOException { if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } RootFolder root = parent.getRoot(RemoteUtil.userName(t), t); if (root == null) { throw new IOException("Unknown root"); } Headers h = t.getRequestHeaders(); for (String h1 : h.keySet()) { LOGGER.debug("key " + h1 + "=" + h.get(h1)); } String id = RemoteUtil.getId(path, t); id = RemoteUtil.strip(id); RendererConfiguration r = render; if (render == null) { r = root.getDefaultRenderer(); } DLNAResource dlna = root.getDLNAResource(id, r); if (dlna == null) { // another error LOGGER.debug("media unkonwn"); throw new IOException("Bad id"); } if (!dlna.isCodeValid(dlna)) { LOGGER.debug("coded object with invalid code"); throw new IOException("Bad code"); } DLNAMediaSubtitle sid = null; long len = dlna.length(); Range range = RemoteUtil.parseRange(t.getRequestHeaders(), len); String mime = root.getDefaultRenderer().getMimeType(dlna.mimeType()); // DLNAResource dlna = res.get(0); WebRender render = (WebRender) r; DLNAMediaInfo m = dlna.getMedia(); if (m == null) { m = new DLNAMediaInfo(); dlna.setMedia(m); } if (mime.equals(FormatConfiguration.MIMETYPE_AUTO) && m.getMimeType() != null) { mime = m.getMimeType(); } int code = 200; dlna.setDefaultRenderer(r); if (dlna.getFormat().isVideo()) { if (flash) { mime = "video/flash"; } else if (!RemoteUtil.directmime(mime) || RemoteUtil.transMp4(mime, m)) { mime = render != null ? render.getVideoMimeType() : RemoteUtil.transMime(); if (FileUtil.isUrl(dlna.getSystemName())) { dlna.setPlayer(new FFmpegWebVideo()); } else { dlna.setPlayer(new FFMpegVideo()); } // code = 206; } if (PMS.getConfiguration().getWebSubs() && dlna.getMediaSubtitle() != null && dlna.getMediaSubtitle().isExternal()) { // fetched on the side sid = dlna.getMediaSubtitle(); dlna.setMediaSubtitle(null); } } if (!RemoteUtil.directmime(mime) && dlna.getFormat().isAudio()) { dlna.setPlayer(new FFmpegAudio()); code = 206; } m.setMimeType(mime); LOGGER.debug("dumping media " + mime + " " + dlna); InputStream in = dlna.getInputStream(range, root.getDefaultRenderer()); Headers hdr = t.getResponseHeaders(); hdr.add("Content-Type", mime); hdr.add("Accept-Ranges", "bytes"); if (range != null) { long end = range.asByteRange().getEnd(); long start = range.asByteRange().getStart(); String rStr = start + "-" + end + "/*"; hdr.add("Content-Range", "bytes " + rStr); if (start != 0) { code = 206; } } hdr.add("Server", PMS.get().getServerName()); hdr.add("Connection", "keep-alive"); t.sendResponseHeaders(code, 0); OutputStream os = t.getResponseBody(); render.start(dlna); if (sid != null) { dlna.setMediaSubtitle(sid); } RemoteUtil.dump(in, os, render); }
public abstract class Player { private static final Logger LOGGER = LoggerFactory.getLogger(Player.class); public static final int VIDEO_SIMPLEFILE_PLAYER = 0; public static final int AUDIO_SIMPLEFILE_PLAYER = 1; public static final int VIDEO_WEBSTREAM_PLAYER = 2; public static final int AUDIO_WEBSTREAM_PLAYER = 3; public static final int MISC_PLAYER = 4; public static final String NATIVE = "NATIVE"; public abstract int purpose(); public abstract JComponent config(); public abstract String id(); public abstract String name(); public abstract int type(); // FIXME this is an implementation detail (and not a very good one). // it's entirely up to engines how they construct their command lines. // need to get rid of this public abstract String[] args(); public abstract String mimeType(); public abstract String executable(); protected static final PmsConfiguration _configuration = PMS.getConfiguration(); protected PmsConfiguration configuration = _configuration; private static List<FinalizeTranscoderArgsListener> finalizeTranscoderArgsListeners = new ArrayList<>(); public static void initializeFinalizeTranscoderArgsListeners() { for (ExternalListener listener : ExternalFactory.getExternalListeners()) { if (listener instanceof FinalizeTranscoderArgsListener) { finalizeTranscoderArgsListeners.add((FinalizeTranscoderArgsListener) listener); } } } public boolean avisynth() { return false; } public boolean excludeFormat(Format extension) { return false; } public boolean isPlayerCompatible(RendererConfiguration renderer) { return true; } public boolean isInternalSubtitlesSupported() { return true; } public boolean isExternalSubtitlesSupported() { return true; } public boolean isTimeSeekable() { return false; } /** * Each engine capable of video hardware acceleration must override this method and set * * <p><code>return true</code>. * * @return false */ public boolean isGPUAccelerationReady() { return false; } /** * @deprecated Use {@link #launchTranscode(net.pms.dlna.DLNAResource, net.pms.dlna.DLNAMediaInfo, * net.pms.io.OutputParams)} instead. */ public final ProcessWrapper launchTranscode( String filename, DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException { return launchTranscode(dlna, media, params); } public abstract ProcessWrapper launchTranscode( DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException; @Override public String toString() { return name(); } // no need to pass Player as a parameter: it's the invocant @Deprecated protected String[] finalizeTranscoderArgs( Player player, String filename, DLNAResource dlna, DLNAMediaInfo media, OutputParams params, String[] cmdArgs) { return finalizeTranscoderArgs(filename, dlna, media, params, cmdArgs); } protected String[] finalizeTranscoderArgs( String filename, DLNAResource dlna, DLNAMediaInfo media, OutputParams params, String[] cmdArgs) { if (finalizeTranscoderArgsListeners.isEmpty()) { return cmdArgs; } else { // make it mutable List<String> cmdList = new ArrayList<>(Arrays.asList(cmdArgs)); for (FinalizeTranscoderArgsListener listener : finalizeTranscoderArgsListeners) { try { cmdList = listener.finalizeTranscoderArgs(this, filename, dlna, media, params, cmdList); } catch (Throwable t) { LOGGER.error( String.format( "Failed to call finalizeTranscoderArgs on listener of type=%s", listener.getClass()), t); } } String[] cmdArray = new String[cmdList.size()]; cmdList.toArray(cmdArray); return cmdArray; } } /** * @deprecated Use {@link #setAudioAndSubs(String fileName, DLNAMediaInfo media, OutputParams * params)} instead. */ public void setAudioAndSubs( String fileName, DLNAMediaInfo media, OutputParams params, PmsConfiguration configuration) { setAudioAndSubs(fileName, media, params); } /** * This method populates the supplied {@link OutputParams} object with the correct audio track * (aid) and subtitles (sid), based on the given filename, its MediaInfo metadata and PMS * configuration settings. * * @param fileName The file name used to determine the availability of subtitles. * @param media The MediaInfo metadata for the file. * @param params The parameters to populate. */ public static void setAudioAndSubs(String fileName, DLNAMediaInfo media, OutputParams params) { setAudioOutputParameters(media, params); setSubtitleOutputParameters(fileName, media, params); } /** * This method populates the supplied {@link OutputParams} object with the correct audio track * (aid) based on the MediaInfo metadata and PMS configuration settings. * * @param media The MediaInfo metadata for the file. * @param params The parameters to populate. */ public static void setAudioOutputParameters(DLNAMediaInfo media, OutputParams params) { PmsConfiguration configuration = PMS.getConfiguration(params); if (params.aid == null && media != null && media.getFirstAudioTrack() != null) { // check for preferred audio DLNAMediaAudio dtsTrack = null; StringTokenizer st = new StringTokenizer(configuration.getAudioLanguages(), ","); while (st.hasMoreTokens()) { String lang = st.nextToken().trim(); LOGGER.trace("Looking for an audio track with lang: " + lang); for (DLNAMediaAudio audio : media.getAudioTracksList()) { if (audio.matchCode(lang)) { params.aid = audio; LOGGER.trace("Matched audio track: " + audio); return; } if (dtsTrack == null && audio.isDTS()) { dtsTrack = audio; } } } // preferred audio not found, take a default audio track, dts first if available if (dtsTrack != null) { params.aid = dtsTrack; LOGGER.trace("Found priority audio track with DTS: " + dtsTrack); } else { params.aid = media.getAudioTracksList().get(0); LOGGER.trace("Chose a default audio track: " + params.aid); } } } /** * This method populates the supplied {@link OutputParams} object with the correct subtitles (sid) * based on the given filename, its MediaInfo metadata and PMS configuration settings. * * <p>TODO: Rewrite this crazy method to be more concise and logical. * * @param fileName The file name used to determine the availability of subtitles. * @param media The MediaInfo metadata for the file. * @param params The parameters to populate. */ public static void setSubtitleOutputParameters( String fileName, DLNAMediaInfo media, OutputParams params) { PmsConfiguration configuration = PMS.getConfiguration(params); String currentLang = null; DLNAMediaSubtitle matchedSub = null; if (params.aid != null) { currentLang = params.aid.getLang(); } if (params.sid != null && params.sid.getId() == -1) { LOGGER.trace("Don't want subtitles!"); params.sid = null; return; } /** Check for live subtitles */ if (params.sid != null && !StringUtils.isEmpty(params.sid.getLiveSubURL())) { LOGGER.debug("Live subtitles " + params.sid.getLiveSubURL()); try { matchedSub = params.sid; String file = OpenSubtitle.fetchSubs(matchedSub.getLiveSubURL(), matchedSub.getLiveSubFile()); if (!StringUtils.isEmpty(file)) { matchedSub.setExternalFile(new File(file)); params.sid = matchedSub; return; } } catch (IOException e) { } } StringTokenizer st = new StringTokenizer(configuration.getAudioSubLanguages(), ";"); /** Check for external and internal subtitles matching the user's language preferences */ boolean matchedInternalSubtitles = false; boolean matchedExternalSubtitles = false; while (st.hasMoreTokens()) { String pair = st.nextToken(); if (pair.contains(",")) { String audio = pair.substring(0, pair.indexOf(',')); String sub = pair.substring(pair.indexOf(',') + 1); audio = audio.trim(); sub = sub.trim(); LOGGER.trace( "Searching for a match for: " + currentLang + " with " + audio + " and " + sub); if (Iso639.isCodesMatching(audio, currentLang) || (currentLang != null && audio.equals("*"))) { if (sub.equals("off")) { /** * Ignore the "off" language for external subtitles if the user setting is enabled TODO: * Prioritize multiple external subtitles properly instead of just taking the first one * we load */ if (configuration.isForceExternalSubtitles()) { for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) { if (present_sub.getExternalFile() != null) { matchedSub = present_sub; matchedExternalSubtitles = true; LOGGER.trace( "Ignoring the \"off\" language because there are external subtitles"); break; } } } if (!matchedExternalSubtitles) { matchedSub = new DLNAMediaSubtitle(); matchedSub.setLang("off"); } } else { for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) { if (present_sub.matchCode(sub) || sub.equals("*")) { if (present_sub.getExternalFile() != null) { if (configuration.isAutoloadExternalSubtitles()) { // Subtitle is external and we want external subtitles, look no further matchedSub = present_sub; LOGGER.trace("Matched external subtitles track: " + matchedSub); break; } else { // Subtitle is external but we do not want external subtitles, keep searching LOGGER.trace( "External subtitles ignored because of user setting: " + present_sub); } } else if (!matchedInternalSubtitles) { matchedSub = present_sub; LOGGER.trace("Matched internal subtitles track: " + matchedSub); if (configuration.isAutoloadExternalSubtitles()) { // Subtitle is internal and we will wait to see if an external one is available // instead matchedInternalSubtitles = true; } else { // Subtitle is internal and we will use it break; } } } } } if (matchedSub != null && !matchedInternalSubtitles) { break; } } } } /** * Check for external subtitles that were skipped in the above code block because they didn't * match language preferences, if there wasn't already a match and the user settings specify it. */ if (matchedSub == null && configuration.isForceExternalSubtitles()) { for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) { if (present_sub.getExternalFile() != null) { matchedSub = present_sub; LOGGER.trace( "Matched external subtitles track that did not match language preferences: " + matchedSub); break; } } } /** * Disable chosen subtitles if the user has disabled all subtitles or if the language * preferences have specified the "off" language. * * <p>TODO: Can't we save a bunch of looping by checking for isDisableSubtitles just after the * Live Subtitles check above? */ if (matchedSub != null && params.sid == null) { if (configuration.isDisableSubtitles() || (matchedSub.getLang() != null && matchedSub.getLang().equals("off"))) { LOGGER.trace("Disabled the subtitles: " + matchedSub); } else { params.sid = matchedSub; } } /** Check for forced subtitles. */ if (!configuration.isDisableSubtitles() && params.sid == null && media != null) { // Check for subtitles again File video = new File(fileName); FileUtil.isSubtitlesExists(video, media, false); if (configuration.isAutoloadExternalSubtitles()) { boolean forcedSubsFound = false; // Priority to external subtitles for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) { if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) { st = new StringTokenizer(configuration.getForcedSubtitleTags(), ","); while (sub.getSubtitlesTrackTitleFromMetadata() != null && st.hasMoreTokens()) { String forcedTags = st.nextToken(); forcedTags = forcedTags.trim(); if (sub.getSubtitlesTrackTitleFromMetadata().toLowerCase().contains(forcedTags) && Iso639.isCodesMatching( sub.getLang(), configuration.getForcedSubtitleLanguage())) { LOGGER.trace( "Forcing preferred subtitles: " + sub.getLang() + "/" + sub.getSubtitlesTrackTitleFromMetadata()); LOGGER.trace("Forced subtitles track: " + sub); if (sub.getExternalFile() != null) { LOGGER.trace( "Found external forced file: " + sub.getExternalFile().getAbsolutePath()); } params.sid = sub; forcedSubsFound = true; break; } } if (forcedSubsFound == true) { break; } } else { LOGGER.trace("Found subtitles track: " + sub); if (sub.getExternalFile() != null) { LOGGER.trace("Found external file: " + sub.getExternalFile().getAbsolutePath()); params.sid = sub; break; } } } } if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) { return; } if (params.sid == null) { st = new StringTokenizer(UMSUtils.getLangList(params.mediaRenderer), ","); while (st.hasMoreTokens()) { String lang = st.nextToken(); lang = lang.trim(); LOGGER.trace("Looking for a subtitle track with lang: " + lang); for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) { if (sub.matchCode(lang) && !(!configuration.isAutoloadExternalSubtitles() && sub.getExternalFile() != null)) { params.sid = sub; LOGGER.trace("Matched subtitles track: " + params.sid); return; } } } } } } /** @see #convertToModX(int, int) */ @Deprecated public int convertToMod4(int number) { return convertToModX(number, 4); } /** * Convert number to be divisible by mod. * * @param number the number to convert * @param mod the number to divide by * @return the number divisible by mod */ public static int convertToModX(int number, int mod) { if (number % mod != 0) { number -= (number % mod); } return number; } /** * Returns whether or not the player can handle a given resource. If the resource is <code>null * </code> compatibility cannot be determined and <code>false</code> will be returned. * * @param resource The {@link DLNAResource} to be matched. * @return True when the resource can be handled, false otherwise. * @since 1.60.0 */ public abstract boolean isCompatible(DLNAResource resource); /** * Returns whether or not another player has the same name and id as this one. * * @param other The other player. * @return True if names and ids match, false otherwise. */ @Override public boolean equals(Object other) { if (other == null || !(other instanceof Player)) { return false; } if (other == this) { return true; } Player otherPlayer = (Player) other; return (otherPlayer.name().equals(this.name()) && otherPlayer.id().equals(this.id())); } }
/** * This method populates the supplied {@link OutputParams} object with the correct subtitles (sid) * based on the given filename, its MediaInfo metadata and PMS configuration settings. * * <p>TODO: Rewrite this crazy method to be more concise and logical. * * @param fileName The file name used to determine the availability of subtitles. * @param media The MediaInfo metadata for the file. * @param params The parameters to populate. */ public static void setSubtitleOutputParameters( String fileName, DLNAMediaInfo media, OutputParams params) { PmsConfiguration configuration = PMS.getConfiguration(params); String currentLang = null; DLNAMediaSubtitle matchedSub = null; if (params.aid != null) { currentLang = params.aid.getLang(); } if (params.sid != null && params.sid.getId() == -1) { LOGGER.trace("Don't want subtitles!"); params.sid = null; return; } /** Check for live subtitles */ if (params.sid != null && !StringUtils.isEmpty(params.sid.getLiveSubURL())) { LOGGER.debug("Live subtitles " + params.sid.getLiveSubURL()); try { matchedSub = params.sid; String file = OpenSubtitle.fetchSubs(matchedSub.getLiveSubURL(), matchedSub.getLiveSubFile()); if (!StringUtils.isEmpty(file)) { matchedSub.setExternalFile(new File(file)); params.sid = matchedSub; return; } } catch (IOException e) { } } StringTokenizer st = new StringTokenizer(configuration.getAudioSubLanguages(), ";"); /** Check for external and internal subtitles matching the user's language preferences */ boolean matchedInternalSubtitles = false; boolean matchedExternalSubtitles = false; while (st.hasMoreTokens()) { String pair = st.nextToken(); if (pair.contains(",")) { String audio = pair.substring(0, pair.indexOf(',')); String sub = pair.substring(pair.indexOf(',') + 1); audio = audio.trim(); sub = sub.trim(); LOGGER.trace( "Searching for a match for: " + currentLang + " with " + audio + " and " + sub); if (Iso639.isCodesMatching(audio, currentLang) || (currentLang != null && audio.equals("*"))) { if (sub.equals("off")) { /** * Ignore the "off" language for external subtitles if the user setting is enabled TODO: * Prioritize multiple external subtitles properly instead of just taking the first one * we load */ if (configuration.isForceExternalSubtitles()) { for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) { if (present_sub.getExternalFile() != null) { matchedSub = present_sub; matchedExternalSubtitles = true; LOGGER.trace( "Ignoring the \"off\" language because there are external subtitles"); break; } } } if (!matchedExternalSubtitles) { matchedSub = new DLNAMediaSubtitle(); matchedSub.setLang("off"); } } else { for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) { if (present_sub.matchCode(sub) || sub.equals("*")) { if (present_sub.getExternalFile() != null) { if (configuration.isAutoloadExternalSubtitles()) { // Subtitle is external and we want external subtitles, look no further matchedSub = present_sub; LOGGER.trace("Matched external subtitles track: " + matchedSub); break; } else { // Subtitle is external but we do not want external subtitles, keep searching LOGGER.trace( "External subtitles ignored because of user setting: " + present_sub); } } else if (!matchedInternalSubtitles) { matchedSub = present_sub; LOGGER.trace("Matched internal subtitles track: " + matchedSub); if (configuration.isAutoloadExternalSubtitles()) { // Subtitle is internal and we will wait to see if an external one is available // instead matchedInternalSubtitles = true; } else { // Subtitle is internal and we will use it break; } } } } } if (matchedSub != null && !matchedInternalSubtitles) { break; } } } } /** * Check for external subtitles that were skipped in the above code block because they didn't * match language preferences, if there wasn't already a match and the user settings specify it. */ if (matchedSub == null && configuration.isForceExternalSubtitles()) { for (DLNAMediaSubtitle present_sub : media.getSubtitleTracksList()) { if (present_sub.getExternalFile() != null) { matchedSub = present_sub; LOGGER.trace( "Matched external subtitles track that did not match language preferences: " + matchedSub); break; } } } /** * Disable chosen subtitles if the user has disabled all subtitles or if the language * preferences have specified the "off" language. * * <p>TODO: Can't we save a bunch of looping by checking for isDisableSubtitles just after the * Live Subtitles check above? */ if (matchedSub != null && params.sid == null) { if (configuration.isDisableSubtitles() || (matchedSub.getLang() != null && matchedSub.getLang().equals("off"))) { LOGGER.trace("Disabled the subtitles: " + matchedSub); } else { params.sid = matchedSub; } } /** Check for forced subtitles. */ if (!configuration.isDisableSubtitles() && params.sid == null && media != null) { // Check for subtitles again File video = new File(fileName); FileUtil.isSubtitlesExists(video, media, false); if (configuration.isAutoloadExternalSubtitles()) { boolean forcedSubsFound = false; // Priority to external subtitles for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) { if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) { st = new StringTokenizer(configuration.getForcedSubtitleTags(), ","); while (sub.getSubtitlesTrackTitleFromMetadata() != null && st.hasMoreTokens()) { String forcedTags = st.nextToken(); forcedTags = forcedTags.trim(); if (sub.getSubtitlesTrackTitleFromMetadata().toLowerCase().contains(forcedTags) && Iso639.isCodesMatching( sub.getLang(), configuration.getForcedSubtitleLanguage())) { LOGGER.trace( "Forcing preferred subtitles: " + sub.getLang() + "/" + sub.getSubtitlesTrackTitleFromMetadata()); LOGGER.trace("Forced subtitles track: " + sub); if (sub.getExternalFile() != null) { LOGGER.trace( "Found external forced file: " + sub.getExternalFile().getAbsolutePath()); } params.sid = sub; forcedSubsFound = true; break; } } if (forcedSubsFound == true) { break; } } else { LOGGER.trace("Found subtitles track: " + sub); if (sub.getExternalFile() != null) { LOGGER.trace("Found external file: " + sub.getExternalFile().getAbsolutePath()); params.sid = sub; break; } } } } if (matchedSub != null && matchedSub.getLang() != null && matchedSub.getLang().equals("off")) { return; } if (params.sid == null) { st = new StringTokenizer(UMSUtils.getLangList(params.mediaRenderer), ","); while (st.hasMoreTokens()) { String lang = st.nextToken(); lang = lang.trim(); LOGGER.trace("Looking for a subtitle track with lang: " + lang); for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) { if (sub.matchCode(lang) && !(!configuration.isAutoloadExternalSubtitles() && sub.getExternalFile() != null)) { params.sid = sub; LOGGER.trace("Matched subtitles track: " + params.sid); return; } } } } } }
@Override public String executable() { return PMS.getConfiguration().getFfmpegPath(); }
@SuppressWarnings("restriction") public class RemoteWeb { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteWeb.class); private KeyStore ks; private KeyManagerFactory kmf; private TrustManagerFactory tmf; private HttpServer server; private SSLContext sslContext; private HashMap<String, String> users; private HashMap<String, String> tags; private Map<String, RootFolder> roots; private RemoteUtil.ResourceManager resources; private static final PmsConfiguration configuration = PMS.getConfiguration(); private static final int defaultPort = configuration.getWebPort(); public RemoteWeb() throws IOException { this(defaultPort); } public RemoteWeb(int port) throws IOException { if (port <= 0) { port = defaultPort; } users = new HashMap<>(); tags = new HashMap<>(); roots = new HashMap<String, RootFolder>(); // Add "classpaths" for resolving web resources resources = AccessController.doPrivileged( new PrivilegedAction<RemoteUtil.ResourceManager>() { public RemoteUtil.ResourceManager run() { return new RemoteUtil.ResourceManager( "file:" + configuration.getProfileDirectory() + "/web/", "jar:file:" + configuration.getProfileDirectory() + "/web.zip!/", "file:" + configuration.getWebPath() + "/"); } }); readCred(); // Setup the socket address InetSocketAddress address = new InetSocketAddress(InetAddress.getByName("0.0.0.0"), port); // Initialize the HTTP(S) server if (configuration.getWebHttps()) { try { server = httpsServer(address); } catch (IOException e) { LOGGER.error("Failed to start WEB interface on HTTPS: {}", e.getMessage()); LOGGER.trace("", e); if (e.getMessage().contains("UMS.jks")) { LOGGER.info( "To enable HTTPS please generate a self-signed keystore file called \"UMS.jks\" using the java 'keytool' commandline utility."); } } catch (GeneralSecurityException e) { LOGGER.error( "Failed to start WEB interface on HTTPS due to a security error: {}", e.getMessage()); LOGGER.trace("", e); } } else { server = HttpServer.create(address, 0); } if (server != null) { int threads = configuration.getWebThreads(); // Add context handlers addCtx("/", new RemoteStartHandler(this)); addCtx("/browse", new RemoteBrowseHandler(this)); RemotePlayHandler playHandler = new RemotePlayHandler(this); addCtx("/play", playHandler); addCtx("/playstatus", playHandler); addCtx("/playlist", playHandler); addCtx("/media", new RemoteMediaHandler(this)); addCtx("/fmedia", new RemoteMediaHandler(this, true)); addCtx("/thumb", new RemoteThumbHandler(this)); addCtx("/raw", new RemoteRawHandler(this)); addCtx("/files", new RemoteFileHandler(this)); addCtx("/doc", new RemoteDocHandler(this)); addCtx("/poll", new RemotePollHandler(this)); server.setExecutor(Executors.newFixedThreadPool(threads)); server.start(); } } private HttpServer httpsServer(InetSocketAddress address) throws IOException, GeneralSecurityException { // Initialize the keystore char[] password = "******".toCharArray(); ks = KeyStore.getInstance("JKS"); try (FileInputStream fis = new FileInputStream("UMS.jks")) { ks.load(fis, password); } // Setup the key manager factory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(ks, password); // Setup the trust manager factory tmf = TrustManagerFactory.getInstance("SunX509"); tmf.init(ks); HttpsServer server = HttpsServer.create(address, 0); sslContext = SSLContext.getInstance("TLS"); sslContext.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); server.setHttpsConfigurator( new HttpsConfigurator(sslContext) { @Override public void configure(HttpsParameters params) { try { // initialise the SSL context SSLContext c = SSLContext.getDefault(); SSLEngine engine = c.createSSLEngine(); params.setNeedClientAuth(true); params.setCipherSuites(engine.getEnabledCipherSuites()); params.setProtocols(engine.getEnabledProtocols()); // get the default parameters SSLParameters defaultSSLParameters = c.getDefaultSSLParameters(); params.setSSLParameters(defaultSSLParameters); } catch (Exception e) { LOGGER.debug("https configure error " + e); } } }); return server; } public String getTag(String user) { String tag = tags.get(user); if (tag == null) { return user; } return tag; } public String getAddress() { return PMS.get().getServer().getHost() + ":" + server.getAddress().getPort(); } public RootFolder getRoot(String user, HttpExchange t) { return getRoot(user, false, t); } public RootFolder getRoot(String user, boolean create, HttpExchange t) { String groupTag = getTag(user); String cookie = RemoteUtil.getCookie("UMS", t); RootFolder root; synchronized (roots) { root = roots.get(cookie); if (root == null) { // Double-check for cookie errors WebRender valid = RemoteUtil.matchRenderer(user, t); if (valid != null) { // A browser of the same type and user is already connected at // this ip but for some reason we didn't get a cookie match. RootFolder validRoot = valid.getRootFolder(); // Do a reverse lookup to see if it's been registered for (Map.Entry<String, RootFolder> entry : roots.entrySet()) { if (entry.getValue() == validRoot) { // Found root = validRoot; cookie = entry.getKey(); LOGGER.debug( "Allowing browser connection without cookie match: {}: {}", valid.getRendererName(), t.getRemoteAddress().getAddress()); break; } } } } if (!create || (root != null)) { t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/"); return root; } ArrayList<String> tag = new ArrayList<>(); tag.add(user); if (!groupTag.equals(user)) { tag.add(groupTag); } tag.add(t.getRemoteAddress().getHostString()); tag.add("web"); root = new RootFolder(tag); try { WebRender render = new WebRender(user); root.setDefaultRenderer(render); render.setRootFolder(root); render.associateIP(t.getRemoteAddress().getAddress()); render.associatePort(t.getRemoteAddress().getPort()); if (configuration.useWebSubLang()) { render.setSubLang(StringUtils.join(RemoteUtil.getLangs(t), ",")); } // render.setUA(t.getRequestHeaders().getFirst("User-agent")); render.setBrowserInfo( RemoteUtil.getCookie("UMSINFO", t), t.getRequestHeaders().getFirst("User-agent")); PMS.get().setRendererFound(render); } catch (ConfigurationException e) { root.setDefaultRenderer(RendererConfiguration.getDefaultConf()); } // root.setDefaultRenderer(RendererConfiguration.getRendererConfigurationByName("web")); root.discoverChildren(); cookie = UUID.randomUUID().toString(); t.getResponseHeaders().add("Set-Cookie", "UMS=" + cookie + ";Path=/"); roots.put(cookie, root); } return root; } public void associate(HttpExchange t, WebRender webRenderer) { webRenderer.associateIP(t.getRemoteAddress().getAddress()); webRenderer.associatePort(t.getRemoteAddress().getPort()); } private void addCtx(String path, HttpHandler h) { HttpContext ctx = server.createContext(path, h); if (configuration.isWebAuthenticate()) { ctx.setAuthenticator( new BasicAuthenticator("") { @Override public boolean checkCredentials(String user, String pwd) { LOGGER.debug("authenticate " + user); return pwd.equals(users.get(user)); // return true; } }); } } private void readCred() throws IOException { String cPath = (String) configuration.getCustomProperty("cred.path"); if (StringUtils.isEmpty(cPath)) { return; } File f = new File(cPath); if (!f.exists()) { return; } try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(f), StandardCharsets.UTF_8))) { String str; while ((str = in.readLine()) != null) { str = str.trim(); if (StringUtils.isEmpty(str) || str.startsWith("#")) { continue; } String[] s = str.split("\\s*=\\s*", 2); if (s.length < 2) { continue; } if (!s[0].startsWith("web")) { continue; } String[] s1 = s[0].split("\\.", 2); String[] s2 = s[1].split(",", 2); if (s2.length < 2) { continue; } // s2[0] == usr s2[1] == pwd s1[1] == tag users.put(s2[0], s2[1]); if (s1.length > 1) { // there is a tag here tags.put(s2[0], s1[1]); } } } } public HttpServer getServer() { return server; } static class RemoteThumbHandler implements HttpHandler { private RemoteWeb parent; public RemoteThumbHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } String id = RemoteUtil.getId("thumb/", t); LOGGER.trace("web thumb req " + id); if (id.contains("logo")) { RemoteUtil.sendLogo(t); return; } RootFolder root = parent.getRoot(RemoteUtil.userName(t), t); if (root == null) { LOGGER.debug("weird root in thumb req"); throw new IOException("Unknown root"); } final DLNAResource r = root.getDLNAResource(id, root.getDefaultRenderer()); if (r == null) { // another error LOGGER.debug("media unknown"); throw new IOException("Bad id"); } InputStream in; if (!configuration.isShowCodeThumbs() && !r.isCodeValid(r)) { // we shouldn't show the thumbs for coded objects // unless the code is entered in = r.getGenericThumbnailInputStream(null); } else { r.checkThumbnail(); in = r.getThumbnailInputStream(); } Headers hdr = t.getResponseHeaders(); hdr.add("Content-Type", r.getThumbnailContentType()); hdr.add("Accept-Ranges", "bytes"); hdr.add("Connection", "keep-alive"); t.sendResponseHeaders(200, in.available()); OutputStream os = t.getResponseBody(); LOGGER.trace("input is {} output is {}", in, os); RemoteUtil.dump(in, os); } } static class RemoteFileHandler implements HttpHandler { private RemoteWeb parent; public RemoteFileHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { LOGGER.debug("file req " + t.getRequestURI()); String path = t.getRequestURI().getPath(); String response = null; String mime = null; int status = 200; if (path.contains("crossdomain.xml")) { response = "<?xml version=\"1.0\"?>" + "<!-- http://www.bitsontherun.com/crossdomain.xml -->" + "<cross-domain-policy>" + "<allow-access-from domain=\"*\" />" + "</cross-domain-policy>"; mime = "text/xml"; } else if (path.startsWith("/files/log/")) { String filename = path.substring(11); if (filename.equals("info")) { String log = PMS.get().getFrame().getLog(); log = log.replaceAll("\n", "<br>"); String fullLink = "<br><a href=\"/files/log/full\">Full log</a><br><br>"; String x = fullLink + log; if (StringUtils.isNotEmpty(log)) { x = x + fullLink; } response = "<html><title>UMS LOG</title><body>" + x + "</body></html>"; } else { File file = parent.getResources().getFile(filename); if (file != null) { filename = file.getName(); HashMap<String, Object> vars = new HashMap<>(); vars.put("title", filename); vars.put( "brush", filename.endsWith("debug.log") ? "debug_log" : filename.endsWith(".log") ? "log" : "conf"); vars.put("log", RemoteUtil.read(file).replace("<", "<")); response = parent.getResources().getTemplate("util/log.html").execute(vars); } else { status = 404; } } mime = "text/html"; } else if (parent.getResources().write(path.substring(7), t)) { // The resource manager found and sent the file, all done. return; } else { status = 404; } if (status == 404 && response == null) { response = "<html><body>404 - File Not Found: " + path + "</body></html>"; mime = "text/html"; } RemoteUtil.respond(t, response, status, mime); } } static class RemoteStartHandler implements HttpHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteStartHandler.class); @SuppressWarnings("unused") private static final String CRLF = "\r\n"; private RemoteWeb parent; public RemoteStartHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { LOGGER.debug("root req " + t.getRequestURI()); if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } if (t.getRequestURI().getPath().contains("favicon")) { RemoteUtil.sendLogo(t); return; } HashMap<String, Object> vars = new HashMap<>(); vars.put("serverName", configuration.getServerName()); vars.put("profileName", configuration.getProfileName()); String response = parent.getResources().getTemplate("start.html").execute(vars); RemoteUtil.respond(t, response, 200, "text/html"); } } static class RemoteDocHandler implements HttpHandler { private static final Logger LOGGER = LoggerFactory.getLogger(RemoteDocHandler.class); @SuppressWarnings("unused") private static final String CRLF = "\r\n"; private RemoteWeb parent; public RemoteDocHandler(RemoteWeb parent) { this.parent = parent; // Make sure logs are available right away getLogs(false); } @Override public void handle(HttpExchange t) throws IOException { LOGGER.debug("root req " + t.getRequestURI()); if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } if (t.getRequestURI().getPath().contains("favicon")) { RemoteUtil.sendLogo(t); return; } HashMap<String, Object> vars = new HashMap<>(); vars.put("logs", getLogs(true)); if (configuration.getUseCache()) { vars.put( "cache", "http://" + PMS.get().getServer().getHost() + ":" + PMS.get().getServer().getPort() + "/console/home"); } String response = parent.getResources().getTemplate("doc.html").execute(vars); RemoteUtil.respond(t, response, 200, "text/html"); } private ArrayList<HashMap<String, String>> getLogs(boolean asList) { Set<File> files = new DbgPacker().getItems(); ArrayList<HashMap<String, String>> logs = asList ? new ArrayList<HashMap<String, String>>() : null; for (File f : files) { if (f.exists()) { String id = String.valueOf(parent.getResources().add(f)); if (asList) { HashMap<String, String> item = new HashMap<>(); item.put("filename", f.getName()); item.put("id", id); logs.add(item); } } } return logs; } } public RemoteUtil.ResourceManager getResources() { return resources; } public String getUrl() { if (server != null) { return (server instanceof HttpsServer ? "https://" : "http://") + PMS.get().getServer().getHost() + ":" + server.getAddress().getPort(); } return null; } static class RemotePollHandler implements HttpHandler { @SuppressWarnings("unused") private static final Logger LOGGER = LoggerFactory.getLogger(RemotePollHandler.class); @SuppressWarnings("unused") private static final String CRLF = "\r\n"; private RemoteWeb parent; public RemotePollHandler(RemoteWeb parent) { this.parent = parent; } @Override public void handle(HttpExchange t) throws IOException { // LOGGER.debug("poll req " + t.getRequestURI()); if (RemoteUtil.deny(t)) { throw new IOException("Access denied"); } RootFolder root = parent.getRoot(RemoteUtil.userName(t), t); WebRender renderer = (WebRender) root.getDefaultRenderer(); String json = renderer.getPushData(); RemoteUtil.respond(t, json, 200, "text"); } } }
/** * Applies the IP filter to the specified internet address. Returns true if the address is not * allowed and therefore should be filtered out, false otherwise. * * @param inetAddress The internet address to verify. * @return True when not allowed, false otherwise. */ private boolean filterIp(InetAddress inetAddress) { return !PMS.getConfiguration().getIpFiltering().allowed(inetAddress); }
public class WEB extends Format { private static final PmsConfiguration configuration = PMS.getConfiguration(); /** {@inheritDoc} */ @Override public Identifier getIdentifier() { return Identifier.WEB; } /** * @deprecated Use {@link #isCompatible(DLNAMediaInfo, RendererConfiguration)} instead. * <p>Returns whether or not a format can be handled by the PS3 natively. This means the * format can be streamed to PS3 instead of having to be transcoded. * @return True if the format can be handled by PS3, false otherwise. */ @Deprecated @Override public boolean ps3compatible() { return type == IMAGE; } @Override public ArrayList<Class<? extends Player>> getProfiles() { ArrayList<Class<? extends Player>> a = new ArrayList<>(); if (type == AUDIO) { PMS r = PMS.get(); for (String engine : configuration.getEnginesAsList(r.getRegistry())) { switch (engine) { case MPlayerWebAudio.ID: a.add(MPlayerWebAudio.class); break; case VideoLanAudioStreaming.ID: a.add(VideoLanAudioStreaming.class); break; } } } else { PMS r = PMS.get(); for (String engine : configuration.getEnginesAsList(r.getRegistry())) { switch (engine) { case FFmpegWebVideo.ID: a.add(FFmpegWebVideo.class); break; case MEncoderWebVideo.ID: a.add(MEncoderWebVideo.class); break; case VideoLanVideoStreaming.ID: a.add(VideoLanVideoStreaming.class); break; case MPlayerWebVideoDump.ID: a.add(MPlayerWebVideoDump.class); break; } } } return a; } /** {@inheritDoc} */ @Override // TODO remove screen - it's been tried numerous times (see forum) and it doesn't work public String[] getId() { return new String[] { "http", "mms", "mmsh", "mmst", "rtsp", "rtp", "udp", "screen", "rtmp", "https" }; } @Override public boolean transcodable() { return true; } /** {@inheritDoc} */ @Override public boolean isCompatible(DLNAMediaInfo media, RendererConfiguration renderer) { // Emulating ps3compatible() return type == IMAGE; } }
/** * This class takes care of registering plugins. Plugin jars are loaded, instantiated and stored for * later retrieval. */ public class ExternalFactory { /** For logging messages. */ private static final Logger LOGGER = LoggerFactory.getLogger(ExternalFactory.class); private static final PmsConfiguration configuration = PMS.getConfiguration(); /** List of external listener class instances. */ private static List<ExternalListener> externalListeners = new ArrayList<>(); /** List of external listener classes. */ private static List<Class<?>> externalListenerClasses = new ArrayList<>(); /** List of external listener classes (not yet started). */ private static List<Class<?>> downloadedListenerClasses = new ArrayList<>(); /** List of urlresolvers. */ private static List<URLResolver> urlResolvers = new ArrayList<>(); private static boolean allDone = false; /** * Returns the list of external listener class instances. * * @return The instances. */ public static List<ExternalListener> getExternalListeners() { return externalListeners; } /** * Stores the instance of an external listener in a list for later retrieval. The same instance * will only be stored once. * * @param listener The instance to store. */ public static void registerListener(ExternalListener listener) { if (!externalListeners.contains(listener)) { externalListeners.add(listener); if (listener instanceof URLResolver) { addURLResolver((URLResolver) listener); } } } /** * Stores the class of an external listener in a list for later retrieval. The same class will * only be stored once. * * @param clazz The class to store. */ private static void registerListenerClass(Class<?> clazz) { if (!externalListenerClasses.contains(clazz)) { externalListenerClasses.add(clazz); } } private static String getMainClass(URL jar) { URL[] jarURLs1 = {jar}; URLClassLoader classLoader = new URLClassLoader(jarURLs1); Enumeration<URL> resources; try { // Each plugin .jar file has to contain a resource named "plugin" // which should contain the name of the main plugin class. resources = classLoader.getResources("plugin"); if (resources.hasMoreElements()) { URL url = resources.nextElement(); char[] name; // Determine the plugin main class name from the contents of // the plugin file. try (InputStreamReader in = new InputStreamReader(url.openStream())) { name = new char[512]; in.read(name); } return new String(name).trim(); } } catch (IOException e) { LOGGER.error("Can't load plugin resources", e); } return null; } private static boolean isLib(URL jar) { return (getMainClass(jar) == null); } public static void loadJARs(URL[] jarURLs, boolean download) { // find lib jars first ArrayList<URL> libs = new ArrayList<>(); for (URL jarURL : jarURLs) { if (isLib(jarURL)) { libs.add(jarURL); } } URL[] jarURLs1 = new URL[libs.size() + 1]; libs.toArray(jarURLs1); int pos = libs.size(); for (URL jarURL : jarURLs) { jarURLs1[pos] = jarURL; loadJAR(jarURLs1, download, jarURL); } } /** This method loads the jar files found in the plugin dir or if installed from the web. */ public static void loadJAR(URL[] jarURL, boolean download, URL newURL) { // Create a classloader to take care of loading the plugin classes from // their URL. URLClassLoader classLoader = new URLClassLoader(jarURL); Enumeration<URL> resources; try { // Each plugin .jar file has to contain a resource named "plugin" // which should contain the name of the main plugin class. resources = classLoader.getResources("plugin"); } catch (IOException e) { LOGGER.error("Can't load plugin resources", e); return; } while (resources.hasMoreElements()) { URL url = resources.nextElement(); try { // Determine the plugin main class name from the contents of // the plugin file. char[] name; try (InputStreamReader in = new InputStreamReader(url.openStream())) { name = new char[512]; in.read(name); } String pluginMainClassName = new String(name).trim(); LOGGER.info("Found plugin: " + pluginMainClassName); if (download) { // Only purge code when downloading! purgeCode(pluginMainClassName, newURL); } // Try to load the class based on the main class name Class<?> clazz = classLoader.loadClass(pluginMainClassName); registerListenerClass(clazz); if (download) { downloadedListenerClasses.add(clazz); } } catch (Exception | NoClassDefFoundError e) { LOGGER.error("Error loading plugin", e); } } } private static void purgeCode(String mainClass, URL newUrl) { Class<?> clazz1 = null; for (Class<?> clazz : externalListenerClasses) { if (mainClass.equals(clazz.getCanonicalName())) { clazz1 = clazz; break; } } if (clazz1 == null) { return; } externalListenerClasses.remove(clazz1); ExternalListener remove = null; for (ExternalListener list : externalListeners) { if (list.getClass().equals(clazz1)) { remove = list; break; } } RendererConfiguration.resetAllRenderers(); if (remove != null) { externalListeners.remove(remove); remove.shutdown(); LooksFrame frame = (LooksFrame) PMS.get().getFrame(); frame.getPt().removePlugin(remove); } for (int i = 0; i < 3; i++) { System.gc(); } URLClassLoader cl = (URLClassLoader) clazz1.getClassLoader(); URL[] urls = cl.getURLs(); for (URL url : urls) { String mainClass1 = getMainClass(url); if (mainClass1 == null || !mainClass.equals(mainClass1)) { continue; } File f = url2file(url); File f1 = url2file(newUrl); if (f1 == null || f == null) { continue; } if (!f1.getName().equals(f.getName())) { addToPurgeFile(f); } } } private static File url2file(URL url) { File f; try { f = new File(url.toURI()); } catch (URISyntaxException e) { f = new File(url.getPath()); } return f; } private static void addToPurgeFile(File f) { try { try (FileWriter out = new FileWriter("purge", true)) { out.write(f.getAbsolutePath() + "\r\n"); out.flush(); } } catch (Exception e) { LOGGER.debug("purge file error " + e); } } private static void purgeFiles() { File purge = new File("purge"); String action = configuration.getPluginPurgeAction(); if (action.equalsIgnoreCase("none")) { purge.delete(); return; } try { try (FileInputStream fis = new FileInputStream(purge); BufferedReader in = new BufferedReader(new InputStreamReader(fis))) { String line; while ((line = in.readLine()) != null) { File f = new File(line); if (action.equalsIgnoreCase("delete")) { f.delete(); } else if (action.equalsIgnoreCase("backup")) { FileUtils.moveFileToDirectory(f, new File("backup"), true); f.delete(); } } } } catch (IOException e) { } purge.delete(); } /** * This method scans the plugins directory for ".jar" files and processes each file that is found. * First, a resource named "plugin" is extracted from the jar file. Its contents determine the * name of the main plugin class. This main plugin class is then loaded and an instance is created * and registered for later use. */ public static void lookup() { // Start by purging files purgeFiles(); File pluginDirectory = new File(configuration.getPluginDirectory()); LOGGER.info("Searching for plugins in " + pluginDirectory.getAbsolutePath()); if (!pluginDirectory.exists()) { LOGGER.warn("Plugin directory doesn't exist: " + pluginDirectory); return; } if (!pluginDirectory.isDirectory()) { LOGGER.warn("Plugin directory is not a directory: " + pluginDirectory); return; } if (!pluginDirectory.canRead()) { LOGGER.warn("Plugin directory is not readable: " + pluginDirectory); return; } // Find all .jar files in the plugin directory File[] jarFiles = pluginDirectory.listFiles( new FileFilter() { @Override public boolean accept(File file) { return file.isFile() && file.getName().toLowerCase().endsWith(".jar"); } }); int nJars = (jarFiles == null) ? 0 : jarFiles.length; if (nJars == 0) { LOGGER.info("No plugins found"); return; } // To load a .jar file the filename needs to converted to a file URL List<URL> jarURLList = new ArrayList<>(); for (int i = 0; i < nJars; ++i) { try { jarURLList.add(jarFiles[i].toURI().toURL()); } catch (MalformedURLException e) { LOGGER.error("Can't convert file path " + jarFiles[i] + " to URL", e); } } URL[] jarURLs = new URL[jarURLList.size()]; jarURLList.toArray(jarURLs); // Load the jars loadJARs(jarURLs, false); // Instantiate the early external listeners immediately. instantiateEarlyListeners(); } /** * This method instantiates the external listeners that need to be instantiated immediately so * they can influence the PMS initialization process. * * <p>Not all external listeners are instantiated immediately to avoid premature initialization * where other parts of PMS have not been initialized yet. Those listeners are instantiated at a * later time by {@link #instantiateLateListeners()}. */ private static void instantiateEarlyListeners() { for (Class<?> clazz : externalListenerClasses) { // Skip the classes that should not be instantiated at this // time but rather at a later time. if (!AdditionalFolderAtRoot.class.isAssignableFrom(clazz) && !AdditionalFoldersAtRoot.class.isAssignableFrom(clazz)) { try { // Create a new instance of the plugin class and store it ExternalListener instance = (ExternalListener) clazz.newInstance(); registerListener(instance); } catch (InstantiationException | IllegalAccessException e) { LOGGER.error("Error instantiating plugin", e); } } } } /** * This method instantiates the external listeners whose class has not yet been instantiated by * {@link #instantiateEarlyListeners()}. */ public static void instantiateLateListeners() { for (Class<?> clazz : externalListenerClasses) { // Only AdditionalFolderAtRoot and AdditionalFoldersAtRoot // classes have been skipped by lookup(). if (AdditionalFolderAtRoot.class.isAssignableFrom(clazz) || AdditionalFoldersAtRoot.class.isAssignableFrom(clazz)) { try { // Create a new instance of the plugin class and store it ExternalListener instance = (ExternalListener) clazz.newInstance(); registerListener(instance); } catch (InstantiationException | IllegalAccessException e) { LOGGER.error("Error instantiating plugin", e); } } } allDone = true; } private static void postInstall(Class<?> clazz) { Method postInstall; try { postInstall = clazz.getDeclaredMethod("postInstall", null); if (Modifier.isStatic(postInstall.getModifiers())) { postInstall.invoke(null, null); } } // Ignore all errors catch (SecurityException | NoSuchMethodException | IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { } } private static void doUpdate(JLabel update, String text) { if (update == null) { return; } update.setText(text); } public static void instantiateDownloaded(JLabel update) { // These are found in the downloadedListenerClasses list for (Class<?> clazz : downloadedListenerClasses) { ExternalListener instance; try { doUpdate(update, Messages.getString("NetworkTab.48") + " " + clazz.getSimpleName()); postInstall(clazz); LOGGER.debug("do inst of " + clazz.getSimpleName()); instance = (ExternalListener) clazz.newInstance(); doUpdate(update, instance.name() + " " + Messages.getString("NetworkTab.49")); registerListener(instance); if (PMS.get().getFrame() instanceof LooksFrame) { LooksFrame frame = (LooksFrame) PMS.get().getFrame(); if (!frame.getPt().appendPlugin(instance)) { LOGGER.warn("Plugin limit of 30 has been reached"); } } } catch (InstantiationException | IllegalAccessException e) { LOGGER.error("Error instantiating plugin", e); } } downloadedListenerClasses.clear(); } public static boolean localPluginsInstalled() { return allDone; } private static boolean quoted(String s) { return s.startsWith("\"") && s.endsWith("\""); } private static String quote(String s) { if (quoted(s)) { return s; } return "\"" + s + "\""; } public static URLResult resolveURL(String url) { String quotedUrl = quote(url); for (URLResolver resolver : urlResolvers) { URLResult res = resolver.urlResolve(url); if (res != null) { if (StringUtils.isEmpty(res.url) || quotedUrl.equals(quote(res.url))) { res.url = null; } if (res.precoder != null && res.precoder.isEmpty()) { res.precoder = null; } if (res.args != null && res.args.isEmpty()) { res.args = null; } if (res.url != null || res.precoder != null || res.args != null) { LOGGER.debug( ((ExternalListener) resolver).name() + " resolver:" + (res.url == null ? "" : " url=" + res.url) + (res.precoder == null ? "" : " precoder=" + res.precoder) + (res.args == null ? "" : " args=" + res.args)); return res; } } } return null; } public static void addURLResolver(URLResolver res) { if (urlResolvers.contains(res)) { return; } if (urlResolvers.isEmpty()) { urlResolvers.add(res); return; } String[] tmp = PMS.getConfiguration().getURLResolveOrder(); if (tmp.length == 0) { // no order at all, just add it urlResolvers.add(res); return; } int id = -1; for (int i = 0; i < tmp.length; i++) { if (tmp[i].equalsIgnoreCase(res.name())) { id = i; break; } } if (id == -1) { // no order here, just add it urlResolvers.add(res); return; } if (id > urlResolvers.size()) { // add it last urlResolvers.add(res); return; } urlResolvers.add(id, res); } }
public class SubtitleUtils { private static final Logger logger = LoggerFactory.getLogger(SubtitleUtils.class); private static PmsConfiguration configuration = PMS.getConfiguration(); private static final Map<String, String> fileCharsetToMencoderSubcpOptionMap = new HashMap<String, String>() { { // Cyrillic / Russian put(CHARSET_IBM855, "enca:ru:cp1251"); put(CHARSET_ISO_8859_5, "enca:ru:cp1251"); put(CHARSET_KOI8_R, "enca:ru:cp1251"); put(CHARSET_MACCYRILLIC, "enca:ru:cp1251"); put(CHARSET_WINDOWS_1251, "enca:ru:cp1251"); put(CHARSET_IBM866, "enca:ru:cp1251"); // Greek put(CHARSET_WINDOWS_1253, "cp1253"); put(CHARSET_ISO_8859_7, "ISO-8859-7"); // Western Europe put(CHARSET_WINDOWS_1252, "cp1252"); // Hebrew put(CHARSET_WINDOWS_1255, "cp1255"); put(CHARSET_ISO_8859_8, "ISO-8859-8"); // Chinese put(CHARSET_ISO_2022_CN, "ISO-2022-CN"); put(CHARSET_BIG5, "enca:zh:big5"); put(CHARSET_GB18030, "enca:zh:big5"); put(CHARSET_EUC_TW, "enca:zh:big5"); put(CHARSET_HZ_GB_2312, "enca:zh:big5"); // Korean put(CHARSET_ISO_2022_KR, "cp949"); put(CHARSET_EUC_KR, "euc-kr"); // Japanese put(CHARSET_ISO_2022_JP, "ISO-2022-JP"); put(CHARSET_EUC_JP, "euc-jp"); put(CHARSET_SHIFT_JIS, "shift-jis"); } }; private static final EnumSet<SubtitleType> SUPPORTS_TIME_SHIFTING = EnumSet.of(SubtitleType.SUBRIP, SubtitleType.ASS); private static final DecimalFormat ASS_DECIMAL_FORMAT = new DecimalFormat("00.00"); private static final DecimalFormat SRT_DECIMAL_FORMAT = new DecimalFormat("00.000"); static { final DecimalFormatSymbols dotDecimalSeparator = new DecimalFormatSymbols(); dotDecimalSeparator.setDecimalSeparator('.'); ASS_DECIMAL_FORMAT.setDecimalFormatSymbols(dotDecimalSeparator); final DecimalFormatSymbols commaDecimalSeparator = new DecimalFormatSymbols(); commaDecimalSeparator.setDecimalSeparator(','); SRT_DECIMAL_FORMAT.setDecimalFormatSymbols(commaDecimalSeparator); } /** * Returns value for -subcp option for non UTF-8 external subtitles based on detected charset. * * @param dlnaMediaSubtitle DLNAMediaSubtitle with external subtitles file. * @return value for mencoder's -subcp option or null if can't determine. */ public static String getSubCpOptionForMencoder(DLNAMediaSubtitle dlnaMediaSubtitle) { if (dlnaMediaSubtitle == null) { throw new NullPointerException("dlnaMediaSubtitle can't be null."); } if (isBlank(dlnaMediaSubtitle.getExternalFileCharacterSet())) { return null; } return fileCharsetToMencoderSubcpOptionMap.get(dlnaMediaSubtitle.getExternalFileCharacterSet()); } /** * Shift timing of subtitles in SSA/ASS or SRT format and converts charset to UTF8 if necessary * * @param inputSubtitles Subtitles file in SSA/ASS or SRT format * @param timeShift Time stamp value * @return Converted subtitles file * @throws IOException */ public static DLNAMediaSubtitle shiftSubtitlesTimingWithUtfConversion( final DLNAMediaSubtitle inputSubtitles, double timeShift) throws IOException { if (inputSubtitles == null) { throw new NullPointerException("inputSubtitles should not be null."); } if (!inputSubtitles.isExternal()) { throw new IllegalArgumentException("inputSubtitles should be external."); } if (isBlank(inputSubtitles.getExternalFile().getName())) { throw new IllegalArgumentException( "inputSubtitles' external file should not have blank name."); } if (inputSubtitles.getType() == null) { throw new NullPointerException("inputSubtitles.getType() should not be null."); } if (!isSupportsTimeShifting(inputSubtitles.getType())) { throw new IllegalArgumentException( "inputSubtitles.getType() " + inputSubtitles.getType() + " is not supported."); } final File convertedSubtitlesFile = new File( configuration.getTempFolder(), getBaseName(inputSubtitles.getExternalFile().getName()) + System.currentTimeMillis() + ".tmp"); FileUtils.forceDeleteOnExit(convertedSubtitlesFile); BufferedReader input; final boolean isSubtitlesCodepageForcedInConfigurationAndSupportedByJVM = isNotBlank(configuration.getSubtitlesCodepage()) && Charset.isSupported(configuration.getSubtitlesCodepage()); final boolean isSubtitlesCodepageAutoDetectedAndSupportedByJVM = isNotBlank(inputSubtitles.getExternalFileCharacterSet()) && Charset.isSupported(inputSubtitles.getExternalFileCharacterSet()); if (isSubtitlesCodepageForcedInConfigurationAndSupportedByJVM) { input = new BufferedReader( new InputStreamReader( new FileInputStream(inputSubtitles.getExternalFile()), Charset.forName(configuration.getSubtitlesCodepage()))); } else if (isSubtitlesCodepageAutoDetectedAndSupportedByJVM) { input = new BufferedReader( new InputStreamReader( new FileInputStream(inputSubtitles.getExternalFile()), Charset.forName(inputSubtitles.getExternalFileCharacterSet()))); } else { input = new BufferedReader( new InputStreamReader(new FileInputStream(inputSubtitles.getExternalFile()))); } final BufferedWriter output = new BufferedWriter( new OutputStreamWriter( new FileOutputStream(convertedSubtitlesFile), Charset.forName("UTF-8"))); String line; double startTime; double endTime; try { if (SubtitleType.ASS.equals(inputSubtitles.getType())) { while ((line = input.readLine()) != null) { if (startsWith(line, "Dialogue:")) { String[] timings = splitPreserveAllTokens(line, ","); if (timings.length >= 3 && isNotBlank(timings[1]) && isNotBlank(timings[1])) { startTime = convertSubtitleTimingStringToTime(timings[1]); endTime = convertSubtitleTimingStringToTime(timings[2]); if (startTime >= timeShift) { timings[1] = convertTimeToSubtitleTimingString( startTime - timeShift, TimingFormat.ASS_TIMING); timings[2] = convertTimeToSubtitleTimingString(endTime - timeShift, TimingFormat.ASS_TIMING); output.write(join(timings, ",") + "\n"); } else { continue; } } else { output.write(line + "\n"); } } else { output.write(line + "\n"); } } } else if (SubtitleType.SUBRIP.equals(inputSubtitles.getType())) { int n = 1; while ((line = input.readLine()) != null) { if (contains(line, ("-->"))) { startTime = convertSubtitleTimingStringToTime(line.substring(0, line.indexOf("-->") - 1)); endTime = convertSubtitleTimingStringToTime(line.substring(line.indexOf("-->") + 4)); if (startTime >= timeShift) { output.write("" + (n++) + "\n"); output.write( convertTimeToSubtitleTimingString( startTime - timeShift, TimingFormat.SRT_TIMING)); output.write(" --> "); output.write( convertTimeToSubtitleTimingString(endTime - timeShift, TimingFormat.SRT_TIMING) + "\n"); while (isNotBlank(line = input.readLine())) { // Read all following subs lines output.write(line + "\n"); } output.write("" + "\n"); } } } } } finally { if (output != null) { output.flush(); output.close(); } if (input != null) { input.close(); } } final DLNAMediaSubtitle convertedSubtitles = new DLNAMediaSubtitle(); convertedSubtitles.setExternalFile(convertedSubtitlesFile); convertedSubtitles.setType(inputSubtitles.getType()); convertedSubtitles.setLang(inputSubtitles.getLang()); convertedSubtitles.setFlavor(inputSubtitles.getFlavor()); convertedSubtitles.setId(inputSubtitles.getId()); return convertedSubtitles; } /** * Check if subtitleType supports time shifting * * @param subtitleType to check * @return true if subtitleType can be time shifted with {@link * #shiftSubtitlesTimingWithUtfConversion(net.pms.dlna.DLNAMediaSubtitle, double)} */ public static boolean isSupportsTimeShifting(SubtitleType subtitleType) { return SUPPORTS_TIME_SHIFTING.contains(subtitleType); } enum TimingFormat { ASS_TIMING, SRT_TIMING, SECONDS_TIMING; } /** * Converts time in seconds to subtitle timing string. * * @param time in seconds * @param timingFormat format of timing string * @return timing string */ static String convertTimeToSubtitleTimingString( final double time, final TimingFormat timingFormat) { if (timingFormat == null) { throw new NullPointerException("timingFormat should not be null."); } double s = Math.abs(time % 60); int h = (int) (time / 3600); int m = Math.abs(((int) (time / 60)) % 60); switch (timingFormat) { case ASS_TIMING: return trim(String.format("% 02d:%02d:%s", h, m, ASS_DECIMAL_FORMAT.format(s))); case SRT_TIMING: return trim(String.format("% 03d:%02d:%s", h, m, SRT_DECIMAL_FORMAT.format(s))); case SECONDS_TIMING: return trim(String.format("% 03d:%02d:%02.0f", h, m, s)); default: return trim(String.format("% 03d:%02d:%02.0f", h, m, s)); } } /** * Converts subtitle timing string to seconds. * * @param timingString in format OO:00:00.000 * @return seconds or null if conversion failed */ static Double convertSubtitleTimingStringToTime(final String timingString) throws NumberFormatException { if (isBlank(timingString)) { throw new IllegalArgumentException("timingString should not be blank."); } final StringTokenizer st = new StringTokenizer(timingString, ":"); try { int h = Integer.parseInt(st.nextToken()); int m = Integer.parseInt(st.nextToken()); double s = Double.parseDouble(replace(st.nextToken(), ",", ".")); if (h >= 0) { return h * 3600 + m * 60 + s; } else { return h * 3600 - m * 60 - s; } } catch (NumberFormatException nfe) { logger.debug("Failed to convert timing string \"" + timingString + "\"."); throw nfe; } } /** * For testing purposes. * * @param configuration */ static void setConfiguration(PmsConfiguration configuration) { SubtitleUtils.configuration = configuration; } }
public class PipeProcess { private static final Logger LOGGER = LoggerFactory.getLogger(PipeProcess.class); private static final PmsConfiguration configuration = PMS.getConfiguration(); private String linuxPipeName; private WindowsNamedPipe mk; private boolean forcereconnect; public PipeProcess(String pipeName, OutputParams params, String... extras) { forcereconnect = false; boolean in = true; if (extras != null && extras.length > 0 && extras[0].equals("out")) { in = false; } if (extras != null) { for (int i = 0; i < extras.length; i++) { if (extras[i].equals("reconnect")) { forcereconnect = true; } } } if (PMS.get().isWindows()) { mk = new WindowsNamedPipe(pipeName, forcereconnect, in, params); } else { linuxPipeName = getPipeName(pipeName); } } public PipeProcess(String pipeName, String... extras) { this(pipeName, null, extras); } private static String getPipeName(String pipeName) { try { return configuration.getTempFolder() + "/" + pipeName; } catch (IOException e) { LOGGER.error("Pipe may not be in temporary directory", e); return pipeName; } } public String getInputPipe() { if (!PMS.get().isWindows()) { return linuxPipeName; } return mk.getPipeName(); } public String getOutputPipe() { if (!PMS.get().isWindows()) { return linuxPipeName; } return mk.getPipeName(); } public ProcessWrapper getPipeProcess() { if (!PMS.get().isWindows()) { OutputParams mkfifo_vid_params = new OutputParams(configuration); mkfifo_vid_params.maxBufferSize = 0.1; mkfifo_vid_params.log = true; String cmdArray[]; if (Platform.isMac() || Platform.isFreeBSD() || Platform.isSolaris()) { cmdArray = new String[] {"mkfifo", "-m", "777", linuxPipeName}; } else { cmdArray = new String[] {"mkfifo", "--mode=777", linuxPipeName}; } ProcessWrapperImpl mkfifo_vid_process = new ProcessWrapperImpl(cmdArray, mkfifo_vid_params); return mkfifo_vid_process; } return mk; } public void deleteLater() { if (!PMS.get().isWindows()) { File f = new File(linuxPipeName); f.deleteOnExit(); } } public BufferedOutputFile getDirectBuffer() throws IOException { if (!PMS.get().isWindows()) { return null; } return mk.getDirectBuffer(); } public InputStream getInputStream() throws IOException { if (!PMS.get().isWindows()) { LOGGER.trace("Opening file " + linuxPipeName + " for reading..."); RandomAccessFile raf = new RandomAccessFile(linuxPipeName, "r"); return new FileInputStream(raf.getFD()); } return mk.getReadable(); } public OutputStream getOutputStream() throws IOException { if (!PMS.get().isWindows()) { LOGGER.trace("Opening file " + linuxPipeName + " for writing..."); RandomAccessFile raf = new RandomAccessFile(linuxPipeName, "rw"); return new FileOutputStream(raf.getFD()); } return mk.getWritable(); } }
public class Request extends HTTPResource { private static final Logger logger = LoggerFactory.getLogger(Request.class); private static final PmsConfiguration configuration = PMS.getConfiguration(); private static final String CRLF = "\r\n"; private static final String HTTP_200_OK = "HTTP/1.1 200 OK"; private static final String HTTP_500 = "HTTP/1.1 500 Internal Server Error"; private static final String HTTP_206_OK = "HTTP/1.1 206 Partial Content"; private static final String HTTP_200_OK_10 = "HTTP/1.0 200 OK"; private static final String HTTP_206_OK_10 = "HTTP/1.0 206 Partial Content"; private static final String CONTENT_TYPE_UTF8 = "CONTENT-TYPE: text/xml; charset=\"utf-8\""; private static final String CONTENT_TYPE = "Content-Type: text/xml; charset=\"utf-8\""; private static SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss", Locale.US); private final String method; private String argument; private String soapaction; private String content; private OutputStream output; private String objectID; private int startingIndex; private int requestCount; private String browseFlag; private long lowRange; private InputStream inputStream; private RendererConfiguration mediaRenderer; private String transferMode; private String contentFeatures; private double timeseek; private double timeRangeEnd; private long highRange; private boolean http10; public RendererConfiguration getMediaRenderer() { return mediaRenderer; } public void setMediaRenderer(RendererConfiguration mediaRenderer) { this.mediaRenderer = mediaRenderer; } public InputStream getInputStream() { return inputStream; } public long getLowRange() { return lowRange; } public void setLowRange(long lowRange) { this.lowRange = lowRange; } public String getTransferMode() { return transferMode; } public void setTransferMode(String transferMode) { this.transferMode = transferMode; } public String getContentFeatures() { return contentFeatures; } public void setContentFeatures(String contentFeatures) { this.contentFeatures = contentFeatures; } public double getTimeseek() { return timeseek; } public void setTimeseek(double timeseek) { this.timeseek = timeseek; } public void setTimeRangeEnd(double timeRangeEnd) { this.timeRangeEnd = timeRangeEnd; } public long getHighRange() { return highRange; } public void setHighRange(long highRange) { this.highRange = highRange; } public boolean isHttp10() { return http10; } public void setHttp10(boolean http10) { this.http10 = http10; } public Request(String method, String argument) { this.method = method; this.argument = argument; } public String getSoapaction() { return soapaction; } public void setSoapaction(String soapaction) { this.soapaction = soapaction; } public String getTextContent() { return content; } public void setTextContent(String content) { this.content = content; } public String getMethod() { return method; } public String getArgument() { return argument; } public void answer(OutputStream output, StartStopListenerDelegate startStopListenerDelegate) throws IOException { this.output = output; long CLoverride = -2; // 0 and above are valid Content-Length values, -1 means omit if (lowRange != 0 || highRange != 0) { output(output, http10 ? HTTP_206_OK_10 : HTTP_206_OK); } else { if (soapaction != null && soapaction.contains("X_GetFeatureList")) { // If we don't return a 500 error, Samsung 2012 TVs time out. output(output, HTTP_500); } else { output(output, http10 ? HTTP_200_OK_10 : HTTP_200_OK); } } StringBuilder response = new StringBuilder(); DLNAResource dlna = null; boolean xbox = mediaRenderer.isXBOX(); // Samsung 2012 TVs have a problematic preceding slash that needs to be removed. if (argument.startsWith("/")) { logger.trace("Stripping preceding slash from: " + argument); argument = argument.substring(1); } if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("console/")) { output(output, "Content-Type: text/html"); response.append(HTMLConsole.servePage(argument.substring(8))); } else if ((method.equals("GET") || method.equals("HEAD")) && argument.startsWith("get/")) { String id = argument.substring(argument.indexOf("get/") + 4, argument.lastIndexOf("/")); id = id.replace("%24", "$"); // popcorn hour ? List<DLNAResource> files = PMS.get().getRootFolder(mediaRenderer).getDLNAResources(id, false, 0, 0, mediaRenderer); if (transferMode != null) { output(output, "TransferMode.DLNA.ORG: " + transferMode); } if (files.size() == 1) { // DNLAresource was found. dlna = files.get(0); String fileName = argument.substring(argument.lastIndexOf("/") + 1); if (fileName.startsWith("thumbnail0000")) { // This is a request for a thumbnail file. output(output, "Content-Type: " + dlna.getThumbnailContentType()); output(output, "Accept-Ranges: bytes"); output(output, "Expires: " + getFUTUREDATE() + " GMT"); output(output, "Connection: keep-alive"); if (mediaRenderer.isMediaParserV2()) { dlna.checkThumbnail(); } inputStream = dlna.getThumbnailInputStream(); } else if (fileName.indexOf("subtitle0000") > -1) { // This is a request for a subtitle file output(output, "Content-Type: text/plain"); output(output, "Expires: " + getFUTUREDATE() + " GMT"); List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitleTracksList(); if (subs != null && !subs.isEmpty()) { // TODO: maybe loop subs to get the requested subtitle type instead of using the first // one DLNAMediaSubtitle sub = subs.get(0); if (sub.isExternal()) { inputStream = new java.io.FileInputStream(sub.getExternalFile()); } } } else { // This is a request for a regular file. String name = dlna.getDisplayName(mediaRenderer); inputStream = dlna.getInputStream( Range.create(lowRange, highRange, timeseek, timeRangeEnd), mediaRenderer); if (inputStream == null) { // No inputStream indicates that transcoding / remuxing probably crashed. logger.error("There is no inputstream to return for " + name); } else { startStopListenerDelegate.start(dlna); output(output, "Content-Type: " + getRendererMimeType(dlna.mimeType(), mediaRenderer)); if (!configuration.isDisableSubtitles()) { // Some renderers (like Samsung devices) allow a custom header for a subtitle URL String subtitleHttpHeader = mediaRenderer.getSubtitleHttpHeader(); if (subtitleHttpHeader != null && !"".equals(subtitleHttpHeader)) { // Device allows a custom subtitle HTTP header; construct it List<DLNAMediaSubtitle> subs = dlna.getMedia().getSubtitleTracksList(); if (subs != null && !subs.isEmpty()) { DLNAMediaSubtitle sub = subs.get(0); String subtitleUrl; String subExtension = sub.getType().getExtension(); if (isNotBlank(subExtension)) { subtitleUrl = "http://" + PMS.get().getServer().getHost() + ':' + PMS.get().getServer().getPort() + "/get/" + id + "/subtitle0000." + subExtension; } else { subtitleUrl = "http://" + PMS.get().getServer().getHost() + ':' + PMS.get().getServer().getPort() + "/get/" + id + "/subtitle0000"; } output(output, subtitleHttpHeader + ": " + subtitleUrl); } } } final DLNAMediaInfo media = dlna.getMedia(); if (media != null) { if (StringUtils.isNotBlank(media.getContainer())) { name += " [container: " + media.getContainer() + "]"; } if (StringUtils.isNotBlank(media.getCodecV())) { name += " [video: " + media.getCodecV() + "]"; } } PMS.get().getFrame().setStatusLine("Serving " + name); // Response generation: // We use -1 for arithmetic convenience but don't send it as a value. // If Content-Length < 0 we omit it, for Content-Range we use '*' to signify // unspecified. boolean chunked = mediaRenderer.isChunkedTransfer(); // Determine the total size. Note: when transcoding the length is // not known in advance, so DLNAMediaInfo.TRANS_SIZE will be returned instead. long totalsize = dlna.length(mediaRenderer); if (chunked && totalsize == DLNAMediaInfo.TRANS_SIZE) { // In chunked mode we try to avoid arbitrary values. totalsize = -1; } long remaining = totalsize - lowRange; long requested = highRange - lowRange; if (requested != 0) { // Determine the range (i.e. smaller of known or requested bytes) long bytes = remaining > -1 ? remaining : inputStream.available(); if (requested > 0 && bytes > requested) { bytes = requested + 1; } // Calculate the corresponding highRange (this is usually redundant). highRange = lowRange + bytes - (bytes > 0 ? 1 : 0); logger.trace( (chunked ? "Using chunked response. " : "") + "Sending " + bytes + " bytes."); output( output, "Content-Range: bytes " + lowRange + "-" + (highRange > -1 ? highRange : "*") + "/" + (totalsize > -1 ? totalsize : "*")); // Content-Length refers to the current chunk size here, though in chunked // mode if the request is open-ended and totalsize is unknown we omit it. if (chunked && requested < 0 && totalsize < 0) { CLoverride = -1; } else { CLoverride = bytes; } } else { // Content-Length refers to the total remaining size of the stream here. CLoverride = remaining; } if (contentFeatures != null) { output(output, "ContentFeatures.DLNA.ORG: " + dlna.getDlnaContentFeatures()); } if (dlna.getPlayer() == null || xbox) { output(output, "Accept-Ranges: bytes"); } output(output, "Connection: keep-alive"); } } } } else if ((method.equals("GET") || method.equals("HEAD")) && (argument.toLowerCase().endsWith(".png") || argument.toLowerCase().endsWith(".jpg") || argument.toLowerCase().endsWith(".jpeg"))) { if (argument.toLowerCase().endsWith(".png")) { output(output, "Content-Type: image/png"); } else { output(output, "Content-Type: image/jpeg"); } output(output, "Accept-Ranges: bytes"); output(output, "Connection: keep-alive"); output(output, "Expires: " + getFUTUREDATE() + " GMT"); inputStream = getResourceInputStream(argument); } else if ((method.equals("GET") || method.equals("HEAD")) && (argument.equals("description/fetch") || argument.endsWith("1.0.xml"))) { String profileName = configuration.getProfileName(); output(output, CONTENT_TYPE); output(output, "Cache-Control: no-cache"); output(output, "Expires: 0"); output(output, "Accept-Ranges: bytes"); output(output, "Connection: keep-alive"); inputStream = getResourceInputStream((argument.equals("description/fetch") ? "PMS.xml" : argument)); if (argument.equals("description/fetch")) { byte b[] = new byte[inputStream.available()]; inputStream.read(b); String s = new String(b); s = s.replace("[uuid]", PMS.get().usn()); // .substring(0, PMS.get().usn().length()-2)); s = s.replace("[host]", PMS.get().getServer().getHost()); s = s.replace("[port]", "" + PMS.get().getServer().getPort()); if (xbox) { logger.debug("DLNA changes for Xbox 360"); s = s.replace( "PS3 Media Server", "PS3 Media Server [" + profileName + "] : Windows Media Connect"); s = s.replace( "<modelName>PMS</modelName>", "<modelName>Windows Media Connect</modelName>"); s = s.replace( "<serviceList>", "<serviceList>" + CRLF + "<service>" + CRLF + "<serviceType>urn:microsoft.com:service:X_MS_MediaReceiverRegistrar:1</serviceType>" + CRLF + "<serviceId>urn:microsoft.com:serviceId:X_MS_MediaReceiverRegistrar</serviceId>" + CRLF + "<SCPDURL>/upnp/mrr/scpd</SCPDURL>" + CRLF + "<controlURL>/upnp/mrr/control</controlURL>" + CRLF + "</service>" + CRLF); } else { s = s.replace("PS3 Media Server", "PS3 Media Server [" + profileName + "]"); } inputStream = new ByteArrayInputStream(s.getBytes()); } } else if (method.equals("POST") && (argument.contains("MS_MediaReceiverRegistrar_control") || argument.contains("mrr/control"))) { output(output, CONTENT_TYPE_UTF8); response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); if (soapaction != null && soapaction.contains("IsAuthorized")) { response.append(HTTPXMLHelper.XBOX_2); response.append(CRLF); } else if (soapaction != null && soapaction.contains("IsValidated")) { response.append(HTTPXMLHelper.XBOX_1); response.append(CRLF); } response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (method.equals("POST") && argument.endsWith("upnp/control/connection_manager")) { output(output, CONTENT_TYPE_UTF8); if (soapaction != null && soapaction.indexOf("ConnectionManager:1#GetProtocolInfo") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.PROTOCOLINFO_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } } else if (method.equals("SUBSCRIBE")) { if (soapaction == null) { // Ignore this return; } output(output, CONTENT_TYPE_UTF8); output(output, "Content-Length: 0"); output(output, "Connection: close"); output(output, "SID: " + PMS.get().usn()); output(output, "Server: " + PMS.get().getServerName()); output(output, "Timeout: Second-1800"); output(output, ""); output.flush(); // output.close(); String cb = soapaction.replace("<", "").replace(">", ""); try { URL soapActionUrl = new URL(cb); String addr = soapActionUrl.getHost(); int port = soapActionUrl.getPort(); Socket sock = new Socket(addr, port); OutputStream out = sock.getOutputStream(); output(out, "NOTIFY /" + argument + " HTTP/1.1"); output(out, "SID: " + PMS.get().usn()); output(out, "SEQ: " + 0); output(out, "NT: upnp:event"); output(out, "NTS: upnp:propchange"); output(out, "HOST: " + addr + ":" + port); output(out, CONTENT_TYPE_UTF8); sock.close(); } catch (MalformedURLException ex) { logger.debug("Cannot parse address and port from soap action \"" + soapaction + "\"", ex); } if (argument.contains("connection_manager")) { response.append( HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ConnectionManager:1")); response.append(HTTPXMLHelper.eventProp("SinkProtocolInfo")); response.append(HTTPXMLHelper.eventProp("SourceProtocolInfo")); response.append(HTTPXMLHelper.eventProp("CurrentConnectionIDs")); response.append(HTTPXMLHelper.EVENT_FOOTER); } else if (argument.contains("content_directory")) { response.append( HTTPXMLHelper.eventHeader("urn:schemas-upnp-org:service:ContentDirectory:1")); response.append(HTTPXMLHelper.eventProp("TransferIDs")); response.append(HTTPXMLHelper.eventProp("ContainerUpdateIDs")); response.append( HTTPXMLHelper.eventProp("SystemUpdateID", "" + DLNAResource.getSystemUpdateId())); response.append(HTTPXMLHelper.EVENT_FOOTER); } } else if (method.equals("POST") && argument.endsWith("upnp/control/content_directory")) { output(output, CONTENT_TYPE_UTF8); if (soapaction != null && soapaction.indexOf("ContentDirectory:1#GetSystemUpdateID") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_HEADER); response.append(CRLF); response.append("<Id>").append(DLNAResource.getSystemUpdateId()).append("</Id>"); response.append(CRLF); response.append(HTTPXMLHelper.GETSYSTEMUPDATEID_FOOTER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction != null && soapaction.indexOf("ContentDirectory:1#GetSortCapabilities") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SORTCAPS_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction != null && soapaction.indexOf("ContentDirectory:1#X_GetFeatureList") > -1) { // Added for Samsung 2012 TVs response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.UPNP_INVALID_ACTION); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction != null && soapaction.indexOf("ContentDirectory:1#GetSearchCapabilities") > -1) { response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SEARCHCAPS_RESPONSE); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); } else if (soapaction != null && (soapaction.contains("ContentDirectory:1#Browse") || soapaction.contains("ContentDirectory:1#Search"))) { objectID = getEnclosingValue(content, "<ObjectID>", "</ObjectID>"); String containerID = null; if (isEmpty(objectID) && xbox) { containerID = getEnclosingValue(content, "<ContainerID>", "</ContainerID>"); if (containerID == null || !containerID.contains("$")) { objectID = "0"; } else { objectID = containerID; containerID = null; } } Object sI = getEnclosingValue(content, "<StartingIndex>", "</StartingIndex>"); Object rC = getEnclosingValue(content, "<RequestedCount>", "</RequestedCount>"); browseFlag = getEnclosingValue(content, "<BrowseFlag>", "</BrowseFlag>"); if (sI != null) { startingIndex = Integer.parseInt(sI.toString()); } if (rC != null) { requestCount = Integer.parseInt(rC.toString()); } response.append(HTTPXMLHelper.XML_HEADER); response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_HEADER); response.append(CRLF); if (soapaction != null && soapaction.contains("ContentDirectory:1#Search")) { response.append(HTTPXMLHelper.SEARCHRESPONSE_HEADER); } else { response.append(HTTPXMLHelper.BROWSERESPONSE_HEADER); } response.append(CRLF); response.append(HTTPXMLHelper.RESULT_HEADER); response.append(HTTPXMLHelper.DIDL_HEADER); if (soapaction != null && soapaction.contains("ContentDirectory:1#Search")) { browseFlag = "BrowseDirectChildren"; } // XBOX virtual containers ... doh String searchCriteria = null; if (xbox && configuration.getUseCache() && PMS.get().getLibrary() != null && containerID != null) { if (containerID.equals("7") && PMS.get().getLibrary().getAlbumFolder() != null) { objectID = PMS.get().getLibrary().getAlbumFolder().getResourceId(); } else if (containerID.equals("6") && PMS.get().getLibrary().getArtistFolder() != null) { objectID = PMS.get().getLibrary().getArtistFolder().getResourceId(); } else if (containerID.equals("5") && PMS.get().getLibrary().getGenreFolder() != null) { objectID = PMS.get().getLibrary().getGenreFolder().getResourceId(); } else if (containerID.equals("F") && PMS.get().getLibrary().getPlaylistFolder() != null) { objectID = PMS.get().getLibrary().getPlaylistFolder().getResourceId(); } else if (containerID.equals("4") && PMS.get().getLibrary().getAllFolder() != null) { objectID = PMS.get().getLibrary().getAllFolder().getResourceId(); } else if (containerID.equals("1")) { String artist = getEnclosingValue(content, "upnp:artist = "", "")"); if (artist != null) { objectID = PMS.get().getLibrary().getArtistFolder().getResourceId(); searchCriteria = artist; } } } List<DLNAResource> files = PMS.get() .getRootFolder(mediaRenderer) .getDLNAResources( objectID, browseFlag != null && browseFlag.equals("BrowseDirectChildren"), startingIndex, requestCount, mediaRenderer); if (searchCriteria != null && files != null) { for (int i = files.size() - 1; i >= 0; i--) { if (!files.get(i).getName().equals(searchCriteria)) { files.remove(i); } } if (files.size() > 0) { files = files.get(0).getChildren(); } } int minus = 0; if (files != null) { for (DLNAResource uf : files) { if (xbox && containerID != null) { uf.setFakeParentId(containerID); } if (uf.isCompatible(mediaRenderer) && (uf.getPlayer() == null || uf.getPlayer().isPlayerCompatible(mediaRenderer))) { response.append(uf.getDidlString(mediaRenderer)); } else { minus++; } } } response.append(HTTPXMLHelper.DIDL_FOOTER); response.append(HTTPXMLHelper.RESULT_FOOTER); response.append(CRLF); int filessize = 0; if (files != null) { filessize = files.size(); } response.append("<NumberReturned>").append(filessize - minus).append("</NumberReturned>"); response.append(CRLF); DLNAResource parentFolder = null; if (files != null && filessize > 0) { parentFolder = files.get(0).getParent(); } if (browseFlag != null && browseFlag.equals("BrowseDirectChildren") && mediaRenderer.isMediaParserV2() && mediaRenderer.isDLNATreeHack()) { // with the new parser, files are parsed and analyzed *before* // creating the DLNA tree, every 10 items (the ps3 asks 10 by 10), // so we do not know exactly the total number of items in the DLNA folder to send // (regular files, plus the #transcode folder, maybe the #imdb one, also files can be // invalidated and hidden if format is broken or encrypted, etc.). // let's send a fake total size to force the renderer to ask following items int totalCount = startingIndex + requestCount + 1; // returns 11 when 10 asked if (filessize - minus <= 0) { // if no more elements, send startingIndex totalCount = startingIndex; } response.append("<TotalMatches>").append(totalCount).append("</TotalMatches>"); } else if (browseFlag != null && browseFlag.equals("BrowseDirectChildren")) { response .append("<TotalMatches>") .append(((parentFolder != null) ? parentFolder.childrenNumber() : filessize) - minus) .append("</TotalMatches>"); } else { // from upnp spec: If BrowseMetadata is specified in the BrowseFlags then TotalMatches = 1 response.append("<TotalMatches>1</TotalMatches>"); } response.append(CRLF); response.append("<UpdateID>"); if (parentFolder != null) { response.append(parentFolder.getUpdateId()); } else { response.append("1"); } response.append("</UpdateID>"); response.append(CRLF); if (soapaction != null && soapaction.contains("ContentDirectory:1#Search")) { response.append(HTTPXMLHelper.SEARCHRESPONSE_FOOTER); } else { response.append(HTTPXMLHelper.BROWSERESPONSE_FOOTER); } response.append(CRLF); response.append(HTTPXMLHelper.SOAP_ENCODING_FOOTER); response.append(CRLF); // logger.trace(response.toString()); } } output(output, "Server: " + PMS.get().getServerName()); if (response.length() > 0) { byte responseData[] = response.toString().getBytes("UTF-8"); output(output, "Content-Length: " + responseData.length); output(output, ""); if (!method.equals("HEAD")) { output.write(responseData); // logger.trace(response.toString()); } } else if (inputStream != null) { if (CLoverride > -2) { // Content-Length override has been set, send or omit as appropriate if (CLoverride > -1 && CLoverride != DLNAMediaInfo.TRANS_SIZE) { // Since PS3 firmware 2.50, it is wiser not to send an arbitrary Content-Length, // as the PS3 will display a network error and request the last seconds of the // transcoded video. Better to send no Content-Length at all. output(output, "Content-Length: " + CLoverride); } } else { int cl = inputStream.available(); logger.trace("Available Content-Length: " + cl); output(output, "Content-Length: " + cl); } if (timeseek > 0 && dlna != null) { String timeseekValue = DLNAMediaInfo.getDurationString(timeseek); String timetotalValue = dlna.getMedia().getDurationString(); output( output, "TimeSeekRange.dlna.org: npt=" + timeseekValue + "-" + timetotalValue + "/" + timetotalValue); output( output, "X-Seek-Range: npt=" + timeseekValue + "-" + timetotalValue + "/" + timetotalValue); } output(output, ""); int sendB = 0; if (lowRange != DLNAMediaInfo.ENDFILE_POS && !method.equals("HEAD")) { sendB = sendBytes(inputStream); // , ((lowRange > 0 && highRange > 0)?(highRange-lowRange):-1) } logger.trace("Sending stream: " + sendB + " bytes of " + argument); PMS.get().getFrame().setStatusLine(null); } else { // inputStream is null if (lowRange > 0 && highRange > 0) { output(output, "Content-Length: " + (highRange - lowRange + 1)); } else { output(output, "Content-Length: 0"); } output(output, ""); } } private void output(OutputStream output, String line) throws IOException { output.write((line + CRLF).getBytes("UTF-8")); logger.trace("Wrote on socket: " + line); } private String getFUTUREDATE() { sdf.setTimeZone(TimeZone.getTimeZone("GMT")); return sdf.format(new Date(10000000000L + System.currentTimeMillis())); } // VISTA tip ?: netsh interface tcp set global autotuninglevel=disabled private int sendBytes(InputStream fis) throws IOException { byte[] buffer = new byte[32 * 1024]; int bytes; int sendBytes = 0; try { while ((bytes = fis.read(buffer)) != -1) { output.write(buffer, 0, bytes); sendBytes += bytes; } } catch (IOException e) { logger.trace( "Sending stream with premature end: " + sendBytes + " bytes of " + argument + ". Reason: " + e.getMessage()); } finally { fis.close(); } return sendBytes; } private String getEnclosingValue(String content, String leftTag, String rightTag) { String result = null; int leftTagPos = content.indexOf(leftTag); int rightTagPos = content.indexOf(rightTag, leftTagPos + 1); if (leftTagPos > -1 && rightTagPos > leftTagPos) { result = content.substring(leftTagPos + leftTag.length(), rightTagPos); } return result; } }
/** * This is a plugin for ps3mediaserver @see http://code.google.com/p/ps3mediaserver/ It allow you to * keep track of which files were viewed. * * @author Cees-Willem Hofstede <*****@*****.**> */ public class ViewStatus implements StartStopListener, ThumbnailExtras, ActionListener { private static final Logger log = LoggerFactory.getLogger(ViewStatus.class); private boolean enabledMV; private JCheckBox cbEnableMV; Date previousDate; private Queue<Date> startDates = new LinkedList<Date>(); PmsConfiguration PMSConf = PMS.getConfiguration(); public ViewStatus() { if (PMSConf.getCustomProperty("enableViewStatus") == null) { // if not set in configuration, enable plugin by default PMSConf.setCustomProperty("enableViewStatus", true); } enabledMV = PMSConf.getCustomProperty("enableViewStatus").equals("true"); // true if plugin is enabled } @Override public void donePlaying(DLNAMediaInfo media, DLNAResource resource) { // currently only for videofiles if (enabledMV && resource.getType() == Format.VIDEO) { // get path information Path infoFilePath = Paths.get(resource.getSystemName()); String folderName = infoFilePath.getParent().toString(); String infoFile = folderName + "/.viewstatus"; String infoKey = resource.getName(); // create handler for properties Properties props = new Properties(); double fileViewPercentage = 0; try { props.load(new FileInputStream(infoFile)); // load the viewinfo file (if any) fileViewPercentage = Integer.parseInt(props.getProperty(infoKey, "0")); } catch (IOException e) { log.error("viewinfo at " + infoFile + " file does not yet exist"); } double playLengthSec = 0; // total length of the file /** * @TODO: calculation below should work without startdate. Is it possible to get the exact * number of seconds the file was stopped? */ playLengthSec = (int) (new Date().getTime() - startDates.poll().getTime()) / 1000; double fullLengthSec = media.getDurationInSeconds(); if (fullLengthSec > 0) { double currentFileViewPercentage = (playLengthSec / fullLengthSec) * 100; // if the watched percentage is bigger than in the viewinfo file, write it to viewinfo if (currentFileViewPercentage > fileViewPercentage) { fileViewPercentage = Math.min(100, currentFileViewPercentage); props.setProperty(infoKey, Integer.toString((int) fileViewPercentage)); try { props.store(new FileOutputStream(infoFile), null); // update the thumbnail media.setThumb(null); InputFile input = new InputFile(); input.setFile(((RealFile) resource).getFile()); media.generateThumbnail(input, resource.getExt(), resource.getType()); } catch (IOException e) { logExeptionError(e); } } } } } @Override public void nowPlaying(DLNAMediaInfo media, DLNAResource resource) { if (enabledMV && resource.getType() == Format.VIDEO) { startDates.add(new Date()); // set the startdate } } @Override public JComponent config() { JPanel configPanel = new JPanel(); cbEnableMV = new JCheckBox("enable mark viewed"); // $NON-NLS-1$ cbEnableMV.setSelected(enabledMV); cbEnableMV.addActionListener(this); configPanel.add(cbEnableMV); return configPanel; } @Override public void shutdown() {} @Override public String name() { return "View Status"; } @Override public void updateThumb(DLNAMediaInfo media, InputFile f) { try { BufferedImage image = ImageIO.read(new ByteArrayInputStream(media.getThumb())); if (image != null) { Graphics g = image.getGraphics(); Path infoFilePath = Paths.get(f.getFile().getPath()); // get path of current file String folderName = infoFilePath.getParent().toString(); // get folder String infoFile = folderName + "/.viewstatus"; // get get infofilename String infoKey = f.getFile().getName(); // get keyname Properties props = new Properties(); try { props.load(new FileInputStream(infoFile)); String viewInfo = ""; String allViewed = props.getProperty("allviewed", "false"); // if allview=true is in the infofile, mark media as viewed if (allViewed.equals("true")) { viewInfo = "viewed"; } else { // get viewing percentage from infofile int fileViewPercentage = Integer.parseInt(props.getProperty(infoKey, "0")); if (fileViewPercentage != 0) { viewInfo = "viewed for " + fileViewPercentage + "%"; } } // if info was set, draw it on the thumbnail if (viewInfo != "") { // draw a senitransparent black bar to increase readability g.setColor(new Color(0, 0, 0, 190)); g.fillRect(0, image.getHeight() - 35, image.getWidth(), 35); // draw info g.setFont(new Font("Arial", Font.PLAIN, 25)); g.setColor(new Color(240, 240, 240)); FontMetrics fm = g.getFontMetrics(); int viewInfoX = (image.getWidth() - fm.stringWidth(viewInfo)) / 2; int viewInfoY = image.getHeight() - 7; g.drawString(viewInfo, viewInfoX, viewInfoY); ByteArrayOutputStream out = new ByteArrayOutputStream(); ImageIO.write(image, "jpeg", out); media.setThumb(out.toByteArray()); } } catch (IOException e) { } } } catch (IOException e) { log.error("Error while updating thumbnail : " + e.getMessage()); } } /** * Log Exceptions by first converting it to string, and than logging that string. * * @param Exeption e */ private void logExeptionError(Exception e) { StringWriter writer = new StringWriter(); e.printStackTrace(new PrintWriter(writer)); log.error(writer.toString()); } @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == cbEnableMV) { enabledMV = cbEnableMV.isSelected(); PMSConf.setCustomProperty("enableViewStatus", enabledMV); } } }