Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/andybalholm/brotli v1.2.1
github.com/autobrr/autobrr v1.77.0
github.com/autobrr/go-mediainfo v0.3.1
github.com/autobrr/go-qbittorrent v1.15.1-0.20260506110806-0f6206131e7a
github.com/autobrr/go-qbittorrent v1.15.1-0.20260513090446-958722ab43aa
Comment thread
s0up4200 marked this conversation as resolved.
github.com/avast/retry-go v3.0.0+incompatible
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.18.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ github.com/autobrr/autobrr v1.77.0 h1:0SNa9g2mk5Qe92vHOt0eJJz7VHB515R/BZyy1fCzik
github.com/autobrr/autobrr v1.77.0/go.mod h1:GstQT2UumxQE54ECtjl5jwdzenaGFzvWxVNEVoLWH4E=
github.com/autobrr/go-mediainfo v0.3.1 h1:4gT1qoIKp4e+9X7PA/YO53lEkylY+GubFhCl8u8o1R4=
github.com/autobrr/go-mediainfo v0.3.1/go.mod h1:xVXgHDAYYpIeGQCQCdTBpoiY/O3tojJfflK+rrbzclQ=
github.com/autobrr/go-qbittorrent v1.15.1-0.20260506110806-0f6206131e7a h1:bWzZDGbwP72DxYSP9+ab68bmEYxf2FKV3LsklFrNt2s=
github.com/autobrr/go-qbittorrent v1.15.1-0.20260506110806-0f6206131e7a/go.mod h1:W/24JQyQI+u5KntK1qzkBC/7amOTBPpV+q5e3C+kCkY=
github.com/autobrr/go-qbittorrent v1.15.1-0.20260513090446-958722ab43aa h1:86CxGg29+Cq2Gs/rS7n42LyH+1YICffkeEPiL6HWPjA=
github.com/autobrr/go-qbittorrent v1.15.1-0.20260513090446-958722ab43aa/go.mod h1:W/24JQyQI+u5KntK1qzkBC/7amOTBPpV+q5e3C+kCkY=
github.com/autobrr/rls v0.8.0 h1:Odz78o5nfrO4hsQ17qiuyKYtiG97+Cs5nrD9TFk8/lI=
github.com/autobrr/rls v0.8.0/go.mod h1:11YJWgNe5H+UQrfFaNGjnkEy0OEsMFnmV4IL2tRKpPk=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
Expand Down
47 changes: 42 additions & 5 deletions internal/api/handlers/qbittorrent_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,18 @@ import (
"encoding/json"
"net/http"
"strconv"
"strings"

"github.com/Masterminds/semver/v3"
qbt "github.com/autobrr/go-qbittorrent"
"github.com/go-chi/chi/v5"
"github.com/rs/zerolog/log"

internalqbittorrent "github.com/autobrr/qui/internal/qbittorrent"
)

var processInfoMinWebAPIVersion = semver.MustParse("2.15.1")

type QBittorrentInfoHandler struct {
clientPool *internalqbittorrent.ClientPool
}
Expand All @@ -36,11 +41,23 @@ type QBittorrentBuildInfo struct {
Platform string `json:"platform,omitempty"`
}

type QBittorrentProcessInfo struct {
LaunchTime int64 `json:"launchTime"`
}

// QBittorrentAppInfo represents qBittorrent application information
type QBittorrentAppInfo struct {
Version string `json:"version"`
WebAPIVersion string `json:"webAPIVersion,omitempty"`
BuildInfo *QBittorrentBuildInfo `json:"buildInfo,omitempty"`
Version string `json:"version"`
WebAPIVersion string `json:"webAPIVersion,omitempty"`
BuildInfo *QBittorrentBuildInfo `json:"buildInfo,omitempty"`
ProcessInfo *QBittorrentProcessInfo `json:"processInfo,omitempty"`
}

type qbittorrentAppInfoClient interface {
GetAppVersionCtx(ctx context.Context) (string, error)
GetWebAPIVersionCtx(ctx context.Context) (string, error)
GetBuildInfoCtx(ctx context.Context) (qbt.BuildInfo, error)
GetProcessInfoCtx(ctx context.Context) (qbt.ProcessInfo, error)
}

// GetQBittorrentAppInfo returns qBittorrent application version and build information
Expand All @@ -64,7 +81,7 @@ func (h *QBittorrentInfoHandler) GetQBittorrentAppInfo(w http.ResponseWriter, r
}

