Refactor to be service-core compatible

master
Jonatan Nilsson 2020-12-08 11:09:46 +00:00
parent 1ac61de439
commit 671c2d177f
23 changed files with 570 additions and 451 deletions

View File

@ -3,21 +3,36 @@ jobs:
build:
docker:
- image: circleci/node:latest
working_directory: ~/caspar-sup
working_directory: ~/app
steps:
- checkout
- setup_remote_docker
- run:
name: Build docker image
command: docker build -t nfpis/caspar-sup:build_${CIRCLE_BUILD_NUM} -t nfpis/caspar-sup:${CIRCLE_SHA1} -t nfpis/caspar-sup:latest .
- deploy:
name: Push to docker
name: Install npm deployment app
command: sudo npm install -g github-release-cli
- run:
name: Build client javascript
command: |
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker push nfpis/caspar-sup
npm install
npm run build
- deploy:
name: Create a release
command: |
PACKAGE_VERSION=$(cat package.json | grep version | head -1 | awk -F: '{ print $2 }' | sed 's/[", ]//g')
echo "Packaging to ${CIRCLE_PROJECT_REPONAME}_build-sc.zip"
zip "${CIRCLE_PROJECT_REPONAME}_build-sc.zip" index.mjs package.json public/* api/**/* api/*
echo "Creating release '${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}'"
github-release upload \
--commitish $CIRCLE_SHA1 \
--token $GITHUB_TOKEN \
--owner $CIRCLE_PROJECT_USERNAME \
--repo $CIRCLE_PROJECT_REPONAME \
--tag "v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}" \
--name "v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM}" \
--body "Automatic CircleCI Build of v${PACKAGE_VERSION}.${CIRCLE_BUILD_NUM} from ${CIRCLE_SHA1}" \
caspar_sup-sc.zip
workflows:
version: 2
build_deploy:
jobs:
- build:
context: org-global
context: github-thething

View File

@ -1,69 +1,219 @@
import CasparConnection from 'casparcg-connection'
const CasparCG = CasparConnection.CasparCG
const AMCP = CasparConnection.AMCP
const timeoutDuration = 5000
import net from 'net'
import parser from 'p3x-xml2json'
let io
let logger
let connection
let casparIsPlaying
let casparIsConnected
let currentHost
let client
let db
export function initialise(log, db, socket) {
io = socket
logger = log
db = db
let queue = []
let reconnectInterval = 1000
let isReconnecting = false
let connected = false
let playing = false
let lastError = ''
connect(db)
function startReconnecting() {
connected = false
playing = false
if (queue.length) {
queue.splice(0, queue.length)
}
if(isReconnecting !== false) return
reconnectInterval = Math.min(reconnectInterval * 1.5, 1000 * 60 * 5)
isReconnecting = setTimeout(connect, reconnectInterval)
}
export function connect(db) {
function clearReconnect() {
if(isReconnecting === false) return
clearTimeout(isReconnecting)
isReconnecting = false
}
export function queueCommand(command) {
return new Promise((res, rej) => {
if (isReconnecting) {
return rej(new Error('CasparCG is not connected, unable to play command'))
}
let request = {
command: command,
res: res,
rej: rej,
started: new Date(),
finished: null,
timeout: null,
}
queue.push(request)
request.timeout = setTimeout(function() {
if (request.finished) return
queue.splice(queue.indexOf(request), 1)
rej(new Error(`CasparCGCommand "${command}" timed out after 15 seconds`))
}, 15000)
logger.info('CasparCG Command:', command)
client.write(command + '\r\n')
})
}
export async function checkPlaying(db, io, wasSuccess) {
if (!connected) return
let path = `http://${db.get('settings.casparplayhost').value()}/client.html`
try {
logger.info('CasparCG: Checking if already playing')
let res = await queueCommand('INFO 1-100')
if (res.body.channel
&& res.body.channel.stage
&& res.body.channel.stage.layer
&& res.body.channel.stage.layer.layer_100
&& res.body.channel.stage.layer.layer_100.foreground
&& res.body.channel.stage.layer.layer_100.foreground.file
&& res.body.channel.stage.layer.layer_100.foreground.file.path === path) {
logger.info('CasparCG: Player is playing')
playing = true
lastError = ''
io.emit('casparcg.status', currentStatus())
return
}
if (wasSuccess) {
logger.warn(res.body, 'CasparCG: Playing was marked as succeeded but could not verify it')
playing = true
lastError = 'Sending play command succeeded but was unable to verify'
io.emit('casparcg.status', currentStatus())
return
}
playing = false
lastError = 'Sending play command'
io.emit('casparcg.status', currentStatus())
logger.info(res.body, 'CasparCG: Sending play command')
res = await queueCommand(`PLAY 1-100 [HTML] "${path}" CUT 1 LINEAR RIGHT`)
return setTimeout(function() {
checkPlaying(db, io, true).then()
}, 300)
} catch (err) {
playing = false
lastError = `CasparCG: Error checking if playing: ${err.message}. Checking again in 5seconds`
logger.error(err, 'CasparCG: Error checking if playing')
io.emit('casparcg.status', currentStatus())
}
return setTimeout(function() {
checkPlaying(db, io, false).then()
}, 5000)
}
export function initialise(log, database, ioOrg) {
io = ioOrg
logger = log
db = database
client = new net.Socket()
client.setEncoding('utf8')
client.on('connect', function () {
clearReconnect()
connected = true
lastError = ''
reconnectInterval = 1000
logger.info('CasparCG: Connected to server')
io.emit('casparcg.status', currentStatus())
checkPlaying(db, io, false).then()
// client.write('INFO 1-100\r\n');
})
client.on('data', function (data) {
let request = null
if (queue.length > 0) {
request = queue[0]
queue.splice(0, 1)
}
if (!request) {
return logger.warn({ data }, 'Received unknown response with no command')
}
let status
let splitted
let header
let body
let parsed
try {
splitted = data.split('\n')
header = splitted[0].replace('\r', '')
status = Number(header.split(' ')[0])
body = splitted.slice(1)
parsed = JSON.parse(parser.toJson(body.join('\n')))
} catch (err) {
return request.rej(err)
}
request.finished = new Date()
clearTimeout(request.timeout)
if (status && status < 300) {
request.res({
status: status,
header: header,
body: parsed || {},
raw: data
})
} else {
request.err({
status: status,
header: header,
body: parsed || {},
raw: data
})
}
})
client.on('error', function (err) {
lastError = 'CasparCG TCP Error: ' + err.code + ', retrying in ' + Math.round(reconnectInterval / 1000) + ' sec'
logger.warn(lastError)
io.emit('casparcg.status', currentStatus())
startReconnecting()
})
client.on('close', function() {
startReconnecting()
})
client.on('end', function() {
startReconnecting()
})
connect()
}
export function connect() {
clearReconnect()
currentHost = db.get('settings').value().casparhost
casparIsPlaying = false
casparIsConnected = false
logger.info('CasparCG: Connectiong to', currentHost + ':' + 5250)
lastError = 'CasparCG: Connecting to ' + currentHost + ':' + 5250
logger.info(lastError)
io.emit('casparcg.status', currentStatus())
connection = new CasparCG({
host: currentHost,
client.connect({
port: 5250,
queueMode: 2,
autoReconnectInterval: timeoutDuration,
onError: err => {
logger.error(err, 'CasparCG: Error')
},
onConnectionStatus: data => {
if (casparIsPlaying) return
casparIsConnected = data.connected
if (!casparIsConnected) {
logger.warn(`CasparCG: connection down, retrying in ${timeoutDuration / 1000} seconds`)
io.emit('casparcg.status', currentStatus())
}
},
onConnected: async connected => {
if (casparIsPlaying) return
logger.info('CasparCG: connected', connected)
if (!casparIsPlaying) {
startPlaying(db).then()
} else {
logger.warn('CasparCG: Stopped from starting play again.')
}
},
host: currentHost
})
}
export function currentStatus(e) {
return {
connected: casparIsConnected,
playing: casparIsPlaying,
error: e,
connected: connected,
playing: playing,
error: lastError,
}
}
export function sendCommand(command) {
return new Promise(function(res, rej) {
})
}
/*
export async function startPlaying(db) {
let ip = db.get('settings').value().casparplayhost
@ -96,9 +246,9 @@ export async function startPlaying(db) {
io.emit('casparcg.status', currentStatus())
logger.info('CasparCG: client is up and playing')
/* console.log(connection)
for (var key in connection) {
for (let key in connection) {
console.log(key, '=', typeof(connection[key]))
} */
}
connection.autoConnect = false
// connection.close()
} else {
@ -106,4 +256,4 @@ export async function startPlaying(db) {
casparIsPlaying = false
io.emit('casparcg.status', currentStatus(e))
}
}
}*/

