{"id":48837221,"url":"https://github.com/geiserx/quality-gate","last_synced_at":"2026-04-23T10:03:03.200Z","repository":{"id":333129204,"uuid":"1128182141","full_name":"GeiserX/quality-gate","owner":"GeiserX","description":"Jellyfin plugin to restrict users to specific media versions based on path-based policies","archived":false,"fork":false,"pushed_at":"2026-04-14T22:23:25.000Z","size":232,"stargazers_count":6,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-04-15T00:33:23.787Z","etag":null,"topics":["4k","access-control","bandwidth","csharp","docker","dotnet","hacktoberfest","homelab","jellyfin","jellyfin-plugin","media-management","media-server","multi-version","open-source","quality-control","self-hosted","streaming","transcoding","user-management","video"],"latest_commit_sha":null,"homepage":null,"language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/GeiserX.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null},"funding":{"github":"geiserx","patreon":"geiser","buy_me_a_coffee":"geiser","thanks_dev":"u/gh/geiserx"}},"created_at":"2026-01-05T09:02:49.000Z","updated_at":"2026-04-14T22:23:19.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/GeiserX/quality-gate","commit_stats":null,"previous_names":["geiserx/jellyfin-quality-gate","geiserx/quality-gate"],"tags_count":32,"template":false,"template_full_name":null,"purl":"pkg:github/GeiserX/quality-gate","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GeiserX%2Fquality-gate","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GeiserX%2Fquality-gate/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GeiserX%2Fquality-gate/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GeiserX%2Fquality-gate/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/GeiserX","download_url":"https://codeload.github.com/GeiserX/quality-gate/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/GeiserX%2Fquality-gate/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32175041,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-23T02:19:40.750Z","status":"ssl_error","status_checked_at":"2026-04-23T02:17:55.737Z","response_time":53,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["4k","access-control","bandwidth","csharp","docker","dotnet","hacktoberfest","homelab","jellyfin","jellyfin-plugin","media-management","media-server","multi-version","open-source","quality-control","self-hosted","streaming","transcoding","user-management","video"],"created_at":"2026-04-15T00:02:14.854Z","updated_at":"2026-04-23T10:03:03.188Z","avatar_url":"https://github.com/GeiserX.png","language":"C#","funding_links":["https://github.com/sponsors/geiserx","https://patreon.com/geiser","https://buymeacoffee.com/geiser","https://thanks.dev/u/gh/geiserx"],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\u003cimg src=\"docs/images/banner.svg\" alt=\"Quality Gate banner\" width=\"900\"/\u003e\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eQuality Gate\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/GeiserX/quality-gate/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/GeiserX/quality-gate?style=flat-square\u0026logo=github\" alt=\"GitHub Release\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://jellyfin.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/Jellyfin-10.11+-00a4dc?style=flat-square\u0026logo=jellyfin\" alt=\"Jellyfin Version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://dotnet.microsoft.com\"\u003e\u003cimg src=\"https://img.shields.io/badge/.NET-9.0-512bd4?style=flat-square\u0026logo=dotnet\" alt=\".NET\"\u003e\u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\u003cimg src=\"https://img.shields.io/github/license/GeiserX/quality-gate?style=flat-square\" alt=\"License\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/GeiserX/quality-gate/actions\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/GeiserX/quality-gate/build.yml?style=flat-square\u0026logo=github-actions\u0026logoColor=white\u0026label=CI\" alt=\"CI\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/GeiserX/quality-gate/actions/workflows/build.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/GeiserX/quality-gate/build.yml?branch=main\u0026style=flat-square\u0026label=tests\" alt=\"Tests\"\u003e\u003c/a\u003e\n\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\u003cstrong\u003eIntelligent media access control for Jellyfin\u003c/strong\u003e\u003c/p\u003e\n\n---\n\n## Features\n\n- **Filename Regex Patterns** -- Match against filenames with regex for [Jellyfin multi-version](https://jellyfin.org/docs/general/server/media/movies/#multiple-versions) setups\n- **Per-User Assignments** -- Assign different policies to different users\n- **Web Configuration** -- Easy-to-use admin interface in Jellyfin dashboard\n- **Multi-Version Support** -- Seamlessly filter available media versions per user\n- **Custom Intros** -- Optional intro video per policy (e.g. a \"lite\" branding for restricted users)\n- **Dangling Symlink Protection** -- Sources whose files don't exist on disk are automatically hidden\n- **Detailed Logging** -- Full audit trail of access decisions\n\n## Use Cases\n\nThis plugin is designed for Jellyfin's [multi-version naming convention](https://jellyfin.org/docs/general/server/media/movies/#multiple-versions), where multiple quality versions of the same movie live together:\n\n```text\nmovies/Movie (2021)/Movie (2021) - 2160p.mkv\nmovies/Movie (2021)/Movie (2021) - 1080p.mkv\nmovies/Movie (2021)/Movie (2021) - 720p.mkv\n```\n\n| Scenario | Solution |\n|----------|----------|\n| **Bandwidth Management** | Restrict remote users to lower-bitrate versions |\n| **Tiered Access** | Premium users get 4K, standard users get 1080p |\n| **Device Optimization** | Mobile users automatically get mobile-optimized versions |\n\n## Installation\n\n### Method 1: Plugin Repository (Recommended)\n\nAdd this repository to your Jellyfin instance for automatic updates:\n\n1. Go to **Dashboard \u003e Plugins \u003e Repositories**\n2. Click **Add** and enter:\n   - **Name**: `Quality Gate`\n   - **URL**: `https://geiserx.github.io/quality-gate/manifest.json`\n3. Go to **Catalog** and install **Quality Gate**\n4. Restart Jellyfin\n\n### Method 2: Manual Installation\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eDocker\u003c/b\u003e\u003c/summary\u003e\n\n```bash\nVERSION=\"3.2.0.0\"\ncurl -L -o QualityGate.zip \\\n  \"https://github.com/GeiserX/quality-gate/releases/download/v${VERSION}/quality-gate_${VERSION}.zip\"\n\nunzip QualityGate.zip -d /path/to/jellyfin/plugins/QualityGate/\ndocker restart jellyfin\n```\n\nOr add to your `docker-compose.yml`:\n```yaml\nvolumes:\n  - ./plugins/QualityGate:/config/plugins/QualityGate\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eLinux (Native)\u003c/b\u003e\u003c/summary\u003e\n\n```bash\nVERSION=\"3.2.0.0\"\ncurl -L -o QualityGate.zip \\\n  \"https://github.com/GeiserX/quality-gate/releases/download/v${VERSION}/quality-gate_${VERSION}.zip\"\n\nsudo unzip QualityGate.zip -d /var/lib/jellyfin/plugins/QualityGate/\nsudo chown -R jellyfin:jellyfin /var/lib/jellyfin/plugins/QualityGate/\nsudo systemctl restart jellyfin\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eWindows\u003c/b\u003e\u003c/summary\u003e\n\n1. Download the [latest release](https://github.com/GeiserX/quality-gate/releases/latest)\n2. Extract to `%LOCALAPPDATA%\\jellyfin\\plugins\\QualityGate\\`\n3. Restart Jellyfin from Services or the tray icon\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003emacOS\u003c/b\u003e\u003c/summary\u003e\n\n```bash\nVERSION=\"3.2.0.0\"\ncurl -L -o QualityGate.zip \\\n  \"https://github.com/GeiserX/quality-gate/releases/download/v${VERSION}/quality-gate_${VERSION}.zip\"\n\nunzip QualityGate.zip -d ~/.local/share/jellyfin/plugins/QualityGate/\n```\n\n\u003c/details\u003e\n\n## Configuration\n\nNavigate to **Dashboard \u003e Quality Gate** to configure the plugin.\n\n### Step 1: Create Policies\n\nPolicies define which filename patterns are allowed or blocked. Click **\"Add Policy\"** to create one.\n\n| Field | Description |\n|-------|-------------|\n| **Policy Name** | A descriptive name (e.g., \"720p Only\", \"No 4K\") |\n| **Allowed Filename Patterns** | Regex patterns matched against the filename. Files must match at least one pattern. |\n| **Blocked Filename Patterns** | Regex patterns matched against the filename. Matching files are always blocked. |\n| **Custom Intro Video** | Optional intro video for users under this policy. Disable the built-in \"Local Intros\" plugin if you only want Quality Gate intros. |\n| **Enabled** | Toggle policy on/off |\n\n### Step 2: Set Default Policy\n\nChoose a policy from the **Default Policy** dropdown. This applies to ALL users who don't have a specific override.\n\n- Select **(No default -- Full Access)** to allow unrestricted access by default\n- Select a policy to restrict all users by default\n\n### Step 3: Configure User Access\n\nThe **User Access** table shows all Jellyfin users and their current policy:\n\n- **Use Default** -- inherits the default policy\n- **Full Access** -- no restrictions\n- Any named policy -- applies that policy's rules\n\nIf an override or the default policy points to a deleted or disabled policy, the dropdown shows **DENIED** until you choose a replacement (fail-closed). This applies to both per-user overrides and the default policy.\n\n### Policy Logic\n\nEvaluation order:\n\n1. **Blocked Filename Patterns**: If filename matches any blocked regex -- **BLOCKED**\n2. **Allowed Filename Patterns**: If defined and filename doesn't match any -- **BLOCKED**\n3. **File existence**: If the file doesn't exist on disk (dangling symlink) -- **BLOCKED**\n4. Otherwise -- **ALLOWED**\n\n| Allowed Pattern | Blocked Pattern | Filename | Result |\n|-----------------|-----------------|----------|--------|\n| `- 720p` | -- | `Movie (2021) - 720p.mkv` | Allowed |\n| `- 720p` | -- | `Movie (2021) - 2160p.mkv` | Blocked |\n| (empty) | `- 2160p\\|- 4K` | `Movie (2021) - 1080p.mkv` | Allowed |\n| (empty) | `- 2160p\\|- 4K` | `Movie (2021) - 2160p.mkv` | Blocked |\n\n\u003e **Tip**: Patterns are case-insensitive regex with a 1-second timeout to prevent ReDoS. Jellyfin also supports bracketed labels (e.g. `Movie (2021) - [1080p].mkv`), so use `\\[?1080p\\]?` to match both formats.\n\n---\n\n## Examples\n\n### Restrict to 720p Only\n\n```text\nPolicy Name: 720p Only\nAllowed Filename Patterns:\n  - 720p\n```\n\nOnly files with `- 720p` in the filename are visible.\n\n### Block 4K Content\n\n```text\nPolicy Name: No 4K\nBlocked Filename Patterns:\n  - 2160p\n  - 4K\n```\n\nEverything is visible except 4K versions.\n\n### Standard Quality (1080p max)\n\n```text\nPolicy Name: Standard\nBlocked Filename Patterns:\n  - 2160p\n  - 4K\n  - Remux\n```\n\n### Tiered Access\n\n1. Create **\"Standard\"** policy (block 4K as above)\n2. Set **Default Policy** to \"Standard\"\n3. Add **Full Access** overrides for premium users\n\n---\n\n## How It Works\n\n1. **Result Filter**: The plugin uses an ASP.NET Core `IAsyncResultFilter` that intercepts API responses **before serialization**, operating on C# objects directly.\n\n2. **MediaSource Filtering**: When Jellyfin returns media sources/versions to the client, the filter removes blocked versions so they don't appear in the UI.\n\n3. **Filename Matching**: Each media version's filename is matched against your policy's regex patterns. For symlinked files, both the symlink filename and the resolved target filename are checked.\n\n4. **File Existence**: Sources whose files don't exist on disk (e.g. dangling symlinks from in-progress transcodes) are automatically hidden, preventing playback errors.\n\n### Library Setup\n\nAll quality versions must be in the **same Jellyfin library** using Jellyfin's [multi-version naming](https://jellyfin.org/docs/general/server/media/movies/#multiple-versions). Each version needs a ` - label` suffix (space, hyphen, space, label):\n\n```text\nmovies/\n  Movie (2021)/\n    Movie (2021) - 2160p.mkv\n    Movie (2021) - 1080p.mkv\n    Movie (2021) - 720p.mkv\n```\n\nJellyfin merges these into a single item with multiple MediaSources. The plugin then filters which sources each user can see.\n\n## Building from Source\n\n### Prerequisites\n\n- [.NET 9.0 SDK](https://dotnet.microsoft.com/download/dotnet/9.0)\n- Git\n\n### Build\n\n```bash\ngit clone https://github.com/GeiserX/quality-gate.git\ncd quality-gate/Jellyfin.Plugin.QualityGate\ndotnet build -c Release\n```\n\nThe compiled plugin will be in `bin/Release/net9.0/`.\n\n## Security\n\n- This plugin handles access control -- review your policies carefully\n- Only administrators can configure policies\n- See [SECURITY.md](SECURITY.md) for vulnerability reporting\n\n## Contributing\n\nContributions are welcome! Please feel free to submit a Pull Request.\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/AmazingFeature`)\n3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)\n4. Push to the branch (`git push origin feature/AmazingFeature`)\n5. Open a Pull Request\n\n## Other Jellyfin Projects by GeiserX\n\n- [smart-covers](https://github.com/GeiserX/smart-covers) -- Cover extraction for books, audiobooks, comics, magazines, and music libraries with online fallback\n- [whisper-subs](https://github.com/GeiserX/whisper-subs) -- Automatically generates subtitles using local AI models powered by Whisper\n- [jellyfin-encoder](https://github.com/GeiserX/jellyfin-encoder) -- Automatic 720p HEVC/AV1 transcoding service with optional symlink creation for Jellyfin multi-version support\n- [jellyfin-telegram-channel-sync](https://github.com/GeiserX/jellyfin-telegram-channel-sync) -- Sync Jellyfin access with Telegram channel membership\n\n## License\n\nThis project is licensed under the GPL-3.0 License -- see the [LICENSE](LICENSE) file for details.\n\n## Acknowledgments\n\n- [Jellyfin](https://jellyfin.org) -- The Free Software Media System\n- The Jellyfin plugin development community\n\n---\n\n\u003cdiv align=\"center\"\u003e\n\n**[Back to Top](#quality-gate)**\n\n\u003c/div\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeiserx%2Fquality-gate","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeiserx%2Fquality-gate","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeiserx%2Fquality-gate/lists"}