streamer

package
v0.80.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: MIT Imports: 36 Imported by: 0

Documentation

Overview

Package streamer manages active torrents for HTTP streaming. Torrents stay loaded while clients are reading; idle ones are evicted.

Index

Constants

View Source
const (
	ErrFileIndexOutOfRange = "file index out of range"
	ErrFavoritesUnavail    = "favorites store unavailable"
)
View Source
const ArtSourceNone = "none"

ArtSourceNone marks "the resolve chain ran and found nothing". Persisted (with a timestamp via SetArt) so ResolveArt doesn't re-run the whole AI+TMDB+web chain on every card render for a title that has no art — see ArtNegativeFresh.

View Source
const DefaultPeerPort = 51469

DefaultPeerPort is the inbound BitTorrent peer port used when none is configured (no VPN forwarded port, no JACKUI_PEER_PORT). Exported so the boot wiring can treat "fell back to default" identically to the streamer.

View Source
const (
	// HealthFreshFor — re-probe only when the persisted snapshot is older.
	HealthFreshFor = 30 * time.Minute
)

Variables

View Source
var ErrTorrentNotActive = errors.New("torrent not active")

ErrTorrentNotActive is the sentinel returned by streamer methods when the requested torrent isn't loaded in the active set. Handlers match it with errors.Is to map to HTTP 404; wrap with %w when adding context.

Functions

func ArtSourceRank

func ArtSourceRank(source string) int

ArtSourceRank orders art sources by trustworthiness so resolution only ever *upgrades* a persisted thumbnail (uploader-curated image > matched poster > web search > raw frame). The web search is a fallback for content TMDB can't match (adult/obscure) and ranks above a raw frame but below a real poster. Exported so the resolver in the handlers layer shares the order.

func DefaultFavoritesPath

func DefaultFavoritesPath(dataDir string) string

DefaultFavoritesPath returns the standard location inside the stream data dir.

func DefaultMetadataCachePath

func DefaultMetadataCachePath(dataDir string) string

DefaultMetadataCachePath returns the standard location inside the stream data dir.

func DefaultSeedsPath

func DefaultSeedsPath(dataDir string) string

DefaultSeedsPath returns the standard location inside the state dir.

func InTimeRange

func InTimeRange(now time.Time, rangeStr string) bool

InTimeRange verifica se a hora atual cai dentro de um time_range como "08:00-18:00". Suporta faixas que viram a noite, ex: "22:00-06:00".

func PhysicalBytes

func PhysicalBytes(info os.FileInfo) int64

PhysicalBytes returns the number of bytes actually allocated on disk for the file described by info. On POSIX filesystems this is `st.Blocks * 512` (the historical stat block unit), which correctly reflects sparse holes — a 10 GB sparse file with only one 4 KiB block written returns 4096.

If the underlying syscall struct isn't available for any reason, we fall back to the logical size so callers never see zero.

func StartBandwidthScheduler

func StartBandwidthScheduler(ctx context.Context, s *Streamer, cfg *config.Config)

StartBandwidthScheduler inicia o loop periódico em background que aplica as regras de limites de banda baseadas no horário.

Types

type CacheEntry

type CacheEntry struct {
	Path       string    `json:"path"` // relative to DataDir
	Size       int64     `json:"size"`
	ModTime    time.Time `json:"modTime"`
	IsActive   bool      `json:"isActive"`   // currently being downloaded/seeded
	IsFavorite bool      `json:"isFavorite"` // protected from eviction
	// InfoHash is the torrent's hex-encoded SHA1 info hash. Populated when the
	// torrent is either active or has a persisted .torrent in metainfoDir.
	// Empty string when we can't resolve the hash — the UI hides Play in that case.
	InfoHash string `json:"infoHash,omitempty"`
}

CacheEntry describes one item on disk in the cache directory.

type CacheStats

type CacheStats struct {
	DataDir   string       `json:"dataDir"`
	TotalSize int64        `json:"totalSize"`
	MaxSize   int64        `json:"maxSize"`   // 0 = unlimited
	NumActive int          `json:"numActive"` // currently loaded torrents
	Entries   []CacheEntry `json:"entries"`
	// Filesystem footprint of the disk hosting DataDir (0 = statfs unavailable).
	DiskFree  int64 `json:"diskFree"`
	DiskTotal int64 `json:"diskTotal"`
	// Lifetime LRU eviction counters (since process start).
	EvictedCount   int64      `json:"evictedCount"`
	EvictedBytes   int64      `json:"evictedBytes"`
	LastEvictionAt *time.Time `json:"lastEvictionAt,omitempty"`
}

CacheStats summarizes disk usage of the streaming cache.

type CachedArt

type CachedArt struct {
	// Source is "torrent" | "tmdb" | "frame" — the chain step that produced it.
	Source string `json:"source"`
	// Path is the DataDir-relative file for byte-backed sources (torrent/frame).
	// Empty for tmdb (the image lives on the remote CDN at PosterURL).
	Path string `json:"path,omitempty"`
	// PosterURL is the remote image for source=="tmdb"; the handler 302s to it.
	PosterURL string `json:"posterUrl,omitempty"`
	TmdbID    int    `json:"tmdbId,omitempty"`
	ImdbID    string `json:"imdbId,omitempty"`
}

CachedArt is the resolved thumbnail for a torrent, persisted per info_hash so we never re-run the (expensive) resolution chain — embedded torrent image, TMDB lookup, or a captured video frame. Stored alongside the metadata row but updated via SetArt() with a disjoint column set, so caching metadata never clobbers the art and vice-versa.

type CachedFile

type CachedFile struct {
	Index   int    `json:"index"`
	Path    string `json:"path"`
	Size    int64  `json:"size"`
	IsVideo bool   `json:"isVideo"`
}

type CachedHealth

type CachedHealth struct {
	Seeders   int       `json:"seeders"`
	Peers     int       `json:"peers"`
	Available bool      `json:"available"` // seeders>0 || peers>0
	CheckedAt time.Time `json:"checkedAt"`
}

CachedHealth is the last-known swarm health for a torrent, persisted so a card can show a prior estimate (with its age) instantly while a fresh probe runs.

type CachedMeta

type CachedMeta struct {
	InfoHash    string       `json:"infoHash"`
	Name        string       `json:"name"`
	TotalSize   int64        `json:"totalSize"`
	Files       []CachedFile `json:"files"`
	PrimaryFile int          `json:"primaryFile"`
	CachedAt    time.Time    `json:"cachedAt"`
}

CachedMeta is the minimal shape we cache (a strict subset of TorrentInfo). Rates and per-file Downloaded/Progress are runtime-only and NOT cached — they would be stale and misleading.

type Chapter

type Chapter struct {
	Index    int     `json:"index"`
	StartSec float64 `json:"startSec"`
	EndSec   float64 `json:"endSec,omitempty"`
	Title    string  `json:"title,omitempty"`
}

