Skip to content

Instantly share code, notes, and snippets.

@frabarz
Created September 14, 2021 04:09
Show Gist options
  • Save frabarz/caf6644aa7b44530d741daef9d1a380e to your computer and use it in GitHub Desktop.
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.
// ==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