Load playlists into Youtube player
Last active
December 2, 2023 20:40
-
-
Save d0ruk/2c0d32eb84cc4db4776fe7a9741c12df to your computer and use it in GitHub Desktop.
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
html { | |
font-size: 16px; | |
} | |
body { | |
background-color: rgba(0, 0, 0, 0.4); | |
padding: 1rem; | |
} | |
#player { | |
box-shadow: 0 0 2px 3px rgba(255, 255, 255, 0.3); | |
padding: 0.5rem; | |
} | |
#settings sup { | |
top: -1rem; | |
color: rgba(255, 255, 255, 0.7); | |
font-weight: bold; | |
} | |
.alert { | |
position: fixed; | |
margin: 0.5rem; | |
} | |
@media (max-width: 992px) { | |
#settings { | |
margin-bottom: 1rem; | |
} | |
} | |
@media (min-width: 992px) { | |
body { | |
margin-top: 3rem; | |
} | |
#settings { | |
position: absolute; | |
top: 1rem; | |
} | |
} |
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
<html> | |
<head> | |
<title>ucra</title> | |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/css/bootstrap.min.css"> | |
<link rel="stylesheet" href="index.css" /> | |
<script src="https://unpkg.com/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/qs.js"></script> | |
<script src="https://www.youtube.com/iframe_api"></script> | |
<script src="https://unpkg.com/[email protected]/dist/localforage.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/debug-umd.js"></script> | |
</head> | |
<body class="p-4"> | |
<div class="row"> | |
<div class="col-xm-12 col-lg-4" id="settings"> | |
<button type="button" class="btn btn-outline-warning" data-bs-toggle="modal" data-bs-target="#settingsModal"> | |
Settings | |
</button> | |
<div class="custom-file d-inline-block"> | |
<input id="fileInput" type="file" class="custom-file-input d-none" accept=".txt"> | |
<label class="custom-file-label" for="fileInput"> | |
<button type="button" class="btn btn-primary"> | |
Pick | |
</button> | |
<sup>?</sup> | |
</label> | |
</div> | |
<div class="alert alert-info d-none" role="alert"> | |
<h5 class="alert-heading">Playlist file</h5> | |
<p>A .txt file with a video ID on each line.</p> | |
<p> | |
If you want to load videos | |
<li>https://www.youtube.com/watch?v=n5h0qHwulI4</li> | |
<li>https://www.youtube.com/watch?v=erg0qHwNrHk</li> | |
</p> | |
<p>Your playlist.txt file needs to be</p> | |
<div><i>n5h0qHwulI4</i></div> | |
<div><i>erg0qHwNrHk</i></div> | |
<br> | |
<aside>Tip: load only a couple dozen videos at a time</aside> | |
</div> | |
</div> | |
<div class="col-xm-12 cold-md-10 col-xl-8 well" id="player"></div> | |
</div> | |
<div id="settingsModal" class="modal" tabindex="-1"> | |
<div class="modal-dialog modal-dialog-centered modal-lg"> | |
<div class="modal-content"> | |
<div class="modal-header"> | |
<h5 class="modal-title">Player settings</h5> | |
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
</div> | |
<div class="modal-body"> | |
<div class="container-fluid"> | |
<div class="row justify-content-center"> | |
<div class="col-sm-10"> | |
<form id="settingsForm"> | |
<div class="input-group mb-3 d-flex"> | |
<label for="height" class="form-label me-2">Height</label> | |
<input type="number" size="5" name="height" id="height" class="form-control"> | |
</div> | |
<div class="input-group mb-3 d-flex"> | |
<label for="width" class="form-label me-2">Width</label> | |
<input type="number" name="width" id="width" class="form-control" form="settingsForm"> | |
</div> | |
<div class="input-group mb-3 form-check form-check-inline"> | |
<span>Autoplay</span> | |
<input class="form-check-input" type="radio" name="autoplay" id="autoplayOn" value="1"> | |
<label for="autoplayOn" class="form-label me-2">On</label> | |
<input class="form-check-input" type="radio" name="autoplay" id="autoplayOff" value="0"> | |
<label for="autoplayOff" class="form-label me-2">Off</label> | |
</div> | |
<div class="input-group mb-3 form-check form-check-inline"> | |
<span>Modest branding</span> | |
<input class="form-check-input" type="radio" name="modestbranding" id="modestbrandingOn" value="1"> | |
<label for="modestbrandingOn" class="form-label me-2">On</label> | |
<input class="form-check-input" type="radio" name="modestbranding" id="modestbrandingOff" value="0"> | |
<label for="modestbrandingOff" class="form-label me-2">Off</label> | |
</div> | |
<div class="input-group mb-3 form-check form-check-inline"> | |
<span>Plays inline</span> | |
<input class="form-check-input" type="radio" name="playsinline" id="playsinlineOn" value="1"> | |
<label for="playsinlineOn" class="form-label me-2">On</label> | |
<input class="form-check-input" type="radio" name="playsinline" id="playsinlineOff" value="0"> | |
<label for="playsinlineOff" class="form-label me-2">Off</label> | |
</div> | |
</form> | |
</div> | |
<div class="modal-footer"> | |
<button type="submit" class="btn btn-primary" form="settingsForm">Apply</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script src="index.js" defer></script> | |
</body> | |
</html> |
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
let player; | |
const log = debug("ucra"); | |
const _playlist = "1n5a9vk1wig,v7c187E5BxY,OoGYwpXM0rg,JZ0MoWajrQ8,Nw4c5RhRYMY,Tiqxn3iOmxY"; | |
const playerSettings = { | |
height: 640, | |
width: 960, | |
// videoId: "JhF_Q3jvaSA", | |
// playerVars: { | |
autoplay: 1, | |
enablejsapi: 1, | |
modestbranding: 1, | |
playsinline: 0, | |
// playlist: ["JhF_Q3jvaSA"], | |
// origin: window.location.origin, | |
// }, | |
}; | |
YT.ready(() => { | |
localforage.config({ | |
driver: localforage.LOCALSTORAGE, | |
name: "ucra", | |
version: 1.0, | |
}); | |
localforage | |
.getItem("playerSettings") | |
.then(val => { | |
if (val) return Object.assign({}, playerSettings, JSON.parse(val)); | |
else return setLocalStorage("playerSettings", playerSettings); | |
}) | |
.then(settings => { | |
formDeserialize(settingsForm, settings); | |
log("Creating Player with settings %o", settings); | |
const { width, height, ...rest } = settings; | |
player = new YT.Player("player", { | |
width, | |
height, | |
// https://developers.google.com/youtube/player_parameters#Parameters | |
playerVars: rest, | |
events: { | |
onReady: onPlayerReady, | |
onStateChange: onPlayerStateChange, | |
}, | |
}); | |
}) | |
.catch(err => log("Failed to start player %O", err)); | |
}); | |
const [input] = qs`.custom-file > input[type=file]`; | |
const [button] = qs`.custom-file > label > button`; | |
const [settingsModal] = qs`#settingsModal`; | |
const [settingsForm] = qs`#settingsForm`; | |
const [sup] = qs`#settings sup`; | |
sup.addEventListener("mouseover", () => { | |
const [alert] = qs`.alert`; | |
alert.classList.remove("d-none"); | |
}); | |
sup.addEventListener("mouseout", () => { | |
const [alert] = qs`.alert`; | |
alert.classList.add("d-none"); | |
}); | |
settingsForm.addEventListener("submit", async () => { | |
const formData = new FormData(settingsForm); | |
const entries = [...formData.entries()]; | |
const data = entries.map(([k, v]) => { | |
const number = parseInt(v); | |
if (!Number.isNaN(number)) return [k, number]; | |
else return [k, v]; | |
}); | |
const settings = Object.fromEntries(data); | |
await setLocalStorage("playerSettings", settings); | |
}); | |
button.addEventListener("click", () => { | |
input.click(); | |
}); | |
input.addEventListener("change", async function () { | |
if (this.files.length > 0) { | |
const contents = await readFile(this.files[0]); | |
const playlist = contents.split("\n"); | |
loadPlaylist(player, playlist); | |
playVideo(player); | |
} | |
}); | |
const onPlayerReady = event => { | |
log("Player ready %o", event); | |
const playlist = _playlist.split(","); | |
loadPlaylist(player, playlist); | |
playVideo(player); | |
}; | |
const onPlayerStateChange = event => { | |
const { data } = event; | |
const [state] = Object.entries(YT.PlayerState).find(([k, v]) => v === data); | |
log("Player state %s", state); | |
}; | |
function formDeserialize(form, data) { | |
log("Serializing into form %o", data); | |
const entries = new URLSearchParams(data).entries(); | |
for (const [key, val] of entries) { | |
//http://javascript-coder.com/javascript-form/javascript-form-value.phtml | |
const input = form.elements?.[key]; | |
if (input instanceof RadioNodeList) input.type = true; | |
switch (input?.type) { | |
case "checkbox": | |
input.checked = !!val; | |
break; | |
case undefined: | |
break; | |
default: | |
input.value = val; | |
break; | |
} | |
} | |
} | |
function setLocalStorage(key, data) { | |
log("Setting %s in localstorage %o", key, data); | |
let value = data; | |
if (typeof data === "object") value = JSON.stringify(data); | |
return localforage | |
.setItem(key, value) | |
.then(() => localforage.getItem(key)) | |
.catch(error => log("Couldn't set %s: %O", key, error)); | |
} | |
function playVideo(player) { | |
log("Playing"); | |
player.playVideo(); | |
} | |
function loadPlaylist(player, array) { | |
if (Array.isArray(array)) { | |
log("Loading playlist %o", array); | |
player.loadPlaylist(array); | |
} | |
} | |
async function readFile(file) { | |
log(`Reading in %s`, file.name); | |
return new Promise((resolve, reject) => { | |
const reader = new FileReader(); | |
reader.onload = () => { | |
resolve(reader.result); | |
}; | |
reader.onerror = reject; | |
reader.readAsText(file); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment