Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
47551af
feat(windows): add HyperHQ launcher and bridge plugin
wizzomafizzo May 9, 2026
7ce1bbc
Merge branch 'main' into feat/windows-hyperhq-launcher
wizzomafizzo May 23, 2026
ad3633c
fix(windows): scope HyperHQ bridge sessions
wizzomafizzo May 23, 2026
2b2f3b2
fix(windows): repair HyperHQ plugin integration
wizzomafizzo May 27, 2026
9021ec1
test(windows): fix HyperHQ mapping test
wizzomafizzo May 27, 2026
ad3c14e
test(windows): satisfy HyperHQ cross lint
wizzomafizzo May 27, 2026
19f39d5
test(windows): avoid manifest struct lint conflict
wizzomafizzo May 27, 2026
bdd60d0
fix(windows): satisfy HyperHQ plugin cross lint
wizzomafizzo May 27, 2026
a2c1403
test(windows): apply manifest lint ordering
wizzomafizzo May 27, 2026
a3fb4e4
fix(windows): document HyperHQ plugin lint exceptions
wizzomafizzo May 27, 2026
b82c0c5
test(windows): satisfy plugin revive lint
wizzomafizzo May 27, 2026
fb4b128
test(windows): address plugin test revive warnings
wizzomafizzo May 27, 2026
f3f1fd9
fix(windows): clear HyperHQ plugin cross lint
wizzomafizzo May 27, 2026
4cfc840
fix(windows): align HyperHQ plugin socket auth (#880)
cjackson234 Jun 3, 2026
0fd1838
merge origin/main into HyperHQ launcher branch
wizzomafizzo Jun 3, 2026
20a5522
fix(ci): stabilize HyperHQ PR checks
wizzomafizzo Jun 4, 2026
1e7c85c
fix(windows): remove HyperHQ plugin QUIC dependency
wizzomafizzo Jun 4, 2026
2958a04
test(api): tighten websocket dispatcher tests
wizzomafizzo Jun 4, 2026
76868ac
chore(windows): polish HyperHQ plugin metadata
wizzomafizzo Jun 4, 2026
dc3df88
test(windows): wrap HyperHQ manifest assertion
wizzomafizzo Jun 4, 2026
62a78b1
test(api): serialize websocket dispatcher tests
wizzomafizzo Jun 4, 2026
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
8 changes: 7 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
version: "2"

run:
go: "1.25"
go: "1.26.3"

linters:
exclusions:
Expand All @@ -17,6 +17,12 @@ linters:
- linters:
- gocritic
path: pkg/helpers/usb_darwin\.go
# Standalone plugin module: separate go.mod with its own dependency surface.
# zerolog/syncutil are Zaparoo Core internals and not imported by the bridge.
- linters:
- depguard
- forbidigo
path: scripts/windows/hyperhq-plugin/
enable:
- staticcheck
- errcheck
Expand Down
4 changes: 4 additions & 0 deletions Taskfile.dist.yml
Original file line number Diff line number Diff line change
Expand Up @@ -179,11 +179,15 @@ tasks:
desc: Run golangci-lint
cmds:
- golangci-lint run ./...
# The HyperHQ plugin module under scripts/windows/hyperhq-plugin/ is
# Windows-only. Local typechecking cannot resolve winio symbols on Linux,
# so it is linted via `task cross-lint:windows` instead.

lint-fix:
desc: Run golangci-lint with auto-fixes
cmds:
- golangci-lint run --fix ./...
# See note on lint above — Windows-only plugin lints via cross-lint:windows.

vulncheck:
desc: Run govulncheck for security vulnerabilities
Expand Down
74 changes: 44 additions & 30 deletions pkg/api/ws_dispatcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ import (
"github.com/stretchr/testify/require"
)

type countingRequestTracker struct {
count int
}

func (t *countingRequestTracker) RequestStarted() { t.count++ }

func (t *countingRequestTracker) RequestEnded() { t.count-- }

func (t *countingRequestTracker) inFlight() int { return t.count }

func indexRPCID(ids []models.RPCID, target models.RPCID) int {
for i, id := range ids {
if id.Equal(target) {
Expand Down Expand Up @@ -83,19 +93,23 @@ func startPriorityWSServer(t *testing.T, methodMap *MethodMap) (wsURL string, cl
}

func TestWebSocketPriorityDispatcherHighPriorityBypassesSlowImage(t *testing.T) {
t.Parallel()
imageStarted := make(chan struct{}, wsLowConcurrency)
highStarted := make(chan struct{})
releaseImages := make(chan struct{})

var methodMap MethodMap
require.NoError(t, methodMap.AddMethod(models.MethodMediaImage, func(env requests.RequestEnv) (any, error) {
imageStarted <- struct{}{}
select {
case <-time.After(250 * time.Millisecond):
case <-releaseImages:
return map[string]string{"kind": "image"}, nil
case <-env.Context.Done():
return nil, env.Context.Err()
}
}))
require.NoError(t, methodMap.AddMethod(models.MethodMediaTagsUpdate, func(requests.RequestEnv) (any, error) {
return map[string]string{"kind": "favorite"}, nil
require.NoError(t, methodMap.AddMethod(models.MethodRun, func(requests.RequestEnv) (any, error) {
close(highStarted)
return map[string]string{"kind": "run"}, nil
}))

wsURL, cleanup := startPriorityWSServer(t, &methodMap)
Expand All @@ -104,33 +118,49 @@ func TestWebSocketPriorityDispatcherHighPriorityBypassesSlowImage(t *testing.T)
conn := dialWS(t, wsURL)
defer func() { _ = conn.Close() }()

for id := 1; id <= 3; id++ {
for id := 1; id <= wsLowConcurrency; id++ {
require.NoError(t, conn.WriteMessage(websocket.TextMessage,
[]byte(fmt.Sprintf(`{"jsonrpc":"2.0","method":"media.image","id":%d}`, id))))
}
for range wsLowConcurrency {
select {
case <-imageStarted:
case <-time.After(2 * time.Second):
t.Fatal("initial image request did not start")
}
}

imageID := models.NewNumberID(int64(wsLowConcurrency + 1))
highID := models.NewNumberID(int64(wsLowConcurrency + 2))
require.NoError(t, conn.WriteMessage(websocket.TextMessage,
[]byte(`{"jsonrpc":"2.0","method":"media.tags.update","params":{"mediaId":1,"add":["user:favorite"]},"id":4}`)))
[]byte(fmt.Sprintf(`{"jsonrpc":"2.0","method":"media.image","id":%s}`, imageID.String()))))
require.NoError(t, conn.WriteMessage(websocket.TextMessage,
[]byte(fmt.Sprintf(`{"jsonrpc":"2.0","method":"run","id":%s}`, highID.String()))))
select {
case <-highStarted:
case <-time.After(2 * time.Second):
t.Fatal("high-priority request did not start")
}
close(releaseImages)

require.NoError(t, conn.SetReadDeadline(time.Now().Add(2*time.Second)))
seen := make([]models.RPCID, 0, 4)
for range 4 {
for indexRPCID(seen, highID) == -1 || indexRPCID(seen, imageID) == -1 {
_, msg, err := conn.ReadMessage()
require.NoError(t, err)
var resp models.ResponseObject
require.NoError(t, json.Unmarshal(msg, &resp))
seen = append(seen, resp.ID)
}

favoriteIndex := indexRPCID(seen, models.NewNumberID(4))
thirdImageIndex := indexRPCID(seen, models.NewNumberID(3))
require.NotEqual(t, -1, favoriteIndex)
require.NotEqual(t, -1, thirdImageIndex)
assert.Less(t, favoriteIndex, thirdImageIndex, "mutation should bypass queued image work")
highIndex := indexRPCID(seen, highID)
imageIndex := indexRPCID(seen, imageID)
require.NotEqual(t, -1, highIndex)
require.NotEqual(t, -1, imageIndex)
assert.Less(t, highIndex, imageIndex, "high-priority request should bypass queued image work")
}

func TestWebSocketPriorityDispatcherPreservesHighPriorityOrder(t *testing.T) {
t.Parallel()

firstDone := make(chan struct{})
var methodMap MethodMap
require.NoError(t, methodMap.AddMethod(models.MethodRun, func(env requests.RequestEnv) (any, error) {
Expand Down Expand Up @@ -177,8 +207,6 @@ func TestWebSocketPriorityDispatcherPreservesHighPriorityOrder(t *testing.T) {
}

func TestWebSocketPriorityDispatcherMediaTransactionBlocksMediaReads(t *testing.T) {
t.Parallel()

txStarted := make(chan struct{})
releaseTx := make(chan struct{})
metaStarted := make(chan struct{}, 1)
Expand Down Expand Up @@ -228,8 +256,6 @@ func TestWebSocketPriorityDispatcherMediaTransactionBlocksMediaReads(t *testing.
}

func TestWebSocketPriorityDispatcherNotificationsDoNotReply(t *testing.T) {
t.Parallel()

var methodMap MethodMap
require.NoError(t, methodMap.AddMethod("test.notify", func(requests.RequestEnv) (any, error) {
return map[string]string{"ok": "true"}, nil
Expand All @@ -250,8 +276,6 @@ func TestWebSocketPriorityDispatcherNotificationsDoNotReply(t *testing.T) {
}

func TestCloseWSDispatcherCancelsQueuedRequests(t *testing.T) {
t.Parallel()

ctx, cancel := context.WithCancel(t.Context())
d := &wsSessionDispatcher{
ctx: ctx,
Expand All @@ -275,13 +299,3 @@ func TestCloseWSDispatcherCancelsQueuedRequests(t *testing.T) {
assert.Equal(t, 0, tracker.inFlight())
assert.Error(t, jobCtx.Err())
}

type countingRequestTracker struct {
count int
}

func (t *countingRequestTracker) RequestStarted() { t.count++ }

func (t *countingRequestTracker) RequestEnded() { t.count-- }

func (t *countingRequestTracker) inFlight() int { return t.count }
7 changes: 7 additions & 0 deletions pkg/assets/systems/Custom.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "Custom",
"name": "Custom",
"category": "Other",
"releaseDate": "",
"manufacturer": ""
}
4 changes: 4 additions & 0 deletions pkg/database/systemdefs/systemdefs.go
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ const (
SystemJ2ME = "J2ME"
SystemGroovy = "Groovy"
SystemPlugNPlay = "PlugNPlay"
SystemCustom = "Custom"
SystemDevErr = "DevErr"
)

Expand Down Expand Up @@ -1151,6 +1152,9 @@ var Systems = map[string]System{
ID: SystemPlugNPlay,
Slugs: []string{"plugandplay", "tvgame", "tvgames"},
},
SystemCustom: {
ID: SystemCustom,
},
SystemIOS: {
ID: SystemIOS,
Slugs: []string{"iphone", "ipad", "applegame", "applegames"},
Expand Down
2 changes: 2 additions & 0 deletions pkg/platforms/shared/schemes.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
SchemeLutris = "lutris"
SchemeHeroic = "heroic"
SchemeGOG = "gog"
SchemeHyperHq = "hyperhq"
)

// Kodi URI scheme constants for Kodi media library items.
Expand Down Expand Up @@ -59,6 +60,7 @@ var customSchemes = []string{
SchemeLutris,
SchemeHeroic,
SchemeGOG,
SchemeHyperHq,
SchemeKodiMovie,
SchemeKodiEpisode,
SchemeKodiSong,
Expand Down
Loading
Loading