// ==UserScript==
// @name Ö1 – Simple <audio> Stream and Download
// @description Sendungen auf Ö1 schnell und einfach anhören und herunterladen
// @namespace https://xmine127.tk/gm/
// @include *://oe1.orf.at/*
// @include *://loopstream01.apa.at/*#DOWNLOAD=*
// @version 1.1.0
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
function $A(collection) {
return Array.prototype.slice.call(collection, 0);
};
function create_download_link(attributes) {
var DOMDownloadLink = document.createElement("a");
for(var attribute in attributes) {
if(attributes.hasOwnProperty(attribute)) {
DOMDownloadLink[attribute] = attributes[attribute];
}
}
DOMDownloadLink.className = "userscript-audio-download";
DOMDownloadLink.appendChild(document.createTextNode("Download"));
return DOMDownloadLink;
}
if(window.location.hash.startsWith("#DOWNLOAD=")) {
// Download link iframe page
var downloadAttributes = JSON.parse(decodeURIComponent(window.location.hash.substr(10)));
document.addEventListener("DOMContentLoaded", function()
{
// Inject style information
inject_content();
// Mark this page's content as not-important
document.documentElement.className = "download-proxy";
// Add the (important) download link
document.body.appendChild(create_download_link(downloadAttributes));
}, true);
return;
}
document.addEventListener("DOMContentLoaded", function()
{
// Inject extra page nodes and styles
inject_content();
// Detect if page URL inherently points to some stream
var pageTrackID = window.location.pathname.match(/^\/programm\/(\d+).*$/);
if(pageTrackID instanceof Array) {
pageTrackID = parseInt(pageTrackID[1]);
}
// Define replacement function for stream window launcher
var konsole_orig = unsafeWindow.konsole;
var konsole = exportFunction(function(url, trackRestore) {
var DOMPlayButton;
var trackID;
if(pageTrackID) {
// Find play area on program pages
DOMPlayButton = document.querySelector(".galleryitem > .overlay-7tage");
// Assume that the page's inherent track should be played
// (although parsing the requested URL would work too)
trackID = pageTrackID;
} else {
// Guess the play button used to play the stream based on the received URL
DOMPlayButton = document.querySelector(".has-7tage > a[href='" + url.replace("'", "\\'") + "']");
// Determine track ID from URL requested by the caller
var URLQuery = url.split("#")[0].split("?")[1];
if(URLQuery) {
var URLOptions = URLQuery.split("&");
for(var i = 0; i < URLOptions.length; i++) {
var URLOption = URLOptions[i];
if(URLOption.substr(0, 9) == "track_id=") {
trackID = parseInt(URLOption.substr(9));
break;
}
};
}
}
// Delegate to original handler if the target stream container could not be determined
if(!trackID || !DOMPlayButton) {
console.log("UserScript: Failed to determine stream container or track ID for URL: " + url);
return konsole_orig(url);
}
var DOMStreamContainer = DOMPlayButton.parentNode.parentNode;
// Generate title string from stream information
var DOMStreamTitle = DOMStreamContainer.querySelector(".textbox > h3");
var DOMStreamDate = DOMStreamContainer.querySelector(".textbox > .datum");
var title = "";
if(DOMStreamTitle) {
title += DOMStreamTitle.textContent.trim();
if(title.substr(title.length - 1) == "*") {
title = title.substr(0, title.length - 1).trim();
}
}
if(DOMStreamDate) {
title += (title != "") ? " vom " : "";
title += DOMStreamDate.textContent.split("|")[1].trim();
}
// Hide "Play episode" button(s)
$A(DOMPlayButton.parentNode.querySelectorAll(".overlay-playbutton")).forEach(function(DOMPlayButton) {
DOMPlayButton.style.display = "none";
});
// Find or add main container below container with information text
var DOMStreamController = DOMStreamContainer.getElementsByClassName("userscript-audio-controller")[0];
if(!DOMStreamController) {
DOMStreamController = document.createElement("div");
DOMStreamController.className = "userscript-audio-controller";
DOMStreamContainer.appendChild(DOMStreamController);
}
// Show progress spinner in controller area
var DOMLoadingSpinner = document.getElementById("fountainG");
DOMLoadingSpinner.style.display = "block";
DOMStreamController.appendChild(DOMLoadingSpinner);
// Download playlist file for stream
var XHRPlaylist = new XMLHttpRequest();
XHRPlaylist.addEventListener("load", function() {
if(XHRPlaylist.status != 200 || !XHRPlaylist.responseXML) {
var status = XHRPlaylist.status + " " + XHRPlaylist.statusText;
console.log("UserScript: Could not retrieve playlist file: " + status);
return konsole_orig(url);
}
// Create list of audio tracks in stream playlist
var playlist = [];
$A(XHRPlaylist.responseXML.querySelectorAll("playlist > track")).forEach(function(XMLTrack) {
var url = XMLTrack.getAttribute("url");
if(url) {
playlist.push(url);
}
});
// Create <audio> tag (with UI) for episode
var DOMAudioContainer = document.createElement("div");
DOMAudioContainer.className = "userscript-audio-container";
DOMStreamController.appendChild(DOMAudioContainer);
var DOMAudio = document.createElement("audio");
DOMAudio.controls = true;
DOMAudio.preload = "auto";
DOMAudio.src = playlist[0];
DOMAudioContainer.appendChild(DOMAudio);
// Create download link
var downloadAttributes = {
href: playlist[0],
download: title,
target: "_new",
type: "audio/mpeg"
};
if(navigator.product == "Gecko") {
// Create <iframe> that contains download link on <audio> domain (thanks Mozilla...)
var DOMDownloadFrame = document.createElement("iframe");
DOMDownloadFrame.className = "userscript-audio-download";
DOMDownloadFrame.src = "//loopstream01.apa.at/welcome.aspx#DOWNLOAD=" + JSON.stringify(downloadAttributes);
DOMStreamController.appendChild(DOMDownloadFrame);
} else {
// Other browser either support the "download" attribute – or they don't…
// The one's that don't, won't support streaming the content and will therefor offer it for download
// regardless... :-)
DOMStreamController.appendChild(create_download_link(downloadAttributes));
}
// Hide progress spinner
DOMLoadingSpinner.style.display = "none";
// Track current playback position
// (so that playback can be continued after page navigation)
function setPlaybackHandler() {
// Do not update state while stream hasn't initialized yet
if(DOMAudio.readyState < 2) {
return;
}
// Store state information for this stream
try {
var storage = window.localStorage;
var keyName = "userscript" + "." + trackID;
if(!DOMAudio.ended) {
storage.setItem(keyName, JSON.stringify({
playing: !DOMAudio.paused,
currentTime: DOMAudio.currentTime
}));
} else {
storage.removeItem(keyName);
}
} catch(e) { console.log(e); /* Handle full storage gracefully */ }
}
DOMAudio.addEventListener("play", setPlaybackHandler);
DOMAudio.addEventListener("playing", setPlaybackHandler);
DOMAudio.addEventListener("seeked", setPlaybackHandler);
DOMAudio.addEventListener("timeupdate", setPlaybackHandler);
DOMAudio.addEventListener("pause", setPlaybackHandler);
DOMAudio.addEventListener("ended", setPlaybackHandler);
// Seek to requested position (once the stream has initialized)
DOMAudio.addEventListener("loadedmetadata", function() {
// Read shared playback information
var state = {};
try {
var storage = window.localStorage;
var keyName = "userscript" + "." + trackID;
state = JSON.parse(storage.getItem(keyName));
} catch(e) { console.log(e); /* Handle failed JSON.parse() gracefully */ }
if(typeof(state.currentTime) === "number") {
DOMAudio.currentTime = state.currentTime;
}
if(state.playing !== true && trackRestore) {
DOMAudio.pause();
}
});
// Start playback
DOMAudio.play();
});
XHRPlaylist.open("GET", "http://oe1.orf.at/programm/" + trackID + "/playlist", true);
XHRPlaylist.send();
}, unsafeWindow);
// Replace page read-only stream window launcher function
Object.defineProperty(unsafeWindow, "konsole", {
value: konsole
});
// Auto-start playback, if the user was previously playing the stream of the current page
if(pageTrackID) {
konsole(null, true);
}
});
function inject_content()
{
// Inject extra HTML tags for loading indicator
var DOMLoadingSpinner = document.createElement("div");
DOMLoadingSpinner.id = "fountainG";
DOMLoadingSpinner.style.display = "none";
for(var i = 1; i <= 8; i++) {
var DOMLoadingItem = document.createElement("div");
DOMLoadingItem.id = "fountainG_" + i;
DOMLoadingItem.className = "fountainG";
DOMLoadingSpinner.appendChild(DOMLoadingItem);
}
document.body.appendChild(DOMLoadingSpinner);
// Inject extra page styles for loading indicator
var DOMStylesheet = document.createElement("style");
DOMStylesheet.type = "text/css";
DOMStylesheet.innerHTML = (function () {/*
html.download-proxy,
html.download-proxy > body {
margin: 0 !important;
padding: 0 !important;
height: 100%;
}
html.download-proxy > body > * {
display: none !important;
}
html.download-proxy > body > .userscript-audio-download {
display: block !important;
}
.userscript-audio-controller {
display: inline-block;
margin-top: 0.5em;
margin-left: 196px;
height: 32px;
}
.userscript-audio-download {
text-indent: -9999px;
width: 64px;
background: #999494 url("//oe1.orf.at/static/img/ico-tile.png") repeat scroll -100px -1372px;
}
.userscript-audio-container {
background-color: #5C5959;
padding-right: 0.5em;
}
.userscript-audio-container,
.userscript-audio-download {
display: inline-block;
vertical-align: top;
height: 100%;
}
.userscript-audio-controller audio {
background-color: white;
}
.overlay-download-liste,
.galleryitem > .hover-infobar,
.galleryitem > .overlay-download {
display: none !important;
}
.gallery > .userscript-audio-container {
margin-top: 0;
}
#fountainG{
position:relative;
width:84px;
height:10px;
margin:auto;
}
.fountainG{
position:absolute;
top:0;
background-color:rgb(0,0,0);
width:10px;
height:10px;
animation-name:bounce_fountainG;
-o-animation-name:bounce_fountainG;
-ms-animation-name:bounce_fountainG;
-webkit-animation-name:bounce_fountainG;
-moz-animation-name:bounce_fountainG;
animation-duration:1.5s;
-o-animation-duration:1.5s;
-ms-animation-duration:1.5s;
-webkit-animation-duration:1.5s;
-moz-animation-duration:1.5s;
animation-iteration-count:infinite;
-o-animation-iteration-count:infinite;
-ms-animation-iteration-count:infinite;
-webkit-animation-iteration-count:infinite;
-moz-animation-iteration-count:infinite;
animation-direction:normal;
-o-animation-direction:normal;
-ms-animation-direction:normal;
-webkit-animation-direction:normal;
-moz-animation-direction:normal;
transform:scale(.3);
-o-transform:scale(.3);
-ms-transform:scale(.3);
-webkit-transform:scale(.3);
-moz-transform:scale(.3);
border-radius:7px;
-o-border-radius:7px;
-ms-border-radius:7px;
-webkit-border-radius:7px;
-moz-border-radius:7px;
}
#fountainG_1{
left:0;
animation-delay:0.6s;
-o-animation-delay:0.6s;
-ms-animation-delay:0.6s;
-webkit-animation-delay:0.6s;
-moz-animation-delay:0.6s;
}
#fountainG_2{
left:10px;
animation-delay:0.75s;
-o-animation-delay:0.75s;
-ms-animation-delay:0.75s;
-webkit-animation-delay:0.75s;
-moz-animation-delay:0.75s;
}
#fountainG_3{
left:21px;
animation-delay:0.9s;
-o-animation-delay:0.9s;
-ms-animation-delay:0.9s;
-webkit-animation-delay:0.9s;
-moz-animation-delay:0.9s;
}
#fountainG_4{
left:31px;
animation-delay:1.05s;
-o-animation-delay:1.05s;
-ms-animation-delay:1.05s;
-webkit-animation-delay:1.05s;
-moz-animation-delay:1.05s;
}
#fountainG_5{
left:42px;
animation-delay:1.2s;
-o-animation-delay:1.2s;
-ms-animation-delay:1.2s;
-webkit-animation-delay:1.2s;
-moz-animation-delay:1.2s;
}
#fountainG_6{
left:52px;
animation-delay:1.35s;
-o-animation-delay:1.35s;
-ms-animation-delay:1.35s;
-webkit-animation-delay:1.35s;
-moz-animation-delay:1.35s;
}
#fountainG_7{
left:63px;
animation-delay:1.5s;
-o-animation-delay:1.5s;
-ms-animation-delay:1.5s;
-webkit-animation-delay:1.5s;
-moz-animation-delay:1.5s;
}
#fountainG_8{
left:73px;
animation-delay:1.64s;
-o-animation-delay:1.64s;
-ms-animation-delay:1.64s;
-webkit-animation-delay:1.64s;
-moz-animation-delay:1.64s;
}
@keyframes bounce_fountainG{
0%{
transform:scale(1);
background-color:rgb(0,0,0);
}
100%{
transform:scale(.3);
background-color:rgb(255,255,255);
}
}
@-o-keyframes bounce_fountainG{
0%{
-o-transform:scale(1);
background-color:rgb(0,0,0);
}
100%{
-o-transform:scale(.3);
background-color:rgb(255,255,255);
}
}
@-ms-keyframes bounce_fountainG{
0%{
-ms-transform:scale(1);
background-color:rgb(0,0,0);
}
100%{
-ms-transform:scale(.3);
background-color:rgb(255,255,255);
}
}
@-webkit-keyframes bounce_fountainG{
0%{
-webkit-transform:scale(1);
background-color:rgb(0,0,0);
}
100%{
-webkit-transform:scale(.3);
background-color:rgb(255,255,255);
}
}
@-moz-keyframes bounce_fountainG{
0%{
-moz-transform:scale(1);
background-color:rgb(0,0,0);
}
100%{
-moz-transform:scale(.3);
background-color:rgb(255,255,255);
}
}
*/}).toString().match(/[^]*\/\*([^]*)\*\/\}$/)[1];
document.head.appendChild(DOMStylesheet);
}