Skip to content

Commit aa12db5

Browse files
authored
Merge pull request #19 from chrusty/terminal_user_interface
Terminal user interface
2 parents 94d6249 + f244ce6 commit aa12db5

22 files changed

+793
-29
lines changed

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,45 @@ If you would like to see what a device is sending, you are able to `watch` the p
128128
$ go-chromecast watch
129129
```
130130

131+
## User Interface
132+
133+
![User-interface example](go-chromecast-ui.png "User-interface example")
134+
135+
A basic terminal user-interface is provided, that supports the following controls:
136+
* Quit: "q"
137+
* Play/Pause: SPACE
138+
* Volume: - / +
139+
* Mute/Unmute: "m"
140+
* Seek (15s): <- / ->
141+
* Previous/Next: PgUp / PgDn
142+
* Stop: "s"
143+
144+
It can be run in the following ways:
145+
146+
### Standalone
147+
148+
If you just want to remote-control a chromecast that is already playing something:
149+
150+
```
151+
$ go-chromecast ui
152+
```
153+
154+
### Playlist
155+
156+
Use the UI in combination with the `playlist` command (detailed above):
157+
158+
```
159+
$ go-chromecast --with-ui playlist /path/to/directory
160+
```
161+
162+
### Load
163+
164+
Use the UI in combination with the `load` command (detailed above):
165+
166+
```
167+
$ go-chromecast --with-ui load /path/to/file.flac
168+
```
169+
131170
## Installing
132171

133172
### Install binaries
@@ -161,6 +200,7 @@ Available Commands:
161200
seek Seek by seconds into the currently playing media
162201
status Current chromecast status
163202
stop Stop casting
203+
ui Run the UI
164204
unpause Unpause the currently playing media on the chromecast
165205
watch Watch all events sent from a chromecaset device
166206
@@ -171,6 +211,7 @@ Flags:
171211
--disable-cache disable the cache
172212
-h, --help help for go-chromecast
173213
-u, --uuid string chromecast device uuid
214+
--with-ui run with a UI
174215
175216
Use "go-chromecast [command] --help" for more information about a command.
176217
```

