# Changelog All notable changes to Media Purge are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow [Semantic Versioning](https://semver.org/) (pre-1.0: minor = new features, patch = fixes only). Docker images for each release are published to GHCR as `:X.Y.Z` and `:X.Y` tags; `:latest` tracks the `main` branch. ## [Unreleased] ## [0.3.1] - 2026-09-08 ### Fixed - **Jellyfin: items marked as watched were treated as never watched.** Jellyfin can report `Played: true` with `PlayCount: 0` — when someone marks an item watched by hand, when a Trakt-style sync writes the state, or when a library was migrated from Emby. Only `PlayCount` was summed, so those items scored as never played and could be recommended for deletion despite a user having explicitly marked them seen. Each user now contributes at least one play when their state says played. Series were affected the same way (episodes marked played with a zero count left the show at `playCount: 0`), though the watched-episode count was always correct. - **Jellyfin: series only a non-first user could see were never scanned.** The series listing stopped at the first user that returned anything, assuming one user always sees the whole library. With per-user library restrictions or parental controls, shows visible only to a later user were missing from the scan entirely — not just their watch stats. Series are now unioned across every enabled user, matching how movies were already handled. - **Plex: play history is now fetched newest-first.** The log is returned oldest-first, so a server whose history exceeded the 100,000-entry cap would have kept its oldest plays and dropped the recent ones that decide whether something was watched lately. Hitting the cap now also logs a warning naming the cutoff date instead of truncating silently. ### Notes - Plex's cross-user aggregation was audited against a live four-account server: all accounts' plays are captured, and of 368 items played only by non-owner accounts, none were flagged "never watched". No Plex behavior change beyond the history ordering above. - New regression suite runs the real Jellyfin provider against a mock two-user server covering cross-user counts, the marked-watched quirk, disabled users, and restricted-library series. ## [0.3.0] - 2026-09-03 ### Added - **Scan history pruning.** Every scan stores a full library snapshot, so the database previously grew without bound. Scans beyond a configurable retention (Settings → General → "Scans to keep", default 20) are now pruned after each scan. A scan is kept while it still has live work attached (files in the recycle bin, a pending downgrade). Dismissals, downgrades, protections, and the activity log are never pruned. - **Durable decisions.** Dismissed and downgraded verdicts now live in their own `settled_item` table keyed by external ids as well as provider ids — they survive scan pruning *and* the media server re-creating an item under a new id. Protected items also capture external ids now (existing protections are backfilled automatically on upgrade). - **Downgrades run through the server-side job queue** — the same one approvals use. The click returns instantly, the sidenav shows "Downgrading… file N of M" progress, the batch survives closed tabs and container restarts, and no HTTP client can time out during a minutes-long file move again. (`POST /api/v1/recommendations/queue` accepts `action: "downgrade"`; the synchronous per-item endpoint still exists.) ### Changed - SQLite now runs in WAL mode with a busy timeout, so the UI stays responsive while a scan's write bursts are in flight. ## [0.2.3] - 2026-09-03 ### Fixed - Downgrade completion now survives Plex re-creating the movie record. When a downgrade's file swap finishes, Plex deletes the old item (all its files vanished) and creates a new one for the replacement — so the provider item id changes and the pending downgrade could never be matched to the smaller file. Scans now fall back to matching by external ids (TMDB/TVDB/IMDB), which survive the re-creation; the settled-item suppression ("don't re-suggest what the user dismissed or downgraded") uses the same external-id keys. ## [0.2.2] - 2026-09-03 ### Fixed - A downgrade's minutes-long file move is now guarded against concurrent requests, the same way approvals are: a client that times out and retries while the park step is still running gets a 409 instead of starting a second move of the same files. ## [0.2.1] - 2026-09-03 ### Fixed - **Downgrade now actually downgrades.** Live testing against a real Radarr showed 0.2.0's flow could never work: Radarr/Sonarr never replace a file they already have — even a disallowed higher quality "meets cutoff", so a profile switch + search grabs nothing. The flow is now staged and reversible: the original files are **parked in Media Purge's recycle bin** (restorable for the retention window), then the *arr entry is switched to the smaller profile, rescanned so it notices the gap, and searched. Restoring from the recycle bin also switches the *arr back to its previous quality profile. A stalled attempt can be retried by clicking Downgrade again while the item is in "Downgrading". See [docs/downgrade.md](docs/downgrade.md). - Purging a downgrade's parked original no longer credits its size to the reclaimed counter — the net saving is credited once, when a scan confirms the swap. ### Notes for existing installs - Additive schema change (two new columns on recycle-bin entries), applied automatically on upgrade. An item left stuck in "Downgrading" by 0.2.0 can simply be downgraded again after upgrading. ## [0.2.0] - 2026-09-03 ### Added - **Downgrade instead of delete.** Recommendations now offer a third action for items managed by Radarr/Sonarr: instead of deleting, switch the entry to a smaller quality profile you pick and trigger a search, so the *arr replaces the file with a smaller release (e.g. a 40 GB 4K remux → a few-GB 1080p encode). Media Purge never touches the files itself; the next scan detects the smaller file, marks the recommendation **downgraded**, and credits the freed space to the dashboard's reclaimed-storage counter. Configure it under **Settings → Integrations → "Downgrade" quality profile** — the action stays hidden until a profile is picked, and dry-run mode is honored. See [docs/downgrade.md](docs/downgrade.md), including the one rule that matters: the chosen profile must **disallow** the item's current quality. - New recommendation statuses `downgrading` / `downgraded` (filterable on the Recommendations page), new activity events `downgrade.requested` / `downgrade.completed`, and new API endpoints: `POST /api/v1/recommendations/:id/downgrade`, `GET /api/v1/integrations/{radarr|sonarr}/quality-profiles`. The bulk endpoint accepts the `downgrade` action. - This changelog, and versioned releases: tagged versions publish `ghcr.io/ktordoff13/media-purge:X.Y` / `:X.Y.Z` images and a GitHub Release with these notes. - CI now guards every push and PR: lint, unit tests, an integration suite running the full downgrade lifecycle against an in-memory database, a build, and an end-to-end smoke test that boots the built API against mock Jellyfin/Radarr servers and walks the whole downgrade loop (`npm run test:e2e`). ### Notes for existing installs - No breaking changes — the new settings and statuses apply automatically on upgrade, and nothing changes in behavior until you pick a downgrade profile. Rolling back to 0.1.x after using the downgrade action is not supported. - Now that versioned tags exist, consider pinning your container to `ghcr.io/ktordoff13/media-purge:0.2` instead of `:latest` so upgrades are opt-in. ## [0.1.0] - 2026-08-17 Initial release: Plex & Jellyfin scanning with aggregated all-user play stats, 8 tunable rules + custom rule builder, staged deletion pipeline (dry-run default, recycle bin, retention window), protected list & `keep` label, Sonarr/Radarr unmonitor-on-approve, server maintenance tasks, optional local AI regret check, unraid Community Applications template.