Series or Directories
This assumes all video files are contained in folders 1-deep, e.g.
RedDwarf_s1/...
RedDwarf_s1/...
It then presents a list of collapsable
<!DOCTYPE html>
<html>
<head>
<meta charset='utf8'/>
<title>Flix Series</title>
</head>
<body>
<?php
$mp4ds = glob("*/");
foreach($mp4ds as $d) {
$d = preg_replace('@/+$@','',$d);
$xs = array_merge(glob("$d/*.mp4"),glob("$d/*.m4a"),glob("$d/*.mp3"));
if( count($xs) > 0 ) {
echo "<section class='collapse'>\n";
echo "<h1>$d</h1>\n";
if( count($xs) > 1 ) {
echo "<p><a href='playlist.php?$d' class='playlist'>$d.m3u</a></p>\n";
}
echo "<ul>\n";
foreach($xs as $mp4) {
$mp4n = preg_replace('@^.*/@',"",$mp4);
echo "<li><a href='stream.php?$mp4'>$mp4n</a></li>\n";
}
echo "</ul>\n";
echo "</section>\n";
}
}
?>
</body>
<script>
window.q = (x,y=document) => y.querySelector(x)
window.qq = (x,y=document) => Array.from(y.querySelectorAll(x))
function acms(e) {
let key = e.key
const code = e.code.toLowerCase()
if( code === "backquote" ) { key = "`" }
if( code.startsWith("digit") ) { key = code.substr(5) }
if( code === "space" ) { key = "space" }
let t = key.toLowerCase()
if( e.shiftKey ) t = "S-"+t
if( e.metaKey ) t = "M-"+t
if( e.ctrlKey ) t = "C-"+t
if( e.altKey ) t = "A-"+t
return t
}
function docopy(text) {
const textArea = document.createElement("textarea");
textArea.value = text;
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
toast("copied",text)
} catch (err) {
console.error('Unable to copy to clipboard', err);
}
document.body.removeChild(textArea);
}
function toast(msg,duration=3000) {
const dialog = document.createElement("dialog")
dialog.classList.add("toast")
dialog.classList.add("bold")
dialog.classList.add("green")
dialog.classList.add("pre")
dialog.textContent = msg
dialog.addEventListener("click",e => {
e.preventDefault()
dialog.close()
dialog.remove()
})
document.body.append(dialog)
dialog.showModal()
setTimeout(_ => {
dialog.close()
dialog.remove()
},duration)
}
qq("a.playlist").forEach( x => {
x.addEventListener("click", e => {
if( e.altKey ) {
e.preventDefault()
const href = x.href
do_copy(href)
}
})
})
function pd(e) { e.preventDefault() }
window.addEventListener("keydown",e => {
const k = acms(e)
switch(k) {
case "-":
case "S-_":
pd(e)
qq("section").forEach(x => x.classList.add("collapse"))
return;
case "=":
case "S-+":
pd(e)
qq("section").forEach(x => x.classList.remove("collapse"))
return;
}
if( k.match(/^[a-z]$/) ) {
pd(e)
let letter = k.toUpperCase()
let sel = `section[letter="${letter}"]`
let sect = q(sel)
if( sect ) {
qq("section").forEach(x => x.classList.add("collapse"))
sect.classList.remove("collapse")
sect.scrollIntoView(true)
}
return
}
})
const sections = qq("section")
sections.forEach( s => {
const h1 = q("h1",s)
if( h1 ) {
h1.addEventListener("click", e => {
e.preventDefault()
s.classList.toggle("collapse")
})
}
})
const links = qq("a")
links.forEach( a => {
a.addEventListener("click",e => {
if( e.shiftKey && ! ( e.ctrlKey || e.altKey || e.metaKey ) ) {
e.preventDefault()
do_copy(a.href)
}
})
})
function do_copy(text,elt=document.body) {
const textArea = document.createElement("textarea")
elt.appendChild(textArea)
textArea.value = text
textArea.style.position = "fixed"
const miles = "-999999px"
textArea.style.left = miles
textArea.style.top = miles
textArea.focus()
textArea.select()
return new Promise((res, rej) => {
if( document.execCommand('copy') ) {
toast(`Copied: ${textArea.value}`)
textArea.remove()
res()
} else {
toast(`Didn't copy: ${text}`)
textArea.remove()
rej()
}
})
}
const toast_time = 800
function toast(msg,the_toast_time=toast_time) {
const dialog = document.createElement("dialog")
dialog.classList.add("toast")
dialog.classList.add("bold")
dialog.classList.add("green")
dialog.textContent = msg
dialog.addEventListener("click",e => {
e.preventDefault()
dialog.close()
dialog.remove()
})
document.body.append(dialog)
dialog.showModal()
setTimeout(_ => {
dialog.close()
dialog.remove()
},the_toast_time)
}
</script>
<style>
body {
background-color: #007;
}
section.collapse {
background-color: #ccc;
display: inline-block;
}
section.collapse ul {
display: none;
}
section.collapse p {
display: none;
}
section p {
padding-left: 1rem;
}
section ul {
list-style-type: none;
}
section {
position: relative;
padding: 0.3rem;
margin: 0.5rem;
border: 1px solid black;
box-shadow: 0.5rem 0.5rem 0.5rem black;
background-color: white;
color: black;
}
section h1 {
width: 100%;
cursor: pointer;
}
section.collapse h1 {
background-color: #ddd;
}
section.collapse h1:hover {
background-color: #dfd;
}
section h1:hover {
color: #070;
}
section.collapse:has(h1:hover) {
background-color: #dfd;
}
a {
text-decoration: none;
}
/* Media */
a[href$=mp3] {
color: #700;
}
a[href$=m4a] {
color: #070;
}
a[href$=mp4] {
color: #007;
}
a[href$=mp3]::before {
content: "?? ";
}
a[href$=m4a]::before {
content: "?? ";
}
a[href$=mp4]::before {
content: "?? ";
}
/* Playlist */
a.playlist {
color: #505;
}
a.playlist::before {
content: "?? ";
}
/* Toast */
dialog.toast {
border: 2px solid black;
font-size: 2rem;
padding: 1.5rem;
box-shadow: 1rem 1rem 1rem black;
width: 60vw;
font-family: "Optima", sans-serif;
}
dialog.toast.bold {
font-weight: bold;
}
dialog.toast.green {
background-color: #070;
color: white;
}
dialog.toast.pre {
white-space: pre-wrap;
}
</style>
</html>