Documentation
¶
Overview ¶
Package watchlist persists per-user search queries that the server polls in the background. New results above the user's seeders threshold trigger a push notification to the user's ntfy.sh topic.
Index ¶
- Constants
- type Enqueuer
- type Hit
- type Notifier
- type NtfyPoster
- type Params
- type Schedule
- type Searcher
- type Store
- func (s *Store) Close()
- func (s *Store) Create(userID int, p Params) (*Watchlist, error)
- func (s *Store) Delete(userID, id int) error
- func (s *Store) Get(userID, id int) (*Watchlist, error)
- func (s *Store) Hits(userID, watchlistID, limit int) ([]Hit, error)
- func (s *Store) List(userID int) ([]Watchlist, error)
- func (s *Store) ListAll() ([]Watchlist, error)
- func (s *Store) ListDue(now time.Time) ([]Watchlist, error)
- func (s *Store) MarkAutoDownloaded(watchlistID int, infoHash string) error
- func (s *Store) MarkChecked(watchlistID int, next time.Time) error
- func (s *Store) MarkSeen(watchlistID int, infoHash, title, magnet string, seeders int, size int64) (isNew bool, err error)
- func (s *Store) Update(userID, id int, p Params) error
- type UserNotifier
- type Watchlist
- type Worker
Constants ¶
const ( SchedInterval = "interval" SchedDaily = "daily" SchedWeekly = "weekly" )
Schedule kinds. "interval" re-checks every N minutes; "daily" checks once a day at HH:MM; "weekly" checks once a week on Weekday at HH:MM.
const DefaultInterval = 15 * time.Minute
DefaultInterval is the fallback cadence used when an interval schedule has Minutes <= 0 ("use the server default") and no global default was configured.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Enqueuer ¶
Enqueuer pushes a hit straight into the downloads queue — implemented by downloads.(*Store).EnqueueMagnet. Kept as an interface so tests can record calls without a real store.
type Hit ¶
type Hit struct {
InfoHash string `json:"infoHash"`
Title string `json:"title"`
Magnet string `json:"magnet"`
Seeders int `json:"seeders"`
Size int64 `json:"size"`
SeenAt time.Time `json:"seenAt"`
AutoDownloaded bool `json:"autoDownloaded"`
}
Hit is a single new torrent detected by the worker for a given watchlist.
type Notifier ¶
Notifier publishes the new hit to the user's chosen channel. Implementations: the live ntfy.sh poster (NtfyPoster) and a test-friendly recorder.
type NtfyPoster ¶
type NtfyPoster struct {
BaseURL string // default "https://ntfy.sh"
Token string // optional access token for protected topics (Authorization: Bearer)
Client *http.Client
}
NtfyPoster posts to https://ntfy.sh/<topic> with a magnet click-action. Uses the public ntfy.sh by default; can point to a self-hosted instance.
type Params ¶
type Params struct {
Query string
Category string
MinSeeders int
NtfyTopic string
Schedule // per-item check schedule
AutoDownload bool
MinResolution string
MaxSizeBytes int64
Codec string
}
Params carries the user-editable fields of a watchlist for Create/Update.
type Schedule ¶
type Schedule struct {
Kind string `json:"schedKind"`
Minutes int `json:"schedMinutes"` // interval: every N minutes; <= 0 means "server default"
Weekday int `json:"schedWeekday"` // weekly: 0=Sunday … 6=Saturday (time.Weekday)
Hour int `json:"schedHour"` // daily/weekly: 0–23
Minute int `json:"schedMinute"` // daily/weekly: 0–59
}
Schedule describes when a watchlist should be re-checked. The fields are flattened into the watchlists table (sched_* columns) and into the JSON API.
func (Schedule) Normalized ¶
Normalized clamps out-of-range values so the DB only ever holds a schedule nextCheckTime can act on. Minutes <= 0 is preserved on purpose: it means "use the server-wide default interval". Exported because the AI schedule parser (handlers) clamps the model's output through the same single path.
type Searcher ¶
type Searcher interface {
Search(query, category string, indexers []string) ([]jackett.Result, error)
}
Searcher is the subset of jackett.Client the worker depends on. Stays small so unit tests can fake it without spinning up an HTTP server.
type Store ¶
type Store struct {
// DefaultEvery is the server-wide interval applied to items whose schedule
// is "interval" with Minutes <= 0 ("server default"). Set once at boot from
// the config (notifications.watchlist_minutes); zero falls back to 15 min.
DefaultEvery time.Duration
// contains filtered or unexported fields
}
func New ¶
New wires the watchlist store onto the shared Postgres pool. Schema is applied centrally (internal/db migrations).
func (*Store) Close ¶
func (s *Store) Close()
Close is a no-op: the shared pool's lifecycle is owned by main.
func (*Store) Create ¶
Create inserts a new watchlist row. The schedule is normalized and next_check_at is computed from it so the worker knows when the item is due.
func (*Store) ListAll ¶
ListAll returns every watchlist across all users — used by manual re-checks.
func (*Store) ListDue ¶
ListDue returns the watchlists whose next check is due at `now`. Rows with a NULL next_check_at (pre-migration) are due immediately.
func (*Store) MarkAutoDownloaded ¶
MarkAutoDownloaded flags a seen hit as auto-enqueued, for display in the UI.
func (*Store) MarkChecked ¶
MarkChecked refreshes last_checked to now and re-arms next_check_at — call after a worker pass over the item.
func (*Store) MarkSeen ¶
func (s *Store) MarkSeen(watchlistID int, infoHash, title, magnet string, seeders int, size int64) (isNew bool, err error)
MarkSeen records an info_hash as already seen. Returns true if this was the first time (i.e., a new hit), false if it was already known.
type UserNotifier ¶
type UserNotifier interface {
NotifyUser(ctx context.Context, userID int, title, body, magnet string) error
}
UserNotifier delivers a hit to the user's own channels (in-app feed + Web Push) — implemented by push.(*Sender). Runs alongside ntfy, which remains topic-based.
type Watchlist ¶
type Watchlist struct {
ID int `json:"id"`
UserID int `json:"userId"`
Query string `json:"query"`
Category string `json:"category"` // optional Jackett category filter
MinSeeders int `json:"minSeeders"`
NtfyTopic string `json:"ntfyTopic"` // optional override of the global default
Schedule // per-item check schedule (sched_* columns)
NextCheckAt time.Time `json:"nextCheckAt"` // when the worker should check this item next
LastChecked time.Time `json:"lastChecked"`
CreatedAt time.Time `json:"createdAt"`
HitCount int `json:"hitCount,omitempty"` // computed from watchlist_seen
// Auto-download: when enabled, new hits that pass the quality filters are
// enqueued straight into the downloads queue instead of just notifying.
AutoDownload bool `json:"autoDownload"`
MinResolution string `json:"minResolution"` // "", "480p", "720p", "1080p", "2160p"
MaxSizeBytes int64 `json:"maxSizeBytes"` // 0 = unlimited
Codec string `json:"codec"` // "", "x264", "x265", "av1"
}
Watchlist is one saved search the worker polls on its own schedule.
type Worker ¶
type Worker struct {
// contains filtered or unexported fields
}
Worker is a per-item scheduler: every tick (1 min) it checks only the watchlists whose next_check_at is due, then re-arms them from their own schedule. Kick(id) short-circuits the wait for a single item (used right after create so the user gets instant feedback). Stateless; safe to stop+restart.
func (*Worker) Kick ¶
Kick schedules an immediate background check of one watchlist (e.g. right after create). Non-blocking: if the buffer is full the regular scheduled pass covers it. Safe on a nil Worker so handlers can stay nil-tolerant.
func (*Worker) RunOnce ¶
func (w *Worker) RunOnce()
RunOnce checks every watchlist regardless of schedule — tests / manual triggers.
func (*Worker) SetEnqueuer ¶
SetEnqueuer enables auto-download by wiring the downloads queue. Call before Start — the worker goroutine reads the field without a lock.
func (*Worker) SetUserNotifier ¶
func (w *Worker) SetUserNotifier(n UserNotifier)
SetUserNotifier wires the per-user channel (in-app feed + Web Push). Call before Start — the worker goroutine reads the field without a lock.