// Shared types/constants for the /transmission workspace panels. The wire shapes mirror what the // officer-transmission sidecar returns under /api/transmission/_officer/* — which in turn keeps // Transmission's own field names, casing warts and all (`percentDone` beside `peer-limit` and // `sequential_download`). Renaming them here would mean this file and Transmission's rpc-spec.md disagree // on what a field is called, which is exactly the confusion you don't want at 2am. // // The `officer*` fields are the sidecar's own additions — see src/servers/sidecar/transmission/rpc.ts. export const TRANSMISSION_SECTIONS = [ { id: 'torrents', label: 'Torrents' }, { id: 'stats', label: 'Statistics' }, { id: 'settings', label: 'Settings' }, { id: 'connection', label: 'Connection' }, ] as const; export type TransmissionSectionId = (typeof TRANSMISSION_SECTIONS)[number]['id']; /** Where /transmission lands, and where an unrecognised section redirects to. */ export const DEFAULT_TRANSMISSION_SECTION: TransmissionSectionId = 'torrents'; export const isTransmissionSection = (value: string | undefined): value is TransmissionSectionId => TRANSMISSION_SECTIONS.some((s) => s.id === value); /** The one place the section URL is spelled, so the nav, the guard and any deep link cannot drift apart. */ export const transmissionSectionPath = (id: TransmissionSectionId) => `/transmission/${id}`; // ── Domain objects ──────────────────────────────────────────────────────────────────────────────── /** Transmission's torrent status enum (rpc-spec.md §3.3). */ export const STATUS = { 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; flagStr: string; isEncrypted: boolean; isIncoming: boolean; isUTP: 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; 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 — present when fetched through useTorrentDetail. hashString?: string; comment?: string; creator?: string; dateCreated?: number; maxConnectedPeers?: number; files?: TorrentFile[]; fileStats?: TorrentFileStat[]; peers?: TorrentPeer[]; peersFrom?: PeersFrom; // Derived by the sidecar. officerTrackerHost: string; officerTrackerStatus: string; officerTrackerErrors: number; }; export type SessionSettings = Record & { version: string; 'rpc-version': number; 'download-dir': string; }; export type StatsBucket = { downloadedBytes: number; uploadedBytes: number; filesAdded: number; sessionCount: number; secondsActive: number; }; export type SessionStats = { activeTorrentCount: number; pausedTorrentCount: number; torrentCount: number; downloadSpeed: number; uploadSpeed: number; 'cumulative-stats': StatsBucket; 'current-stats': StatsBucket; }; /** The ids the sidecar accepts at POST /_officer/torrents/action. */ export type TorrentAction = | 'start' | 'start-now' | 'stop' | 'verify' | 'reannounce' | 'queue-top' | 'queue-up' | 'queue-down' | 'queue-bottom'; export const PRIORITY = { Low: -1, Normal: 0, High: 1 } as const; /** Transmission's seedRatioMode / seedIdleMode share this vocabulary. */ export const LIMIT_MODE = { Global: 0, Single: 1, Unlimited: 2 } as const;