diff options
| -rw-r--r-- | htroot/env/base.css | 16 | ||||
| -rw-r--r-- | htroot/js/yacysearchAudio.js | 123 | ||||
| -rw-r--r-- | htroot/yacysearch.html | 7 | ||||
| -rw-r--r-- | htroot/yacysearchitem.html | 49 | ||||
| -rw-r--r-- | htroot/yacysearchitem.java | 141 |
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. |