application/application.go

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ func (a *Application) Status() (*cast.Application, *cast.Media, *cast.Volume) {
249249

250250
func (a *Application) Pause() error {
251251
if a.media == nil {
252-
return errors.New("media not yet initialised, there is nothing to pause")
252+
return ErrNoMediaPause
253253
}
254254
return a.sendMediaRecv(&cast.MediaHeader{
255255
PayloadHeader: cast.PauseHeader,
@@ -259,7 +259,7 @@ func (a *Application) Pause() error {
259259

260260
func (a *Application) Unpause() error {
261261
if a.media == nil {
262-
return errors.New("media not yet initialised, there is nothing to unpause")
262+
return ErrNoMediaUnpause
263263
}
264264
return a.sendMediaRecv(&cast.MediaHeader{
265265
PayloadHeader: cast.PlayHeader,
@@ -269,7 +269,7 @@ func (a *Application) Unpause() error {
269269

270270
func (a *Application) StopMedia() error {
271271
if a.media == nil {
272-
return errors.New("media not yet initialised, there is nothing to stop")
272+
return ErrNoMediaStop
273273
}
274274
return a.sendMediaRecv(&cast.MediaHeader{
275275
PayloadHeader: cast.StopHeader,
@@ -283,7 +283,7 @@ func (a *Application) Stop() error {
283283

284284
func (a *Application) Next() error {
285285
if a.media == nil {
286-
return errors.New("media not yet initialised, there is nothing to go to next")
286+
return ErrNoMediaNext
287287
}
288288

289289
// TODO(vishen): Get the number of queue items, if none, possibly just skip to the end?
@@ -296,7 +296,7 @@ func (a *Application) Next() error {
296296

297297
func (a *Application) Previous() error {
298298
if a.media == nil {
299-
return errors.New("media not yet initialised, there is nothing previous")
299+
return ErrNoMediaPrevious
300300
}
301301

302302
// TODO(vishen): Get the number of queue items, if none, possibly just jump to beginning?
@@ -310,7 +310,7 @@ func (a *Application) Previous() error {
310310
func (a *Application) Skip() error {
311311

312312
if a.media == nil {
313-
return errors.New("media not yet initialised, there is nothing to skip")
313+
return ErrNoMediaSkip
314314
}
315315

316316
// Get the latest media status
@@ -329,7 +329,7 @@ func (a *Application) Skip() error {
329329

330330
func (a *Application) Seek(value int) error {
331331
if a.media == nil {
332-
return errors.New("media not yet initialised")
332+
return ErrMediaNotYetInitialised
333333
}
334334

335335
return a.sendMediaRecv(&cast.MediaHeader{
@@ -342,7 +342,7 @@ func (a *Application) Seek(value int) error {
342342

343343
func (a *Application) SeekFromStart(value int) error {
344344
if a.media == nil {
345-
return errors.New("media not yet initialised")
345+
return ErrMediaNotYetInitialised
346346
}
347347

348348
// Get the latest media status
@@ -362,6 +362,28 @@ func (a *Application) SeekFromStart(value int) error {
362362
})
363363
}
364364

365+
func (a *Application) SetVolume(value float32) error {
366+
if value > 1 || value < 0 {
367+
return ErrVolumeOutOfRange
368+
}
369+
370+
return a.sendDefaultRecv(&cast.SetVolume{
371+
PayloadHeader: cast.VolumeHeader,
372+
Volume: cast.Volume{
373+
Level: value,
374+
},
375+
})
376+
}
377+
378+
func (a *Application) SetMuted(value bool) error {
379+
return a.sendDefaultRecv(&cast.SetVolume{
380+
PayloadHeader: cast.VolumeHeader,
381+
Volume: cast.Volume{
382+
Muted: value,
383+
},
384+
})
385+
}
386+
365387
func (a *Application) getMediaStatus() (*cast.MediaStatusResponse, error) {
366388
apiMessage, err := a.sendAndWaitMediaRecv(&cast.GetStatusHeader)
367389
if err != nil {
@@ -695,7 +717,7 @@ func (a *Application) startStreamingServer() error {
695717
go func() {
696718
a.log("media server listening on %d", a.serverPort)
697719
if err := a.httpServer.Serve(listener); err != nil && err != http.ErrServerClosed {
698-
log.Fatal(err)
720+
log.WithField("package", "application").WithError(err).Fatal("error serving HTTP")
699721
}
700722
}()
701723

@@ -719,14 +741,14 @@ func (a *Application) serveLiveStreaming(w http.ResponseWriter, r *http.Request,
719741
w.Header().Set("Transfer-Encoding", "chunked")
720742

721743
if err := cmd.Run(); err != nil {
722-
log.Printf("error transcoding %q: %v", filename, err)
744+
log.WithField("package", "application").WithField("filename", filename).WithError(err).Error("error transcoding")
723745
}
724746

725747
}
726748

727749
func (a *Application) log(message string, args ...interface{}) {
728750
if a.debug {
729-
log.Infof("[application] %s", fmt.Sprintf(message, args...))
751+
log.WithField("package", "application").Infof(message, args...)
730752
}
731753
}
732754

@@ -778,15 +800,15 @@ func (a *Application) sendDefaultRecv(payload cast.Payload) error {
778800

779801
func (a *Application) sendMediaConn(payload cast.Payload) error {
780802
if a.application == nil {
781-
return errors.New("application isn't set")
803+
return ErrApplicationNotSet
782804
}
783805
_, err := a.send(payload, defaultSender, a.application.TransportId, namespaceConn)
784806
return err
785807
}
786808

787809
func (a *Application) sendMediaRecv(payload cast.Payload) error {
788810
if a.application == nil {
789-
return errors.New("application isn't set")
811+
return ErrApplicationNotSet
790812
}
791813
_, err := a.send(payload, defaultSender, a.application.TransportId, namespaceMedia)
792814
return err
@@ -802,14 +824,14 @@ func (a *Application) sendAndWaitDefaultRecv(payload cast.Payload) (*pb.CastMess
802824

803825
func (a *Application) sendAndWaitMediaConn(payload cast.Payload) (*pb.CastMessage, error) {
804826
if a.application == nil {
805-
return nil, errors.New("application isn't set")
827+
return nil, ErrApplicationNotSet
806828
}
807829
return a.sendAndWait(payload, defaultSender, a.application.TransportId, namespaceConn)
808830
}
809831

810832
func (a *Application) sendAndWaitMediaRecv(payload cast.Payload) (*pb.CastMessage, error) {
811833
if a.application == nil {
812-
return nil, errors.New("application isn't set")
834+
return nil, ErrApplicationNotSet
813835
}
814836
return a.sendAndWait(payload, defaultSender, a.application.TransportId, namespaceMedia)
815837
}

application/errors.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package application
2+
3+
import "github.com/pkg/errors"
4+
5+
var (
6+
ErrApplicationNotSet = errors.New("application isn't set")
7+
ErrMediaNotYetInitialised = errors.New("media not yet initialised")
8+
ErrNoMediaNext = errors.New("media not yet initialised, there is nothing to go to next")
9+
ErrNoMediaPause = errors.New("media not yet initialised, there is nothing to pause")
10+
ErrNoMediaPrevious = errors.New("media not yet initialised, there is nothing previous")
11+
ErrNoMediaSkip = errors.New("media not yet initialised, there is nothing to skip")
12+
ErrNoMediaStop = errors.New("media not yet initialised, there is nothing to stop")
13+
ErrNoMediaUnpause = errors.New("media not yet initialised, there is nothing to unpause")
14+
ErrVolumeOutOfRange = errors.New("specified volume is out of range (0 - 1)")
15+
)

cast/connection.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func (c *Connection) SetDebug(debug bool) { c.debug = debug }
5353

5454
func (c *Connection) log(message string, args ...interface{}) {
5555
if c.debug {
56-
log.Printf("[connection] %s", fmt.Sprintf(message, args...))
56+
log.WithField("package", "cast").Debugf(message, args...)
5757
}
5858
}
5959

cast/payload.go

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ type MediaHeader struct {
6060
}
6161

6262
type Volume struct {
63-
Level float32 `json:"level"`
63+
Level float32 `json:"level,omitempty"`
6464
Muted bool `json:"muted"`
6565
}
6666

@@ -107,16 +107,26 @@ type QueueData struct {
107107
}
108108

109109
type MediaItem struct {
110-
ContentId string `json:"contentId"`
111-
ContentType string `json:"contentType"`
112-
StreamType string `json:"streamType"`
113-
Duration float32 `json:"duration"`
114-
Metadata struct {
115-
MetadataType int `json:"metadataType`
116-
Title string `json:"title"`
117-
SongName string `json:"songName"`
118-
Artist string `json:"artist"`
119-
} `json:"metadata"`
110+
ContentId string `json:"contentId"`
111+
ContentType string `json:"contentType"`
112+
StreamType string `json:"streamType"`
113+
Duration float32 `json:"duration"`
114+
Metadata MediaMetadata `json:"metadata"`
115+
}
116+
117+
type MediaMetadata struct {
118+
MetadataType int `json:"metadataType`
119+
Artist string `json:"artist"`
120+
Title string `json:"title"`
121+
Subtitle string `json:"subtitle"`
122+
Images []Image `json:"images"`
123+
ReleaseDate string `json:"releaseDate"`
124+
}
125+
126+
type Image struct {
127+
URL string `json:"url"`
128+
Height int `json:"height"`
129+
Width int `json:"width"`
120130
}
121131

122132
type Media struct {
@@ -135,3 +145,8 @@ type MediaStatusResponse struct {
135145
PayloadHeader
136146
Status []Media `json:"status"`
137147
}
148+
149+
type SetVolume struct {
150+
PayloadHeader
151+
Volume Volume `json:"volume"`
152+
}

cmd/load.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ package cmd
1717
import (
1818
"fmt"
1919

20+
"github.com/vishen/go-chromecast/ui"
21+
22+
"github.com/sirupsen/logrus"
2023
"github.com/spf13/cobra"
2124
)
2225

@@ -40,8 +43,27 @@ that ffmpeg is installed.`,
4043
fmt.Printf("unable to get cast application: %v\n", err)
4144
return nil
4245
}
46+
4347
contentType, _ := cmd.Flags().GetString("content-type")
4448
transcode, _ := cmd.Flags().GetBool("transcode")
49+
50+
// Optionally run a UI when playing this media:
51+
runWithUI, _ := cmd.Flags().GetBool("with-ui")
52+
if runWithUI {
53+
go func() {
54+
if err := app.Load(args[0], contentType, transcode); err != nil {
55+
logrus.WithError(err).Fatal("unable to load media")
56+
}
57+
}()
58+
59+
ccui, err := ui.NewUserInterface(app)
60+
if err != nil {
61+
logrus.WithError(err).Fatal("unable to prepare a new user-interface")
62+
}
63+
return ccui.Run()
64+
}
65+
66+
// Otherwise just run in CLI mode:
4567
if err := app.Load(args[0], contentType, transcode); err != nil {
4668
fmt.Printf("unable to load media: %v\n", err)
4769
return nil

cmd/playlist.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import (
2525
"strings"
2626
"time"
2727

28+
"github.com/sirupsen/logrus"
2829
"github.com/spf13/cobra"
30+
"github.com/vishen/go-chromecast/ui"
2931
)
3032

3133
type mediaFile struct {
@@ -60,6 +62,7 @@ that ffmpeg is installed.`,
6062
fmt.Printf("unable to get cast application: %v\n", err)
6163
return nil
6264
}
65+
6366
contentType, _ := cmd.Flags().GetString("content-type")
6467
transcode, _ := cmd.Flags().GetBool("transcode")
6568
forcePlay, _ := cmd.Flags().GetBool("force-play")
@@ -197,6 +200,22 @@ that ffmpeg is installed.`,
197200
fmt.Printf("- %s\n", f)
198201
}
199202

203+
// Optionally run a UI when playing this media:
204+
runWithUI, _ := cmd.Flags().GetBool("with-ui")
205+
if runWithUI {
206+
go func() {
207+
if err := app.QueueLoad(filenames[indexToPlayFrom:], contentType, transcode); err != nil {
208+
logrus.WithError(err).Fatal("unable to play playlist on cast application")
209+
}
210+
}()
211+
212+
ccui, err := ui.NewUserInterface(app)
213+
if err != nil {
214+
logrus.WithError(err).Fatal("unable to prepare a new user-interface")
215+
}
216+
return ccui.Run()
217+
}
218+
200219
if err := app.QueueLoad(filenames[indexToPlayFrom:], contentType, transcode); err != nil {
201220
fmt.Printf("unable to play playlist on cast application: %v\n", err)
202221
return nil

cmd/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func Execute() {
4141
func init() {
4242
rootCmd.PersistentFlags().Bool("debug", false, "debug logging")
4343
rootCmd.PersistentFlags().Bool("disable-cache", false, "disable the cache")
44+
rootCmd.PersistentFlags().Bool("with-ui", false, "run with a UI")
4445
rootCmd.PersistentFlags().StringP("device", "d", "", "chromecast device, ie: 'Chromecast' or 'Google Home Mini'")
4546
rootCmd.PersistentFlags().StringP("device-name", "n", "", "chromecast device name")
4647
rootCmd.PersistentFlags().StringP("uuid", "u", "", "chromecast device uuid")

0 commit comments

Comments
 (0)