Compare commits
16 Commits
master
...
database-b
Author | SHA1 | Date | |
---|---|---|---|
9ad5795475 | |||
06e1e19f72 | |||
ee5dd22f5f | |||
1f9a0ea586 | |||
38f832e8d8 | |||
c0e2ab0129 | |||
14d915fb7c | |||
087a06a20c | |||
88fc77f0a5 | |||
4c79a7859a | |||
bc48e231ee | |||
232929a47a | |||
e23f994894 | |||
7e8f11a33c | |||
5782510af1 | |||
9576373dbf |
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,3 +3,7 @@ lib
|
|||||||
.vagrant
|
.vagrant
|
||||||
config.json
|
config.json
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
|
||||||
|
db.sqlite3
|
||||||
|
.vagrant-cache
|
||||||
|
*.log
|
||||||
|
2
Vagrantfile
vendored
2
Vagrantfile
vendored
@ -17,7 +17,7 @@ Vagrant.configure("2") do |config|
|
|||||||
|
|
||||||
config.vm.provision "shell", inline: "sudo apt-get update -y"
|
config.vm.provision "shell", inline: "sudo apt-get update -y"
|
||||||
config.vm.provision "shell", inline: "sudo apt-get upgrade -y"
|
config.vm.provision "shell", inline: "sudo apt-get upgrade -y"
|
||||||
config.vm.provision "shell", inline: "sudo apt-get install -y make nodejs npm"
|
config.vm.provision "shell", inline: "sudo apt-get install -y make nodejs npm sqlite3"
|
||||||
end
|
end
|
||||||
|
|
||||||
# vim:set ts=2 sw=2 et:
|
# vim:set ts=2 sw=2 et:
|
||||||
|
14
archive/file-to-sqlite.sh
Executable file
14
archive/file-to-sqlite.sh
Executable file
@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
ls media/*\ -\ *.mp3 | while read line ; do
|
||||||
|
UUID=$(./jukebox/node_modules/.bin/uuid v4)
|
||||||
|
echo "mv \"$line\" \"media/$UUID\""
|
||||||
|
|
||||||
|
FILE=$(basename "$line" ".mp3")
|
||||||
|
set -- $FILE
|
||||||
|
|
||||||
|
TAG=${@:1:1}
|
||||||
|
LABEL=${@:3}
|
||||||
|
echo "INSERT INTO tags (tag) VALUES (\"$TAG\");" 1>>/dev/stderr;
|
||||||
|
echo "INSERT INTO library (tag, label, uuid) VALUES (\"$TAG\", \"$LABEL\", \"$UUID\");" 1>>/dev/stderr;
|
||||||
|
done
|
@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
"media_path": "/path/to/media",
|
"media_path": "/path/to/media",
|
||||||
"script_path": "/path/to/scripts",
|
"script_path": "/path/to/scripts",
|
||||||
|
"db": "/path/to/jukebox.sqlite3",
|
||||||
|
|
||||||
"play_throttle": 5000,
|
"play_throttle": 5000,
|
||||||
"pause_throttle": 2000,
|
"pause_throttle": 2000,
|
||||||
|
9
create.sql
Normal file
9
create.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
CREATE TABLE tags (tag TEXT, seen_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP);
|
||||||
|
INSERT INTO tags (tag) VALUES
|
||||||
|
("12121212"),
|
||||||
|
("badc0ffee");
|
||||||
|
CREATE TABLE library (uuid TEXT PRIMARY KEY, tag TEXT, label TEXT);
|
||||||
|
INSERT INTO library (uuid, tag, label) VALUES
|
||||||
|
("585687cb-1ff7-4106-a888-a40ff09b7fa1", "12121212", "OK Then"),
|
||||||
|
("85a6f785-51e1-4220-be75-fd306b973221", "12121212", "Other"),
|
||||||
|
("b4eabe35-9f96-4455-9294-56cc375cfbc4", "badc0ffee", " Brazil Theme");
|
@ -7,7 +7,7 @@ const { spawn } = require('child_process');
|
|||||||
const { EventEmitter } = require('events');
|
const { EventEmitter } = require('events');
|
||||||
|
|
||||||
module.exports.ChildProcessEmitter = class ChildProcessEmitter extends EventEmitter {
|
module.exports.ChildProcessEmitter = class ChildProcessEmitter extends EventEmitter {
|
||||||
constructor(command, logger) {
|
constructor(command, logger, cwd) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
var emitter = this;
|
var emitter = this;
|
||||||
@ -17,10 +17,14 @@ module.exports.ChildProcessEmitter = class ChildProcessEmitter extends EventEmit
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.stderrFilters = [];
|
this.stderrFilters = [];
|
||||||
|
|
||||||
|
var options = {
|
||||||
|
cwd: cwd,
|
||||||
|
};
|
||||||
|
|
||||||
var cmd = command[0],
|
var cmd = command[0],
|
||||||
args = command.slice(1);
|
args = command.slice(1);
|
||||||
|
|
||||||
this.childProcess = spawn(cmd, args);
|
this.childProcess = spawn(cmd, args, options);
|
||||||
|
|
||||||
this.childProcess.on('error', function(err) {
|
this.childProcess.on('error', function(err) {
|
||||||
emitter.logger.error("process error", { class: emitter.constructor.name, err: String(err).trim() });
|
emitter.logger.error("process error", { class: emitter.constructor.name, err: String(err).trim() });
|
||||||
|
@ -22,13 +22,13 @@ class Library extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.backend.find(tag, path => {
|
this.backend.find(tag, track => {
|
||||||
if (!path) {
|
if (!track) {
|
||||||
this.emit('action', new tags.NotFoundTag(tag));
|
this.emit('action', new tags.NotFoundTag(tag));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.emit('action', new tags.FileTag(tag, path));
|
this.emit('action', new tags.FileTag(tag, track));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
jukebox/library/sqlite-backend.js
Normal file
29
jukebox/library/sqlite-backend.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const Track = require('./track');
|
||||||
|
|
||||||
|
class SqliteBackend {
|
||||||
|
constructor(config, db, logger) {
|
||||||
|
this.config = config;
|
||||||
|
this.db = db;
|
||||||
|
this.logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
find(tag, callback) {
|
||||||
|
this.db.run("INSERT INTO tags (tag) VALUES (?)", tag);
|
||||||
|
this.db.get("SELECT uuid, tag, label FROM library WHERE tag = ? ORDER BY RANDOM() LIMIT 1", tag, (err, row) => {
|
||||||
|
if (typeof row === 'undefined') {
|
||||||
|
this.logger.debug("no matching tag in sqlite backend", { tag: tag });
|
||||||
|
return callback();
|
||||||
|
}
|
||||||
|
this.logger.debug("found a matching tag", { tag: tag });
|
||||||
|
callback(new Track(row['tag'], row['uuid'], row['label']));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function(config, db, logger) {
|
||||||
|
return new SqliteBackend(config, db, logger);
|
||||||
|
};
|
||||||
|
|
||||||
|
// vim:ts=2 sw=2 et:
|
@ -29,13 +29,13 @@ class NotFoundTag extends Tag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FileTag extends Tag {
|
class FileTag extends Tag {
|
||||||
constructor(tag, path) {
|
constructor(tag, track) {
|
||||||
super(tag);
|
super(tag);
|
||||||
this.path = path;
|
this.track = track;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return basename(this.path);
|
return this.track.label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
jukebox/library/track.js
Normal file
11
jukebox/library/track.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
class Track {
|
||||||
|
constructor(tag, uuid, label) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.uuid = uuid;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Track;
|
@ -13,7 +13,7 @@ const DEFAULT_UNKNOWN_THROTTLE = 2000;
|
|||||||
|
|
||||||
class MediaPlayer extends ChildProcessEmitter {
|
class MediaPlayer extends ChildProcessEmitter {
|
||||||
constructor(config, logger) {
|
constructor(config, logger) {
|
||||||
super(config.mpg321, logger);
|
super(config.mpg321, logger, config.media_path);
|
||||||
|
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ class MediaPlayer extends ChildProcessEmitter {
|
|||||||
|
|
||||||
_playFile(tag) {
|
_playFile(tag) {
|
||||||
this.emit('command', tag);
|
this.emit('command', tag);
|
||||||
this.send("LOAD " + tag.path);
|
this.send("LOAD " + tag.track.uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
_unknown(tag) {
|
_unknown(tag) {
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
"use strict";
|
|
||||||
|
|
||||||
const mustache = require('mustache');
|
|
||||||
const readFileSync = require('fs').readFileSync;
|
|
||||||
|
|
||||||
var template_dir = undefined;
|
|
||||||
|
|
||||||
class View {
|
|
||||||
constructor() {
|
|
||||||
this.template = readFileSync(template_dir + this.path(), {encoding: 'utf8'})
|
|
||||||
mustache.parse(this.template)
|
|
||||||
}
|
|
||||||
|
|
||||||
render(vars) {
|
|
||||||
return mustache.render(this.template, vars, this.partials());
|
|
||||||
}
|
|
||||||
|
|
||||||
path() {
|
|
||||||
throw 'Subclasses must define a template path';
|
|
||||||
}
|
|
||||||
|
|
||||||
partials() {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class BaseView extends View {
|
|
||||||
path() {
|
|
||||||
return 'base.mustache';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class IndexView extends View {
|
|
||||||
path() {
|
|
||||||
return 'index.mustache';
|
|
||||||
}
|
|
||||||
|
|
||||||
partials() {
|
|
||||||
return {
|
|
||||||
log: views.log.template
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LogView extends View {
|
|
||||||
path() {
|
|
||||||
return 'log.mustache';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const views = {};
|
|
||||||
|
|
||||||
module.exports = function(tpl_dir) {
|
|
||||||
template_dir = tpl_dir;
|
|
||||||
|
|
||||||
views.base = new BaseView();
|
|
||||||
views.index = new IndexView();
|
|
||||||
views.log = new LogView();
|
|
||||||
|
|
||||||
return views;
|
|
||||||
}
|
|
1053
package-lock.json
generated
1053
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -17,11 +17,14 @@
|
|||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"bufferutil": "^4.0.1",
|
"bufferutil": "^4.0.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"express-handlebars": "^3.1.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
"morgan": "^1.7.0",
|
"morgan": "^1.7.0",
|
||||||
"mustache": "^3.1.0",
|
"multer": "^1.4.2",
|
||||||
|
"sqlite": "^3.0.3",
|
||||||
"throttle-debounce": "^2.1.0",
|
"throttle-debounce": "^2.1.0",
|
||||||
"utf-8-validate": "^5.0.2",
|
"utf-8-validate": "^5.0.2",
|
||||||
|
"uuid": "^3.3.3",
|
||||||
"winston": "^3.2.1",
|
"winston": "^3.2.1",
|
||||||
"ws": "^7.2.1"
|
"ws": "^7.2.1"
|
||||||
},
|
},
|
||||||
|
115
player.js
115
player.js
@ -2,16 +2,29 @@
|
|||||||
|
|
||||||
const config = require('./config.json')
|
const config = require('./config.json')
|
||||||
|
|
||||||
const views = require('./jukebox/views')(__dirname + '/templates/');
|
|
||||||
const library = require('./jukebox/library');
|
const library = require('./jukebox/library');
|
||||||
|
|
||||||
const MediaLibraryFileBackend = require('./jukebox/library/file-backend')(config);
|
|
||||||
const MediaLibrary = new library.Library(config, MediaLibraryFileBackend);
|
|
||||||
|
|
||||||
const ScriptRunner = require('./jukebox/scripts')(config);
|
const ScriptRunner = require('./jukebox/scripts')(config);
|
||||||
|
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const morgan = require('morgan')
|
const morgan = require('morgan');
|
||||||
|
const handlebars = require('express-handlebars');
|
||||||
|
const multer = require('multer');
|
||||||
|
const uuid = require('uuid/v4');
|
||||||
|
|
||||||
|
var storage = multer.diskStorage({
|
||||||
|
destination: function (req, file, cb) {
|
||||||
|
cb(null, config.media_path)
|
||||||
|
},
|
||||||
|
filename: function (req, file, cb) {
|
||||||
|
var track_uuid = uuid();
|
||||||
|
cb(null, track_uuid)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var upload = multer({
|
||||||
|
storage: storage,
|
||||||
|
});
|
||||||
|
|
||||||
const { createLogger, format, transports } = require('winston');
|
const { createLogger, format, transports } = require('winston');
|
||||||
|
|
||||||
@ -29,6 +42,12 @@ const MediaPlayer = require('./jukebox/media-player')(config, logger);
|
|||||||
const TagReader = require('./jukebox/tag-reader')(config, logger);
|
const TagReader = require('./jukebox/tag-reader')(config, logger);
|
||||||
const play_log = require('./jukebox/library/play-log');
|
const play_log = require('./jukebox/library/play-log');
|
||||||
|
|
||||||
|
const sqlite3 = require('sqlite3');
|
||||||
|
var db = new sqlite3.Database(config.db);
|
||||||
|
|
||||||
|
const MediaLibrarySqliteBackend = require('./jukebox/library/sqlite-backend')(config, db, logger);
|
||||||
|
const MediaLibrary = new library.Library(config, MediaLibrarySqliteBackend);
|
||||||
|
|
||||||
const PlayLog = new play_log.PlayLog();
|
const PlayLog = new play_log.PlayLog();
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
@ -55,6 +74,7 @@ function exitHandler(options, err) {
|
|||||||
|
|
||||||
process.on('exit', exitHandler.bind(null));
|
process.on('exit', exitHandler.bind(null));
|
||||||
process.on('SIGINT', exitHandler.bind(null, {exit:true}));
|
process.on('SIGINT', exitHandler.bind(null, {exit:true}));
|
||||||
|
process.on('SIGUSR2', exitHandler.bind(null, {exit:true}));
|
||||||
|
|
||||||
const throttledTag = throttle(config.global_throttle, true, tag => {
|
const throttledTag = throttle(config.global_throttle, true, tag => {
|
||||||
ScriptRunner.find(tag).then((fulfilled) => {
|
ScriptRunner.find(tag).then((fulfilled) => {
|
||||||
@ -85,37 +105,74 @@ MediaPlayer.on('command', tag => {
|
|||||||
PlayLog.updateLog(tag);
|
PlayLog.updateLog(tag);
|
||||||
});
|
});
|
||||||
|
|
||||||
PlayLog.on('update', tag => {
|
var hbs = handlebars.create({
|
||||||
var data = {
|
extname: ".hbs",
|
||||||
html: views.log.render({ play_log: PlayLog.getLog() }),
|
|
||||||
tag: tag.tag
|
|
||||||
};
|
|
||||||
|
|
||||||
wss.broadcast(JSON.stringify(data));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
MediaLibrary.on('action', MediaPlayer.handleTag.bind(MediaPlayer));
|
MediaLibrary.on('action', MediaPlayer.handleTag.bind(MediaPlayer));
|
||||||
|
|
||||||
|
app.engine(".hbs", hbs.engine);
|
||||||
|
|
||||||
|
app.set('view engine', '.hbs');
|
||||||
|
app.set('views', __dirname + '/views');
|
||||||
|
|
||||||
app.use(morgan('dev'))
|
app.use(morgan('dev'))
|
||||||
|
app.use(express.json())
|
||||||
app.use(express.static(__dirname + '/static'))
|
app.use(express.static(__dirname + '/static'))
|
||||||
|
|
||||||
app.get('/', function (req, res, next) {
|
PlayLog.on('update', tag => {
|
||||||
try {
|
app.render("log", { layout: false, play_log: PlayLog.getLog(), }, (err, html) => {
|
||||||
var index = views.index.render({
|
var data = {
|
||||||
last_tag: PlayLog.getLastTag()
|
html: html,
|
||||||
, config: config
|
tag: tag.tag
|
||||||
, play_log: PlayLog.getLog()
|
};
|
||||||
|
|
||||||
|
wss.broadcast(JSON.stringify(data));
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
var html = views.base.render({
|
app.get('/', function(req, res, next) {
|
||||||
title: 'Home'
|
res.render('index', {
|
||||||
, content: index
|
last_tag: PlayLog.getLastTag(),
|
||||||
})
|
config: config,
|
||||||
|
play_log: PlayLog.getLog(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
res.send(html)
|
app.get('/api/tags', function(req, res, next) {
|
||||||
} catch (e) {
|
db.all("SELECT tags.tag, count(library.tag) tag_count FROM (SELECT DISTINCT tag FROM tags) tags LEFT JOIN library ON tags.tag = library.tag GROUP BY tags.tag", (err, rows) => {
|
||||||
next(e)
|
res.send(rows);
|
||||||
}
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.patch('/api/tracks/:track', function(req, res, next) {
|
||||||
|
var track_uuid = req.params.track,
|
||||||
|
label = req.body.label,
|
||||||
|
tag = req.body.tag;
|
||||||
|
|
||||||
|
db.run("UPDATE library SET label = ?, tag = ? WHERE uuid = ?", label, tag, track_uuid, err => {
|
||||||
|
db.get("SELECT * FROM library WHERE uuid = ?", track_uuid, (err, row) => {
|
||||||
|
res.send(row);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/tracks', function(req, res, next) {
|
||||||
|
db.all("SELECT * FROM library ORDER BY label ASC", (err, rows) => {
|
||||||
|
res.send(rows);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/api/tracks', upload.single('track'), function(req, res, next) {
|
||||||
|
db.run("INSERT INTO library (label, uuid) VALUES (?, ?)", req.file.originalname, req.file.filename, err => {
|
||||||
|
res.redirect('/library');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/library', function(req, res, next) {
|
||||||
|
res.render('library', {
|
||||||
|
title: 'Library',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
wss.broadcast = function broadcast(data) {
|
wss.broadcast = function broadcast(data) {
|
||||||
@ -126,16 +183,16 @@ wss.broadcast = function broadcast(data) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
wss.on('connection', function (ws) {
|
wss.on('connection', function(ws) {
|
||||||
logger.info('websocket client connected');
|
logger.info('websocket client connected');
|
||||||
ws.on('close', function () {
|
ws.on('close', function() {
|
||||||
logger.info('websocket client disconnected');
|
logger.info('websocket client disconnected');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on('request', app);
|
server.on('request', app);
|
||||||
|
|
||||||
server.listen(config.port, function () {
|
server.listen(config.port, function() {
|
||||||
logger.info('express listening on http://localhost:' + config.port)
|
logger.info('express listening on http://localhost:' + config.port)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,56 +1,54 @@
|
|||||||
@import url("https://fonts.googleapis.com/css?family=Source+Code+Pro:400,200,700");
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
font-family: 'Source Code Pro', monospace;
|
font-family: Arial;
|
||||||
|
margin: 1em;
|
||||||
}
|
}
|
||||||
.header {
|
h1 {
|
||||||
background: url("https://images.unsplash.com/photo-1451337516015-6b6e9a44a8a3?crop=entropy&fit=crop&fm=jpg&h=975&ixjsv=2.1.0&ixlib=rb-0.3.5&q=80&w=1925");
|
font-size: 2rem;
|
||||||
background-position: center;
|
margin: 0.5em 0;
|
||||||
}
|
}
|
||||||
.header .page-title {
|
h1 a, h1 a:active, h1 a:visited, h1 a:hover {
|
||||||
font-weight: 200;
|
color: hsl(182.8, 70.2%, 42.2%);
|
||||||
font-size: 80px;
|
|
||||||
text-align: center;
|
|
||||||
padding: 100px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.nav {
|
|
||||||
background: #000;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.nav li {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
.nav a {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 20px;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
.nav a:hover {
|
h2 {
|
||||||
background-color: #3ab795;
|
margin-bottom: 0.5ex;
|
||||||
text-decoration: underline;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
.main-content {
|
li {
|
||||||
margin: 50px auto;
|
margin-left: 1em;
|
||||||
max-width: 600px;
|
|
||||||
}
|
}
|
||||||
.main-content p {
|
p {
|
||||||
line-height: 1.6;
|
margin: 1em 0;
|
||||||
margin-bottom: 1.7em;
|
|
||||||
}
|
}
|
||||||
.footer {
|
footer {
|
||||||
margin: 50px auto;
|
margin-top: 2em;
|
||||||
max-width: 600px;
|
font-size: 0.75rem;
|
||||||
border-top: 1px solid #444;
|
|
||||||
padding: 20px 0;
|
|
||||||
}
|
}
|
||||||
.footer p {
|
.media-track {
|
||||||
color: #444;
|
margin: 1ex 0;
|
||||||
font-size: 12px;
|
padding: 1ex;
|
||||||
|
border: 1px solid hsl(0, 0%, 88.2%);
|
||||||
|
}
|
||||||
|
.media-track-highlighted {
|
||||||
|
background-color: hsl(81, 100%, 96.1%);
|
||||||
|
border-color: hsl(118.1, 72%, 58%);
|
||||||
|
border-style: dotted;
|
||||||
|
}
|
||||||
|
.media-track-tagless {
|
||||||
|
background-color: hsl(0, 100%, 95.7%);
|
||||||
|
border-color: hsl(0, 79.5%, 67.5%);
|
||||||
|
border-style: dashed;
|
||||||
|
}
|
||||||
|
option.unused {
|
||||||
|
font-style: italic;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
.upload-box {
|
||||||
|
margin: 2em 0;
|
||||||
|
padding: 1ex;
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@ window.onload = function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setInterval(function() {
|
setInterval(function() {
|
||||||
if (ws.readyState == WebSocket.CLOSED) {
|
if (ws && ws.readyState == WebSocket.CLOSED) {
|
||||||
connectWebsocket();
|
connectWebsocket();
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
161
static/library.js
Normal file
161
static/library.js
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
(function() {
|
||||||
|
|
||||||
|
var appData = {
|
||||||
|
tracks: [],
|
||||||
|
tags: [],
|
||||||
|
highlightedTag: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const MediaTrack = {
|
||||||
|
props: [
|
||||||
|
'track',
|
||||||
|
'tags',
|
||||||
|
'highlightedTag',
|
||||||
|
],
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
editing: false,
|
||||||
|
saving: false,
|
||||||
|
trackForReset: {},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
patch_url: function() {
|
||||||
|
return '/api/tracks/' + this.track.uuid;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
edit: function() {
|
||||||
|
if (this.editing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.trackForReset = Object.assign({}, this.track);
|
||||||
|
this.saving = false;
|
||||||
|
this.editing = true;
|
||||||
|
},
|
||||||
|
cancel: function() {
|
||||||
|
this.track = Object.assign({}, this.trackForReset);
|
||||||
|
this.editing = false;
|
||||||
|
},
|
||||||
|
save: function() {
|
||||||
|
this.saving = true;
|
||||||
|
|
||||||
|
var request = new Request(this.patch_url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
tag: this.track.tag,
|
||||||
|
label: this.track.label,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(request)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
this.editing = false;
|
||||||
|
this.track.label = response.label;
|
||||||
|
this.track.tag = response.tag;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
highlightTag: function() {
|
||||||
|
this.$root.highlightedTag = this.track.tag;
|
||||||
|
},
|
||||||
|
highlightNone: function() {
|
||||||
|
this.$root.highlightedTag = null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="media-track"
|
||||||
|
:class="{ 'media-track-highlighted': track.tag === highlightedTag, 'media-track-tagless': track.tag === '' }"
|
||||||
|
@click="edit()"
|
||||||
|
@mouseover="highlightTag()"
|
||||||
|
@mouseout="highlightNone()">
|
||||||
|
<template v-if="editing">
|
||||||
|
<form method="patch" :action="patch_url" @keyup.27="cancel">
|
||||||
|
<select v-model="track.tag" :disabled="saving">
|
||||||
|
<option value=""></option>
|
||||||
|
<option :class="{unused: tag.tag_count == 0}" v-for="tag in tags">
|
||||||
|
{{ tag.tag }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" v-model="track.label" :disabled="saving">
|
||||||
|
<input type="submit" value="Save" @click.prevent.stop.once="save()" :disabled="saving">
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<template v-else>{{ track.label }}</template>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
const MediaLibrary = {
|
||||||
|
props: ['tracks', 'tags', 'highlightedTag'],
|
||||||
|
components: {
|
||||||
|
'media-track': MediaTrack,
|
||||||
|
},
|
||||||
|
template: `
|
||||||
|
<div class="media-library">
|
||||||
|
<media-track v-for="track in tracks"
|
||||||
|
:key="track.uuid"
|
||||||
|
:track="track"
|
||||||
|
:tags="tags"
|
||||||
|
:highlightedTag="highlightedTag"
|
||||||
|
></media-track>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const UploadBox = {
|
||||||
|
template: `
|
||||||
|
<div class="upload-box">
|
||||||
|
<h2>Upload File</h2>
|
||||||
|
<form method="post" action="/api/tracks" enctype="multipart/form-data">
|
||||||
|
<input name="track" type="file">
|
||||||
|
<input type="submit" value="Upload">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
};
|
||||||
|
|
||||||
|
var app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: appData,
|
||||||
|
components: {
|
||||||
|
'media-library': MediaLibrary,
|
||||||
|
'upload-box': UploadBox,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = new Request('/api/tags');
|
||||||
|
fetch(request)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
appData.tags = response;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
var request = new Request('/api/tracks');
|
||||||
|
fetch(request)
|
||||||
|
.then(response => {
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
appData.tracks = response;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
// vim:ts=2 sw=2 et:
|
11965
static/vue.debug.js
Normal file
11965
static/vue.debug.js
Normal file
File diff suppressed because it is too large
Load Diff
@ -4,6 +4,9 @@
|
|||||||
{{> log}}
|
{{> log}}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
<p><a href="/library">Manage library</a></p>
|
||||||
|
|
||||||
|
<script src="/jukebox.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var jukebox = {
|
var jukebox = {
|
||||||
port: {{ config.port }}
|
port: {{ config.port }}
|
@ -4,19 +4,19 @@
|
|||||||
<head>
|
<head>
|
||||||
<title>jukebox.stop.wtf</title>
|
<title>jukebox.stop.wtf</title>
|
||||||
<link rel=stylesheet href=/css/index.css>
|
<link rel=stylesheet href=/css/index.css>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class=header>
|
<div class=header>
|
||||||
<h1 class=page-title>jukebox.stop.wtf</h1>
|
<h1 class=page-title><a href="/">jukebox.stop.wtf</a></h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-content">
|
<div class="main-content">
|
||||||
{{{ content }}}
|
{{{body}}}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<footer>
|
||||||
<p>made with love by annika</p>
|
<p>made with love by annika</p>
|
||||||
</div>
|
</footer>
|
||||||
<script src="/jukebox.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
10
views/library.hbs
Normal file
10
views/library.hbs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<div id="app">
|
||||||
|
<media-library
|
||||||
|
:tracks="tracks"
|
||||||
|
:tags="tags"
|
||||||
|
:highlighted-tag="highlightedTag"
|
||||||
|
></media-library>
|
||||||
|
<upload-box></upload-box>
|
||||||
|
</div>
|
||||||
|
<script src="/vue.debug.js"></script>
|
||||||
|
<script src="/library.js"></script>
|
2
views/log.hbs
Normal file
2
views/log.hbs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
{{! Used for rending *just* the log line partial. }}
|
||||||
|
{{> log}}
|
Loading…
Reference in New Issue
Block a user