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
- 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) IntentFor(userID, showKey string) (streams.Intent, bool)
- func (c *Cache) LastSchedulerRun() time.Time
- func (c *Cache) LearnLanguageProfile(userID, audioLang, subtitleLang string)
- func (c *Cache) Load(dir string) error
- func (c *Cache) MarkProcessed(key string)
- func (c *Cache) RecordIntent(userID, showKey string, intent *streams.Intent)
- func (c *Cache) Save(dir 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 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
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.3.6
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
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 ¶
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) Load ¶ added in v1.6.3
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 ¶
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
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
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
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 ¶
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). 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).