Diagrams etc. (#5)
Co-authored-by: Gronod <gordon@i3omb.com> Co-authored-by: gitea-actions[bot] <gitea-actions[bot]@i3omb.com> Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -18,7 +18,7 @@ Comprehensive technical documentation covering the full architecture of the **so
|
||||
10. [Frontend Architecture](#10-frontend-architecture)
|
||||
11. [Configuration](#11-configuration)
|
||||
12. [Deployment](#12-deployment)
|
||||
13. [UML Diagrams (PlantUML)](#13-uml-diagrams-plantuml)
|
||||
13. [Diagrams (Mermaid)](#13-diagrams)
|
||||
|
||||
---
|
||||
|
||||
@@ -775,32 +775,618 @@ The `.gitea/workflows/` directory contains three pipeline definitions:
|
||||
| `build-image.yml` | Push to `main` / `develop` | Build and push Docker image to `docker.i3omb.com` |
|
||||
| `create-release.yml` | Tag push (`v*`) | Create a Gitea release |
|
||||
|
||||
> **Diagrams** are written in Mermaid and render natively in Gitea — no CI workflow required. See [Section 13](#13-diagrams).
|
||||
|
||||
---
|
||||
|
||||
## 13. UML Diagrams (PlantUML)
|
||||
## 13. Diagrams
|
||||
|
||||
All diagrams below are in PlantUML syntax. Render with any PlantUML tool or VS Code extension.
|
||||
All diagrams are written in [Mermaid](https://mermaid.js.org/) and render natively in Gitea and GitHub markdown.
|
||||
|
||||
### 13.1 Component Diagram
|
||||
|
||||
See [`diagrams/component.puml`](diagrams/component.puml)
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Browser
|
||||
html[index.html]
|
||||
appjs[app.js]
|
||||
css[style.css]
|
||||
html -->|loads| appjs
|
||||
html -->|loads| css
|
||||
end
|
||||
|
||||
### 13.2 Sequence Diagrams
|
||||
subgraph Express Server
|
||||
entry[index.js\nEntry Point]
|
||||
appfactory[app.js\ncreateApp factory]
|
||||
|
||||
- **Authentication**: [`diagrams/seq-auth.puml`](diagrams/seq-auth.puml)
|
||||
- **Dashboard Request**: [`diagrams/seq-dashboard.puml`](diagrams/seq-dashboard.puml)
|
||||
- **Polling Cycle**: [`diagrams/seq-polling.puml`](diagrams/seq-polling.puml)
|
||||
subgraph Middleware
|
||||
hm[helmet\nCSP nonce + HSTS]
|
||||
rl[express-rate-limit\nAPI + login]
|
||||
cp[cookie-parser\nsigned cookies]
|
||||
ej[express.json\n64kb limit]
|
||||
es[express.static]
|
||||
requireauth[requireAuth.js]
|
||||
verifycsrf[verifyCsrf.js\ndouble-submit]
|
||||
end
|
||||
|
||||
### 13.3 Class / Entity Diagrams
|
||||
subgraph Routes
|
||||
auth[auth.js\n/api/auth\npre-CSRF]
|
||||
dashboard[dashboard.js\n/api/dashboard\n+SSE /stream]
|
||||
emby_r[emby.js\n/api/emby]
|
||||
sab_r[sabnzbd.js\n/api/sabnzbd]
|
||||
sonarr_r[sonarr.js\n/api/sonarr]
|
||||
radarr_r[radarr.js\n/api/radarr]
|
||||
end
|
||||
|
||||
- **Server Classes**: [`diagrams/class-server.puml`](diagrams/class-server.puml)
|
||||
- **Data Model**: [`diagrams/class-data.puml`](diagrams/class-data.puml)
|
||||
subgraph Utilities
|
||||
poller[poller.js]
|
||||
cache[cache.js\nMemoryCache]
|
||||
config[config.js]
|
||||
qbt[qbittorrent.js\nQBittorrentClient]
|
||||
tokenstore[tokenStore.js\ntokens.json]
|
||||
sanitize[sanitizeError.js]
|
||||
logger[logger.js]
|
||||
end
|
||||
|
||||
### 13.4 State Diagrams
|
||||
entry --> appfactory
|
||||
entry --> es
|
||||
entry --> poller
|
||||
|
||||
- **Frontend UI States**: [`diagrams/state-ui.puml`](diagrams/state-ui.puml)
|
||||
- **Poller States**: [`diagrams/state-poller.puml`](diagrams/state-poller.puml)
|
||||
appfactory --> hm & rl & cp & ej
|
||||
appfactory -->|pre-CSRF| auth
|
||||
appfactory --> verifycsrf
|
||||
appfactory --> dashboard & emby_r & sab_r & sonarr_r & radarr_r
|
||||
|
||||
### 13.5 Activity Diagram
|
||||
dashboard & emby_r & sab_r & sonarr_r & radarr_r --> requireauth
|
||||
auth --> tokenstore
|
||||
dashboard --> cache & poller & config & qbt
|
||||
poller --> cache & config & qbt & logger
|
||||
qbt --> config & logger
|
||||
auth & dashboard -.-> sanitize
|
||||
end
|
||||
|
||||
- **Download Matching**: [`diagrams/activity-matching.puml`](diagrams/activity-matching.puml)
|
||||
subgraph External Services
|
||||
emby[Emby / Jellyfin]
|
||||
sab[SABnzbd]
|
||||
sonarr[Sonarr]
|
||||
radarr[Radarr]
|
||||
qbit[qBittorrent]
|
||||
end
|
||||
|
||||
auth --> emby
|
||||
dashboard --> emby
|
||||
poller --> sab & sonarr & radarr
|
||||
qbt --> qbit
|
||||
emby_r --> emby
|
||||
sab_r --> sab
|
||||
sonarr_r --> sonarr
|
||||
radarr_r --> radarr
|
||||
|
||||
appjs -->|POST /login\nGET /me\nGET /csrf\nPOST /logout| auth
|
||||
appjs -->|GET /stream SSE\nGET /user-downloads\nGET /status| dashboard
|
||||
es -->|serve static| html
|
||||
```
|
||||
|
||||
### 13.2 Authentication Sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Browser as Browser (app.js)
|
||||
participant Auth as Express /api/auth
|
||||
participant Tokens as TokenStore (tokens.json)
|
||||
participant Emby as Emby Server
|
||||
|
||||
rect rgb(240,240,255)
|
||||
Note over Browser,Auth: Page Load
|
||||
Browser->>Auth: GET /api/auth/me
|
||||
Auth->>Auth: Read emby_user cookie (signed if COOKIE_SECRET)
|
||||
alt Cookie valid
|
||||
Auth-->>Browser: { authenticated: true, user }
|
||||
Browser->>Auth: GET /api/auth/csrf
|
||||
Auth-->>Browser: { csrfToken } + Set csrf_token cookie
|
||||
Browser->>Browser: store csrfToken in memory
|
||||
Browser->>Browser: showDashboard() + startSSE()
|
||||
else No cookie / tampered
|
||||
Auth-->>Browser: { authenticated: false }
|
||||
Browser->>Browser: showLogin()
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(240,255,240)
|
||||
Note over Browser,Emby: Login
|
||||
User->>Browser: Enter credentials (+ rememberMe)
|
||||
Browser->>Auth: POST /api/auth/login
|
||||
Note right of Auth: Rate limit: max 10 failed\nattempts per IP / 15 min
|
||||
Auth->>Emby: POST /Users/authenticatebyname\nDeviceId = sha256(username)[0:16]
|
||||
alt Valid credentials
|
||||
Emby-->>Auth: { User.Id, AccessToken }
|
||||
Auth->>Emby: GET /Users/{id}
|
||||
Emby-->>Auth: { Name, Policy.IsAdministrator }
|
||||
Auth->>Tokens: storeToken(userId, AccessToken)
|
||||
Note right of Tokens: Server-side only\n31-day TTL, atomic write
|
||||
Auth->>Auth: Set emby_user cookie\nhttpOnly, sameSite=strict\nsecure (if TRUST_PROXY)\nrememberMe → Max-Age 30d
|
||||
Auth->>Auth: Set csrf_token cookie\nhttpOnly=false, sameSite=strict
|
||||
Auth-->>Browser: { success: true, user, csrfToken }
|
||||
Browser->>Browser: showDashboard() + startSSE()
|
||||
else Invalid credentials
|
||||
Emby-->>Auth: 401
|
||||
Auth-->>Browser: { success: false, error }
|
||||
end
|
||||
end
|
||||
|
||||
rect rgb(255,245,230)
|
||||
Note over Browser,Auth: Logout
|
||||
User->>Browser: Click Logout
|
||||
Browser->>Browser: stopSSE()
|
||||
Browser->>Auth: POST /api/auth/logout
|
||||
Auth->>Tokens: getToken(userId)
|
||||
Tokens-->>Auth: { accessToken }
|
||||
Auth->>Emby: POST /Sessions/Logout
|
||||
Auth->>Tokens: clearToken(userId)
|
||||
Auth->>Auth: clearCookie(emby_user, csrf_token)
|
||||
Auth-->>Browser: { success: true }
|
||||
Browser->>Browser: showLogin()
|
||||
end
|
||||
```
|
||||
|
||||
### 13.3 Dashboard SSE Stream Sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor User
|
||||
participant Browser as Browser (app.js)
|
||||
participant Dashboard as Express /api/dashboard
|
||||
participant Cache as MemoryCache
|
||||
participant Poller
|
||||
participant Ext as External Services
|
||||
|
||||
User->>Browser: Login success / valid session
|
||||
Browser->>Dashboard: GET /api/dashboard/stream (EventSource)
|
||||
Dashboard->>Dashboard: requireAuth: extract user/isAdmin
|
||||
Dashboard->>Dashboard: Set Content-Type: text/event-stream\nRegister in activeClients
|
||||
|
||||
opt Polling disabled AND cache empty
|
||||
Dashboard->>Poller: pollAllServices()
|
||||
Poller->>Ext: Parallel API calls
|
||||
Ext-->>Poller: Raw data
|
||||
Poller->>Cache: set poll:* keys (TTL=30s)
|
||||
end
|
||||
|
||||
Dashboard->>Cache: get all poll:* keys
|
||||
Dashboard->>Dashboard: Build maps, match downloads\nextractUserTag / buildTagBadges
|
||||
Dashboard-->>Browser: data: { user, isAdmin, downloads }
|
||||
Browser->>Browser: hideLoading() + renderDownloads()
|
||||
|
||||
loop Every poll cycle
|
||||
Poller->>Poller: pollAllServices() complete
|
||||
Poller->>Dashboard: onPollComplete callback fires
|
||||
Dashboard->>Cache: get all poll:* keys
|
||||
Dashboard->>Dashboard: Rebuild payload
|
||||
Dashboard-->>Browser: data: { user, isAdmin, downloads }
|
||||
Browser->>Browser: renderDownloads() diff-based
|
||||
end
|
||||
|
||||
Note over Dashboard,Browser: : heartbeat every 25s keeps connection alive
|
||||
|
||||
User->>Browser: Close tab / logout
|
||||
Browser->>Dashboard: TCP close (req close event)
|
||||
Dashboard->>Dashboard: offPollComplete(cb)\nclearInterval(heartbeat)\ndelete activeClients[key]
|
||||
```
|
||||
|
||||
### 13.4 Background Polling Cycle
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Entry as index.js (startup)
|
||||
participant Poller
|
||||
participant Config
|
||||
participant SAB as SABnzbd (per instance)
|
||||
participant Sonarr as Sonarr (per instance)
|
||||
participant Radarr as Radarr (per instance)
|
||||
participant QBT as qBittorrent Client
|
||||
participant Cache as MemoryCache
|
||||
|
||||
Entry->>Poller: startPoller()
|
||||
alt POLL_INTERVAL > 0
|
||||
Poller->>Poller: pollAllServices() immediate
|
||||
Poller->>Poller: setInterval(pollAllServices, POLL_INTERVAL)
|
||||
else POLL_INTERVAL = 0
|
||||
Poller-->>Entry: on-demand mode
|
||||
end
|
||||
|
||||
Note over Poller: Each poll cycle
|
||||
Poller->>Poller: polling flag check (skip if concurrent)
|
||||
Poller->>Poller: polling = true
|
||||
|
||||
Poller->>Config: getSABnzbdInstances() / getSonarrInstances() / getRadarrInstances()
|
||||
Config-->>Poller: instance configs
|
||||
|
||||
Note over Poller,Cache: All 9 fetches run in parallel via Promise.all, each wrapped in timed()
|
||||
|
||||
Poller->>SAB: GET /api?mode=queue
|
||||
SAB-->>Poller: { queue: { slots, status, speed } }
|
||||
Poller->>SAB: GET /api?mode=history&limit=10
|
||||
SAB-->>Poller: { history: { slots } }
|
||||
Poller->>Sonarr: GET /api/v3/tag + queue + history
|
||||
Sonarr-->>Poller: tags, queue records (includeSeries), history
|
||||
Poller->>Radarr: GET /api/v3/tag + queue + history
|
||||
Radarr-->>Poller: tags, queue records (includeMovie), history
|
||||
Poller->>QBT: getTorrents()
|
||||
QBT-->>Poller: [{ name, progress, ... }]
|
||||
|
||||
Poller->>Poller: Record per-task timings\nlastPollTimings = { totalMs, timestamp, tasks }
|
||||
Poller->>Cache: set poll:* keys (TTL = POLL_INTERVAL × 3)
|
||||
Poller->>Poller: Notify SSE subscribers (forEach cb())
|
||||
Poller->>Poller: polling = false
|
||||
```
|
||||
|
||||
### 13.5 Server Class Diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class EntryPoint["index.js (EntryPoint)"] {
|
||||
+startPoller()
|
||||
+app.listen()
|
||||
setupLogging()
|
||||
serveStatic()
|
||||
}
|
||||
class createApp["app.js (createApp factory)"] {
|
||||
+createApp(skipRateLimits?) Express
|
||||
mountHelmet()
|
||||
mountRateLimiters()
|
||||
mountRoutes()
|
||||
mountErrorHandler()
|
||||
}
|
||||
class AuthRouter["auth.js (Router)"] {
|
||||
+POST /login
|
||||
+GET /me
|
||||
+GET /csrf
|
||||
+POST /logout
|
||||
authenticateViaEmby()
|
||||
issueCookies()
|
||||
revokeToken()
|
||||
}
|
||||
class DashboardRouter["dashboard.js (Router)"] {
|
||||
-activeClients Map
|
||||
+GET /stream SSE
|
||||
+GET /user-downloads
|
||||
+GET /user-summary
|
||||
+GET /status
|
||||
+GET /cover-art
|
||||
buildDownloadPayload()
|
||||
extractUserTag()
|
||||
buildTagBadges()
|
||||
getEmbyUsers()
|
||||
}
|
||||
class RequireAuth["requireAuth.js (Middleware)"] {
|
||||
+requireAuth(req, res, next)
|
||||
readCookie()
|
||||
validateSchema()
|
||||
}
|
||||
class VerifyCsrf["verifyCsrf.js (Middleware)"] {
|
||||
+verifyCsrf(req, res, next)
|
||||
timingSafeEqual()
|
||||
}
|
||||
class MemoryCache {
|
||||
-store Map
|
||||
+get(key) any
|
||||
+set(key, value, ttlMs)
|
||||
+invalidate(key)
|
||||
+clear()
|
||||
+getStats() CacheStats
|
||||
}
|
||||
class Poller {
|
||||
-POLL_INTERVAL number
|
||||
-polling boolean
|
||||
-subscribers Set
|
||||
+startPoller()
|
||||
+stopPoller()
|
||||
+pollAllServices()
|
||||
+onPollComplete(cb)
|
||||
+offPollComplete(cb)
|
||||
+getLastPollTimings() PollTimings
|
||||
}
|
||||
class Config {
|
||||
+getSABnzbdInstances() Instance[]
|
||||
+getSonarrInstances() Instance[]
|
||||
+getRadarrInstances() Instance[]
|
||||
+getQbittorrentInstances() Instance[]
|
||||
}
|
||||
class QBittorrentClient {
|
||||
-url string
|
||||
-authCookie string
|
||||
+login() bool
|
||||
+getTorrents() Torrent[]
|
||||
+makeRequest(endpoint)
|
||||
}
|
||||
class TokenStore {
|
||||
-STORE_PATH string
|
||||
-TOKEN_TTL_MS 31days
|
||||
+storeToken(userId, token)
|
||||
+getToken(userId)
|
||||
+clearToken(userId)
|
||||
atomicWrite()
|
||||
pruneExpired()
|
||||
}
|
||||
class SanitizeError {
|
||||
+sanitizeError(err) string
|
||||
redactQueryParams()
|
||||
redactAuthHeaders()
|
||||
}
|
||||
|
||||
EntryPoint --> createApp : createApp()
|
||||
EntryPoint --> Poller : startPoller()
|
||||
createApp --> AuthRouter : mount pre-CSRF
|
||||
createApp --> VerifyCsrf : apply to /api
|
||||
createApp --> DashboardRouter
|
||||
DashboardRouter --> RequireAuth
|
||||
DashboardRouter --> MemoryCache
|
||||
DashboardRouter --> Poller
|
||||
DashboardRouter --> Config
|
||||
DashboardRouter ..> SanitizeError
|
||||
AuthRouter --> TokenStore
|
||||
AuthRouter ..> SanitizeError
|
||||
Poller --> MemoryCache
|
||||
Poller --> Config
|
||||
Poller --> QBittorrentClient
|
||||
QBittorrentClient --> Config
|
||||
```
|
||||
|
||||
### 13.6 Data Model Diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Download {
|
||||
+type series|movie|torrent
|
||||
+title string
|
||||
+coverArt string
|
||||
+status string
|
||||
+progress string
|
||||
+size string
|
||||
+mb string
|
||||
+mbmissing string
|
||||
+speed string
|
||||
+eta string
|
||||
+seriesName string
|
||||
+movieName string
|
||||
+allTags string[]
|
||||
+matchedUserTag string
|
||||
+tagBadges TagBadge[]
|
||||
+importIssues string[]
|
||||
+downloadPath string
|
||||
+targetPath string
|
||||
+arrLink string
|
||||
+seeds number
|
||||
+peers number
|
||||
+availability string
|
||||
+hash string
|
||||
+completedAt string
|
||||
}
|
||||
class TagBadge {
|
||||
+label string
|
||||
+matchedUser string
|
||||
}
|
||||
class APIResponse {
|
||||
+user string
|
||||
+isAdmin boolean
|
||||
+downloads Download[]
|
||||
}
|
||||
class SSEEvent {
|
||||
+user string
|
||||
+isAdmin boolean
|
||||
+downloads Download[]
|
||||
}
|
||||
class StatusResponse {
|
||||
+server ServerInfo
|
||||
+polling PollingInfo
|
||||
+cache CacheStats
|
||||
+clients ClientInfo[]
|
||||
}
|
||||
class SessionCookie {
|
||||
+id string
|
||||
+name string
|
||||
+isAdmin boolean
|
||||
}
|
||||
class SABnzbdQueueSlot {
|
||||
+filename string
|
||||
+percentage string
|
||||
+mb string
|
||||
+mbmissing string
|
||||
+timeleft string
|
||||
+status string
|
||||
}
|
||||
class qBittorrentTorrent {
|
||||
+name string
|
||||
+hash string
|
||||
+progress float
|
||||
+state string
|
||||
+dlspeed number
|
||||
+eta number
|
||||
+num_seeds number
|
||||
+num_leechs number
|
||||
+availability number
|
||||
}
|
||||
class SonarrQueueRecord {
|
||||
+seriesId number
|
||||
+series SonarrSeries
|
||||
+title string
|
||||
+trackedDownloadStatus string
|
||||
+statusMessages StatusMessage[]
|
||||
}
|
||||
class RadarrQueueRecord {
|
||||
+movieId number
|
||||
+movie RadarrMovie
|
||||
+title string
|
||||
+trackedDownloadStatus string
|
||||
+statusMessages StatusMessage[]
|
||||
}
|
||||
|
||||
APIResponse "1" *-- "many" Download
|
||||
SSEEvent "1" *-- "many" Download
|
||||
Download "1" *-- "many" TagBadge
|
||||
SABnzbdQueueSlot ..> Download : matched and transformed
|
||||
qBittorrentTorrent ..> Download : mapTorrentToDownload()
|
||||
SonarrQueueRecord ..> Download : coverArt, seriesName, tags
|
||||
RadarrQueueRecord ..> Download : coverArt, movieName, tags
|
||||
```
|
||||
|
||||
### 13.7 Frontend UI State Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> SplashScreen : Page load
|
||||
|
||||
SplashScreen --> CheckAuth : checkAuthentication()
|
||||
|
||||
state CheckAuth <<choice>>
|
||||
CheckAuth --> LoginForm : No session
|
||||
CheckAuth --> Dashboard : Valid session
|
||||
|
||||
state LoginForm {
|
||||
[*] --> Idle
|
||||
Idle --> Submitting : Submit form
|
||||
Submitting --> Error : Auth failed
|
||||
Error --> Submitting : Re-submit
|
||||
Submitting --> [*] : Auth success
|
||||
}
|
||||
|
||||
LoginForm --> Dashboard : Auth success\n(fade transition)
|
||||
|
||||
state Dashboard {
|
||||
[*] --> Rendering
|
||||
Rendering --> Rendering : SSE message → renderDownloads()
|
||||
Rendering --> Rendering : Theme change
|
||||
|
||||
state SSEConnection {
|
||||
[*] --> Connecting
|
||||
Connecting --> Connected : First message
|
||||
Connected --> Reconnecting : Connection lost
|
||||
Reconnecting --> Connected : Auto-reconnect
|
||||
Connected --> Connecting : showAll toggled
|
||||
}
|
||||
|
||||
state StatusPanel {
|
||||
[*] --> Closed
|
||||
Closed --> Open : Click Status (admin)
|
||||
Open --> Closed : Click close
|
||||
Open --> Open : 5s timer refresh
|
||||
}
|
||||
}
|
||||
|
||||
Dashboard --> LoginForm : Logout (stopSSE)
|
||||
```
|
||||
|
||||
### 13.8 Poller State Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> CheckConfig : startPoller()
|
||||
|
||||
state CheckConfig <<choice>>
|
||||
CheckConfig --> Disabled : POLL_INTERVAL = 0
|
||||
CheckConfig --> Idle : POLL_INTERVAL > 0
|
||||
|
||||
state Disabled {
|
||||
[*] --> OnDemand
|
||||
OnDemand : No background timer.\nData fetched when dashboard\nrequest finds empty cache.
|
||||
}
|
||||
|
||||
Disabled --> Polling : dashboard triggers pollAllServices()
|
||||
Polling --> Disabled : Poll complete (on-demand)
|
||||
|
||||
Idle --> Polling : setInterval fires\nor immediate first poll
|
||||
|
||||
state Polling {
|
||||
[*] --> Locked
|
||||
Locked : polling = true
|
||||
Locked --> Fetching
|
||||
Fetching --> Storing : All promises resolved
|
||||
Fetching --> HandleError : Per-service error (caught)
|
||||
Storing --> Notifying : Cache updated\nTTL = POLL_INTERVAL × 3
|
||||
Notifying : Notify SSE subscribers
|
||||
Notifying --> Done
|
||||
Done : polling = false
|
||||
Done --> [*]
|
||||
}
|
||||
|
||||
state HandleError {
|
||||
[*] --> LogError
|
||||
LogError : Log error, polling = false
|
||||
}
|
||||
|
||||
Polling --> Idle : Poll complete
|
||||
HandleError --> Idle : Next interval
|
||||
|
||||
state ConcurrentSkip {
|
||||
[*] --> Skip
|
||||
Skip : polling === true, skip cycle
|
||||
}
|
||||
Idle --> ConcurrentSkip : Interval fires while\nprevious still running
|
||||
ConcurrentSkip --> Idle : Log skip
|
||||
```
|
||||
|
||||
### 13.9 Download Matching Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([Start: user request]) --> B[Read all poll:* keys from MemoryCache]
|
||||
B --> C[Build seriesMap, moviesMap\nsonarrTagMap, radarrTagMap]
|
||||
C --> D{showAll?}
|
||||
D -->|yes| E[Fetch Emby user list\ncached 60s → embyUserMap]
|
||||
D -->|no| F
|
||||
E --> F[userDownloads = empty array]
|
||||
|
||||
F --> G[/SABnzbd queue slots/]
|
||||
G --> H{Matches Sonarr queue?}
|
||||
H -->|yes| I[Resolve series\nextractAllTags + extractUserTag]
|
||||
I --> J{showAll + anyTag\nor matchedUserTag?}
|
||||
J -->|yes| K[Build Download object\nAdd tagBadges if showAll\nAdd importIssues, admin fields]
|
||||
K --> L[Push to userDownloads]
|
||||
H --> M{Matches Radarr queue?}
|
||||
M -->|yes| N[Resolve movie\nextractAllTags + extractUserTag]
|
||||
N --> J
|
||||
|
||||
L --> G
|
||||
|
||||
G --> O[/SABnzbd history slots/]
|
||||
O --> P{Matches Sonarr history?}
|
||||
P -->|yes| Q[Resolve series\nBuild Download type=series\nAdd completedAt]
|
||||
Q --> R{showAll+anyTag\nor matchedUserTag?}
|
||||
R -->|yes| S[Push to userDownloads]
|
||||
P --> T{Matches Radarr history?}
|
||||
T -->|yes| U[Resolve movie\nBuild Download type=movie\nAdd completedAt]
|
||||
U --> R
|
||||
|
||||
S --> O
|
||||
|
||||
O --> V[/qBittorrent torrents/]
|
||||
V --> W{Matches Sonarr queue?}
|
||||
W -->|yes| X[mapTorrentToDownload\n+ enrich with series]
|
||||
X --> Y{Tag matches?}
|
||||
Y -->|yes| Z[Push to userDownloads]
|
||||
W --> AA{Matches Radarr queue?}
|
||||
AA -->|yes| AB[mapTorrentToDownload\n+ enrich with movie]
|
||||
AB --> Y
|
||||
AA --> AC{Matches Sonarr history?}
|
||||
AC -->|yes| AD[Resolve series via seriesMap]
|
||||
AD --> Y
|
||||
AC --> AE{Matches Radarr history?}
|
||||
AE -->|yes| AF[Resolve movie via moviesMap]
|
||||
AF --> Y
|
||||
AE -->|no| AG[Skip - unmatched torrent]
|
||||
|
||||
Z --> V
|
||||
AG --> V
|
||||
|
||||
V --> AH([Return JSON\nuser, isAdmin, downloads])
|
||||
|
||||
style K fill:#d4edda
|
||||
style Q fill:#d4edda
|
||||
style U fill:#d4edda
|
||||
style X fill:#d4edda
|
||||
style AB fill:#d4edda
|
||||
style AD fill:#d4edda
|
||||
style AF fill:#d4edda
|
||||
style AG fill:#f8d7da
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user