Files
go2tv/soapcalls/soapcallers.go
2024-12-28 00:28:34 +02:00

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
}