Skip to content

feat: cache jellyfin collections#2800

Merged
enoch85 merged 3 commits into
Maintainerr:developmentfrom
natekspencer:jellyfin-collections-cache
Apr 28, 2026
Merged

feat: cache jellyfin collections#2800
enoch85 merged 3 commits into
Maintainerr:developmentfrom
natekspencer:jellyfin-collections-cache

Conversation

@natekspencer

Copy link
Copy Markdown
Contributor

Description & Design

Stores jellyfin collections and collection children to cache to avoid excessive repeat queries during rules that look at collections.

Related issue

AI-Assisted Development

Checklist

  • I have read the CONTRIBUTING.md document.
  • I understand the code I am submitting and can explain how it works
  • I have performed a self-review of my code
  • I have linted and formatted my code
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

How to test

Please describe the steps to test your changes, including any setup required.

  1. Add a rule targeting Jellyfin collections
  2. Run rule

Additional context

Currently, rules that target Jellyfin collections result in a query for all collections and a query for each collection's children, repeated for every item processed. This quickly balloons and results in excessively long runtimes. For example, targeting a library with 500 items and 50 collections would result in 25,500 queries per rule run. With caching, this is reduced to 51 calls.

@natekspencer natekspencer requested a review from enoch85 as a code owner April 28, 2026 15:55
@natekspencer natekspencer force-pushed the jellyfin-collections-cache branch from 0bb1756 to 4afc3cc Compare April 28, 2026 15:58
@natekspencer

Copy link
Copy Markdown
Contributor Author

I'm not sure about this.

Cache is good for fairly static objects, but collections isn't one of them. Cache can be very error prune. What's your motivation for this PR?

My direct motivation: I have a rule that takes up to 20 minutes without the cache and drops to under 2 with the cache.

More details:
Right now, a rule that targets items in a collection such as:

- firstValue: Jellyfin.collection_names
  action: CONTAINS_PARTIAL
  customValue:
    type: text
    value: Cleanup Eligible

currently causes excessive calls to collections and their children for every single item processed, which results in a significant performance problem at scale.

Collections don't need to be re-queried for every item within a single rule run. If a collection changes mid-run and that is a concern, every previously processed item would need to be re-evaluated, resulting in a cyclic problem that isn't worth solving. The jellyfin: cache items appear to be purged between rule group runs anyway, ensuring it is always fresh at the start of each rule group run.

@enoch85

enoch85 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

I'm not sure about this.
Cache is good for fairly static objects, but collections isn't one of them. Cache can be very error prune. What's your motivation for this PR?

My direct motivation: I have a rule that takes up to 20 minutes without the cache and drops to under 2 with the cache.

More details: Right now, a rule that targets items in a collection such as:

- firstValue: Jellyfin.collection_names
  action: CONTAINS_PARTIAL
  customValue:
    type: text
    value: Cleanup Eligible

currently causes excessive calls to collections and their children for every single item processed, which results in a significant performance problem at scale.

Collections don't need to be re-queried for every item within a single rule run. If a collection changes mid-run and that is a concern, every previously processed item would need to be re-evaluated, resulting in a cyclic problem that isn't worth solving. The jellyfin: cache items appear to be purged between rule group runs anyway, ensuring it is always fresh at the start of each rule group run.

Yeah, sorry, deleted my comment when I read your descripton. 🙈

I will review this soon. Almost ready to say LGTM though. 😆

@natekspencer

Copy link
Copy Markdown
Contributor Author

Yeah, sorry, deleted my comment when I read your descripton. 🙈

I will review this soon. Almost ready to say LGTM though. 😆

All good, haha. ^_^

@enoch85

enoch85 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

OK, I will fix some minor stuff here:

  1. Both new caches can go stale because the mutation paths don't del them:

createCollection (service.ts:1337) → should invalidate jellyfin:collections:
deleteCollection (service.ts:1377) → should invalidate both jellyfin:collections: and jellyfin:collections:children:
addToCollection / addBatchToCollection (service.ts:1483, :1487) → should invalidate jellyfin:collections:children: (and arguably the parent collections cache, since childCount is now stale)
removeFromCollection / removeBatchFromCollection (service.ts:1560, :1580) → same as above

  1. Empty results are cached

  2. Tests

