Skip to content

Commit faf10eb

Browse files
committed
fixed scanner, added localip command and a few other ip utils
Further fixes (adding in --broad-search)
1 parent 137bf80 commit faf10eb

30 files changed

Lines changed: 854 additions & 90 deletions

application/application.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ type App interface {
8787
AddMessageFunc(f CastMessageFunc)
8888
PlayedItems() map[string]PlayedItem
8989
PlayableMediaType(filename string) bool
90+
GetLocalIP() (string, error)
9091
}
9192

9293
type Application struct {
@@ -241,6 +242,10 @@ func (a *Application) App() *cast.Application { return a.application }
241242
func (a *Application) Media() *cast.Media { return a.media }
242243
func (a *Application) Volume() *cast.Volume { return a.volumeReceiver }
243244

245+
func (a *Application) GetLocalIP() (string, error) {
246+
return a.getLocalIP()
247+
}
248+
244249
func (a *Application) AddMessageFunc(f CastMessageFunc) {
245250
a.messageMu.Lock()
246251
defer a.messageMu.Unlock()

cmd/load-app.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ var loadAppCmd = &cobra.Command{
2828
the chromecast receiver app to be specified. An older list can be found
2929
here https://gist.github.com/jloutsenhizer/8855258.
3030
`,
31-
Run: func(cmd *cobra.Command, args []string) {
32-
if len(args) != 2 {
33-
exit("requires exactly two arguments")
34-
}
35-
app, err := castApplication(cmd, args)
36-
if err != nil {
37-
exit("unable to get cast application: %v", err)
38-
}
31+
Run: func(cmd *cobra.Command, args []string) {
32+
if len(args) != 2 {
33+
exit("requires exactly two arguments")
34+
}
35+
app, err := castApplication(cmd, args)
36+
if err != nil {
37+
exit("unable to get cast application: %v", err)
38+
}
3939

4040
// Optionally run a UI when playing this media:
4141
runWithUI, _ := cmd.Flags().GetBool("with-ui")
@@ -64,4 +64,5 @@ here https://gist.github.com/jloutsenhizer/8855258.
6464

6565
func init() {
6666
rootCmd.AddCommand(loadAppCmd)
67+
loadAppCmd.Flags().BoolP("broad-search", "b", false, "Search for devices using comprehensive network scanning (slower but finds more devices)")
6768
}

cmd/load.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,5 @@ func init() {
7777
loadCmd.Flags().Bool("detach", false, "detach from waiting until media finished. Only works with url loaded external media")
7878
loadCmd.Flags().StringP("content-type", "c", "", "content-type to serve the media file as")
7979
loadCmd.Flags().Int("start-time", 0, "start time to play media, in seconds")
80+
loadCmd.Flags().BoolP("broad-search", "b", false, "Search for devices using comprehensive network scanning (slower but finds more devices)")
8081
}

cmd/localip.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"net"
6+
7+
"github.com/spf13/cobra"
8+
)
9+
10+
var localIPCmd = &cobra.Command{
11+
Use: "localip",
12+
Short: "Print the local IP address used by go-chromecast",
13+
Run: func(cmd *cobra.Command, args []string) {
14+
ifaceName, _ := cmd.Flags().GetString("iface")
15+
ip, err := detectLocalIP(ifaceName)
16+
if err != nil {
17+
exit("unable to determine local IP: %v", err)
18+
}
19+
fmt.Println(ip)
20+
},
21+
}
22+
23+
// detectLocalIP attempts to detect the local IP address based on the network interface
24+
func detectLocalIP(ifaceName string) (string, error) {
25+
var iface *net.Interface
26+
var err error
27+
28+
if ifaceName != "" {
29+
iface, err = net.InterfaceByName(ifaceName)
30+
if err != nil {
31+
return "", err
32+
}
33+
}
34+
35+
if iface != nil {
36+
// Use the specified interface
37+
addrs, err := iface.Addrs()
38+
if err != nil {
39+
return "", err
40+
}
41+
for _, addr := range addrs {
42+
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
43+
if ipnet.IP.To4() != nil {
44+
return ipnet.IP.String(), nil
45+
}
46+
}
47+
}
48+
} else {
49+
// Try to find the default route interface
50+
interfaces, err := net.Interfaces()
51+
if err != nil {
52+
return "", err
53+
}
54+
55+
for _, iface := range interfaces {
56+
if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {
57+
continue
58+
}
59+
60+
addrs, err := iface.Addrs()
61+
if err != nil {
62+
continue
63+
}
64+
65+
for _, addr := range addrs {
66+
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
67+
if ipnet.IP.To4() != nil {
68+
return ipnet.IP.String(), nil
69+
}
70+
}
71+
}
72+
}
73+
}
74+
75+
return "", fmt.Errorf("could not detect local IP address")
76+
}
77+
78+
func init() {
79+
localIPCmd.Flags().String("iface", "", "network interface to use for detecting local IP (optional)")
80+
rootCmd.AddCommand(localIPCmd)
81+
}

cmd/ls.go

Lines changed: 165 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,15 @@ package cmd
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"net"
21+
"sort"
22+
"sync"
2023
"time"
2124

25+
"github.com/seancfoley/ipaddress-go/ipaddr"
2226
"github.com/spf13/cobra"
27+
"github.com/vishen/go-chromecast/application"
2328
castdns "github.com/vishen/go-chromecast/dns"
2429
)
2530

@@ -30,30 +35,179 @@ var lsCmd = &cobra.Command{
3035
Run: func(cmd *cobra.Command, args []string) {
3136
ifaceName, _ := cmd.Flags().GetString("iface")
3237
dnsTimeoutSeconds, _ := cmd.Flags().GetInt("dns-timeout")
38+
broadSearch, _ := cmd.Flags().GetBool("broad-search")
39+
3340
var iface *net.Interface
3441
var err error
3542
if ifaceName != "" {
3643
if iface, err = net.InterfaceByName(ifaceName); err != nil {
3744
exit("unable to find interface %q: %v", ifaceName, err)
3845
}
46+
} else {
47+
// If no interface was specified, try to auto-detect the best interface
48+
if iface, err = detectBestInterface(); err != nil {
49+
// If auto-detection fails, continue without interface (original behavior)
50+
iface = nil
51+
}
3952
}
40-
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(dnsTimeoutSeconds))
41-
defer cancel()
42-
castEntryChan, err := castdns.DiscoverCastDNSEntries(ctx, iface)
43-
if err != nil {
44-
exit("unable to discover chromecast devices: %v", err)
53+
54+
if broadSearch {
55+
// Use hybrid approach: mDNS + port scanning
56+
foundDevices := performBroadSearch(iface, dnsTimeoutSeconds)
57+
if len(foundDevices) == 0 {
58+
outputError("no cast devices found on network")
59+
} else {
60+
for i, device := range foundDevices {
61+
outputInfo("%d) device=%q device_name=%q address=\"%s:%d\" uuid=%q",
62+
i+1, device.Device, device.DeviceName, device.AddrV4, device.Port, device.UUID)
63+
}
64+
}
65+
} else {
66+
// Use original mDNS-only approach
67+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(dnsTimeoutSeconds))
68+
defer cancel()
69+
castEntryChan, err := castdns.DiscoverCastDNSEntries(ctx, iface)
70+
if err != nil {
71+
exit("unable to discover chromecast devices: %v", err)
72+
}
73+
i := 1
74+
for d := range castEntryChan {
75+
outputInfo("%d) device=%q device_name=%q address=\"%s:%d\" uuid=%q", i, d.Device, d.DeviceName, d.AddrV4, d.Port, d.UUID)
76+
i++
77+
}
78+
if i == 1 {
79+
outputError("no cast devices found on network")
80+
}
4581
}
46-
i := 1
82+
},
83+
}
84+
85+
// CastDevice represents a discovered Chromecast device
86+
type CastDevice struct {
87+
Device string
88+
DeviceName string
89+
AddrV4 string
90+
Port int
91+
UUID string
92+
}
93+
94+
// performBroadSearch does a comprehensive search using both mDNS and port scanning
95+
func performBroadSearch(iface *net.Interface, dnsTimeoutSeconds int) []CastDevice {
96+
var allDevices []CastDevice
97+
deviceMap := make(map[string]CastDevice) // Use UUID as key to deduplicate
98+
99+
// First, try mDNS discovery
100+
outputInfo("Performing mDNS discovery...")
101+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(dnsTimeoutSeconds*3)) // Use 3x timeout for broad search
102+
castEntryChan, err := castdns.DiscoverCastDNSEntries(ctx, iface)
103+
if err == nil {
47104
for d := range castEntryChan {
48-
outputInfo("%d) device=%q device_name=%q address=\"%s:%d\" uuid=%q", i, d.Device, d.DeviceName, d.AddrV4, d.Port, d.UUID)
49-
i++
105+
device := CastDevice{
106+
Device: d.Device,
107+
DeviceName: d.DeviceName,
108+
AddrV4: d.AddrV4.String(),
109+
Port: d.Port,
110+
UUID: d.UUID,
111+
}
112+
if device.UUID != "" {
113+
deviceMap[device.UUID] = device
114+
} else {
115+
// If no UUID, use address:port as key
116+
key := fmt.Sprintf("%s:%d", device.AddrV4, device.Port)
117+
deviceMap[key] = device
118+
}
50119
}
51-
if i == 1 {
52-
outputError("no cast devices found on network")
120+
}
121+
cancel()
122+
123+
outputInfo("Found %d devices via mDNS, performing port scan to find additional devices...", len(deviceMap))
124+
125+
// Then, do a targeted port scan on the local subnet
126+
if localSubnet, err := detectLocalSubnet(""); err == nil {
127+
ipRange, err := ipaddr.NewIPAddressString(localSubnet).ToSequentialRange()
128+
if err == nil {
129+
// Use a smaller set of ports for ls to keep it reasonably fast
130+
ports := []int{8009, 8008, 8443, 32236} // Common ports + known group port
131+
132+
var wg sync.WaitGroup
133+
ipCh := make(chan *ipaddr.IPAddress, 100)
134+
135+
// Send IPs to scan
136+
go func() {
137+
it := ipRange.Iterator()
138+
for it.HasNext() {
139+
ip := it.Next()
140+
ipCh <- ip
141+
}
142+
close(ipCh)
143+
}()
144+
145+
// Scan IPs in parallel
146+
for i := 0; i < 20; i++ { // Use fewer goroutines than scan command
147+
wg.Add(1)
148+
go func() {
149+
defer wg.Done()
150+
dialer := &net.Dialer{
151+
Timeout: 300 * time.Millisecond,
152+
}
153+
for ip := range ipCh {
154+
for _, port := range ports {
155+
conn, err := dialer.Dial("tcp", fmt.Sprintf("%v:%d", ip, port))
156+
if err != nil {
157+
continue
158+
}
159+
conn.Close()
160+
161+
// Try to get device info
162+
if info, err := application.GetInfo(ip.String()); err == nil {
163+
device := CastDevice{
164+
Device: "Unknown Device",
165+
DeviceName: info.Name,
166+
AddrV4: ip.String(),
167+
Port: port,
168+
UUID: "", // Port scan doesn't give us UUID
169+
}
170+
171+
// Use address:port as key since we don't have UUID from port scan
172+
key := fmt.Sprintf("%s:%d", device.AddrV4, device.Port)
173+
174+
// Only add if we haven't seen this device yet
175+
if _, exists := deviceMap[key]; !exists {
176+
// Also check if we have this device by name on a different port
177+
found := false
178+
for _, existing := range deviceMap {
179+
if existing.DeviceName == device.DeviceName && existing.AddrV4 == device.AddrV4 {
180+
found = true
181+
break
182+
}
183+
}
184+
if !found {
185+
deviceMap[key] = device
186+
}
187+
}
188+
}
189+
}
190+
}
191+
}()
192+
}
193+
wg.Wait()
53194
}
54-
},
195+
}
196+
197+
// Convert map to slice and sort
198+
for _, device := range deviceMap {
199+
allDevices = append(allDevices, device)
200+
}
201+
202+
// Sort by device name for consistent output
203+
sort.Slice(allDevices, func(i, j int) bool {
204+
return allDevices[i].DeviceName < allDevices[j].DeviceName
205+
})
206+
207+
return allDevices
55208
}
56209

57210
func init() {
211+
lsCmd.Flags().Bool("broad-search", false, "perform comprehensive search using both mDNS and port scanning")
58212
rootCmd.AddCommand(lsCmd)
59213
}

cmd/mute.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ var muteCmd = &cobra.Command{
2323
Use: "mute",
2424
Short: "Mute the chromecast",
2525
Run: func(cmd *cobra.Command, args []string) {
26-
app, err := castApplication(cmd, args)
26+
app, err := castApplication(cmd, args)
2727
if err != nil {
2828
exit("unable to get cast application: %v", err)
2929
}
@@ -35,4 +35,5 @@ var muteCmd = &cobra.Command{
3535

3636
func init() {
3737
rootCmd.AddCommand(muteCmd)
38+
muteCmd.Flags().BoolP("broad-search", "b", false, "Search for devices using comprehensive network scanning (slower but finds more devices)")
3839
}

cmd/next.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ var nextCmd = &cobra.Command{
2323
Use: "next",
2424
Short: "Play the next available media",
2525
Run: func(cmd *cobra.Command, args []string) {
26-
app, err := castApplication(cmd, args)
26+
app, err := castApplication(cmd, args)
2727
if err != nil {
2828
exit("unable to get cast application: %v", err)
2929
}
@@ -35,4 +35,5 @@ var nextCmd = &cobra.Command{
3535

3636
func init() {
3737
rootCmd.AddCommand(nextCmd)
38+
nextCmd.Flags().BoolP("broad-search", "b", false, "Search for devices using comprehensive network scanning (slower but finds more devices)")
3839
}

cmd/pause.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ var pauseCmd = &cobra.Command{
2323
Use: "pause",
2424
Short: "Pause the currently playing media on the chromecast",
2525
Run: func(cmd *cobra.Command, args []string) {
26-
app, err := castApplication(cmd, args)
26+
app, err := castApplication(cmd, args)
2727
if err != nil {
2828
exit("unable to get cast application: %v", err)
2929
}
@@ -35,4 +35,5 @@ var pauseCmd = &cobra.Command{
3535

3636
func init() {
3737
rootCmd.AddCommand(pauseCmd)
38+
pauseCmd.Flags().BoolP("broad-search", "b", false, "Search for devices using comprehensive network scanning (slower but finds more devices)")
3839
}

cmd/playlist.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,4 +226,5 @@ func init() {
226226
playlistCmd.Flags().Bool("transcode", true, "transcode the media to mp4 if media type is unrecognised")
227227
playlistCmd.Flags().Bool("force-play", false, "attempt to play a media type even if it is unrecognised")
228228
playlistCmd.Flags().StringP("content-type", "c", "", "content-type to serve the media file as")
229+
playlistCmd.Flags().BoolP("broad-search", "b", false, "Search for devices using comprehensive network scanning (slower but finds more devices)")
229230
}

0 commit comments

Comments
 (0)