cache

package
v1.6.3 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: GPL-2.0, GPL-3.0 Imports: 20 Imported by: 0

Documentation

Overview

Package cache is the on-disk persistence layer for processed-episode deduplication, per-user language profiles, shared-user tokens, and the scheduler's last-run marker.

On-disk layout: state is split across three files in the cache dir by retention class, so one corrupt file never costs another class's state:

profiles.json  language_profiles                     irreplaceable learned state
tokens.json    user_tokens                           re-fetchable encrypted secrets
state.json     processed_episodes, last_scheduler_run disposable operational state

Field names, types, and JSON tags within each file are an inviolate read-forward / write-back contract across deploys — any change is a migration, not a refactor. A pre-split /config/cache.json (the legacy union schema, see Data) is migrated automatically on first load: its sections seed anything a split file does not yet cover, the split files are then written eagerly, and the legacy file is removed once all three split files exist on disk. Per-section precedence is split-file > legacy > fresh, so a partially failed migration can never lose a section that either source still holds.

Index

Constants

View Source
const (
	KeyPrefixTimeline  = "timeline:"
	KeyPrefixScheduler = "scheduler:"
	KeyPrefixStreams   = "streams:"
)

Cache-key prefix constants. The exact literal values are part of the on-disk state.json schema (inviolate contract item 7; legacy cache.json carries the same prefixes) — do NOT change them without a migration.

Variables

This section is empty.

Functions

func DecryptToken added in v1.2.0

func DecryptToken(key []byte, value string) (string, error)

DecryptToken reverses EncryptToken. If the value does not carry the "enc:" prefix (legacy plaintext), it is returned unchanged — this enables transparent migration of pre-encryption cache files.

func DeriveKey added in v1.2.0

func DeriveKey(plexToken string) ([]byte, error)

DeriveKey produces a 32-byte AES-256 key from the admin PLEX_TOKEN using HKDF-SHA256 with a fixed salt and info string. The output is deterministic for a given token — no plex.tv round-trip is needed for decryption on restart.

func EncryptToken added in v1.2.0

func EncryptToken(key []byte, plaintext string) (string, error)

EncryptToken encrypts a plaintext token using AES-256-GCM with a random 12-byte nonce. Returns "enc:" + base64url(nonce || ciphertext).

func IsEncrypted added in v1.2.0

func IsEncrypted(value string) bool

IsEncrypted reports whether a stored value carries the encryption prefix, indicating it was produced by EncryptToken.

Types

type Cache

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

Cache is the concurrent-safe persistent cache. The zero value is usable; prefer New for explicit initialization of the backing maps.

func New

func New() *Cache

New returns a Cache with its maps pre-initialized. The zero value also works (maps are lazily created by the mutation methods) — New is the preferred construction point for application code because it documents intent.

func (*Cache) CheckAndMark added in v1.3.6

func (c *Cache) CheckAndMark(key string) bool

CheckAndMark atomically tests and sets the recent-processed window for key: it returns true and records the current time when key was not processed within the last 5 minutes, or false (leaving the existing timestamp intact) when it was. The check and the mark happen under a single lock acquisition so two concurrent callers cannot both observe "not processed" for the same key — unlike a WasRecentlyProcessed-then-MarkProcessed sequence, which has a TOCTOU gap between its two separate lock acquisitions. Inline-prunes old entries when the map grows past 10000 entries.

func (*Cache) IntentFor added in v1.6.3

func (c *Cache) IntentFor(userID, showKey string) (streams.Intent, bool)

IntentFor returns the recorded intent for a (user, show) pair, deep- copied so callers cannot mutate cache state. ok=false when no intent has been observed for the pair.

func (*Cache) LastSchedulerRun

func (c *Cache) LastSchedulerRun() time.Time

LastSchedulerRun returns the timestamp of the last deep-analysis run. A zero time.Time indicates the scheduler has never run (fresh install or cache reset).

func (*Cache) LearnLanguageProfile

func (c *Cache) LearnLanguageProfile(userID, audioLang, subtitleLang string)

LearnLanguageProfile records a user's audio→subtitle language preference. Empty audioLang is treated as "unknown" and ignored — this prevents the profile map from accumulating an empty-key entry for streams whose language is not reported by Plex.

func (*Cache) Load added in v1.6.3

func (c *Cache) Load(dir string) error

Load reads the cache from the split-layout files in dir, migrating a legacy cache.json when present. Missing files are a fresh start for their section; a corrupt or oversized file resets ONLY its own section (the others load normally). The returned error joins every per-section failure — a non-nil return means at least one section started fresh, not that the whole cache did.

