From 0bef5b35afe6fef3fe5ab7c695cba30537920265 Mon Sep 17 00:00:00 2001
From: Alex Ballas <alex@ballas.org>
Date: Sat, 13 Feb 2021 01:42:34 +0200
Subject: [PATCH] Fully deprecated the goupnp/soap package. Fixed some Windows
 compatibility issues. Improved the subtitles support and error handling.

---
 flagfuncs.go              |  50 ++++++++++++-----
 go.mod                    |   1 -
 go.sum                    |   4 --
 go2tv.go                  |   1 +
 servefiles/servefiles.go  |   8 +--
 soapcalls/soapbuilders.go | 113 ++++++++++++++++++++++++++++++++++++--
 soapcalls/soapcallers.go  |  40 ++++++++------
 7 files changed, 173 insertions(+), 44 deletions(-)

diff --git a/flagfuncs.go b/flagfuncs.go
index 9abee986..fbdd1062 100644
--- a/flagfuncs.go
+++ b/flagfuncs.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"net/url"
 	"os"
+	"path/filepath"
+	"runtime"
 	"sort"
 )
 
@@ -25,44 +27,56 @@ func listFlagFunction() error {
 	sort.Ints(keys)
 
 	for _, k := range keys {
-		fmt.Printf("\033[1mDevice %v\033[0m\n", k)
-		fmt.Printf("\033[1m--------\033[0m\n")
-		fmt.Printf("\033[1mModel\033[0m: %s\n", devices[k][0])
-		fmt.Printf("\033[1mURL\033[0m:   %s\n", devices[k][1])
+		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) {
+	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 == true {
 		return true, nil
 	}
 
-	if err := checkVflag(); err != nil {
-		return false, err
-	}
-
 	if err := checkSflag(); err != nil {
 		return false, err
 	}
+
 	return false, nil
 }
 
 func checkVflag() error {
-	if *videoArg == "" {
-		err := errors.New("No video file defined")
-		return err
-	}
-	if _, err := os.Stat(*videoArg); os.IsNotExist(err) {
-		return err
+	if *listPtr == false {
+		if *videoArg == "" {
+			err := errors.New("No video file defined")
+			return err
+		}
+		if _, err := os.Stat(*videoArg); os.IsNotExist(err) {
+			return err
+		}
 	}
 	return nil
 }
@@ -72,6 +86,14 @@ func checkSflag() error {
 		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
 }
diff --git a/go.mod b/go.mod
index 5a6a1aae..2561dbbf 100644
--- a/go.mod
+++ b/go.mod
@@ -3,7 +3,6 @@ module github.com/alexballas/go2tv
 go 1.15
 
 require (
-	github.com/huin/goupnp v1.0.0
 	github.com/koron/go-ssdp v0.0.2
 	golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c // indirect
 )
diff --git a/go.sum b/go.sum
index c61a2cda..e50145f8 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,7 @@
-github.com/huin/goupnp v1.0.0 h1:wg75sLpL6DZqwHQN6E1Cfk6mtfzS45z8OV+ic+DtHRo=
-github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc=
-github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o=
 github.com/koron/go-ssdp v0.0.2 h1:fL3wAoyT6hXHQlORyXUW4Q23kkQpJRgEAYcZB5BR71o=
 github.com/koron/go-ssdp v0.0.2/go.mod h1:XoLfkAiA2KeZsYh4DbHxD7h3nR2AZNqVQOa+LJuqPYs=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA=
 golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
diff --git a/go2tv.go b/go2tv.go
index 88439070..cd64f62d 100644
--- a/go2tv.go
+++ b/go2tv.go
@@ -78,6 +78,7 @@ func main() {
 	go func() { s.ServeFiles(serverStarted, absVideoFile, absSubtitlesFile) }()
 	// Wait for HTTP server to properly initialize
 	<-serverStarted
+
 	if err := tvdata.SendtoTV("Play"); err != nil {
 		fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
 		os.Exit(1)
diff --git a/servefiles/servefiles.go b/servefiles/servefiles.go
index 340660ad..d824ed9e 100644
--- a/servefiles/servefiles.go
+++ b/servefiles/servefiles.go
@@ -72,14 +72,14 @@ func (f *filesToServe) serveSubtitlesHandler(w http.ResponseWriter, req *http.Re
 	filePath, err := os.Open(f.Subtitles)
 	defer filePath.Close()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
-		os.Exit(1)
+		http.Error(w, "", 404)
+		return
 	}
 
 	fileStat, err := filePath.Stat()
 	if err != nil {
-		fmt.Fprintf(os.Stderr, "Encountered error(s): %s\n", err)
-		os.Exit(1)
+		http.Error(w, "", 404)
+		return
 	}
 	http.ServeContent(w, req, filepath.Base(f.Subtitles), fileStat.ModTime(), filePath)
 
diff --git a/soapcalls/soapbuilders.go b/soapcalls/soapbuilders.go
index 5668a2f6..ba5dbf6c 100644
--- a/soapcalls/soapbuilders.go
+++ b/soapcalls/soapbuilders.go
@@ -6,18 +6,65 @@ import (
 	"fmt"
 )
 
-type Envelope struct {
+// 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"`
-	Body     Body     `xml:"s:Body"`
+	PlayBody PlayBody `xml:"s:Body"`
 }
 
-type Body struct {
+// 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
+}
+
+// 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"`
@@ -26,11 +73,13 @@ type SetAVTransportURI struct {
 	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"`
@@ -40,6 +89,7 @@ type DIDLLite struct {
 	DIDLLiteItem DIDLLiteItem `xml:"item"`
 }
 
+// DIDLLiteItem .
 type DIDLLiteItem struct {
 	XMLName          xml.Name         `xml:"item"`
 	ID               string           `xml:"id,attr"`
@@ -52,18 +102,21 @@ type DIDLLiteItem struct {
 	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"`
@@ -112,11 +165,11 @@ func setAVTransportSoapBuild(videoURL, subtitleURL string) ([]byte, error) {
 		return make([]byte, 0), err
 	}
 
-	d := Envelope{
+	d := SetAVTransportEnvelope{
 		XMLName:  xml.Name{},
 		Schema:   "http://schemas.xmlsoap.org/soap/envelope/",
 		Encoding: "http://schemas.xmlsoap.org/soap/encoding/",
-		Body: Body{
+		Body: SetAVTransportBody{
 			XMLName: xml.Name{},
 			SetAVTransportURI: SetAVTransportURI{
 				XMLName:     xml.Name{},
@@ -142,3 +195,53 @@ func setAVTransportSoapBuild(videoURL, subtitleURL string) ([]byte, error) {
 
 	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
+}
diff --git a/soapcalls/soapcallers.go b/soapcalls/soapcallers.go
index 69dd3a57..ae92f3c6 100644
--- a/soapcalls/soapcallers.go
+++ b/soapcalls/soapcallers.go
@@ -5,18 +5,8 @@ import (
 	"fmt"
 	"net/http"
 	"net/url"
-
-	"github.com/huin/goupnp/soap"
 )
 
-type playStopRequest struct {
-	InstanceID string
-	Speed      string
-}
-
-type playStopResponse struct {
-}
-
 // TVPayload - we need this populated in order
 type TVPayload struct {
 	TransportURL string
@@ -54,18 +44,36 @@ func setAVTransportSoapCall(videoURL, subtitleURL, transporturl string) error {
 
 // PlayStopSoapCall - Build and call the play soap call
 func playStopSoapCall(action, transporturl string) error {
+	parsedURLtransport, err := url.Parse(transporturl)
+	if err != nil {
+		return err
+	}
 
-	psRequest := &playStopRequest{InstanceID: "0", Speed: "1"}
-	psResponse := &playStopResponse{}
+	var xml []byte
+	if action == "Play" {
+		xml, err = playSoapBuild()
+	}
 
-	parsedURL, err := url.Parse(transporturl)
+	if action == "Stop" {
+		xml, err = stopSoapBuild()
+	}
+
+	client := &http.Client{}
+	req, err := http.NewRequest("POST", parsedURLtransport.String(), bytes.NewReader(xml))
 	if err != nil {
 		return err
 	}
 
-	newPlaycall := soap.NewSOAPClient(*parsedURL)
-	if err := newPlaycall.PerformAction("urn:schemas-upnp-org:service:AVTransport:1",
-		action, psRequest, psResponse); err != nil {
+	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