Chapter is one chapter marker embedded in the media (MKV/MP4). Times are in seconds. The player navigates by setting video.currentTime to StartSec — this works for both direct-play and HLS (the transcode drops embedded chapters, so a <track kind="chapters"> would be empty; the probe list is the source).

type Config

type Config struct {
	DataDir      string        // where pieces are written; subdirs per torrent
	IdleTimeout  time.Duration // drop torrent after this much inactivity (files stay)
	MetadataWait time.Duration // how long to block waiting for .torrent metadata
	MaxCacheSize int64         // total cache cap in bytes; 0 = unlimited (no eviction)
	// MaxDownloadRate caps inbound peer bandwidth in bytes/sec; 0 = unlimited.
	// Wired into the anacrolix ClientConfig.DownloadRateLimiter. Can be updated
	// at runtime via Streamer.SetRateLimits.
	MaxDownloadRate int64
	// MaxUploadRate caps outbound peer bandwidth in bytes/sec; 0 = unlimited.
	MaxUploadRate int64
	// JackettHost is the host (no port) of the configured Jackett instance. The
	// SSRF guard trusts it (Jackett lives on the private LAN, so its download
	// links are legitimately private addresses) and the apikey below is injected
	// server-side so it never has to travel through the browser.
	JackettHost   string
	JackettAPIKey string
	// ListenPort is the BitTorrent peer port for inbound connections. 0 → DefaultPeerPort.
	// Behind a VPN this should be the provider's forwarded port so peers can
	// reach us (seed + better leech). See resolvePeerPort in main.
	ListenPort int
	// ── Performance / hardware tuning (0/"" = default da lib) ──
	// Readahead é o buffer de leitura à frente por sessão de streaming, em bytes.
	// 0 → 32 MiB. Aplicado por Reader; mutável ao vivo via SetStreamReadahead.
	Readahead int64
	// StorageBackend: "file" (default, grava direto) ou "mmap" (page cache).
	// Lido só na construção do client (New) — mudar exige reiniciar o processo.
	StorageBackend string
	// Tuning de peers/CPU — só aplicados em New (exigem reinício). 0 = default
	// anacrolix (conns=50, half-open=25, peersHighWater=500, pieceHashers=2).
	MaxConnsPerTorrent int
	HalfOpenConns      int
	PeersHighWater     int
	PieceHashers       int
	// SeedTrackers lista substrings de announce URLs cujos torrents devem
	// continuar seedando após o uso (não dropados). Ver Streamer.seedTrackers.
	SeedTrackers []string
}

type DownloadStorageSpec

type DownloadStorageSpec struct {
	BaseDir  string
	Sanitize func(string) string
}

DownloadStorageSpec tells the add path to write a torrent's data DIRECTLY to its final destination on bulk storage instead of the SSD piece cache.

  • BaseDir is the PARENT directory already resolved by the downloads worker (e.g. downloadDir/<username> or sharedDir/<category>) — WITHOUT the torrent name segment. The name isn't known until metadata arrives, so the storage appends it itself inside TorrentDirMaker (which runs post-metadata).
  • Sanitize turns the real torrent name (t.Name()) into the final folder segment. It is INJECTED (rather than imported) because the canonical sanitizer lives in internal/downloads, which already imports this package — importing it back would be a cycle. Passing it as a func keeps the path the storage writes to byte-identical to the path the worker's completionDest computes, so the move-on-completion is a no-op.

type Favorite

type Favorite struct {
	Name        string    `json:"name"`     // matches CacheEntry.Path (filesystem name)
	InfoHash    string    `json:"infoHash"` // hex hash, if known
	Magnet      string    `json:"magnet"`   // magnet URI — enables Play from /favorites without re-search
	UserID      int       `json:"userId"`
	FavoritedAt time.Time `json:"favoritedAt"`
	Reason      string    `json:"reason"`   // "manual" | "auto-5min"
	FolderID    *int      `json:"folderId"` // nil = root level; otherwise nested in a FavoriteFolder
	// Sort hints filled by the handler from the metadata cache (a separate DB, so
	// no JOIN here). Zero/nil = unknown (never resolved/probed) — sorts last.
	TotalSize int64 `json:"totalSize,omitempty"` // bytes; 0 = unknown
	Seeders   *int  `json:"seeders,omitempty"`   // nil = never probed
}

type FavoriteFolder

type FavoriteFolder struct {
	ID        int       `json:"id"`
	UserID    int       `json:"userId"`
	Name      string    `json:"name"`
	ParentID  *int      `json:"parentId"`
	Position  int       `json:"position"`
	Hidden    bool      `json:"hidden"`
	CreatedAt time.Time `json:"createdAt"`
}

FavoriteFolder represents an organizational folder in the user's favorites tree. Subfolders are modeled via ParentID (nil = root level). One user's tree can be arbitrarily deep; cycle prevention is handled at Move time.

type FavoritesStore

type FavoritesStore struct {
	// contains filtered or unexported fields
}

FavoritesStore persists "favorite" markings for streamed torrents. Favorites are protected from cache eviction (both LRU and manual clear-all).

Schema: one row per torrent name (as stored on disk), nullable info_hash for cross-reference.

func NewFavorites

func NewFavorites(pool *sql.DB) (*FavoritesStore, error)

NewFavorites wires the favorites store onto the shared Postgres pool. Schema is applied centrally (internal/db migrations).

func (*FavoritesStore) Add

func (f *FavoritesStore) Add(name, infoHash, magnet, reason string, userID int) error

Add marks a stream as favorite. Idempotent — re-adding refreshes the timestamp. userID=0 means "no auth/legacy". magnet may be empty if unknown.

func (*FavoritesStore) Close

func (f *FavoritesStore) Close()

Close is a no-op: the shared pool's lifecycle is owned by main.

func (*FavoritesStore) CreateFolder

func (f *FavoritesStore) CreateFolder(userID int, name string, parentID *int, hidden bool) (*FavoriteFolder, error)

CreateFolder makes a new folder under the optional parent. parentID nil creates a root-level folder. hidden keeps it out of the default listing.

func (*FavoritesStore) DeleteFolder

func (f *FavoritesStore) DeleteFolder(userID, id int) error

DeleteFolder removes a folder. ON DELETE CASCADE removes subfolders too; favorites in deleted folders fall back to root (ON DELETE SET NULL).

func (*FavoritesStore) GetFolder

func (f *FavoritesStore) GetFolder(userID, id int) (*FavoriteFolder, error)

GetFolder fetches a single folder; returns error if it doesn't belong to user.

func (*FavoritesStore) HashSetForUser

func (f *FavoritesStore) HashSetForUser(userID int, includeAll bool) (map[string]bool, error)

HashSetForUser returns all info_hashes the user has favorited as a set. Used pelo handler de busca pra enriquecer SearchResult com isFavorited via uma query só, em vez de IsFavoriteByHash N vezes. includeAll=true devolve hashes de todos os usuários (admin "all=1"). Hashes vazios são pulados.

