Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
4c8f722
feat(sse): torrent streaming
s0up4200 Nov 2, 2025
3689977
perf(sse): share torrent snapshots across subscribers
s0up4200 Nov 2, 2025
25bff12
perf(sse): coalesce snapshots and shut down cleanly
s0up4200 Nov 2, 2025
e64e29b
fix: time
s0up4200 Nov 2, 2025
9efaa56
fix(torrents): keep HTTP fallback until stream connects
s0up4200 Nov 2, 2025
937464c
fix(sse): drop racy reassignment of group options
s0up4200 Nov 2, 2025
130400e
fix(web): keep torrent list order when streaming updates
s0up4200 Nov 2, 2025
212df06
Merge branch 'main' into feat/sse
s0up4200 Nov 2, 2025
1ab5f33
fix(sse): guard group options read with lock
s0up4200 Nov 2, 2025
9401f9e
perf(sse): prevent back-to-back forced syncs
s0up4200 Nov 2, 2025
bc819df
fix(sse): quote stream options to avoid group collisions
s0up4200 Nov 2, 2025
285d6f2
fix(web): handle stream error payloads
s0up4200 Nov 2, 2025
662d991
fix(sse): release publish lock and read error field
s0up4200 Nov 2, 2025
ddd36ec
fix(web): prevent sync stream key collisions
s0up4200 Nov 2, 2025
d71aaff
fix(qbittorrent): stop SSE torrent payload from forcing fresh sync
s0up4200 Nov 2, 2025
e2d8f91
feat(web): hydrate metadata hook from SSE torrent payload
s0up4200 Nov 2, 2025
fd4d4dd
refactor(web): drive dashboard instance stats directly from SSE
s0up4200 Nov 2, 2025
af6a38a
feat(api): add support for cached torrent data preference in API and …
s0up4200 Nov 2, 2025
45c6bad
perf(qbittorrent): reuse manual filter slice for counts
s0up4200 Nov 2, 2025
65c3b24
fix(sse): increase default sync interval from 2 to 3 seconds
s0up4200 Nov 2, 2025
5b2858f
fix(metadata): always refetch categories and tags
s0up4200 Nov 2, 2025
44db9d7
fix(torrents): only prefer cache when stream is active
s0up4200 Nov 2, 2025
c4c6cd0
fix(torrents): prefer fresh data when stream disconnects
s0up4200 Nov 2, 2025
00953af
fix(sse): reduce default sync interval to 2 seconds and increase cont…
s0up4200 Nov 2, 2025
7fa3347
fix(web): avoid leaking torrent metadata across instances
s0up4200 Nov 2, 2025
2bd2bf8
fix(torrents): smooth SSE handoff when filters change
s0up4200 Nov 2, 2025
fa914ab
refactor(frontend): rely on torrent stream metadata for instance sidebar
s0up4200 Nov 2, 2025
ca4e09a
feat(sse): stream all instances over a single connection
s0up4200 Nov 2, 2025
786a4ed
feat(stream): include qBittorrent app info in SSE payloads
s0up4200 Nov 2, 2025
15dda8f
feat(web): hydrate instance preferences via SSE
s0up4200 Nov 2, 2025
a16d089
Merge branch 'main' into feat/sse
s0up4200 Nov 4, 2025
fb3882c
fix(web): prevent instance metadata cache clobbering
s0up4200 Nov 4, 2025
c7684d1
fix(web): keep displaced torrents in stream merge
s0up4200 Nov 4, 2025
1a503a8
test(sse): add sort_order to instance schema
s0up4200 Nov 4, 2025
55e1b11
refactor(mobile): remove stream status div
s0up4200 Nov 4, 2025
e86442d
fix(dashboard): update fetch logic for app info
s0up4200 Nov 4, 2025
c6d495a
feat(dashboard): throttle SSE updates
s0up4200 Nov 4, 2025
a4f9f37
fix(sse): add heartbeat keepalive events
s0up4200 Nov 5, 2025
65fd25f
Merge branch 'main' into feat/sse
s0up4200 Nov 6, 2025
bc932cf
Merge branch 'main' into feat/sse
s0up4200 Nov 7, 2025
9839fd6
Merge branch 'main' into feat/sse
s0up4200 Nov 9, 2025
c0d5228
Merge branch 'main' into feat/sse
s0up4200 Nov 12, 2025
c554258
fix: stabilize SSE stream
s0up4200 Nov 13, 2025
472778c
Merge branch 'main' into feat/sse
s0up4200 Nov 15, 2025
a5a133f
Merge branch 'main' into feat/sse
s0up4200 Nov 18, 2025
568aafe
fix(cross-seed): skip disabled instances and stabilize metadata hook
s0up4200 Nov 18, 2025
00aa401
Merge branch 'main' into feat/sse
s0up4200 Dec 8, 2025
4c13ad0
feat(sync): add reconnect logic on tab visibility change to ensure ac…
s0up4200 Dec 8, 2025
ce11041
fix(sse): add instance metadata to stream and reduce polling
s0up4200 Dec 8, 2025
36a3489
fix(sse): improve error logging, add backoff tests, and document lock…
s0up4200 Dec 8, 2025
1dff441
fix(sse): improve error propagation, logging, and test coverage
s0up4200 Dec 8, 2025
ed1497d
fix(web): add type guard for SSE event data access
s0up4200 Dec 8, 2025
6331806
fix(sse): notify client on marshal failure instead of silent drop
s0up4200 Dec 8, 2025
ef3a1e9
fix(sse): improve error handling, logging, and nil safety
s0up4200 Dec 8, 2025
cc1ce02
Merge branch 'main' into feat/sse
s0up4200 Dec 11, 2025
6df7ea0
refactor(logging): change log level from Debug to Trace in sync manag…
s0up4200 Dec 11, 2025
7290b0d
test(sse): add coverage for subscription lifecycle and edge cases
s0up4200 Dec 11, 2025
311dbb4
fix(sse): add nil fallback for torrents cache and handle unknown vers…
s0up4200 Dec 11, 2025
cd37d44
Merge branch 'main' into feat/sse
s0up4200 Dec 11, 2025
2a4dd15
Merge branch 'main' into feat/sse
s0up4200 Dec 14, 2025
56acd1e
chore: merge origin/develop into feat/sse
s0up4200 Feb 20, 2026
783cc87
fix(web): resolve post-merge torrent UI type regressions
s0up4200 Feb 21, 2026
86c5867
fix(lint): resolve new golangci issues on feat/sse
s0up4200 Feb 21, 2026
9a2ebd9
fix(sse): avoid duplicate sync errors and fresh-tag fallback
s0up4200 Feb 21, 2026
f995f50
fix(web): restore dashboard fallback and clear metadata cache
s0up4200 Feb 21, 2026
b24d0ee
fix(web): remove duplicate torrents footer status bar
s0up4200 Feb 21, 2026
a9e2e07
refactor(web): stream torrent details state and remove dead status bar
s0up4200 Feb 21, 2026
c587c5e
fix(sse): normalize default sort and unlock pagination on growth
s0up4200 Feb 21, 2026
b8d7c68
fix(sse): clear stale state on sync and stream failures
s0up4200 Feb 21, 2026
7ed9030
fix(sse): disable write deadline for stream responses
s0up4200 Feb 21, 2026
7e9a646
fix(web): show dot-only status when sse is healthy
s0up4200 Feb 21, 2026
6f9621d
fix(sse): handle missing instance store without panic
s0up4200 Feb 21, 2026
457b7d0
Merge branch 'develop' into feat/sse
s0up4200 Feb 21, 2026
c0f56d7
fix(dashboard): restore issue filter links
s0up4200 Feb 21, 2026
fd1a606
fix(web): align SSE torrents cache key
s0up4200 Feb 21, 2026
f0ebdec
feat(sse): prefer stream for task count and speed state
s0up4200 Feb 21, 2026
d1c1c11
fix(sse): avoid stale preference cache and skip fresh reads
s0up4200 Feb 21, 2026
4a8e80e
fix(web): prefer fresh fallback state over stale stream
s0up4200 Feb 21, 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ require (
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
github.com/tmaxmax/go-sse v0.11.0
github.com/zeebo/bencode v1.0.0
golang.org/x/crypto v0.43.0
golang.org/x/image v0.32.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tmaxmax/go-sse v0.11.0 h1:nogmJM6rJUoOLoAwEKeQe5XlVpt9l7N82SS1jI7lWFg=
github.com/tmaxmax/go-sse v0.11.0/go.mod h1:u/2kZQR1tyngo1lKaNCj1mJmhXGZWS1Zs5yiSOD+Eg8=
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
Expand Down
20 changes: 20 additions & 0 deletions internal/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (

"github.com/autobrr/qui/internal/api/handlers"
"github.com/autobrr/qui/internal/api/middleware"
"github.com/autobrr/qui/internal/api/sse"
"github.com/autobrr/qui/internal/auth"
"github.com/autobrr/qui/internal/backups"
"github.com/autobrr/qui/internal/config"
Expand Down Expand Up @@ -51,6 +52,7 @@ type Server struct {
updateService *update.Service
trackerIconService *trackericons.Service
backupService *backups.Service
streamManager *sse.StreamManager
}

type Dependencies struct {
Expand All @@ -70,6 +72,11 @@ type Dependencies struct {
}

func NewServer(deps *Dependencies) *Server {
streamManager := sse.NewStreamManager(deps.ClientPool, deps.SyncManager, deps.InstanceStore)
if deps.ClientPool != nil {
deps.ClientPool.SetSyncEventSink(streamManager)
}

s := Server{
server: &http.Server{
ReadHeaderTimeout: time.Second * 15,
Expand All @@ -90,6 +97,7 @@ func NewServer(deps *Dependencies) *Server {
updateService: deps.UpdateService,
trackerIconService: deps.TrackerIconService,
backupService: deps.BackupService,
streamManager: streamManager,
}

return &s
Expand Down Expand Up @@ -152,6 +160,15 @@ func (s *Server) tryToServe(addr, protocol string) error {
}

func (s *Server) Shutdown(ctx context.Context) error {
if s.streamManager != nil {
shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()

if err := s.streamManager.Shutdown(shutdownCtx); err != nil && !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
s.logger.Warn().Err(err).Msg("failed to shut down stream manager cleanly")
}
}

return s.server.Shutdown(ctx)
}

Expand Down Expand Up @@ -300,6 +317,9 @@ func (s *Server) Handler() (*chi.Mux, error) {
})

r.Get("/capabilities", instancesHandler.GetInstanceCapabilities)
if s.streamManager != nil {
r.Get("/stream", s.streamManager.ServeInstance)
}

// Torrent creator
r.Route("/torrent-creator", func(r chi.Router) {
Expand Down
36 changes: 36 additions & 0 deletions internal/api/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import (
"fmt"
"net/http"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"unsafe"

"github.com/alexedwards/scs/v2"
"github.com/go-chi/chi/v5"
Expand Down Expand Up @@ -39,6 +41,7 @@ type routeKey struct {

var undocumentedRoutes = map[routeKey]struct{}{
{Method: http.MethodGet, Path: "/api/auth/validate"}: {},
{Method: http.MethodGet, Path: "/api/instances/{instanceId}/stream"}: {},
{Method: http.MethodPost, Path: "/api/instances/{instanceId}/backups/run"}: {},
{Method: http.MethodGet, Path: "/api/instances/{instanceId}/backups/runs"}: {},
{Method: http.MethodDelete, Path: "/api/instances/{instanceId}/backups/runs"}: {},
Expand All @@ -50,6 +53,21 @@ var undocumentedRoutes = map[routeKey]struct{}{
{Method: http.MethodPut, Path: "/api/instances/{instanceId}/backups/settings"}: {},
}

func TestNewServerRegistersStreamManagerAsSyncSink(t *testing.T) {
clientPool := &qbittorrent.ClientPool{}

server := NewServer(&Dependencies{
Config: &config.AppConfig{Config: &domain.Config{BaseURL: "/"}},
ClientPool: clientPool,
})

require.NotNil(t, server.streamManager, "expected stream manager to be initialized")

sink := getClientPoolSyncEventSink(t, clientPool)
require.NotNil(t, sink, "expected client pool to have a sync sink registered")
require.Same(t, server.streamManager, sink, "stream manager should be registered as sync sink")
}

func TestAllEndpointsDocumented(t *testing.T) {
server := NewServer(newTestDependencies(t))
router, err := server.Handler()
Expand Down Expand Up @@ -243,3 +261,21 @@ func formatRoutes(routes []routeKey) string {
}
return strings.Join(lines, "\n")
}

func getClientPoolSyncEventSink(t *testing.T, pool *qbittorrent.ClientPool) qbittorrent.SyncEventSink {
t.Helper()

value := reflect.ValueOf(pool).Elem().FieldByName("syncEventSink")
if !value.IsValid() {
t.Fatalf("client pool does not expose syncEventSink field")
}

exposed := reflect.NewAt(value.Type(), unsafe.Pointer(value.UnsafeAddr())).Elem()
if exposed.IsNil() {
return nil
}

sink, ok := exposed.Interface().(qbittorrent.SyncEventSink)
require.True(t, ok, "unexpected sink type stored on client pool")
return sink
}
Loading
Loading