1486 lines
49 KiB
Go
1486 lines
49 KiB
Go
package soapcalls
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-retryablehttp"
|
|
"github.com/pkg/errors"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
type States struct {
|
|
PreviousState string
|
|
NewState string
|
|
ProcessStop bool
|
|
}
|
|
|
|
var (
|
|
ErrNoMatchingFileType = errors.New("no matching file type")
|
|
ErrZombieCallbacks = errors.New("zombie callbacks, we should ignore those")
|
|
)
|
|
|
|
// TVPayload is the heart of Go2TV. We pass that type to the
|
|
// webserver. We need to explicitly initialize it.
|
|
type TVPayload struct {
|
|
Logger zerolog.Logger
|
|
LogOutput io.Writer
|
|
ctx context.Context
|
|
CurrentTimers map[string]*time.Timer
|
|
InitialMediaRenderersStates map[string]bool
|
|
MediaRenderersStates map[string]*States
|
|
FFmpegSeek int
|
|
FFmpegPath string
|
|
FFmpegSubsPath string
|
|
EventURL string
|
|
ControlURL string
|
|
MediaURL string
|
|
MediaType string
|
|
MediaPath string
|
|
SubtitlesURL string
|
|
CallbackURL string
|
|
ConnectionManagerURL string
|
|
RenderingControlURL string
|
|
mu sync.RWMutex
|
|
initLogOnce sync.Once
|
|
Transcode bool
|
|
Seekable bool
|
|
}
|
|
|
|
type getMuteRespBody struct {
|
|
XMLName xml.Name `xml:"Envelope"`
|
|
Text string `xml:",chardata"`
|
|
EncodingStyle string `xml:"encodingStyle,attr"`
|
|
S string `xml:"s,attr"`
|
|
Body struct {
|
|
Text string `xml:",chardata"`
|
|
GetMuteResponse struct {
|
|
Text string `xml:",chardata"`
|
|
U string `xml:"u,attr"`
|
|
CurrentMute string `xml:"CurrentMute"`
|
|
} `xml:"GetMuteResponse"`
|
|
} `xml:"Body"`
|
|
}
|
|
|
|
type getVolumeRespBody struct {
|
|
XMLName xml.Name `xml:"Envelope"`
|
|
Text string `xml:",chardata"`
|
|
EncodingStyle string `xml:"encodingStyle,attr"`
|
|
S string `xml:"s,attr"`
|
|
Body struct {
|
|
Text string `xml:",chardata"`
|
|
GetVolumeResponse struct {
|
|
Text string `xml:",chardata"`
|
|
U string `xml:"u,attr"`
|
|
CurrentVolume string `xml:"CurrentVolume"`
|
|
} `xml:"GetVolumeResponse"`
|
|
} `xml:"Body"`
|
|
}
|
|
|
|
type protocolInfoResponse struct {
|
|
XMLName xml.Name `xml:"Envelope"`
|
|
Text string `xml:",chardata"`
|
|
S string `xml:"s,attr"`
|
|
EncodingStyle string `xml:"encodingStyle,attr"`
|
|
Body struct {
|
|
Text string `xml:",chardata"`
|
|
GetProtocolInfoResponse struct {
|
|
Text string `xml:",chardata"`
|
|
U string `xml:"u,attr"`
|
|
Source string `xml:"Source"`
|
|
Sink string `xml:"Sink"`
|
|
} `xml:"GetProtocolInfoResponse"`
|
|
} `xml:"Body"`
|
|
}
|
|
|
|
type getMediaInfoResponse struct {
|
|
XMLName xml.Name `xml:"Envelope"`
|
|
Text string `xml:",chardata"`
|
|
EncodingStyle string `xml:"encodingStyle,attr"`
|
|
S string `xml:"s,attr"`
|
|
Body struct {
|
|
Text string `xml:",chardata"`
|
|
GetMediaInfoResponse struct {
|
|
Text string `xml:",chardata"`
|
|
U string `xml:"u,attr"`
|
|
NrTracks string `xml:"NrTracks"`
|
|
MediaDuration string `xml:"MediaDuration"`
|
|
CurrentURI string `xml:"CurrentURI"`
|
|
CurrentURIMetaData string `xml:"CurrentURIMetaData"`
|
|
NextURI string `xml:"NextURI"`
|
|
NextURIMetaData string `xml:"NextURIMetaData"`
|
|
PlayMedium string `xml:"PlayMedium"`
|
|
RecordMedium string `xml:"RecordMedium"`
|
|
WriteStatus string `xml:"WriteStatus"`
|
|
} `xml:"GetMediaInfoResponse"`
|
|
} `xml:"Body"`
|
|
}
|
|
|
|
type getTransportInfoResponse struct {
|
|
XMLName xml.Name `xml:"Envelope"`
|
|
Text string `xml:",chardata"`
|
|
S string `xml:"s,attr"`
|
|
EncodingStyle string `xml:"encodingStyle,attr"`
|
|
Body struct {
|
|
Text string `xml:",chardata"`
|
|
GetTransportInfoResponse struct {
|
|
Text string `xml:",chardata"`
|
|
U string `xml:"u,attr"`
|
|
CurrentTransportState string `xml:"CurrentTransportState"`
|
|
CurrentTransportStatus string `xml:"CurrentTransportStatus"`
|
|
CurrentSpeed string `xml:"CurrentSpeed"`
|
|
} `xml:"GetTransportInfoResponse"`
|
|
} `xml:"Body"`
|
|
}
|
|
|
|
type getPositionInfoResponse struct {
|
|
XMLName xml.Name `xml:"Envelope"`
|
|
Text string `xml:",chardata"`
|
|
S string `xml:"s,attr"`
|
|
EncodingStyle string `xml:"encodingStyle,attr"`
|
|
Body struct {
|
|
Text string `xml:",chardata"`
|
|
GetPositionInfoResponse struct {
|
|
Text string `xml:",chardata"`
|
|
U string `xml:"u,attr"`
|
|
Track string `xml:"Track"`
|
|
TrackDuration string `xml:"TrackDuration"`
|
|
TrackMetaData string `xml:"TrackMetaData"`
|
|
TrackURI string `xml:"TrackURI"`
|
|
RelTime string `xml:"RelTime"`
|
|
AbsTime string `xml:"AbsTime"`
|
|
RelCount string `xml:"RelCount"`
|
|
AbsCount string `xml:"AbsCount"`
|
|
} `xml:"GetPositionInfoResponse"`
|
|
} `xml:"Body"`
|
|
}
|
|
|
|
func (p *TVPayload) setAVTransportSoapCall() error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall parse error: %w", err)
|
|
}
|
|
|
|
xmlData, err := setAVTransportSoapBuild(p)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "setAVTransportSoapBuild").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall soap build error: %w", err)
|
|
}
|
|
|
|
retryClient := retryablehttp.NewClient()
|
|
retryClient.RetryMax = 3
|
|
retryClient.Logger = nil
|
|
client := retryClient.StandardClient()
|
|
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlData))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "setAVTransportSoapCall").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlData))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "Do POST").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setAVTransportSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("setAVTransportSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "setAVTransportSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *TVPayload) setNextAVTransportSoapCall(clear bool) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall parse error: %w", err)
|
|
}
|
|
|
|
xmlData, err := setNextAVTransportSoapBuild(p, clear)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "setNextAVTransportSoapBuild").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall soap build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlData))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#SetNextAVTransportURI"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "setNextAVTransportSoapCall").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlData))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Do POST").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "setNextAVTransportSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("setNextAVTransportSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "setNextAVTransportSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return nil
|
|
}
|
|
|
|
// PlayPauseStopSoapCall builds and sends the AVTransport actions for Play Pause and Stop.
|
|
func (p *TVPayload) PlayPauseStopSoapCall(action string) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall parse error: %w", err)
|
|
}
|
|
|
|
var xmlData []byte
|
|
retry := false
|
|
|
|
switch action {
|
|
case "Play":
|
|
xmlData, err = playSoapBuild()
|
|
case "Stop":
|
|
xmlData, err = stopSoapBuild()
|
|
retry = true
|
|
case "Pause":
|
|
xmlData, err = pauseSoapBuild()
|
|
}
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "Action Error").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall action error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
if retry {
|
|
retryClient := retryablehttp.NewClient()
|
|
retryClient.RetryMax = 3
|
|
retryClient.Logger = nil
|
|
client = retryClient.StandardClient()
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlData))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#` + action + `"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "AVTransportActionSoapCall").Str("Action", action+" Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlData))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "Do POST").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "AVTransportActionSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("AVTransportActionSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "AVTransportActionSoapCall").Str("Action", action+" Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return nil
|
|
}
|
|
|
|
// SeekSoapCall builds and sends the AVTransport actions for Seek.
|
|
func (p *TVPayload) SeekSoapCall(reltime string) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall parse error: %w", err)
|
|
}
|
|
|
|
var xmlData []byte
|
|
|
|
xmlData, err = seekSoapBuild(reltime)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "Action Error").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall action error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlData))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#Seek"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SeekSoapCall").Str("Action", "Seek Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlData))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "Do POST").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SeekSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("SeekSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SeekSoapCall").Str("Action", "Seek Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return nil
|
|
}
|
|
|
|
// SubscribeSoapCall send a SUBSCRIBE request to the DMR device.
|
|
// If we explicitly pass the UUID, then we refresh it instead.
|
|
func (p *TVPayload) SubscribeSoapCall(uuidInput string) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
delete(p.CurrentTimers, uuidInput)
|
|
|
|
parsedURLcontrol, err := url.Parse(p.EventURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "URL Parse #1").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall #1 parse error: %w", err)
|
|
}
|
|
|
|
parsedURLcallback, err := url.Parse(p.CallbackURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "URL Parse #2").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall #2 parse error: %w", err)
|
|
}
|
|
|
|
retryClient := retryablehttp.NewClient()
|
|
retryClient.RetryMax = 3
|
|
retryClient.Logger = nil
|
|
|
|
client := retryClient.StandardClient()
|
|
|
|
req, err := http.NewRequestWithContext(p.ctx, "SUBSCRIBE", parsedURLcontrol.String(), nil)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "Prepare SUBSCRIBE").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall SUBSCRIBE error: %w", 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")
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SubscribeSoapCall").Str("Action", "Subscribe Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg("")
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "Do SUBSCRIBE").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall Do SUBSCRIBE error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SubscribeSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("SubscribeSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SubscribeSoapCall").Str("Action", "Subscribe Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
var uuid string
|
|
|
|
if res.Status != "200 OK" {
|
|
if uuidInput != "" {
|
|
// We're calling the unsubscribe method to make sure
|
|
// we clean up any remaining states for the specific
|
|
// uuid. The actual UNSUBSCRIBE request to the media
|
|
// renderer may still fail with error 412, but it's fine.
|
|
_ = p.UnsubscribeSoapCall(uuid)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if len(res.Header["Sid"]) == 0 {
|
|
// This should be an impossible case
|
|
return nil
|
|
}
|
|
|
|
uuid = res.Header["Sid"][0]
|
|
uuid = strings.TrimLeft(uuid, "[")
|
|
uuid = strings.TrimLeft(uuid, "]")
|
|
uuid = strings.TrimPrefix(uuid, "uuid:")
|
|
|
|
// We don't really need to initialize or set
|
|
// the State if we're just refreshing the uuid.
|
|
if uuidInput == "" {
|
|
p.CreateMRstate(uuid)
|
|
}
|
|
|
|
timeoutReply := "300"
|
|
if len(res.Header["Timeout"]) > 0 {
|
|
timeoutReply = strings.TrimPrefix(res.Header["Timeout"][0], "Second-")
|
|
}
|
|
|
|
p.RefreshLoopUUIDSoapCall(uuid, timeoutReply)
|
|
|
|
return nil
|
|
}
|
|
|
|
// UnsubscribeSoapCall sends an UNSUBSCRIBE request to the DMR device
|
|
// and cleans up any stored states for the provided UUID.
|
|
func (p *TVPayload) UnsubscribeSoapCall(uuid string) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
p.DeleteMRstate(uuid)
|
|
|
|
parsedURLcontrol, err := url.Parse(p.EventURL)
|
|
if err != nil {
|
|
return fmt.Errorf("UnsubscribeSoapCall parse error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
|
|
req, err := http.NewRequestWithContext(p.ctx, "UNSUBSCRIBE", parsedURLcontrol.String(), nil)
|
|
if err != nil {
|
|
return fmt.Errorf("UnsubscribeSoapCall UNSUBSCRIBE error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SID": []string{"uuid:" + uuid},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
req.Header.Del("User-Agent")
|
|
|
|
_, err = client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("UnsubscribeSoapCall Do UNSUBSCRIBE error: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RefreshLoopUUIDSoapCall refreshes the UUID.
|
|
func (p *TVPayload) RefreshLoopUUIDSoapCall(uuid, timeout string) {
|
|
triggerTime := 5
|
|
timeoutInt, err := strconv.Atoi(timeout)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Refresh token after Timeout / 2 seconds.
|
|
if timeoutInt > 20 {
|
|
triggerTime = timeoutInt / 5
|
|
}
|
|
triggerTimefunc := time.Duration(triggerTime) * time.Second
|
|
|
|
f := p.refreshLoopUUIDAsyncSoapCall(uuid)
|
|
timer := time.AfterFunc(triggerTimefunc, f)
|
|
p.CurrentTimers[uuid] = timer
|
|
}
|
|
|
|
func (p *TVPayload) refreshLoopUUIDAsyncSoapCall(uuid string) func() {
|
|
return func() {
|
|
_ = p.SubscribeSoapCall(uuid)
|
|
}
|
|
}
|
|
|
|
// GetMuteSoapCall sends a SOAP request to the TV to get the current mute status.
|
|
// It constructs the SOAP request, sends it to the TV, and parses the response.
|
|
func (p *TVPayload) GetMuteSoapCall() (string, error) {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedRenderingControlURL, err := url.Parse(p.RenderingControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = getMuteSoapBuild()
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "Build").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedRenderingControlURL.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:RenderingControl:1#GetMute"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetMuteSoapCall").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("GetMuteSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
var buf bytes.Buffer
|
|
|
|
tresp := io.TeeReader(res.Body, &buf)
|
|
|
|
var respGetMute getMuteRespBody
|
|
if err = xml.NewDecoder(tresp).Decode(&respGetMute); err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "XML Decode").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall XML Decode error: %w", err)
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(&buf)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetMuteSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return "", fmt.Errorf("GetMuteSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetMuteSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return respGetMute.Body.GetMuteResponse.CurrentMute, nil
|
|
}
|
|
|
|
// SetMuteSoapCall sends a SOAP request to set the mute state of the TV.
|
|
// It constructs the SOAP request, sets the necessary headers, and sends the request to the TV's RenderingControlURL.
|
|
// The function logs the request and response details for debugging purposes.
|
|
func (p *TVPayload) SetMuteSoapCall(number string) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedRenderingControlURL, err := url.Parse(p.RenderingControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetMuteSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("SetMuteSoapCall parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = setMuteSoapBuild(number)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetMuteSoapCall").Str("Action", "Build").Err(err).Msg("")
|
|
return fmt.Errorf("SetMuteSoapCall build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedRenderingControlURL.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetMuteSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("SetMuteSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:RenderingControl:1#SetMute"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetMuteSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("SetMuteSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SetMuteSoapCall").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("SetMuteSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetMuteSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("SetMuteSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetMuteSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("SetMuteSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SetMuteSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetVolumeSoapCall returns tue volume level for our device.
|
|
func (p *TVPayload) GetVolumeSoapCall() (int, error) {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedRenderingControlURL, err := url.Parse(p.RenderingControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = getVolumeSoapBuild()
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Build").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedRenderingControlURL.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:RenderingControl:1#GetVolume"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetVolumeSoapCall").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Do POST").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
var buf bytes.Buffer
|
|
|
|
tresp := io.TeeReader(res.Body, &buf)
|
|
|
|
var respGetVolume getVolumeRespBody
|
|
if err = xml.NewDecoder(tresp).Decode(&respGetVolume); err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "XML Decode").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall XML Decode error: %w", err)
|
|
}
|
|
|
|
intVolume, err := strconv.Atoi(respGetVolume.Body.GetVolumeResponse.CurrentVolume)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Parse Volume").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall failed to parse volume value: %w", err)
|
|
}
|
|
|
|
if intVolume < 0 {
|
|
intVolume = 0
|
|
}
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(&buf)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetVolumeSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return 0, fmt.Errorf("GetVolumeSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetVolumeSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return intVolume, nil
|
|
}
|
|
|
|
// SetVolumeSoapCall sets the desired volume level.
|
|
func (p *TVPayload) SetVolumeSoapCall(v string) error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedRenderingControlURL, err := url.Parse(p.RenderingControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetVolumeSoapCall").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("SetVolumeSoapCall parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = setVolumeSoapBuild(v)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetVolumeSoapCall").Str("Action", "Build").Err(err).Msg("")
|
|
return fmt.Errorf("SetVolumeSoapCall build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedRenderingControlURL.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetVolumeSoapCall").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("SetVolumeSoapCall POST error: %w", err)
|
|
}
|
|
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:RenderingControl:1#SetVolume"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetVolumeSoapCall").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("SetVolumeSoapCall Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SetVolumeSoapCall").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("SetVolumeSoapCall Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetVolumeSoapCall").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("SetVolumeSoapCall Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "SetVolumeSoapCall").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("SetVolumeSoapCall Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "SetVolumeSoapCall").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetProtocolInfo retrieves the protocol information from the device.
|
|
// It constructs a SOAP request, sends it to the device, and processes the response.
|
|
func (p *TVPayload) GetProtocolInfo() error {
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedConnectionManagerURL, err := url.Parse(p.ConnectionManagerURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetProtocolInfo").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return fmt.Errorf("GetProtocolInfo parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = getProtocolInfoSoapBuild()
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetProtocolInfo").Str("Action", "Build").Err(err).Msg("")
|
|
return fmt.Errorf("GetProtocolInfo build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedConnectionManagerURL.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetProtocolInfo").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return fmt.Errorf("GetProtocolInfo POST error: %w", err)
|
|
}
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:ConnectionManager:1#GetProtocolInfo"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetProtocolInfo").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return fmt.Errorf("GetProtocolInfo Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetProtocolInfo").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("GetProtocolInfo Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetProtocolInfo").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return fmt.Errorf("GetProtocolInfo Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetProtocolInfo").Str("Action", "Readall").Err(err).Msg("")
|
|
return fmt.Errorf("GetProtocolInfo Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetProtocolInfo").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
if err := parseProtocolInfo(resBytes, p.MediaType); err != nil {
|
|
return fmt.Errorf("GetProtocolInfo Selected device does not support the media type: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Gapless requests our device's media info and returns the Next URI.
|
|
func (p *TVPayload) Gapless() (string, error) {
|
|
if p == nil {
|
|
return "", errors.New("Gapless, nil tvdata")
|
|
}
|
|
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = getMediaInfoSoapBuild()
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "Build").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless POST error: %w", err)
|
|
}
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetMediaInfo"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "Gapless").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return "", fmt.Errorf("Gapless Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "Readall").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "Gapless").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
var respMedialInfo getMediaInfoResponse
|
|
|
|
if err := xml.Unmarshal(resBytes, &respMedialInfo); err != nil {
|
|
p.Log().Error().Str("Method", "Gapless").Str("Action", "Unmarshal").Err(err).Msg("")
|
|
return "", fmt.Errorf("Gapless Failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
nextURI := respMedialInfo.Body.GetMediaInfoResponse.NextURI
|
|
|
|
return nextURI, nil
|
|
}
|
|
|
|
// GetTransportInfo retrieves the transport information from the TV device.
|
|
// It returns a slice of strings containing the current transport state, status, and speed, or an error if the operation fails.
|
|
func (p *TVPayload) GetTransportInfo() ([]string, error) {
|
|
if p == nil {
|
|
return nil, errors.New("GetTransportInfo, nil tvdata")
|
|
}
|
|
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = getTransportInfoSoapBuild()
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Build").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo POST error: %w", err)
|
|
}
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetTransportInfo").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetTransportInfo Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Readall").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetTransportInfo").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
var respTransportInfo getTransportInfoResponse
|
|
|
|
if err := xml.Unmarshal(resBytes, &respTransportInfo); err != nil {
|
|
p.Log().Error().Str("Method", "GetTransportInfo").Str("Action", "Unmarshal").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetTransportInfo Failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
r := respTransportInfo.Body.GetTransportInfoResponse
|
|
state := r.CurrentTransportState
|
|
status := r.CurrentTransportStatus
|
|
speed := r.CurrentSpeed
|
|
|
|
return []string{state, status, speed}, nil
|
|
}
|
|
|
|
// GetPositionInfo retrieves the position information of the TV.
|
|
// It returns a slice of strings containing the track duration and relative time, or an error if the operation fails.
|
|
func (p *TVPayload) GetPositionInfo() ([]string, error) {
|
|
if p == nil {
|
|
return nil, errors.New("GetPositionInfo, nil tvdata")
|
|
}
|
|
|
|
if p.ctx == nil {
|
|
p.ctx = context.Background()
|
|
}
|
|
|
|
parsedURLtransport, err := url.Parse(p.ControlURL)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "URL Parse").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo parse error: %w", err)
|
|
}
|
|
|
|
var xmlbuilder []byte
|
|
|
|
xmlbuilder, err = getPositionInfoSoapBuild()
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "Build").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo build error: %w", err)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
req, err := http.NewRequestWithContext(p.ctx, "POST", parsedURLtransport.String(), bytes.NewReader(xmlbuilder))
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "Prepare POST").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo POST error: %w", err)
|
|
}
|
|
req.Header = http.Header{
|
|
"SOAPAction": []string{`"urn:schemas-upnp-org:service:AVTransport:1#GetPositionInfo"`},
|
|
"content-type": []string{"text/xml"},
|
|
"charset": []string{"utf-8"},
|
|
"Connection": []string{"close"},
|
|
}
|
|
|
|
headerBytesReq, err := json.Marshal(req.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "Header Marshaling").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo Request Marshaling error: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetPositionInfo").Str("Action", "Request").
|
|
RawJSON("Headers", headerBytesReq).
|
|
Msg(string(xmlbuilder))
|
|
|
|
res, err := client.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("GetPositionInfo Do POST error: %w", err)
|
|
}
|
|
defer res.Body.Close()
|
|
|
|
headerBytesRes, err := json.Marshal(res.Header)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "Header Marshaling #2").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo Response Marshaling error: %w", err)
|
|
}
|
|
|
|
resBytes, err := io.ReadAll(res.Body)
|
|
if err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "Readall").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo Failed to read response: %w", err)
|
|
}
|
|
|
|
p.Log().Debug().
|
|
Str("Method", "GetPositionInfo").Str("Action", "Response").Str("Status Code", strconv.Itoa(res.StatusCode)).
|
|
RawJSON("Headers", headerBytesRes).
|
|
Msg(string(resBytes))
|
|
|
|
var respPositionInfo getPositionInfoResponse
|
|
|
|
if err := xml.Unmarshal(resBytes, &respPositionInfo); err != nil {
|
|
p.Log().Error().Str("Method", "GetPositionInfo").Str("Action", "Unmarshal").Err(err).Msg("")
|
|
return nil, fmt.Errorf("GetPositionInfo Failed to unmarshal response: %w", err)
|
|
}
|
|
|
|
r := respPositionInfo.Body.GetPositionInfoResponse
|
|
duration := r.TrackDuration
|
|
reltime := r.RelTime
|
|
|
|
return []string{duration, reltime}, nil
|
|
}
|
|
|
|
// SendtoTV is a higher level method that gracefully handles the various
|
|
// states when communicating with the DMR devices.
|
|
func (p *TVPayload) SendtoTV(action string) error {
|
|
if action == "ClearQueue" {
|
|
if err := p.setNextAVTransportSoapCall(true); err != nil {
|
|
return fmt.Errorf("SendtoTV setNextAVTransportSoapCall call error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if action == "Queue" {
|
|
if err := p.setNextAVTransportSoapCall(false); err != nil {
|
|
return fmt.Errorf("SendtoTV setNextAVTransportSoapCall call error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if action == "Play1" {
|
|
if err := p.GetProtocolInfo(); err != nil {
|
|
return fmt.Errorf("SendtoTV getProtocolInfo call error: %w", err)
|
|
}
|
|
if err := p.SubscribeSoapCall(""); err != nil {
|
|
return fmt.Errorf("SendtoTV subscribe call error: %w", err)
|
|
}
|
|
if err := p.setAVTransportSoapCall(); err != nil {
|
|
return fmt.Errorf("SendtoTV set AVT Transport error: %w", err)
|
|
}
|
|
action = "Play"
|
|
}
|
|
|
|
if action == "Stop" {
|
|
p.mu.RLock()
|
|
localStates := make(map[string]*States)
|
|
for key, value := range p.MediaRenderersStates {
|
|
localStates[key] = value
|
|
}
|
|
p.mu.RUnlock()
|
|
|
|
// Cleaning up all uuids on force stop.
|
|
for uuids := range localStates {
|
|
if err := p.UnsubscribeSoapCall(uuids); err != nil {
|
|
return fmt.Errorf("SendtoTV unsubscribe call error: %w", err)
|
|
}
|
|
}
|
|
|
|
// Clear timers on Stop to avoid errors responses
|
|
// from the media renderers. If we don't clear those, we
|
|
// might receive a "412 Precondition Failed" error.
|
|
for uuid, timer := range p.CurrentTimers {
|
|
timer.Stop()
|
|
delete(p.CurrentTimers, uuid)
|
|
}
|
|
}
|
|
|
|
if err := p.PlayPauseStopSoapCall(action); err != nil {
|
|
return fmt.Errorf("SendtoTV Play/Stop/Pause action error: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateMRstate updates the mediaRenderersStates map with the state.
|
|
// Returns true or false to verify that the actual update took place.
|
|
func (p *TVPayload) UpdateMRstate(previous, new, uuid string) bool {
|
|
if previous == "" || new == "" {
|
|
return false
|
|
}
|
|
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
// If the UUID is not available in p.InitialMediaRenderersStates,
|
|
// it probably expired and there is not much we can do with it.
|
|
// Trying to send an UNSUBSCRIBE call for that UUID will result
|
|
// in a 412 error as per the UPNP documentation
|
|
// https://openconnectivity.org/upnp-specs/UPnP-arch-DeviceArchitecture-v1.1.pdf
|
|
// (page 94).
|
|
if p.InitialMediaRenderersStates[uuid] {
|
|
p.MediaRenderersStates[uuid].PreviousState = previous
|
|
p.MediaRenderersStates[uuid].NewState = new
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// CreateMRstate initializes the media renderer state for a given UUID.
|
|
// It locks the TVPayload to ensure thread safety, sets the initial state
|
|
// to true, and creates a new States instance for the media renderer.
|
|
func (p *TVPayload) CreateMRstate(uuid string) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.InitialMediaRenderersStates[uuid] = true
|
|
p.MediaRenderersStates[uuid] = &States{}
|
|
}
|
|
|
|
// DeleteMRstate removes the media renderer state associated with the given UUID
|
|
// from both InitialMediaRenderersStates and MediaRenderersStates maps. It ensures
|
|
// that the operation is thread-safe by acquiring a lock on the TVPayload's mutex.
|
|
func (p *TVPayload) DeleteMRstate(uuid string) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
delete(p.InitialMediaRenderersStates, uuid)
|
|
delete(p.MediaRenderersStates, uuid)
|
|
}
|
|
|
|
// SetProcessStopTrue sets the ProcessStop field to true for the media renderer
|
|
// identified by the given UUID. It locks the TVPayload's mutex to ensure
|
|
// thread-safe access to the MediaRenderersStates map.
|
|
func (p *TVPayload) SetProcessStopTrue(uuid string) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
p.MediaRenderersStates[uuid].ProcessStop = true
|
|
}
|
|
|
|
// GetProcessStop checks if the process stop flag is set for a given media renderer identified by the UUID.
|
|
// It returns true if the process stop flag is set, or an error if the media renderer is not in the initial state.
|
|
func (p *TVPayload) GetProcessStop(uuid string) (bool, error) {
|
|
p.mu.RLock()
|
|
defer p.mu.RUnlock()
|
|
if p.InitialMediaRenderersStates[uuid] {
|
|
return p.MediaRenderersStates[uuid].ProcessStop, nil
|
|
}
|
|
|
|
return true, ErrZombieCallbacks
|
|
}
|
|
|
|
func (p *TVPayload) Log() *zerolog.Logger {
|
|
if p.LogOutput != nil {
|
|
p.initLogOnce.Do(func() {
|
|
p.Logger = zerolog.New(p.LogOutput).With().Timestamp().Logger()
|
|
})
|
|
}
|
|
|
|
return &p.Logger
|
|
}
|
|
|
|
func parseProtocolInfo(b []byte, mt string) error {
|
|
var respProtocolInfo protocolInfoResponse
|
|
|
|
// We were unable to detect the media type, so we
|
|
// should allow this to go through by default and
|
|
// hope the DMR accepts it.
|
|
if mt == "/" {
|
|
return nil
|
|
}
|
|
|
|
if strings.Contains(mt, "/") {
|
|
mt = strings.Split(mt, "/")[0]
|
|
}
|
|
|
|
if err := xml.Unmarshal(b, &respProtocolInfo); err != nil {
|
|
return err
|
|
}
|
|
|
|
protocols := strings.Split(respProtocolInfo.Body.GetProtocolInfoResponse.Sink, ",")
|
|
|
|
// We got no response from the device. Instead of just straight failing, we should
|
|
// just let the device play our media file and hope it works.
|
|
if len(protocols) == 0 {
|
|
return nil
|
|
}
|
|
|
|
for _, i := range protocols {
|
|
items := strings.Split(i, ":")
|
|
// Here we hardcode check the http-get protocol. We would need to change that
|
|
// if we were to support rtp/rtsp/udp.
|
|
if len(items) == 4 && items[0] == "http-get" && strings.Contains(items[2], "/") {
|
|
ftype := strings.Split(items[2], "/")[0]
|
|
if ftype == mt {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return ErrNoMatchingFileType
|
|
}
|