func (*FavoritesStore) HiddenHashSet

func (f *FavoritesStore) HiddenHashSet(userID int, includeAll bool) (map[string]bool, error)

HiddenHashSet returns, as a set, the info_hashes of favourites that live in a hidden folder. Used to keep hidden-folder titles out of Continue Watching and the downloads list the same way they're kept out of the favourites view. includeAll=true (admin) spans every user's hidden folders.

func (*FavoritesStore) HiddenLocalPaths

func (f *FavoritesStore) HiddenLocalPaths(userID int) ([]HiddenLocalPath, error)

HiddenLocalPaths returns the local (mount, path) pairs the user has hidden.

func (*FavoritesStore) HiddenLocalPathsAll

func (f *FavoritesStore) HiddenLocalPathsAll() ([]HiddenLocalPathOwned, error)

HiddenLocalPathsAll returns every hidden local (mount, path) across all users, tagged with the owning user_id. Admin-only callers use it so an item one user hid stays hidden in the cross-user listing too.

func (*FavoritesStore) IsFavorite

func (f *FavoritesStore) IsFavorite(name string) bool

IsFavorite reports whether the given on-disk name is favorited (by any user). Used by Streamer cache eviction logic — protects favorites of all users from auto-delete.

func (*FavoritesStore) IsFavoriteByHash

func (f *FavoritesStore) IsFavoriteByHash(infoHash string) bool

IsFavoriteByHash reports whether any favorite row references this infoHash.

func (*FavoritesStore) IsFavoriteOf

func (f *FavoritesStore) IsFavoriteOf(name string, userID int) bool

IsFavoriteOf reports whether the given name is favorited specifically by a user.

func (*FavoritesStore) List

func (f *FavoritesStore) List(userID int, includeAll, includeHidden bool) ([]Favorite, error)

List returns favorites for a user (or all when includeAll=true), most recent first.

func (*FavoritesStore) ListFolders

func (f *FavoritesStore) ListFolders(userID int, includeHidden bool) ([]FavoriteFolder, error)

ListFolders returns all folders for a user. The UI builds the tree client-side via parent_id linkage — simpler than recursive SQL.

func (*FavoritesStore) MoveFavoriteToFolder

func (f *FavoritesStore) MoveFavoriteToFolder(userID int, name string, folderID *int) error

MoveFavoriteToFolder reassigns a favorite to a folder (nil = root).

func (*FavoritesStore) MoveFolder

func (f *FavoritesStore) MoveFolder(userID, id int, newParent *int) error

MoveFolder re-parents a folder. Pass nil to move to root. Cycle prevention: walk the parent chain — if we encounter the folder being moved, reject.

func (*FavoritesStore) ReconcileMagnets

func (f *FavoritesStore) ReconcileMagnets() (int, error)

ReconcileMagnets repairs magnet-less favorites without any network call:

  1. info_hash present, magnet empty → synthesize a tracker-less magnet (anacrolix finds peers via DHT — same shape the quick-favorite fallback uses).
  2. both empty, but the metadata cache (same DB) has a row with the same name → adopt that info_hash and synthesize the magnet. Only helps favorites whose torrent was activated at least once (metadata is written post-activation).

Returns the number of rows repaired. Idempotent.

func (*FavoritesStore) RecoverViaSearch

func (f *FavoritesStore) RecoverViaSearch(searcher MagnetSearcher, limit int) (int, error)

RecoverViaSearch re-searches each magnet-less favorite by name and links the best confident match (see bestMagnetMatch). Bounded by limit (re-links per boot) and conservative on purpose: a wrong match is worse than none. Returns the number repaired. A nil searcher or limit<=0 is a no-op.

func (*FavoritesStore) Remove

func (f *FavoritesStore) Remove(name string, userID int, includeAll bool) error

Remove unmarks a favorite. If userID > 0 and not includeAll, only that user's row is removed.

func (*FavoritesStore) RenameFolder

func (f *FavoritesStore) RenameFolder(userID, id int, newName string) error

RenameFolder updates the display name.

func (*FavoritesStore) SetFolderHidden

func (f *FavoritesStore) SetFolderHidden(userID, id int, hidden bool) error

SetFolderHidden flips a folder's hidden curtain.

func (*FavoritesStore) SetLocalPathHidden

func (f *FavoritesStore) SetLocalPathHidden(userID int, mount, path string, hidden bool) error

SetLocalPathHidden hides (hidden=true) or unhides a local (mount, path) for a user. Idempotent: hiding an already-hidden path is a no-op.

type FileInfo

type FileInfo struct {
	Index      int     `json:"index"`
	Path       string  `json:"path"`
	Size       int64   `json:"size"`
	IsVideo    bool    `json:"isVideo"`
	Downloaded int64   `json:"downloaded"`
	Progress   float64 `json:"progress"` // 0..1
	Priority   string  `json:"priority"` // none|low|normal|high
}

FileInfo is the JSON-friendly view of a file inside a torrent.

type FilePathResolver

type FilePathResolver func(hash metainfo.Hash, fileIdx int) (string, bool)

FilePathResolver resolves an info_hash and file index to a local physical file path. Returns the file path and true if the file is completed and exists on disk.

type GlobalRate

type GlobalRate struct {
	DownRate       int64 `json:"downRate"`
	UpRate         int64 `json:"upRate"`
	ActiveTorrents int   `json:"activeTorrents"`
}

GlobalRate aggregates download/upload rates across all active torrents.

type HashResult

type HashResult struct {
	Hash string `json:"hash"`
	Size int64  `json:"size"`
}

HashResult is the cached OpenSubtitles file hash for one (torrent, file).

func ComputeFileOSHash

func ComputeFileOSHash(f io.ReadSeeker, size int64) (HashResult, error)

ComputeFileOSHash opens a file on disk and returns its OpenSubtitles hash. Used by the /local subtitle endpoints — bypasses the torrent-only OSHash() path so legendas funcionam pra arquivos fora do streamer.

type HiddenLocalPath

type HiddenLocalPath struct {
	Mount string `json:"mount"`
	Path  string `json:"path"`
}

HiddenLocalPath is a (mount, path) the user has marked hidden in the local browser.

type HiddenLocalPathOwned

type HiddenLocalPathOwned struct {
	UserID int
	Mount  string
	Path   string
}

HiddenLocalPathOwned is a hidden local (mount, path) together with the user that hid it. Used by the admin "all users" download view, which must resolve each path under the OWNER's scope (UserSubpath mounts) — not the requester's.

type MagnetMatch

type MagnetMatch struct {
	Title    string
	Magnet   string
	InfoHash string
	Seeders  int
}

MagnetMatch is the minimal slice of a search result the recovery needs to re-link a magnet-less favorite by re-searching its title.

type MagnetSearcher

type MagnetSearcher interface {
	SearchByName(name string) ([]MagnetMatch, error)
}

