Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EAAS #30

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
Draft

EAAS #30

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ jobs:
strategy:
matrix:
goversion:
- '1.17'
- '1.18'
- '1.19'
- '1.20'
- '1.21'
- '1.22'
Expand Down
26 changes: 15 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,32 @@ The code of this project is an **experimental** reverse-engineering effort and t

If you have any other Denon devices you would like to test this library against, please do! Even better, you can let me know if you run into any bugs by reporting them [as an issue ticket](https://github.com/icedream/go-stagelinq/issues).

## Building

Please make sure you have a recent version of Go with module support enabled.
## Demo programs

### stagelinq-discover
This repository gives you example programs to play around with to test this
library's functionality:

You may install the `stagelinq-discover` example binary by one of two means:
- `stagelinq-discover`: Simple code to discover devices and dump their states.
- `beatinfo`: Like `stagelinq-discover` except it will dump the beat info stream instead.
- `storage`: A demo for serving a remote library via the EAAS protocol.

- `git clone` this repository and run `go build -v ./cmd/stagelinq-discover` to build the binary.
- Run `go install github.com/icedream/go-stagelinq/cmd/stagelinq-discover` to install the binary to your `$GOPATH`.
## Building

### beatinfo
Please make sure you have Go 1.19 or newer.

You may install the `beatinfo` example binary by one of two means:
You may install the binaries in this repository one of two means:

- `git clone` this repository and run `go build -v ./cmd/beatinfo` to build the binary.
- Run `go install github.com/icedream/go-stagelinq/cmd/beatinfo` to install the binary to your `$GOPATH`.
- `git clone` this repository and run `go build -v ./cmd/<binary>` to build the binary.
- Run `go install github.com/icedream/go-stagelinq/cmd/<binary>` to install the binary to your `$GOPATH`.

## Usage

To use this library, import `"github.com/icedream/go-stagelinq"` in your Go project. This will give you access to the `stagelinq` library namespace.

EAAS functionality is served in a subpackage via `"github.com/icedream/go-stagelinq/eaas"`.

Make sure to run `go mod tidy` for Go to pick up the library properly and update `go.mod` and `go.sum` in your project.

[Go code documentation is available](https://pkg.go.dev/github.com/icedream/go-stagelinq).

## Testing
Expand Down
13 changes: 8 additions & 5 deletions beat_info_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ package stagelinq

import (
"net"

"github.com/icedream/go-stagelinq/internal/messages"
"github.com/icedream/go-stagelinq/internal/socket"
)

// BeatInfo represents a received BeatInfo message.
Expand All @@ -18,7 +21,7 @@ type BeatInfoConnection struct {
beatInfoC chan *BeatInfo
}

var beatInfoConnectionMessageSet = newDeviceConnMessageSet([]message{&beatEmitMessage{}})
var beatInfoConnectionMessageSet = newDeviceConnMessageSet([]messages.Message{&beatEmitMessage{}})

func NewBeatInfoConnection(conn net.Conn, token Token) (bic *BeatInfoConnection, err error) {
msgConn := newMessageConnection(conn, beatInfoConnectionMessageSet)
Expand All @@ -34,11 +37,11 @@ func NewBeatInfoConnection(conn net.Conn, token Token) (bic *BeatInfoConnection,

// perform in-protocol service request
msgConn.WriteMessage(&serviceAnnouncementMessage{
tokenPrefixedMessage: tokenPrefixedMessage{
Token: token,
TokenPrefixedMessage: messages.TokenPrefixedMessage{
Token: messages.Token(token),
},
Service: "BeatInfo",
Port: uint16(getPort(conn.LocalAddr())),
Port: uint16(socket.GetPortFromAddress(conn.LocalAddr())),
})

go func() {
Expand All @@ -51,7 +54,7 @@ func NewBeatInfoConnection(conn net.Conn, token Token) (bic *BeatInfoConnection,
close(beatInfoConn.beatInfoC)
}()
for {
var msg message
var msg messages.Message
msg, err = msgConn.ReadMessage()
if err != nil {
return
Expand Down
38 changes: 38 additions & 0 deletions cmd/storage-discover/key_uuid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"io"
"os"

"github.com/google/uuid"
)

var zeroUUID = uuid.UUID{}

func loadUUIDKey() (string, error) {
var id uuid.UUID
if f, err := os.Open("eaas-id.txt"); err == nil {
defer f.Close()
keyBytes, err := io.ReadAll(f)
if err != nil {
return "", err
}
id, err = uuid.ParseBytes(keyBytes)
if err != nil {
return "", err
}
}
if id == zeroUUID {
var err error
id, err = uuid.NewUUID()
if err != nil {
return "", err
}
keyBytes, err := id.MarshalBinary()
if err != nil {
return "", err
}
os.WriteFile("eaas-id.txt", keyBytes, 0o600)
}
return id.String(), nil
}
180 changes: 180 additions & 0 deletions cmd/storage-discover/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package main

import (
"context"
"encoding/json"
"errors"
"flag"
"io"
"log"
"os"
"time"

"github.com/icedream/go-stagelinq/eaas"
"github.com/icedream/go-stagelinq/eaas/proto/enginelibrary"
"github.com/icedream/go-stagelinq/eaas/proto/networktrust"
"github.com/rivo/tview"
)

const (
appName = "Icedream StagelinQ Receiver"
appVersion = "0.0.0"
timeout = 15 * time.Second
)

var (
grpcURL string
hostname string
identity string
)

func init() {
flag.StringVar(&grpcURL, "server", "", "GRPC URL of the remote Engine Library to connect to. If empty, will discover devices instead.")
flag.Parse()

var err error
hostname, err = os.Hostname()
if err != nil {
hostname = "eaas-demo"
}
}

type App struct {
*tview.Application
}

func main() {
if len(grpcURL) == 0 {
runDiscovery()
return
}

runEngineLibraryUI(grpcURL)
}

func marshalJSON(v any) []byte {
s, _ := json.Marshal(v)
return s
}

func runEngineLibraryUI(grpcURL string) {
// load our identity so we don't have to repeatedly re-verify
identity, err := loadUUIDKey()
if err != nil {
log.Fatal(err)
}

ctx := context.Background()
connection, err := eaas.DialContext(ctx, grpcURL)
if err != nil {
log.Fatal(err)
}

log.Println("Waiting for approval on the other end...")
resp, err := connection.CreateTrust(ctx, &networktrust.CreateTrustRequest{
DeviceName: &hostname,
// I honestly don't know why in the proto it was defined as "Ed25519Pk"...
Ed25519Pk: &identity,
// ...or why there even is a WireguardPort field, too?!
})
if err != nil {
log.Fatal(err)
}
switch {
case resp.GetGranted() != nil:
log.Println("Access granted")
case resp.GetBusy() != nil:
log.Fatal("Busy")
case resp.GetDenied() != nil:
log.Fatal("Access denied")
default:
panic("unexpected response")
}

getLibraryResp, err := connection.GetLibrary(ctx, &enginelibrary.GetLibraryRequest{})
if err != nil {
panic(err)
}
var pageSize uint32 = 25
getTracksResp, err := connection.GetTracks(ctx, &enginelibrary.GetTracksRequest{
PageSize: &pageSize,
})
if err != nil {
panic(err)
}
for _, track := range getTracksResp.GetTracks() {
log.Printf("Track: %s", string(marshalJSON(track)))
getTrackResp, err := connection.GetTrack(ctx, &enginelibrary.GetTrackRequest{
TrackId: track.GetMetadata().Id,
})
if err != nil {
log.Println("\tfailed to GetTrack on this track")
continue
}
log.Printf("\t%+v", getTrackResp)
}
for _, playlist := range getLibraryResp.GetPlaylists() {
log.Printf("Playlist: %s", string(marshalJSON(playlist)))
getTracksResp, err := connection.GetTracks(ctx, &enginelibrary.GetTracksRequest{
PlaylistId: playlist.Id,
PageSize: &pageSize,
})
if errors.Is(err, io.EOF) {
// BUG - empty playlist causes EOF, reconnect
connection, err = eaas.DialContext(ctx, grpcURL)
if err != nil {
panic(err)
}
}
if err != nil {
panic(err)
}
for _, track := range getTracksResp.GetTracks() {
log.Printf("\tTrack: ID %s", track.GetMetadata().GetId())
}
}
}

func runDiscovery() {
discoverer, err := eaas.NewDiscovererWithConfiguration(&eaas.DiscovererConfiguration{
DiscoveryTimeout: timeout,
})
if err != nil {
panic(err)
}
defer discoverer.Shutdown()

discoverer.ScanEvery(5 * time.Second)

deadline := time.After(timeout)
foundDevices := []*eaas.Device{}

log.Printf("Listening for devices for %s", timeout)

discoveryLoop:
for {
select {
case <-deadline:
break discoveryLoop
default:
device, err := discoverer.Discover(timeout)
if err != nil {
log.Printf("WARNING: %s", err.Error())
continue discoveryLoop
}
if device == nil {
continue
}
// check if we already found this device before
for _, foundDevice := range foundDevices {
if foundDevice.IsEqual(device) {
continue discoveryLoop
}
}
foundDevices = append(foundDevices, device)
log.Printf("%s %q %q", device.Hostname, device.URL, device.SoftwareVersion)
}
}

log.Printf("Found devices: %d", len(foundDevices))
}
3 changes: 3 additions & 0 deletions cmd/storage/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*.m4a filter=lfs diff=lfs merge=lfs -text
*.beatgrid filter=lfs diff=lfs merge=lfs -text
*.waveform filter=lfs diff=lfs merge=lfs -text
3 changes: 3 additions & 0 deletions cmd/storage/Icedream - Whiplash (Radio Edit).m4a
Git LFS file not shown
3 changes: 3 additions & 0 deletions cmd/storage/Icedream - Whiplash (Radio Edit).m4a.beatgrid
Git LFS file not shown
3 changes: 3 additions & 0 deletions cmd/storage/Icedream - Whiplash (Radio Edit).m4a.waveform
Git LFS file not shown
Loading