View File

@ -1,5 +1,5 @@
import { currentStatus } from './client.mjs'
export async function casparConnection(ctx) {
export async function casparStatus(ctx) {
ctx.socket.emit('casparcg.status', currentStatus())
}

View File

@ -1,67 +0,0 @@
import nconf from 'nconf'
import { readFileSync } from 'fs'
// Helper method for global usage.
nconf.inTest = () => nconf.get('NODE_ENV') === 'test'
// Config follow the following priority check order:
// 1. Arguments
// 2. package.json
// 3. Enviroment variables
// 4. config/config.json
// 5. default settings
// Load arguments as highest priority
nconf.argv()
// Load package.json for name and such
let pckg = JSON.parse(readFileSync('./package.json'))
let project = {
name: pckg.name,
version: pckg.version,
description: pckg.description,
author: pckg.author,
license: pckg.license,
homepage: pckg.homepage,
}
// If we have global.it, there's a huge chance
// we're in test mode so we force node_env to be test.
if (typeof global.it === 'function') {
project.NODE_ENV = 'test'
}
// Load overrides as second priority
nconf.overrides(project)
// Load enviroment variables as third priority
nconf.env()
// Load any overrides from the appropriate config file
let configFile = 'config/config.json'
if (nconf.get('NODE_ENV') === 'test') {
configFile = 'config/config.test.json'
}
nconf.file('main', configFile)
// Load defaults
nconf.file('default', 'config/config.default.json')
// Final sanity checks
/* istanbul ignore if */
if (typeof global.it === 'function' & !nconf.inTest()) {
// eslint-disable-next-line no-console
console.log('Critical: potentially running test on production enviroment. Shutting down.')
process.exit(1)
}
export default nconf

View File

@ -1,162 +0,0 @@
import lowdb from 'lowdb'
import FileAsync from 'lowdb/adapters/FileAsync.js'
import log from './log.mjs'
let lastId = -1
// Take from https://github.com/typicode/lodash-id/blob/master/src/index.js
// from package lodash-id
const lodashId = {
// Empties properties
__empty: function (doc) {
this.forEach(doc, function (value, key) {
delete doc[key]
})
},
// Copies properties from an object to another
__update: function (dest, src) {
this.forEach(src, function (value, key) {
dest[key] = value
})
},
// Removes an item from an array
__remove: function (array, item) {
var index = this.indexOf(array, item)
if (index !== -1) array.splice(index, 1)
},
__id: function () {
var id = this.id || 'id'
return id
},
getById: function (collection, id) {
var self = this
return this.find(collection, function (doc) {
if (self.has(doc, self.__id())) {
return doc[self.__id()] === id
}
})
},
createId: function (collection, doc) {
let next = new Date().getTime()
if (next <= lastId) {
next = lastId + 1
}
lastId = next
return next
},
insert: function (collection, doc) {
doc[this.__id()] = doc[this.__id()] || this.createId(collection, doc)
var d = this.getById(collection, doc[this.__id()])
if (d) throw new Error('Insert failed, duplicate id')
collection.push(doc)
return doc
},
upsert: function (collection, doc) {
if (doc[this.__id()]) {
// id is set
var d = this.getById(collection, doc[this.__id()])
if (d) {
// replace properties of existing object
this.__empty(d)
this.assign(d, doc)
} else {
// push new object
collection.push(doc)
}
} else {
// create id and push new object
doc[this.__id()] = this.createId(collection, doc)
collection.push(doc)
}
return doc
},
updateById: function (collection, id, attrs) {
var doc = this.getById(collection, id)
if (doc) {
this.assign(doc, attrs, {id: doc.id})
}
return doc
},
updateWhere: function (collection, predicate, attrs) {
var self = this
var docs = this.filter(collection, predicate)
docs.forEach(function (doc) {
self.assign(doc, attrs, {id: doc.id})
})
return docs
},
replaceById: function (collection, id, attrs) {
var doc = this.getById(collection, id)
if (doc) {
var docId = doc.id
this.__empty(doc)
this.assign(doc, attrs, {id: docId})
}
return doc
},
removeById: function (collection, id) {
var doc = this.getById(collection, id)
this.__remove(collection, doc)
return doc
},
removeWhere: function (collection, predicate) {
var self = this
var docs = this.filter(collection, predicate)
docs.forEach(function (doc) {
self.__remove(collection, doc)
})
return docs
}
}
const adapter = new FileAsync('db.json')
export default function GetDB() {
return lowdb(adapter)
.then(function(db) {
db._.mixin(lodashId)
db.defaults({
graphics: [],
presets: [],
playing: [],
schedule: [],
settings: {
casparplayhost: 'localhost:3000',
casparhost: 'host.docker.internal',
},
version: 1,
trash: [],
})
.write()
.then(
function() { },
function(e) { log.error(e, 'Error writing defaults to lowdb') }
)
return db
})
}

View File

@ -45,7 +45,7 @@ export async function create(ctx, data) {
await graphics.insert(data).write()
let graphic = graphics.last().value()
ctx.io.emit('graphic.single', graphic)
ctx.io.emit('graphic.created', graphic)
}
/*

View File

@ -7,7 +7,11 @@ export function register(ctx, name, method) {
}
ctx.socket.on(name, async (data) => {
ctx.log.info('Got event', name)
if (name.indexOf('list') > 0 || name.indexOf('all') || name.indexOf('total')) {
ctx.log.debug('Got event', name)
} else {
ctx.log.info('Got event', name)
}
try {
await method(ctx, data)

View File

@ -1,20 +0,0 @@
import bunyan from 'bunyan-lite'
import defaults from './defaults.mjs'
import config from './config.mjs'
// Clone the settings as we will be touching
// on them slightly.
let settings = defaults(config.get('bunyan'), null)
// Replace any instance of 'process.stdout' with the
// actual reference to the process.stdout.
for (let i = 0; i < settings.streams.length; i++) {
if (settings.streams[i].stream === 'process.stdout') {
settings.streams[i].stream = process.stdout
}
}
// Create our logger.
const logger = bunyan.createLogger(settings)
export default logger

View File

@ -1,7 +1,6 @@
import logger from './log.mjs'
import { register } from './io/helper.mjs'
import { contentConnection } from './content/connection.mjs'
import { casparConnection } from './casparcg/connection.mjs'
import { casparStatus } from './casparcg/status.mjs'
import * as content from './content/routes.mjs'
import * as engine from './engine/routes.mjs'
@ -10,7 +9,7 @@ import * as preset from './preset/routes.mjs'
import * as settings from './settings/routes.mjs'
import * as schedule from './schedule/routes.mjs'
function onConnection(server, db, data) {
function onConnection(server, db, logger, data) {
const io = server
const socket = data
const log = logger.child({
@ -20,7 +19,7 @@ function onConnection(server, db, data) {
let ctx = { io, socket, log, db }
contentConnection(ctx)
casparConnection(ctx)
casparStatus(ctx)
register(ctx, 'content', content)
register(ctx, 'engine', engine)

View File

@ -1,30 +1,53 @@
import path from 'path'
import { fileURLToPath } from 'url'
import socket from 'socket.io-serveronly'
import http from 'http'
import nStatic from 'node-static'
import * as casparcg from './casparcg/client.mjs'
import nStatic from 'node-static-lib'
import lowdb from './db.mjs'
import config from './config.mjs'
import log from './log.mjs'
import * as casparcg from './casparcg/client.mjs'
import onConnection from './routerio.mjs'
log.info('Server: Opening database db.json')
export function run(config, db, log, core, http, orgPort) {
log.info('Server: Opening database db.json')
db.defaults({
graphics: [],
presets: [],
playing: [],
schedule: [],
settings: {
casparplayhost: 'localhost:3000',
casparhost: 'localhost',
},
version: 1,
trash: [],
})
.write()
.then(
function() { },
function(e) { log.error(e, 'Error writing defaults to lowdb') }
)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const staticRoot = path.join(__dirname,'../public')
const fileServer = new nStatic.Server(staticRoot)
lowdb().then(function(db) {
const fileServer = new nStatic.Server('./public')
const server = http.createServer(function (req, res) {
const child = log.child({})
const d1 = new Date().getTime()
let isFinished = false
var done = function () {
if (isFinished) return
isFinished = true
var requestTime = new Date().getTime() - d1
let level = 'info'
if (res.status >= 400) {
let level = 'debug'
if (res.statusCode >= 400) {
level = 'warn'
}
if (res.status >= 500) {
if (res.statusCode >= 500) {
level = 'error'
}
@ -45,7 +68,9 @@ lowdb().then(function(db) {
fileServer.serve(req, res, function (err) {
if (err) {
log.error(err);
if (err.status !== 404) {
log.error(err, req.url);
}
res.writeHead(err.status, err.headers);
res.end(err.message);
@ -55,21 +80,20 @@ lowdb().then(function(db) {
})
const io = new socket(server)
io.on('connection', onConnection.bind(this, io, db))
io.on('connection', onConnection.bind(this, io, db, log))
casparcg.initialise(log, db, io)
server.listen(config.get('server:port'), '0.0.0.0', function(err) {
if (err) {
log.fatal(err)
return process.exit(2)
}
log.info(`Server is listening on ${config.get('server:port')}`)
let port = orgPort || 3000
return new Promise(function(resolve, reject) {
server.listen(port, '0.0.0.0', function(err) {
if (err) {
return reject(err)
}
log.event.info(`Server is listening on ${port} serving files on ${staticRoot}`)
log.info(`Server is listening on ${port} serving files on ${staticRoot}`)
resolve()
})
})
}, function(e) {
log.fatal(e, 'Critical error loading database')
process.exit(1)
}).catch(function(e) {
log.fatal(e, 'Critical error starting server')
process.exit(1)
})
}

View File

@ -25,12 +25,12 @@ export async function update(ctx, data) {
return
}
await Settings.setValue(data.name, data.value)
await ctx.db.set('settings.' + data.name, data.value).write()
let output = await Settings.getSettings()
let output = ctx.db.get('settings').value()
ctx.io.emit('settings.all', output)
if (data.name === 'casparcg') {
if (data.name.startsWith('caspar')) {
connect()
}
}

View File

@ -1,18 +1,30 @@
const m = require('mithril')
const createModule = require('../common/module')
const Module = require('../module')
const components = require('../common/components')
const socket = require('../../shared/socket')
const store = require('../store')
const Add = createModule({
const Add = Module({
init: function() {
this.monitor('engines', 'engine.all', [])
store.listen('graphic.single', data => {
if (data.name === this.graphic.name) {
m.route.set(`/graphic/${data.id}`)
this.engines = []
this.graphic = { }
this._socketOn(() => this.socketOpen())
},
socketOpen: function() {
socket.on('engine.all', (res) => {
this.engines = res
m.redraw()
})
socket.on('graphic.created', (res) => {
if (res.name === this.graphic.name) {
m.route.set(`/graphic/${res.id}`)
}
})
this.graphic = { }
socket.emit('engine.all', {})
},
updated: function(name, control) {
@ -34,24 +46,25 @@ const Add = createModule({
removing: function() {
store.unlisten('graphic.single')
},
}, function() {
return [
m('h4.header', 'Add graphic'),
components.error(this.error),
m('label', { for: 'create-name' }, 'Name'),
m('input#create-name[type=text]', {
oninput: (control) => this.updated('name', control),
}),
m('label', { for: 'create-engine' }, 'Engine'),
m('select', {
onchange: (control) => this.updated('engine', control),
}, this.engines.map(engine =>
m('option', { key: engine, value: engine }, engine)
)),
m('input[type=submit]', {
value: 'Create',
onclick: () => this.create(),
}),
]
view: function() {
return [
m('h4.header', 'Add graphic'),
components.error(this.error),
m('label', { for: 'create-name' }, 'Name'),
m('input#create-name[type=text]', {
oninput: (control) => this.updated('name', control),
}),
m('label', { for: 'create-engine' }, 'Engine'),
m('select', {
onchange: (control) => this.updated('engine', control),
}, this.engines.map(engine =>
m('option', { key: engine, value: engine }, engine)
)),
m('input[type=submit]', {
value: 'Create',
onclick: () => this.create(),
}),
]
},
})
module.exports = Add

View File

@ -7,10 +7,9 @@ exports.error = function(error) {
}
exports.presetOnlyList = function(module, graphic, title, color = 'green', button = 'Display now', schedule = 'Schedule') {
return [
m('label.graphic-label', { key: 'first' }, title),
return m('div', [
m('label.graphic-label', title),
m('div.graphic-presetlist', {
key: `second-${graphic.id}`,
oncreate: control => module.presetlistInit(control),
},
module.presets.map(item =>
@ -37,10 +36,9 @@ exports.presetOnlyList = function(module, graphic, title, color = 'green', butto
),
module.presets.length &&
m('button.red.graphic-presetremove', {
key: 'third',
onclick: () => (module.displayRemove = !module.displayRemove),
}, 'Remove entries') || null,
]
])
}
exports.presetButtons = function(module, green, blue) {

View File

@ -105,7 +105,7 @@ exports.settings = function(module, graphic) {
m('div.graphic-property', [
m('input#graphic-newproperty[type=text]', {
value: module.newProperty,
oninput: m.withAttr('value', val => (module.newProperty = val)),
oninput: (control) => { module.newProperty = control.target.value },
}),
m('button', {
onclick: module.addProperty.bind(module),

View File

@ -20,13 +20,15 @@ exports.view = function(module, graphic) {
return [
m('div.graphic-presetadd', [
m('h3.graphic-presetadd-header', 'Create preset/display graphic'),
graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
m('label', { for: `preset-add-${index}` }, prop),
m(`input#preset-add-${index}[type=text]`, {
value: module.current[prop] || '',
oninput: module.updated.bind(module, prop, 'current'),
}),
])),
m.fragment(
graphic.settings.properties.map((prop, index) => m.fragment({ key: `prop-${index}` }, [
m('label', { for: `preset-add-${index}` }, prop),
m(`input#preset-add-${index}[type=text]`, {
value: module.current[prop] || '',
oninput: module.updated.bind(module, prop, 'current'),
}),
]))
),
components.presetButtons(module, 'Display live now', 'Add to preset list'),
]),
components.presetOnlyList(module, graphic, 'Presets'),
@ -105,7 +107,7 @@ exports.settings = function(module, graphic) {
m('div.graphic-property', [
m('input#graphic-newproperty[type=text]', {
value: module.newProperty,
oninput: m.withAttr('value', val => (module.newProperty = val)),
oninput: (control) => { module.newProperty = control.target.value },
}),
m('button', {
onclick: module.addProperty.bind(module),

View File

@ -1,20 +1,54 @@
const m = require('mithril')
const createModule = require('./common/module')
const Module = require('./module')
// const createModule = require('./common/module')
const socket = require('../shared/socket')
const Menu = createModule({
const Menu = Module({
init: function() {
this.monitor('list', 'graphic.all', [])
this.monitor('settings', 'settings.all', {})
this.monitor('schedule', 'schedule.total', { total: 0 })
this.monitor('status', 'casparcg.status', {
this.list = []
this.settings = {}
this.totalSchedule = 0
this.status = {
connected: false,
playing: false,
})
error: '',
}
this._socketOn(() => this.socketOpen())
this.newHost = ''
this.enableEdit = false
},
socketOpen: function() {
socket.on('graphic.all', (res) => {
this.list = res
m.redraw()
})
socket.on('graphic.created', (res) => {
this.list.push(res)
m.redraw()
})
this.on('settings.all', (res) => {
this.settings = res
m.redraw()
})
this.on('schedule.total', (res) => {
this.totalSchedule = res.total
m.redraw()
})
this.on('casparcg.status', (res) => {
this.status = res
m.redraw()
})
socket.emit('graphic.all', {})
socket.emit('settings.all', {})
socket.emit('schedule.total', {})
socket.emit('casparcg.status', {})
},
setHost(value) {
this.newHost = value
this.enableEdit = true
@ -29,42 +63,41 @@ const Menu = createModule({
this.newHost = ''
this.enableEdit = false
},
}, function() {
return [
m('a', {
href: '/',
oncreate: m.route.link,
class: m.route.get() === '/' && 'active' || '',
}, `Schedule (${this.schedule.total})` ),
m('h4.header.header--space', 'Graphics'),
this.list.map((item) =>
m('a', {
href: `/graphic/${item.id}`,
oncreate: m.route.link,
class: m.route.get() === `/graphic/${item.id}` && 'active' || '',
}, item.name)
),
m('h5.header.header--space', 'Other'),
m('a', {
href: '/add',
oncreate: m.route.link,
class: m.route.get() === '/add' && 'active' || '',
}, 'Add graphic' ),
m('h5.header.header--space', 'CasparCG Status'),
m('input[type=text]', {
placeholder: 'Host IP',
value: this.newHost || this.settings.casparhost || '',
oninput: control => this.setHost(control.target.value),
}),
this.enableEdit && m('button', {
onclick: () => this.saveNewHost(),
}, 'Connect'),
m('div.status', {
class: this.status.connected && 'green',
}, 'connected'),
m('div.status', {
class: this.status.playing && 'green',
}, 'playing'),
]
view: function() {
return [
m(m.route.Link, {
href: '/',
class: m.route.get() === '/' && 'active' || '',
}, `Schedule (${this.totalSchedule})` ),
m('h4.header.header--space', 'Graphics'),
this.list.map((item) =>
m(m.route.Link, {
href: `/graphic/${item.id}`,
class: m.route.get() === `/graphic/${item.id}` && 'active' || '',
}, item.name)
),
m('h5.header.header--space', 'Other'),
m(m.route.Link, {
href: '/add',
class: m.route.get() === '/add' && 'active' || '',
}, 'Add graphic' ),
m('h5.header.header--space', 'CasparCG Status'),
m('input[type=text]', {
placeholder: 'Host IP',
value: this.newHost || this.settings.casparhost || '',
oninput: control => this.setHost(control.target.value),
}),
this.enableEdit && m('button', {
onclick: () => this.saveNewHost(),
}, 'Connect'),
m('div.status', {
class: this.status.connected && 'green',
}, 'connected'),
m('div.status', {
class: this.status.playing && 'green',
}, 'playing'),
m('div.status-error', { hidden: !this.status.error }, this.status.error)
]
}
})
module.exports = Menu

81
app/main/module.js Normal file
View File

@ -0,0 +1,81 @@
const defaults = require('../shared/defaults')
const socket = require('../shared/socket')
// https://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript/4819886#4819886
// LOL
function is_touch_device() {
var prefixes = ' -webkit- -moz- -o- -ms- '.split(' ')
var mq = function(query) {
return window.matchMedia(query).matches
}
if (('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
return true
}
// include the 'heartz' as a way to have a non matching MQ to help terminate the join
// https://git.io/vznFH
var query = ['(', prefixes.join('touch-enabled),('), 'heartz', ')'].join('')
return mq(query)
}
module.exports = function Module(module) {
return defaults(module, {
init: function() {},
oninit: function(vnode) {
this._listeners = []
this.init(vnode)
},
_listeners: null,
_socketOn: function(cb) {
socket.on('connect', () => cb())
if (socket.connected) {
cb()
}
},
_initDragula: function(control, cb) {
let dragContainer = document.getElementById('dragcontainer')
let out = dragula([control.dom], {
mirrorContainer: dragContainer,
invalid: el => el.className !== 'graphic-preset-reorder'
&& el.className !== 'graphic-preset',
})
out.on('dragend', () => {
if (is_touch_device()) {
document.body.style.cssText = ''
window.scroll(0, document.body.data)
}
})
out.on('drag', () => {
if (is_touch_device()) {
document.body.data = window.scrollY
document.body.style.cssText = `position: fixed; left: 0; right: 0; overflow: hidden; top: -${window.scrollY}px;`
dragContainer.style.marginTop = `${document.body.data}px`
}
})
out.on('drop', (a, b, c, d) => {
cb(a, d)
})
},
on: function(name, cb) {
this._listeners.push([name, cb])
socket.on(name, cb)
},
remove: function() {},
onremove: function() {
this.remove()
if (!this._listeners) return
for (let i = 0; i < this._listeners.length; i++) {
socket.removeListener(this._listeners[0], this._listeners[1])
}
},
})
}

34
app/shared/defaults.js Normal file
View File

@ -0,0 +1,34 @@
// taken from isobject npm library
function isObject(val) {
return val != null && typeof val === 'object' && Array.isArray(val) === false
}
module.exports = function defaults(options, def) {
let out = { }
if (options) {
Object.keys(options || {}).forEach(key => {
out[key] = options[key]
if (Array.isArray(out[key])) {
out[key] = out[key].map(item => {
if (isObject(item)) return defaults(item)
return item
})
} else if (out[key] && typeof out[key] === 'object') {
out[key] = defaults(options[key], def && def[key])
}
})
}
if (def) {
Object.keys(def).forEach(function(key) {
if (typeof out[key] === 'undefined') {
out[key] = def[key]
}
})
}
return out
}

9
config.json Normal file
View File

@ -0,0 +1,9 @@
{
"name": "caspar-sup",
"serviceName": "Caspar Sup",
"description": "Caspar Sup manager",
"port": 3000,
"managePort": 3001,
"appRepository": null,
"manageRepository": null
}

View File

@ -1,9 +1,5 @@
import log from './api/log.mjs'
// Run the database script automatically.
log.info('Starting server.')
import('./api/server.mjs').catch((error) => {
log.error(error, 'Error while starting server')
process.exit(1)
})
export function start(config, db, log, core, http, port) {
return import('./api/server.mjs').then(function(module) {
return module.run(config, db, log, core, http, port)
})
}

View File

@ -9,13 +9,10 @@
"js:build:client": "asbundle app/client/index.js public/client.js",
"js:build:status": "asbundle app/status/index.js public/status.js",
"js:watch": "nodemon --watch app --exec \"npm run build\"",
"start:watch": "nodemon --experimental-modules --watch api index.mjs | bunyan -o short",
"start:watch": "nodemon --watch api --watch runner.mjs --watch index.mjs runner.mjs | bunyan",
"start": "node --experimental-modules index.mjs | bunyan -o short",
"dev": "run-p js:watch start:watch",
"build": "run-p js:build:main js:build:client js:build:status",
"docker": "docker run -it --rm --name my-running-script -p 3000:3000 -v \"%cd%\":/usr/src/app -w /usr/src/app node:alpine",
"docker:install": "npm run docker -- npm install",
"docker:dev": "npm run docker -- npm run dev"
"build": "npm run js:build:main && npm run js:build:client && npm run js:build:status"
},
"repository": {
"type": "git",
@ -33,19 +30,16 @@
},
"homepage": "https://github.com/nfp-projects/caspar-sup#readme",
"dependencies": {
"bunyan-lite": "^1.0.1",
"casparcg-connection": "4.9.0",
"lodash": "^4.5.0",
"lowdb": "^1.0.0",
"nconf": "^0.9.1",
"node-static": "^0.7.11",
"socket.io-serveronly": "^2.3.0",
"tslib": "^1.11.1"
"node-static-lib": "^1.0.0",
"p3x-xml2json": "^2020.10.131",
"socket.io-serveronly": "^2.3.0"
},
"devDependencies": {
"asbundle": "^2.6.1",
"dragula": "^3.7.2",
"mithril": "^1.1.5",
"npm-run-all": "^4.1.2"
"mithril": "^2.0.4",
"npm-run-all": "^4.1.2",
"service-core": "^2.0.0"
}
}

View File

@ -149,6 +149,12 @@ nav .status {
position: relative;
margin-left: 1.8em;
}
nav .status-error {
color: red;
font-size: 0.8rem;
text-align: center;
margin-top: 5px;
}
nav .status::after {
position: absolute;
left: 0;

10
runner.mjs Normal file
View File

@ -0,0 +1,10 @@
import ServiceCore from 'service-core'
import * as server from './index.mjs'
const serviceCore = new ServiceCore('sc-manager', import.meta.url)
serviceCore.init(server)
.then(function() {})
.catch(function(err) {
serviceCore.log.error(err, 'Unknown error starting server')
})