MagnetSearcher re-resolves a favorite by its stored name (its release title). The adapter in cmd/server wraps the Jackett client; tests inject a fake.

type MetadataCache

type MetadataCache struct {
	// contains filtered or unexported fields
}

MetadataCache persists TorrentInfo snapshots keyed by info_hash so the UI can render the file list + name *instantly* when reopening a torrent the user has touched before, even before anacrolix finishes its DHT metadata fetch.

Why this exists: a magnet-only torrent takes 3-10s to resolve to .torrent metadata from peers/DHT on each fresh load. The first time we accept the delay; subsequent opens of the same hash should be ~instant. Metadata is immutable per info_hash so we can keep entries forever — only the on-demand torrent client load takes any time at all.

func NewMetadataCache

func NewMetadataCache(pool *sql.DB) (*MetadataCache, error)

NewMetadataCache wires the metadata cache onto the shared Postgres pool. Schema is applied centrally (internal/db migrations).

func (*MetadataCache) ArtNegativeFresh

func (m *MetadataCache) ArtNegativeFresh(infoHash string, ttl time.Duration) bool

ArtNegativeFresh reports whether a recent "no art found" marker exists, so the caller can short-circuit instead of re-running the expensive resolve chain. A real art source (rank > 0) is never treated as a negative marker.

func (*MetadataCache) Close

func (m *MetadataCache) Close() error

Close is a no-op: the shared pool's lifecycle is owned by main.

func (*MetadataCache) Get

func (m *MetadataCache) Get(infoHash string) *CachedMeta

Get returns a cached snapshot or nil if not present. Never returns error for "not found" — the caller treats nil as "no cache" and falls through to the live torrent client.

func (*MetadataCache) GetArt

func (m *MetadataCache) GetArt(infoHash string) *CachedArt

GetArt returns the persisted thumbnail for an info_hash, or nil when none has been resolved yet. Never errors on "not found" — callers treat nil as "no art".

func (*MetadataCache) GetHealth

func (m *MetadataCache) GetHealth(infoHash string) *CachedHealth

GetHealth returns the persisted swarm health, or nil if never probed.

func (*MetadataCache) GetSortMeta

func (m *MetadataCache) GetSortMeta(hashes []string) map[string]SortMeta

GetSortMeta returns size+seeders for the given hashes in a single query, keyed by info_hash. Hashes with no cached row are simply absent from the map. Used to enrich the favorites list for sorting without a cross-DB JOIN.

func (*MetadataCache) Set

func (m *MetadataCache) Set(info *TorrentInfo) error

Set saves a snapshot. Called by Streamer.Add() once anacrolix delivers metadata, so subsequent opens of the same hash hit the cache.

func (*MetadataCache) SetArt

func (m *MetadataCache) SetArt(infoHash string, art *CachedArt) error

SetArt persists a resolved thumbnail. Uses a column set disjoint from Set() so it neither requires nor clobbers the metadata snapshot — an art-only row (name=”) is created if the torrent's metadata hasn't been cached yet.

func (*MetadataCache) SetHealth

func (m *MetadataCache) SetHealth(infoHash string, seeders, peers int) error

SetHealth persists a swarm-health probe result (disjoint columns — creates an health-only row if the torrent's metadata isn't cached yet).

type PeerInfo

type PeerInfo struct {
	Addr         string  `json:"addr"`
	Client       string  `json:"client,omitempty"`
	Network      string  `json:"network,omitempty"` // "tcp" | "utp" | ...
	Availability float64 `json:"availability"`      // 0..1 fraction of pieces the peer has
	DownRate     int64   `json:"downRate"`          // bytes/s we receive from this peer
	UpRate       int64   `json:"upRate"`            // bytes/s we send to this peer
	Downloaded   int64   `json:"downloaded"`        // data bytes read from this peer
	Uploaded     int64   `json:"uploaded"`          // data bytes written to this peer
	IsSeeder     bool    `json:"isSeeder"`          // peer reports all pieces
	Receiving    bool    `json:"receiving"`         // inferred: downRate > 0
	Sending      bool    `json:"sending"`           // inferred: upRate > 0
	Encrypted    bool    `json:"encrypted,omitempty"`
}

PeerInfo is the JSON-friendly view of one connected peer, for the downloads "Peers" panel. anacrolix v1.61.0 doesn't export choke/interest, so Sending / Receiving are INFERRED from live transfer rates rather than read directly.

type ProbeResult

type ProbeResult struct {
	Audio     []Track   `json:"audio"`
	Subtitles []Track   `json:"subtitles"`
	Chapters  []Chapter `json:"chapters"`
	// DurationSec is the total media duration in seconds, 0 when ffprobe
	// couldn't determine it (e.g. MP4 with moov-at-end whose tail isn't
	// downloaded yet). Callers must treat 0 as "unknown" and fall back.
	DurationSec float64 `json:"durationSec"`
	// VideoCodec / Container / AudioCodec são os fatos da fonte; NeedsTranscode é
	// a DECISÃO (navegador-agnóstica): MKV/HEVC/AV1/AC3/DTS não tocam direto em
	// browser nenhum → tem que transcodificar pra HLS. O front decide por isto
	// (não mais pelo NOME do arquivo, que errava e mandava incompatível pro
	// direct-play → errorCode 4 no Safari). Mesma lógica do classifyForBrowser
	// dos arquivos locais. Vazio até o ffprobe rodar.
	VideoCodec      string `json:"videoCodec"`
	Container       string `json:"container"`
	AudioCodec      string `json:"audioCodec"`
	NeedsTranscode  bool   `json:"needsTranscode"`
	TranscodeReason string `json:"transcodeReason,omitempty"`
}

ProbeResult lists all switchable tracks in a torrent file.

func ProbeLocal added in v0.79.5

func ProbeLocal(ctx context.Context, path string) (ProbeResult, error)

