diff --git a/RELEASES-COMMITS-MAP.md b/RELEASES-COMMITS-MAP.md index 1a6e2dd..0d1af2a 100644 --- a/RELEASES-COMMITS-MAP.md +++ b/RELEASES-COMMITS-MAP.md @@ -37,3 +37,4 @@ Ship recipe: **`HOWTO-DEVEL-DEPLOY.md`**. - `df34f65` feat: commercial family key strip - `04b43a0` fix: music templates by tag overlap - uncommitted: `DeskApp.Sync()` matches `downloadAll` (disabled / master-off clips) +| 0.6.81 | `02aff5715a386a68333dc65aa44af05836e6adb3` | dirty | 2026-09-15T21:06:49-03:00 | `0a0266909c800a58620b3e0c8a47919a8315000a19accc1c1405f76fc6e0b113` | build | diff --git a/protocol.go b/protocol.go index 4734a90..173f45f 100644 --- a/protocol.go +++ b/protocol.go @@ -28,14 +28,17 @@ import ( ) const ( - defaultWSURL = "wss://wss.radio.mmapps.com.br:443" - develWSURL = "wss://wss-devel-radio-test-only.radio.mmapps.com.br:443" - develMarkName = "devel_device.mark" - maxDownloadSize = 80 << 20 - heartbeatInterval = 5 * time.Minute - pingInterval = 10 * time.Second - pollInterval = 30 * time.Second - playerTypeDesktopGo = "desktop-go" + defaultWSURL = "wss://wss.radio.mmapps.com.br:443" + develWSURL = "wss://wss-devel-radio-test-only.radio.mmapps.com.br:443" + develMarkName = "devel_device.mark" + maxDownloadSize = 80 << 20 + heartbeatInterval = 5 * time.Minute + pingInterval = 10 * time.Second + pollInterval = 30 * time.Second + // Android WssClient PERIODIC_REFRESH_INTERVAL_MS: registered players refetch + // config/playlist/files on their own, since the WSS does not push refresh. + catalogRefreshInterval = 10 * time.Minute + playerTypeDesktopGo = "desktop-go" ) func resolveWSURL(root string) string { @@ -193,6 +196,14 @@ type RadioSync struct { filesMu sync.Mutex lastFiles []ServerMediaFile debugLogsMu sync.Mutex + refreshEvery time.Duration +} + +func (s *RadioSync) refreshInterval() time.Duration { + if s != nil && s.refreshEvery > 0 { + return s.refreshEvery + } + return catalogRefreshInterval } func (s *RadioSync) SetStatusFn(fn func() map[string]any) { @@ -489,12 +500,18 @@ func (s *RadioSync) session(ctx context.Context) error { func (s *RadioSync) keepalive(ctx context.Context, conn *websocket.Conn) { pingTick := time.NewTicker(pingInterval) pollTick := time.NewTicker(pollInterval) + refreshTick := time.NewTicker(s.refreshInterval()) defer pingTick.Stop() defer pollTick.Stop() + defer refreshTick.Stop() for { select { case <-ctx.Done(): return + case <-refreshTick.C: + if !unregisteredNeedsConfigPoll(s) { + s.requestCatalogRefresh(conn) + } case <-pingTick.C: ts := time.Now().UnixMilli() if err := s.send(conn, map[string]any{ diff --git a/protocol_test.go b/protocol_test.go index 35d04f3..acb313a 100644 --- a/protocol_test.go +++ b/protocol_test.go @@ -3,11 +3,17 @@ package main import ( "bytes" "context" + "net/http" + "net/http/httptest" "os" "path/filepath" + "strings" + "sync" "testing" "time" + "github.com/gorilla/websocket" + "stream-player/core" ) @@ -752,3 +758,77 @@ func TestResolveWSURLIgnoresDirectoryNamedMark(t *testing.T) { t.Fatalf("got %q want prod when mark is a directory", got) } } + +func collectGetDataEndpoints(t *testing.T, s *RadioSync, wait time.Duration) map[string]int { + t.Helper() + got := map[string]int{} + var mu sync.Mutex + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + up := websocket.Upgrader{} + c, err := up.Upgrade(w, r, nil) + if err != nil { + return + } + defer c.Close() + for { + var msg map[string]any + if err := c.ReadJSON(&msg); err != nil { + return + } + if msg["type"] == "getData" { + mu.Lock() + got[msg["endpoint"].(string)]++ + mu.Unlock() + } + } + })) + defer srv.Close() + conn, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(srv.URL, "http"), nil) + if err != nil { + t.Fatal(err) + } + defer conn.Close() + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + s.keepalive(ctx, conn) + mu.Lock() + defer mu.Unlock() + out := map[string]int{} + for k, v := range got { + out[k] = v + } + return out +} + +func TestKeepaliveRequestsCatalogRefreshWhenRegistered(t *testing.T) { + dir := t.TempDir() + rdb, _, err := openRadioDB(dir, exeMasterKey()) + if err != nil { + t.Fatal(err) + } + defer rdb.Close() + rdb.SetDeviceUID("N8Q0CZ3N") + _ = rdb.SaveServerIdentity(ServerIdentity{StoreID: 99, StoreName: "Lj_04"}) + s := &RadioSync{uid: "N8Q0CZ3N", rdb: rdb, refreshEvery: 40 * time.Millisecond} + got := collectGetDataEndpoints(t, s, 400*time.Millisecond) + for _, ep := range []string{"config", "playlist", "files"} { + if got[ep] < 2 { + t.Fatalf("registered player must refetch %s on the timer, got %v", ep, got) + } + } +} + +func TestKeepaliveSkipsCatalogRefreshWhenUnregistered(t *testing.T) { + dir := t.TempDir() + rdb, _, err := openRadioDB(dir, exeMasterKey()) + if err != nil { + t.Fatal(err) + } + defer rdb.Close() + rdb.SetDeviceUID("N8Q0CZ3N") + s := &RadioSync{uid: "N8Q0CZ3N", rdb: rdb, refreshEvery: 40 * time.Millisecond} + got := collectGetDataEndpoints(t, s, 400*time.Millisecond) + if got["files"] != 0 || got["playlist"] != 0 { + t.Fatalf("unregistered player must not refetch the catalog, got %v", got) + } +}