This commit is contained in:
Ondrej Zara 2019-03-25 15:49:23 +01:00
parent 37fdd5413c
commit 0e43a33c4a
15 changed files with 385 additions and 34 deletions

View file

@ -4,14 +4,13 @@ body {
flex-direction: column;
height: 100vh;
}
header,
footer {
body > header,
body > footer {
box-shadow: 0 0 3px #000;
}
main {
flex-grow: 1;
overflow-x: hidden;
overflow-y: auto;
overflow: hidden;
}
nav ul {
margin: 0;
@ -51,7 +50,19 @@ nav ul li.active {
#player:not([data-flags~=repeat]) .repeat {
opacity: 0.5;
}
.component {
height: 100%;
display: flex;
flex-direction: column;
}
#queue {
height: 100%;
display: flex;
flex-direction: column;
}
#queue ul {
flex-grow: 1;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
@ -69,3 +80,15 @@ nav ul li.active {
#queue .current {
font-weight: bold;
}
#library {
height: 100%;
display: flex;
flex-direction: column;
}
#library ul {
flex-grow: 1;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
}

View file

@ -3,13 +3,16 @@ body {
display: flex;
flex-direction: column;
height: 100vh;
}
header, footer {
> header, > footer {
box-shadow: 0 0 3px #000;
}
}
@import "main.less";
@import "nav.less";
@import "player.less";
@import "component.less";
@import "queue.less";
@import "library.less";

5
app/css/component.less Normal file
View file

@ -0,0 +1,5 @@
.component {
height: 100%;
display: flex;
flex-direction: column;
}

14
app/css/library.less Normal file
View file

@ -0,0 +1,14 @@
#library {
.component;
ul {
flex-grow: 1;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;
}
li {
}
}

View file

@ -1,5 +1,4 @@
main {
flex-grow: 1;
overflow-x: hidden;
overflow-y: auto;
overflow: hidden;
}

View file

