Update project structure

This commit is contained in:
Alex Ballas
2021-04-13 20:18:25 +03:00
parent 5ddac8d52d
commit 6b3567fd64
9 changed files with 1 additions and 1355 deletions

View File

@@ -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/

View File

@@ -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
View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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("&#34;"), []byte(`"`))
b = bytes.ReplaceAll(b, []byte("&amp;"), []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
}

View File

@@ -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")
}

View File

@@ -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
}