From e8bf2ee30d9bc9ea12c3b8d402ca013b48e824e8 Mon Sep 17 00:00:00 2001 From: zzz Date: Tue, 12 Nov 2019 12:20:19 +0000 Subject: [PATCH] i2psnark: Audio playlist support Add HTML5 player for single-file torrents --- .../org/klomp/snark/web/I2PSnarkServlet.java | 184 +++++++++++++++++- history.txt | 13 ++ .../src/net/i2p/router/RouterVersion.java | 2 +- 3 files changed, 197 insertions(+), 2 deletions(-) diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 5ee217b06e..543a10751c 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -235,6 +235,18 @@ public class I2PSnarkServlet extends BasicServlet { File resource = getResource(pathInContext); if (resource == null) { resp.sendError(404); + } else if (req.getParameter("playlist") != null) { + String base = addPaths(req.getRequestURI(), "/"); + String listing = getPlaylist(req.getRequestURL().toString(), base, req.getParameter("sort")); + if (listing != null) { + setHTMLHeaders(resp); + // TODO custom name + resp.setContentType("audio/mpegurl; charset=UTF-8; name=\"playlist.m3u\""); + resp.addHeader("Content-Disposition", "attachment; filename=\"playlist.m3u\""); + resp.getWriter().write(listing); + } else { + resp.sendError(404); + } } else { String base = addPaths(req.getRequestURI(), "/"); String listing = getListHTML(resource, base, true, method.equals("POST") ? req.getParameterMap() : null, @@ -3333,6 +3345,26 @@ public class I2PSnarkServlet extends BasicServlet { if (ls == null) { // We are only showing the torrent info section + // unless audio or video... + if (storage != null && storage.complete()) { + String mime = getMimeType(r.getName()); + boolean isAudio = mime != null && (mime.startsWith("audio/") || mime.equals("application/ogg")); + boolean isVideo = mime != null && mime.startsWith("video/"); + if (isAudio || isVideo) { + // HTML5 + if (isAudio) + buf.append(""); + else + buf.append(""); + } + } if (er || ec) displayComments(snark, er, ec, esc, buf); if (includeForm) @@ -3356,7 +3388,7 @@ public class I2PSnarkServlet extends BasicServlet { sort = Integer.parseInt(sortParam); } catch (NumberFormatException nfe) {} } - Collections.sort(fileList, Sorters.getFileComparator(sort, this)); + DataHelper.sort(fileList, Sorters.getFileComparator(sort, this)); } // second table - dir info @@ -3436,6 +3468,16 @@ public class I2PSnarkServlet extends BasicServlet { .append(_t("Up to higher level directory")) .append("\n"); + // playlist button + if (hasCompleteAudio(fileList, storage, remainingArray)) { + buf.append("" + + ""); + buf.append(toImg("music")); + buf.append(' ').append(_t("Audio Playlist")).append("\n"); + } boolean showSaveButton = false; boolean rowEven = true; @@ -3609,6 +3651,146 @@ public class I2PSnarkServlet extends BasicServlet { return buf.toString(); } + /** + * Is there at least one complete audio file in this directory or below? + * Recursive. + * + * @since 0.9.44 + */ + private boolean hasCompleteAudio(List fileList, + Storage storage, long[] remainingArray) { + for (Sorters.FileAndIndex fai : fileList) { + if (fai.isDirectory) { + // recurse + File[] ls = fai.file.listFiles(); + if (ls != null && ls.length > 0) { + List fl2 = new ArrayList(ls.length); + for (int i = 0; i < ls.length; i++) { + fl2.add(new Sorters.FileAndIndex(ls[i], storage, remainingArray)); + } + if (hasCompleteAudio(fl2, storage, remainingArray)) + return true; + } + continue; + } + if (fai.remaining != 0) + continue; + String name = fai.file.getName(); + String mime = getMimeType(name); + if (mime != null && (mime.startsWith("audio/") || mime.equals("application/ogg"))) + return true; + } + return false; + } + + /** + * Get the audio files in the resource list as a m3u playlist. + * https://en.wikipedia.org/wiki/M3U + * + * @param base The encoded base URL + * @param sortParam may be null + * @return String of HTML or null if no files or on error + * @since 0.9.44 + */ + private String getPlaylist(String reqURL, String base, String sortParam) throws IOException { + String decodedBase = decodePath(base); + String title = decodedBase; + String cpath = _contextPath + '/'; + if (title.startsWith(cpath)) + title = title.substring(cpath.length()); + + // Get the snark associated with this directory + String torrentName; + String pathInTorrent; + int slash = title.indexOf('/'); + if (slash > 0) { + torrentName = title.substring(0, slash); + pathInTorrent = title.substring(slash); + } else { + torrentName = title; + pathInTorrent = "/"; + } + Snark snark = _manager.getTorrentByBaseName(torrentName); + if (snark == null) + return null; + Storage storage = snark.getStorage(); + if (storage == null) + return null; + File sbase = storage.getBase(); + File r; + if (pathInTorrent.equals("/")) + r = sbase; + else + r = new File(sbase, pathInTorrent); + if (!r.isDirectory()) + return null; + File[] ls = r.listFiles(); + if (ls == null) + return null; + List fileList = new ArrayList(ls.length); + // precompute remaining for all files for efficiency + long[] remainingArray = (storage != null) ? storage.remaining() : null; + for (int i = 0; i < ls.length; i++) { + fileList.add(new Sorters.FileAndIndex(ls[i], storage, remainingArray)); + } + + boolean showSort = fileList.size() > 1; + int sort = 0; + if (showSort) { + if (sortParam != null) { + try { + sort = Integer.parseInt(sortParam); + } catch (NumberFormatException nfe) {} + } + DataHelper.sort(fileList, Sorters.getFileComparator(sort, this)); + } + StringBuilder buf = new StringBuilder(512); + getPlaylist(buf, fileList, reqURL, sort, storage, remainingArray); + String rv = buf.toString(); + if (rv.length() <= 0) + return null; + return rv; + } + + /** + * Append playlist entries in m3u format to buf. + * Recursive. + * + * @param buf out parameter + * @param reqURL encoded, WITH trailing slash + * @since 0.9.44 + */ + private void getPlaylist(StringBuilder buf, List fileList, + String reqURL, int sort, + Storage storage, long[] remainingArray) { + for (Sorters.FileAndIndex fai : fileList) { + if (fai.isDirectory) { + // recurse + File[] ls = fai.file.listFiles(); + if (ls != null && ls.length > 0) { + List fl2 = new ArrayList(ls.length); + for (int i = 0; i < ls.length; i++) { + fl2.add(new Sorters.FileAndIndex(ls[i], storage, remainingArray)); + } + if (ls.length > 1) + DataHelper.sort(fl2, Sorters.getFileComparator(sort, this)); + String name = fai.file.getName(); + String url2 = reqURL + encodePath(name) + '/'; + getPlaylist(buf, fl2, url2, sort, storage, remainingArray); + } + continue; + } + if (fai.remaining != 0) + continue; + String name = fai.file.getName(); + String mime = getMimeType(name); + if (mime != null && (mime.startsWith("audio/") || mime.equals("application/ogg"))) { + // TODO Extended M3U + buf.append(reqURL).append(encodePath(name)).append('\n'); + } + } + } + /** * @param er ratings enabled globally * @param ec comments enabled globally diff --git a/history.txt b/history.txt index bd530578ca..c60fbfd64f 100644 --- a/history.txt +++ b/history.txt @@ -1,9 +1,22 @@ +2019-11-12 zzz + * i2psnark: Audio playlist support + +2019-11-11 zzz + * KeyGenerator: Use new PrivateKey constructor + * Router: Set default sig type to EdDSA for Android (ticket #2643) + +2019-11-08 zzz + * i2psnark: Add HTML5 players on details page + 2019-11-06 idk * Router: - Use Local Application Data(%LOCALAPPDATA%) instead of Roaming for config * Console: - Change home page organization, headlines, to expose more information +2019-11-05 zzz + * Router: No longer check the clove ID in the Bloom filter + 2019-11-02 zzz * Router: NSR/ES fixes for proposal 144 diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 0725033fa6..f6e6df3a5c 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 3; + public final static long BUILD = 4; /** for example "-test" */ public final static String EXTRA = "";