ProbeLocal runs ffprobe on a local file path and returns the parsed tracks, chapters, duration and codec/container facts. It reuses parseProbeOutput — the SAME decoder the torrent path (Probe) uses — so /api/local/* handlers share ONE ffprobe invocation + parser with the torrent side instead of duplicating it. The caller owns the ctx/timeout.

type SeedEntry

type SeedEntry struct {
	InfoHash string    `json:"infoHash"`
	Magnet   string    `json:"magnet"`
	Name     string    `json:"name"`
	AddedAt  time.Time `json:"addedAt"`
}

SeedEntry is one persisted seed row.

type SeedsStore

type SeedsStore struct {
	// contains filtered or unexported fields
}

SeedsStore persists the torrents that must keep seeding (because they belong to a configured seed-tracker, e.g. jackui). On boot the streamer re-adds every entry so seeding resumes without the user re-opening anything.

This is separate from FavoritesStore: favorites are a user-facing list that only protects pieces from LRU eviction, whereas a seed entry is an automatic, tracker-driven marker whose job is to bring the torrent back into the swarm.

func NewSeeds

func NewSeeds(pool *sql.DB) (*SeedsStore, error)

NewSeeds wires the seeds store onto the shared Postgres pool. Schema is applied centrally (internal/db migrations).

func (*SeedsStore) Add

func (s *SeedsStore) Add(infoHash, magnet, name string) error

Add upserts a seed entry. Idempotent — re-adding the same hash refreshes the magnet/name without disturbing added_at. Nil-safe receiver.

func (*SeedsStore) Close

func (s *SeedsStore) Close() error

Close is a no-op: the shared pool's lifecycle is owned by main.

func (*SeedsStore) Has

func (s *SeedsStore) Has(infoHash string) bool

Has reports whether a hash is already persisted. Nil-safe receiver.

func (*SeedsStore) List

func (s *SeedsStore) List() ([]SeedEntry, error)

List returns all persisted seed entries, newest first. Nil-safe receiver.

func (*SeedsStore) Remove

func (s *SeedsStore) Remove(infoHash string) error

Remove deletes a seed entry. Nil-safe receiver.

type SidecarSubtitle

type SidecarSubtitle struct {
	Index    int    `json:"index"` // file index in torrent (use for download)
	Path     string `json:"path"`  // full path within torrent
	Size     int64  `json:"size"`
	Language string `json:"language"` // ISO-639 best-effort from filename
	Format   string `json:"format"`   // "srt" | "vtt" | "ass" | "ssa" | "sub"
}

SidecarSubtitle describes a standalone subtitle file inside the torrent (not embedded in the container, just next to the video).

type SortMeta

type SortMeta struct {
	TotalSize int64
	Seeders   int
}

SortMeta is the size+seeders pair used to sort the favorites list. Seeders is -1 when the swarm was never probed (so it sorts last).

type Streamer

type Streamer struct {
	// contains filtered or unexported fields
}

func New

func New(cfg Config) (*Streamer, error)

func NewForTesting

func NewForTesting() *Streamer

NewForTesting returns a Streamer with only the fields the non-torrent-client-touching handlers exercise (active map, downloads protection set, rate limiters). Opening a real anacrolix client requires binding UDP :42069, which collides between parallel test packages and a running dev server. Use this in handler/unit tests that don't need the torrent transport.

func (*Streamer) AcquireViewer

func (s *Streamer) AcquireViewer(hash metainfo.Hash)

AcquireViewer registers an open player session ("lease") on a torrent and cancels any pending drop. Called when the player opens a stream. No-op if the torrent isn't active (e.g. a local file, which lives outside the streamer).

func (*Streamer) ActiveList

func (s *Streamer) ActiveList() []*TorrentInfo

ActiveList returns a snapshot of every torrent currently loaded by the streamer, formatted for the Transmission-style downloads UI. Each entry has rate samples taken under the streamer lock so the numbers are consistent across the slice.

func (*Streamer) Add

func (s *Streamer) Add(ctx context.Context, magnetOrURL string) (*TorrentInfo, error)

Add loads a magnet OR an HTTP(S) URL to a .torrent file and waits for metadata. Returns the torrent info once available.

For .torrent URLs (common in private trackers and some Jackett providers that don't return a magnet), we fetch the file, parse the metainfo, and add via AddTorrentSpec — same downstream behavior as magnet.

func (*Streamer) AddForDownload

func (s *Streamer) AddForDownload(ctx context.Context, magnetOrURL string, ds DownloadStorageSpec) (*TorrentInfo, error)

AddForDownload is Add but writes the torrent's data DIRECTLY to its final destination on bulk storage (ds.BaseDir/<sanitize(name)>/...) instead of the SSD piece cache. Used by the downloads worker so torrents larger than the cache don't overflow it and the move-on-completion becomes a no-op. The streaming path (Add/EnsureActive) is unaffected — it passes ds=nil.

func (*Streamer) CanProbeHealth

func (s *Streamer) CanProbeHealth(hash metainfo.Hash, magnet string) bool

CanProbeHealth reports whether a swarm probe is possible for this hash: we need either a magnet (its tr= trackers) or a cached .torrent (its announce list, which carries a private tracker's passkey). Private results from jackui ship no magnet, so the cached .torrent is the only tracker source.

func (*Streamer) ClearAll

func (s *Streamer) ClearAll() error

ClearAll drops every active torrent and wipes the DataDir, *except* favorites. Favorites are preserved on disk; their active torrent is dropped but files remain.

func (*Streamer) ClearEntry

func (s *Streamer) ClearEntry(name string) error

ClearEntry removes a specific cache entry from disk (by relative path). Refuses if the entry is favorited (use Favorites().Remove first). If the torrent is currently active, it is dropped first.

func (*Streamer) Client

func (s *Streamer) Client() *torrent.Client

Client exposes the underlying anacrolix torrent client so external packages (the downloads worker, primarily) can resolve a hash → *torrent.Torrent and inspect file progress without going through the streaming-oriented helpers.

func (*Streamer) Close

func (s *Streamer) Close()

func (*Streamer) Drop

func (s *Streamer) Drop(hash metainfo.Hash)

func (*Streamer) DropSeed

func (s *Streamer) DropSeed(hash metainfo.Hash)

maybePersistSeed records the torrent in the seed store when it matches a configured seed-tracker, so seeding resumes automatically on the next boot. No-op without a seed store or when the torrent isn't a keep-seeding match. DropSeed para de auto-seedar um torrent de vez: remove o registro PERSISTENTE (.seeds.db) para que ele NÃO volte no próximo boot (resumeSeeding) e o dropa da memória. Usar nas ações EXPLÍCITAS do usuário (parar de seedar / remover torrent / excluir download) — ao contrário do Drop genérico (idle/health), que preserva o auto-seed. Sem isto, um torrent auto-seedado reaparecia como "ativo" para sempre, mesmo após ser removido.

func (*Streamer) EnsureActive

func (s *Streamer) EnsureActive(ctx context.Context, magnet string) (metainfo.Hash, error)

EnsureActive guarantees the streamer has a torrent loaded for the given magnet. Wraps the regular Add() pipeline so metadata cache + favorites stay consistent. Returns the InfoHash for callers that need to address the torrent directly.

func (*Streamer) EnsureActiveForDownload

func (s *Streamer) EnsureActiveForDownload(ctx context.Context, magnet string, ds DownloadStorageSpec) (metainfo.Hash, error)

EnsureActiveForDownload is EnsureActive for the download-to-bulk path: it adds the magnet writing data straight to ds.BaseDir on bulk storage and returns the info hash. Mirrors EnsureActive otherwise (metadata cache + favorites stay consistent via the shared Add pipeline).

func (*Streamer) ExtractArtwork

func (s *Streamer) ExtractArtwork(ctx context.Context, hash metainfo.Hash, fileIdx int) ([]byte, bool, error)

ExtractArtwork pulls the embedded cover-art picture stream out of an audio file (MP3 APIC frame, FLAC PICTURE block, M4A covr atom, etc.) via ffmpeg and caches the JPEG on disk. Subsequent requests hit the disk cache for free.

Returns (jpegBytes, fromCache, error). Empty bytes + nil error means the file has no embedded artwork — caller should serve a fallback placeholder.

func (*Streamer) ExtractSubtitle

func (s *Streamer) ExtractSubtitle(ctx context.Context, hash metainfo.Hash, fileIdx, trackIdx int) ([]byte, error)

func (*Streamer) ExtractThumbnail

func (s *Streamer) ExtractThumbnail(ctx context.Context, hash metainfo.Hash, fileIdx int, atSeconds int) ([]byte, bool, error)

ExtractSubtitle pulls one embedded text-subtitle track out of the file as WebVTT. Image-based subs (PGS, DVD) are rejected — those need burn-in via transcoding. trackIdx must be the absolute stream index from Probe()'s Subtitles[i].Index. ExtractThumbnail seeks `atSeconds` into the given file and grabs a single frame as JPEG. Cached on disk under .thumbs/{hash}/{file}/{bucket}.jpg where bucket = round(seconds / 10), so consecutive hover positions reuse the same thumb and the disk doesn't explode. Resolution capped at 240 wide to keep payloads tiny (~20 KB each).

Returns (jpeg, fromCache, error). Empty bytes + nil error means we couldn't decode at that timestamp (rare — likely seeking past the end). The handler translates that into HTTP 204.

func (*Streamer) Favorites

func (s *Streamer) Favorites() *FavoritesStore

Favorites returns the attached store (may be nil). Nil-safe receiver — when the streamer itself failed to init (some tests / degraded boots), handlers can still call s.Favorites() without panicking.

func (*Streamer) FileReader

func (s *Streamer) FileReader(hash metainfo.Hash, fileIdx int) (io.ReadSeekCloser, *torrent.File, error)

FileReader returns a ReadSeeker for one file, configured for streaming. The reader keeps the torrent alive (refreshes lastAccess on each read).

func (*Streamer) FileRelPath

func (s *Streamer) FileRelPath(h metainfo.Hash, fileIdx int) string

FileRelPath resolves the torrent-relative path of file fileIdx — the same shape anacrolix File.Path() returns ("<name>/<sub>/<file>" on multi-file torrents, "<name>" on single-file ones) — WITHOUT activating the torrent. Sources, in order: the persisted metadata cache (filled on every Add), then the cached .torrent on disk. Returns "" when neither knows the torrent.

Why it exists: a whole-torrent download persists ONE completed row whose file_path is a directory; mapping a *file index* into that tree needs the in-torrent path, and resolving it from the live torrent would mean re-adding it to the swarm — the exact thing serving finished downloads from disk avoids (a dead swarm would block playback despite the bytes being local).

func (*Streamer) Get

func (s *Streamer) Get(hash metainfo.Hash) (*TorrentInfo, error)

Get returns the current TorrentInfo for an active torrent.

func (*Streamer) GlobalStats

func (s *Streamer) GlobalStats() GlobalRate

GlobalStats returns aggregate download/upload rates across all active torrents. Snapshot taken under the streamer lock; safe to poll from a handler.

func (*Streamer) HasFilePathResolver

func (s *Streamer) HasFilePathResolver() bool

HasFilePathResolver reports whether the file-path resolver has been wired yet. Boot-time seed resumption waits on this so relocatedStorage (which needs the resolver to locate moved files) doesn't lose a race against the resolver being set — otherwise a resumed seed would fall back to the empty cache storage and show 0%.

func (*Streamer) HealthSnapshot

func (s *Streamer) HealthSnapshot(hash metainfo.Hash) (health *CachedHealth, active bool)

HealthSnapshot returns the cheapest available swarm health: live stats when the torrent is active (also refreshing the persisted copy), else the last probe (nil if never probed). Never touches the swarm.

func (*Streamer) ImportTorrentBytes

func (s *Streamer) ImportTorrentBytes(data []byte) (hash, name string, err error)

ImportTorrentBytes parses a raw .torrent file, persists its metainfo to the cache (so a later play skips the DHT round-trip), and returns the info hash + torrent name. Does NOT add the torrent to the active set — the import flow only records a favorite; playback adds it on demand.

func (*Streamer) IsDownloadProtected

func (s *Streamer) IsDownloadProtected(name string) bool

IsDownloadProtected reports whether `name` is currently in the download protection set. Used by tests + the cache eviction code.

anacrolix grava arquivos SINGLE-FILE como "<name>.part" enquanto o download não terminou — ao mesmo tempo `t.Name()` (que o worker registra) NÃO inclui o sufixo. Sem essa tolerância o enforceCacheLimit passa "<name>.part" e consulta um set que só tem "<name>", então conclui que o arquivo NÃO está protegido e o LRU deleta o .part — anacrolix perde os pieces no disco e recomeça do zero. (Multi-file torrents não sofrem porque o entry é o diretório, cujo nome casa com t.Name() exatamente.)

func (*Streamer) ListenPort

func (s *Streamer) ListenPort() int

func (*Streamer) LiveStats

func (s *Streamer) LiveStats(hash metainfo.Hash) (down, up, uploaded int64, seeders int, ok bool)

LiveStats returns a torrent's current down/up rate + connected seeders WITHOUT building the full file list. buildInfo (used by Get) iterates t.Files() — a 778-file pack walks every file under the client lock, so enriching the downloads list via Get made GET /api/downloads take many SECONDS (worse under active-download lock contention). The list only needs the per-torrent rate/seeders, so this skips the O(files) loop → O(1) per torrent. uploaded is the cumulative bytes served THIS session (anacrolix BytesWrittenData; resets on re-add). ok=false when the torrent isn't active.

func (*Streamer) MatchesSeedTrackerCached

func (s *Streamer) MatchesSeedTrackerCached(hash metainfo.Hash) bool

MatchesSeedTrackerCached reports whether the torrent's CACHED metainfo announce matches a configured seed-tracker, without activating the torrent. Used at boot to decide which completed downloads to auto-reactivate for seeding. Returns false when no metainfo is cached or no seed-trackers are configured.

func (*Streamer) MetadataCache

func (s *Streamer) MetadataCache() *MetadataCache

MetadataCache returns the attached cache (may be nil).

func (*Streamer) MetainfoPath

func (s *Streamer) MetainfoPath(h metainfo.Hash) string

func (*Streamer) OSHash

func (s *Streamer) OSHash(ctx context.Context, hash metainfo.Hash, fileIdx int) (HashResult, error)

OSHash returns the OpenSubtitles hash for one file in an active torrent. Blocks until the hash is computed or ctx expires. Caches the result. Returns ("", 0, error) if the file isn't streamable as hash (too small, not active, timeout).

func (*Streamer) ParseMagnet

func (s *Streamer) ParseMagnet(magnet string) (hash, name string, err error)

ParseMagnet validates a magnet URI and extracts its info hash + display name without touching the network. Used by the import flow to preview what a pasted magnet resolves to before committing it to favorites.

func (*Streamer) Pause

func (s *Streamer) Pause(hash metainfo.Hash) error

Pause soft-pauses a torrent by zeroing its max established connections. anacrolix lacks a native Pause; this is the closest equivalent — existing peers drop off as TCP keepalives expire, and no new peers are accepted. On-disk pieces stay, so Resume picks up where we left off.

func (*Streamer) PauseAll

func (s *Streamer) PauseAll() int

PauseAll soft-pauses every active torrent. Returns the count of newly paused torrents (already-paused ones are not double-counted).

func (*Streamer) Peers

func (s *Streamer) Peers(hash metainfo.Hash) ([]PeerInfo, error)

Peers returns a snapshot of the currently-connected peers of an active torrent for the downloads inspector. Errors when the torrent isn't active (dropped or never opened). The peer set is read live from anacrolix.

func (*Streamer) Prefetch

func (s *Streamer) Prefetch(hash metainfo.Hash, fileIdx int) error

Prefetch hints the anacrolix piece scheduler to start downloading the head of `fileIdx` on the already-active torrent, *without* serving any bytes back.

Use case: while the user watches episode N of a series, we kick off pieces of N+1 in the background so the cut between episodes is near-instantaneous. Same idea for the next item in a playlist when it's the same torrent.

Implementation: opens a Reader, seeks to 0, sets a generous readahead, reads a small head chunk, then closes after a short delay so the priority hint outlives the request lifecycle. The bytes already on disk stay there — only the in-memory priority hint goes away when the reader closes.

Returns immediately; the actual download is asynchronous in anacrolix.

func (*Streamer) Probe

func (s *Streamer) Probe(ctx context.Context, hash metainfo.Hash, fileIdx int) (ProbeResult, error)

Probe runs ffprobe on the torrent file and lists audio + subtitle tracks. Reads at most 16 MB from the start of the file (enough for MKV/MP4 headers). Results are cached per (torrent, file).

func (*Streamer) ProbeHealthAsync

func (s *Streamer) ProbeHealthAsync(hash metainfo.Hash, magnet string)

ProbeHealthAsync runs a background swarm probe for an INACTIVE torrent: scrape the trackers (magnet tr= + cached .torrent announce list) for the real swarm size, persist it, falling back to a brief swarm-connect count. Throttled + deduped. No-op when there's no tracker source or a probe is already running.

func (*Streamer) RateLimits

func (s *Streamer) RateLimits() (down, up int64)

RateLimits exposes the configured global bandwidth caps in bytes/sec. A value of 0 means unlimited.

func (*Streamer) ReadArtBytes

func (s *Streamer) ReadArtBytes(rel string) ([]byte, error)

ReadArtBytes returns the cached art file for a DataDir-relative path produced by SaveArtBytes. The path is validated to stay within the .art dir so a crafted CachedArt.Path can't read arbitrary files.

func (*Streamer) ReadSidecar

func (s *Streamer) ReadSidecar(ctx context.Context, hash metainfo.Hash, fileIdx int) ([]byte, string, error)

ReadSidecar fetches the full subtitle file by index. Returns the raw bytes — caller is responsible for format conversion (e.g., SRT → VTT).

func (*Streamer) RecheckAllFiles

func (s *Streamer) RecheckAllFiles(hash metainfo.Hash) error

RecheckAllFiles força o "Force Recheck" em TODOS os arquivos de um torrent (download de torrent inteiro). Mesmo contrato do RecheckFile; os arquivos são re-hashados sequencialmente numa única goroutine — um torrent de milhares de arquivos não pode disparar milhares de hash loops concorrentes.

func (*Streamer) RecheckFile

func (s *Streamer) RecheckFile(hash metainfo.Hash, fileIdx int) error

RecheckFile força uma re-verificação completa dos pieces de um arquivo, IGNORANDO o dedup do verifiedFiles e re-hashando até pieces marcados como "complete" no momento. Caso de uso: ação manual do user via UI ("recheck") quando ele suspeita que os bytes no disco estão corrompidos (BitErrors) ou quando o tamanho/contagem do downloads.db não bate com o real. Diferente do VerifyFile, que pula pieces já completos e dedupa por processo, aqui valida tudo de novo — semantics equivalente ao "Force Recheck" do qBittorrent. Roda em goroutine porque um filme grande leva minutos.

func (*Streamer) RegisterDownload

func (s *Streamer) RegisterDownload(name string)

RegisterDownload marks a torrent (by directory name == torrent.Name()) as part of an in-progress background download. While registered, its on-disk pieces are protected from enforceCacheLimit eviction even if no one is streaming. Idempotent.

func (*Streamer) ReleaseViewer

func (s *Streamer) ReleaseViewer(hash metainfo.Hash) (scheduled, lastViewer bool)

ReleaseViewer drops a player session's lease. When the LAST viewer leaves a stream-only (non-download) torrent, it schedules a drop after viewerGrace instead of dropping eagerly — so other viewers keep streaming and a quick reopen cancels the teardown.

Returns (scheduled, lastViewer). scheduled is true when a drop was scheduled. lastViewer is true whenever THIS call removed the final viewer — even when the torrent is kept alive (background download or seed-tracker): the HLS transcode exists only to feed the player, so the caller must stop it once nobody is watching, while the torrent keeps seeding/downloading on its own.

func (*Streamer) Resume

func (s *Streamer) Resume(hash metainfo.Hash) error

Resume re-enables peer connections previously zeroed by Pause. Idempotent.

func (*Streamer) ResumeAll

func (s *Streamer) ResumeAll() int

ResumeAll re-enables every soft-paused torrent.

func (*Streamer) SaveArtBytes

func (s *Streamer) SaveArtBytes(hash metainfo.Hash, data []byte) (string, error)

SaveArtBytes writes resolved art (torrent image or captured frame) under the .art cache dir and returns the DataDir-relative path to persist in CachedArt.

func (*Streamer) SetFavorites

func (s *Streamer) SetFavorites(f *FavoritesStore)

SetFavorites attaches the favorites store. Must be called before any GC tick.

func (*Streamer) SetFilePathResolver

func (s *Streamer) SetFilePathResolver(r FilePathResolver)

SetFilePathResolver registers the custom file path resolver function (typically querying the downloads DB).

func (*Streamer) SetFilePriority

func (s *Streamer) SetFilePriority(hash metainfo.Hash, fileIdx int, label string) error

SetFilePriority changes the priority of a single file in the active torrent.

func (*Streamer) SetMetadataCache

func (s *Streamer) SetMetadataCache(c *MetadataCache)

SetMetadataCache attaches the metadata snapshot cache. Optional — when set, every successful Add() persists the file list so the UI can render it instantly next time the same info_hash is opened.

func (*Streamer) SetPriority

func (s *Streamer) SetPriority(hash metainfo.Hash, label string) error

SetPriority changes the requested piece priority for every file in the torrent. anacrolix uses this to bias the request scheduler — "high" pieces will be fetched before "normal", which precede "low". Accepted labels: "low" | "normal" | "high".

func (*Streamer) SetRateLimits

func (s *Streamer) SetRateLimits(down, up int64)

SetRateLimits updates the global download/upload bandwidth caps in bytes/sec. 0 = unlimited. Takes effect immediately — anacrolix re-reads the limiter on every chunk transfer.

func (*Streamer) SetSeedTrackers

func (s *Streamer) SetSeedTrackers(trackers []string)

SetSeedTrackers replaces the live list of tracker substrings whose torrents must keep seeding. Applied immediately; matched case-insensitively against announce URLs. Safe to call at runtime (e.g. from the settings endpoint).

func (*Streamer) SetSeeds

func (s *Streamer) SetSeeds(st *SeedsStore)

SetSeeds attaches the persistent seed store (info_hash → magnet) so torrents kept alive for seeding are re-added on boot. Optional — nil disables persistence (seeding still works in-memory until the process exits).

func (*Streamer) SetStreamReadahead

func (s *Streamer) SetStreamReadahead(mb int)

SetStreamReadahead atualiza ao vivo o readahead de streaming (em MB). Vale a partir do próximo Reader aberto. mb<=0 volta ao default. Não exige reinício.

func (*Streamer) Sidecars

func (s *Streamer) Sidecars(hash metainfo.Hash, videoFileIdx int) ([]SidecarSubtitle, error)

Sidecars returns subtitle files inside the torrent. If videoFileIdx >= 0, results are ranked by path proximity to that video file (same directory first, then sibling 'subs' dirs, then anywhere).

func (*Streamer) Stats

func (s *Streamer) Stats() (*CacheStats, error)

Stats walks the DataDir and returns disk usage stats. "Active" entries are torrents currently loaded in memory (likely being read).

func (*Streamer) StreamReadaheadForTesting

func (s *Streamer) StreamReadaheadForTesting() int64

StreamReadaheadForTesting expõe o readahead efetivo (em bytes) para testes de outros pacotes verificarem que um setter foi aplicado.

func (*Streamer) TorrentImage

func (s *Streamer) TorrentImage(ctx context.Context, hash metainfo.Hash) ([]byte, string, error)

func (*Streamer) TrackerStats

func (s *Streamer) TrackerStats(ctx context.Context, hash metainfo.Hash, magnet string) []TrackerScrape

TrackerStats scrapes every known tracker (magnet tr= + the cached .torrent's announce list, which carries a private tracker's passkey) and returns the per-tracker swarm sizes for the info panel. Tracker hosts are masked — the passkey is used to scrape but never returned.

func (*Streamer) UnregisterDownload

func (s *Streamer) UnregisterDownload(name string)

UnregisterDownload removes the eviction protection. Call after the download completes or is cancelled. Idempotent.

func (*Streamer) UpdateJackettHost

func (s *Streamer) UpdateJackettHost(rawURL string)

UpdateJackettHost refreshes the trusted Jackett hostname used by the SSRF guard. Called after the user changes the Jackett URL via the API.

func (*Streamer) VerifyFile

func (s *Streamer) VerifyFile(hash metainfo.Hash, fileIdx int) error

VerifyFile is the exported entrypoint para o worker de downloads disparar a reconciliação de pieces no disco antes de pedir mais dados ao swarm. Reusa o mesmo dedupe set (`verifiedFiles`) que o caminho de streaming, então a verificação acontece NO MÁXIMO uma vez por (hash, file) por processo — não importa se foi streaming ou download que disparou primeiro.

Background: anacrolix tradicionalmente não re-verifica em startup; confia no bolt DB. Se o shutdown anterior foi ungraceful (SIGKILL, container OOM), o bolt fica desatualizado e anacrolix "esquece" pieces que estão no disco. Sem essa chamada, o worker pede ao swarm bytes que já temos.

func (*Streamer) VerifyTorrent

func (s *Streamer) VerifyTorrent(hash metainfo.Hash) error

VerifyTorrent reconciles on-disk pieces for EVERY file of a torrent — the whole-torrent download path. Same rationale and per-(hash,file) dedupe as VerifyFile, applied file by file (sequencial: custo proporcional ao que está no disco; pieces ausentes falham o hash rápido via sparse reads).

type TorrentInfo

type TorrentInfo struct {
	InfoHash  string     `json:"infoHash"`
	Name      string     `json:"name"`
	TotalSize int64      `json:"totalSize"`
	Files     []FileInfo `json:"files"`
	Peers     int        `json:"peers"`
	Seeders   int        `json:"seeders"`
	DownRate  int64      `json:"downRate"` // bytes/sec, sampled between polls
	UpRate    int64      `json:"upRate"`   // bytes/sec, sampled between polls
	// Cumulative payload byte counters. BytesDownloaded is the completed bytes of
	// the selected pieces; BytesUploaded is what we've served this SESSION (the
	// anacrolix counter resets when the torrent is re-added — e.g. after a restart).
	BytesDownloaded int64   `json:"bytesDownloaded"`
	BytesUploaded   int64   `json:"bytesUploaded"`
	Progress        float64 `json:"progress"`
	PrimaryFile     int     `json:"primaryFile"` // suggested video file index
	// Status is one of "downloading", "paused", "seeding", "complete".
	// Surfaced for the Transmission-style downloads UI.
	Status string `json:"status,omitempty"`
	// Priority is the user-set piece priority ("low" | "normal" | "high"); empty
	// when the user has not changed it from the anacrolix default.
	Priority string   `json:"priority,omitempty"`
	Trackers []string `json:"trackers,omitempty"`
}

TorrentInfo is the JSON-friendly view returned to the frontend.

type Track

type Track struct {
	Index    int    `json:"index"`              // absolute stream index in the container (use with `-map 0:N`)
	Type     string `json:"type"`               // "audio" | "subtitle"
	Codec    string `json:"codec"`              // e.g. "aac", "ac3", "subrip", "ass", "hdmv_pgs_subtitle"
	Language string `json:"language,omitempty"` // ISO 639-2 (e.g. "por", "eng") from container tags
	Title    string `json:"title,omitempty"`    // human-friendly title set by uploader, if any
	Default  bool   `json:"default"`
	Forced   bool   `json:"forced,omitempty"`
	Channels int    `json:"channels,omitempty"`
	Image    bool   `json:"image,omitempty"` // true if subtitle is image-based (PGS, DVD) — needs burn-in
}

Track describes one audio or subtitle stream inside a container.

type TrackerScrape

type TrackerScrape struct {
	Tracker  string `json:"tracker"`
	Seeders  int    `json:"seeders"`
	Leechers int    `json:"leechers"`
	OK       bool   `json:"ok"`
}

TrackerScrape is one tracker's reported swarm size. Tracker is the masked display name (host only — never the passkey-bearing URL). OK is false when the tracker didn't answer or doesn't know the torrent.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL