Skip to content

Instantly share code, notes, and snippets.

@georgenaranjo96
Created January 18, 2021 10:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save georgenaranjo96/e2d18697048301e2d10b2b9c551cb296 to your computer and use it in GitHub Desktop.
Save georgenaranjo96/e2d18697048301e2d10b2b9c551cb296 to your computer and use it in GitHub Desktop.
pip
// 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