Documentation
¶
Overview ¶
Package torrent owns the anacrolix/torrent client lifecycle and the Session type that exposes it to the rest of haul.
⚠ Before changing anything in this file, run:
go test ./internal/core/torrent/... -run TestSessionIntegration_DownloadFromPeer
This test spins up a local seeder and verifies the Session can actually download through its configured peer-wire / DHT / IPBlocklist wiring. The "torrent stalls at 0 peers" bug has regressed three times — the test catches it in <1s. See haul/CLAUDE.md for the full list of files guarded by this regression suite.
Index ¶
- Constants
- Variables
- func CheckVPN()
- func GetVPNStatus() (active bool, iface, ip string)
- func SetFirstPeerTimeoutForTesting(d time.Duration) time.Duration
- func SetPublicIPDetectTimeoutForTesting(d time.Duration) time.Duration
- func SetSessionStartupGraceForTesting(d time.Duration) time.Duration
- type AddRequest
- type FileInfo
- type HealthReport
- type HistoryFilter
- type HistoryRecord
- type HookRunner
- type Info
- type PeerInfo
- type PieceStateRun
- type PiecesInfo
- type RequesterMetadata
- type Session
- func (s *Session) Add(ctx context.Context, req AddRequest) (result *Info, resultErr error)
- func (s *Session) AddNoPeersTorrentForTesting(seed string, addedAt time.Time) string
- func (s *Session) AddTags(hash string, tags []string) error
- func (s *Session) AddTrackers(hash string, urls []string, tier int) error
- func (s *Session) CheckSeedLimits(ctx context.Context)
- func (s *Session) CheckSpeedSchedule(cfg config.SpeedScheduleConfig)
- func (s *Session) CheckStalls(ctx context.Context)
- func (s *Session) Close()
- func (s *Session) ExportTorrent(hash string) ([]byte, error)
- func (s *Session) ForceStart(hash string) error
- func (s *Session) Get(hash string) (*Info, error)
- func (s *Session) GetArchivedCount() int
- func (s *Session) GetFiles(hash string) ([]FileInfo, error)
- func (s *Session) GetHealth() *HealthReport
- func (s *Session) GetMetadata(hash string) (*RequesterMetadata, error)
- func (s *Session) GetStallInfo(hash string) (*StallInfo, error)
- func (s *Session) GetTransferStats() TransferStats
- func (s *Session) IsAltSpeedActive() bool
- func (s *Session) List() []Info
- func (s *Session) ListStalled() []StalledTorrent
- func (s *Session) LookupHistory(ctx context.Context, f HistoryFilter) ([]HistoryRecord, error)
- func (s *Session) MaxActiveDownloads() int
- func (s *Session) Pause(hash string) error
- func (s *Session) PauseOnComplete() bool
- func (s *Session) Peers(hash string) ([]PeerInfo, error)
- func (s *Session) Pieces(hash string) (*PiecesInfo, error)
- func (s *Session) PublishHealth(ctx context.Context)
- func (s *Session) Reannounce(hash string) error
- func (s *Session) Recheck(ctx context.Context, hash string) error
- func (s *Session) Remove(ctx context.Context, hash string, deleteFiles bool) error
- func (s *Session) RemoveTags(hash string, tags []string) error
- func (s *Session) RemoveTracker(hash, url string) error
- func (s *Session) Resume(hash string) error
- func (s *Session) SetAltSpeedEnabled(enabled bool)
- func (s *Session) SetCategory(hash, category string) error
- func (s *Session) SetGlobalDownloadLimit(n int)
- func (s *Session) SetGlobalUploadLimit(n int)
- func (s *Session) SetLocation(hash, newPath string) error
- func (s *Session) SetMaxActiveDownloads(n int)
- func (s *Session) SetMetadata(hash string, meta RequesterMetadata) error
- func (s *Session) SetPauseOnComplete(v bool)
- func (s *Session) SetPriority(hash string, priority int) error
- func (s *Session) SetSeedLimits(hash string, ratioLimit float64, timeLimitSecs int) error
- func (s *Session) StartWatchDir(ctx context.Context, dir string) error
- func (s *Session) Trackers(hash string) ([]TrackerInfo, error)
- type StallInfo
- type StallLevel
- type StalledTorrent
- type Status
- type TrackerInfo
- type TransferStats
- type WebhookDispatcher
Constants ¶
const ( ReasonNoPeersEver = "no_peers_ever" // pre-metadata, no peer in the firstPeerTimeout window ReasonNoPeers = "no_peers" // had peers at some point, now has none + no data for stall_timeout ReasonNoSeeders = "no_seeders" // has peers but no seeds, no data for stall_timeout ReasonNoDataReceived = "no_data_received" )
Stall reason strings. These show up in the event bus and in the HTTP API response for /torrents/{hash}/stall — Pilot/Prism use them to decide whether to blocklist the release.
Variables ¶
var DefaultPublicTrackers = [][]string{
{
"https://tracker.opentrackr.org:443/announce",
"https://tracker.torrent.eu.org:443/announce",
"https://tracker1.bt.moack.co.kr:443/announce",
},
{
"udp://tracker.opentrackr.org:1337/announce",
"udp://open.demonii.com:1337/announce",
"udp://open.stealth.si:80/announce",
"udp://tracker.torrent.eu.org:451/announce",
"udp://exodus.desync.com:6969/announce",
},
{
"udp://tracker.moeking.me:6969/announce",
"udp://explodie.org:6969/announce",
"udp://tracker1.bt.moack.co.kr:80/announce",
"udp://tracker.tiny-vps.com:6969/announce",
},
}
DefaultPublicTrackers is a curated list of reliable public trackers. Magnets from public indexers often arrive with zero tracker URLs, making metadata resolution depend entirely on DHT — which is slow and unreliable behind VPNs. Adding these trackers dramatically speeds up peer discovery.
Sources: ngosang/trackerslist, newtrackon.com Prefer HTTPS/HTTP trackers over UDP for better VPN compatibility.
Functions ¶
func CheckVPN ¶
func CheckVPN()
CheckVPN detects VPN status by looking for tunnel interfaces and checking the external IP. Call periodically (every 60s).
func GetVPNStatus ¶
GetVPNStatus returns the cached VPN status.
func SetFirstPeerTimeoutForTesting ¶
SetFirstPeerTimeoutForTesting lets cross-package tests (e.g. api/v1 stall handler tests) shrink the no-peers-ever stall threshold so a fresh torrent crosses it in milliseconds. Returns the previous value so the caller can restore it on cleanup. Production code must NOT call this — it changes the stall-detection contract globally.
func SetPublicIPDetectTimeoutForTesting ¶
SetPublicIPDetectTimeoutForTesting lets tests in other packages (e.g. api/v1) short-circuit the 10-second public-IP lookup when constructing a real Session. Returns the previous value so the caller can restore it. Production code must NOT call this.
func SetSessionStartupGraceForTesting ¶
SetSessionStartupGraceForTesting lets cross-package tests bypass the 10-minute warm-up suppression window so ListStalled actually surfaces the seeded torrent. Returns the previous value so the caller can restore it. Production code must NOT call this.
Types ¶
type AddRequest ¶
type AddRequest struct {
// URI is a magnet link, HTTP URL to a .torrent file, or empty if File is set.
URI string `json:"uri"`
// File is raw .torrent file bytes. Mutually exclusive with URI.
File []byte `json:"-"`
// Category to assign.
Category string `json:"category"`
// SavePath overrides the default download directory.
SavePath string `json:"save_path"`
// Tags to assign.
Tags []string `json:"tags"`
// Paused starts the torrent in paused state.
Paused bool `json:"paused"`
// Metadata holds optional media context from the requesting service (Pilot/Prism).
Metadata *RequesterMetadata `json:"metadata,omitempty"`
}
AddRequest is the input for adding a new torrent.
type FileInfo ¶
type FileInfo struct {
Index int `json:"index"`
Path string `json:"path"`
Size int64 `json:"size"`
Priority string `json:"priority"` // "skip", "normal", "high"
Progress float64 `json:"progress"`
}
FileInfo describes a file within a torrent.
type HealthReport ¶
type HealthReport struct {
ActiveDownloads int64 `json:"active_downloads"`
ActiveUploads int64 `json:"active_uploads"`
TotalTorrents int `json:"total_torrents"`
DownloadSpeed int64 `json:"download_speed_bps"`
UploadSpeed int64 `json:"upload_speed_bps"`
DiskFreeBytes int64 `json:"disk_free_bytes"`
DiskTotalBytes int64 `json:"disk_total_bytes"`
StalledCount int `json:"stalled_count"`
EngineStatus string `json:"engine_status"`
PeersConnected int `json:"peers_connected"`
VPNActive bool `json:"vpn_active"`
VPNInterface string `json:"vpn_interface,omitempty"`
ExternalIP string `json:"external_ip,omitempty"`
}
HealthReport is the structured health data for Pulse dashboard.
type HistoryFilter ¶
type HistoryFilter struct {
// Service narrows to "pilot" / "prism" / "manual" / "". Required
// when looking up by an arr-side ID, since IDs are only unique
// within a service's namespace.
Service string
// One of these is the typical lookup key. Multiple may be set
// (combined with AND), but in practice the caller picks one.
InfoHash string
MovieID string
SeriesID string
EpisodeID string
// Semantic match — for "find any download for this episode of
// this show, regardless of release". Requires at least TMDBID.
TMDBID int
Season int
Episode int
// IncludeRemoved=false (default) returns only active records.
// Set true to include removed-but-not-yet-purged history.
IncludeRemoved bool
// Limit caps the result set. 0 → 100 default.
Limit int
}
HistoryFilter narrows a LookupHistory query. All fields are optional — only non-zero values are added to the WHERE clause. The query joins the filters with AND.
type HistoryRecord ¶
type HistoryRecord struct {
InfoHash string `json:"info_hash"`
Name string `json:"name"`
SavePath string `json:"save_path"`
Category string `json:"category"`
AddedAt string `json:"added_at"` // RFC3339
CompletedAt string `json:"completed_at,omitempty"` // empty when still in progress
RemovedAt string `json:"removed_at,omitempty"` // empty when active
// Requester metadata — opaque IDs the arr supplied at grab time.
// Empty when the torrent was added without metadata (e.g. directly
// in Haul's UI rather than via Pilot/Prism).
Requester string `json:"requester,omitempty"`
MovieID string `json:"movie_id,omitempty"`
SeriesID string `json:"series_id,omitempty"`
EpisodeID string `json:"episode_id,omitempty"`
TMDBID int `json:"tmdb_id,omitempty"`
Season int `json:"season,omitempty"`
Episode int `json:"episode,omitempty"`
}
HistoryRecord is the lookup-API view of a torrent's persistent history record. Pilot/Prism use this to answer "have I ever downloaded this episode/movie?" without polling Haul's live state.
Both active and previously-removed torrents are returned (callers distinguish via RemovedAt). Files-on-disk status is NOT tracked here — Haul knows the save_path but not whether the file still exists; if Pilot/Prism care, they stat the path themselves.
type HookRunner ¶
type HookRunner struct {
// contains filtered or unexported fields
}
HookRunner executes external commands on torrent events.
func NewHookRunner ¶
func NewHookRunner(onAdd, onComplete string, logger *slog.Logger) *HookRunner
NewHookRunner creates a hook runner with optional commands. Commands support these variables:
- %h = info hash
- %n = torrent name
- %p = content path
- %c = category
func (*HookRunner) HandleEvent ¶
func (r *HookRunner) HandleEvent(_ context.Context, e events.Event)
HandleEvent implements events.Handler.
type Info ¶
type Info struct {
InfoHash string `json:"info_hash"`
Name string `json:"name"`
Status Status `json:"status"`
SavePath string `json:"save_path"`
Category string `json:"category"`
Tags []string `json:"tags"`
Size int64 `json:"size"`
Downloaded int64 `json:"downloaded"`
Uploaded int64 `json:"uploaded"`
Progress float64 `json:"progress"`
DownloadRate int64 `json:"download_rate"`
UploadRate int64 `json:"upload_rate"`
Seeds int `json:"seeds"`
Peers int `json:"peers"`
SeedRatio float64 `json:"seed_ratio"`
ETA int64 `json:"eta"`
AddedAt time.Time `json:"added_at"`
CompletedAt *time.Time `json:"completed_at,omitempty"`
ContentPath string `json:"content_path"`
Requester string `json:"requester,omitempty"` // "pilot" | "prism" | "manual" | ""
// Stalled is true when the backend's stall detector classifies this
// torrent as inactive — see internal/core/torrent/stall.go for the
// thresholds. The frontend uses this to render a distinct "Stalled"
// status badge and color, replacing the old "download_rate == 0"
// frontend-side heuristic which flipped on every brief connection blip.
// Always false for non-downloading statuses.
Stalled bool `json:"stalled"`
// StalledAt is non-nil when the stall watcher escalated past level 3
// and auto-paused the torrent. Distinct from Stalled (which is
// transient and only meaningful while downloading): once StalledAt
// is set, the torrent is permanently marked as needing user
// attention until they resume it. Resume clears StalledAt and
// removes the auto-applied 'stalled' tag.
StalledAt *time.Time `json:"stalled_at,omitempty"`
}
Info is the external representation of a torrent.
type PeerInfo ¶
type PeerInfo struct {
Addr string `json:"addr"` // "1.2.3.4:54321"
Client string `json:"client"` // "qBittorrent 4.5.0", "unknown" if the peer hasn't sent a client name
Network string `json:"network"` // "tcp" or "utp"
Encrypted bool `json:"encrypted"` // the peer prefers / supports encryption
Progress float64 `json:"progress"` // 0..1 — fraction of pieces the peer has
DownloadRate int64 `json:"download_rate"` // bytes/sec we're receiving from them
UploadRate int64 `json:"upload_rate"` // bytes/sec we're sending them (best-effort; 0 until anacrolix exposes per-peer upload rate)
Downloaded int64 `json:"downloaded"` // total useful data bytes read from this peer
Uploaded int64 `json:"uploaded"` // total data bytes written to this peer
}
PeerInfo is the external representation of a single connected peer. Built on demand by Session.Peers — not part of the bulk torrent list to keep the hot-path response small.
type PieceStateRun ¶
type PieceStateRun struct {
Length int `json:"length"`
State string `json:"state"` // "complete" | "partial" | "checking" | "missing"
}
PieceStateRun is a run-length-encoded entry describing a series of consecutive pieces in the same state. Mirrors anacrolix's PieceStateRuns output but serialised as plain JSON.
type PiecesInfo ¶
type PiecesInfo struct {
NumPieces int `json:"num_pieces"`
PieceSize int64 `json:"piece_size"`
Runs []PieceStateRun `json:"runs"`
}
PiecesInfo is a snapshot of a torrent's piece-level state. See plans/haul-torrent-detail-enhancements.md §4 for how the frontend consumes this (canvas-rendered piece bar with per-piece arrival flashes).
type RequesterMetadata ¶
type RequesterMetadata struct {
Requester string `json:"requester,omitempty"` // "prism", "pilot", "manual"
MediaType string `json:"media_type,omitempty"` // "movie", "tv", "unknown"
Title string `json:"title,omitempty"` // "Breaking Bad" or "Fight Club"
Year int `json:"year,omitempty"` // 2008
TMDBID int `json:"tmdb_id,omitempty"` // 550
SeasonNumber int `json:"season_number,omitempty"` // 1
EpisodeNumber int `json:"episode_number,omitempty"` // 4
EpisodeTitle string `json:"episode_title,omitempty"` // "Cancer Man"
Quality string `json:"quality,omitempty"` // "Bluray-1080p"
QualityCodec string `json:"quality_codec,omitempty"` // "x265"
RequestedBy string `json:"requested_by,omitempty"` // user who requested
RequestedAt string `json:"requested_at,omitempty"` // ISO8601
// Arr-side identifiers — UUID-shaped strings the requester uses to
// reference its own DB rows. Empty when the caller didn't supply them.
MovieID string `json:"movie_id,omitempty"` // Prism movie UUID
SeriesID string `json:"series_id,omitempty"` // Pilot series UUID
EpisodeID string `json:"episode_id,omitempty"` // Pilot episode UUID
}
RequesterMetadata holds structured context from the service that requested the download.
The MovieID/SeriesID/EpisodeID fields carry the arr's own UUIDs so Pilot/Prism can later look up "have I downloaded anything for episode_id=X?" via Haul's history endpoints. They're opaque strings to Haul — Haul never resolves them, just stores and returns them.
type Session ¶
type Session struct {
// contains filtered or unexported fields
}
Session manages the torrent engine and wraps anacrolix/torrent.
func NewSession ¶
func NewSession(cfg config.TorrentConfig, db *sql.DB, bus *events.Bus, logger *slog.Logger) (*Session, error)
NewSession creates a new torrent session.
func (*Session) AddNoPeersTorrentForTesting ¶
AddNoPeersTorrentForTesting registers a torrent in the session's internal map with `addedAt = past`, no peers, no metadata — the exact shape both GetStallInfo and ListStalled classify as no_peers_ever once firstPeerTimeout elapses. The hash is derived from `seed` so each test can predict the resulting info_hash.
Cross-package callers (api/v1 stall handler tests) use this to seed state that the production add-torrent path would otherwise require real anacrolix peers to produce. The torrent is NOT registered with the anacrolix client — the stall classifiers only read managedTorrent fields, so the missing client-side handle is safe for the tested no-peers-ever path. Returns the info-hash hex.
Production code MUST NOT call this — it bypasses Add, the session DB, and the lifecycle hooks.
func (*Session) AddTrackers ¶
AddTrackers appends one or more announce URLs to the torrent at the given tier. Idempotent — duplicates are silently ignored. Empty URLs are skipped. The torrent kicks off an announce on the new trackers asynchronously; effect is visible on next Trackers() call.
func (*Session) CheckSeedLimits ¶
CheckSeedLimits checks all seeding torrents against their seed limits and pauses any that have exceeded them. Call this periodically.
func (*Session) CheckSpeedSchedule ¶
func (s *Session) CheckSpeedSchedule(cfg config.SpeedScheduleConfig)
CheckSpeedSchedule enables/disables alt speed based on the schedule config.
func (*Session) CheckStalls ¶
CheckStalls inspects all torrents for stall conditions and publishes events for the severe cases. There are two distinct stall classes:
**No peers ever**: the torrent was added more than firstPeerTimeout ago but has never observed a single peer. Typically means the release is dead (stale indexer data, trackers have no alive seeders), or Haul's networking is misconfigured. Pilot/Prism subscribe to this event and use it to blocklist the release.
**Activity-based escalation**: the torrent had peers at some point, started downloading, but lost all activity. Progressive remediation: Level 1 reannounce → Level 2 force DHT → Level 3 archive.
Both classes are suppressed during the sessionStartupGrace window (default 10 min) because anacrolix itself is still warming up.
func (*Session) Close ¶
func (s *Session) Close()
Close shuts down the torrent engine. The piece-completion store is owned by anacrolix once handed to it in NewSession — fileClientImpl.Close() closes it, so we must not close it again here (a double-close errors with "database not open" on bolt).
func (*Session) ExportTorrent ¶
ExportTorrent returns the raw .torrent bytes for the given hash so an operator can re-add it elsewhere. Reads from the torrents.torrent_data column populated at add-time by saveTorrentData.
Returns (nil, error) if the hash is unknown OR the torrent_data column is empty (legacy rows from before persistence wiring or before metadata arrived). Caller should distinguish those cases via the error message.
func (*Session) ForceStart ¶
ForceStart resumes a torrent and marks it to bypass queue limits.
func (*Session) GetArchivedCount ¶
GetArchivedCount returns the number of torrents in the "archived" category.
func (*Session) GetHealth ¶
func (s *Session) GetHealth() *HealthReport
GetHealth returns a structured health report.
func (*Session) GetMetadata ¶
func (s *Session) GetMetadata(hash string) (*RequesterMetadata, error)
GetMetadata retrieves the requester metadata for a torrent.
func (*Session) GetStallInfo ¶
GetStallInfo returns stall information for a specific torrent. Unlike the bulk ListStalled it also carries the inactivity counters for torrents that haven't crossed the stall threshold yet.
func (*Session) GetTransferStats ¶
func (s *Session) GetTransferStats() TransferStats
GetTransferStats returns aggregate session statistics.
func (*Session) IsAltSpeedActive ¶
IsAltSpeedActive returns whether alt speed mode is currently active.
func (*Session) ListStalled ¶
func (s *Session) ListStalled() []StalledTorrent
ListStalled iterates all managed torrents and returns those currently classified as stalled, filtering out the session startup grace period. Semantics match CheckStalls exactly; callers get the same decisions without having to re-implement the heuristic client-side.
func (*Session) LookupHistory ¶
func (s *Session) LookupHistory(ctx context.Context, f HistoryFilter) ([]HistoryRecord, error)
LookupHistory returns torrent records matching the filter. Used by Pilot/Prism's history-aware UIs (library badges, manual-search guardrail, "downloaded but not in library" rail).
Returns an empty slice (not an error) when nothing matches — callers should treat "no record" as "Haul has never seen this".
func (*Session) MaxActiveDownloads ¶
MaxActiveDownloads returns the runtime-effective cap on concurrently downloading torrents. Zero or negative means unlimited.
func (*Session) Pause ¶
Pause pauses a torrent. This is the user-initiated pause path: it clears queuePaused so the queue gate will treat this torrent as sticky-paused and never auto-resume it.
func (*Session) PauseOnComplete ¶
PauseOnComplete returns the runtime-effective value of the pause-on- complete setting. Initialized from cfg.PauseOnComplete at Session creation, optionally overridden by the `settings` DB table at startup, and mutated at runtime by SetPauseOnComplete (which the settings HTTP handler calls on UI toggle).
func (*Session) Peers ¶
Peers returns a snapshot of currently-connected peers for a torrent. Empty slice if the torrent has no peers or metadata isn't ready. Error only for unknown hash.
func (*Session) Pieces ¶
func (s *Session) Pieces(hash string) (*PiecesInfo, error)
Pieces returns a run-length-encoded snapshot of the torrent's piece state. Returns (nil, nil) if metadata hasn't been received yet — the frontend renders "Waiting for metadata…" in that case.
func (*Session) PublishHealth ¶
PublishHealth publishes a health update event. Call periodically.
func (*Session) Reannounce ¶
Reannounce forces a fresh announce to every tracker by restarting the torrent's announcers. ModifyTrackers stops the existing tracker scrapers and starts new ones, which announce immediately; Metainfo() clones the live announce list, so runtime-added trackers survive.
func (*Session) Remove ¶
Remove removes a torrent. If deleteFiles is true, downloaded data is deleted.
DB cleanup runs in a defer so it survives a panic from anacrolix/torrent's Drop() — without that, a library-internal crash mid-Drop would leave an orphan torrents row that restoreFromDB would resurrect on next startup, re-triggering the same panic. Real bug observed in the field with John Wick (info hash ec5086c1c…): library panic during tracker announce dispatcher → DB DELETE never ran → permanent crashloop on restart.
func (*Session) RemoveTags ¶
RemoveTags removes tags from a torrent.
func (*Session) RemoveTracker ¶
RemoveTracker rebuilds the announce list for the torrent without the given URL. anacrolix doesn't expose a "remove this tracker" call, so we collect the current list, filter out the removed URL, and reset the list via SetAnnounceList.
func (*Session) Resume ¶
Resume resumes a paused torrent. This is the user-initiated resume path: it clears queuePaused so subsequent state changes are clean. The queue gate runs immediately after; if Resume puts the active set over the cap, the lowest-priority active torrent (which may be the one just resumed) gets queue-paused.
func (*Session) SetAltSpeedEnabled ¶
SetAltSpeedEnabled toggles alternative speed limits at runtime. Note: anacrolix/torrent rate limiters are set at client creation. We track the state for the schedule checker and API to report.
func (*Session) SetCategory ¶
SetCategory assigns a category to a torrent.
func (*Session) SetGlobalDownloadLimit ¶
SetGlobalDownloadLimit updates the global download rate limit at runtime. n is in bytes per second; n == 0 means unlimited. Operates on the *rate.Limiter that was wired into anacrolix's client at startup, so the new limit applies immediately to every active torrent without rebuilding the engine.
func (*Session) SetGlobalUploadLimit ¶
SetGlobalUploadLimit mirrors SetGlobalDownloadLimit for upload.
func (*Session) SetLocation ¶
SetLocation moves a torrent's data to a new save path.
func (*Session) SetMaxActiveDownloads ¶
SetMaxActiveDownloads updates the runtime cap on concurrently downloading torrents and immediately re-runs the queue gate so the new value takes effect now (not just on next add/complete). Called by the settings HTTP handler.
func (*Session) SetMetadata ¶
func (s *Session) SetMetadata(hash string, meta RequesterMetadata) error
SetMetadata attaches structured requester metadata to a torrent.
Persists the full struct to the `metadata` JSON column AND denormalizes the indexed fields (requester_*, requester_tmdb_id, season, episode, movie_id, series_id, episode_id) into their own columns so Pilot/Prism's history-lookup queries can hit indexes instead of scanning JSON.
func (*Session) SetPauseOnComplete ¶
SetPauseOnComplete updates the runtime pause-on-complete setting. Called by the settings HTTP handler when the user flips the "Stop seeding when complete" toggle in the UI. This is the only method that affects runtime behavior — writing to the `settings` DB table alone does NOT take effect until this is called.
func (*Session) SetPriority ¶
SetPriority sets the queue priority (lower = higher priority).
After updating the DB, the queue gate re-runs so a priority change takes effect immediately: if the user dragged a queued torrent above the cap, it gets resumed and the displaced torrent is queue-paused.
func (*Session) SetSeedLimits ¶
SetSeedLimits sets per-torrent seed ratio and time limits.
func (*Session) StartWatchDir ¶
StartWatchDir launches the auto-add goroutine. Caller (main.go) is responsible for setting up Session lifecycle; we tie our cleanup to a passed-in done channel so a session shutdown closes the watcher cleanly. Errors during goroutine setup are returned synchronously; runtime errors are logged.
type StallInfo ¶
type StallInfo struct {
Stalled bool `json:"stalled"`
Level StallLevel `json:"level"`
InactiveSecs int64 `json:"inactive_secs"`
LastActivity *time.Time `json:"last_activity,omitempty"`
Reason string `json:"reason"`
}
StallInfo holds stall detection data for a torrent.
type StallLevel ¶
type StallLevel int
StallLevel classifies the severity of a stall.
const ( StallNone StallLevel = 0 StallLevel1 StallLevel = 1 // No activity for stall_timeout — reannounce StallLevel2 StallLevel = 2 // No activity for 2x stall_timeout — force DHT StallLevel3 StallLevel = 3 // No activity for 5x stall_timeout — notify ecosystem StallNoPeersEver StallLevel = 4 // Never got a single peer — classic "dead torrent" signal )
type StalledTorrent ¶
type StalledTorrent struct {
InfoHash string `json:"info_hash"`
Name string `json:"name"`
Level StallLevel `json:"level"`
Reason string `json:"reason"`
InactiveSecs int64 `json:"inactive_secs"`
AddedAt time.Time `json:"added_at"`
}
StalledTorrent pairs a torrent's identity with its current stall status. Used by the HTTP /api/v1/stalls endpoint so a consumer (Pilot's stall watcher) can get all stalled torrents in one call instead of N+1.
type TrackerInfo ¶
TrackerInfo is a single configured tracker from the torrent's metainfo. v1 does NOT include live announce state (last announce time, reported peers/seeds, errors) — see plans/haul-torrent-detail-enhancements.md §6.1.
type TransferStats ¶
type TransferStats struct {
TotalTorrents int `json:"total_torrents"`
ActiveDownloads int `json:"active_downloads"`
ActiveUploads int `json:"active_uploads"`
TotalDownloaded int64 `json:"total_downloaded"`
TotalUploaded int64 `json:"total_uploaded"`
DownloadSpeed int64 `json:"download_speed"`
UploadSpeed int64 `json:"upload_speed"`
TotalPeers int `json:"peers_connected"`
TotalSeeds int `json:"seeds_connected"`
}
TransferStats holds aggregate session statistics.
type WebhookDispatcher ¶
type WebhookDispatcher struct {
// contains filtered or unexported fields
}
WebhookDispatcher sends events to configured webhook URLs.
func NewWebhookDispatcher ¶
func NewWebhookDispatcher(webhooks []config.WebhookConfig, logger *slog.Logger) *WebhookDispatcher
NewWebhookDispatcher creates a dispatcher for outbound webhooks.
func (*WebhookDispatcher) HandleEvent ¶
func (d *WebhookDispatcher) HandleEvent(_ context.Context, e events.Event)
HandleEvent implements events.Handler — sends matching events to webhooks.