func (*Cache) MarkProcessed

func (c *Cache) MarkProcessed(key string)

MarkProcessed records the current time against the key. Inline-prunes old entries when the map grows past 10000 entries to keep memory bounded.

func (*Cache) RecordIntent added in v1.6.3

func (c *Cache) RecordIntent(userID, showKey string, intent *streams.Intent)

RecordIntent stores a user's observed track selection for a show, replacing any previous intent for the same (user, show) pair. The intent is deep-copied so the caller retains exclusive ownership of the passed value. A nil intent or empty userID/showKey is ignored — an intent that cannot be keyed is meaningless.

func (*Cache) Save added in v1.6.3

func (c *Cache) Save(dir string) error

Save atomically writes the three split-layout files into dir (each temp file + rename, 0o600 — tokens live in one of them and the uniform mode keeps the contract simple). Encoding for all three files happens first, under one lock acquisition, so the files are a consistent snapshot and an encode failure (e.g. token encryption) writes nothing at all. Disk writes then run lock-free so a concurrent MarkProcessed / WasRecentlyProcessed (the listener goroutine) never blocks on the scheduler goroutine's fsync. Write failures are per-file: the remaining files are still attempted and the joined error is returned.

func (*Cache) SetEncryptionKey added in v1.2.0

func (c *Cache) SetEncryptionKey(key []byte)

SetEncryptionKey configures the AES-256 key used to encrypt user tokens at the disk boundary (Save/Load). The key should be derived from the admin PLEX_TOKEN via DeriveKey. When nil, tokens are stored and read as plaintext (backward-compatible with pre-encryption cache files).

func (*Cache) SetLastSchedulerRun

func (c *Cache) SetLastSchedulerRun(t time.Time)

SetLastSchedulerRun records the supplied timestamp as the most recent scheduler run. Stored as a unix int64 on disk (contract item 7 — persisted schema is frozen).

func (*Cache) SetUserTokens

func (c *Cache) SetUserTokens(tokens map[string]string)

SetUserTokens replaces the stored user-token map wholesale. The supplied map is defensive-copied so callers retain exclusive ownership of the original. Passing nil clears the map to an empty non-nil value.

func (*Cache) SubtitleLangForAudio

func (c *Cache) SubtitleLangForAudio(userID, audioLang string) (string, bool)

SubtitleLangForAudio returns the learned subtitle language for a given audio language and user. Returns ("", false) if no profile exists.

func (*Cache) UserTokens

func (c *Cache) UserTokens() map[string]string

UserTokens returns a defensive copy of the userID → accessToken map. Mutating the returned map does not affect cache state — callers must use SetUserTokens to persist changes.

func (*Cache) WasRecentlyProcessed

func (c *Cache) WasRecentlyProcessed(key string) bool

WasRecentlyProcessed reports whether the given key was marked processed within the last 5 minutes. Used as a short-term dedup window for rapid successive webhook / websocket events on the same episode.

type Data

type Data struct {
	// ProcessedEpisodes tracks recently processed episode keys to avoid
	// re-processing the same episode on rapid successive events.
	// Keys carry a subsystem prefix from keys.go:
	// "streams:{userID}:{ratingKey}:{audioID}:{subID}" (play events),
	// "timeline:{itemID}" (library scans), and "scheduler:{ratingKey}"
	// (deep-analysis runs). Persisted in state.json.
	ProcessedEpisodes map[string]int64 `json:"processed_episodes"`
	// LanguageProfiles maps userID → audioLang → subtitleLang.
	// Empty subtitle string means "no subtitles" for that audio language.
	// Persisted in profiles.json.
	LanguageProfiles map[string]map[string]string `json:"language_profiles"`
	// Intents maps userID → showRatingKey → the user's last observed
	// track selection for that show (see streams.Intent). Persisted in
	// profiles.json; absent from the legacy union schema (pre-intent
	// versions), so legacy migration leaves it empty.
	Intents map[string]map[string]streams.Intent `json:"intents,omitempty"`
	// UserTokens maps userID → accessToken for shared users. Persisted in
	// tokens.json, encrypted at the disk boundary when a key is set.
	UserTokens map[string]string `json:"user_tokens"`
	// LastSchedulerRun is the unix timestamp of the last scheduler run.
	// Persisted in state.json.
	LastSchedulerRun int64 `json:"last_scheduler_run"`
}

Data is the in-memory state shape. It doubles as the decode target for the legacy pre-split cache.json union schema, whose field names and JSON tags it preserves verbatim (read-forward contract).

Jump to

Keyboard shortcuts

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