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 = "";