// Get qBittorrent version and build info
appInfo, err := h.getQBittorrentAppInfo(ctx, client)
appInfo, err := getQBittorrentAppInfo(ctx, client)
if err != nil {
log.Error().Err(err).Int("instanceID", instanceID).Msg("Failed to get qBittorrent application info")
RespondError(w, http.StatusInternalServerError, "Failed to get qBittorrent application info")
Expand All @@ -77,7 +94,7 @@ func (h *QBittorrentInfoHandler) GetQBittorrentAppInfo(w http.ResponseWriter, r
}

// getQBittorrentAppInfo fetches application info from qBittorrent API
func (h *QBittorrentInfoHandler) getQBittorrentAppInfo(ctx context.Context, client *internalqbittorrent.Client) (*QBittorrentAppInfo, error) {
func getQBittorrentAppInfo(ctx context.Context, client qbittorrentAppInfoClient) (*QBittorrentAppInfo, error) {
// Get qBittorrent application version
version, err := client.GetAppVersionCtx(ctx)
if err != nil {
Expand Down Expand Up @@ -115,5 +132,25 @@ func (h *QBittorrentInfoHandler) getQBittorrentAppInfo(ctx context.Context, clie
},
}

if supportsProcessInfo(webAPIVersion) {
processInfo, err := client.GetProcessInfoCtx(ctx)
if err != nil {
log.Debug().Err(err).Str("webAPIVersion", webAPIVersion).Msg("Failed to get qBittorrent process info")
} else {
appInfo.ProcessInfo = &QBittorrentProcessInfo{
LaunchTime: processInfo.LaunchTime,
}
}
}

return appInfo, nil
}

func supportsProcessInfo(webAPIVersion string) bool {
version, err := semver.NewVersion(strings.TrimSpace(webAPIVersion))
if err != nil {
return false
}

return !version.LessThan(processInfoMinWebAPIVersion)
}
104 changes: 104 additions & 0 deletions internal/api/handlers/qbittorrent_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package handlers

import (
"context"
"errors"
"testing"

qbt "github.com/autobrr/go-qbittorrent"
"github.com/stretchr/testify/require"
)

type stubQBittorrentAppInfoClient struct {
version string
webAPIVersion string
buildInfo qbt.BuildInfo
processInfo qbt.ProcessInfo
processInfoCalled bool
processInfoCallErr error
}

func (c *stubQBittorrentAppInfoClient) GetAppVersionCtx(context.Context) (string, error) {
return c.version, nil
}

func (c *stubQBittorrentAppInfoClient) GetWebAPIVersionCtx(context.Context) (string, error) {
return c.webAPIVersion, nil
}

func (c *stubQBittorrentAppInfoClient) GetBuildInfoCtx(context.Context) (qbt.BuildInfo, error) {
return c.buildInfo, nil
}

func (c *stubQBittorrentAppInfoClient) GetProcessInfoCtx(context.Context) (qbt.ProcessInfo, error) {
c.processInfoCalled = true
return c.processInfo, c.processInfoCallErr
}

func TestGetQBittorrentAppInfoProcessInfo(t *testing.T) {
processInfoErr := errors.New("process info unavailable")

tests := []struct {
name string
version string
webAPIVersion string
processInfo qbt.ProcessInfo
processInfoErr error
wantProcessInfoCalled bool
wantLaunchTime int64
wantProcessInfo bool
}{
{
name: "includes process info for supported web api",
version: "v5.2.0",
webAPIVersion: "2.15.1",
processInfo: qbt.ProcessInfo{LaunchTime: 1769331513},
wantProcessInfoCalled: true,
wantLaunchTime: 1769331513,
wantProcessInfo: true,
},
{
name: "skips process info for older web api",
version: "v5.1.4",
webAPIVersion: "2.11.4",
processInfo: qbt.ProcessInfo{LaunchTime: 1769331513},
wantProcessInfoCalled: false,
wantProcessInfo: false,
},
{
name: "omits process info when supported call fails",
version: "v5.2.0",
webAPIVersion: "2.15.1",
processInfoErr: processInfoErr,
wantProcessInfoCalled: true,
wantProcessInfo: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := &stubQBittorrentAppInfoClient{
version: tt.version,
webAPIVersion: tt.webAPIVersion,
buildInfo: qbt.BuildInfo{
Libtorrent: "2.0.11",
Platform: "linux",
},
processInfo: tt.processInfo,
processInfoCallErr: tt.processInfoErr,
}

info, err := getQBittorrentAppInfo(context.Background(), client)

require.NoError(t, err)
require.Equal(t, tt.wantProcessInfoCalled, client.processInfoCalled)
if !tt.wantProcessInfo {
require.Nil(t, info.ProcessInfo)
return
}

require.NotNil(t, info.ProcessInfo)
require.Equal(t, tt.wantLaunchTime, info.ProcessInfo.LaunchTime)
})
}
}
12 changes: 12 additions & 0 deletions internal/web/swagger/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6093,6 +6093,18 @@ components:
example: "2.11.4"
buildInfo:
$ref: '#/components/schemas/QBittorrentBuildInfo'
processInfo:
$ref: '#/components/schemas/QBittorrentProcessInfo'

QBittorrentProcessInfo:
type: object
description: qBittorrent process information
properties:
launchTime:
type: integer
format: int64
description: Process launch time as UTC epoch seconds
example: 1769331513

CrossSeedInstanceResult:
type: object
Expand Down
14 changes: 12 additions & 2 deletions web/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ import { usePersistedTitleBarSpeeds } from "@/hooks/usePersistedTitleBarSpeeds"
import { useQBittorrentAppInfo } from "@/hooks/useQBittorrentAppInfo"
import { useTitleBarSpeeds } from "@/hooks/useTitleBarSpeeds"
import { api } from "@/lib/api"
import { copyTextToClipboard, formatBytes, getRatioColor } from "@/lib/utils"
import { copyTextToClipboard, formatBytes, formatDuration, getRatioColor } from "@/lib/utils"
import type { InstanceResponse, ServerState, TorrentCounts, TorrentResponse, TorrentStats } from "@/types"
import { useMutation, useQueries, useQueryClient } from "@tanstack/react-query"
import { Link } from "@tanstack/react-router"
import { Activity, AlertCircle, AlertTriangle, ArrowDown, ArrowUp, ArrowUpDown, Ban, BrickWallFire, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Download, ExternalLink, Eye, EyeOff, Globe, HardDrive, Info, Link2, Minus, Pencil, Plus, Rabbit, RefreshCcw, Trash2, Turtle, Upload, X, Zap } from "lucide-react"
import { Activity, AlertCircle, AlertTriangle, ArrowDown, ArrowUp, ArrowUpDown, Ban, BrickWallFire, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Clock, Download, ExternalLink, Eye, EyeOff, Globe, HardDrive, Info, Link2, Minus, Pencil, Plus, Rabbit, RefreshCcw, Trash2, Turtle, Upload, X, Zap } from "lucide-react"
import { useEffect, useMemo, useState } from "react"
import { toast } from "sonner"

Expand Down Expand Up @@ -218,6 +218,8 @@ function InstanceCard({
const appVersion = qbittorrentAppInfo?.version || qbittorrentVersionInfo?.appVersion || ""
const webAPIVersion = qbittorrentAppInfo?.webAPIVersion || qbittorrentVersionInfo?.webAPIVersion || ""
const libtorrentVersion = qbittorrentAppInfo?.buildInfo?.libtorrent || ""
const launchTime = qbittorrentAppInfo?.processInfo?.launchTime
const uptimeSeconds = typeof launchTime === "number" && launchTime > 0 ? Math.max(0, Math.floor(Date.now() / 1000) - launchTime) : null
const displayUrl = instance.host

// Determine card state
Expand Down Expand Up @@ -558,6 +560,14 @@ function InstanceCard({
<span>{`Show ${isAdvancedMetricsOpen ? "less" : "more"}`}</span>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-2 mt-2">
{uptimeSeconds !== null && (
<div className="flex items-center gap-2 text-xs">
<Clock className="h-3 w-3 text-muted-foreground" />
<span className="text-muted-foreground">Uptime</span>
<span className="ml-auto font-medium">{formatDuration(uptimeSeconds)}</span>
</div>
)}

{serverState?.total_peer_connections !== undefined && (
<div className="flex items-center gap-2 text-xs">
<Activity className="h-3 w-3 text-muted-foreground" />
Expand Down
5 changes: 5 additions & 0 deletions web/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,11 @@ export interface QBittorrentAppInfo {
version: string
webAPIVersion?: string
buildInfo?: QBittorrentBuildInfo
processInfo?: QBittorrentProcessInfo
}

export interface QBittorrentProcessInfo {
launchTime: number
}

// Torrent Creation Types
Expand Down