Created
September 14, 2021 04:09
-
-
Save frabarz/caf6644aa7b44530d741daef9d1a380e to your computer and use it in GitHub Desktop.
Userscript to add basic music player functionality to the directory index page generated for local file:// URLs. Is enabled only if it finds at least one .mp3 file in the list.
This file contains hidden or 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
// ==UserScript== | |
// @name Firefox Directory Index Music Player | |
// @namespace Generic scripts | |
// @match file:///C:/Users/**/* | |
// @grant none | |
// @version 1.1 | |
// @author frabarz | |
// ==/UserScript== | |
const entries = Array.from(document.querySelectorAll("a.file[href$='.mp3']"), node => { | |
node.dataset.hash = shortHash(node.href); | |
return [node.dataset.hash, node.href]; | |
}); | |
const fileMap = Object.fromEntries(entries); | |
const playlist = entries.map(entry => entry[1]); | |
const player = createPlayerDOM(); | |
const playerState = { | |
history: [], | |
isPlaylist: sessionStorage.getItem("playlist") === "true", | |
isShuffling: sessionStorage.getItem("shuffle") === "true", | |
} | |
if (entries.length > 0) { | |
const wrapper = document.createElement("div"); | |
wrapper.className = "frbrz-local-player"; | |
wrapper.appendChild(createPlaylistButtonDOM()); | |
wrapper.appendChild(createShuffleButtonDOM()); | |
wrapper.appendChild(createRwdButtonDOM()); | |
wrapper.appendChild(createFwdButtonDOM()); | |
wrapper.appendChild(player); | |
document.body.appendChild(wrapper); | |
document.body.addEventListener("click", evt => { | |
if (evt.target.matches("a.file[href$='.mp3']")) { | |
evt.stopPropagation(); | |
evt.preventDefault(); | |
player.src = evt.target.href; | |
player.play(); | |
} | |
}, true); | |
inyectStyleSheetRules(); | |
enableMediaSessionAPI(); | |
permalinkAutoPlay(); | |
} | |
function setAndPlay(file) { | |
player.src = file; | |
player.play(); | |
const playHistory = playerState.history; | |
const index = playHistory.indexOf(file); | |
if (index === -1) playHistory.push(file); | |
} | |
function playPrev() { | |
const playHistory = playerState.history; | |
const index = playHistory.indexOf(player.src); | |
const prev = index === 0 ? playHistory.length - 1 : index - 1; | |
setAndPlay(playHistory[prev]); | |
} | |
function playNext() { | |
const playHistory = playerState.history; | |
if (playHistory.length === playlist.length) { | |
playHistory.splice(0, playHistory.length); | |
} | |
const unplayed = playlist.filter(file => !playHistory.includes(file)); | |
if (playerState.isShuffling) shuffleArray(unplayed); | |
setAndPlay(unplayed[0]); | |
} | |
function createPlayerDOM() { | |
const player = document.createElement("audio"); | |
player.controls = true; | |
player.addEventListener("play", evt => { | |
const index = playlist.indexOf(player.src); | |
if (index > -1) { | |
location.hash = shortHash(player.src); | |
let songName = decodeURIComponent(player.src.split("/").pop().replace(/\.mp3$/, "")); | |
if (songName.includes(" - ")) { | |
const [artist, title] = songName.split(" - "); | |
document.title = `${title} - ${artist}`; | |
if ("mediaSession" in navigator) { | |
navigator.mediaSession.metadata = new MediaMetadata({title, artist}); | |
} | |
} | |
else { | |
document.title = songName; | |
if ("mediaSession" in navigator) { | |
navigator.mediaSession.metadata = undefined; | |
} | |
} | |
} | |
}); | |
player.addEventListener("ended", evt => { | |
if (playerState.isPlaylist) playNext(); | |
}); | |
return player; | |
} | |
function createPlaylistButtonDOM() { | |
const playlistToggle = document.createElement("button"); | |
playlistToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path d="M5.3 13.3a1 1 0 00-.3.7c0 .6.4 1 1 1 .3 0 .5-.1.7-.3l3-3c.2-.2.3-.4.3-.7s-.1-.5-.3-.7l-3-3A1 1 0 006 7c-.6 0-1 .4-1 1 0 .3.1.5.3.7L6.6 10H1c-.6 0-1 .4-1 1s.4 1 1 1h5.6l-1.3 1.3zM19 1H3a1 1 0 00-1 1v6h1a3 3 0 013-3c.8 0 1.6.3 2.1.9l.1.1H9v.8l1 1V6h8v3h-6.8c.3.3.5.6.6 1H18v3h-6.8l-.1.1-.9.9H18v3h-8v-2.8l-1 1V17H4v-.8c-.6-.5-1-1.3-1-2.2H2v4c0 .5.5 1 1 1h16c.6 0 1-.5 1-1V2c0-.5-.5-1-1-1z"/></svg>`; | |
playlistToggle.className = "toggle-playlist"; | |
playlistToggle.classList.toggle("inactive", !playerState.isPlaylist); | |
playlistToggle.addEventListener("click", evt => { | |
playerState.isPlaylist = !playerState.isPlaylist; | |
playlistToggle.classList.toggle("inactive", !playerState.isPlaylist); | |
sessionStorage.setItem("playlist", `${playerState.isPlaylist}`); | |
}); | |
return playlistToggle; | |
} | |
function createShuffleButtonDOM() { | |
const shuffleToggle = document.createElement("button"); | |
shuffleToggle.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M14.5 5h2l-1.2 1.3a1 1 0 00-.3.7 1 1 0 001.7.7l3-3c.2-.2.3-.4.3-.7a1 1 0 00-.3-.7l-3-3a1 1 0 00-1.4 1.4L16.6 3H14a1 1 0 00-.8.4l-2.9 3.5 1.3 1.5L14.5 5zm2.2 7.3a1 1 0 00-1.4 1.4l1.3 1.3h-2.1L4.8 3.4A1 1 0 004 3H1a1 1 0 00-1 1c0 .6.5 1 1 1h2.5l9.7 11.6c.2.3.5.4.8.4h2.6l-1.3 1.3a1 1 0 00-.3.7 1 1 0 001.7.7l3-3c.2-.2.3-.4.3-.7a1 1 0 00-.3-.7l-3-3zM3.5 15H1a1 1 0 00-1 1c0 .6.5 1 1 1h3c.3 0 .6-.1.8-.4l2.9-3.5-1.3-1.5L3.5 15z" clip-rule="evenodd"/></svg>`; | |
shuffleToggle.className = "toggle-shuffle"; | |
shuffleToggle.classList.toggle("inactive", !playerState.isShuffling); | |
shuffleToggle.addEventListener("click", evt => { | |
playerState.isShuffling = !playerState.isShuffling; | |
shuffleToggle.classList.toggle("inactive", !playerState.isShuffling); | |
sessionStorage.setItem("shuffle", `${playerState.isShuffling}`); | |
}); | |
return shuffleToggle; | |
} | |
function createRwdButtonDOM() { | |
const rwButton = document.createElement("button"); | |
rwButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M15 3h-2a1 1 0 00-1 1v4L5.6 3.2A1 1 0 005 3a1 1 0 00-1 1v12c0 .6.5 1 1 1 .2 0 .4 0 .6-.2L12 12v4c0 .6.5 1 1 1h2c.6 0 1-.5 1-1V4c0-.5-.4-1-1-1z" clip-rule="evenodd"/></svg>`; | |
rwButton.className = "button-prev"; | |
rwButton.style.transform = "rotate(180deg)"; | |
rwButton.addEventListener("click", playPrev); | |
return rwButton; | |
} | |
function createFwdButtonDOM() { | |
const ffButton = document.createElement("button"); | |
ffButton.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M15 3h-2a1 1 0 00-1 1v4L5.6 3.2A1 1 0 005 3a1 1 0 00-1 1v12c0 .6.5 1 1 1 .2 0 .4 0 .6-.2L12 12v4c0 .6.5 1 1 1h2c.6 0 1-.5 1-1V4c0-.5-.4-1-1-1z" clip-rule="evenodd"/></svg>`; | |
ffButton.className = "button-next"; | |
ffButton.addEventListener("click", playNext); | |
return ffButton; | |
} | |
function inyectStyleSheetRules() { | |
const inyectStyleSheetRule = rule => document.styleSheets[0].insertRule(rule); | |
inyectStyleSheetRule`.frbrz-local-player { | |
position: sticky; | |
bottom: 1rem; | |
display: grid; | |
grid-template-columns: auto auto auto auto 1fr; | |
align-items: stretch; | |
grid-template-rows: 40px; | |
gap: 2px; | |
margin-top: 2rem; | |
}`; | |
inyectStyleSheetRule`.frbrz-local-player button { | |
background: rgba(0, 0, 0, 0.718); | |
border: 0 none; | |
color: #fff; | |
padding: 0 1rem; | |
}`; | |
inyectStyleSheetRule`.frbrz-local-player button.inactive { | |
background: rgba(0, 0, 0, 0.4); | |
border: 0 none; | |
}`; | |
inyectStyleSheetRule`.frbrz-local-player button svg { | |
display: block; | |
fill: #fff; | |
max-height: 20px; | |
}`; | |
inyectStyleSheetRule`.frbrz-local-player button:hover svg { | |
fill: #48A0F7; | |
}`; | |
} | |
// Add integration with multimedia keys | |
function enableMediaSessionAPI() { | |
if ("mediaSession" in navigator) { | |
navigator.mediaSession.setActionHandler("previoustrack", playPrev); | |
navigator.mediaSession.setActionHandler("nexttrack", playNext); | |
} | |
} | |
function permalinkAutoPlay() { | |
const locationHash = location.hash.slice(1); | |
const file = fileMap[locationHash]; | |
if (file) { | |
player.src = file; | |
player.play(); | |
} | |
} | |
function shortHash(t) { | |
if ("string" === typeof t || "number" === typeof t) { | |
return ((t, e) => { | |
const r = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
e = e || 62; | |
let n, o = [], l = ""; | |
const u = t < 0 ? "-" : ""; | |
for (t = Math.abs(t); t >= e;) { | |
n = t % e; | |
t = Math.floor(t / e) | |
o.push(r[n]); | |
} | |
if (t > 0) o.push(r[t]); | |
for (var h = o.length - 1; h >= 0; h--) l += o[h]; | |
return u + l | |
})((t => { | |
let e = 0; | |
if (0 == t.length) return e; | |
for (let r = 0; r < t.length; r++) { | |
e = (e << 5) - e + t.charCodeAt(r); | |
e &= e | |
} | |
return e; | |
})(String(t)), 61).replace("-", "Z") | |
} | |
throw new Error("Unexpected input type"); | |
} | |
function shuffleArray(array) { | |
let currentIndex = array.length; | |
let randomIndex; | |
// While there remain elements to shuffle... | |
while (0 !== currentIndex) { | |
// Pick a remaining element... | |
randomIndex = Math.floor(Math.random() * currentIndex); | |
currentIndex--; | |
// And swap it with the current element. | |
const swapped = [array[randomIndex], array[currentIndex]]; | |
array[currentIndex] = swapped[0]; | |
array[randomIndex] = swapped[1]; | |
} | |
return array; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment