mirror of https://github.com/icedream/ts3bot.git
Merge pull request #20 from icedream/impl/webchimera
Use LibVLC via WebChimera.js instead of VLC through its HTTP API. This closes #17 and fixes #10, finishes two tasks of #2 and actually solves a few additional issues like: - Not being able to resume playback from a stopped playlist. - Unclear and buggy definition of volume range. - Bot just showing "playing next playlist entry" instead of actually showing the title of the next playlist entry. - Missing "previous" command to go to previous playlist entry. - Missing "loop" command to loop the playlist. (Now exists in form of "loop on"/"loop off") - Missing "stop-after" command to stop playback after current track ends. - A small bug in ts3query.iced that didn't escape line breaks. (Was in use here for "playlist"/"list" commands which got removed in the process.) Known issues: - Starting with an empty playlist, adding an entry and then typing "next" will automatically play the first entry. This is not really a bug or even an issue but definitely unexpected behavior that is coming from VLC.develop
commit
034d12df07
120
app.iced
120
app.iced
|
@ -46,13 +46,16 @@ await services.find("pulseaudio").start defer err
|
||||||
if err
|
if err
|
||||||
log.warn "PulseAudio could not start up, audio may not act as expected!"
|
log.warn "PulseAudio could not start up, audio may not act as expected!"
|
||||||
|
|
||||||
# VLC HTTP API
|
# VLC via WebChimera.js
|
||||||
await services.find("vlc").start defer err
|
vlcService = services.find("vlc")
|
||||||
|
await vlcService.start defer err, vlc
|
||||||
if err
|
if err
|
||||||
log.warn "VLC could not start up!"
|
log.warn "VLC could not start up!"
|
||||||
await module.exports.shutdown defer()
|
await module.exports.shutdown defer()
|
||||||
process.exit 1
|
process.exit 1
|
||||||
vlc = services.find("vlc").instance
|
|
||||||
|
# Cached information for tracks in playlist
|
||||||
|
vlcMediaInfo = {}
|
||||||
|
|
||||||
# TeamSpeak3
|
# TeamSpeak3
|
||||||
ts3clientService = services.find("ts3client")
|
ts3clientService = services.find("ts3client")
|
||||||
|
@ -60,6 +63,16 @@ ts3clientService = services.find("ts3client")
|
||||||
ts3clientService.on "started", (ts3proc) =>
|
ts3clientService.on "started", (ts3proc) =>
|
||||||
ts3query = ts3clientService.query
|
ts3query = ts3clientService.query
|
||||||
|
|
||||||
|
# VLC event handling
|
||||||
|
vlc.onPlaying = () =>
|
||||||
|
info = vlcMediaInfo[vlc.playlist.items[vlc.playlist.currentItem].mrl]
|
||||||
|
ts3query.sendtextmessage 2, 0, "Now playing [URL=#{info.originalUrl}]#{info.title}[/URL]."
|
||||||
|
vlc.onPaused = () => ts3query.sendtextmessage 2, 0, "Paused."
|
||||||
|
vlc.onForward = () => ts3query.sendtextmessage 2, 0, "Fast-forwarding..."
|
||||||
|
vlc.onBackward = () => ts3query.sendtextmessage 2, 0, "Rewinding..."
|
||||||
|
vlc.onEncounteredError = () => log.error "VLC has encountered an error! You will need to restart the bot.", arguments
|
||||||
|
vlc.onStopped = () => ts3query.sendtextmessage 2, 0, "Stopped."
|
||||||
|
|
||||||
ts3query.currentScHandlerID = 1
|
ts3query.currentScHandlerID = 1
|
||||||
ts3query.mydata = {}
|
ts3query.mydata = {}
|
||||||
|
|
||||||
|
@ -145,7 +158,8 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
|
|
||||||
switch name.toLowerCase()
|
switch name.toLowerCase()
|
||||||
when "pause"
|
when "pause"
|
||||||
vlc.status.pause()
|
# now we can toggle-pause playback this easily! yay!
|
||||||
|
vlc.togglePause()
|
||||||
return
|
return
|
||||||
when "play"
|
when "play"
|
||||||
inputBB = paramline.trim()
|
inputBB = paramline.trim()
|
||||||
|
@ -153,7 +167,7 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
|
|
||||||
# we gonna interpret play without a url as an attempt to unpause the current song
|
# we gonna interpret play without a url as an attempt to unpause the current song
|
||||||
if input.length <= 0
|
if input.length <= 0
|
||||||
vlc.status.resume()
|
vlc.play()
|
||||||
return
|
return
|
||||||
|
|
||||||
# only allow playback from file if it's a preconfigured alias
|
# only allow playback from file if it's a preconfigured alias
|
||||||
|
@ -168,11 +182,7 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
|
|
||||||
# TODO: permission system to check if uid is allowed to play this url or alias
|
# TODO: permission system to check if uid is allowed to play this url or alias
|
||||||
|
|
||||||
await vlc.status.empty defer(err)
|
vlc.playlist.clear()
|
||||||
if err
|
|
||||||
log.warn "Couldn't empty VLC playlist", err
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later."
|
|
||||||
return
|
|
||||||
|
|
||||||
# let's give youtube-dl a shot!
|
# let's give youtube-dl a shot!
|
||||||
await youtubedl.getInfo input, [
|
await youtubedl.getInfo input, [
|
||||||
|
@ -189,24 +199,51 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
if not info.url?
|
if not info.url?
|
||||||
info.url = input
|
info.url = input
|
||||||
info.title = input # URL as title
|
info.title = input # URL as title
|
||||||
|
info.originalUrl = input
|
||||||
|
vlcMediaInfo[info.url] = info
|
||||||
|
|
||||||
await vlc.status.play info.url, defer(err)
|
# play it in VLC
|
||||||
if err
|
vlc.play info.url
|
||||||
vlc.status.empty()
|
when "stop-after"
|
||||||
log.warn "VLC API returned an error when trying to play", err
|
vlc.playlist.mode = vlc.playlist.Single
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?"
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item."
|
||||||
return
|
when "loop"
|
||||||
|
inputBB = paramline
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Now playing [URL=#{input}]#{info.title}[/URL]."
|
input = null
|
||||||
|
switch (removeBB paramline).toLowerCase().trim()
|
||||||
|
when ""
|
||||||
|
# just show current mode
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is #{if vlc.playlist.mode == vlc.playlist.Loop then "on" else "off"}."
|
||||||
|
when "on"
|
||||||
|
# enable looping
|
||||||
|
vlc.playlist.mode = vlc.playlist.Loop
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is now on."
|
||||||
|
when "off"
|
||||||
|
# disable looping
|
||||||
|
vlc.playlist.mode = vlc.playlist.Normal
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Playlist looping is now off."
|
||||||
|
else
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]#{name} on|off[/B] - Turns playlist looping on or off"
|
||||||
|
return
|
||||||
when "next"
|
when "next"
|
||||||
await vlc.status.next defer(err)
|
if vlc.playlist.items.count == 0
|
||||||
if err
|
ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
|
||||||
vlc.status.empty()
|
|
||||||
log.warn "VLC API returned an error when trying to skip current song", err
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "This didn't work. Does the playlist have multiple songs?"
|
|
||||||
return
|
return
|
||||||
|
if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem == vlc.playlist.items.count - 1
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist."
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to next playlist item, this is the last one!"
|
||||||
|
return
|
||||||
|
vlc.playlist.next()
|
||||||
|
when "prev", "previous"
|
||||||
|
if vlc.playlist.items.count == 0
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty."
|
||||||
|
return
|
||||||
|
if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem <= 0
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Can't jump to previous playlist item, this is the first one!"
|
||||||
|
return
|
||||||
|
vlc.playlist.prev()
|
||||||
|
when "empty", "clear"
|
||||||
|
vlc.playlist.clear()
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Cleared the playlist."
|
||||||
when "enqueue", "add", "append"
|
when "enqueue", "add", "append"
|
||||||
inputBB = paramline.trim()
|
inputBB = paramline.trim()
|
||||||
input = (removeBB paramline).trim()
|
input = (removeBB paramline).trim()
|
||||||
|
@ -242,36 +279,25 @@ ts3clientService.on "started", (ts3proc) =>
|
||||||
if not info.url?
|
if not info.url?
|
||||||
info.url = input
|
info.url = input
|
||||||
info.title = input # URL as title
|
info.title = input # URL as title
|
||||||
|
info.originalUrl = input
|
||||||
|
vlcMediaInfo[info.url] = info
|
||||||
|
|
||||||
await vlc.status.enqueue info.url, defer(err)
|
# add it in VLC
|
||||||
if err
|
vlc.playlist.add info.url
|
||||||
vlc.status.empty()
|
|
||||||
log.warn "VLC API returned an error when trying to play", err
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Something seems to be wrong with the specified media. Try checking the URL/path you provided?"
|
|
||||||
return
|
|
||||||
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist."
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist."
|
||||||
|
|
||||||
|
# TODO: Do we need to make sure that vlc.playlist.mode is not set to "Single" here or is that handled automatically?
|
||||||
when "stop"
|
when "stop"
|
||||||
await vlc.status.stop defer(err)
|
vlc.stop()
|
||||||
|
|
||||||
vlc.status.empty()
|
|
||||||
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback."
|
|
||||||
when "vol"
|
when "vol"
|
||||||
vol = parseInt paramline
|
vol = parseInt paramline
|
||||||
|
|
||||||
if paramline.trim().length <= 0 or vol > 511 or vol < 0
|
if paramline.trim().length <= 0 or vol == NaN or vol > 200 or vol < 0
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol <number>[/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup."
|
ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol <number>[/B] - takes a number between 0 (0%) and 200 (200%) to set the volume. 100% is 100. Defaults to 50 (50%) on startup."
|
||||||
return
|
return
|
||||||
|
|
||||||
await vlc.status.volume paramline, defer(err)
|
vlc.audio.volume = vol
|
||||||
|
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%."
|
||||||
if err
|
|
||||||
log.warn "Failed to set volume", err
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out."
|
|
||||||
return
|
|
||||||
|
|
||||||
ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set."
|
|
||||||
when "changenick"
|
when "changenick"
|
||||||
nick = if paramline.length > params[0].length then paramline else params[0]
|
nick = if paramline.length > params[0].length then paramline else params[0]
|
||||||
if nick.length < 3 or nick.length > 30
|
if nick.length < 3 or nick.length > 30
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"string.prototype.startswith": "^0.2.0",
|
"string.prototype.startswith": "^0.2.0",
|
||||||
"sync": "^0.2.5",
|
"sync": "^0.2.5",
|
||||||
"valid-url": "^1.0.9",
|
"valid-url": "^1.0.9",
|
||||||
"vlc-api": "0.0.0",
|
"webchimera.js": "^0.1.38",
|
||||||
"which": "^1.1.2",
|
"which": "^1.1.2",
|
||||||
"winston": "^1.0.1",
|
"winston": "^1.0.1",
|
||||||
"xvfb": "git://github.com/icedream/node-xvfb.git",
|
"xvfb": "git://github.com/icedream/node-xvfb.git",
|
||||||
|
|
|
@ -1,88 +1,43 @@
|
||||||
spawn = require("child_process").spawn
|
spawn = require("child_process").spawn
|
||||||
services = require("../services")
|
services = require("../services")
|
||||||
config = require("../config")
|
config = require("../config")
|
||||||
VLCApi = require("vlc-api")
|
wc = require("webchimera.js")
|
||||||
StreamSplitter = require("stream-splitter")
|
StreamSplitter = require("stream-splitter")
|
||||||
require_bin = require("../require_bin")
|
|
||||||
|
|
||||||
vlcBinPath = require_bin "vlc"
|
|
||||||
|
|
||||||
module.exports = class VLCService extends services.Service
|
module.exports = class VLCService extends services.Service
|
||||||
dependencies: [
|
dependencies: [
|
||||||
"pulseaudio"
|
"pulseaudio"
|
||||||
]
|
]
|
||||||
constructor: -> super "VLC",
|
constructor: -> super "VLC",
|
||||||
|
###
|
||||||
|
# Starts an instance of VLC and keeps it ready for service.
|
||||||
|
###
|
||||||
start: (cb) ->
|
start: (cb) ->
|
||||||
if @_process
|
if @_instance
|
||||||
cb? null, @_process
|
cb? null, @_instance
|
||||||
return
|
return
|
||||||
|
|
||||||
calledCallback = false
|
calledCallback = false
|
||||||
|
|
||||||
proc = null
|
instance = wc.createPlayer [
|
||||||
doStart = null
|
"--aout", "pulse",
|
||||||
doStart = () =>
|
"--no-video"
|
||||||
await services.find("pulseaudio").start defer(err)
|
]
|
||||||
if err
|
instance.audio.volume = 50
|
||||||
throw new Error "Dependency pulseaudio failed."
|
|
||||||
|
|
||||||
proc = spawn vlcBinPath, [
|
@_instance = instance
|
||||||
"-I", "http",
|
cb? null, @_instance
|
||||||
"--http-host", config.get("vlc-host"),
|
|
||||||
"--http-port", config.get("vlc-port"),
|
|
||||||
"--http-password", config.get("vlc-password")
|
|
||||||
"--aout", "pulse",
|
|
||||||
"--volume", "128", # 50% volume
|
|
||||||
"--no-video"
|
|
||||||
],
|
|
||||||
stdio: ['ignore', 'pipe', 'pipe']
|
|
||||||
detached: true
|
|
||||||
|
|
||||||
# logging
|
|
||||||
stdoutTokenizer = proc.stdout.pipe StreamSplitter "\n"
|
|
||||||
stdoutTokenizer.encoding = "utf8";
|
|
||||||
stdoutTokenizer.on "token", (token) =>
|
|
||||||
token = token.trim() # get rid of \r
|
|
||||||
@log.debug token
|
|
||||||
|
|
||||||
stderrTokenizer = proc.stderr.pipe StreamSplitter "\n"
|
|
||||||
stderrTokenizer.encoding = "utf8";
|
|
||||||
stderrTokenizer.on "token", (token) =>
|
|
||||||
token = token.trim() # get rid of \r
|
|
||||||
@log.debug token
|
|
||||||
|
|
||||||
proc.on "exit", () =>
|
|
||||||
if @state == "stopping"
|
|
||||||
return
|
|
||||||
if not calledCallback
|
|
||||||
calledCallback = true
|
|
||||||
@log.warn "VLC terminated unexpectedly during startup."
|
|
||||||
cb? new Error "VLC terminated unexpectedly."
|
|
||||||
@log.warn "VLC terminated unexpectedly, restarting."
|
|
||||||
doStart()
|
|
||||||
|
|
||||||
@_process = proc
|
|
||||||
|
|
||||||
doStart()
|
|
||||||
|
|
||||||
setTimeout (() =>
|
|
||||||
if not calledCallback
|
|
||||||
calledCallback = true
|
|
||||||
|
|
||||||
@instance = new VLCApi
|
|
||||||
host: ":#{encodeURIComponent config.get("vlc-password")}@#{config.get("vlc-host")}",
|
|
||||||
port: config.get("vlc-port")
|
|
||||||
cb? null, @instance), 1500 # TODO: Use some more stable condition
|
|
||||||
|
|
||||||
|
###
|
||||||
|
# Shuts down the VLC instance.
|
||||||
|
###
|
||||||
stop: (cb) ->
|
stop: (cb) ->
|
||||||
if not @_process
|
if not @_instance
|
||||||
cb?()
|
cb?()
|
||||||
return
|
return
|
||||||
|
|
||||||
@instance = null
|
# TODO: Is there even a proper way to shut this down?
|
||||||
|
@_instance = null
|
||||||
@_process.kill()
|
|
||||||
await @_process.once "exit", defer()
|
|
||||||
|
|
||||||
cb?()
|
cb?()
|
||||||
|
|
||||||
|
|
|
@ -10,9 +10,23 @@ merge = require "merge"
|
||||||
|
|
||||||
parserLog = getLogger "parser"
|
parserLog = getLogger "parser"
|
||||||
|
|
||||||
escape = (value) => value.toString().replace(///\\///g, "\\\\").replace(/\//g, "\\/").replace(/\|/g, "\\|").replace(///\ ///g, "\\s")
|
escape = (value) => value.toString()\
|
||||||
|
.replace(/\\/g, "\\\\")\
|
||||||
|
.replace(/\//g, "\\/")\
|
||||||
|
.replace(/\|/g, "\\|")\
|
||||||
|
.replace(/\n/g, "\\n")\
|
||||||
|
.replace(/\r/g, "\\r")\
|
||||||
|
.replace(/\t/g, "\\t")\
|
||||||
|
.replace(/\ /g, "\\s")
|
||||||
|
|
||||||
unescape = (value) => value.toString().replace(/\\s/g, " ").replace(/\\\//g, "/").replace(/\\\|/g, "|").replace(/\\\\/g, "\\")
|
unescape = (value) => value.toString()\
|
||||||
|
.replace(/\\s/g, " ")\
|
||||||
|
.replace(/\\t/g, "\t")\
|
||||||
|
.replace(/\\r/g, "\r")\
|
||||||
|
.replace(/\\n/g, "\n")\
|
||||||
|
.replace(/\\\|/g, "|")\
|
||||||
|
.replace(/\\\//g, "/")\
|
||||||
|
.replace(/\\\\/g, "\\")
|
||||||
|
|
||||||
buildCmd = (name, namedArgs, posArgs) =>
|
buildCmd = (name, namedArgs, posArgs) =>
|
||||||
# TODO: Add support for collected arguments (aka lists)
|
# TODO: Add support for collected arguments (aka lists)
|
||||||
|
|
Loading…
Reference in New Issue