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.
The persisted JSON schema (field names, types, tags) is an inviolate contract — the on-disk /config/cache.json file is read-forward / write-back across deploys, so any schema change is a migration, not a refactor.
Index ¶
- Constants
- func DecryptToken(key []byte, value string) (string, error)
- func DeriveKey(plexToken string) ([]byte, error)
- func EncryptToken(key []byte, plaintext string) (string, error)
- func IsEncrypted(value string) bool
- type Cache
- func (c *Cache) CheckAndMark(key string) bool
- func (c *Cache) LastSchedulerRun() time.Time
- func (c *Cache) LearnLanguageProfile(userID, audioLang, subtitleLang string)
- func (c *Cache) LoadFrom(path string) error
- func (c *Cache) MarkProcessed(key string)
- func (c *Cache) SaveTo(path string) error
- func (c *Cache) SetEncryptionKey(key []byte)
- func (c *Cache) SetLastSchedulerRun(t time.Time)
- func (c *Cache) SetUserTokens(tokens map[string]string)
- func (c *Cache) SubtitleLangForAudio(userID, audioLang string) (string, bool)
- func (c *Cache) UserTokens() map[string]string
- func (c *Cache) WasRecentlyProcessed(key string) bool
- type Data
Constants ¶
const ( KeyPrefixTimeline = "timeline:" KeyPrefixScheduler = "scheduler:" KeyPrefixStreams = "streams:" )
Cache-key prefix constants. The exact literal values are part of the on-disk cache.json schema (inviolate contract item 7) — do NOT change them without a migration.
Variables ¶
This section is empty.
Functions ¶
func DecryptToken ¶ added in v1.2.0
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
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
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
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.4.0
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) LastSchedulerRun ¶
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 ¶
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) LoadFrom ¶
LoadFrom reads the cache from the given path. A missing file returns nil (fresh start). Capped at maxCacheSize bytes via atomicfile.ReadBounded. Warns if the file has permissive mode bits set, as tokens are stored here.
func (*Cache) MarkProcessed ¶
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) SaveTo ¶
SaveTo atomically writes the cache to the given path (temp file + rename) and ensures the final file is 0o600 (user tokens live here). The temp file is removed on any failure path so partial writes don't clutter the dir. User tokens are encrypted at the disk boundary if an encryption key is configured; the in-memory Data.UserTokens map stays plaintext.
The marshal runs under c.mu (via encodeForSave) for a consistent snapshot; the disk write runs lock-free so a concurrent MarkProcessed / WasRecentlyProcessed (the listener goroutine) never blocks on the scheduler goroutine's fsync.
func (*Cache) SetEncryptionKey ¶ added in v1.2.0
SetEncryptionKey configures the AES-256 key used to encrypt user tokens at the disk boundary (SaveTo/LoadFrom). 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 ¶
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 ¶
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 ¶
SubtitleLangForAudio returns the learned subtitle language for a given audio language and user. Returns ("", false) if no profile exists.
func (*Cache) UserTokens ¶
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 ¶
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).
ProcessedEpisodes map[string]int64 `json:"processed_episodes"`
// LanguageProfiles maps userID → audioLang → subtitleLang.
// Empty subtitle string means "no subtitles" for that audio language.
LanguageProfiles map[string]map[string]string `json:"language_profiles"`
// UserTokens maps userID → accessToken for shared users.
UserTokens map[string]string `json:"user_tokens"`
// LastSchedulerRun is the unix timestamp of the last scheduler run.
LastSchedulerRun int64 `json:"last_scheduler_run"`
}
Data is the JSON schema persisted to /config/cache.json. Field names and JSON tags are frozen — the on-disk file is read-forward across deploys.