@ -1,5 +1,9 @@
#queue {
.component;
ul {
flex-grow: 1;
overflow: auto;
list-style: none;
margin: 0;
padding: 0;

View file

@ -24,9 +24,19 @@
</section>
</header>
<main>
<section id="queue"></section>
<section id="queue">
<header></header>
<ul></ul>
</section>
<section id="playlists"></section>
<section id="library"></section>
<section id="library">
<header></header>
<ul></ul>
</section>
<section id="fs">
<header></header>
<ul></ul>
</section>
<section id="misc"></section>
</main>
<footer>
@ -35,6 +45,7 @@
<li data-for="queue">Q</li>
<li data-for="playlists">Playlists</li>
<li data-for="library">Library</li>
<li data-for="fs">FS</li>
<li data-for="misc">Misc</li>
</ul>
</nav>

View file

@ -3,8 +3,10 @@ import * as mpd from "./lib/mpd.js";
import * as player from "./player.js";
import * as queue from "./queue.js";
import * as library from "./library.js";
import * as fs from "./fs.js";
const components = { queue };
const components = { queue, library, fs };
export function activate(what) {
for (let id in components) {
@ -29,8 +31,7 @@ async function init() {
await mpd.init();
player.init(document.querySelector("#player"));
activate("queue");
window.mpd = mpd;
activate("fs");
}

65
app/js/fs.js Normal file
View file

@ -0,0 +1,65 @@
import * as app from "./app.js";
import * as mpd from "./lib/mpd.js";
import * as html from "./lib/html.js";
import * as player from "./player.js";
import * as format from "./lib/format.js";
import * as ui from "./lib/ui.js";
let node;
const SORT = "-Track";
function buildHeader(path) {
filter = filter || {};
let header = node.querySelector("header");
html.clear(header);
let button = html.button({}, "Music Library", header);
button.addEventListener("click", e => listArtists());
let artist = filter["Artist"];
if (artist) {
let artistFilter = {"Artist":artist};
let button = html.button({}, artist, header);
button.addEventListener("click", e => listAlbums(artistFilter));
let album = filter["Album"];
if (album) {
let albumFilter = Object.assign({}, artistFilter, {"Album":album});
let button = html.button({}, album, header);
button.addEventListener("click", e => listSongs(albumFilter));
}
}
}
function buildDirectory(data, parent) {
let path = data["directory"];
let node = ui.group(path, {}, parent);
node.addEventListener("click", e => list(path));
return node;
}
function buildFile(data, parent) {
return ui.song(data, parent);
}
function buildResults(results) {
let ul = node.querySelector("ul");
html.clear(ul);
results["directory"].forEach(directory => buildDirectory(directory, ul));
results["file"].forEach(file => buildFile(file, ul));
}
async function list(path) {
let results = await mpd.listPath(path);
buildResults(results);
// buildHeader(path);
}
export async function activate() {
list("");
}
export function init(n) {
node = n;
}

View file

@ -15,19 +15,6 @@ function load(key) {
return localStorage.getItem(`${STORAGE_PREFIX}-${key}`);
}
async function getImageData(songUrl) {
let data = [];
let offset = 0;
while (1) {
let params = ["albumart", `"${mpd.escape(songUrl)}"`, offset];
let lines = await mpd.command(params.join(" "));
data = data.concat(lines[2]);
let metadata = parser.linesToStruct(lines.slice(0, 2));
if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]);
}
}
async function bytesToImage(bytes) {
let blob = new Blob([bytes]);
let src = URL.createObjectURL(blob);
@ -62,7 +49,7 @@ export async function get(artist, album, songUrl = null) {
cache[key] = promise;
try {
let data = await getImageData(songUrl);
let data = await mpd.albumArt(songUrl);
let bytes = new Uint8Array(data);
let image = await bytesToImage(bytes);
let url = resize(image).toDataURL(MIME);

View file

@ -34,7 +34,19 @@ function processQueue() {
ws.send(current.cmd);
}
export function escape(str) {
function serializeFilter(filter) {
let tokens = ["("];
Object.entries(filter).forEach(([key, value], index) => {
index && tokens.push(" AND ");
tokens.push(`(${key} == "${value}")`);
});
tokens.push(")");
let filterStr = tokens.join("");
return `"${escape(filterStr)}"`;
}
function escape(str) {
return str.replace(/(['"\\])/g, "\\$1");
}
@ -54,7 +66,10 @@ export async function commandAndStatus(cmd) {
export async function status() {
let lines = await command(["status", "currentsong"]);
return parser.linesToStruct(lines);
let status = parser.linesToStruct(lines);
// duration returned 2x => arrayfied
if ("duration" in status) { status["duration"] = status["duration"][0]; }
return status;
}
export async function listQueue() {
@ -62,6 +77,55 @@ export async function listQueue() {
return parser.songList(lines);
}
export async function enqueue(fileOrFilter, sort = null) {
if (typeof(fileOrFilter) == "string") {
return command(`addid "${escape(fileOrFilter)}"`);
}
let tokens = ["findadd"];
tokens.push(serializeFilter(fileOrFilter));
// sort && tokens.push("sort", sort); FIXME not implemented in MPD
return command(tokens.join(" "));
}
export async function listPath(path) {
let lines = await command(`lsinfo "${escape(path)}"`);
return parser.pathContents(lines);
}
export async function listTags(tag, filter = null) {
let tokens = ["list", tag];
if (filter) {
tokens.push(serializeFilter(filter));
let fakeGroup = Object.keys(filter)[0]; // FIXME hack for MPD < 0.21.6
tokens.push("group", fakeGroup);
}
let lines = await command(tokens.join(" "));
let parsed = parser.linesToStruct(lines);
return [].concat(parsed[tag] || []);
}
export async function listSongs(filter) {
let tokens = ["find"];
tokens.push(serializeFilter(filter));
let lines = await command(tokens.join(" "));
return parser.songList(lines);
}
export async function albumArt(songUrl) {
let data = [];
let offset = 0;
while (1) {
let params = ["albumart", `"${escape(songUrl)}"`, offset];
let lines = await command(params.join(" "));
data = data.concat(lines[2]);
let metadata = parser.linesToStruct(lines.slice(0, 2));
if (data.length >= Number(metadata["size"])) { return data; }
offset += Number(metadata["binary"]);
}
}
export async function init() {
return new Promise((resolve, reject) => {
try {

View file

@ -3,7 +3,18 @@ export function linesToStruct(lines) {
lines.forEach(line => {
let cindex = line.indexOf(":");
if (cindex == -1) { throw new Error(`Malformed line "${line}"`); }
result[line.substring(0, cindex)] = line.substring(cindex+2);
let key = line.substring(0, cindex);
let value = line.substring(cindex+2);
if (key in result) {
let old = result[key];
if (old instanceof Array) {
old.push(value);
} else {
result[key] = [old, value];
}
} else {
result[key] = value;
}
});
return result;
}
@ -16,6 +27,7 @@ export function songList(lines) {
if (line.startsWith("file:") && batch.length) {
let song = linesToStruct(batch);
songs.push(song);
batch = [];
}
batch.push(lines.shift());
}
@ -27,3 +39,27 @@ export function songList(lines) {
return songs;
}
export function pathContents(lines) {
const prefixes = ["file", "directory", "playlist"];
let batch = [];
let result = {};
let batchPrefix = null;
prefixes.forEach(prefix => result[prefix] = []);
while (lines.length) {
let line = lines[0];
let prefix = line.split(":")[0];
if (prefixes.includes(prefix)) { // begin of a new batch
if (batch.length) { result[batchPrefix].push(linesToStruct(batch)); }
batchPrefix = prefix;
batch = [];
}
batch.push(lines.shift());
}
if (batch.length) { result[batchPrefix].push(linesToStruct(batch)); }
return result;
}

47
app/js/lib/ui.js Normal file
View file

@ -0,0 +1,47 @@
import * as mpd from "./mpd.js";
import * as html from "./html.js";
function playButton(fileOrFilter, parent) {
let button = html.button({}, "▶", parent);
button.addEventListener("click", async e => {
e.stopPropagation();
await mpd.command("clear");
await mpd.enqueue(fileOrFilter, SORT);
await mpd.command("play");
app.activate("queue");
player.update();
});
return button;
}
function addButton(fileOrFilter, parent) {
let button = html.button({}, "+", parent);
button.addEventListener("click", async e => {
e.stopPropagation();
await mpd.enqueue(fileOrFilter, SORT);
// fixme notification?
});
return button;
}
export function song(data, parent) {
let node = html.node("li", {}, "", parent);
let file = data["file"];
playButton(file, node);
addButton(file, node);
html.node("h3", {}, data["Title"], node);
return node;
}
export function group(label, filter, parent) {
let node = html.node("li", {}, label, parent);
playButton(filter, node);
addButton(filter, node);
return node;
}

93
app/js/library.js Normal file
View file

@ -0,0 +1,93 @@
import * as app from "./app.js";
import * as mpd from "./lib/mpd.js";
import * as html from "./lib/html.js";
import * as player from "./player.js";
import * as format from "./lib/format.js";
import * as ui from "./lib/ui.js";
let node;
const SORT = "-Track";
function buildHeader(filter) {
filter = filter || {};
let header = node.querySelector("header");
html.clear(header);
let button = html.button({}, "Music Library", header);
button.addEventListener("click", e => listArtists());
let artist = filter["Artist"];
if (artist) {
let artistFilter = {"Artist":artist};
let button = html.button({}, artist, header);
button.addEventListener("click", e => listAlbums(artistFilter));
let album = filter["Album"];
if (album) {
let albumFilter = Object.assign({}, artistFilter, {"Album":album});
let button = html.button({}, album, header);
button.addEventListener("click", e => listSongs(albumFilter));
}
}
}
function buildAlbum(album, filter, parent) {
let childFilter = Object.assign({}, filter, {"Album": album});
let node = ui.group(album, childFilter, parent);
node.addEventListener("click", e => listSongs(childFilter));
return node;
}
function buildArtist(artist, filter, parent) {
let childFilter = Object.assign({}, filter, {"Artist": artist});
let node = ui.group(artist, childFilter, parent);
node.addEventListener("click", e => listAlbums(childFilter));
return node;
}
function buildSongs(songs, filter) {
let ul = node.querySelector("ul");
html.clear(ul);
songs.map(song => ui.song(song, ul));
}
function buildAlbums(albums, filter) {
let ul = node.querySelector("ul");
html.clear(ul);
albums.map(album => buildAlbum(album, filter, ul));
}
function buildArtists(artists, filter) {
let ul = node.querySelector("ul");
html.clear(ul);
artists.map(artist => buildArtist(artist, filter, ul));
}
async function listSongs(filter) {
let songs = await mpd.listSongs(filter);
buildSongs(songs, filter);
buildHeader(filter);
}
async function listAlbums(filter) {
let albums = await mpd.listTags("Album", filter);
buildAlbums(albums, filter);
buildHeader(filter);
}
async function listArtists(filter) {
let artists = await mpd.listTags("Artist", filter);
buildArtists(artists, filter);
buildHeader(filter);
}
export async function activate() {
listArtists();
}
export function init(n) {
node = n;
}

View file

@ -44,12 +44,11 @@ function buildSong(song) {
}
function buildSongs(songs) {
html.clear(node);
let ul = node.querySelector("ul");
html.clear(ul);
let ul = html.node("ul");
songs.map(buildSong).forEach(li => ul.appendChild(li));
node.appendChild(ul);
updateCurrent();
}