summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--htroot/env/base.css16
-rw-r--r--htroot/js/yacysearchAudio.js123
-rw-r--r--htroot/yacysearch.html7
-rw-r--r--htroot/yacysearchitem.html49
-rw-r--r--htroot/yacysearchitem.java141
5 files changed, 306 insertions, 30 deletions
diff --git a/htroot/env/base.css b/htroot/env/base.css
index 3cd0e750e..fd11056e1 100644
--- a/htroot/env/base.css
+++ b/htroot/env/base.css
@@ -1110,6 +1110,22 @@ div#tagcloud {
color: #ff8c00;
}
+.embeddedAudios {
+ /* Remove bootstrap ul bottom margin so that there is no interval with the expandable list */
+ margin-bottom: 0px;
+}
+
+/* Button to expand/collapse audio results beyond the initial number of elements display limit */
+.expandAudiosBtn[aria-expanded="true"] .glyphicon:before {
+ /* Repeated same char as in the glyphicon-chevron-left class */
+ content: "\e079\e079";
+}
+
+.expandAudiosBtn[aria-expanded="false"] .glyphicon:before {
+ /* Repeated same char as in the glyphicon-chevron-right class */
+ content: "\e080\e080";
+}
+
/******* yacysearch.html end ***********/
/******* yacysearchtrailer.html **********
diff --git a/htroot/js/yacysearchAudio.js b/htroot/js/yacysearchAudio.js
index 3d04cd663..123eed929 100644
--- a/htroot/js/yacysearchAudio.js
+++ b/htroot/js/yacysearchAudio.js
@@ -20,6 +20,24 @@
/* Functions dedicated to control playing of YaCy audio search results */
/**
+ * Show elements that are only useful when JavaScript is enabled
+ */
+function showJSAudioControls() {
+ var audioElems = document.getElementsByTagName("audio");
+ var audioControls = document.getElementById("audioControls");
+ if(audioElems != null && audioElems.length > 0 && audioControls != null && audioControls.className.indexOf("hidden") >= 0) {
+ audioControls.className = audioControls.className.replace("hidden", "");
+ }
+
+ var expandAudioButtons = document.getElementsByClassName("expandAudiosBtn");
+ if(expandAudioButtons != null) {
+ for(var i = 0; i < expandAudioButtons.length; i++) {
+ expandAudioButtons[i].className = expandAudioButtons[i].className.replace("hidden", "");
+ }
+ }
+}
+
+/**
* Handle embedded audio result load error.
*
* @param event
@@ -28,11 +46,15 @@
function handleAudioLoadError(event) {
if (event != null && event.target != null) {
/* Fill the title attribute to provide some feedback about the error without need for looking at the console */
+ var titleAddition;
if (event.target.error != null && event.target.error.message) {
- event.target.title = "Cannot play ("
+ titleAddition = " - Cannot play ("
+ event.target.error.message + ")";
} else {
- event.target.title = "Cannot play";
+ titleAddition = " - Cannot play";
+ }
+ if(event.target.title == null || event.target.title.indexOf(titleAddition) < 0) {
+ event.target.title += titleAddition;
}
/* Apply CSS class marking error for visual feedback*/
@@ -165,7 +187,7 @@ function playAllNext(playerElem) {
while (nextTrack < audioElems.length) {
var audioElem = audioElems[nextTrack];
if (audioElem != null && audioElem.error == null
- && audioElem.play) {
+ && audioElem.play && audioElem.className.indexOf("hidden") < 0) {
if(audioElem.currentTime > 0) {
audioElem.currentTime = 0;
}
@@ -177,8 +199,23 @@ function playAllNext(playerElem) {
/* Go to the next element when not playable */
nextTrack++;
}
+ if(nextTrack >= audioElems.length) {
+ /* No other result to play */
+ if (currentTrack >= 0 && currentTrack < audioElems.length
+ && !audioElems[currentTrack].paused
+ && audioElems[currentTrack].pause) {
+ audioElems[currentTrack].pause();
+ }
+ playerElem.setAttribute("data-current-track", -1);
+ updatePlayAllButton(false);
+ }
} else {
/* No other result to play */
+ if (currentTrack >= 0 && currentTrack < audioElems.length
+ && !audioElems[currentTrack].paused
+ && audioElems[currentTrack].pause) {
+ audioElems[currentTrack].pause();
+ }
playerElem.setAttribute("data-current-track", -1);
updatePlayAllButton(false);
}
@@ -224,7 +261,7 @@ function handlePlayAllBtnClick() {
var audioElems = document.getElementsByTagName("audio");
var playerElem = document.getElementById("audioControls");
var playAllIcon = document.getElementById("playAllIcon");
- if (playerElem != null && audioElems != null) {
+ if (playerElem != null && audioElems != null && playAllIcon != null) {
if (playAllIcon.className == "glyphicon glyphicon-play" && audioElems.length > 0) {
var currentTrack = parseInt(playerElem
.getAttribute("data-current-track"));
@@ -243,7 +280,7 @@ function handlePlayAllBtnClick() {
while (currentTrack < audioElems.length) {
var currentAudioElem = audioElems[currentTrack];
if (currentAudioElem != null && currentAudioElem.error == null
- && currentAudioElem.play) {
+ && currentAudioElem.play && currentAudioElem.className.indexOf("hidden") < 0) {
currentAudioElem.play();
updatePlayAllButton(true);
break;
@@ -291,4 +328,80 @@ function handleStopAllBtnClick() {
if (playerElem != null) {
playerElem.setAttribute("data-current-track", -1);
}
+}
+
+/**
+ * Toggle visibility on a list of audio elements beyond the initial limit of elements to display.
+ * @param {HTMLButtonElement} button the button used to expand the audio elements
+ * @param {String} expandableAudiosId the id of the container of audio elements which visibility has to be toggled
+ * @param {String} hiddenCountId the id of the element containing the number of hidden elements when expandable audios are collapsed
+ * @param {String} evenMoreCountId the id of the eventual element containing the number of hidden elements remaining when audios are expanded
+ */
+function toggleExpandableAudios(button, expandableAudiosId, hiddenCountId, evenMoreCountId) {
+ var expandableAudiosContainer = document.getElementById(expandableAudiosId);
+ var hiddenCountElem = document.getElementById(hiddenCountId);
+ var evenMoreElem = document.getElementById(evenMoreCountId);
+ if(button != null && expandableAudiosContainer != null) {
+ var childrenAudioElems = expandableAudiosContainer.getElementsByTagName("audio");
+ var playerElem = document.getElementById("audioControls");
+ var playAllIcon = document.getElementById("playAllIcon");
+ var currentPlayAllTrack = playerElem != null ? parseInt(playerElem
+ .getAttribute("data-current-track")) : -1;
+ if(button.getAttribute("aria-expanded") == "true") {
+ var currentPlayAllAudioElem = null;
+ if(!isNaN(currentPlayAllTrack) && currentPlayAllTrack >= 0) {
+ /* Currently playing all audio results */
+ var audioElems = document.getElementsByTagName("audio");
+ if(audioElems != null && audioElems.length > currentPlayAllTrack) {
+ currentPlayAllAudioElem = audioElems[currentPlayAllTrack];
+ }
+ }
+ /* Additionnaly we modify the aria-expanded state for improved accessiblity */
+ button.setAttribute("aria-expanded", "false");
+ button.title = button.getAttribute("data-title-collapsed");
+ expandableAudiosContainer.className += " hidden";
+ if(hiddenCountElem != null) {
+ hiddenCountElem.className = hiddenCountElem.className.replace("hidden", "");
+ }
+ if(evenMoreElem != null) {
+ evenMoreElem.className += " hidden";
+ }
+ var hidingPlayAll = false;
+ for(var i = 0; i < childrenAudioElems.length; i++) {
+ var audioElem = childrenAudioElems[i];
+ if(currentPlayAllAudioElem == audioElem) {
+ /* Playing all results, and the currently playing element will be hidden*/
+ hidingPlayAll = true;
+ } else if (audioElem.pause && !audioElem.paused) {
+ /* Pause this as it will be hidden */
+ audioElem.pause();
+ }
+ audioElem.className += " hidden";
+ }
+ if(hidingPlayAll) {
+ if (playAllIcon.className == "glyphicon glyphicon-play") {
+ /* Stop playing all */
+ updatePlayAllButton(false);
+ playerElem.setAttribute("data-current-track", -1);
+ } else {
+ /* Continue playing all to an element that is not hidden */
+ playAllNext(playerElem);
+ }
+ }
+ } else {
+ /* Additionnaly we modify the aria-expanded state for improved accessiblity */
+ button.setAttribute("aria-expanded", "true");
+ button.title = button.getAttribute("data-title-expanded");
+ expandableAudiosContainer.className = expandableAudiosContainer.className.replace("hidden", "");
+ if(hiddenCountElem != null) {
+ hiddenCountElem.className += " hidden";
+ }
+ if(evenMoreElem != null) {
+ evenMoreElem.className = evenMoreElem.className.replace("hidden", "");
+ }
+ for(var j = 0; j < childrenAudioElems.length; j++) {
+ childrenAudioElems[j].className = childrenAudioElems[j].className.replace("hidden", "");
+ }
+ }
+ }
} \ No newline at end of file
diff --git a/htroot/yacysearch.html b/htroot/yacysearch.html
index a31a11d11..824d4d23d 100644
--- a/htroot/yacysearch.html
+++ b/htroot/yacysearch.html
@@ -29,12 +29,7 @@
#(resultTable)#::::
#(embed)#::
- /* Show the overall audio play control buttons only when JavaScript is enabled as they are useless without it */
- var audioElems = document.getElementsByTagName("audio");
- var audioControls = document.getElementById("audioControls");
- if(audioElems != null && audioElems.length > 0 && audioControls != null && audioControls.className.indexOf("hidden") >= 0) {
- audioControls.className = audioControls.className.replace("hidden", "");
- }
+ showJSAudioControls();
#(/embed)#
#(/resultTable)#
diff --git a/htroot/yacysearchitem.html b/htroot/yacysearchitem.html
index 5760aaaa7..fc647aa99 100644
--- a/htroot/yacysearchitem.html
+++ b/htroot/yacysearchitem.html
@@ -65,13 +65,48 @@
<tr class="#(col)#TableCellLight::TableCellDark#(/col)#">
<td>#[name]#</td>
<td><a href="#[href]#" target="#[target]#" #(noreferrer)#::rel="noreferrer"#(/noreferrer)#>#[hrefshort]#</a>
- <td class="text-center">#(embed)#::<audio src="#[href]#"
- preload="none" controls="controls"
- onplaying="handleAudioPlaying(event)"
- onpause="handleAudioPause(event)"
- onerror="handleAudioLoadError(event)"
- onended="handleAudioEnded(event)">Not supported
- </audio>#(/embed)#
+ <td class="text-center">#(embed)#::#(list)#::<ul class="embeddedAudios list-unstyled">#(/list)#
+ #{audioSources}#
+ #(list)#::<li>#(/list)#
+ <audio src="#[href]#"
+ title="#[title]#"
+ preload="none" controls="controls"
+ onplaying="handleAudioPlaying(event)"
+ onpause="handleAudioPause(event)"
+ onerror="handleAudioLoadError(event)"
+ onended="handleAudioEnded(event)">Not supported
+ </audio>
+ #(list)#::</li>#(/list)#
+ #{/audioSources}#
+ #(list)#::</ul>#(/list)#
+ #(moreAudios)#::
+ <ul id="expandableAudios_#[urlhash]#" class="list-unstyled hidden">
+ #{audioSources}#<li>
+ <audio src="#[href]#"
+ title="#[title]#"
+ class="hidden"
+ preload="none" controls="controls"
+ onplaying="handleAudioPlaying(event)"
+ onpause="handleAudioPause(event)"
+ onerror="handleAudioLoadError(event)"
+ onended="handleAudioEnded(event)">Not supported
+ </audio></li>#{/audioSources}#
+ </ul>
+ <ul class="list-unstyled">
+ <li>
+ <span id="hiddenCount_#[urlhash]#">#[hiddenCount]# more...</span>
+ <button class="expandAudiosBtn btn btn-default btn-xs hidden" type="button"
+ aria-controls="expandableAudios_#[urlhash]#" aria-expanded="false"
+ title="Show #[expandableCount]# more"
+ data-title-collapsed="Show #[expandableCount]# more"
+ data-title-expanded="Show only the first #[firstLimit]#"
+ onclick="toggleExpandableAudios(this, 'expandableAudios_#[urlhash]#', 'hiddenCount_#[urlhash]#', 'evenMoreCount_#[urlhash]#')">
+ <span class="glyphicon"></span>
+ </button>
+ #(evenMore)#<span id="evenMoreCount_#[urlhash]#" class="hidden">#[count]# more...</span>#(/evenMore)#
+ </li>
+ </ul>
+ #(/moreAudios)##(/embed)#
</td>
</tr>#(/item)#
::
diff --git a/htroot/yacysearchitem.java b/htroot/yacysearchitem.java
index ec0c082a5..fdb1fffe1 100644
--- a/htroot/yacysearchitem.java
+++ b/htroot/yacysearchitem.java
@@ -31,10 +31,13 @@ import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
+import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.Locale;
+import java.util.Set;
import java.util.StringTokenizer;
+import java.util.TreeSet;
import net.yacy.cora.date.GenericFormatter;
import net.yacy.cora.date.ISO8601Formatter;
@@ -461,18 +464,12 @@ public class yacysearchitem {
final String resultUrlstring = ms.url().toNormalform(true);
final String target = sb.getConfig(resultUrlstring.matches(target_special_pattern) ? SwitchboardConstants.SEARCH_TARGET_SPECIAL : SwitchboardConstants.SEARCH_TARGET_DEFAULT, "_self");
prop.putHTML("content_item_href", resultUrlstring);
- final String mediaType = ms.mime();
- if (extendedSearchRights && mediaType != null && mediaType.startsWith("audio/")) {
- /*
- * Display HTML5 embedded audio only :
- * - when content-type is known to be audio (each browser has its own set of supported audio subtypes,
- * so the browser will then handle itself eventual report about unsupported media format)
- * - to authenticated users with extended search rights to prevent any media redistribution issue
- */
- prop.put("content_item_embed", true);
- prop.putHTML("content_item_embed_href", resultUrlstring);
- prop.putHTML("content_item_embed_mediaType", mediaType);
- } else {
+ if(theSearch.query.contentdom == ContentDomain.AUDIO && extendedSearchRights) {
+ /*
+ * Display HTML5 embedded audio only to authenticated users with extended search rights to prevent any media redistribution issue
+ */
+ processEmbedAudio(prop, theSearch, ms);
+ }else {
prop.put("content_item_embed", false);
}
prop.put("content_item_noreferrer", noreferrer ? 1 : 0);
@@ -491,6 +488,126 @@ public class yacysearchitem {
}
/**
+ *
+ * @param prop the target properties
+ * @param theSearch the search event
+ * @param result a result entry
+ */
+ private static void processEmbedAudio(final serverObjects prop, final SearchEvent theSearch,
+ final URIMetadataNode result) {
+ final String mediaType = result.mime();
+
+ if (mediaType != null && mediaType.startsWith("audio/")) {
+ /*
+ * content-type is known to be audio : each browser has its own set of supported
+ * audio subtypes, so the browser will then handle itself eventual report about
+ * unsupported media format
+ */
+ prop.put("content_item_embed", true);
+ prop.put("content_item_embed_list", false);
+ prop.put("content_item_embed_audioSources", 1);
+ appendEmbeddedAudio(result, result.url(), prop, "content_item_embed_audioSources_0");
+ prop.put("content_item_embed_audioSources_0_list", false);
+ } else if (result.laudio() > 0 && !theSearch.query.isStrictContentDom()) {
+ /*
+ * The result media type is not audio, but there are some links to audio
+ * resources : render a limited list of embedded audio elements
+ */
+ final TreeSet<MultiProtocolURL> audioLinks = new TreeSet<>(
+ Comparator.comparing(MultiProtocolURL::getHost).thenComparing(MultiProtocolURL::getFile));
+ final int firstAudioLinksLimit = 3;
+ final int secondAudioLinksLimit = 50;
+
+ filterAudioLinks(URIMetadataNode.getLinks(result, false), audioLinks, result.laudio());
+ filterAudioLinks(URIMetadataNode.getLinks(result, true), audioLinks, result.laudio());
+
+ if (!audioLinks.isEmpty()) {
+ prop.put("content_item_embed", true);
+ final boolean hasMoreThanOne = audioLinks.size() > 1;
+ prop.put("content_item_embed_list", hasMoreThanOne);
+ prop.put("content_item_embed_audioSources", Math.min(audioLinks.size(), firstAudioLinksLimit));
+ final Iterator<MultiProtocolURL> linksIter = audioLinks.iterator();
+ for (int i = 0; linksIter.hasNext() && i < firstAudioLinksLimit; i++) {
+ appendEmbeddedAudio(result, linksIter.next(), prop, "content_item_embed_audioSources_" + i);
+ prop.put("content_item_embed_audioSources_" + i + "_list", hasMoreThanOne);
+ }
+ if (audioLinks.size() > firstAudioLinksLimit) {
+ prop.put("content_item_embed_moreAudios", true);
+ prop.put("content_item_embed_moreAudios_firstLimit", firstAudioLinksLimit);
+ prop.put("content_item_embed_moreAudios_hiddenCount",
+ String.valueOf(audioLinks.size() - firstAudioLinksLimit));
+ prop.put("content_item_embed_moreAudios_expandableCount",
+ String.valueOf(Math.min(audioLinks.size(), secondAudioLinksLimit) - firstAudioLinksLimit));
+ prop.put("content_item_embed_moreAudios_urlhash", ASCII.String(result.hash()));
+
+ prop.put("content_item_embed_moreAudios_audioSources",
+ Math.min(audioLinks.size(), secondAudioLinksLimit) - firstAudioLinksLimit);
+ for (int i = 0; linksIter.hasNext() && i < (secondAudioLinksLimit - firstAudioLinksLimit); i++) {
+ appendEmbeddedAudio(result, linksIter.next(), prop,
+ "content_item_embed_moreAudios_audioSources_" + i);
+ }
+ } else {
+ prop.put("content_item_embed_moreAudios", false);
+ }
+ prop.put("content_item_embed_moreAudios_evenMore", audioLinks.size() > secondAudioLinksLimit);
+ if (audioLinks.size() > secondAudioLinksLimit) {
+ prop.put("content_item_embed_moreAudios_evenMore_count",
+ String.valueOf(audioLinks.size() - secondAudioLinksLimit));
+ prop.put("content_item_embed_moreAudios_evenMore_urlhash", ASCII.String(result.hash()));
+ }
+ } else {
+ prop.put("content_item_embed", false);
+ }
+ }
+ }
+
+ /**
+ * Write the properties of an embedded audio element to prop. All parameters must not be null.
+ * @param mainResult the result entry to which the audio link belongs
+ * @param audioLink an audio link URL
+ * @param prop the target properties
+ * @param propPrefix the prefix to use when appending prop
+ */
+ private static void appendEmbeddedAudio(final URIMetadataNode mainResult,
+ final MultiProtocolURL audioLink, final serverObjects prop, final String propPrefix) {
+ prop.putHTML(propPrefix + "_href", audioLink.toString());
+
+ /* Add a title to help user distinguish embedded elements of the list */
+ final String title;
+ if(audioLink.getHost().equals(mainResult.url().getHost())) {
+ /* Inbound link : the file name is sufficient */
+ title = shorten(audioLink.getFileName(), MAX_NAME_LENGTH);
+ } else {
+ /* Outbound link : it may help to know where the file is hosted without having to inspect the html element */
+ title = nxTools.shortenURLString(audioLink.toString(), MAX_URL_LENGTH);
+ }
+ prop.putHTML(propPrefix+ "_title", title);
+ }
+
+ /**
+ * Add to the target set, valid URLs from the iterator that are classified as
+ * audio from their file name extension.
+ *
+ * @param linksIter an iterator on URL strings
+ * @param target the target set to fill
+ * @param targetMaxSize the maximum target set size
+ */
+ protected static void filterAudioLinks(final Iterator<String> linksIter, final Set<MultiProtocolURL> target,
+ final int targetMaxSize) {
+ while (linksIter.hasNext() && target.size() < targetMaxSize) {
+ final String linkStr = linksIter.next();
+ try {
+ final MultiProtocolURL url = new MultiProtocolURL(linkStr);
+ if (Classification.isAudioExtension(MultiProtocolURL.getFileExtension(url.getFileName()))) {
+ target.add(url);
+ }
+ } catch (final MalformedURLException ignored) {
+ /* Continue to next link */
+ }
+ }
+ }
+
+ /**
* Tries to retrieve favicon url from solr result document, or generates
* default favicon URL (i.e. "http://host/favicon.ico") from resultURL and
* port.