Skip to content

Latest commit

 

History

320 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Note

Part of Flowfin. It works with any Jellyfin server, and with the Flowfin clients.

Jellyfin Smart Collections

Collections from a rule engine over Jellyfin library metadata.

A collection in Jellyfin is a list somebody has to keep. This plugin makes it a list somebody describes once. You write a rule, the plugin evaluates it against the library, and the collection it owns holds exactly the items the rule matches. When the library changes, the collection follows.

Status

The plugin is not finished and is not published to any catalogue. What is described below is the design the repository is being built to, and the tracker carries the work that has to land before any of it runs on a server. Nothing here is a promise about a release date.

Do not install this on a server you care about yet.

Which server versions it runs on

Two lines are supported, and each one gets its own package, because they host different runtimes:

Server line Runtime the server hosts
Jellyfin 10.11 .NET 9
Jellyfin 12.0 .NET 10

That table is read from the server's own project file rather than from documentation:

gh api "repos/jellyfin/jellyfin/contents/Jellyfin.Server/Jellyfin.Server.csproj?ref=v10.11.11" \
  --jq .content | base64 -d | grep TargetFramework
    <TargetFramework>net9.0</TargetFramework>
gh api "repos/jellyfin/jellyfin/contents/Jellyfin.Server/Jellyfin.Server.csproj?ref=v12.0-rc4" \
  --jq .content | base64 -d | grep TargetFramework
    <TargetFramework>net10.0</TargetFramework>

A package built for one line does not load on the other, and installing the wrong one is not a subtle failure: the server refuses the assembly.

Installing

There is no catalogue entry yet, so the only route today is to build from a clone and copy the output into the server's plugin directory:

git clone https://github.com/Flowfin/jellyfin-plugin-smart-collections
cd jellyfin-plugin-smart-collections
dotnet build -c Release

Copy the built assemblies into a directory of its own under the server's plugins path, then restart the server. There are two of them, the plugin and the engine it calls, and both are named in the artifacts list of the manifest the packages are built from:

grep -A3 '^artifacts:' build.yaml
artifacts:
  - "Jellyfin.Plugin.SmartCollections.Engine.dll"
  - "Jellyfin.Plugin.SmartCollections.dll"

Copying one and not the other loads a plugin that fails the moment it reaches the missing half. Where the plugins path is depends on how the server was installed, and the server's own documentation is the authority for it.

Once a catalogue entry exists, this section will name the repository URL to add and this paragraph will go away.

Uninstalling

Removing the plugin leaves every collection it generated in the library, exactly as the last refresh left it. Nothing is deleted, renamed or emptied, and reinstalling adopts those collections again instead of building a second set beside them.

What uninstalling does to the collections carries the behaviour, the reason for it, the two bounds on the promise, and what does and does not hold it today.

What a rule looks like

A rule is one JSON document per collection, held in the plugin's data directory. It says which item kinds it collects, what has to be true of an item for it to belong, and how the collection is ordered.

{
    "schemaVersion": 1,
    "id": "nineties-thrillers",
    "name": "Nineties Thrillers",
    "collects": ["movie"],
    "sort": [{ "field": "productionYear", "direction": "descending" }],
    "limit": 50,
    "match": {
        "allOf": [
            { "field": "genres", "operator": "contains", "value": "Thriller" },
            { "field": "productionYear", "operator": "greaterThanOrEqual", "value": 1990 },
            { "field": "productionYear", "operator": "lessThanOrEqual", "value": 1999 }
        ]
    }
}

Saved into the rule directory, that produces a collection called Nineties Thrillers holding the fifty most recent films in the library whose genres include Thriller and whose production year falls in the nineties, newest first.