Drops cached entries when collections are created, deleted, updated,
or when items are added/removed, so reads within the TTL window can't
serve pre-mutation state. Also skips caching empty results to avoid
sticking a transient zero-collection response.
@enoch85

enoch85 commented Apr 28, 2026

Copy link
Copy Markdown
Collaborator

@natekspencer Thanks for this PR! I'm sure users will be happier now when it just says "swosch" and rules are done. 🚀

I'll merge this when tests pass. 👍

@natekspencer

Copy link
Copy Markdown
Contributor Author

@natekspencer Thanks for this PR! I'm sure users will be happier now when it just says "swosch" and rules are done. 🚀

I'll merge this when tests pass. 👍

I know I will be! Thanks for your work on this, too!

@enoch85 enoch85 merged commit 35a4d44 into Maintainerr:development Apr 28, 2026
9 checks passed
@enoch85 enoch85 added this to the 3.9.0 milestone Apr 28, 2026 — with GitHub Codespaces
@natekspencer natekspencer deleted the jellyfin-collections-cache branch April 28, 2026 17:35
maintainerr-automation Bot added a commit that referenced this pull request Apr 28, 2026
* build(deps): bump the nestjs group with 2 updates (#2785)

Bumps the nestjs group with 2 updates: [@nestjs/event-emitter](https://github.com/nestjs/event-emitter) and [@nestjs/swagger](https://github.com/nestjs/swagger).


Updates `@nestjs/event-emitter` from 3.0.1 to 3.1.0
- [Commits](nestjs/event-emitter@3.0.1...3.1.0)

Updates `@nestjs/swagger` from 11.4.1 to 11.4.2
- [Release notes](https://github.com/nestjs/swagger/releases)
- [Commits](nestjs/swagger@11.4.1...11.4.2)

---
updated-dependencies:
- dependency-name: "@nestjs/event-emitter"
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: nestjs
- dependency-name: "@nestjs/swagger"
  dependency-version: 11.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: nestjs
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump nodemailer from 8.0.6 to 8.0.7 (#2786)

Bumps [nodemailer](https://github.com/nodemailer/nodemailer) from 8.0.6 to 8.0.7.
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](nodemailer/nodemailer@v8.0.6...v8.0.7)

---
updated-dependencies:
- dependency-name: nodemailer
  dependency-version: 8.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump @typescript-eslint/parser from 8.58.2 to 8.59.1 (#2787)

Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 8.58.2 to 8.59.1.
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/parser/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.1/packages/parser)

---
updated-dependencies:
- dependency-name: "@typescript-eslint/parser"
  dependency-version: 8.59.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump prettier-plugin-tailwindcss from 0.7.3 to 0.8.0 (#2790)

Bumps [prettier-plugin-tailwindcss](https://github.com/tailwindlabs/prettier-plugin-tailwindcss) from 0.7.3 to 0.8.0.
- [Changelog](https://github.com/tailwindlabs/prettier-plugin-tailwindcss/blob/main/CHANGELOG.md)
- [Commits](tailwindlabs/prettier-plugin-tailwindcss@v0.7.3...v0.8.0)

---
updated-dependencies:
- dependency-name: prettier-plugin-tailwindcss
  dependency-version: 0.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump react-hook-form from 7.72.1 to 7.74.0 (#2793)

Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.72.1 to 7.74.0.
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](react-hook-form/react-hook-form@v7.72.1...v7.74.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.74.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump jsdom from 29.0.2 to 29.1.0 (#2794)

Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.0.2 to 29.1.0.
- [Commits](jsdom/jsdom@v29.0.2...v29.1.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 29.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump typescript-eslint from 8.59.0 to 8.59.1 (#2791)

Bumps [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) from 8.59.0 to 8.59.1.
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.59.1/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: typescript-eslint
  dependency-version: 8.59.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump @tanstack/eslint-plugin-query (#2795)

Bumps [@tanstack/eslint-plugin-query](https://github.com/TanStack/query/tree/HEAD/packages/eslint-plugin-query) from 5.99.0 to 5.100.5.
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/eslint-plugin-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/eslint-plugin-query@5.100.5/packages/eslint-plugin-query)

---
updated-dependencies:
- dependency-name: "@tanstack/eslint-plugin-query"
  dependency-version: 5.100.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump actions/github-script from 8 to 9 (#2788)

Bumps [actions/github-script](https://github.com/actions/github-script) from 8 to 9.
- [Commits](actions/github-script@v8...v9)

---
updated-dependencies:
- dependency-name: actions/github-script
  dependency-version: '9'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: enoch85 <mailto@danielhansson.nu>

* build(deps): bump peter-evans/find-comment from 3 to 4 (#2789)

Bumps [peter-evans/find-comment](https://github.com/peter-evans/find-comment) from 3 to 4.
- [Commits](peter-evans/find-comment@v3...v4)

---
updated-dependencies:
- dependency-name: peter-evans/find-comment
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: enoch85 <mailto@danielhansson.nu>

* feat: add custom collection poster support (#2784)

* feat: add custom collection poster support

* style: format collection poster picker

* fix: tolerate poster cleanup failures on collection delete

* fix(collections): push poster on first rule-driven create, cap uploads at 500 KB

- Push stored poster when addToCollectionInternal first creates the
  media-server collection, so brand-new rules apply the user's poster
- Lower upload limit from 10 MB to 500 KB via shared contracts constants
- Style the Clear button as buttonType=danger so it reads as a button

* feat(collections): refresh server metadata on poster clear, move picker right

- DELETE /poster now also calls refreshItemMetadata via the media-server
  abstraction; response carries refreshRequested so callers can adapt
- Picker shows a softened best-effort message when a refresh is requested
- Move the poster section into the right column of the rule-group modal
  to balance whitespace
- Docs and Swagger describe the new contract and the no-guarantee semantics

* fix(rules,tasks): unstick rules-collections lock and clarify sw_watchers labels (#2801)

- ExecutionLockService.acquire() stored the chained promise instead of `current`,
  so the release callback's `locks.get(key) === current` check never matched
  and the map entry leaked. tryAcquire then returned null forever after the
  first scheduled run, breaking manual Trigger Now until restart. Store
  `current` directly; FIFO chaining is preserved by `await prior`.
- rule-executor-job-manager.executeJob now runs emitStatusUpdate inside the
  inner try/finally that owns release(), and emitStatusUpdate itself swallows
  listener throws at debug level so a misbehaving SSE client can't poison
  the executor.
- Sharpen sw_watchers humanName to "Users that watched at least one episode"
  and sw_allEpisodesSeenBy to "Users that watched every episode" across all
  three servers; add semantic comments in the getters pointing at the
  alternative property. No behaviour change for the watchers data.

Fixes #2798
Fixes #2799

* feat: cache jellyfin collections (#2800)

* Cache jellyfin collections to avoid excessive repeat queries

* Invalidate jellyfin collection caches on mutation

Drops cached entries when collections are created, deleted, updated,
or when items are added/removed, so reads within the TTL window can't
serve pre-mutation state. Also skips caching empty results to avoid
sticking a transient zero-collection response.

---------

Co-authored-by: enoch85 <mailto@danielhansson.nu>

* fix(logs): block path traversal in log file download endpoint

The safeLogFileRegex was unanchored, allowing any string containing a
maintainerr-YYYY-MM-DD.log substring to pass validation. Combined with
path.join, an attacker could read arbitrary files via URL-encoded
traversal segments (e.g. maintainerr-2026-01-01.log%2F..%2F..%2Fetc%2Fpasswd).

Anchor the regex and add a defense-in-depth canonical-path check that
rejects symlinks and verifies the resolved path stays inside the logs
directory.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: enoch85 <mailto@danielhansson.nu>
Co-authored-by: Nathan Spencer <natekspencer@gmail.com>
@maintainerr-automation

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.9.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants