Created
January 18, 2021 10:08
-
-
Save georgenaranjo96/e2d18697048301e2d10b2b9c551cb296 to your computer and use it in GitHub Desktop.
pip
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Variables used by Scriptable. | |
// These must be at the very top of the file. Do not edit. | |
// icon-color: deep-purple; icon-glyph: desktop; | |
var configuration = args.shortcutParameter | |
await runloop(makeEvents, await main(configuration)) | |
async function main(configuration) { | |
let webView = new WebView() | |
reload(webView) | |
let exit = webView.present() | |
let url = configuration.url | |
let streams = await getStreams(url) | |
let stream = await injectQualityControl(webView, streams, configuration.hd) | |
await injectVideo(webView, stream) | |
await handleStreamType(webView, stream) | |
await handleAutoPipIfNeeded(configuration, webView) | |
await handleCompletionShortcutIfNeeded(configuration, webView) | |
await handlePlaybackSpeedIfNeeded(configuration, webView, stream) | |
return { exit, stream, webView } | |
} | |
async function handleAutoPipIfNeeded(configuration, webView) { | |
if (configuration.pip === true) { | |
webView.evaluateJavaScript(`video.webkitSetPresentationMode("picture-in-picture")`) | |
} | |
} | |
async function handleCompletionShortcutIfNeeded(configuration, webView) { | |
let completion = configuration.completion | |
if (completion && completion.length > 0) { | |
await webView.evaluateJavaScript(`setTimeout(() => { completion(null) }, 1000)`, true) | |
Safari.open(`shortcuts://run-shortcut?name=${encodeURI(completion)}`) | |
} | |
} | |
async function handleStreamType(webView, stream) { | |
let type = stream.type | |
if (type === "live") { | |
await webView.evaluateJavaScript(`document.getElementById("controls").innerHTML = "<h1>Showing live video</h1>"`) | |
} else if (type === "upcoming") { | |
await webView.evaluateJavaScript(`document.getElementById("controls").innerHTML = "<h1>Upcoming video. Check back later</h1>"`) | |
} | |
await webView.evaluateJavaScript(`document.getElementById("controls").style.display = "block"`) | |
} | |
async function handlePlaybackSpeedIfNeeded(configuration, webView, stream) { | |
if (stream.type !== "video") { return } | |
let speed = parseFloat(configuration.speed).toFixed(2).toString() | |
if (speed !== "1.00") { | |
let javaScript = ` | |
let element = document.getElementById('speed${speed}') | |
element.className += " active" | |
document.getElementById('speed1.00').className = document.getElementById('speed1.00').className.replace(' active', '') | |
completion(null) | |
` | |
await webView.evaluateJavaScript(javaScript, true) | |
await handlePlayback(speed, webView, stream) | |
} | |
} | |
function makeEvents(configuration) { | |
return [ | |
configuration.exit.then(() => { | |
return { | |
name: "exit", | |
terminates: true | |
} | |
}), | |
configuration.webView.evaluateJavaScript(injectEvents(), true).then((event) => { | |
if (event === "download") { | |
return { | |
name: event, | |
action: () => { handleDownload(configuration.stream.url) } | |
} | |
} else if (event.startsWith("adjustPlayback")) { | |
let rate = event.match("adjustPlayback(.*)")[1] | |
return { | |
name: event, | |
action: () => { handlePlayback(rate, configuration.webView, configuration.stream) } | |
} | |
} else if (event.startsWith("adjustQuality")) { | |
let id = event.match("adjustQuality(.*)")[1] | |
return { | |
name: event, | |
action: () => { handleQuality(id, configuration.webView) } | |
} | |
} else { | |
console.log("Unknown event " + event) | |
} | |
}) | |
] | |
} | |
async function runloop(makeEvents, configuration) { | |
while (await handle(makeEvents(configuration))) {} | |
configuration.webView.loadHTML("") | |
Script.complete() | |
} | |
async function handle(events) { | |
return await Promise.race(events).then(async (event) => { | |
console.log("handling event " + event.name) | |
if (event.action) { await event.action() } | |
return !(event.terminates) ?? true | |
}) | |
} | |
async function reload(webView) { | |
await webView.loadHTML(getHTML()) | |
} | |
async function injectQualityControl(webView, streams, prefersHD) { | |
var stream | |
if (prefersHD) { | |
stream = streams[0] | |
} else { | |
stream = streams[streams.length - 1] | |
} | |
let buttons = streams.map((stream, index) => { | |
var cls = "button1 qualityButton" | |
if (index === 0 && prefersHD || index === streams.length - 1 && !prefersHD) { | |
cls += " active" | |
} | |
return `<div class='${cls}' id='${stream.id}' data-src='${stream.url}' onclick='adjustQuality(this)'>${stream.name}</div>` | |
}).join("") | |
let javaScript = `document.getElementById("qualityContainer").innerHTML = "${buttons}"` | |
await webView.evaluateJavaScript(javaScript) | |
return stream | |
} | |
async function injectVideo(webView, stream) { | |
if (stream.type === "upcoming") { | |
let javaScript = ` | |
document.getElementById("loadingContainer").style.display = "none" | |
document.getElementById("container").style.display = "block" | |
document.getElementById("videoContainer").innerHTML = "<img src='${stream.thumbnail}'></img>" | |
` | |
await webView.evaluateJavaScript(javaScript) | |
} else { | |
let javaScript = ` | |
document.getElementById("loadingContainer").style.display = "none" | |
document.getElementById("container").style.display = "block" | |
document.getElementById("videoContainer").innerHTML = "<video id='video' controls='controls' class='video-stream' x-webkit-airplay='allow' autoplay playsinline src='${stream.url}'></video>" | |
` | |
await webView.evaluateJavaScript(javaScript) | |
await webView.evaluateJavaScript(getPlayJS(), true) | |
} | |
} | |
async function handleDownload(videoURL) { | |
let alert = new Alert() | |
alert.title = "Select method for downloading video" | |
alert.addAction("Download using Safari") | |
alert.addAction("Using a Shortcut") | |
alert.addAction("Copy URL") | |
alert.addCancelAction("Cancel") | |
let selected = await alert.present() | |
if (selected === 0) { | |
Safari.open(videoURL) | |
} else if (selected === 1) { | |
ShareSheet.present([videoURL]) | |
} else if (selected === 2) { | |
Pasteboard.copyString(videoURL) | |
} | |
} | |
async function handlePlayback(playbackRate, webView, stream) { | |
if (stream.type !== "video") { return Promise.resolve(null) } | |
console.log("adjusting playback " + playbackRate) | |
let javaScript = ` | |
video.playbackRate = ${playbackRate} | |
` | |
await webView.evaluateJavaScript(javaScript) | |
} | |
async function handleQuality(id, webView) { | |
let javaScript = ` | |
rate = video.playbackRate | |
currentTime = video.currentTime | |
video.src = document.getElementById("${id}").getAttribute("data-src") | |
video.onloadedmetadata = function() { | |
video.onloadedmetadata = null | |
video.currentTime = currentTime | |
completion(null) | |
} | |
video.playbackRate = rate | |
` | |
await webView.evaluateJavaScript(javaScript, true) | |
} | |
async function getStreams(url) { | |
var videoId = getParameterByName("v", url) | |
if (videoId === null) { | |
let parts = url.split("/") | |
videoId = parts[parts.length - 1] | |
} | |
let request = new Request(`https://www.youtube.com/get_video_info?video_id=${videoId}&el=detailpage`) | |
let encoded = await request.loadString() | |
let decoded = decodeURIComponent(encoded) | |
let response = decoded.split("&").filter((line) => { return line.startsWith("player_response") })[0].split("player_response=")[1] | |
let info = JSON.parse(response) | |
let videoDetails = info["videoDetails"] | |
if (videoDetails["isUpcoming"]) { | |
let thumbnails = videoDetails["thumbnail"]["thumbnails"] | |
let thumbnail = thumbnails[thumbnails.length - 1].url | |
return [{ | |
id: "upcoming", | |
name: "Upcoming", | |
type: "upcoming", | |
thumbnail: thumbnail | |
}] | |
} else { | |
let streamingData = info["streamingData"] | |
let streams = streamingData["formats"] | |
let hls = streamingData["hlsManifestUrl"] | |
if (streams !== undefined) { | |
return streams | |
.sort((stream1, stream2) => { return stream1.height < stream2.height }) | |
.map((stream) => { | |
return { | |
url: stream.url, | |
id: stream.itag.toString(), | |
name: stream.qualityLabel, | |
type: "video" | |
} | |
}) | |
} else if (hls !== undefined) { | |
return [{ | |
url: hls, | |
id: "live", | |
name: "Live", | |
type: "live" | |
}] | |
} | |
} | |
} | |
function getParameterByName(name, url) { | |
if (!url) url = window.location.href; | |
name = name.replace(/[\[\]]/g, "\\$&"); | |
var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"), | |
results = regex.exec(url); | |
if (!results) return null; | |
if (!results[2]) return ''; | |
return decodeURIComponent(results[2].replace(/\+/g, " ")); | |
} | |
function getPlayJS() { | |
return ` | |
let video = document.getElementsByTagName("video")[0] | |
video.onplay = function() { | |
video.onplay = null | |
completion(null) | |
} | |
null | |
` | |
} | |
function injectEvents() { | |
return ` | |
function download() { | |
completion("download") | |
} | |
function adjustPlaybackSpeed(btn) { | |
var btns = document.getElementsByClassName("speedbutton") | |
for (var i = 0; i < btns.length; i++) { | |
btns[i].className = btns[i].className.replace(" active", "") | |
} | |
btn.className += " active" | |
completion("adjustPlayback" + btn.getAttribute("data-value")) | |
} | |
function adjustQuality(btn) { | |
var btns = document.getElementsByClassName("qualitybutton") | |
for (var i = 0; i < btns.length; i++) { | |
btns[i].className = btns[i].className.replace(" active", "") | |
} | |
btn.className += " active" | |
completion("adjustQuality" + btn.id) | |
} | |
null | |
` | |
} | |
function getHTML() { | |
return ` | |
<html> | |
<meta name="viewport" content="width=device-width, user-scalable=no" /> | |
<style> | |
body { | |
background-color: #3B4252; | |
} | |
.text { | |
font-family: -apple-system; | |
color: white; | |
display: block; | |
text-align: center; | |
} | |
#container { | |
display: none; | |
align-items: center; | |
justify-content: center; | |
height: 100%; | |
width: 100%; | |
} | |
#loadingContainer { | |
display: flex; | |
width: 100%; | |
height: 100%; | |
align-items: center; | |
justify-content: center; | |
} | |
.buttonContainer { | |
background:#123; | |
width:100%; | |
height:40px; | |
line-height:40px; | |
display: table; | |
} | |
.videoContainer { | |
width: 100%; | |
} | |
#controls { | |
display: none; | |
} | |
.button1{ | |
border-style: solid; | |
border-width: 0.05em; | |
border-color: #2E3440; | |
font-family: -apple-system; | |
font-weight: semibold; | |
background: #4c566a; | |
color: white; | |
padding: 5px 10px; | |
text-align: center; | |
display: table-cell; | |
} | |
.active { | |
background: #2E3440; | |
} | |
h1 { | |
font-family: -apple-system; | |
font-size: 20; | |
color: white; | |
text-align: center; | |
} | |
img { | |
width: 100% !important; | |
height: auto !important; | |
} | |
video { | |
width: 100% !important; | |
height: auto !important; | |
} | |
</style> | |
<body> | |
<div id="loadingContainer"> | |
<h1>Loading your video. Stay tuned...</h1> | |
</div> | |
<div id="container"> | |
<div id="videoContainer"> | |
</div> | |
<p> | |
<div id="controls"> | |
<div class="buttonContainer"> | |
<div class="button1 speedbutton" id="speed0.25" data-value="0.25" onclick="adjustPlaybackSpeed(this)">0.25x</div> | |
<div class="button1 speedbutton" id="speed0.50" data-value="0.50" onclick="adjustPlaybackSpeed(this)">0.50x</div> | |
<div class="button1 speedbutton" id="speed0.75" data-value="0.75" onclick="adjustPlaybackSpeed(this)">0.75x</div> | |
</div> | |
<div class="buttonContainer"> | |
<div class="button1 active speedbutton" id="speed1.00" data-value="1.0" onclick="adjustPlaybackSpeed(this)">Normal</div> | |
</div> | |
<div class="buttonContainer"> | |
<div class="button1 speedbutton" id="speed1.25" data-value="1.25" onclick="adjustPlaybackSpeed(this)">1.25x</div> | |
<div class="button1 speedbutton" id="speed1.50" data-value="1.50" onclick="adjustPlaybackSpeed(this)">1.50x</div> | |
<div class="button1 speedbutton" id="speed1.75" data-value="1.75" onclick="adjustPlaybackSpeed(this)">1.75x</div> | |
<div class="button1 speedbutton" id="speed2.00" data-value="2.0" onclick="adjustPlaybackSpeed(this)">2.0x</div> | |
</div> | |
<br><br> | |
<div class="buttonContainer" id="qualityContainer"/></div> | |
<br><br> | |
<div class="buttonContainer"> | |
<div onclick='download()' class='button1'>Download Video</div> | |
</div> | |
</div> | |
</div> | |
</body> | |
</html> | |
` | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment