Skip to content

Instantly share code, notes, and snippets.

@rHermes
Created August 30, 2018 09:39
Show Gist options
  • Save rHermes/c2015483e8fad67748fd0d69de4a87a1 to your computer and use it in GitHub Desktop.
Save rHermes/c2015483e8fad67748fd0d69de4a87a1 to your computer and use it in GitHub Desktop.
package main
import (
"bufio"
"fmt"
"io"
"log"
"math/rand"
"net"
"net/http"
"sync"
"github.com/Microsoft/go-winio"
"github.com/buger/jsonparser"
"github.com/pressly/chi"
)
// TODO(rhermes): List the content of \\.\pipe\ and filter over some pattern,
// to get the current mpv instances, and present them as choices in the root
// form. This will allow me to have multiple mpv instance open and also allow me
// to set it up in my mpv conf, so that a named pipe is open automatically.
// These don't have the the last few bytes, as we append a request_id.
var (
JPC_PAUSE_ON = []byte(`{"command": ["set_property", "pause", true]`)
JPC_PAUSE_OFF = []byte(`{"command": ["set_property", "pause", false]`)
JPC_PAUSE_TOGGLE_ = []byte(`{"command": ["cycle", "pause"]`)
JPC_OSC_OFF = []byte(`{"command": ["script-message", "osc-visibility", "never"]`)
JPC_OSC_ON = []byte(`{"command": ["script-message", "osc-visibility", "always"]`)
JPC_PLAYLIST_PREV = []byte(`{"command": ["playlist-prev"]`)
JPC_PLAYLIST_NEXT = []byte(`{"command": ["playlist-next"]`)
JPC_CHAPTER_PREV = []byte(`{"command": ["add", "chapter", -1]`)
JPC_CHAPTER_NEXT = []byte(`{"command": ["add", "chapter", 1]`)
JPC_PRESS_LEFT = []byte(`{"command": ["keypress", "LEFT"]`)
JPC_PRESS_RIGHT = []byte(`{"command": ["keypress", "RIGHT"]`)
)
type MPVClient struct {
nc net.Conn
rw *bufio.ReadWriter
wg sync.WaitGroup
rd *rand.Rand
// This is used for routing
i2c map[uint32](chan []byte)
i2cMtx sync.Mutex
}
func (mc *MPVClient) Close() error {
// If we currently have outstanding return values for commands, we wait.
mc.wg.Wait()
return mc.nc.Close()
}
func (mc *MPVClient) inputMonitor() {
for {
dbt, err := mc.rw.ReadBytes('\n')
if err != nil {
if err != io.EOF {
log.Println(err.Error())
}
break
}
// We need to check if this is an event or not.
ename, err := jsonparser.GetString(dbt, "event")
if err != nil && err != jsonparser.KeyPathNotFoundError {
log.Fatal(err)
}
// Is this a command event?
if err == jsonparser.KeyPathNotFoundError {
mc.i2cMtx.Lock()
// Get msg id.
msgID, err := jsonparser.GetInt(dbt, "request_id")
if err != nil {
log.Fatal(err)
}
ch, ok := mc.i2c[uint32(msgID)]
if !ok {
log.Fatal("We haven't seen this ID before!")
}
go func(msg []byte) {
ch <- msg
close(ch)
}(dbt)
mc.i2cMtx.Unlock()
mc.wg.Done()
} else {
log.Printf("We got event ( %s ): %s", ename, string(dbt))
}
}
}
// Helper function to avoid code repetition.
func (mc *MPVClient) sendCommand(cmd []byte) (<-chan []byte, error) {
// BUG(rhermes): There could be a problem here if the error happens,
// but if I Put the wg.Add after any of these, there could be race conditions.
mc.i2cMtx.Lock()
defer mc.i2cMtx.Unlock()
msgID := mc.rd.Uint32()
ncmd := []byte(fmt.Sprintf("%s, \"request_id\": %d}\n", cmd, msgID))
if _, err := mc.rw.Write(ncmd); err != nil {
return nil, err
}
if err := mc.rw.Flush(); err != nil {
return nil, err
}
// Make the one off channel
mc.i2c[msgID] = make(chan []byte)
mc.wg.Add(1)
return mc.i2c[msgID], nil
}
// Export the commands we need.
func (mc *MPVClient) PauseToggle() (<-chan []byte, error) { return mc.sendCommand(JPC_PAUSE_TOGGLE_) }
func (mc *MPVClient) OSCOff() (<-chan []byte, error) { return mc.sendCommand(JPC_OSC_OFF) }
func (mc *MPVClient) OSCOn() (<-chan []byte, error) { return mc.sendCommand(JPC_OSC_ON) }
func (mc *MPVClient) PlaylistPrev() (<-chan []byte, error) { return mc.sendCommand(JPC_PLAYLIST_PREV) }
func (mc *MPVClient) PlaylistNext() (<-chan []byte, error) { return mc.sendCommand(JPC_PLAYLIST_NEXT) }
func (mc *MPVClient) ChapterPrev() (<-chan []byte, error) { return mc.sendCommand(JPC_CHAPTER_PREV) }
func (mc *MPVClient) ChapterNext() (<-chan []byte, error) { return mc.sendCommand(JPC_CHAPTER_NEXT) }
func (mc *MPVClient) PressLeft() (<-chan []byte, error) { return mc.sendCommand(JPC_PRESS_LEFT) }
func (mc *MPVClient) PressRight() (<-chan []byte, error) { return mc.sendCommand(JPC_PRESS_RIGHT) }
func NewMPVClient(pipeName string) (*MPVClient, error) {
var mc MPVClient
nc, err := winio.DialPipe(pipeName, nil)
if err != nil {
return nil, err
}
mc.nc = nc
mc.rw = bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
mc.rd = rand.New(rand.NewSource(0))
mc.i2c = make(map[uint32](chan []byte))
go mc.inputMonitor()
return &mc, nil
}
func basicHandler(f func() (<-chan []byte, error)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
res, err := f()
if err != nil {
log.Println(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
log.Println(string(<-res))
http.Redirect(w, r, "/", http.StatusFound)
}
}
func main() {
// Setup logger
log.SetFlags(log.Flags() | log.Llongfile)
mc, err := NewMPVClient(`\\.\pipe\mpv_socket`)
if err != nil {
log.Fatal(err)
}
defer mc.Close()
r := chi.NewRouter()
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, `
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>I <3 Senpai~</h1>
<ul>
<li><a href="/api/pauseToggle">pauseToggle</a></li>
<li></li>
<li><a href="/api/oscOff">oscOff</a></li>
<li><a href="/api/oscOn">oscOn</a></li>
<li></li>
<li><a href="/api/playlistPrev">playlistPrev</a></li>
<li><a href="/api/playlistNext">playlistNext</a></li>
<li></li>
<li><a href="/api/chapterPrev">chapterPrev</a></li>
<li><a href="/api/chapterNext">chapterNext</a></li>
<li></li>
<li><a href="/api/pressLeft">pressLeft</a></li>
<li><a href="/api/pressRight">pressRight</a></li>
</ul>
</body>
</html>
`)
})
// Pause
r.Get("/api/pauseToggle", basicHandler(mc.PauseToggle))
// OSC
r.Get("/api/oscOff", basicHandler(mc.OSCOff))
r.Get("/api/oscOn", basicHandler(mc.OSCOn))
// Playlist
r.Get("/api/playlistNext", basicHandler(mc.PlaylistNext))
r.Get("/api/playlistPrev", basicHandler(mc.PlaylistPrev))
// Playlist
r.Get("/api/chapterNext", basicHandler(mc.ChapterNext))
r.Get("/api/chapterPrev", basicHandler(mc.ChapterPrev))
// Keys
r.Get("/api/pressLeft", basicHandler(mc.PressLeft))
r.Get("/api/pressRight", basicHandler(mc.PressRight))
http.ListenAndServe("192.168.1.177:3333", r)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment