// Wire types for the Transmission RPC surface this sidecar uses. // // Field names are Transmission's OWN, verbatim — including its inconsistent casing (`percentDone` next to // `begin_piece`, `peer-limit`, `sequential_download`). That is deliberate: renaming them would mean every // future field needs a translation entry, and a name that doesn't appear in Transmission's rpc-spec.md is a // name you cannot grep for when something misbehaves. The only fields we invent are the `officer*` derived // ones at the bottom of Torrent, which are prefixed so they can never be mistaken for upstream data. /** Transmission's torrent status enum (rpc-spec.md §3.3). */ export const TorrentStatus = { Stopped: 0, CheckWait: 1, Check: 2, DownloadWait: 3, Download: 4, SeedWait: 5, Seed: 6, } as const; export type TrackerStat = { announce: string; announceState: number; downloadCount: number; hasAnnounced: boolean; hasScraped: boolean; host: string; id: number; isBackup: boolean; lastAnnouncePeerCount: number; lastAnnounceResult: string; lastAnnounceSucceeded: boolean; lastAnnounceTime: number; lastScrapeResult: string; lastScrapeSucceeded: boolean; lastScrapeTime: number; leecherCount: number; nextAnnounceTime: number; scrape: string; seederCount: number; tier: number; }; export type TorrentFile = { bytesCompleted: number; length: number; name: string; }; export type TorrentFileStat = { bytesCompleted: number; priority: number; wanted: boolean; }; export type TorrentPeer = { address: string; clientName: string; clientIsChoked: boolean; clientIsInterested: boolean; flagStr: string; isDownloadingFrom: boolean; isEncrypted: boolean; isIncoming: boolean; isUploadingTo: boolean; isUTP: boolean; peerIsChoked: boolean; peerIsInterested: boolean; port: number; progress: number; rateToClient: number; rateToPeer: number; }; export type PeersFrom = { fromCache: number; fromDht: number; fromIncoming: number; fromLpd: number; fromLtep: number; fromPex: number; fromTracker: number; }; export type Torrent = { id: number; name: string; status: number; totalSize: number; sizeWhenDone: number; leftUntilDone: number; haveValid: number; percentDone: number; metadataPercentComplete: number; recheckProgress?: number; downloadedEver: number; uploadedEver: number; corruptEver?: number; uploadRatio: number; rateDownload: number; rateUpload: number; eta: number; peersSendingToUs: number; peersGettingFromUs: number; addedDate: number; doneDate: number; activityDate: number; secondsSeeding: number; downloadDir: string; bandwidthPriority: number; queuePosition: number; isPrivate: boolean; labels: string[]; error: number; errorString: string; pieceCount: number; pieceSize: number; magnetLink: string; group: string; 'file-count': number; trackerStats: TrackerStat[]; trackerList?: string; // Per-torrent limits — the fields the "Other settings" dialog reads and writes. honorsSessionLimits: boolean; downloadLimited: boolean; downloadLimit: number; uploadLimited: boolean; uploadLimit: number; 'peer-limit': number; seedRatioMode: number; seedRatioLimit: number; seedIdleMode: number; seedIdleLimit: number; sequential_download?: boolean; // Detail-only fields — present when fetched through /_officer/torrents/:id. hashString?: string; comment?: string; creator?: string; dateCreated?: number; maxConnectedPeers?: number; files?: TorrentFile[]; fileStats?: TorrentFileStat[]; peers?: TorrentPeer[]; peersFrom?: PeersFrom; // ── Derived by the sidecar, never sent by Transmission ── /** * The primary tracker's host with its port and any ignorable subdomain prefix stripped * (`udp://tr.example.org:1337/announce` → `example.org`). The UI groups by this, so the normalisation * has to be one implementation, not one per caller. */ officerTrackerHost: string; /** Human-readable announce state of the primary tracker, for the "Tracker Status" column. */ officerTrackerStatus: string; /** Count of trackers that have reported an error, so the UI can flag a torrent without walking the array. */ officerTrackerErrors: number; }; export type SessionSettings = Record & { version: string; 'rpc-version': number; 'download-dir': string; }; export type SessionStats = { activeTorrentCount: number; pausedTorrentCount: number; torrentCount: number; downloadSpeed: number; uploadSpeed: number; 'cumulative-stats': StatsBucket; 'current-stats': StatsBucket; }; export type StatsBucket = { downloadedBytes: number; uploadedBytes: number; filesAdded: number; sessionCount: number; secondsActive: number; }; /** The fields the torrent list needs. Mirrors what the list UI actually renders — nothing speculative. */ export const LIST_FIELDS = [ 'activityDate', 'addedDate', 'bandwidthPriority', 'corruptEver', 'doneDate', 'downloadDir', 'downloadedEver', 'downloadLimit', 'downloadLimited', 'error', 'errorString', 'eta', 'file-count', 'group', 'haveValid', 'honorsSessionLimits', 'id', 'isPrivate', 'labels', 'leftUntilDone', 'magnetLink', 'metadataPercentComplete', 'name', 'peer-limit', 'peersGettingFromUs', 'peersSendingToUs', 'percentDone', 'pieceCount', 'pieceSize', 'queuePosition', 'rateDownload', 'rateUpload', 'secondsSeeding', 'seedIdleLimit', 'seedIdleMode', 'seedRatioLimit', 'seedRatioMode', 'sequential_download', 'sizeWhenDone', 'status', 'totalSize', 'trackerList', 'trackerStats', 'uploadedEver', 'uploadLimit', 'uploadLimited', 'uploadRatio', ] as const; /** Extra fields only the detail pane needs — big arrays we refuse to pull for every row on every poll. */ export const DETAIL_FIELDS = [ 'comment', 'creator', 'dateCreated', 'fileStats', 'files', 'hashString', 'maxConnectedPeers', 'peers', 'peersFrom', 'recheckProgress', ] as const;