upwatch: hold a seeding torrent app for one line test, then put its limit back exactly (R-1250)
gates / gates (push) Successful in 1m22s

When the agent waits for quiet and qBittorrent or Transmission runs: record the limit first,
hold at 1 KiB/s, ask the agent for a test on request, put the old limit back (deferred; a
household change during the test stays). Start-up puts back a limit a killed controller left;
the 50 % rule skips while a hold record exists. Commit only.
This commit is contained in:
2026-10-24 13:19:07 +02:00
parent bddc477e22
commit 046b808792
10 changed files with 789 additions and 33 deletions
+36 -15
View File
@@ -211,10 +211,32 @@ type Transmission struct {
func (t *Transmission) Stack() string { return "transmission" }
func (t *Transmission) Ask(ctx context.Context) (Report, error) {
pw, err := t.Box.Env("transmission", "PASS")
b, err := t.rpc(ctx, `{"method":"session-stats"}`)
if err != nil {
return Report{}, err
}
var st struct {
Arguments struct {
Current struct {
UploadedBytes uint64 `json:"uploadedBytes"`
} `json:"current-stats"`
} `json:"arguments"`
}
if err := json.Unmarshal(b, &st); err != nil {
return Report{}, err
}
return Report{UploadCounter: st.Arguments.Current.UploadedBytes, HasUploadCounter: true}, nil
}
var rpcMethodRe = regexp.MustCompile(`"method":"([a-z-]+)"`)
// rpc is one Transmission RPC call: Basic auth with the stack's PASS, and the X-Transmission-Session-Id handshake
// (the first answer is 409 carrying it). It returns the 200 body; `result` other than "success" is an error.
func (t *Transmission) rpc(ctx context.Context, body string) ([]byte, error) {
pw, err := t.Box.Env("transmission", "PASS")
if err != nil {
return nil, err
}
base := t.URL
if base == "" {
base = "http://transmission:9091"
@@ -224,32 +246,31 @@ func (t *Transmission) Ask(ctx context.Context) (Report, error) {
c = &http.Client{Timeout: 20 * time.Second}
}
h := map[string]string{"Authorization": "Basic " + base64.StdEncoding.EncodeToString([]byte("admin:"+pw)), "Host": "transmission:9091"}
body := []byte(`{"method":"session-stats"}`)
method := body
if m := rpcMethodRe.FindStringSubmatch(body); m != nil {
method = m[1]
}
for i := 0; i < 2; i++ {
code, b, hd, err := httpDo(ctx, c, "POST", base+"/transmission/rpc", h, body, "application/json")
code, b, hd, err := httpDo(ctx, c, "POST", base+"/transmission/rpc", h, []byte(body), "application/json")
if err != nil {
return Report{}, err
return nil, err
}
if code == 409 {
h["X-Transmission-Session-Id"] = hd.Get("X-Transmission-Session-Id")
continue
}
if code != 200 {
return Report{}, fmt.Errorf("transmission session-stats: HTTP %d", code)
return nil, fmt.Errorf("transmission %s: HTTP %d", method, code)
}
var st struct {
Arguments struct {
Current struct {
UploadedBytes uint64 `json:"uploadedBytes"`
} `json:"current-stats"`
} `json:"arguments"`
var r struct {
Result string `json:"result"`
}
if err := json.Unmarshal(b, &st); err != nil {
return Report{}, err
if err := json.Unmarshal(b, &r); err == nil && r.Result != "" && r.Result != "success" {
return nil, fmt.Errorf("transmission %s: %s", method, r.Result)
}
return Report{UploadCounter: st.Arguments.Current.UploadedBytes, HasUploadCounter: true}, nil
return b, nil
}
return Report{}, fmt.Errorf("transmission session-stats: no session id")
return nil, fmt.Errorf("transmission %s: no session id", method)
}
// ── Plex ─────────────────────────────────────────────────────────────────────────────────────────────────────────