Files
cameradar/internal/ui/summary.go
T
2026-03-13 01:59:23 -07:00

153 lines
3.8 KiB
Go

package ui
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/Ullaakut/cameradar/v6"
)
// FormatSummary builds a human-readable summary of discovered streams.
func FormatSummary(streams []cameradar.Stream, _ error) string {
accessible, others := partitionStreams(streams)
var builder strings.Builder
builder.WriteString(fmt.Sprintf("Accessible streams: %d\n", len(accessible)))
if len(accessible) == 0 {
builder.WriteString("• None\n")
} else {
for _, stream := range accessible {
builder.WriteString(formatStream(stream))
}
}
if len(others) > 0 {
builder.WriteString("\n")
builder.WriteString(fmt.Sprintf("Other discovered streams: %d\n", len(others)))
for _, stream := range others {
builder.WriteString(formatStream(stream))
}
}
return builder.String()
}
func partitionStreams(streams []cameradar.Stream) ([]cameradar.Stream, []cameradar.Stream) {
var accessible []cameradar.Stream
var others []cameradar.Stream
for _, stream := range streams {
if stream.Available {
accessible = append(accessible, stream)
} else {
others = append(others, stream)
}
}
// Sort streams by address and port.
sort.Slice(accessible, func(i, j int) bool {
if accessible[i].Address.String() == accessible[j].Address.String() {
return accessible[i].Port < accessible[j].Port
}
return accessible[i].Address.String() < accessible[j].Address.String()
})
sort.Slice(others, func(i, j int) bool {
if others[i].Address.String() == others[j].Address.String() {
return others[i].Port < others[j].Port
}
return others[i].Address.String() < others[j].Address.String()
})
return accessible, others
}
func formatStream(stream cameradar.Stream) string {
var builder strings.Builder
builder.WriteString("• ")
builder.WriteString(stream.Address.String())
builder.WriteString(":")
builder.WriteString(strconv.FormatUint(uint64(stream.Port), 10))
if stream.Device != "" {
builder.WriteString(" (")
builder.WriteString(stream.Device)
builder.WriteString(")")
}
builder.WriteString("\n")
builder.WriteString(" Authentication: ")
builder.WriteString(authTypeLabel(stream.AuthenticationType))
builder.WriteString("\n")
if len(stream.Routes) > 0 {
builder.WriteString(" Routes: ")
builder.WriteString(strings.Join(stream.Routes, ", "))
builder.WriteString("\n")
} else {
builder.WriteString(" Routes: not found\n")
}
if stream.CredentialsFound {
builder.WriteString(" Credentials: ")
builder.WriteString(stream.Username)
builder.WriteString(":")
builder.WriteString(stream.Password)
builder.WriteString("\n")
} else {
builder.WriteString(" Credentials: not found\n")
}
builder.WriteString(" Availability: ")
if stream.Available {
builder.WriteString("yes\n")
} else {
builder.WriteString("no\n")
}
if stream.RouteFound && stream.CredentialsFound {
builder.WriteString(" RTSP URL: ")
builder.WriteString(formatRTSPURL(stream))
builder.WriteString("\n")
}
builder.WriteString(" Admin panel: ")
builder.WriteString(formatAdminPanelURL(stream))
builder.WriteString("\n")
return builder.String()
}
func formatRTSPURL(stream cameradar.Stream) string {
path := "/" + strings.TrimLeft(strings.TrimSpace(stream.Route()), "/")
credentials := ""
if stream.Username != "" || stream.Password != "" {
credentials = stream.Username + ":" + stream.Password + "@"
}
scheme := "rtsp"
if stream.Secure {
scheme = "rtsps"
}
return fmt.Sprintf("%s://%s%s:%d%s", scheme, credentials, stream.Address.String(), stream.Port, path)
}
func formatAdminPanelURL(stream cameradar.Stream) string {
return fmt.Sprintf("http://%s/", stream.Address.String())
}
func authTypeLabel(auth cameradar.AuthType) string {
switch auth {
case cameradar.AuthNone:
return "none"
case cameradar.AuthBasic:
return "basic"
case cameradar.AuthDigest:
return "digest"
default:
return fmt.Sprintf("unknown(%d)", auth)
}
}