Initial Tdarr-tray: KDE systray applet for the Tdarr workstation node
All checks were successful
release / release (push) Successful in 16s
All checks were successful
release / release (push) Successful in 16s
PyQt6 QSystemTrayIcon: green=running, red=stopped, yellow=transition. Pause/resume the node via the API (frees/reclaims the GPU, no sudo) so you can free the 4060 Ti before gaming. Playbook standard: README (per-OS prereqs + build/run/test), doc/plan.md, .gitignore (build/ first), VS Code build task -> ./build, install.sh + one-line curl (clone or release), Gitea Actions rolling-latest release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011YPrEZVBpPVoCRyJz2exkZ
This commit is contained in:
65
.gitea/workflows/release.yaml
Normal file
65
.gitea/workflows/release.yaml
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
name: release
|
||||||
|
|
||||||
|
# Publicerar tray-appen (tdarr-tray.py) + install.sh till en rullande
|
||||||
|
# 'latest'-release; en vX.Y.Z-tag ger en versionerad release. Runnern använder
|
||||||
|
# sin egen GITHUB_TOKEN mot Gitea-API:t.
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master, main]
|
||||||
|
tags: ["v*"]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: "Release-tag (tom = 'latest')"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Bestäm tag
|
||||||
|
id: t
|
||||||
|
run: |
|
||||||
|
case "$GITHUB_REF" in
|
||||||
|
refs/tags/*) T="$GITHUB_REF_NAME" ;;
|
||||||
|
*) T="${{ inputs.tag }}"; [ -z "$T" ] && T="latest" ;;
|
||||||
|
esac
|
||||||
|
echo "tag=$T" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Paketera + syntaxkoll
|
||||||
|
run: |
|
||||||
|
set -eux
|
||||||
|
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO=sudo
|
||||||
|
$SUDO apt-get update && $SUDO apt-get install -y --no-install-recommends jq curl ca-certificates python3 || true
|
||||||
|
python3 -m py_compile tdarr-tray.py
|
||||||
|
mkdir -p dist
|
||||||
|
cp tdarr-tray.py dist/tdarr-tray.py
|
||||||
|
cp install.sh dist/install.sh
|
||||||
|
cd dist && sha256sum tdarr-tray.py install.sh > checksums.txt && ls -la
|
||||||
|
|
||||||
|
- name: Skapa release + ladda upp assets
|
||||||
|
env:
|
||||||
|
API: http://gitea-d:3000/api/v1
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
TAG: ${{ steps.t.outputs.tag }}
|
||||||
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
BODY="Tdarr-tray $TAG — systray-applet för Tdarr-noden (KDE/Plasma).
|
||||||
|
|
||||||
|
Installera (en rad):
|
||||||
|
curl -fsSL https://gitea.brasse-pc.eu/brasse/Tdarr-tray/raw/branch/master/install.sh | sh
|
||||||
|
|
||||||
|
Kräver python3 + PyQt6. Se wiki."
|
||||||
|
payload=$(jq -n --arg tag "$TAG" --arg body "$BODY" '{tag_name:$tag,name:$tag,body:$body}')
|
||||||
|
rid=$(curl -s -X POST "$API/repos/$REPO/releases" -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' -d "$payload" | jq -r '.id // empty')
|
||||||
|
[ -z "$rid" ] && rid=$(curl -s "$API/repos/$REPO/releases/tags/$TAG" -H "Authorization: token $TOKEN" | jq -r '.id')
|
||||||
|
curl -s -X PATCH "$API/repos/$REPO/releases/$rid" -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||||||
|
-d "$(jq -n --arg tag "$TAG" --arg body "$BODY" '{name:$tag,body:$body}')" -o /dev/null
|
||||||
|
for f in tdarr-tray.py install.sh checksums.txt; do
|
||||||
|
aid=$(curl -s "$API/repos/$REPO/releases/$rid/assets" -H "Authorization: token $TOKEN" | jq -r ".[] | select(.name==\"$f\") | .id")
|
||||||
|
[ -n "$aid" ] && [ "$aid" != "null" ] && curl -s -X DELETE "$API/repos/$REPO/releases/$rid/assets/$aid" -H "Authorization: token $TOKEN" -o /dev/null
|
||||||
|
curl -s -X POST "$API/repos/$REPO/releases/$rid/assets?name=$f" -H "Authorization: token $TOKEN" -F "attachment=@dist/$f" -o /dev/null -w " $f -> HTTP %{http_code}\n"
|
||||||
|
done
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
build/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.env
|
||||||
40
.vscode/tasks.json
vendored
Normal file
40
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "build",
|
||||||
|
"detail": "Bygger en körbar tdarr-tray till ./build/",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "mkdir -p build && install -m 0755 tdarr-tray.py build/tdarr-tray && echo '-> build/tdarr-tray'",
|
||||||
|
"group": { "kind": "build", "isDefault": true },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Installera + autostart",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "./install.sh",
|
||||||
|
"options": { "cwd": "${workspaceFolder}" },
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Kör (förgrund)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "python3 tdarr-tray.py",
|
||||||
|
"isBackground": true,
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Status (--check)",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "python3 tdarr-tray.py --check",
|
||||||
|
"group": "test",
|
||||||
|
"problemMatcher": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Stoppa tray",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "pkill -f tdarr-tray && echo stoppad || echo 'ingen instans'",
|
||||||
|
"problemMatcher": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
63
README.md
Normal file
63
README.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# Tdarr-tray
|
||||||
|
|
||||||
|
En **systray-applet** (KDE/Plasma, Wayland) för att styra Tdarr-noden på
|
||||||
|
workstationen — se om den transkoderar och pausa/återuppta den med ett klick
|
||||||
|
(t.ex. innan du spelar: paus frigör GPU:n direkt).
|
||||||
|
|
||||||
|
## Färger
|
||||||
|
| Ikon | Betydelse |
|
||||||
|
|------|-----------|
|
||||||
|
| 🟢 grön | noden **körs** (ansluten, ej pausad — transkoderar eller redo) |
|
||||||
|
| 🔴 röd | noden **körs inte** (pausad eller offline) |
|
||||||
|
| 🟡 gul | **övergång** — håller på att pausas/återupptas |
|
||||||
|
|
||||||
|
Symbol i ikonen: ▶ (körs), ❚❚ (stoppad), ⋯ (övergång).
|
||||||
|
|
||||||
|
## Installera
|
||||||
|
|
||||||
|
En rad (laddar ner appen, sätter upp autostart, startar den):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://gitea.brasse-pc.eu/brasse/Tdarr-tray/raw/branch/master/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Från en klon: `./install.sh`. Installerar till `~/.local/bin/tdarr-tray` +
|
||||||
|
autostart i `~/.config/autostart/`.
|
||||||
|
|
||||||
|
## Prerequisites (per OS)
|
||||||
|
|
||||||
|
Python ≥ 3.8 + **PyQt6**:
|
||||||
|
|
||||||
|
| OS | Kommando |
|
||||||
|
|----|----------|
|
||||||
|
| Garuda/Arch (dev-boxen) | `sudo pacman -S --needed python python-pyqt6` |
|
||||||
|
| Debian/Ubuntu | `sudo apt install python3 python3-pyqt6` |
|
||||||
|
|
||||||
|
Kräver en systray (KDE/Plasma har det inbyggt).
|
||||||
|
|
||||||
|
## Meny (högerklick på ikonen)
|
||||||
|
Status · **⏸ Pausa / ▶ Återuppta nod** (via Tdarrs API, frigör/återtar GPU:n) ·
|
||||||
|
Uppdatera nu · Öppna Tdarr i webbläsaren · Avsluta.
|
||||||
|
|
||||||
|
## Konfiguration (miljövariabler)
|
||||||
|
| Var | Standard | Beskrivning |
|
||||||
|
|-----|----------|-------------|
|
||||||
|
| `TDARR_URL` | `http://192.168.0.19:8266` | Tdarr-serverns API |
|
||||||
|
| `TDARR_NODE` | `Workstation-4060Ti` | nodnamn att styra |
|
||||||
|
| `TDARR_API_KEY` | (läses ur `~/tdarr-node/.env`, rad `apiKey=`) | API-nyckel (`x-api-key`) |
|
||||||
|
| `TDARR_POLL_MS` | `5000` | polling-intervall |
|
||||||
|
|
||||||
|
## Bygg (till ./build) / test
|
||||||
|
|
||||||
|
VS Code: **Run Build Task** (`Ctrl+Shift+B`) → `build`-tasken lägger en körbar
|
||||||
|
`tdarr-tray` i `build/` (gitignoread). Manuellt / rök-koll:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p build && install -m 0755 tdarr-tray.py build/tdarr-tray
|
||||||
|
python3 -m py_compile tdarr-tray.py # syntax
|
||||||
|
python3 tdarr-tray.py --check # headless statuskoll
|
||||||
|
```
|
||||||
|
|
||||||
|
## Mer
|
||||||
|
- Plan & designbeslut: [`doc/plan.md`](doc/plan.md).
|
||||||
|
- Arkitektur / hur den funkar: repots **wiki**.
|
||||||
34
doc/plan.md
Normal file
34
doc/plan.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Tdarr-tray — plan
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
En systray-applet på workstationen som visar om Tdarr-noden (den som kör NVENC på
|
||||||
|
4060 Ti) transkoderar, och låter dig pausa/återuppta den med ett klick — så du
|
||||||
|
snabbt kan frigöra GPU:n innan du spelar och återuppta efteråt.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
- Python 3 + **PyQt6** (`QSystemTrayIcon`). KDE/Plasma, Wayland.
|
||||||
|
- Pratar med Tdarrs API (`x-api-key`), samma auth som tdarr-cli.
|
||||||
|
|
||||||
|
## Deploy target
|
||||||
|
Pattern A (installerbar). `install.sh` → `~/.local/bin/tdarr-tray` + autostart
|
||||||
|
(`~/.config/autostart/`). Release-CI publicerar `tdarr-tray.py` + `install.sh` till
|
||||||
|
rullande `latest`; en-rads `curl … | sh`.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
- Pollar `GET /api/v2/get-nodes` var 5:e sekund, matchar noden på **namn** (id
|
||||||
|
ändras vid varje nod-restart).
|
||||||
|
- Läge → ikonfärg: körs (grön) / körs inte = pausad|offline (röd) / övergång (gul).
|
||||||
|
- Aktiva transkodningar = `workers` med `idle:false`.
|
||||||
|
- Paus/återuppta: `POST /api/v2/update-node {nodeUpdates:{nodePaused:…}}` (ingen sudo;
|
||||||
|
paus dödar ffmpeg → encoder 0 %).
|
||||||
|
|
||||||
|
## Roadmap / expansions
|
||||||
|
- Notis (ntfy/knotify) när kön är klar eller noden felar.
|
||||||
|
- Visa kö-längd / sparat diskutrymme i tooltip.
|
||||||
|
- Snabbval: pausa i X minuter, eller auto-paus när ett spel körs (t.ex. via
|
||||||
|
gamemode/process-watch).
|
||||||
|
- Stöd att styra flera noder (inte bara workstationen).
|
||||||
|
|
||||||
|
## Open questions
|
||||||
|
- Ska "stäng av" även kunna stoppa container (kräver docker/sudo) eller räcker
|
||||||
|
paus (frigör GPU:n redan)? Nuläge: paus räcker.
|
||||||
33
install.sh
Executable file
33
install.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Installerar tray-appen till ~/.local/bin + autostart, och startar den.
|
||||||
|
# Från clone: ./install.sh
|
||||||
|
# En rad: curl -fsSL https://gitea.brasse-pc.eu/brasse/Tdarr-tray/raw/branch/master/install.sh | sh
|
||||||
|
set -eu
|
||||||
|
REPO="https://gitea.brasse-pc.eu/brasse/Tdarr-tray"
|
||||||
|
BIN="$HOME/.local/bin/tdarr-tray"
|
||||||
|
DESKTOP="$HOME/.config/autostart/tdarr-tray.desktop"
|
||||||
|
mkdir -p "$HOME/.local/bin" "$HOME/.config/autostart"
|
||||||
|
SRC="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
|
||||||
|
if [ -n "${SRC:-}" ] && [ -f "$SRC/tdarr-tray.py" ]; then
|
||||||
|
install -m 0755 "$SRC/tdarr-tray.py" "$BIN"
|
||||||
|
else
|
||||||
|
if command -v curl >/dev/null 2>&1; then curl -fsSL -o "$BIN" "$REPO/releases/download/latest/tdarr-tray.py"; else wget -qO "$BIN" "$REPO/releases/download/latest/tdarr-tray.py"; fi
|
||||||
|
chmod +x "$BIN"
|
||||||
|
fi
|
||||||
|
cat > "$DESKTOP" <<DESK
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Tdarr Tray
|
||||||
|
Comment=Styr Tdarr workstation-noden från systemfältet
|
||||||
|
Exec=$BIN
|
||||||
|
Icon=applications-multimedia
|
||||||
|
Terminal=false
|
||||||
|
Categories=Utility;
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
DESK
|
||||||
|
pkill -f "$BIN" 2>/dev/null || true
|
||||||
|
sleep 1
|
||||||
|
nohup "$BIN" >/dev/null 2>&1 & disown 2>/dev/null || true
|
||||||
|
echo "Installerat: $BIN"
|
||||||
|
echo "Autostart: $DESKTOP"
|
||||||
|
echo "Startad (kolla systemfältet)."
|
||||||
191
tdarr-tray.py
Normal file
191
tdarr-tray.py
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""tdarr-tray — KDE/systray-applet för Tdarr workstation-noden.
|
||||||
|
|
||||||
|
Tray-ikonen visar nodens läge och du kan pausa/återuppta den (t.ex. innan du
|
||||||
|
spelar — paus frigör GPU:n direkt).
|
||||||
|
|
||||||
|
Färger:
|
||||||
|
🟢 grön = noden körs (aktiv/redo att transkoda)
|
||||||
|
🔴 röd = noden körs inte (pausad eller offline)
|
||||||
|
🟡 gul = håller på att starta/stänga ner (övergång)
|
||||||
|
|
||||||
|
Konfig (env, med rimliga standardvärden):
|
||||||
|
TDARR_URL standard http://192.168.0.19:8266
|
||||||
|
TDARR_NODE nodnamn, standard Workstation-4060Ti
|
||||||
|
TDARR_API_KEY annars läses apiKey=... ur ~/tdarr-node/.env
|
||||||
|
|
||||||
|
`tdarr-tray.py --check` = headless statuskoll (ingen GUI).
|
||||||
|
"""
|
||||||
|
import sys, os, json, urllib.request
|
||||||
|
|
||||||
|
API = os.environ.get("TDARR_URL", "http://192.168.0.19:8266").rstrip("/")
|
||||||
|
NODE_NAME = os.environ.get("TDARR_NODE", "Workstation-4060Ti")
|
||||||
|
POLL_MS = int(os.environ.get("TDARR_POLL_MS", "5000"))
|
||||||
|
|
||||||
|
|
||||||
|
def load_key():
|
||||||
|
k = os.environ.get("TDARR_API_KEY")
|
||||||
|
if k:
|
||||||
|
return k.strip()
|
||||||
|
p = os.path.expanduser("~/tdarr-node/.env")
|
||||||
|
if os.path.exists(p):
|
||||||
|
for line in open(p):
|
||||||
|
if line.strip().startswith("apiKey="):
|
||||||
|
return line.split("=", 1)[1].strip()
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
KEY = load_key()
|
||||||
|
|
||||||
|
|
||||||
|
def api(method, path, body=None):
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
req = urllib.request.Request(API + path, data=data, method=method,
|
||||||
|
headers={"x-api-key": KEY, "Content-Type": "application/json"})
|
||||||
|
with urllib.request.urlopen(req, timeout=8) as r:
|
||||||
|
raw = r.read().decode()
|
||||||
|
try:
|
||||||
|
return json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def get_node():
|
||||||
|
"""Nod-objektet (med _id) eller None om offline/saknas."""
|
||||||
|
try:
|
||||||
|
d = api("GET", "/api/v2/get-nodes")
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return None
|
||||||
|
for k, n in d.items():
|
||||||
|
if n.get("nodeName") == NODE_NAME:
|
||||||
|
n["_id"] = k
|
||||||
|
return n
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def set_paused(node_id, paused):
|
||||||
|
api("POST", "/api/v2/update-node",
|
||||||
|
{"data": {"nodeID": node_id, "nodeUpdates": {"nodePaused": bool(paused)}}})
|
||||||
|
|
||||||
|
|
||||||
|
def node_detail(n):
|
||||||
|
"""Detaljtext för status/tooltip."""
|
||||||
|
if n is None:
|
||||||
|
return "Offline"
|
||||||
|
if n.get("nodePaused"):
|
||||||
|
return "Pausad"
|
||||||
|
workers = n.get("workers") or {}
|
||||||
|
active = [w for w in workers.values() if not w.get("idle")]
|
||||||
|
return f"Transkoderar {len(active)} fil(er)" if active else "Ledig (väntar på jobb)"
|
||||||
|
|
||||||
|
|
||||||
|
def is_running(n):
|
||||||
|
"""True om noden körs (ansluten + ej pausad)."""
|
||||||
|
return n is not None and not n.get("nodePaused")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- headless-läge ----------------
|
||||||
|
def check():
|
||||||
|
n = get_node()
|
||||||
|
print(f"{NODE_NAME}: {'RUNNING' if is_running(n) else 'STOPPED'} — {node_detail(n)}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------- GUI ----------------
|
||||||
|
def run_gui():
|
||||||
|
from PyQt6.QtWidgets import QApplication, QSystemTrayIcon, QMenu
|
||||||
|
from PyQt6.QtGui import QIcon, QPixmap, QPainter, QColor, QAction, QPolygonF
|
||||||
|
from PyQt6.QtCore import QTimer, Qt, QPointF, QRectF
|
||||||
|
|
||||||
|
COLORS = {"running": "#2ecc40", "stopped": "#ff4136", "transition": "#ffca28"}
|
||||||
|
|
||||||
|
def make_icon(state):
|
||||||
|
pm = QPixmap(64, 64)
|
||||||
|
pm.fill(Qt.GlobalColor.transparent)
|
||||||
|
p = QPainter(pm)
|
||||||
|
p.setRenderHint(QPainter.RenderHint.Antialiasing)
|
||||||
|
# färgad badge
|
||||||
|
p.setBrush(QColor(COLORS[state]))
|
||||||
|
p.setPen(Qt.PenStyle.NoPen)
|
||||||
|
p.drawEllipse(4, 4, 56, 56)
|
||||||
|
# vit symbol
|
||||||
|
p.setBrush(QColor("white"))
|
||||||
|
if state == "running": # play-triangel ▶
|
||||||
|
tri = QPolygonF([QPointF(26, 20), QPointF(26, 44), QPointF(46, 32)])
|
||||||
|
p.drawPolygon(tri)
|
||||||
|
elif state == "stopped": # paus-staplar ❚❚
|
||||||
|
p.drawRoundedRect(QRectF(23, 21, 7, 22), 2, 2)
|
||||||
|
p.drawRoundedRect(QRectF(34, 21, 7, 22), 2, 2)
|
||||||
|
else: # övergång: tre prickar …
|
||||||
|
for cx in (22, 32, 42):
|
||||||
|
p.drawEllipse(QPointF(cx, 32), 3.2, 3.2)
|
||||||
|
p.end()
|
||||||
|
return QIcon(pm)
|
||||||
|
|
||||||
|
class Tray:
|
||||||
|
def __init__(self, app):
|
||||||
|
self.app = app
|
||||||
|
self.node = None
|
||||||
|
self.pending = None # måltillstånd (paused True/False) under övergång
|
||||||
|
self.icons = {k: make_icon(k) for k in COLORS}
|
||||||
|
self.tray = QSystemTrayIcon()
|
||||||
|
self.menu = QMenu()
|
||||||
|
self.status_act = QAction("…"); self.status_act.setEnabled(False)
|
||||||
|
self.toggle_act = QAction("Pausa nod"); self.toggle_act.triggered.connect(self.toggle)
|
||||||
|
self.refresh_act = QAction("Uppdatera nu"); self.refresh_act.triggered.connect(self.refresh)
|
||||||
|
self.open_act = QAction("Öppna Tdarr i webbläsaren")
|
||||||
|
self.open_act.triggered.connect(lambda: os.system("xdg-open https://tdarr.brasse-pc.eu >/dev/null 2>&1 &"))
|
||||||
|
self.quit_act = QAction("Avsluta"); self.quit_act.triggered.connect(app.quit)
|
||||||
|
for a in [self.status_act, None, self.toggle_act, self.refresh_act, self.open_act, None, self.quit_act]:
|
||||||
|
self.menu.addSeparator() if a is None else self.menu.addAction(a)
|
||||||
|
self.tray.setContextMenu(self.menu)
|
||||||
|
self.tray.activated.connect(lambda *_: self.refresh())
|
||||||
|
self.tray.setIcon(self.icons["transition"])
|
||||||
|
self.tray.show()
|
||||||
|
self.timer = QTimer(); self.timer.timeout.connect(self.refresh); self.timer.start(POLL_MS)
|
||||||
|
self.refresh()
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
self.node = get_node()
|
||||||
|
detail = node_detail(self.node)
|
||||||
|
# övergång klar när verkligt läge når målet
|
||||||
|
if self.pending is not None and self.node is not None and \
|
||||||
|
bool(self.node.get("nodePaused")) == self.pending:
|
||||||
|
self.pending = None
|
||||||
|
if self.pending is not None:
|
||||||
|
state = "transition"
|
||||||
|
else:
|
||||||
|
state = "running" if is_running(self.node) else "stopped"
|
||||||
|
self.tray.setIcon(self.icons[state])
|
||||||
|
self.status_act.setText(f"{NODE_NAME}: {detail}")
|
||||||
|
self.tray.setToolTip(f"Tdarr — {NODE_NAME}\n{detail}")
|
||||||
|
self.toggle_act.setEnabled(self.node is not None)
|
||||||
|
self.toggle_act.setText("▶ Återuppta nod" if (self.node and self.node.get("nodePaused")) else "⏸ Pausa nod")
|
||||||
|
|
||||||
|
def toggle(self):
|
||||||
|
if not self.node:
|
||||||
|
return
|
||||||
|
target = not self.node.get("nodePaused") # target = ny paused-status
|
||||||
|
self.pending = target
|
||||||
|
self.tray.setIcon(self.icons["transition"]) # gul direkt
|
||||||
|
try:
|
||||||
|
set_paused(self.node["_id"], target)
|
||||||
|
except Exception:
|
||||||
|
self.pending = None
|
||||||
|
QTimer.singleShot(700, self.refresh)
|
||||||
|
|
||||||
|
app = QApplication(sys.argv)
|
||||||
|
app.setQuitOnLastWindowClosed(False)
|
||||||
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
||||||
|
print("Ingen systray tillgänglig", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
Tray(app)
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if not KEY:
|
||||||
|
print("VARNING: ingen API-nyckel (TDARR_API_KEY eller ~/tdarr-node/.env)", file=sys.stderr)
|
||||||
|
sys.exit(check() if "--check" in sys.argv else run_gui())
|
||||||
Reference in New Issue
Block a user