Update project structure
This commit is contained in:
2
Makefile
2
Makefile
@@ -1,7 +1,7 @@
|
|||||||
LDFLAGS="-s -w -X main.build=`date -u +%Y%m%d%H%M%S` -X main.version=`cat ./version.txt`"
|
LDFLAGS="-s -w -X main.build=`date -u +%Y%m%d%H%M%S` -X main.version=`cat ./version.txt`"
|
||||||
|
|
||||||
build: clean
|
build: clean
|
||||||
go build -ldflags $(LDFLAGS) -o build/go2tv go2tv.go flagfuncs.go
|
go build -ldflags $(LDFLAGS) -o build/go2tv cmd/go2tv/go2tv.go cmd/go2tv/flagfuncs.go
|
||||||
|
|
||||||
install:
|
install:
|
||||||
mkdir -p /usr/local/bin/
|
mkdir -p /usr/local/bin/
|
||||||
|
146
flagfuncs.go
146
flagfuncs.go
@@ -1,146 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
func listFlagFunction() error {
|
|
||||||
if len(devices) == 0 {
|
|
||||||
err := errors.New("-l and -t can't be used together")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
fmt.Println()
|
|
||||||
|
|
||||||
// We loop through this map twice as we need to maintain
|
|
||||||
// the correct order.
|
|
||||||
keys := make([]int, 0)
|
|
||||||
for k := range devices {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
|
|
||||||
sort.Ints(keys)
|
|
||||||
|
|
||||||
for _, k := range keys {
|
|
||||||
boldStart := ""
|
|
||||||
boldEnd := ""
|
|
||||||
|
|
||||||
if runtime.GOOS == "linux" {
|
|
||||||
boldStart = "\033[1m"
|
|
||||||
boldEnd = "\033[0m"
|
|
||||||
}
|
|
||||||
fmt.Printf("%sDevice %v%s\n", boldStart, k, boldEnd)
|
|
||||||
fmt.Printf("%s--------%s\n", boldStart, boldEnd)
|
|
||||||
fmt.Printf("%sModel:%s %s\n", boldStart, boldEnd, devices[k][0])
|
|
||||||
fmt.Printf("%sURL:%s %s\n", boldStart, boldEnd, devices[k][1])
|
|
||||||
fmt.Println()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkflags() (bool, error) {
|
|
||||||
checkVerflag()
|
|
||||||
|
|
||||||
if err := checkVflag(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkTflag(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
list, err := checkLflag()
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if list {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := checkSflag(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVflag() error {
|
|
||||||
if !*listPtr {
|
|
||||||
if *videoArg == "" {
|
|
||||||
err := errors.New("No video file defined")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(*videoArg); os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkSflag() error {
|
|
||||||
if *subsArg != "" {
|
|
||||||
if _, err := os.Stat(*subsArg); os.IsNotExist(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// The checkVflag should happen before
|
|
||||||
// checkSflag so we're safe to call *videoArg
|
|
||||||
// here. If *subsArg is empty, try to
|
|
||||||
// automatically find the srt from the
|
|
||||||
// video filename.
|
|
||||||
*subsArg = (*videoArg)[0:len(*videoArg)-
|
|
||||||
len(filepath.Ext(*videoArg))] + ".srt"
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkTflag() error {
|
|
||||||
if *targetPtr == "" {
|
|
||||||
err := loadSSDPservices()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dmrURL, err = devicePicker(1)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Validate URL before proceeding.
|
|
||||||
_, err := url.ParseRequestURI(*targetPtr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dmrURL = *targetPtr
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkLflag() (bool, error) {
|
|
||||||
if *listPtr {
|
|
||||||
if err := listFlagFunction(); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkVerflag() {
|
|
||||||
if *versionPtr {
|
|
||||||
fmt.Printf("Go2TV Version: %s, ", version)
|
|
||||||
fmt.Printf("Build: %s\n", build)
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
}
|
|
130
go2tv.go
130
go2tv.go
@@ -1,130 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/alexballas/go2tv/httphandlers"
|
|
||||||
"github.com/alexballas/go2tv/interactive"
|
|
||||||
"github.com/alexballas/go2tv/iptools"
|
|
||||||
"github.com/alexballas/go2tv/soapcalls"
|
|
||||||
"github.com/koron/go-ssdp"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
version string
|
|
||||||
build string
|
|
||||||
dmrURL string
|
|
||||||
serverStarted = make(chan struct{})
|
|
||||||
devices = make(map[int][]string)
|
|
||||||
videoArg = flag.String("v", "", "Path to the video file.")
|
|
||||||
subsArg = flag.String("s", "", "Path to the subtitles file.")
|
|
||||||
listPtr = flag.Bool("l", false, "List all available UPnP/DLNA MediaRenderer models and URLs.")
|
|
||||||
targetPtr = flag.String("t", "", "Cast to a specific UPnP/DLNA MediaRenderer URL.")
|
|
||||||
versionPtr = flag.Bool("version", false, "Print version.")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
exit, err := checkflags()
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
if exit {
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
absVideoFile, err := filepath.Abs(*videoArg)
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
absSubtitlesFile, err := filepath.Abs(*subsArg)
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
transportURL, controlURL, err := soapcalls.DMRextractor(dmrURL)
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
whereToListen, err := iptools.URLtoListenIPandPort(dmrURL)
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
newsc, err := interactive.InitNewScreen()
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
// The String() method of the net/url package will properly escape the URL
|
|
||||||
// compared to the url.QueryEscape() method.
|
|
||||||
videoFileURLencoded := &url.URL{Path: filepath.Base(absVideoFile)}
|
|
||||||
subsFileURLencoded := &url.URL{Path: filepath.Base(absSubtitlesFile)}
|
|
||||||
|
|
||||||
tvdata := &soapcalls.TVPayload{
|
|
||||||
TransportURL: transportURL,
|
|
||||||
ControlURL: controlURL,
|
|
||||||
CallbackURL: "http://" + whereToListen + "/callback",
|
|
||||||
VideoURL: "http://" + whereToListen + "/" + videoFileURLencoded.String(),
|
|
||||||
SubtitlesURL: "http://" + whereToListen + "/" + subsFileURLencoded.String(),
|
|
||||||
}
|
|
||||||
|
|
||||||
s := httphandlers.NewServer(whereToListen)
|
|
||||||
|
|
||||||
// We pass the tvdata here as we need the callback handlers to be able to react
|
|
||||||
// to the different media renderer states.
|
|
||||||
go func() {
|
|
||||||
s.ServeFiles(serverStarted, absVideoFile, absSubtitlesFile, &httphandlers.HTTPPayload{Soapcalls: tvdata, Screen: newsc})
|
|
||||||
}()
|
|
||||||
// Wait for HTTP server to properly initialize
|
|
||||||
<-serverStarted
|
|
||||||
|
|
||||||
err = tvdata.SendtoTV("Play1")
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
newsc.InterInit(*tvdata)
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadSSDPservices() error {
|
|
||||||
list, err := ssdp.Search(ssdp.All, 1, "")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
counter := 0
|
|
||||||
for _, srv := range list {
|
|
||||||
// We only care about the AVTransport services for basic actions
|
|
||||||
// (stop,play,pause). If we need to extend this to support other functionality
|
|
||||||
// like volume control we need to use the RenderingControl service.
|
|
||||||
if srv.Type == "urn:schemas-upnp-org:service:AVTransport:1" {
|
|
||||||
counter++
|
|
||||||
devices[counter] = []string{srv.Server, srv.Location}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if counter > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.New("loadSSDPservices: No available Media Renderers")
|
|
||||||
}
|
|
||||||
|
|
||||||
func devicePicker(i int) (string, error) {
|
|
||||||
if i > len(devices) || len(devices) == 0 || i <= 0 {
|
|
||||||
return "", errors.New("devicePicker: Requested device not available")
|
|
||||||
}
|
|
||||||
keys := make([]int, 0)
|
|
||||||
for k := range devices {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Ints(keys)
|
|
||||||
for _, k := range keys {
|
|
||||||
if i == k {
|
|
||||||
return devices[k][1], nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "", errors.New("devicePicker: Something went terribly wrong")
|
|
||||||
}
|
|
||||||
|
|
||||||
func check(err error) {
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,168 +0,0 @@
|
|||||||
package httphandlers
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"html"
|
|
||||||
"io"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alexballas/go2tv/interactive"
|
|
||||||
"github.com/alexballas/go2tv/soapcalls"
|
|
||||||
)
|
|
||||||
|
|
||||||
// filesToServe defines the files we need to serve.
|
|
||||||
type filesToServe struct {
|
|
||||||
Video string
|
|
||||||
Subtitles string
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPserver - new http.Server instance.
|
|
||||||
type HTTPserver struct {
|
|
||||||
http *http.Server
|
|
||||||
mux *http.ServeMux
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTTPPayload - We need some of the soapcalls magic in this package too. We need
|
|
||||||
// to expose the ControlURL to the callback handler.
|
|
||||||
type HTTPPayload struct {
|
|
||||||
Soapcalls *soapcalls.TVPayload
|
|
||||||
Screen *interactive.NewScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServeFiles - Start HTTP server and serve the files.
|
|
||||||
func (s *HTTPserver) ServeFiles(serverStarted chan<- struct{}, videoPath, subtitlesPath string, tvpayload *HTTPPayload) {
|
|
||||||
files := &filesToServe{
|
|
||||||
Video: videoPath,
|
|
||||||
Subtitles: subtitlesPath,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mux.HandleFunc("/"+filepath.Base(files.Video), files.serveVideoHandler)
|
|
||||||
s.mux.HandleFunc("/"+filepath.Base(files.Subtitles), files.serveSubtitlesHandler)
|
|
||||||
s.mux.HandleFunc("/callback", tvpayload.callbackHandler)
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", s.http.Addr)
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
serverStarted <- struct{}{}
|
|
||||||
s.http.Serve(ln)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *filesToServe) serveVideoHandler(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Header().Set("transferMode.dlna.org", "Streaming")
|
|
||||||
w.Header().Set("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=017000 00000000000000000000000000")
|
|
||||||
|
|
||||||
filePath, err := os.Open(f.Video)
|
|
||||||
check(err)
|
|
||||||
defer filePath.Close()
|
|
||||||
|
|
||||||
fileStat, err := filePath.Stat()
|
|
||||||
check(err)
|
|
||||||
|
|
||||||
http.ServeContent(w, req, filepath.Base(f.Video), fileStat.ModTime(), filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *filesToServe) serveSubtitlesHandler(w http.ResponseWriter, req *http.Request) {
|
|
||||||
w.Header().Set("transferMode.dlna.org", "Streaming")
|
|
||||||
w.Header().Set("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=017000 00000000000000000000000000")
|
|
||||||
|
|
||||||
filePath, err := os.Open(f.Subtitles)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer filePath.Close()
|
|
||||||
|
|
||||||
fileStat, err := filePath.Stat()
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.ServeContent(w, req, filepath.Base(f.Subtitles), fileStat.ModTime(), filePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *HTTPPayload) callbackHandler(w http.ResponseWriter, req *http.Request) {
|
|
||||||
reqParsed, _ := io.ReadAll(req.Body)
|
|
||||||
sidVal, sidExists := req.Header["Sid"]
|
|
||||||
|
|
||||||
if !sidExists {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if sidVal[0] == "" {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
uuid := sidVal[0]
|
|
||||||
uuid = strings.TrimLeft(uuid, "[")
|
|
||||||
uuid = strings.TrimLeft(uuid, "]")
|
|
||||||
uuid = strings.TrimLeft(uuid, "uuid:")
|
|
||||||
|
|
||||||
// Apparently we should ignore the first message
|
|
||||||
// On some media renderers we receive a STOPPED message
|
|
||||||
// even before we start streaming.
|
|
||||||
seq, err := soapcalls.GetSequence(uuid)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if seq == 0 {
|
|
||||||
soapcalls.IncreaseSequence(uuid)
|
|
||||||
_, _ = fmt.Fprintf(w, "OK\n")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
reqParsedUnescape := html.UnescapeString(string(reqParsed))
|
|
||||||
previousstate, newstate, err := soapcalls.EventNotifyParser(reqParsedUnescape)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !soapcalls.UpdateMRstate(previousstate, newstate, uuid) {
|
|
||||||
http.Error(w, "", 404)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if newstate == "PLAYING" {
|
|
||||||
p.Screen.EmmitMsg("Playing")
|
|
||||||
}
|
|
||||||
if newstate == "PAUSED_PLAYBACK" {
|
|
||||||
p.Screen.EmmitMsg("Paused")
|
|
||||||
}
|
|
||||||
if newstate == "STOPPED" {
|
|
||||||
p.Screen.EmmitMsg("Stopped")
|
|
||||||
p.Soapcalls.UnsubscribeSoapCall(uuid)
|
|
||||||
p.Screen.Current.Fini()
|
|
||||||
os.Exit(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We could just not send anything here
|
|
||||||
// as the core server package would still
|
|
||||||
// default to a 200 OK empty response.
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewServer - create a new HTTP server.
|
|
||||||
func NewServer(a string) HTTPserver {
|
|
||||||
srv := HTTPserver{
|
|
||||||
http: &http.Server{Addr: a},
|
|
||||||
mux: http.NewServeMux(),
|
|
||||||
}
|
|
||||||
srv.http.Handler = srv.mux
|
|
||||||
|
|
||||||
return srv
|
|
||||||
}
|
|
||||||
|
|
||||||
func check(err error) {
|
|
||||||
if err != nil {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,128 +0,0 @@
|
|||||||
package interactive
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/alexballas/go2tv/soapcalls"
|
|
||||||
"github.com/gdamore/tcell/v2"
|
|
||||||
"github.com/gdamore/tcell/v2/encoding"
|
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
)
|
|
||||||
|
|
||||||
// NewScreen .
|
|
||||||
type NewScreen struct {
|
|
||||||
Current tcell.Screen
|
|
||||||
videoTitle string
|
|
||||||
lastAction string
|
|
||||||
}
|
|
||||||
|
|
||||||
var flipflop bool = true
|
|
||||||
|
|
||||||
func (p *NewScreen) emitStr(x, y int, style tcell.Style, str string) {
|
|
||||||
s := p.Current
|
|
||||||
for _, c := range str {
|
|
||||||
var comb []rune
|
|
||||||
w := runewidth.RuneWidth(c)
|
|
||||||
if w == 0 {
|
|
||||||
comb = []rune{c}
|
|
||||||
c = ' '
|
|
||||||
w = 1
|
|
||||||
}
|
|
||||||
s.SetContent(x, y, c, comb, style)
|
|
||||||
x += w
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EmmitMsg - Display the actions to the interactive terminal
|
|
||||||
func (p *NewScreen) EmmitMsg(inputtext string) {
|
|
||||||
p.lastAction = inputtext
|
|
||||||
s := p.Current
|
|
||||||
titleLen := len("Title: " + p.videoTitle)
|
|
||||||
w, h := s.Size()
|
|
||||||
boldStyle := tcell.StyleDefault.
|
|
||||||
Background(tcell.ColorBlack).
|
|
||||||
Foreground(tcell.ColorWhite).Bold(true)
|
|
||||||
blinkStyle := tcell.StyleDefault.
|
|
||||||
Background(tcell.ColorBlack).
|
|
||||||
Foreground(tcell.ColorWhite).Blink(true)
|
|
||||||
|
|
||||||
s.Clear()
|
|
||||||
|
|
||||||
p.emitStr(w/2-titleLen/2, h/2-2, tcell.StyleDefault, "Title: "+p.videoTitle)
|
|
||||||
if inputtext == "Waiting for status..." {
|
|
||||||
p.emitStr(w/2-len(inputtext)/2, h/2, blinkStyle, inputtext)
|
|
||||||
} else {
|
|
||||||
p.emitStr(w/2-len(inputtext)/2, h/2, boldStyle, inputtext)
|
|
||||||
}
|
|
||||||
p.emitStr(1, 1, tcell.StyleDefault, "Press ESC to stop and exit.")
|
|
||||||
p.emitStr(w/2-len("Press p to Pause/Play.")/2, h/2+2, tcell.StyleDefault, "Press p to Pause/Play.")
|
|
||||||
|
|
||||||
s.Show()
|
|
||||||
}
|
|
||||||
|
|
||||||
// InterInit - Start the interactive terminal
|
|
||||||
func (p *NewScreen) InterInit(tv soapcalls.TVPayload) {
|
|
||||||
var videoTitle string
|
|
||||||
|
|
||||||
videoTitlefromURL, err := url.Parse(tv.VideoURL)
|
|
||||||
if err != nil {
|
|
||||||
videoTitle = tv.VideoURL
|
|
||||||
} else {
|
|
||||||
videoTitle = strings.TrimLeft(videoTitlefromURL.Path, "/")
|
|
||||||
}
|
|
||||||
p.videoTitle = videoTitle
|
|
||||||
|
|
||||||
encoding.Register()
|
|
||||||
s := p.Current
|
|
||||||
if e := s.Init(); e != nil {
|
|
||||||
_, _ = fmt.Fprintf(os.Stderr, "%v\n", e)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
defStyle := tcell.StyleDefault.
|
|
||||||
Background(tcell.ColorBlack).
|
|
||||||
Foreground(tcell.ColorWhite)
|
|
||||||
s.SetStyle(defStyle)
|
|
||||||
|
|
||||||
p.lastAction = "Waiting for status..."
|
|
||||||
p.EmmitMsg(p.lastAction)
|
|
||||||
|
|
||||||
for {
|
|
||||||
switch ev := s.PollEvent().(type) {
|
|
||||||
case *tcell.EventResize:
|
|
||||||
s.Sync()
|
|
||||||
p.EmmitMsg(p.lastAction)
|
|
||||||
case *tcell.EventKey:
|
|
||||||
if ev.Key() == tcell.KeyEscape {
|
|
||||||
tv.SendtoTV("Stop")
|
|
||||||
s.Fini()
|
|
||||||
os.Exit(0)
|
|
||||||
} else if ev.Rune() == 'p' {
|
|
||||||
if flipflop {
|
|
||||||
flipflop = false
|
|
||||||
tv.SendtoTV("Pause")
|
|
||||||
} else {
|
|
||||||
flipflop = true
|
|
||||||
tv.SendtoTV("Play")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// InitNewScreen - Just initializing our new tcell screen
|
|
||||||
func InitNewScreen() (*NewScreen, error) {
|
|
||||||
s, e := tcell.NewScreen()
|
|
||||||
if e != nil {
|
|
||||||
return nil, errors.New("can't start new interactive screen")
|
|
||||||
}
|
|
||||||
q := NewScreen{
|
|
||||||
Current: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
return &q, nil
|
|
||||||
}
|
|
@@ -1,50 +0,0 @@
|
|||||||
package iptools
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// URLtoListenIPandPort for a given internal URL, find the correct IP/Interface to listen to.
|
|
||||||
func URLtoListenIPandPort(u string) (string, error) {
|
|
||||||
parsedURL, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
conn, err := net.Dial("udp", parsedURL.Host)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
ipToListen := strings.Split(conn.LocalAddr().String(), ":")[0]
|
|
||||||
portToListen, err := checkAndPickPort(ipToListen, 3500)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
res := ipToListen + ":" + portToListen
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAndPickPort(ip string, port int) (string, error) {
|
|
||||||
var numberOfchecks int
|
|
||||||
CHECK:
|
|
||||||
numberOfchecks++
|
|
||||||
conn, err := net.Listen("tcp", ip+":"+strconv.Itoa(port))
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "address already in use") {
|
|
||||||
if numberOfchecks == 1000 {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
port++
|
|
||||||
goto CHECK
|
|
||||||
} else {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
conn.Close()
|
|
||||||
return strconv.Itoa(port), nil
|
|
||||||
}
|
|
@@ -1,304 +0,0 @@
|
|||||||
package soapcalls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// PlayEnvelope - As in Play Pause Stop.
|
|
||||||
type PlayEnvelope struct {
|
|
||||||
XMLName xml.Name `xml:"s:Envelope"`
|
|
||||||
Schema string `xml:"xmlns:s,attr"`
|
|
||||||
Encoding string `xml:"s:encodingStyle,attr"`
|
|
||||||
PlayBody PlayBody `xml:"s:Body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayBody .
|
|
||||||
type PlayBody struct {
|
|
||||||
XMLName xml.Name `xml:"s:Body"`
|
|
||||||
PlayAction PlayAction `xml:"u:Play"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayAction .
|
|
||||||
type PlayAction struct {
|
|
||||||
XMLName xml.Name `xml:"u:Play"`
|
|
||||||
AVTransport string `xml:"xmlns:u,attr"`
|
|
||||||
InstanceID string
|
|
||||||
Speed string
|
|
||||||
}
|
|
||||||
|
|
||||||
// PauseEnvelope - As in Play Pause Stop.
|
|
||||||
type PauseEnvelope struct {
|
|
||||||
XMLName xml.Name `xml:"s:Envelope"`
|
|
||||||
Schema string `xml:"xmlns:s,attr"`
|
|
||||||
Encoding string `xml:"s:encodingStyle,attr"`
|
|
||||||
PauseBody PauseBody `xml:"s:Body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PauseBody .
|
|
||||||
type PauseBody struct {
|
|
||||||
XMLName xml.Name `xml:"s:Body"`
|
|
||||||
PauseAction PauseAction `xml:"u:Pause"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PauseAction .
|
|
||||||
type PauseAction struct {
|
|
||||||
XMLName xml.Name `xml:"u:Pause"`
|
|
||||||
AVTransport string `xml:"xmlns:u,attr"`
|
|
||||||
InstanceID string
|
|
||||||
Speed string
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopEnvelope - As in Play Pause Stop.
|
|
||||||
type StopEnvelope struct {
|
|
||||||
XMLName xml.Name `xml:"s:Envelope"`
|
|
||||||
Schema string `xml:"xmlns:s,attr"`
|
|
||||||
Encoding string `xml:"s:encodingStyle,attr"`
|
|
||||||
StopBody StopBody `xml:"s:Body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopBody .
|
|
||||||
type StopBody struct {
|
|
||||||
XMLName xml.Name `xml:"s:Body"`
|
|
||||||
StopAction StopAction `xml:"u:Stop"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// StopAction .
|
|
||||||
type StopAction struct {
|
|
||||||
XMLName xml.Name `xml:"u:Stop"`
|
|
||||||
AVTransport string `xml:"xmlns:u,attr"`
|
|
||||||
InstanceID string
|
|
||||||
Speed string
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAVTransportEnvelope .
|
|
||||||
type SetAVTransportEnvelope struct {
|
|
||||||
XMLName xml.Name `xml:"s:Envelope"`
|
|
||||||
Schema string `xml:"xmlns:s,attr"`
|
|
||||||
Encoding string `xml:"s:encodingStyle,attr"`
|
|
||||||
Body SetAVTransportBody `xml:"s:Body"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAVTransportBody .
|
|
||||||
type SetAVTransportBody struct {
|
|
||||||
XMLName xml.Name `xml:"s:Body"`
|
|
||||||
SetAVTransportURI SetAVTransportURI `xml:"u:SetAVTransportURI"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAVTransportURI .
|
|
||||||
type SetAVTransportURI struct {
|
|
||||||
XMLName xml.Name `xml:"u:SetAVTransportURI"`
|
|
||||||
AVTransport string `xml:"xmlns:u,attr"`
|
|
||||||
InstanceID string
|
|
||||||
CurrentURI string
|
|
||||||
CurrentURIMetaData CurrentURIMetaData `xml:"CurrentURIMetaData"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// CurrentURIMetaData .
|
|
||||||
type CurrentURIMetaData struct {
|
|
||||||
XMLName xml.Name `xml:"CurrentURIMetaData"`
|
|
||||||
Value []byte `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DIDLLite .
|
|
||||||
type DIDLLite struct {
|
|
||||||
XMLName xml.Name `xml:"DIDL-Lite"`
|
|
||||||
SchemaDIDL string `xml:"xmlns,attr"`
|
|
||||||
DC string `xml:"xmlns:dc,attr"`
|
|
||||||
Sec string `xml:"xmlns:sec,attr"`
|
|
||||||
SchemaUPNP string `xml:"xmlns:upnp,attr"`
|
|
||||||
DIDLLiteItem DIDLLiteItem `xml:"item"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DIDLLiteItem .
|
|
||||||
type DIDLLiteItem struct {
|
|
||||||
XMLName xml.Name `xml:"item"`
|
|
||||||
ID string `xml:"id,attr"`
|
|
||||||
ParentID string `xml:"parentID,attr"`
|
|
||||||
Restricted string `xml:"restricted,attr"`
|
|
||||||
UPNPClass string `xml:"upnp:class"`
|
|
||||||
DCtitle string `xml:"dc:title"`
|
|
||||||
ResNode []ResNode `xml:"res"`
|
|
||||||
SecCaptionInfo SecCaptionInfo `xml:"sec:CaptionInfo"`
|
|
||||||
SecCaptionInfoEx SecCaptionInfoEx `xml:"sec:CaptionInfoEx"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResNode .
|
|
||||||
type ResNode struct {
|
|
||||||
XMLName xml.Name `xml:"res"`
|
|
||||||
ProtocolInfo string `xml:"protocolInfo,attr"`
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SecCaptionInfo .
|
|
||||||
type SecCaptionInfo struct {
|
|
||||||
XMLName xml.Name `xml:"sec:CaptionInfo"`
|
|
||||||
Type string `xml:"sec:type,attr"`
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SecCaptionInfoEx .
|
|
||||||
type SecCaptionInfoEx struct {
|
|
||||||
XMLName xml.Name `xml:"sec:CaptionInfoEx"`
|
|
||||||
Type string `xml:"sec:type,attr"`
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func setAVTransportSoapBuild(videoURL, subtitleURL string) ([]byte, error) {
|
|
||||||
var videoTitle string
|
|
||||||
|
|
||||||
videoTitlefromURL, err := url.Parse(videoURL)
|
|
||||||
if err != nil {
|
|
||||||
videoTitle = videoURL
|
|
||||||
} else {
|
|
||||||
videoTitle = strings.TrimLeft(videoTitlefromURL.Path, "/")
|
|
||||||
}
|
|
||||||
l := DIDLLite{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
SchemaDIDL: "urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/",
|
|
||||||
DC: "http://purl.org/dc/elements/1.1/",
|
|
||||||
Sec: "http://www.sec.co.kr/",
|
|
||||||
SchemaUPNP: "urn:schemas-upnp-org:metadata-1-0/upnp/",
|
|
||||||
DIDLLiteItem: DIDLLiteItem{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
ID: "0",
|
|
||||||
ParentID: "-1",
|
|
||||||
Restricted: "false",
|
|
||||||
UPNPClass: "object.item.videoItem.movie",
|
|
||||||
DCtitle: videoTitle,
|
|
||||||
ResNode: []ResNode{{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
ProtocolInfo: "http-get:*:video/mp4:*",
|
|
||||||
Value: videoURL,
|
|
||||||
}, {
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
ProtocolInfo: "http-get:*:text/srt:*",
|
|
||||||
Value: subtitleURL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
SecCaptionInfo: SecCaptionInfo{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Type: "srt",
|
|
||||||
Value: subtitleURL,
|
|
||||||
},
|
|
||||||
SecCaptionInfoEx: SecCaptionInfoEx{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Type: "srt",
|
|
||||||
Value: subtitleURL,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
a, err := xml.Marshal(l)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
|
|
||||||
d := SetAVTransportEnvelope{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
|
||||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
|
||||||
Body: SetAVTransportBody{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
SetAVTransportURI: SetAVTransportURI{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
|
||||||
InstanceID: "0",
|
|
||||||
CurrentURI: videoURL,
|
|
||||||
CurrentURIMetaData: CurrentURIMetaData{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Value: a,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
|
||||||
b, err := xml.Marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
// That looks like an issue just with my Samsung TV.
|
|
||||||
b = bytes.ReplaceAll(b, []byte("""), []byte(`"`))
|
|
||||||
b = bytes.ReplaceAll(b, []byte("&"), []byte("&"))
|
|
||||||
|
|
||||||
return append(xmlStart, b...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func playSoapBuild() ([]byte, error) {
|
|
||||||
d := PlayEnvelope{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
|
||||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
|
||||||
PlayBody: PlayBody{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
PlayAction: PlayAction{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
|
||||||
InstanceID: "0",
|
|
||||||
Speed: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
|
||||||
b, err := xml.Marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(xmlStart, b...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func stopSoapBuild() ([]byte, error) {
|
|
||||||
d := StopEnvelope{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
|
||||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
|
||||||
StopBody: StopBody{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
StopAction: StopAction{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
|
||||||
InstanceID: "0",
|
|
||||||
Speed: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
|
||||||
b, err := xml.Marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(xmlStart, b...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pauseSoapBuild() ([]byte, error) {
|
|
||||||
d := PauseEnvelope{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
Schema: "http://schemas.xmlsoap.org/soap/envelope/",
|
|
||||||
Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
|
|
||||||
PauseBody: PauseBody{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
PauseAction: PauseAction{
|
|
||||||
XMLName: xml.Name{},
|
|
||||||
AVTransport: "urn:schemas-upnp-org:service:AVTransport:1",
|
|
||||||
InstanceID: "0",
|
|
||||||
Speed: "1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
xmlStart := []byte("<?xml version='1.0' encoding='utf-8'?>")
|
|
||||||
b, err := xml.Marshal(d)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
return make([]byte, 0), err
|
|
||||||
}
|
|
||||||
|
|
||||||
return append(xmlStart, b...), nil
|
|
||||||
}
|
|
@@ -1,318 +0,0 @@
|
|||||||
package soapcalls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type states struct {
|
|
||||||
previousState string
|
|
||||||
newState string
|
|
||||||
sequence int
|
|
||||||
}
|
|
||||||
|
|
||||||
var mediaRenderersStates = make(map[string]*states)
|
|
||||||
var initialMediaRenderersStates = make(map[string]interface{})
|
|
||||||
|
|
||||||
var mu sync.Mutex
|
|
||||||
|
|
||||||
// TVPayload - this is the heard of Go2TV.
|
|
||||||
type TVPayload struct {
|
|
||||||
TransportURL string
|
|
||||||
VideoURL string
|
|
||||||
SubtitlesURL string
|
|
||||||
ControlURL string
|
|
||||||
CallbackURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TVPayload) setAVTransportSoapCall() error {
|
|
||||||
parsedURLtransport, err := url.Parse(p.TransportURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
xml, err := setAVTransportSoapBuild(p.VideoURL, p.SubtitlesURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := http.Header{
|
|
||||||
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"`},
|
|
||||||
"content-type": []string{"text/xml"},
|
|
||||||
"charset": []string{"utf-8"},
|
|
||||||
"Connection": []string{"close"},
|
|
||||||
}
|
|
||||||
req.Header = headers
|
|
||||||
|
|
||||||
_, err = client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PlayStopSoapCall - Build and call the play soap call.
|
|
||||||
func (p *TVPayload) playStopPauseSoapCall(action string) error {
|
|
||||||
parsedURLtransport, err := url.Parse(p.TransportURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var xml []byte
|
|
||||||
if action == "Play" {
|
|
||||||
xml, err = playSoapBuild()
|
|
||||||
}
|
|
||||||
if action == "Stop" {
|
|
||||||
xml, err = stopSoapBuild()
|
|
||||||
}
|
|
||||||
if action == "Pause" {
|
|
||||||
xml, err = pauseSoapBuild()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := http.Header{
|
|
||||||
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#` + action + `"`},
|
|
||||||
"content-type": []string{"text/xml"},
|
|
||||||
"charset": []string{"utf-8"},
|
|
||||||
"Connection": []string{"close"},
|
|
||||||
}
|
|
||||||
req.Header = headers
|
|
||||||
|
|
||||||
_, err = client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SubscribeSoapCall - Subscribe to a media renderer
|
|
||||||
// If we explicitly pass the uuid, then we refresh it instead.
|
|
||||||
func (p *TVPayload) SubscribeSoapCall(uuidInput string) error {
|
|
||||||
parsedURLcontrol, err := url.Parse(p.ControlURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedURLcallback, err := url.Parse(p.CallbackURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("SUBSCRIBE", parsedURLcontrol.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var headers http.Header
|
|
||||||
if uuidInput == "" {
|
|
||||||
headers = http.Header{
|
|
||||||
"USER-AGENT": []string{runtime.GOOS + " UPnP/1.1 " + "Go2TV"},
|
|
||||||
"CALLBACK": []string{"<" + parsedURLcallback.String() + ">"},
|
|
||||||
"NT": []string{"upnp:event"},
|
|
||||||
"TIMEOUT": []string{"Second-300"},
|
|
||||||
"Connection": []string{"close"},
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
headers = http.Header{
|
|
||||||
"SID": []string{"uuid:" + uuidInput},
|
|
||||||
"TIMEOUT": []string{"Second-300"},
|
|
||||||
"Connection": []string{"close"},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.Header = headers
|
|
||||||
req.Header.Del("User-Agent")
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
uuid := resp.Header["Sid"][0]
|
|
||||||
uuid = strings.TrimLeft(uuid, "[")
|
|
||||||
uuid = strings.TrimLeft(uuid, "]")
|
|
||||||
uuid = strings.TrimLeft(uuid, "uuid:")
|
|
||||||
|
|
||||||
// We don't really need to initialize or set
|
|
||||||
// the State if we're just refreshing the uuid.
|
|
||||||
if uuidInput == "" {
|
|
||||||
CreateMRstate(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
timeoutReply := strings.TrimLeft(resp.Header["Timeout"][0], "Second-")
|
|
||||||
p.RefreshLoopUUIDSoapCall(uuid, timeoutReply)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UnsubscribeSoapCall - exported that as we use
|
|
||||||
// it for the callback stuff in the httphandlers package.
|
|
||||||
func (p *TVPayload) UnsubscribeSoapCall(uuid string) error {
|
|
||||||
parsedURLcontrol, err := url.Parse(p.ControlURL)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
|
|
||||||
req, err := http.NewRequest("UNSUBSCRIBE", parsedURLcontrol.String(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
headers := http.Header{
|
|
||||||
"SID": []string{"uuid:" + uuid},
|
|
||||||
"Connection": []string{"close"},
|
|
||||||
}
|
|
||||||
|
|
||||||
req.Header = headers
|
|
||||||
req.Header.Del("User-Agent")
|
|
||||||
|
|
||||||
_, err = client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteMRstate(uuid)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RefreshLoopUUIDSoapCall - Refresh the UUID.
|
|
||||||
func (p *TVPayload) RefreshLoopUUIDSoapCall(uuid, timeout string) error {
|
|
||||||
var triggerTime int
|
|
||||||
timeoutInt, err := strconv.Atoi(timeout)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh token after after Timeout / 2 seconds.
|
|
||||||
if timeoutInt > 1 {
|
|
||||||
triggerTime = timeoutInt / 2
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerTimefunc := time.Duration(triggerTime) * time.Second
|
|
||||||
|
|
||||||
// We're doing this as time.AfterFunc can't handle
|
|
||||||
// function arguments.
|
|
||||||
f := p.refreshLoopUUIDAsyncSoapCall(uuid)
|
|
||||||
time.AfterFunc(triggerTimefunc, f)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *TVPayload) refreshLoopUUIDAsyncSoapCall(uuid string) func() {
|
|
||||||
return func() {
|
|
||||||
p.SubscribeSoapCall(uuid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendtoTV - Send to TV.
|
|
||||||
func (p *TVPayload) SendtoTV(action string) error {
|
|
||||||
if action == "Play1" {
|
|
||||||
if err := p.SubscribeSoapCall(""); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := p.setAVTransportSoapCall(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
action = "Play"
|
|
||||||
}
|
|
||||||
|
|
||||||
if action == "Stop" {
|
|
||||||
// Cleaning up all uuids on force stop.
|
|
||||||
for uuids := range mediaRenderersStates {
|
|
||||||
p.UnsubscribeSoapCall(uuids)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := p.playStopPauseSoapCall(action); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMRstate - Update the mediaRenderersStates map
|
|
||||||
// with the state. Return true or false to verify that
|
|
||||||
// the actual update took place.
|
|
||||||
func UpdateMRstate(previous, new, uuid string) bool {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
// If the uuid is not one of the UUIDs we stored in
|
|
||||||
// soapcalls.InitialMediaRenderersStates it means that
|
|
||||||
// probably it expired and there is not much we can do
|
|
||||||
// with it. Trying to send an unsubscribe for those will
|
|
||||||
// probably result in a 412 error as per the upnpn documentation
|
|
||||||
// http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
|
||||||
// (page 94).
|
|
||||||
if initialMediaRenderersStates[uuid] == true {
|
|
||||||
mediaRenderersStates[uuid].previousState = previous
|
|
||||||
mediaRenderersStates[uuid].newState = new
|
|
||||||
mediaRenderersStates[uuid].sequence++
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMRstate .
|
|
||||||
func CreateMRstate(uuid string) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
initialMediaRenderersStates[uuid] = true
|
|
||||||
mediaRenderersStates[uuid] = &states{
|
|
||||||
previousState: "",
|
|
||||||
newState: "",
|
|
||||||
sequence: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteMRstate .
|
|
||||||
func DeleteMRstate(uuid string) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
delete(initialMediaRenderersStates, uuid)
|
|
||||||
delete(mediaRenderersStates, uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncreaseSequence .
|
|
||||||
func IncreaseSequence(uuid string) {
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
mediaRenderersStates[uuid].sequence++
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSequence .
|
|
||||||
func GetSequence(uuid string) (int, error) {
|
|
||||||
if initialMediaRenderersStates[uuid] == true {
|
|
||||||
return mediaRenderersStates[uuid].sequence, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1, errors.New("zombie callbacks, we should ignore those")
|
|
||||||
}
|
|
@@ -1,110 +0,0 @@
|
|||||||
package soapcalls
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"errors"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Root - root node.
|
|
||||||
type Root struct {
|
|
||||||
XMLName xml.Name `xml:"root"`
|
|
||||||
Device Device `xml:"device"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Device - device node (we should only expect one?).
|
|
||||||
type Device struct {
|
|
||||||
XMLName xml.Name `xml:"device"`
|
|
||||||
ServiceList ServiceList `xml:"serviceList"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ServiceList - serviceList node
|
|
||||||
type ServiceList struct {
|
|
||||||
XMLName xml.Name `xml:"serviceList"`
|
|
||||||
Services []Service `xml:"service"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service - service node.
|
|
||||||
type Service struct {
|
|
||||||
XMLName xml.Name `xml:"service"`
|
|
||||||
Type string `xml:"serviceType"`
|
|
||||||
ID string `xml:"serviceId"`
|
|
||||||
ControlURL string `xml:"controlURL"`
|
|
||||||
EventSubURL string `xml:"eventSubURL"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventPropertySet .
|
|
||||||
type EventPropertySet struct {
|
|
||||||
XMLName xml.Name `xml:"propertyset"`
|
|
||||||
EventInstance EventInstance `xml:"property>LastChange>Event>InstanceID"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventInstance .
|
|
||||||
type EventInstance struct {
|
|
||||||
XMLName xml.Name `xml:"InstanceID"`
|
|
||||||
Value string `xml:"val,attr"`
|
|
||||||
EventCurrentTransportActions EventCurrentTransportActions `xml:"CurrentTransportActions"`
|
|
||||||
EventTransportState EventTransportState `xml:"TransportState"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventCurrentTransportActions .
|
|
||||||
type EventCurrentTransportActions struct {
|
|
||||||
Value string `xml:"val,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventTransportState .
|
|
||||||
type EventTransportState struct {
|
|
||||||
Value string `xml:"val,attr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// DMRextractor - Get the AVTransport URL from the main DMR xml.
|
|
||||||
func DMRextractor(dmrurl string) (string, string, error) {
|
|
||||||
var root Root
|
|
||||||
|
|
||||||
parsedURL, err := url.Parse(dmrurl)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
req, err := http.NewRequest("GET", dmrurl, nil)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
xmlresp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
defer xmlresp.Body.Close()
|
|
||||||
|
|
||||||
xmlbody, err := io.ReadAll(xmlresp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
xml.Unmarshal(xmlbody, &root)
|
|
||||||
for i := 0; i < len(root.Device.ServiceList.Services); i++ {
|
|
||||||
if root.Device.ServiceList.Services[i].ID == "urn:upnp-org:serviceId:AVTransport" {
|
|
||||||
avtransportControlURL := parsedURL.Scheme + "://" + parsedURL.Host + root.Device.ServiceList.Services[i].ControlURL
|
|
||||||
avtransportEventSubURL := parsedURL.Scheme + "://" + parsedURL.Host + root.Device.ServiceList.Services[i].EventSubURL
|
|
||||||
return avtransportControlURL, avtransportEventSubURL, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", "", errors.New("something broke somewhere - wrong DMR URL?")
|
|
||||||
}
|
|
||||||
|
|
||||||
// EventNotifyParser - Parse the Notify messages from the media renderer.
|
|
||||||
func EventNotifyParser(xmlbody string) (string, string, error) {
|
|
||||||
var root EventPropertySet
|
|
||||||
err := xml.Unmarshal([]byte(xmlbody), &root)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
previousstate := root.EventInstance.EventCurrentTransportActions.Value
|
|
||||||
newstate := root.EventInstance.EventTransportState.Value
|
|
||||||
|
|
||||||
return previousstate, newstate, nil
|
|
||||||
}
|
|
Reference in New Issue
Block a user