watchlist

package
v0.78.2 Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: MIT Imports: 13 Imported by: 0

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

View Source
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.

View Source
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

type Enqueuer interface {
	EnqueueMagnet(userID int, infoHash, name, magnet, tracker string) error
}

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

type Notifier interface {
	Notify(ctx context.Context, topic, title, body, magnet string) error
}

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.

func (*NtfyPoster) Notify

func (n *NtfyPoster) Notify(ctx context.Context, topic, title, body, magnet string) error

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

func (s Schedule) Normalized() Schedule

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

func New(pool *sql.DB) (*Store, error)

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

func (s *Store) Create(userID int, p Params) (*Watchlist, error)

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) Delete

func (s *Store) Delete(userID, id int) error

Delete removes a watchlist (and cascades the seen rows via FK).

func (*Store) Get

func (s *Store) Get(userID, id int) (*Watchlist, error)

Get returns a single watchlist owned by userID.

func (*Store) Hits

func (s *Store) Hits(userID, watchlistID, limit int) ([]Hit, error)

Hits returns the most recent hits for a watchlist owned by userID.

func (*Store) List

func (s *Store) List(userID int) ([]Watchlist, error)

List returns all watchlists for a user, newest first, with hit counts.

func (*Store) ListAll

func (s *Store) ListAll() ([]Watchlist, error)

ListAll returns every watchlist across all users — used by manual re-checks.

func (*Store) ListDue

func (s *Store) ListDue(now time.Time) ([]Watchlist, error)

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

func (s *Store) MarkAutoDownloaded(watchlistID int, infoHash string) error

MarkAutoDownloaded flags a seen hit as auto-enqueued, for display in the UI.

func (*Store) MarkChecked

func (s *Store) MarkChecked(watchlistID int, next time.Time) error

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.

func (*Store) Update

func (s *Store) Update(userID, id int, p Params) error

Update modifies an existing watchlist owned by userID. A schedule change recomputes next_check_at immediately. Changing the query resets last_checked so the next worker pass re-baselines (marks the current results as seen) instead of auto-downloading the whole new result set.

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.

func (*Watchlist) MatchesFilters

func (w *Watchlist) MatchesFilters(title string, size int64) bool

MatchesFilters reports whether a release title/size passes this watchlist's auto-download quality filters. Seeders are checked by the caller (the same min-seeders gate also guards notifications).

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 NewWorker

func NewWorker(store *Store, searcher Searcher, notifier Notifier, defaultTopic string, interval time.Duration) *Worker

func (*Worker) Kick

func (w *Worker) Kick(id int)

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

func (w *Worker) SetEnqueuer(e Enqueuer)

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.

func (*Worker) Start

func (w *Worker) Start()

func (*Worker) Stop

func (w *Worker) Stop()

Stop signals the worker and waits for its goroutine to exit. sync.Once makes a second call safe (double close(stop) would otherwise panic); WaitGroup ensures no runOnce is still touching the stores after Stop returns.

Jump to

Keyboard shortcuts

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