All checks were successful
Build and Push Docker Image / build (push) Successful in 23s
- docs/ARCHITECTURE.md: full system overview, technology stack, directory structure, component architecture, data flow, auth, polling/caching, download matching pipeline, API reference, frontend architecture, configuration, deployment guide - docs/diagrams/component.puml: system component diagram - docs/diagrams/seq-auth.puml: authentication sequence diagram - docs/diagrams/seq-dashboard.puml: dashboard request sequence diagram - docs/diagrams/seq-polling.puml: background polling cycle sequence - docs/diagrams/class-server.puml: server-side class/module diagram - docs/diagrams/class-data.puml: data model / entity diagram - docs/diagrams/state-ui.puml: frontend UI state diagram - docs/diagrams/state-poller.puml: poller state diagram - docs/diagrams/activity-matching.puml: download matching activity diagram
198 lines
4.4 KiB
Plaintext
198 lines
4.4 KiB
Plaintext
@startuml class-server
|
|
!theme plain
|
|
title sofarr — Server Class / Module Diagram
|
|
|
|
package "server/index.js" as entry {
|
|
class "EntryPoint" as ep <<module>> {
|
|
- LOG_LEVELS : Object
|
|
- currentLevel : number
|
|
- logFile : WriteStream
|
|
+ shouldLog(level) : boolean
|
|
--
|
|
Configures Express app,
|
|
mounts routes, starts poller
|
|
}
|
|
}
|
|
|
|
package "server/routes" {
|
|
class "auth.js" as auth <<router>> {
|
|
+ POST /login
|
|
+ GET /me
|
|
+ POST /logout
|
|
--
|
|
Authenticates via Emby API
|
|
Sets/reads httpOnly cookie
|
|
}
|
|
|
|
class "dashboard.js" as dashboard <<router>> {
|
|
- activeClients : Map<string, ClientInfo>
|
|
- CLIENT_STALE_MS : 30000
|
|
--
|
|
+ GET /user-downloads
|
|
+ GET /user-summary
|
|
+ GET /status
|
|
--
|
|
- getCoverArt(item) : string|null
|
|
- extractUserTag(tags, tagMap) : string|null
|
|
- sanitizeTagLabel(input) : string
|
|
- tagMatchesUser(tag, username) : boolean
|
|
- getImportIssues(record) : string[]|null
|
|
- getSonarrLink(series) : string|null
|
|
- getRadarrLink(movie) : string|null
|
|
- getActiveClients() : ClientInfo[]
|
|
}
|
|
|
|
class "emby.js" as emby_r <<router>> {
|
|
+ GET /sessions
|
|
+ GET /users/:id
|
|
+ GET /users
|
|
+ GET /session/:sessionId/user
|
|
}
|
|
|
|
class "sabnzbd.js" as sab_r <<router>> {
|
|
+ GET /queue
|
|
+ GET /history
|
|
}
|
|
|
|
class "sonarr.js" as sonarr_r <<router>> {
|
|
+ GET /queue
|
|
+ GET /history
|
|
+ GET /series/:id
|
|
+ GET /series
|
|
}
|
|
|
|
class "radarr.js" as radarr_r <<router>> {
|
|
+ GET /queue
|
|
+ GET /history
|
|
+ GET /movies/:id
|
|
+ GET /movies
|
|
}
|
|
}
|
|
|
|
package "server/utils" {
|
|
class "MemoryCache" as cache {
|
|
- store : Map<string, CacheEntry>
|
|
+ get(key) : any|null
|
|
+ set(key, value, ttlMs) : void
|
|
+ invalidate(key) : void
|
|
+ clear() : void
|
|
+ getStats() : CacheStats
|
|
}
|
|
|
|
class "CacheEntry" as ce <<value>> {
|
|
+ value : any
|
|
+ expiresAt : number
|
|
}
|
|
|
|
class "CacheStats" as cs <<value>> {
|
|
+ entryCount : number
|
|
+ totalSizeBytes : number
|
|
+ entries : CacheEntryStats[]
|
|
}
|
|
|
|
class "Poller" as poller <<module>> {
|
|
- POLL_INTERVAL : number
|
|
- POLLING_ENABLED : boolean
|
|
- polling : boolean
|
|
- lastPollTimings : PollTimings|null
|
|
- intervalHandle : number|null
|
|
--
|
|
+ startPoller() : void
|
|
+ stopPoller() : void
|
|
+ pollAllServices() : Promise<void>
|
|
+ getLastPollTimings() : PollTimings|null
|
|
--
|
|
- timed(label, fn) : TimedResult
|
|
}
|
|
|
|
class "PollTimings" as pt <<value>> {
|
|
+ totalMs : number
|
|
+ timestamp : string (ISO)
|
|
+ tasks : { label, ms }[]
|
|
}
|
|
|
|
class "Config" as config <<module>> {
|
|
+ getSABnzbdInstances() : Instance[]
|
|
+ getSonarrInstances() : Instance[]
|
|
+ getRadarrInstances() : Instance[]
|
|
+ getQbittorrentInstances() : Instance[]
|
|
--
|
|
- parseInstances(envVar, ...) : Instance[]
|
|
}
|
|
|
|
class "Instance" as inst <<value>> {
|
|
+ id : string
|
|
+ name : string
|
|
+ url : string
|
|
+ apiKey : string
|
|
+ username? : string
|
|
+ password? : string
|
|
}
|
|
|
|
class "QBittorrentClient" as qbt {
|
|
- id : string
|
|
- name : string
|
|
- url : string
|
|
- username : string
|
|
- password : string
|
|
- authCookie : string|null
|
|
--
|
|
+ login() : Promise<boolean>
|
|
+ makeRequest(endpoint, config) : Promise<Response>
|
|
+ getTorrents() : Promise<Torrent[]>
|
|
}
|
|
|
|
class "qbittorrent.js" as qbt_mod <<module>> {
|
|
- persistedClients : QBittorrentClient[]|null
|
|
--
|
|
+ getTorrents() : Promise<Torrent[]>
|
|
+ getClients() : QBittorrentClient[]
|
|
+ mapTorrentToDownload(torrent) : Download
|
|
+ formatBytes(bytes) : string
|
|
+ formatSpeed(bps) : string
|
|
+ formatEta(seconds) : string
|
|
}
|
|
|
|
class "Logger" as logger <<module>> {
|
|
- logFile : WriteStream
|
|
+ logToFile(message) : void
|
|
}
|
|
|
|
class "ClientInfo" as ci <<value>> {
|
|
+ user : string
|
|
+ refreshRateMs : number
|
|
+ lastSeen : number (timestamp)
|
|
}
|
|
}
|
|
|
|
' Relationships
|
|
ep --> auth
|
|
ep --> dashboard
|
|
ep --> emby_r
|
|
ep --> sab_r
|
|
ep --> sonarr_r
|
|
ep --> radarr_r
|
|
ep --> poller : startPoller()
|
|
|
|
dashboard --> cache : read/write
|
|
dashboard --> poller : pollAllServices()
|
|
dashboard --> qbt_mod : mapTorrentToDownload()
|
|
dashboard --> config
|
|
|
|
poller --> cache : set poll:* keys
|
|
poller --> config : get instances
|
|
poller --> qbt_mod : getTorrents()
|
|
|
|
qbt_mod --> config : getQbittorrentInstances()
|
|
qbt_mod *-- qbt : creates
|
|
qbt --> logger
|
|
|
|
cache *-- ce : stores
|
|
cache ..> cs : returns from getStats()
|
|
poller ..> pt : stores/returns
|
|
dashboard *-- ci : stores in activeClients
|
|
|
|
config ..> inst : returns
|
|
|
|
@enduml
|