diff --git a/.punused-ignore b/.punused-ignore index 4206024..2ce4c25 100644 --- a/.punused-ignore +++ b/.punused-ignore @@ -104,21 +104,11 @@ function NewFromHTTP is used in test only # --------------------------------------------------------------------------- -# The Plex stream-type enum, after internal/streams adopted plexapi.Stream. -# Structural: production stopped naming the enum, and the names exist for the -# construction sites. -# -# streams.Stream now embeds plexapi.Stream, which promotes the two predicates -# this package used to declare byte-identically (IsAudio, IsSubtitle) โ€” and -# those two were the last production readers of the local enum. Production -# reads a stream's kind only through the promoted methods now, so the three -# names below are named by tests alone: 145 construction sites in -# internal/streams' own tests, 67 in internal/tracksync's, plus -# streams_test.go's `const streamTypeVideo StreamType = 1`. They stay as -# aliases onto plexapi's declarations because the alternative is rewriting -# those 212 sites onto the library's spelling to say the same thing โ€” easing a -# move to a new home is exactly what the rulebook (ยง11) licenses an alias for, -# and StreamTypeAudio is the app's own vocabulary at every one of them. +# The Plex stream-type enum. Structural: production reads a stream's kind only +# through the IsAudio and IsSubtitle that plexapi.Stream promotes into +# streams.Stream, so these names exist for the 212 construction sites in the +# internal/streams and internal/tracksync tests. They stay as aliases onto +# plexapi's declarations because they are the app's own vocabulary there. class StreamType is used in test only constant StreamTypeAudio is used in test only constant StreamTypeSubtitle is used in test only diff --git a/Dockerfile b/Dockerfile index 19759cc..abb3c6e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ # check=error=true FROM golang:1.27.0-alpine@sha256:7d5cbf6833f7331dafd25a2e8b9673477f559759ff8ed4ca8efabe6795ad08db AS builder # GOTOOLCHAIN=auto: a Renovate dep bump requiring a newer Go downloads that toolchain -# instead of failing the build (org convention, go.md/ci-cd.md); still reproducible -# because go.mod pins the toolchain version. `local` would hard-fail such a build. +# instead of failing the build; still reproducible because go.mod pins the +# toolchain version. `local` would hard-fail such a build. ENV GOTOOLCHAIN=auto WORKDIR /src diff --git a/README.md b/README.md index a5d2cb8..158005c 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ services: image: ghcr.io/cplieger/plex-language-sync:latest container_name: plex-language-sync restart: unless-stopped - stop_grace_period: 20s # time to save what it learned on stop, Docker's 10s default is too short + stop_grace_period: 20s # time to save what it learned on stop. Docker's 10s default is too short # Run "sudo mkdir -p /opt/appdata/plex-language-sync && sudo chown 1000:1000 /opt/appdata/plex-language-sync" # before the first start, or nothing it learns is saved. If .env sets PUID and PGID, use those numbers. user: "${PUID:-1000}:${PGID:-1000}" @@ -72,7 +72,7 @@ Settings are environment variables. The app reads them once at start, so restart | Variable | Description | Default | | --- | --- | --- | -| `PLEX_URL` | Address of your Plex server, with scheme and port, such as `http://192.0.2.10:32400` | required | +| `PLEX_URL` | Address of your Plex server, with scheme and port, such as `http://192.0.2.10:32400`. `PLEX_URL_FILE` reads it from a file instead | required | | `PLEX_TOKEN` | Plex token of the server's owner. `PLEX_TOKEN_FILE` reads it from a file instead | required | | `UPDATE_LEVEL` | `show` changes the whole show, `season` only the current season | `show` | | `UPDATE_STRATEGY` | `all` changes every episode in scope, `next` only the episodes after the one played | `all` | diff --git a/compose.yaml b/compose.yaml index 7221ca4..5158660 100644 --- a/compose.yaml +++ b/compose.yaml @@ -4,7 +4,7 @@ services: image: ghcr.io/cplieger/plex-language-sync:latest container_name: plex-language-sync restart: unless-stopped - stop_grace_period: 20s # time to save what it learned on stop, Docker's 10s default is too short + stop_grace_period: 20s # time to save what it learned on stop. Docker's 10s default is too short # Run "sudo mkdir -p /opt/appdata/plex-language-sync && sudo chown 1000:1000 /opt/appdata/plex-language-sync" # before the first start, or nothing it learns is saved. If .env sets PUID and PGID, use those numbers. user: "${PUID:-1000}:${PGID:-1000}" diff --git a/config.go b/config.go index 9608bdd..98412e8 100644 --- a/config.go +++ b/config.go @@ -215,14 +215,13 @@ func splitTrim(s string) []string { } // loadSchedulerInterval parses DEEP_SCAN_INTERVAL and reports the daily -// deep-analysis cadence and whether the scheduler runs at all. The value -// is a Go duration ("24h", "12h"), matching the fleet docker-*-scheduler -// convention. The sentinels "off" and "disabled" (case-insensitive) or a -// zero duration ("0", "0s") disable the scheduler entirely: the app then -// runs WebSocket-only (the daily pass is a safety net over the real-time -// listener, and there is no external trigger). Unset defaults to -// defaultSchedulerInterval, enabled. Any other parse failure falls back -// to the default (enabled) with a warning rather than refusing to start. +// deep-analysis cadence and whether the scheduler runs at all. The value is a +// Go duration ("24h", "12h"), like the cplieger docker-*-scheduler images. The +// sentinels "off" and "disabled" (case-insensitive) or a zero duration ("0", +// "0s") disable the scheduler: the app then runs WebSocket-only, since the daily +// pass is a safety net over the real-time listener. Unset defaults to +// defaultSchedulerInterval, enabled. Any other parse failure falls back to the +// default (enabled) with a warning rather than refusing to start. func loadSchedulerInterval() (interval time.Duration, enabled bool) { interval = defaultSchedulerInterval enabled = true diff --git a/docs/configuration.md b/docs/configuration.md index b8e8c6e..deb1734 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -8,9 +8,9 @@ Every setting is an environment variable, set in `.env` or in the `environment:` A misspelled value for `UPDATE_LEVEL`, `UPDATE_STRATEGY`, `SUBTITLE_MATCH_TIER`, `DEEP_SCAN_INTERVAL` or `LOG_LEVEL` logs a warning and falls back to the default, so the app still starts. A negative `DEEP_SCAN_INTERVAL` is treated the same way. Set `DEEP_SCAN_INTERVAL` to `off`, `disabled` or a zero duration such as `0` or `0s` to turn the deep scan off. `LOG_LEVEL` ignores case, accepts `warning` for `warn`, and accepts an offset such as `info+2`. A missing or blank `PLEX_URL` or `PLEX_TOKEN` stops the start with an error that names the variable. -## Keeping the token in a file +## Keeping the token and address in files -Set `PLEX_TOKEN_FILE` to the path of a file inside the container, instead of `PLEX_TOKEN`, to read the token from [a Docker secret](https://github.com/cplieger/docs/blob/main/docs/hardening.md#secrets-in-files). One trailing line ending is removed, and so is any space around the token. +Set `PLEX_URL_FILE` and `PLEX_TOKEN_FILE` to the path of a file inside the container, instead of `PLEX_URL` and `PLEX_TOKEN`, to read each value from [a Docker secret](https://github.com/cplieger/docs/blob/main/docs/hardening.md#secrets-in-files). One trailing line ending is removed, and so is any space around the value. ## Leaving shows and libraries out diff --git a/internal/deepscan/deepscan.go b/internal/deepscan/deepscan.go index c0ae9f8..d60b37e 100644 --- a/internal/deepscan/deepscan.go +++ b/internal/deepscan/deepscan.go @@ -1,42 +1,11 @@ -// Package deepscan owns the periodic deep-analysis tick and its -// sub-workers (recent-history replay + recently-added sweep). Named for what -// it does rather than "scheduler": that name belongs to the first-party -// scheduler library this package consumes, and the collision forced a -// schedlib alias on the library at every use. -// -// Responsibilities: -// - Schedule a periodic deep-analysis run on a fixed Go-duration -// interval (default 24h), matching the fleet docker-*-scheduler -// convention: one pass at startup (when the last run is older than -// one interval) plus a time.Ticker every interval thereafter. The -// pass is a safety net over the real-time WebSocket listener, so a -// drifting wall-clock start hour is immaterial; using an interval -// rather than an absolute HH:MM boundary means the app reads no -// local wall-clock time (no TZ / time/tzdata dependency). -// - Fan out per-item work across a bounded worker pool -// with a circuit breaker that aborts the -// pass after a threshold of consecutive per-item failures. -// - Persist the last-run record in a scheduler.Stamp file on the -// persistent volume so a cold restart does not double-run the -// analysis. -// -// Stable contracts preserved (keep these exact: Loki alerts grep the log -// strings): -// - WARN slog keys ("scheduler: aborting history processing after -// consecutive failures", "scheduler: failed to fetch history", -// "scheduler: failed to fetch sections", "scheduler: deep analysis -// already in progress, skipping") byte-for-byte identical. -// - INFO slog keys ("scheduler enabled", "scheduled deep analysis -// starting", "running initial deep analysis", "deep analysis -// completed", "scheduler: processing recently added episode", -// "scheduler stopped") identical. -// -// Consumer note: every collaborator is an interface THIS package declares (see -// deps.go) โ€” plexReader, EpisodeReader, runLedger, skipChecker and Syncer, each -// naming only the methods the pass calls. Nothing is imported from a shared -// contract package, and Syncer in particular is declared here rather than -// imported so deepscan needs no dependency on internal/tracksync. In practice -// main.go wires the concrete *tracksync.Syncer through. +// Package deepscan owns the periodic deep-analysis pass (recent-history replay +// plus recently-added sweep) over a bounded worker pool whose circuit breaker +// aborts after consecutive per-item failures. It runs on a Go-duration interval +// (default 24h), so it reads no wall-clock time, and a scheduler.Stamp file +// keeps a cold restart from double-running. alerts/logql.yaml matches "deep +// analysis completed" and docs/monitoring.md names "scheduled deep analysis +// starting": keep both exact. Every collaborator is an interface declared in +// deps.go, so deepscan imports no internal/tracksync. package deepscan import ( @@ -182,8 +151,8 @@ func (s *Scheduler) Run(ctx context.Context) { s.deepAnalysis(ctx) } - // Fixed-interval scheduling via scheduler.RunLoop (the fleet - // docker-*-scheduler convention). FireOnStart is false: the conditional + // Fixed-interval scheduling via scheduler.RunLoop, like the cplieger + // docker-*-scheduler images. FireOnStart is false: the conditional // startup pass above already handled the immediate run (RunLoop's // unconditional FireOnStart would ignore the last-run stamp and double-run // on a recent restart), and FirstDelay phases the first tick from the diff --git a/main_test.go b/main_test.go index 5331382..eac4f31 100644 --- a/main_test.go +++ b/main_test.go @@ -1284,7 +1284,7 @@ func TestResolveStallCounter_pausedEventsNeverJoinTheAbsentPairs(t *testing.T) { func TestResolveStallCounter_anEmptyClientIsNotASecondClient(t *testing.T) { // A notification carrying no clientIdentifier is one recognisable // condition, not a distinct client per event. Counting each as new - // would make a run of them look like a fleet-wide failure. + // would make a run of them look like a failure across every client. c := &resolveStallCounter{} for range resolveStallThreshold * 2 { if v := c.miss(playingBy("", "100"), false); v.stalled {