Reading it clause by clause:

  • schemaVersion is what lets a later version of the plugin read this document without guessing at it. A document without one is refused rather than interpreted, and a document from a version the plugin does not know is refused with both numbers in the message.
  • id is the rule's identity and does not change when the name does. It is what the plugin stamps on the collection it owns, so renaming a collection does not orphan it.
  • name is what the collection is called in your library, and nothing more than that: it is display text, so changing it renames the collection the rule already owns instead of producing a second one. A document without one is refused rather than named after its file, because the file name is the plugin's business and you should not have to rename a file to rename a collection. Two rules may deliberately carry the same name; what tells their collections apart is the stamp rather than the title.
  • collects names the item kinds the rule gathers, and every document carries one. It is refused rather than defaulted, because a rule with no scope reads every item in your library, and refused rather than inferred from the fields the rule names, because that would make adding one condition change the size of the query. The names are a declared list rather than the server's own enumeration; the field reference is where each one is written down with what it means.
  • sort is the order the collection is written in, and it is optional. Each term names a field and a direction, and the terms are read in the order you wrote them: the first decides, the second decides what the first left tied. Every order ends with the item identifier underneath whatever you declared, which is what makes it total - without that, two films released on one day would sit either way round and the collection would differ between two refreshes. A document with no sort gets that identifier order alone.
  • limit is the greatest number of items the collection holds, and it is optional. It counts what the rule collects rather than what the server answered, so it is the first fifty of your rule and not the first fifty the library happened to hand over. A limit written without a sort is refused: the first fifty of a set nobody ordered are the items whose identifiers happen to sort first, which is reproducible and is not a thing anybody means. The sort reference is where both members are written down.
  • match is a tree of conditions and is required. allOf requires every clause to hold. Which composition operators exist and how deeply they may nest is part of the rule language rather than something a document decides for itself. A document with no match is refused rather than read as a rule that collects everything it declares a scope for, because that reading turns a misspelled member name into a collection holding your whole library.
  • field and operator come from a declared table rather than from whatever property happens to exist on some class, so an unknown field is a validation message naming the legal ones and not an exception at evaluation time.
  • An order and a limit go together, and neither is a member of this version. A limit without a total order is a collection whose contents change between two runs over an unchanged library, so when they arrive the order is defined down to the last tie.

A member this version does not declare is refused, and the message names the member. That is what makes a misspelling visible: a document writing mach where it meant match is told so, instead of being read as a rule that says nothing. It costs no room for a later version, because a document written for one declares a higher schemaVersion and is refused with both numbers in the message before any of its members are read.

The field table, the operator set, the value forms, the composition groups and the item kinds are declared in the engine and each is written out on a page of its own: fields and item kinds, operators, value forms and composition. Every one of those pages is held to its own table by the suite in both directions, so a page naming something the engine does not declare and something the engine declares with no line on its page both red the build.

Worked documents are those tables assembled. Each one is complete, each is handed to the same validator a rules directory scan hands a file to, and an example this plugin would refuse reds the suite rather than sitting there to be copied.

This section is the front page rather than the reference. The reference is the rule language, which gathers those pages and the refusals in one place and is held to the pages in the tree by the suite in both directions. The document above showed an order and a limit until 2026-09-04, as the shape those two are planned in. They came out when a member this version does not declare became a refusal, because an example the plugin would refuse is worse than one that stops short.

What a rule deliberately cannot say

Some of the language's limits are choices rather than gaps, and they are worth knowing before you write a rule that wants them.

There are no regular expressions. A pattern supplied by an operator and evaluated on a server task thread can be made to run for an unbounded time, and this plugin does not put that on your server.

There is no per-user state. A Jellyfin collection is server-wide and every account sees the same one, so a rule about what one person has watched or favourited would build a list everyone sees out of one person's viewing.

Items are not pinned. Membership comes from the rule, so an item added to a generated collection by hand is removed the next time the rule runs.

Each of these is recorded with its reasoning, along with the refusals this page does not name, in what a rule cannot say. None of them is permanent, and the way to lift one is to argue with the reason written there rather than with this page.

Reporting something

Bug reports and feature requests go through the issue templates, which ask for the server version, the plugin version and the rule document that reproduced the problem. Without those three a report about a collection cannot be acted on.

Anything with a security dimension goes through SECURITY.md instead, which names a private route.

Licence

GPL-3.0. See LICENSE.

About

Collections from a rule engine over Jellyfin library metadata

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages