diff --git a/app.iced b/app.iced index 02782f8..a05428b 100644 --- a/app.iced +++ b/app.iced @@ -46,13 +46,16 @@ await services.find("pulseaudio").start defer err if err log.warn "PulseAudio could not start up, audio may not act as expected!" -# VLC HTTP API -await services.find("vlc").start defer err +# VLC via WebChimera.js +vlcService = services.find("vlc") +await vlcService.start defer err, vlc if err log.warn "VLC could not start up!" await module.exports.shutdown defer() process.exit 1 -vlc = services.find("vlc").instance + +# Cached information for tracks in playlist +vlcMediaInfo = {} # TeamSpeak3 ts3clientService = services.find("ts3client") @@ -60,6 +63,16 @@ ts3clientService = services.find("ts3client") ts3clientService.on "started", (ts3proc) => 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.mydata = {} @@ -145,7 +158,8 @@ ts3clientService.on "started", (ts3proc) => switch name.toLowerCase() when "pause" - vlc.status.pause() + # now we can toggle-pause playback this easily! yay! + vlc.togglePause() return when "play" 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 if input.length <= 0 - vlc.status.resume() + vlc.play() return # 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 - await vlc.status.empty defer(err) - if err - log.warn "Couldn't empty VLC playlist", err - ts3query.sendtextmessage args.targetmode, invoker.id, "Sorry, an error occurred. Try again later." - return + vlc.playlist.clear() # let's give youtube-dl a shot! await youtubedl.getInfo input, [ @@ -189,24 +199,51 @@ ts3clientService.on "started", (ts3proc) => if not info.url? info.url = input info.title = input # URL as title - - await vlc.status.play info.url, defer(err) - if err - 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, "Now playing [URL=#{input}]#{info.title}[/URL]." + info.originalUrl = input + vlcMediaInfo[info.url] = info + + # play it in VLC + vlc.play info.url + when "stop-after" + vlc.playlist.mode = vlc.playlist.Single + ts3query.sendtextmessage args.targetmode, invoker.id, "Playback will stop after the current playlist item." + when "loop" + inputBB = paramline + 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" - await vlc.status.next defer(err) - if err - 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?" + if vlc.playlist.items.count == 0 + ts3query.sendtextmessage args.targetmode, invoker.id, "The playlist is empty." return - - ts3query.sendtextmessage args.targetmode, invoker.id, "Playing next song in the playlist." + if vlc.playlist.mode != vlc.playlist.Loop and vlc.playlist.currentItem == vlc.playlist.items.count - 1 + 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" inputBB = paramline.trim() input = (removeBB paramline).trim() @@ -242,36 +279,25 @@ ts3clientService.on "started", (ts3proc) => if not info.url? info.url = input info.title = input # URL as title + info.originalUrl = input + vlcMediaInfo[info.url] = info - await vlc.status.enqueue info.url, defer(err) - if err - 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 - + # add it in VLC + vlc.playlist.add info.url ts3query.sendtextmessage args.targetmode, invoker.id, "Added [URL=#{input}]#{info.title}[/URL] to the playlist." - when "stop" - await vlc.status.stop defer(err) - vlc.status.empty() - - ts3query.sendtextmessage args.targetmode, invoker.id, "Stopped playback." + # TODO: Do we need to make sure that vlc.playlist.mode is not set to "Single" here or is that handled automatically? + when "stop" + vlc.stop() when "vol" vol = parseInt paramline - if paramline.trim().length <= 0 or vol > 511 or vol < 0 - ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol [/B] - takes a number between 0 (0%) and 1024 (400%) to set the volume. 100% is 256. Defaults to 128 (50%) on startup." - return - - await vlc.status.volume paramline, defer(err) - - if err - log.warn "Failed to set volume", err - ts3query.sendtextmessage args.targetmode, invoker.id, "That unfortunately didn't work out." + if paramline.trim().length <= 0 or vol == NaN or vol > 200 or vol < 0 + ts3query.sendtextmessage args.targetmode, invoker.id, "[B]vol [/B] - takes a number between 0 (0%) and 200 (200%) to set the volume. 100% is 100. Defaults to 50 (50%) on startup." return - ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set." + vlc.audio.volume = vol + ts3query.sendtextmessage args.targetmode, invoker.id, "Volume set to #{vol}%." when "changenick" nick = if paramline.length > params[0].length then paramline else params[0] if nick.length < 3 or nick.length > 30 diff --git a/package.json b/package.json index bc41172..78ac299 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "string.prototype.startswith": "^0.2.0", "sync": "^0.2.5", "valid-url": "^1.0.9", - "vlc-api": "0.0.0", + "webchimera.js": "^0.1.38", "which": "^1.1.2", "winston": "^1.0.1", "xvfb": "git://github.com/icedream/node-xvfb.git", diff --git a/services/vlc.iced b/services/vlc.iced index d0be6c4..b52ee83 100644 --- a/services/vlc.iced +++ b/services/vlc.iced @@ -1,88 +1,43 @@ spawn = require("child_process").spawn services = require("../services") config = require("../config") -VLCApi = require("vlc-api") +wc = require("webchimera.js") StreamSplitter = require("stream-splitter") -require_bin = require("../require_bin") - -vlcBinPath = require_bin "vlc" module.exports = class VLCService extends services.Service dependencies: [ "pulseaudio" ] constructor: -> super "VLC", + ### + # Starts an instance of VLC and keeps it ready for service. + ### start: (cb) -> - if @_process - cb? null, @_process + if @_instance + cb? null, @_instance return calledCallback = false - proc = null - doStart = null - doStart = () => - await services.find("pulseaudio").start defer(err) - if err - throw new Error "Dependency pulseaudio failed." - - proc = spawn vlcBinPath, [ - "-I", "http", - "--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() + instance = wc.createPlayer [ + "--aout", "pulse", + "--no-video" + ] + instance.audio.volume = 50 - @_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 + @_instance = instance + cb? null, @_instance + ### + # Shuts down the VLC instance. + ### stop: (cb) -> - if not @_process + if not @_instance cb?() return - @instance = null - - @_process.kill() - await @_process.once "exit", defer() + # TODO: Is there even a proper way to shut this down? + @_instance = null cb?() diff --git a/ts3query.iced b/ts3query.iced index a9505e6..3b1b076 100644 --- a/ts3query.iced +++ b/ts3query.iced @@ -10,9 +10,23 @@ merge = require "merge" parserLog = getLogger "parser" -escape = (value) => value.toString().replace(///\\///g, "\\\\").replace(/\//g, "\\/").replace(/\|/g, "\\|").replace(///\ ///g, "\\s") - -unescape = (value) => value.toString().replace(/\\s/g, " ").replace(/\\\//g, "/").replace(/\\\|/g, "|").replace(/\\\\/g, "\\") +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(/\\t/g, "\t")\ + .replace(/\\r/g, "\r")\ + .replace(/\\n/g, "\n")\ + .replace(/\\\|/g, "|")\ + .replace(/\\\//g, "/")\ + .replace(/\\\\/g, "\\") buildCmd = (name, namedArgs, posArgs) => # TODO: Add support for collected arguments (aka lists)