Compare commits

..

126 Commits

Author SHA1 Message Date
CodeDevMLH
0b4434c51c Update manifest.json for release v1.6.2.0 [skip ci] 2026-02-03 18:18:35 +00:00
CodeDevMLH
dd6583c055 Bump version to 1.6.2.0 and update changelog for new features
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-02-03 19:17:44 +01:00
CodeDevMLH
a0b0514159 Update manifest.json for release v1.6.1.0 [skip ci] 2026-02-03 18:08:59 +00:00
CodeDevMLH
e977c83e8f Bump version to 1.6.1.0 in project file and manifest
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-02-03 19:08:07 +01:00
CodeDevMLH
e281f5c579 test other ui design 2026-02-03 19:08:02 +01:00
CodeDevMLH
e6b646e478 Update .gitignore to include test-site-new.html [skip CI] 2026-02-03 18:50:22 +01:00
CodeDevMLH
8d6bc12fa4 Update manifest.json for release v1.6.0.0 [skip ci] 2026-02-03 17:49:06 +00:00
CodeDevMLH
f036e748da Add client-side toggle option for seasonal settings
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-02-03 18:48:11 +01:00
CodeDevMLH
0177a7caea Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 22:40:50 +00:00
CodeDevMLH
c45e9f6156 Auto-Update MediaBar Enhanced to v1.2.3.7
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 22:40:00 +00:00
CodeDevMLH
8dc9b9f157 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 21:32:51 +00:00
CodeDevMLH
e73c6c14bb Auto-Update MediaBar Enhanced to v1.2.3.6
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 21:32:01 +00:00
CodeDevMLH
aa1c60f9ce Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 20:22:17 +00:00
CodeDevMLH
d1e668bcff Auto-Update MediaBar Enhanced to v1.2.3.5
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 20:21:27 +00:00
CodeDevMLH
0454d43f32 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 01:10:34 +00:00
CodeDevMLH
61b3b51139 Auto-Update MediaBar Enhanced to v1.2.3.4
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 01:09:45 +00:00
CodeDevMLH
60d1a546a2 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 01:06:40 +00:00
CodeDevMLH
669933d270 Auto-Update MediaBar Enhanced to v1.2.3.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 01:05:53 +00:00
CodeDevMLH
053f0ccfa7 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:46:16 +00:00
CodeDevMLH
60e998fc7f Auto-Update MediaBar Enhanced to v1.2.3.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-28 00:45:27 +00:00
CodeDevMLH
3aa631198a Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:31:33 +00:00
CodeDevMLH
b1876d655e Auto-Update MediaBar Enhanced to v1.2.3.2
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 00:30:43 +00:00
CodeDevMLH
9a9f89c1fc Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-28 00:18:24 +00:00
CodeDevMLH
2fb41f6442 Auto-Update MediaBar Enhanced to v1.2.3.1
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-28 00:17:35 +00:00
CodeDevMLH
4ab949e6d7 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-27 23:55:34 +00:00
CodeDevMLH
7d1024c917 Auto-Update MediaBar Enhanced to v1.2.3.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-27 23:54:49 +00:00
CodeDevMLH
c2e3d55110 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-24 22:54:52 +00:00
CodeDevMLH
c6503a90bb Auto-Update MediaBar Enhanced to v1.2.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-01-24 22:54:01 +00:00
CodeDevMLH
b70787d5ec Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-22 23:51:52 +00:00
CodeDevMLH
a9eb8113a6 Auto-Update MediaBar Enhanced to v1.2.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-22 23:51:03 +00:00
CodeDevMLH
aaf15d8934 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-21 23:12:11 +00:00
CodeDevMLH
ec298ebde0 bump version to 1.5.1.0 in project file
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 53s
2026-01-22 00:11:19 +01:00
CodeDevMLH
bb108d0f49 Update manifest.json for release v1.5.1.0 [skip ci] 2026-01-21 22:53:15 +00:00
MLH
f271e1715d Merge pull request 'fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes' (#1) from dev into main
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 57s
Reviewed-on: #1
2026-01-21 23:52:18 +01:00
CodeDevMLH
bd0e2779e5 fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes
All checks were successful
🏗️ Build Plugin / build (push) Successful in 53s
🏗️ Build Plugin / build (pull_request) Successful in 58s
2026-01-21 23:51:23 +01:00
CodeDevMLH
53a1682868 Enhance descriptions and overviews in manifest.json for Seasonals and Media Bar Enhanced plugins [skip ci] 2026-01-09 00:32:40 +01:00
CodeDevMLH
a7df2fd832 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 23:31:54 +00:00
CodeDevMLH
c56cde860b Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-08 23:31:04 +00:00
CodeDevMLH
59211e27c6 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 23:15:57 +00:00
CodeDevMLH
a2b1179353 Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 47s
2026-01-08 23:15:12 +00:00
CodeDevMLH
c7f34ec92f Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 22:16:58 +00:00
CodeDevMLH
4c011cf560 Auto-Update MediaBar Enhanced to v1.2.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 48s
2026-01-08 22:16:11 +00:00
CodeDevMLH
e5f78c711d Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 15:27:46 +00:00
CodeDevMLH
98a536315b Auto-Update MediaBar Enhanced to v1.1.2.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 15:27:00 +00:00
CodeDevMLH
01343848e3 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 14:55:53 +00:00
CodeDevMLH
113e7dd0f7 Auto-Update MediaBar Enhanced to v1.1.1.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 14:55:04 +00:00
CodeDevMLH
1bc4176771 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-08 02:16:44 +00:00
CodeDevMLH
b091e5592d Auto-Update MediaBar Enhanced to v1.1.0.0
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-08 02:15:57 +00:00
CodeDevMLH
cb2d86340e Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:34:26 +00:00
CodeDevMLH
57a92e94de Remove build.yaml configuration file for Seasonals plugin
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 57s
2026-01-07 00:33:36 +01:00
CodeDevMLH
30bc8bef39 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:27:26 +00:00
CodeDevMLH
2d237063a3 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-01-06 23:26:36 +00:00
CodeDevMLH
049a5075b5 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:18:28 +00:00
CodeDevMLH
cb2e9c4b07 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:17:39 +00:00
CodeDevMLH
5a39f85082 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:13:12 +00:00
CodeDevMLH
b863d201b9 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:12:24 +00:00
CodeDevMLH
78b50b41c2 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 23:11:24 +00:00
CodeDevMLH
0ba1545fd6 test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 49s
2026-01-07 00:10:33 +01:00
CodeDevMLH
16c4e0f29b fix link 2026-01-07 00:09:41 +01:00
CodeDevMLH
3581b8cbb2 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:57:27 +00:00
CodeDevMLH
7184c93f6f Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-06 22:56:39 +00:00
CodeDevMLH
0969f0238f Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:45:32 +00:00
CodeDevMLH
12f868d3f9 test ..
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-06 23:44:42 +01:00
CodeDevMLH
dc7be56807 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2026-01-06 23:41:57 +01:00
CodeDevMLH
cb60690e6b fix log name 2026-01-06 23:41:55 +01:00
CodeDevMLH
e0397bb2e8 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:40:15 +00:00
CodeDevMLH
822bcafd11 Auto-Update MediaBar Enhanced to v1.0.0.3
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 50s
2026-01-06 22:39:29 +00:00
CodeDevMLH
429d96c816 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:37:52 +00:00
CodeDevMLH
e948055f0f test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2026-01-06 23:37:01 +01:00
CodeDevMLH
5129d46163 fix remote repo 2026-01-06 23:36:39 +01:00
CodeDevMLH
660f7142ef Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:33:07 +00:00
CodeDevMLH
9f657588d8 fix changelog
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 52s
2026-01-06 23:32:16 +01:00
CodeDevMLH
0457f1a764 Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2026-01-06 23:30:37 +01:00
CodeDevMLH
f196c6c296 fix url 2026-01-06 23:30:36 +01:00
CodeDevMLH
2d2dcaee71 Update manifest.json for release v1.5.0.0 [skip ci] 2026-01-06 22:21:50 +00:00
CodeDevMLH
399b19f384 fixes
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 54s
2026-01-06 23:20:57 +01:00
CodeDevMLH
cba5bc8c95 add media bar
Some checks failed
Auto Release Plugin / build-and-release (push) Failing after 46s
2026-01-06 23:12:45 +01:00
CodeDevMLH
0c97e2f4c4 Update build workflow to include branch specification for push events 2026-01-06 23:10:35 +01:00
CodeDevMLH
631fbfb1cb Refactor SeasonalsPlugin: Simplify script injection logic (v1.5.0.0)
Some checks failed
🏗️ Build Plugin / build (push) Failing after 55s
Auto Release Plugin / build-and-release (push) Failing after 1m5s
2026-01-06 22:59:18 +01:00
CodeDevMLH
5f14caeeb7 Add central manifest update step to release automation workflow 2026-01-06 22:14:09 +01:00
CodeDevMLH
bb4d35fc00 Update manifest.json for release v1.4.1.0 [skip ci] 2026-01-06 21:12:47 +00:00
CodeDevMLH
a30729cf0b small fixes
All checks were successful
🏗️ Build Plugin / build (push) Successful in 58s
Auto Release Plugin / build-and-release (push) Successful in 1m9s
2026-01-06 22:11:34 +01:00
CodeDevMLH
83bc5f6b1d Update manifest.json for release v1.4.1.0 [skip ci] 2026-01-06 20:29:59 +00:00
CodeDevMLH
8bcfaee22e Update logos for improved branding
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m4s
🏗️ Build Plugin / build (push) Successful in 1m8s
2026-01-06 21:28:51 +01:00
CodeDevMLH
6882922c5b Update release guide to remove build.yaml version update instructions 2026-01-02 22:58:19 +01:00
CodeDevMLH
35ea057110 modded logo
Some checks failed
🏗️ Build Plugin / build (push) Successful in 59s
Auto Release Plugin / build-and-release (push) Failing after 1m6s
2026-01-02 22:58:09 +01:00
CodeDevMLH
2656606f8c Add new build workflow for Jellyfin Plugin and deprecate old configuration 2026-01-02 21:33:19 +01:00
CodeDevMLH
024343f045 fix wrong plugin name in build workflow 2026-01-02 21:27:52 +01:00
CodeDevMLH
887e697a90 Remove unused environment variable from build workflow 2026-01-02 21:26:16 +01:00
CodeDevMLH
6e623668d5 Add build workflow for Jellyfin Plugin with artifact upload 2026-01-02 21:23:35 +01:00
CodeDevMLH
17c60d2fb3 Remove internal build workflow configuration 2026-01-02 19:51:31 +01:00
CodeDevMLH
b4ce6a7d18 Rename artifact upload name to 'plugin-build-artifact' for clarity 2026-01-02 19:50:33 +01:00
CodeDevMLH
d4ec7cf31d Update artifact upload action and retention period in build workflow 2026-01-02 19:48:08 +01:00
CodeDevMLH
7f01da0301 Downgrade upload-artifact action to v4 for compatibility 2026-01-02 19:46:20 +01:00
CodeDevMLH
0e19769d79 Downgrade upload-artifact action to v5 for compatibility 2026-01-02 19:43:31 +01:00
CodeDevMLH
0cda35f636 Add environment variable to build job for package management 2026-01-02 19:37:45 +01:00
CodeDevMLH
f8e30efe66 Update .NET version specification in build workflow to use "9.x" 2026-01-02 19:34:19 +01:00
CodeDevMLH
4160ac59ca Update jellyfin-plugin-repository-manager to version 1.1.1 in build workflow 2026-01-02 19:32:39 +01:00
CodeDevMLH
62ebe8b293 Refactor build workflow to streamline plugin build process and enhance artifact management 2026-01-02 19:25:49 +01:00
CodeDevMLH
fd891e84c1 Update release automation to ignore all markdown files 2025-12-31 01:38:04 +01:00
CodeDevMLH
de211d4567 Update release automation to ignore changes in the .gitea directory 2025-12-31 01:26:19 +01:00
CodeDevMLH
dc292504b0 Update manifest.json for release v1.4.1.0 [skip ci] 2025-12-31 00:24:59 +00:00
CodeDevMLH
9cf4da92ac Add GitHub Actions workflows for building and releasing the Jellyfin plugin
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 51s
2025-12-31 01:24:07 +01:00
CodeDevMLH
817b8786c8 Update manifest.json for release v1.4.1.0 [skip ci] 2025-12-31 00:19:26 +00:00
CodeDevMLH
fe7f1a30bb test
All checks were successful
Auto Release Plugin / build-and-release (push) Successful in 1m8s
2025-12-31 01:18:17 +01:00
MLH
2d72c6e957 .github/renovate.json gelöscht 2025-12-31 01:17:37 +01:00
MLH
e8c4d5a044 .github/workflows/test.yaml gelöscht 2025-12-31 01:15:14 +01:00
MLH
781c605e12 .github/workflows/test.yaml aktualisiert
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 11s
2025-12-31 01:13:17 +01:00
CodeDevMLH
c712777d61 Fix workflow path for internal build and add build_internal.yaml
Some checks failed
🏗️ Build Plugin / call (push) Failing after 1m10s
2025-12-31 00:35:10 +01:00
MLH
9755763e36 .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
2025-12-31 00:30:55 +01:00
MLH
3d24c84c0f .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
2025-12-31 00:30:17 +01:00
MLH
73b64c7302 Dateien nach ".github" hochladen
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
🏗️ Build Plugin / test_connection (push) Successful in 3s
2025-12-31 00:29:28 +01:00
MLH
f42bba5dc3 .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 1s
🏗️ Build Plugin / test_connection (push) Successful in 3s
2025-12-31 00:21:42 +01:00
MLH
8095196b1e .github/workflows/build.yaml aktualisiert
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
🏗️ Build Plugin / test_connection (push) Successful in 3s
2025-12-30 22:42:13 +01:00
MLH
41ef12d970 .github/workflows/build.yaml aktualisiert 2025-12-30 22:41:13 +01:00
MLH
c984464f78 .github/workflows/test.yaml hinzugefügt
Some checks failed
🏗️ Build Plugin / call (push) Failing after 0s
2025-12-30 22:37:28 +01:00
CodeDevMLH
29af8b8671 add gitea actions
Some checks failed
🏗️ Build Plugin / call (push) Failing after 1s
2025-12-30 22:16:56 +01:00
CodeDevMLH
2df173de8e remove build from gitignore 2025-12-30 21:54:12 +01:00
CodeDevMLH
6ea83e777a Merge branch 'main' of ssh://git.mahom03-spacecloud.de:44322/CodeDevMLH/Jellyfin-Seasonals-Plugin 2025-12-30 16:00:05 +01:00
CodeDevMLH
96d847a5f8 fix: comment out changelog body in release automation 2025-12-30 16:00:03 +01:00
CodeDevMLH
2e6a1c9322 feat: Add GitHub Actions workflow for automated plugin releases. 2025-12-30 15:44:43 +01:00
CodeDevMLH
90e028eb3b feat: Add GitHub Actions workflow for automated plugin releases. 2025-12-30 15:26:47 +01:00
CodeDevMLH
0db833c4dc fix: improve fireworks display handling for scrolling and container positioning 2025-12-30 02:09:18 +01:00
CodeDevMLH
8c00a35a22 fix: update checksum and timestamp for version 1.4.1.0 in manifest 2025-12-30 01:46:50 +01:00
CodeDevMLH
8d6d202ee9 fix: update checksum and timestamp for version 1.4.1.0 in manifest 2025-12-30 00:12:44 +01:00
CodeDevMLH
d9a4f623a0 fix: update checksum and timestamp for version 1.4.1.0 in manifest 2025-12-29 23:21:33 +01:00
36 changed files with 1445 additions and 1080 deletions

View File

@@ -0,0 +1,45 @@
name: '🏗️ Build Plugin'
on:
push:
branches:
- dev
paths-ignore:
- '**/*.md'
- '.gitea/**'
- '.github/**'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
pull_request:
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.x"
- name: Build Jellyfin Plugin
run: |
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
- name: Upload Artifact
uses: christopherHX/gitea-upload-artifact@v4
with:
name: plugin-build-artifact
retention-days: 5
if-no-files-found: error
path: bin/Publish/Jellyfin.Plugin.Seasonals.zip

View File

@@ -0,0 +1,45 @@
name: '🏗️ Build Plugin'
on:
push:
paths-ignore:
- '**/*.md'
- '.gitea/**'
- '.github/**'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
pull_request:
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
env:
PIP_BREAK_SYSTEM_PACKAGES: "1"
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.x"
- name: Build Jellyfin Plugin
uses: oddstr13/jellyfin-plugin-repository-manager@v1.1.1
id: jprm
with:
dotnet-target: "net9.0"
- name: Upload Artifact
uses: christopherHX/gitea-upload-artifact@v4
with:
name: plugin-build-artifact
retention-days: 5
if-no-files-found: error
path: ${{ steps.jprm.outputs.artifact }}

View File

@@ -0,0 +1,186 @@
name: Auto Release Plugin
on:
push:
branches:
- main
paths-ignore:
- '.gitea/**'
- '.github/**'
- '*.md'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
- name: Read Version from Manifest
id: read_version
run: |
VERSION=$(jq -r '.[0].versions[0].version' manifest.json)
CHANGELOG=$(jq -r '.[0].versions[0].changelog' manifest.json)
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' manifest.json)
echo "Detected Version: $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "TARGET_ABI=$TARGET_ABI" >> $GITHUB_ENV
# Also export GUID for later use
PLUGIN_GUID=$(jq -r '.[0].guid' manifest.json)
echo "PLUGIN_GUID=$PLUGIN_GUID" >> $GITHUB_ENV
# Escape newlines in changelog for GITHUB_ENV
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Build and Zip
shell: bash
run: |
# Inject version from manifest into the build
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish /p:Version=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }}
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
cd ../..
# Calculate hash
HASH=$(md5sum bin/Publish/Jellyfin.Plugin.Seasonals.zip | awk '{ print $1 }')
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Export variables for next steps
echo "ZIP_HASH=$HASH" >> $GITHUB_ENV
echo "BUILD_TIME=$TIME" >> $GITHUB_ENV
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
- name: Update manifest.json
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
echo "Updating manifest.json with:"
echo "Hash: ${{ env.ZIP_HASH }}"
echo "Time: ${{ env.BUILD_TIME }}"
echo "Url: $DOWNLOAD_URL"
jq --arg hash "${{ env.ZIP_HASH }}" \
--arg time "${{ env.BUILD_TIME }}" \
--arg url "$DOWNLOAD_URL" \
'.[0].versions[0].checksum = $hash | .[0].versions[0].timestamp = $time | .[0].versions[0].sourceUrl = $url' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json
- name: Create Release
uses: akkuman/gitea-release-action@v1
with:
server_url: "https://git.mahom03-spacecloud.de"
body: ${{ env.CHANGELOG }}
token: ${{ secrets.GITHUB_TOKEN }}
files: ${{ env.ZIP_PATH }}
name: "v${{ env.VERSION }}"
tag_name: "v${{ env.VERSION }}"
draft: false
prerelease: false
# Update Message in Remote Repository
- name: Checkout Central Manifest Repo
uses: actions/checkout@v6
with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
path: central-manifest
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest
shell: bash
run: |
cd central-manifest
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
# 1. Get info from previous steps
VERSION="${{ env.VERSION }}"
HASH="${{ env.ZIP_HASH }}"
TIME="${{ env.BUILD_TIME }}"
DOWNLOAD_URL="https://git.mahom03-spacecloud.de/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
# 2. Get info from env
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
CHANGELOG="${{ env.CHANGELOG }}"
TARGET_ABI="${{ env.TARGET_ABI }}"
echo "Updating Central Manifest for Plugin GUID: $PLUGIN_GUID"
# 3. Update/Prepend entry in central manifest.json
# Logic:
# - If array is empty or new version != old version: PREPEND new entry
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
jq --arg guid "$PLUGIN_GUID" \
--arg hash "$HASH" \
--arg time "$TIME" \
--arg url "$DOWNLOAD_URL" \
--arg ver "$VERSION" \
--arg changelog "$CHANGELOG" \
--arg abi "$TARGET_ABI" \
'map(if .guid == $guid then
if .versions[0].version != $ver then
# New Version -> Prepend
.versions = [{
"version": $ver,
"changelog": $changelog,
"targetAbi": $abi,
"sourceUrl": $url,
"checksum": $hash,
"timestamp": $time
}] + .versions
else
# Same Version -> Update existing (overwrite top)
.versions[0].changelog = $changelog |
.versions[0].targetAbi = $abi |
.versions[0].sourceUrl = $url |
.versions[0].checksum = $hash |
.versions[0].timestamp = $time
end
else . end)' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest
run: |
cd central-manifest
git config user.name "CodeDevMLH"
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
# Check if there are changes
if [[ -n $(git status -s) ]]; then
git add manifest.json
git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
git push
else
echo "No changes to central manifest."
fi

View File

@@ -3,16 +3,43 @@ name: '🏗️ Build Plugin'
on: on:
push: push:
branches: branches:
- master - dev
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
- '.gitea/**'
- '.github/**'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
pull_request: pull_request:
branches:
- master
paths-ignore: paths-ignore:
- '**/*.md' - '**/*.md'
workflow_dispatch: workflow_dispatch:
jobs: jobs:
call: build:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/build.yaml@master runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: "9.x"
- name: Build Jellyfin Plugin
run: |
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: plugin-build-artifact
retention-days: 5
if-no-files-found: error
path: bin/Publish/Jellyfin.Plugin.Seasonals.zip

View File

@@ -1,20 +0,0 @@
name: '📝 Create/Update Release Draft & Release Bump PR'
on:
push:
branches:
- master
paths-ignore:
- build.yaml
workflow_dispatch:
repository_dispatch:
types:
- update-prep-command
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/changelog.yaml@master
with:
repository-name: jellyfin/jellyfin-plugin-template
secrets:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,13 +0,0 @@
# Allows for the definition of PR and Issue /commands
name: '📟 Slash Command Dispatcher'
on:
issue_comment:
types:
- created
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-dispatch.yaml@master
secrets:
token: .

View File

@@ -1,16 +0,0 @@
name: '🔀 PR Rebase Command'
on:
repository_dispatch:
types:
- rebase-command
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/command-rebase.yaml@master
with:
rebase-head: ${{ github.event.client_payload.pull_request.head.label }}
repository-full-name: ${{ github.event.client_payload.github.payload.repository.full_name }}
comment-id: ${{ github.event.client_payload.github.payload.comment.id }}
secrets:
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,18 +0,0 @@
name: '🚀 Publish Plugin'
on:
release:
types:
- released
workflow_dispatch:
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/publish.yaml@master
with:
version: ${{ github.event.release.tag_name }}
is-unstable: ${{ github.event.release.prerelease }}
secrets:
deploy-host: ${{ secrets.DEPLOY_HOST }}
deploy-user: ${{ secrets.DEPLOY_USER }}
deploy-key: ${{ secrets.DEPLOY_KEY }}

184
.github/workflows/release_automation.yml vendored Normal file
View File

@@ -0,0 +1,184 @@
name: Auto Release Plugin
on:
push:
branches:
- main
paths-ignore:
- '.github/**'
- 'README.md'
- 'jellyfin.ruleset'
- '.gitignore'
- '.editorconfig'
- 'LICENSE'
- 'logo.png'
jobs:
build-and-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup .NET
uses: actions/setup-dotnet@v5
with:
dotnet-version: '9.x'
- name: Read Version from Manifest
id: read_version
run: |
VERSION=$(jq -r '.[0].versions[0].version' manifest.json)
CHANGELOG=$(jq -r '.[0].versions[0].changelog' manifest.json)
TARGET_ABI=$(jq -r '.[0].versions[0].targetAbi' manifest.json)
echo "Detected Version: $VERSION"
echo "VERSION=$VERSION" >> $GITHUB_ENV
echo "TARGET_ABI=$TARGET_ABI" >> $GITHUB_ENV
# Also export GUID for later use
PLUGIN_GUID=$(jq -r '.[0].guid' manifest.json)
echo "PLUGIN_GUID=$PLUGIN_GUID" >> $GITHUB_ENV
# Escape newlines in changelog for GITHUB_ENV
echo "CHANGELOG<<EOF" >> $GITHUB_ENV
echo "$CHANGELOG" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Build and Zip
shell: bash
run: |
# Inject version from manifest into the build
dotnet build Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj --configuration Release --output bin/Publish /p:Version=${{ env.VERSION }} /p:AssemblyVersion=${{ env.VERSION }}
cd bin/Publish
zip -r Jellyfin.Plugin.Seasonals.zip *
cd ../..
# Calculate hash
HASH=$(md5sum bin/Publish/Jellyfin.Plugin.Seasonals.zip | awk '{ print $1 }')
TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Export variables for next steps
echo "ZIP_HASH=$HASH" >> $GITHUB_ENV
echo "BUILD_TIME=$TIME" >> $GITHUB_ENV
echo "ZIP_PATH=bin/Publish/Jellyfin.Plugin.Seasonals.zip" >> $GITHUB_ENV
- name: Update manifest.json
shell: bash
run: |
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
VERSION="${{ env.VERSION }}"
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
echo "Updating manifest.json with:"
echo "Hash: ${{ env.ZIP_HASH }}"
echo "Time: ${{ env.BUILD_TIME }}"
echo "Url: $DOWNLOAD_URL"
jq --arg hash "${{ env.ZIP_HASH }}" \
--arg time "${{ env.BUILD_TIME }}" \
--arg url "$DOWNLOAD_URL" \
'.[0].versions[0].checksum = $hash | .[0].versions[0].timestamp = $time | .[0].versions[0].sourceUrl = $url' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit manifest.json
uses: stefanzweifel/git-auto-commit-action@v7
with:
commit_message: "Update manifest.json for release v${{ env.VERSION }} [skip ci]"
file_pattern: manifest.json
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: "v${{ env.VERSION }}"
name: "v${{ env.VERSION }}"
# body: ${{ env.CHANGELOG }}
files: ${{ env.ZIP_PATH }}
draft: false
prerelease: false
generate_release_notes: true
# Update Message in Remote Repository
- name: Checkout Central Manifest Repo
uses: actions/checkout@v6
with:
repository: ${{ github.repository_owner }}/jellyfin-plugin-manifest
path: central-manifest
token: ${{ secrets.JELLYFIN_PLUGIN_MANIFEST_UPDATER_PAT }}
- name: Update Central Manifest
shell: bash
run: |
cd central-manifest
REPO_OWNER="${{ github.repository_owner }}"
REPO_NAME="${{ github.event.repository.name }}"
# 1. Get info from previous steps
VERSION="${{ env.VERSION }}"
HASH="${{ env.ZIP_HASH }}"
TIME="${{ env.BUILD_TIME }}"
DOWNLOAD_URL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download/v$VERSION/Jellyfin.Plugin.Seasonals.zip"
# 2. Get info from env
PLUGIN_GUID="${{ env.PLUGIN_GUID }}"
CHANGELOG="${{ env.CHANGELOG }}"
TARGET_ABI="${{ env.TARGET_ABI }}"
echo "Updating Central Manifest for Plugin GUID: $PLUGIN_GUID"
# 3. Update/Prepend entry in central manifest.json
# Logic:
# - If array is empty or new version != old version: PREPEND new entry
# - If new version == old version: OVERWRITE (update) existing entry (re-release)
jq --arg guid "$PLUGIN_GUID" \
--arg hash "$HASH" \
--arg time "$TIME" \
--arg url "$DOWNLOAD_URL" \
--arg ver "$VERSION" \
--arg changelog "$CHANGELOG" \
--arg abi "$TARGET_ABI" \
'map(if .guid == $guid then
if .versions[0].version != $ver then
# New Version -> Prepend
.versions = [{
"version": $ver,
"changelog": $changelog,
"targetAbi": $abi,
"sourceUrl": $url,
"checksum": $hash,
"timestamp": $time
}] + .versions
else
# Same Version -> Update existing (overwrite top)
.versions[0].changelog = $changelog |
.versions[0].targetAbi = $abi |
.versions[0].sourceUrl = $url |
.versions[0].checksum = $hash |
.versions[0].timestamp = $time
end
else . end)' \
manifest.json > manifest.json.tmp && mv manifest.json.tmp manifest.json
- name: Commit and Push Central Manifest
run: |
cd central-manifest
git config user.name "CodeDevMLH"
git config user.email "145071728+CodeDevMLH@users.noreply.github.com"
# Check if there are changes
if [[ -n $(git status -s) ]]; then
git add manifest.json
git commit -m "Auto-Update Seasonals to v${{ env.VERSION }}"
git push
else
echo "No changes to central manifest."
fi

View File

@@ -1,20 +0,0 @@
name: '🔬 Run CodeQL'
on:
push:
branches: [ master ]
paths-ignore:
- '**/*.md'
pull_request:
branches: [ master ]
paths-ignore:
- '**/*.md'
schedule:
- cron: '24 2 * * 4'
workflow_dispatch:
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/scan-codeql.yaml@master
with:
repository-name: jellyfin/jellyfin-plugin-template

View File

@@ -1,18 +0,0 @@
name: '🧪 Test Plugin'
on:
push:
branches:
- master
paths-ignore:
- '**/*.md'
pull_request:
branches:
- master
paths-ignore:
- '**/*.md'
workflow_dispatch:
jobs:
call:
uses: jellyfin/jellyfin-meta-plugins/.github/workflows/test.yaml@master

2
.gitignore vendored
View File

@@ -5,5 +5,5 @@ obj/
artifacts artifacts
test-site.html test-site.html
build.yaml test-site-new.html
RELEASE_GUIDE.md RELEASE_GUIDE.md

View File

@@ -3,6 +3,8 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Jellyfin.Plugin.Seasonals;
using Jellyfin.Plugin.Seasonals.Configuration;
namespace Jellyfin.Plugin.Seasonals.Api; namespace Jellyfin.Plugin.Seasonals.Api;
@@ -19,9 +21,9 @@ public class SeasonalsController : ControllerBase
/// <returns>The configuration object.</returns> /// <returns>The configuration object.</returns>
[HttpGet("Config")] [HttpGet("Config")]
[Produces("application/json")] [Produces("application/json")]
public ActionResult<object> GetConfig() public ActionResult<PluginConfiguration> GetConfig()
{ {
return SeasonalsPlugin.Instance?.Configuration ?? new object(); return SeasonalsPlugin.Instance?.Configuration ?? new PluginConfiguration();
} }
/// <summary> /// <summary>
@@ -38,7 +40,7 @@ public class SeasonalsController : ControllerBase
return BadRequest(); return BadRequest();
} }
var assembly = Assembly.GetExecutingAssembly(); var assembly = typeof(SeasonalsPlugin).Assembly;
// Convert path to resource name // Convert path to resource name
var resourcePath = path.Replace('/', '.').Replace('\\', '.'); var resourcePath = path.Replace('/', '.').Replace('\\', '.');
var resourceName = $"Jellyfin.Plugin.Seasonals.Web.{resourcePath}"; var resourceName = $"Jellyfin.Plugin.Seasonals.Web.{resourcePath}";

View File

@@ -15,6 +15,7 @@ public class PluginConfiguration : BasePluginConfiguration
IsEnabled = true; IsEnabled = true;
SelectedSeason = "none"; SelectedSeason = "none";
AutomateSeasonSelection = true; AutomateSeasonSelection = true;
EnableClientSideToggle = true;
Autumn = new AutumnOptions(); Autumn = new AutumnOptions();
Snowflakes = new SnowflakesOptions(); Snowflakes = new SnowflakesOptions();
@@ -43,6 +44,14 @@ public class PluginConfiguration : BasePluginConfiguration
/// </summary> /// </summary>
public bool AutomateSeasonSelection { get; set; } public bool AutomateSeasonSelection { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable client-side toggle for users.
/// </summary>
public bool EnableClientSideToggle { get; set; }
/// <summary>
/// Gets or sets the Seasonals options.
/// </summary>
public AutumnOptions Autumn { get; set; } public AutumnOptions Autumn { get; set; }
public SnowflakesOptions Snowflakes { get; set; } public SnowflakesOptions Snowflakes { get; set; }
public SnowfallOptions Snowfall { get; set; } public SnowfallOptions Snowfall { get; set; }

View File

@@ -2,21 +2,21 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Template</title> <title>Seasonals Configuration</title>
</head> </head>
<body> <body>
<div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox"> <div id="SeasonalsConfigPage" data-role="page" class="page type-interior pluginConfigurationPage" data-require="emby-input,emby-button,emby-select,emby-checkbox">
<style> <style>
select option:disabled { select option:disabled {
color: #a3a3a3 !important; color: #a3a3a3 !important;
} }
</style> </style>
<div data-role="content"> <div data-role="content">
<div class="content-primary"> <div class="content-primary">
<div class="sectionTitleContainer"> <div class="sectionTitleContainer">
<h2 class="sectionTitle">Seasonals</h2> <h2 class="sectionTitle">Seasonals</h2>
<a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;" <a is="emby-linkbutton" class="raised raised-mini emby-button" style="margin-left: 2em;"
target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals"> target="_blank" href="https://github.com/CodeDevMLH/Jellyfin-Seasonals">
<i class="md-icon button-icon button-icon-left secondaryText"></i> <i class="md-icon button-icon button-icon-left secondaryText"></i>
<span>Help</span> <span>Help</span>
</a> </a>
@@ -38,6 +38,15 @@
</label> </label>
<div class="fieldDescription">Automatically select the season based on the date.</div> <div class="fieldDescription">Automatically select the season based on the date.</div>
</div> </div>
<div class="checkboxContainer checkboxContainer-withDescription">
<label class="emby-checkbox-label">
<input id="EnableClientSideToggle" name="EnableClientSideToggle" type="checkbox"
is="emby-checkbox" />
<span>Allow Client-Side Toggle</span>
</label>
<div class="fieldDescription">If enabled, users will see a settings icon in the header to toggle
animations for their browser.</div>
</div>
<div class="selectContainer"> <div class="selectContainer">
<label class="selectLabel" for="SelectedSeason">Selected Season</label> <label class="selectLabel" for="SelectedSeason">Selected Season</label>
<select is="emby-select" id="SelectedSeason" name="SelectedSeason" class="emby-select-withcolor emby-select"> <select is="emby-select" id="SelectedSeason" name="SelectedSeason" class="emby-select-withcolor emby-select">
@@ -530,6 +539,7 @@
document.querySelector('#IsEnabled').checked = config.IsEnabled; document.querySelector('#IsEnabled').checked = config.IsEnabled;
document.querySelector('#SelectedSeason').value = config.SelectedSeason; document.querySelector('#SelectedSeason').value = config.SelectedSeason;
document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection; document.querySelector('#AutomateSeasonSelection').checked = config.AutomateSeasonSelection;
document.querySelector('#EnableClientSideToggle').checked = config.EnableClientSideToggle !== undefined ? config.EnableClientSideToggle : true;
// Advanced Config // Advanced Config
// Autumn // Autumn
@@ -621,98 +631,100 @@
document.querySelector('#SeasonalsConfigForm') document.querySelector('#SeasonalsConfigForm')
.addEventListener('submit', function(e) { .addEventListener('submit', function(e) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) { ApiClient.getPluginConfiguration(SeasonalsConfig.pluginUniqueId).then(function (config) {
config.IsEnabled = document.querySelector('#IsEnabled').checked; config.IsEnabled = document.querySelector('#IsEnabled').checked;
config.SelectedSeason = document.querySelector('#SelectedSeason').value; config.SelectedSeason = document.querySelector('#SelectedSeason').value;
config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked; config.AutomateSeasonSelection = document.querySelector('#AutomateSeasonSelection').checked;
config.EnableClientSideToggle = document.querySelector('#EnableClientSideToggle').checked;
// Advanced Config
// Autumn
config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
// Snowflakes // Advanced Config
config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value); // Autumn
config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked; config.Autumn.EnableAutumn = document.querySelector('#EnableAutumn').checked;
config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked; config.Autumn.LeafCount = parseInt(document.querySelector('#AutumnLeafCount').value);
config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked; config.Autumn.EnableRandomLeaves = document.querySelector('#EnableRandomLeaves').checked;
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked; config.Autumn.EnableRandomLeavesMobile = document.querySelector('#EnableRandomLeavesMobile').checked;
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked; config.Autumn.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationAutumn').checked;
config.Autumn.EnableRotation = document.querySelector('#EnableRotation').checked;
// Snowfall // Snowflakes
config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked; config.Snowflakes.SnowflakeCount = parseInt(document.querySelector('#SnowflakesCount').value);
config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value); config.Snowflakes.EnableSnowflakes = document.querySelector('#EnableSnowflakes').checked;
config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value); config.Snowflakes.EnableRandomSnowflakes = document.querySelector('#EnableRandomSnowflakes').checked;
config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value); config.Snowflakes.EnableRandomSnowflakesMobile = document.querySelector('#EnableRandomSnowflakesMobile').checked;
config.Snowflakes.EnableColoredSnowflakes = document.querySelector('#EnableColoredSnowflakes').checked;
config.Snowflakes.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationSnowflakes').checked;
// Snowstorm // Snowfall
config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked; config.Snowfall.EnableSnowfall = document.querySelector('#EnableSnowfall').checked;
config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value); config.Snowfall.SnowflakesCount = parseInt(document.querySelector('#SnowfallCount').value);
config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value); config.Snowfall.SnowflakesCountMobile = parseInt(document.querySelector('#SnowfallCountMobile').value);
config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value); config.Snowfall.Speed = parseFloat(document.querySelector('#SnowfallSpeed').value);
config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
// Fireworks // Snowstorm
config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked; config.Snowstorm.EnableSnowstorm = document.querySelector('#EnableSnowstorm').checked;
config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value); config.Snowstorm.SnowflakesCount = parseInt(document.querySelector('#SnowstormCount').value);
config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value); config.Snowstorm.SnowflakesCountMobile = parseInt(document.querySelector('#SnowstormCountMobile').value);
config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked; config.Snowstorm.Speed = parseFloat(document.querySelector('#SnowstormSpeed').value);
config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value); config.Snowstorm.HorizontalWind = parseFloat(document.querySelector('#SnowstormHorizontalWind').value);
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value); config.Snowstorm.VerticalVariation = parseFloat(document.querySelector('#SnowstormVerticalVariation').value);
// Halloween // Fireworks
config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked; config.Fireworks.EnableFireworks = document.querySelector('#EnableFireworks').checked;
config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value); config.Fireworks.ParticleCount = parseInt(document.querySelector('#FireworksParticles').value);
config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked; config.Fireworks.LaunchInterval = parseInt(document.querySelector('#FireworksInterval').value);
config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked; config.Fireworks.ScrollFireworks = document.querySelector('#ScrollFireworks').checked;
config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked; config.Fireworks.MinFireworks = parseInt(document.querySelector('#MinFireworks').value);
config.Fireworks.MaxFireworks = parseInt(document.querySelector('#MaxFireworks').value);
// Hearts // Halloween
config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked; config.Halloween.EnableHalloween = document.querySelector('#EnableHalloween').checked;
config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value); config.Halloween.SymbolCount = parseInt(document.querySelector('#HalloweenCount').value);
config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked; config.Halloween.EnableRandomSymbols = document.querySelector('#EnableRandomHalloween').checked;
config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked; config.Halloween.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHalloweenMobile').checked;
config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked; config.Halloween.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHalloween').checked;
// Christmas // Hearts
config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked; config.Hearts.EnableHearts = document.querySelector('#EnableHearts').checked;
config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value); config.Hearts.SymbolCount = parseInt(document.querySelector('#HeartsCount').value);
config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked; config.Hearts.EnableRandomSymbols = document.querySelector('#EnableRandomHearts').checked;
config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked; config.Hearts.EnableRandomSymbolsMobile = document.querySelector('#EnableRandomHeartsMobile').checked;
config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked; config.Hearts.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationHearts').checked;
// Santa // Christmas
config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked; config.Christmas.EnableChristmas = document.querySelector('#EnableChristmas').checked;
config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value); config.Christmas.SymbolCount = parseInt(document.querySelector('#ChristmasCount').value);
config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value); config.Christmas.EnableRandomChristmas = document.querySelector('#EnableRandomChristmas').checked;
config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value); config.Christmas.EnableRandomChristmasMobile = document.querySelector('#EnableRandomChristmasMobile').checked;
config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value); config.Christmas.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationChristmas').checked;
config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
// Easter // Santa
config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked; config.Santa.EnableSanta = document.querySelector('#EnableSanta').checked;
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value); config.Santa.SnowflakesCount = parseInt(document.querySelector('#SantaSnowflakes').value);
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked; config.Santa.SnowflakesCountMobile = parseInt(document.querySelector('#SantaSnowflakesMobile').value);
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked; config.Santa.SantaSpeed = parseFloat(document.querySelector('#SantaSpeed').value);
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked; config.Santa.SantaSpeedMobile = parseFloat(document.querySelector('#SantaSpeedMobile').value);
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked; config.Santa.SnowFallSpeed = parseFloat(document.querySelector('#SantaSnowFallSpeed').value);
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value); config.Santa.MaxSantaRestTime = parseFloat(document.querySelector('#MaxSantaRestTime').value);
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value); config.Santa.MinSantaRestTime = parseFloat(document.querySelector('#MinSantaRestTime').value);
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value); config.Santa.MaxPresentFallSpeed = parseFloat(document.querySelector('#MaxPresentFallSpeed').value);
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value); config.Santa.MinPresentFallSpeed = parseFloat(document.querySelector('#MinPresentFallSpeed').value);
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) { // Easter
Dashboard.processPluginConfigurationUpdateResult(result); config.Easter.EnableEaster = document.querySelector('#EnableEaster').checked;
config.Easter.EggCount = parseInt(document.querySelector('#EasterEggCount').value);
config.Easter.EnableRandomEaster = document.querySelector('#EnableRandomEaster').checked;
config.Easter.EnableRandomEasterMobile = document.querySelector('#EnableRandomEasterMobile').checked;
config.Easter.EnableDifferentDuration = document.querySelector('#EnableDifferentDurationEaster').checked;
config.Easter.EnableBunny = document.querySelector('#EasterBunny').checked;
config.Easter.BunnyDuration = parseInt(document.querySelector('#BunnyDuration').value);
config.Easter.HopHeight = parseInt(document.querySelector('#HopHeight').value);
config.Easter.MinBunnyRestTime = parseInt(document.querySelector('#MinBunnyRestTime').value);
config.Easter.MaxBunnyRestTime = parseInt(document.querySelector('#MaxBunnyRestTime').value);
ApiClient.updatePluginConfiguration(SeasonalsConfig.pluginUniqueId, config).then(function (result) {
Dashboard.processPluginConfigurationUpdateResult(result);
}); });
}); });

View File

@@ -0,0 +1,49 @@
using System;
using Jellyfin.Plugin.Seasonals.Model;
namespace Jellyfin.Plugin.Seasonals.Helpers
{
public static class TransformationPatches
{
public static string IndexHtml(PatchRequestPayload payload)
{
// Always return original content if something fails or is null
string? originalContents = payload?.Contents;
if (string.IsNullOrEmpty(originalContents))
{
return originalContents ?? string.Empty;
}
try
{
// Safety Check: If plugin is disabled, do nothing
if (SeasonalsPlugin.Instance?.Configuration.IsEnabled != true)
{
return originalContents;
}
// Use StringBuilder for efficient modification
var builder = new System.Text.StringBuilder(originalContents);
// Inject Script if missing
if (!originalContents.Contains("seasonals.js", StringComparison.Ordinal))
{
var scriptIndex = originalContents.LastIndexOf(ScriptInjector.Marker, StringComparison.OrdinalIgnoreCase);
if (scriptIndex != -1)
{
builder.Insert(scriptIndex, ScriptInjector.ScriptTag + Environment.NewLine);
}
}
return builder.ToString();
}
catch
{
// On error, return original content to avoid breaking the UI
return originalContents;
}
}
}
}

View File

@@ -12,8 +12,8 @@
<!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> --> <!-- <TreatWarningsAsErrors>false</TreatWarningsAsErrors> -->
<Title>Jellyfin Seasonals Plugin</Title> <Title>Jellyfin Seasonals Plugin</Title>
<Authors>CodeDevMLH</Authors> <Authors>CodeDevMLH</Authors>
<Version>1.4.0.0</Version> <Version>1.6.2.0</Version>
<RepositoryUrl>https://github.com/CodeDevMLH/jellyfin-plugin-seasonals</RepositoryUrl> <RepositoryUrl>https://github.com/CodeDevMLH/Jellyfin-Seasonals</RepositoryUrl>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace Jellyfin.Plugin.Seasonals.Model
{
public class PatchRequestPayload
{
[JsonPropertyName("contents")]
public string? Contents { get; set; }
}
}

View File

@@ -1,8 +1,13 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Linq;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using Jellyfin.Plugin.Seasonals.Helpers;
using System.Collections.Generic;
using System.Linq;
namespace Jellyfin.Plugin.Seasonals; namespace Jellyfin.Plugin.Seasonals;
@@ -13,8 +18,8 @@ public class ScriptInjector
{ {
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly ILogger<ScriptInjector> _logger; private readonly ILogger<ScriptInjector> _logger;
private const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>"; public const string ScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
private const string Marker = "</body>"; public const string Marker = "</body>";
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ScriptInjector"/> class. /// Initializes a new instance of the <see cref="ScriptInjector"/> class.
@@ -30,98 +35,92 @@ public class ScriptInjector
/// <summary> /// <summary>
/// Injects the script tag into index.html if it's not already present. /// Injects the script tag into index.html if it's not already present.
/// </summary> /// </summary>
/// <returns>True if injection was successful or already present, false otherwise.</returns> public void Inject()
public bool Inject()
{ {
try try
{ {
var webPath = GetWebPath(); var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath)) if (string.IsNullOrEmpty(webPath))
{ {
_logger.LogWarning("Could not find Jellyfin web path. Script injection skipped."); _logger.LogWarning("Could not find Jellyfin web path. Script injection skipped. Attempting fallback.");
return false; RegisterFileTransformation();
return;
} }
var indexPath = Path.Combine(webPath, "index.html"); var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
_logger.LogWarning("index.html not found at {Path}. Script injection skipped.", indexPath); _logger.LogWarning("index.html not found at {Path}. Script injection skipped. Attempting fallback.", indexPath);
return false; RegisterFileTransformation();
return;
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (content.Contains(ScriptTag, StringComparison.Ordinal)) if (!content.Contains(ScriptTag))
{ {
_logger.LogInformation("Seasonals script already injected."); var index = content.IndexOf(Marker, StringComparison.OrdinalIgnoreCase);
return true; if (index != -1)
{
content = content.Insert(index, ScriptTag + Environment.NewLine);
File.WriteAllText(indexPath, content);
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
}
else
{
_logger.LogWarning("Script already present in index.html. Or could not be injected.");
}
} }
// Insert before the closing body tag
var newContent = content.Replace(Marker, $"{ScriptTag}\n{Marker}", StringComparison.Ordinal);
if (string.Equals(newContent, content, StringComparison.Ordinal))
{
_logger.LogWarning("Could not find closing body tag in index.html. Script injection skipped.");
return false;
}
File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully injected Seasonals script into index.html.");
return true;
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
_logger.LogWarning("Access was denied when attempting to inject script into index.html. Automatic injection failed. Please ensure the Jellyfin web directory is writable by the process, or manually add the script tag: {ScriptTag}", ScriptTag); _logger.LogWarning("Unauthorized access when attempting to inject script into index.html. Automatic injection failed. Attempting fallback now...");
return false; RegisterFileTransformation();
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error injecting Seasonals script."); _logger.LogError(ex, "Error injecting Seasonals script. Attempting fallback.");
return false; RegisterFileTransformation();
} }
} }
/// <summary> /// <summary>
/// Removes the script tag from index.html. /// Removes the script tag from index.html.
/// </summary> /// </summary>
public bool Remove() public void Remove()
{ {
UnregisterFileTransformation();
try try
{ {
var webPath = GetWebPath(); var webPath = GetWebPath();
if (string.IsNullOrEmpty(webPath)) if (string.IsNullOrEmpty(webPath))
{ {
return false; return;
} }
var indexPath = Path.Combine(webPath, "index.html"); var indexPath = Path.Combine(webPath, "index.html");
if (!File.Exists(indexPath)) if (!File.Exists(indexPath))
{ {
return false; return;
} }
var content = File.ReadAllText(indexPath); var content = File.ReadAllText(indexPath);
if (!content.Contains(ScriptTag, StringComparison.Ordinal)) if (content.Contains(ScriptTag))
{ {
return true; content = content.Replace(ScriptTag + Environment.NewLine, "").Replace(ScriptTag, "");
File.WriteAllText(indexPath, content);
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
} else {
_logger.LogInformation("Seasonals script tag not found in index.html. No removal necessary.");
} }
// Try to remove with newline first, then just the tag to ensure clean removal
var newContent = content.Replace($"{ScriptTag}\n", "", StringComparison.Ordinal)
.Replace(ScriptTag, "", StringComparison.Ordinal);
File.WriteAllText(indexPath, newContent);
_logger.LogInformation("Successfully removed Seasonals script from index.html.");
return true;
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
{ {
_logger.LogWarning("Permission denied when attempting to remove script from index.html."); _logger.LogWarning("Unauthorized access when attempting to remove script from index.html.");
return false;
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, "Error removing Seasonals script."); _logger.LogError(ex, "Error removing Seasonals script.");
return false;
} }
} }
@@ -135,4 +134,74 @@ public class ScriptInjector
var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public); var prop = _appPaths.GetType().GetProperty("WebPath", BindingFlags.Instance | BindingFlags.Public);
return prop?.GetValue(_appPaths) as string; return prop?.GetValue(_appPaths) as string;
} }
private void RegisterFileTransformation()
{
_logger.LogInformation("Seasonals Fallback. Registering file transformations.");
List<JObject> payloads = new List<JObject>();
{
JObject payload = new JObject();
payload.Add("id", "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
payload.Add("fileNamePattern", "index.html");
payload.Add("callbackAssembly", GetType().Assembly.FullName);
payload.Add("callbackClass", typeof(TransformationPatches).FullName);
payload.Add("callbackMethod", nameof(TransformationPatches.IndexHtml));
payloads.Add(payload);
}
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
foreach (JObject payload in payloads)
{
pluginInterfaceType.GetMethod("RegisterTransformation")?.Invoke(null, new object?[] { payload });
}
_logger.LogInformation("File transformations registered successfully.");
}
else
{
_logger.LogWarning("FileTransformation plugin found but PluginInterface type missing.");
}
}
else
{
_logger.LogWarning("FileTransformation plugin assembly not found. Fallback injection skipped.");
}
}
private void UnregisterFileTransformation()
{
try
{
Assembly? fileTransformationAssembly =
AssemblyLoadContext.All.SelectMany(x => x.Assemblies).FirstOrDefault(x =>
x.FullName?.Contains(".FileTransformation") ?? false);
if (fileTransformationAssembly != null)
{
Type? pluginInterfaceType = fileTransformationAssembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (pluginInterfaceType != null)
{
Guid id = Guid.Parse("ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4");
pluginInterfaceType.GetMethod("RemoveTransformation")?.Invoke(null, new object?[] { id });
_logger.LogInformation("File transformation unregistered successfully.");
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Error attempting to unregister file transformation. It might not have been registered.");
}
}
} }

View File

@@ -1,17 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using Jellyfin.Plugin.Seasonals.Configuration; using Jellyfin.Plugin.Seasonals.Configuration;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Plugins; using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Jellyfin.Plugin.Seasonals; namespace Jellyfin.Plugin.Seasonals;
@@ -38,16 +33,11 @@ public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
if (Configuration.IsEnabled) if (Configuration.IsEnabled)
{ {
if (!_scriptInjector.Inject()) _scriptInjector.Inject();
{
TryRegisterFallback(loggerFactory.CreateLogger("FileTransformationFallback"));
}
} }
else else
{ {
if (!_scriptInjector.Remove()) { _scriptInjector.Remove();
TryRemoveFallback(loggerFactory.CreateLogger("FileTransformationFallback"));
}
} }
} }
@@ -61,16 +51,11 @@ public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
{ {
if (Configuration.IsEnabled) if (Configuration.IsEnabled)
{ {
if (!_scriptInjector.Inject()) _scriptInjector.Inject();
{
TryRegisterFallback(_loggerFactory.CreateLogger("FileTransformationFallback"));
}
} }
else else
{ {
if (!_scriptInjector.Remove()) { _scriptInjector.Remove();
TryRemoveFallback(_loggerFactory.CreateLogger("FileTransformationFallback"));
}
} }
} }
} }
@@ -86,140 +71,7 @@ public class SeasonalsPlugin : BasePlugin<PluginConfiguration>, IHasWebPages
/// </summary> /// </summary>
public static SeasonalsPlugin? Instance { get; private set; } public static SeasonalsPlugin? Instance { get; private set; }
/// <summary>
/// Callback method for FileTransformation plugin.
/// </summary>
/// <param name="payload">The payload containing the file contents.</param>
/// <returns>The modified file contents.</returns>
public static string TransformIndexHtml(JObject payload)
{
// CRITICAL: Always return original content if something fails or is null
string? originalContents = payload?["contents"]?.ToString();
if (string.IsNullOrEmpty(originalContents))
{
return originalContents ?? string.Empty;
}
try
{
var html = originalContents;
const string scriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\" defer></script>";
// MARK: Remember me to remove legacy script tag support in future versions...
const string legacyScriptTag = "<script src=\"/Seasonals/Resources/seasonals.js\"></script>";
if (Instance?.Configuration.IsEnabled != true)
{
// Remove script if present
if (html.Contains("seasonals.js", StringComparison.Ordinal))
{
return html.Replace(scriptTag, "", StringComparison.OrdinalIgnoreCase)
.Replace(legacyScriptTag, "", StringComparison.OrdinalIgnoreCase);
}
return html;
}
if (!html.Contains("seasonals.js", StringComparison.Ordinal))
{
var inject = $"{scriptTag}\n<body";
return html.Replace("<body", inject, StringComparison.OrdinalIgnoreCase);
}
return html;
}
catch
{
// On error, return original content to avoid breaking the UI
return originalContents;
}
}
private void TryRegisterFallback(ILogger logger)
{
try
{
// Find the FileTransformation assembly across all load contexts
var assembly = AssemblyLoadContext.All
.SelectMany(x => x.Assemblies)
.FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false);
if (assembly == null)
{
logger.LogWarning("FileTransformation plugin not found. Fallback injection skipped.");
return;
}
var type = assembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (type == null)
{
logger.LogWarning("Jellyfin.Plugin.FileTransformation.PluginInterface not found.");
return;
}
var method = type.GetMethod("RegisterTransformation");
if (method == null)
{
logger.LogWarning("RegisterTransformation method not found.");
return;
}
// Create JObject payload directly using Newtonsoft.Json
var payload = new JObject
{
{ "id", Id.ToString() },
{ "fileNamePattern", "index.html" },
{ "callbackAssembly", this.GetType().Assembly.FullName },
{ "callbackClass", this.GetType().FullName },
{ "callbackMethod", nameof(TransformIndexHtml) }
};
// Invoke RegisterTransformation with the JObject payload
method.Invoke(null, new object[] { payload });
logger.LogInformation("Successfully registered fallback transformation via FileTransformation plugin.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to register fallback transformation.");
}
}
private void TryRemoveFallback(ILogger logger)
{
try
{
var assembly = AssemblyLoadContext.All
.SelectMany(x => x.Assemblies)
.FirstOrDefault(x => x.FullName?.Contains(".FileTransformation") ?? false);
if (assembly == null)
{
logger.LogWarning("FileTransformation plugin not found. Fallback removal skipped.");
return;
}
var type = assembly.GetType("Jellyfin.Plugin.FileTransformation.PluginInterface");
if (type == null)
{
logger.LogWarning("Jellyfin.Plugin.FileTransformation.PluginInterface not found.");
return;
}
var method = type.GetMethod("RemoveTransformation");
if (method == null)
{
logger.LogWarning("RemoveTransformation method not found.");
return;
}
Guid pluginId = Id is Guid g ? g : Guid.Parse(Id.ToString());
method.Invoke(null, new object[] { pluginId });
logger.LogInformation("Successfully unregistered fallback transformation via FileTransformation plugin.");
}
catch (Exception ex)
{
logger.LogError(ex, "Error attempting to unregister fallback transformation.");
}
}
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<PluginPageInfo> GetPages() public IEnumerable<PluginPageInfo> GetPages()

View File

@@ -42,6 +42,11 @@ function toggleFirework() {
} }
} else { } else {
fireworksContainer.style.display = 'block'; // show fireworks fireworksContainer.style.display = 'block'; // show fireworks
if (scrollFireworks) {
fireworksContainer.style.height = `${document.documentElement.scrollHeight}px`;
}
if (msgPrinted) { if (msgPrinted) {
console.log('Fireworks visible'); console.log('Fireworks visible');
startFireworks(); startFireworks();
@@ -117,8 +122,8 @@ function launchFirework() {
let startY, endY; let startY, endY;
if (scrollFireworks) { if (scrollFireworks) {
// Y-position considers scrolling // Y-position considers scrolling
startY = window.scrollY + window.innerHeight; // Bottom edge of the window plus the scroll offset startY = window.scrollY + window.innerHeight;
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2 + window.scrollY; // Area around the middle, but also with scrolling endY = window.scrollY + (Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2);
} else { } else {
startY = window.innerHeight; // Bottom edge of the window startY = window.innerHeight; // Bottom edge of the window
endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2; // Area around the middle endY = Math.random() * window.innerHeight * 0.5 + window.innerHeight * 0.2; // Area around the middle
@@ -135,15 +140,27 @@ function launchFirework() {
// Start the firework routine // Start the firework routine
function startFireworks() { function startFireworks() {
const fireworkContainer = document.querySelector('.fireworks') || document.createElement("div"); let fireworkContainer = document.querySelector('.fireworks');
if (!document.querySelector('.fireworks')) { if (!fireworkContainer) {
fireworkContainer = document.createElement("div");
fireworkContainer.className = "fireworks"; fireworkContainer.className = "fireworks";
fireworkContainer.setAttribute("aria-hidden", "true"); fireworkContainer.setAttribute("aria-hidden", "true");
fireworkContainer.style.position = scrollFireworks ? 'absolute' : 'fixed';
document.body.appendChild(fireworkContainer); document.body.appendChild(fireworkContainer);
} }
fireworkContainer.style.position = scrollFireworks ? 'absolute' : 'fixed';
if (scrollFireworks) {
fireworkContainer.style.height = `${document.documentElement.scrollHeight}px`;
fireworkContainer.style.width = '100%';
fireworkContainer.style.top = '0';
fireworkContainer.style.left = '0';
} else {
fireworkContainer.style.height = '100%';
fireworkContainer.style.width = '100%';
}
fireworksInterval = setInterval(() => { fireworksInterval = setInterval(() => {
const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks; const randomCount = Math.floor(Math.random() * maxFireworks) + minFireworks;
for (let i = 0; i < randomCount; i++) { for (let i = 0; i < randomCount; i++) {

View File

@@ -60,6 +60,7 @@ observer.observe(document.body, {
attributes: true // observe changes to attributes (e.g. class changes) attributes: true // observe changes to attributes (e.g. class changes)
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -78,8 +79,12 @@ function initializeCanvas() {
container.appendChild(canvas); container.appendChild(canvas);
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
// Initial resize
resizeCanvas(container); resizeCanvas(container);
window.addEventListener('resize', () => resizeCanvas(container));
// Initialize ResizeObserver
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
resizeObserver.observe(container);
} }
function removeCanvas() { function removeCanvas() {
@@ -96,15 +101,37 @@ function removeCanvas() {
animationFrameIdSanta = null; animationFrameIdSanta = null;
console.log('Santa animation frame canceled'); console.log('Santa animation frame canceled');
} }
// Disconnect ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
console.log('Canvas removed'); console.log('Canvas removed');
} }
} }
function resizeCanvas(container) { function resizeCanvas(container) {
if (!canvas) return; if (!canvas) return;
const oldWidth = canvas.width;
const oldHeight = canvas.height;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
canvas.width = rect.width; canvas.width = rect.width;
canvas.height = rect.height; canvas.height = rect.height;
// Scale snowflakes positions if dimensions changed (to avoid clustering)
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
const scaleX = canvas.width / oldWidth;
const scaleY = canvas.height / oldHeight;
snowflakes.forEach(flake => {
flake.x *= scaleX;
flake.y *= scaleY;
});
}
} }
function createSnowflakes(container) { function createSnowflakes(container) {

View File

@@ -99,12 +99,21 @@ function determineCurrentTheme() {
return 'none'; // Fallback (nothing) return 'none'; // Fallback (nothing)
} }
// helper to resolve paths for local testing vs production
function resolvePath(path) {
if (window.location.protocol === 'file:' || window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1') {
return path.replace('/Seasonals/Resources/', './');
}
return path;
}
// load theme csss // load theme csss
function loadThemeCSS(cssPath) { function loadThemeCSS(cssPath) {
if (!cssPath) return; if (!cssPath) return;
const link = document.createElement('link'); const link = document.createElement('link');
link.rel = 'stylesheet'; link.rel = 'stylesheet';
// link.href = resolvePath(cssPath);
link.href = cssPath; link.href = cssPath;
link.onerror = () => { link.onerror = () => {
@@ -121,6 +130,7 @@ function loadThemeJS(jsPath) {
const script = document.createElement('script'); const script = document.createElement('script');
script.src = jsPath; script.src = jsPath;
// script.src = resolvePath(jsPath);
script.defer = true; script.defer = true;
script.onerror = () => { script.onerror = () => {
@@ -145,14 +155,24 @@ function updateThemeContainer(containerClass) {
console.log(`Seasonals-Container class updated to "${containerClass}".`); console.log(`Seasonals-Container class updated to "${containerClass}".`);
} }
function removeSelf() { // function removeSelf() {
const script = document.currentScript; // const script = document.currentScript;
if (script) script.parentNode.removeChild(script); // if (script) script.parentNode.removeChild(script);
console.log('External script removed:', script); // console.log('External script removed:', script);
} // }
// initialize theme // initialize theme
async function initializeTheme() { async function initializeTheme() {
// Check local user preference
const isEnabled = getSavedSetting('seasonals-enabled', 'true') === 'true';
if (!isEnabled) {
console.log('Seasonals disabled by user preference.');
return;
}
const forcedTheme = getSavedSetting('seasonals-theme', 'auto');
let automateThemeSelection = true; let automateThemeSelection = true;
let defaultTheme = 'none'; let defaultTheme = 'none';
@@ -172,7 +192,11 @@ async function initializeTheme() {
} }
let currentTheme; let currentTheme;
if (automateThemeSelection === false) {
if (forcedTheme !== 'auto') {
currentTheme = forcedTheme;
console.log(`User forced theme: ${currentTheme}`);
} else if (automateThemeSelection === false) {
currentTheme = defaultTheme; currentTheme = defaultTheme;
} else { } else {
currentTheme = determineCurrentTheme(); currentTheme = determineCurrentTheme();
@@ -182,7 +206,6 @@ async function initializeTheme() {
if (!currentTheme || currentTheme === 'none') { if (!currentTheme || currentTheme === 'none') {
console.log('No theme selected.'); console.log('No theme selected.');
removeSelf();
return; return;
} }
@@ -198,9 +221,146 @@ async function initializeTheme() {
if (theme.js) loadThemeJS(theme.js); if (theme.js) loadThemeJS(theme.js);
console.log(`Theme "${currentTheme}" loaded.`); console.log(`Theme "${currentTheme}" loaded.`);
removeSelf();
} }
initializeTheme(); initializeTheme();
// User UI Seasonal Settings
function getSavedSetting(key, defaultValue) {
const value = localStorage.getItem(key);
return value !== null ? value : defaultValue;
}
function setSavedSetting(key, value) {
localStorage.setItem(key, value);
}
function createSettingsIcon() {
const button = document.createElement('button');
button.type = 'button';
button.className = 'paper-icon-button-light headerButton seasonal-settings-button';
button.title = 'Seasonal Settings';
button.innerHTML = '<span class="material-icons">ac_unit</span>';
button.style.verticalAlign = 'middle';
button.addEventListener('click', (e) => {
e.stopPropagation();
toggleSettingsPopup(button);
});
return button;
}
function createSettingsPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) existing.remove();
const popup = document.createElement('div');
popup.className = 'seasonal-settings-popup dialog';
Object.assign(popup.style, {
position: 'fixed',
zIndex: '10000',
backgroundColor: '#202020',
padding: '1.5em',
borderRadius: '1em',
boxShadow: '0 0 20px rgba(0,0,0,0.5)',
minWidth: '250px',
color: '#fff',
maxWidth: '300px'
});
const rect = anchorElement.getBoundingClientRect();
popup.style.top = `${rect.bottom + 10}px`;
popup.style.right = `${window.innerWidth - rect.right}px`;
popup.innerHTML = `
<div class="checkboxContainer checkboxContainer-withDescription" style="margin-bottom: 1.5em;">
<label class="emby-checkbox-label">
<input id="seasonal-enable-toggle" type="checkbox" is="emby-checkbox" class="emby-checkbox" />
<span class="checkboxLabel">Enable Seasonals</span>
</label>
</div>
<div class="selectContainer">
<label class="selectLabel" for="seasonal-theme-select" style="margin-bottom: 0.5em; display: block; color: inherit;">Force Theme</label>
<select is="emby-select" id="seasonal-theme-select" class="emby-select-withcolor emby-select" style="width: 100%; padding: 0.5em; background-color: rgba(0,0,0,0.2); border: 1px solid rgba(255,255,255,0.1); color: inherit; border-radius: 4px;">
<option value="auto">Auto (Date Based)</option>
</select>
</div>
`;
// Populate Select Options
const themeSelect = popup.querySelector('#seasonal-theme-select');
Object.keys(themeConfigs).forEach(key => {
if (key === 'none') return;
const option = document.createElement('option');
option.value = key;
// Capitalize first letter
option.textContent = key.charAt(0).toUpperCase() + key.slice(1);
themeSelect.appendChild(option);
});
// Set Initial Values
const enabledCheckbox = popup.querySelector('#seasonal-enable-toggle');
enabledCheckbox.checked = getSavedSetting('seasonals-enabled', 'true') === 'true';
themeSelect.value = getSavedSetting('seasonals-theme', 'auto');
// Event Listeners
enabledCheckbox.addEventListener('change', (e) => {
setSavedSetting('seasonals-enabled', e.target.checked);
location.reload();
});
themeSelect.addEventListener('change', (e) => {
setSavedSetting('seasonals-theme', e.target.value);
location.reload();
});
// Close on click outside
const closeHandler = (e) => {
if (!popup.contains(e.target) && e.target !== anchorElement && !anchorElement.contains(e.target)) {
popup.remove();
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
document.body.appendChild(popup);
}
function toggleSettingsPopup(anchorElement) {
const existing = document.querySelector('.seasonal-settings-popup');
if (existing) {
existing.remove();
} else {
createSettingsPopup(anchorElement);
}
}
function injectSettingsIcon() {
const observer = new MutationObserver((mutations, obs) => {
// Check if admin has enabled this feature
if (window.SeasonalsPluginConfig && window.SeasonalsPluginConfig.EnableClientSideToggle === false) {
return;
}
const headerRight = document.querySelector('.headerRight');
if (headerRight && !document.querySelector('.seasonal-settings-button')) {
const icon = createSettingsIcon();
headerRight.prepend(icon);
// obs.disconnect();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
injectSettingsIcon();

View File

@@ -55,6 +55,7 @@ observer.observe(document.body, {
attributes: true // observe changes to attributes (e.g. class changes) attributes: true // observe changes to attributes (e.g. class changes)
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -73,8 +74,12 @@ function initializeCanvas() {
container.appendChild(canvas); container.appendChild(canvas);
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
// Initial resize
resizeCanvas(container); resizeCanvas(container);
window.addEventListener('resize', () => resizeCanvas(container));
// Initialize ResizeObserver
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
resizeObserver.observe(container);
} }
function removeCanvas() { function removeCanvas() {
@@ -86,15 +91,37 @@ function removeCanvas() {
animationFrameId = null; animationFrameId = null;
console.log('Animation frame canceled'); console.log('Animation frame canceled');
} }
// Disconnect ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
console.log('Canvas removed'); console.log('Canvas removed');
} }
} }
function resizeCanvas(container) { function resizeCanvas(container) {
if (!canvas) return; if (!canvas) return;
const oldWidth = canvas.width;
const oldHeight = canvas.height;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
canvas.width = rect.width; canvas.width = rect.width;
canvas.height = rect.height; canvas.height = rect.height;
// Scale snowflakes positions if dimensions changed (to avoid clustering)
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
const scaleX = canvas.width / oldWidth;
const scaleY = canvas.height / oldHeight;
snowflakes.forEach(flake => {
flake.x *= scaleX;
flake.y *= scaleY;
});
}
} }
function createSnowflakes(container) { function createSnowflakes(container) {

View File

@@ -57,6 +57,7 @@ observer.observe(document.body, {
attributes: true // observe changes to attributes (e.g. class changes) attributes: true // observe changes to attributes (e.g. class changes)
}); });
let resizeObserver; // Observer for resize events
function initializeCanvas() { function initializeCanvas() {
if (document.getElementById('snowfallCanvas')) { if (document.getElementById('snowfallCanvas')) {
@@ -75,8 +76,12 @@ function initializeCanvas() {
container.appendChild(canvas); container.appendChild(canvas);
ctx = canvas.getContext('2d'); ctx = canvas.getContext('2d');
// Initial resize
resizeCanvas(container); resizeCanvas(container);
window.addEventListener('resize', () => resizeCanvas(container));
// Initialize ResizeObserver
resizeObserver = new ResizeObserver(() => resizeCanvas(container));
resizeObserver.observe(container);
} }
function removeCanvas() { function removeCanvas() {
@@ -88,15 +93,37 @@ function removeCanvas() {
animationFrameId = null; animationFrameId = null;
console.log('Animation frame canceled'); console.log('Animation frame canceled');
} }
// Disconnect ResizeObserver
if (resizeObserver) {
resizeObserver.disconnect();
resizeObserver = null;
}
console.log('Canvas removed'); console.log('Canvas removed');
} }
} }
function resizeCanvas(container) { function resizeCanvas(container) {
if (!canvas) return; if (!canvas) return;
const oldWidth = canvas.width;
const oldHeight = canvas.height;
const rect = container.getBoundingClientRect(); const rect = container.getBoundingClientRect();
canvas.width = rect.width; canvas.width = rect.width;
canvas.height = rect.height; canvas.height = rect.height;
// Scale snowflakes positions if dimensions changed (to avoid clustering)
if (oldWidth > 0 && oldHeight > 0 && snowflakes.length > 0) {
const scaleX = canvas.width / oldWidth;
const scaleY = canvas.height / oldHeight;
snowflakes.forEach(flake => {
flake.x *= scaleX;
flake.y *= scaleY;
});
}
} }
function createSnowflakes(container) { function createSnowflakes(container) {

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Seasonals Test Display</title>
<link rel="stylesheet" href="snowfall.css">
<style>
body {
background-color: black;
color: white;
margin: 0;
font-family: sans-serif;
}
/* Mock Jellyfin Header */
.skinHeader {
background-color: #101010;
height: 60px;
display: flex;
align-items: center;
padding: 0 1em;
border-bottom: 1px solid #333;
}
.headerRight {
margin-left: auto;
display: flex;
align-items: center;
}
.paper-icon-button-light {
background: none;
border: none;
color: white;
cursor: pointer;
padding: 10px;
}
.material-icons {
font-family: 'Material Icons';
/* Placeholder if not loaded */
font-weight: normal;
font-style: normal;
font-size: 24px;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
</style>
<!-- Load Material Icons for the snowflake -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<!-- Mock Header Structure -->
<div class="skinHeader">
<div class="skinHeader-content">
<span>Jellyfin Mock Header</span>
</div>
<div class="headerRight">
<button class="paper-icon-button-light">
<span class="material-icons">person</span>
</button>
</div>
</div>
<div class="seasonals-container"></div>
<!-- Load the main script -->
<script src="seasonals.js"></script>
</body>
</html>

View File

@@ -1,624 +0,0 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v9.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v9.0": {
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
"dependencies": {
"Jellyfin.Controller": "10.11.0",
"Jellyfin.Model": "10.11.0"
},
"runtime": {
"Jellyfin.Plugin.Seasonals.dll": {}
}
},
"BitFaster.Caching/2.5.4": {
"runtime": {
"lib/net6.0/BitFaster.Caching.dll": {
"assemblyVersion": "2.5.4.0",
"fileVersion": "2.5.4.0"
}
}
},
"Diacritics/4.0.17": {
"runtime": {
"lib/net9.0/Diacritics.dll": {
"assemblyVersion": "4.0.17.0",
"fileVersion": "4.0.17.0"
}
}
},
"ICU4N/60.1.0-alpha.356": {
"dependencies": {
"J2N": "2.0.0",
"Microsoft.Extensions.Caching.Memory": "9.0.10"
},
"runtime": {
"lib/netstandard2.0/ICU4N.dll": {
"assemblyVersion": "60.0.0.0",
"fileVersion": "60.1.0.0"
}
}
},
"ICU4N.Transliterator/60.1.0-alpha.356": {
"dependencies": {
"ICU4N": "60.1.0-alpha.356"
},
"runtime": {
"lib/netstandard2.0/ICU4N.Transliterator.dll": {
"assemblyVersion": "60.0.0.0",
"fileVersion": "60.1.0.0"
}
}
},
"J2N/2.0.0": {
"runtime": {
"lib/net6.0/J2N.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.0.0"
}
}
},
"Jellyfin.Common/10.11.0": {
"dependencies": {
"Jellyfin.Model": "10.11.0",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/MediaBrowser.Common.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Controller/10.11.0": {
"dependencies": {
"BitFaster.Caching": "2.5.4",
"Jellyfin.Common": "10.11.0",
"Jellyfin.MediaEncoding.Keyframes": "10.11.0",
"Jellyfin.Model": "10.11.0",
"Jellyfin.Naming": "10.11.0",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
"Microsoft.Extensions.Configuration.Binder": "9.0.10",
"System.Threading.Tasks.Dataflow": "9.0.10"
},
"runtime": {
"lib/net9.0/MediaBrowser.Controller.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Data/10.11.0": {
"dependencies": {
"Jellyfin.Database.Implementations": "10.11.0",
"Microsoft.Extensions.Logging": "9.0.10"
},
"runtime": {
"lib/net9.0/Jellyfin.Data.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Database.Implementations/10.11.0": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Relational": "9.0.10",
"Polly": "8.6.4"
},
"runtime": {
"lib/net9.0/Jellyfin.Database.Implementations.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Extensions/10.11.0": {
"dependencies": {
"Diacritics": "4.0.17",
"ICU4N.Transliterator": "60.1.0-alpha.356"
},
"runtime": {
"lib/net9.0/Jellyfin.Extensions.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"NEbml": "1.1.0.5"
},
"runtime": {
"lib/net9.0/Jellyfin.MediaEncoding.Keyframes.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Model/10.11.0": {
"dependencies": {
"Jellyfin.Data": "10.11.0",
"Jellyfin.Extensions": "10.11.0",
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"System.Globalization": "4.3.0",
"System.Text.Json": "9.0.10"
},
"runtime": {
"lib/net9.0/MediaBrowser.Model.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Jellyfin.Naming/10.11.0": {
"dependencies": {
"Jellyfin.Common": "10.11.0",
"Jellyfin.Model": "10.11.0"
},
"runtime": {
"lib/net9.0/Emby.Naming.dll": {
"assemblyVersion": "10.11.0.0",
"fileVersion": "10.11.0.0"
}
}
},
"Microsoft.EntityFrameworkCore/9.0.10": {
"dependencies": {
"Microsoft.EntityFrameworkCore.Abstractions": "9.0.10",
"Microsoft.EntityFrameworkCore.Analyzers": "9.0.10",
"Microsoft.Extensions.Caching.Memory": "9.0.10",
"Microsoft.Extensions.Logging": "9.0.10"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.dll": {
"assemblyVersion": "9.0.10.0",
"fileVersion": "9.0.1025.47514"
}
}
},
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Abstractions.dll": {
"assemblyVersion": "9.0.10.0",
"fileVersion": "9.0.1025.47514"
}
}
},
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {},
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
"dependencies": {
"Microsoft.EntityFrameworkCore": "9.0.10",
"Microsoft.Extensions.Caching.Memory": "9.0.10",
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10",
"Microsoft.Extensions.Logging": "9.0.10"
},
"runtime": {
"lib/net8.0/Microsoft.EntityFrameworkCore.Relational.dll": {
"assemblyVersion": "9.0.10.0",
"fileVersion": "9.0.1025.47514"
}
}
},
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Caching.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Caching.Memory/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Caching.Abstractions": "9.0.10",
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"Microsoft.Extensions.Options": "9.0.10",
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Caching.Memory.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Configuration.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Configuration.Binder.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.DependencyInjection/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.DependencyInjection.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
"runtime": {
"lib/net9.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Logging/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "9.0.10",
"Microsoft.Extensions.Logging.Abstractions": "9.0.10",
"Microsoft.Extensions.Options": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Logging.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Logging.Abstractions.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Options/9.0.10": {
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.10",
"Microsoft.Extensions.Primitives": "9.0.10"
},
"runtime": {
"lib/net9.0/Microsoft.Extensions.Options.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.Extensions.Primitives/9.0.10": {
"runtime": {
"lib/net9.0/Microsoft.Extensions.Primitives.dll": {
"assemblyVersion": "9.0.0.0",
"fileVersion": "9.0.1025.47515"
}
}
},
"Microsoft.NETCore.Platforms/1.1.0": {},
"Microsoft.NETCore.Targets/1.1.0": {},
"NEbml/1.1.0.5": {
"runtime": {
"lib/netstandard2.0/NEbml.Core.dll": {
"assemblyVersion": "1.1.0.5",
"fileVersion": "1.1.0.5"
}
}
},
"Polly/8.6.4": {
"dependencies": {
"Polly.Core": "8.6.4"
},
"runtime": {
"lib/net6.0/Polly.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.6.4.5033"
}
}
},
"Polly.Core/8.6.4": {
"runtime": {
"lib/net8.0/Polly.Core.dll": {
"assemblyVersion": "8.0.0.0",
"fileVersion": "8.6.4.5033"
}
}
},
"System.Globalization/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Text.Json/9.0.10": {},
"System.Threading.Tasks.Dataflow/9.0.10": {}
}
},
"libraries": {
"Jellyfin.Plugin.Seasonals/1.0.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"BitFaster.Caching/2.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1QroTY1PVCZOSG9FnkkCrmCKk/+bZCgI/YXq376HnYwUDJ4Ho0EaV4YaA/5v5WYLnwIwIO7RZkdWbg9pxIpueQ==",
"path": "bitfaster.caching/2.5.4",
"hashPath": "bitfaster.caching.2.5.4.nupkg.sha512"
},
"Diacritics/4.0.17": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FmMvVQRsfon+x5P+dxz4mvV8wt45xr25EAOCkuo/Cjtc7lVYV5cZUSsNXwmKQpwO+TokIHpzxb8ENpqrm4yBlQ==",
"path": "diacritics/4.0.17",
"hashPath": "diacritics.4.0.17.nupkg.sha512"
},
"ICU4N/60.1.0-alpha.356": {
"type": "package",
"serviceable": true,
"sha512": "sha512-YMZtDnjcqWzziOKiE7w6Ma7Rl5vuFDxzOsUlHh1QyfghbNEIZQOLRs9MMfwCWAjX6n9UitrF6vLXy55Z5q+4Fg==",
"path": "icu4n/60.1.0-alpha.356",
"hashPath": "icu4n.60.1.0-alpha.356.nupkg.sha512"
},
"ICU4N.Transliterator/60.1.0-alpha.356": {
"type": "package",
"serviceable": true,
"sha512": "sha512-lFOSO6bbEtB6HkWMNDJAq+rFwVyi9g6xVc5O/2xHa6iZnV7wLVDqCbaQ4W4vIeBSQZAafqhxciaEkmAvSdzlCg==",
"path": "icu4n.transliterator/60.1.0-alpha.356",
"hashPath": "icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512"
},
"J2N/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-M5bwDajAARZiyqupU+rHQJnsVLxNBOHJ8vKYHd8LcLIb1FgLfzzcJvc31Qo5Xz/GEHFjDF9ScjKL/ks/zRTXuA==",
"path": "j2n/2.0.0",
"hashPath": "j2n.2.0.0.nupkg.sha512"
},
"Jellyfin.Common/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-TitN7+qWFt2l0V5b+KTRt7ABDCvfZdvSC6qBG1uHS18Y80xrbrSCJ9O6BH/of310h6a4lxKlQjUtTPHCzeG2AA==",
"path": "jellyfin.common/10.11.0",
"hashPath": "jellyfin.common.10.11.0.nupkg.sha512"
},
"Jellyfin.Controller/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WV+PQy9AHdZLvYqUsNq6ZyQoxaiaEWLz0EwZGOiu8xSrepQLFope2U1VFHVCNbARwesg7s/B+9uB03eXDsQw9w==",
"path": "jellyfin.controller/10.11.0",
"hashPath": "jellyfin.controller.10.11.0.nupkg.sha512"
},
"Jellyfin.Data/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-YEz7/85b98Rj14IJJIVqmzJsi69LDOKo4Ox+VHbh1vj3tkWomSPayzvG3kyU8I0yFMrd6+Ta55C20kZ2XC7vQg==",
"path": "jellyfin.data/10.11.0",
"hashPath": "jellyfin.data.10.11.0.nupkg.sha512"
},
"Jellyfin.Database.Implementations/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-oLblVZzqF9zuLMdfqp8pbusSVQq6b40/RcHjGF1hxYozVNEi+UhiDX8aJipYBOrh33FFAofoQq468BvZixpPcw==",
"path": "jellyfin.database.implementations/10.11.0",
"hashPath": "jellyfin.database.implementations.10.11.0.nupkg.sha512"
},
"Jellyfin.Extensions/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1ufj+Rm0Bn6C990i2wwiT5UHPZfD65GOtJK6NcDU//DDMbuoGX1LQZxuCx+rhhRg1XdHPWzYASARYyNlFQa6cg==",
"path": "jellyfin.extensions/10.11.0",
"hashPath": "jellyfin.extensions.10.11.0.nupkg.sha512"
},
"Jellyfin.MediaEncoding.Keyframes/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/OBcg4Qj825elOGNj5bNRfABKzfAf4qNQj0/d/DwhG/+V/wsKuxS0Pc/xOEagVVjXOnqGPZz/+k8D4UvnvMoHw==",
"path": "jellyfin.mediaencoding.keyframes/10.11.0",
"hashPath": "jellyfin.mediaencoding.keyframes.10.11.0.nupkg.sha512"
},
"Jellyfin.Model/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-h+61RSXn4sk8fS6Zx9RkDyVnI5VnNbrsR2p8WcvybtNSW2pgU2uZ9pwEv2awD3ifX69weqYpQLMh91f6aidW2A==",
"path": "jellyfin.model/10.11.0",
"hashPath": "jellyfin.model.10.11.0.nupkg.sha512"
},
"Jellyfin.Naming/10.11.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2++xSbhdFSb1J3XySjC6UU+uII6OdKc0DfkYx/E1oN7mSjoftyZR8eU045kVWBwsAxr+UcMI6t2DYfES2tJkRA==",
"path": "jellyfin.naming/10.11.0",
"hashPath": "jellyfin.naming.10.11.0.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-WjjxVyOTVs85V7SUe+lZjtGOEeVzF4RO8amrqL4adgbyThNq7vGCFzPw8buZj44gHeQYD5V/uZ/6XuqG9Jq+kA==",
"path": "microsoft.entityframeworkcore/9.0.10",
"hashPath": "microsoft.entityframeworkcore.9.0.10.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-I3TWAs5Lbzmzu8S0T6qXhzBiO3CznYLrfE59W0npkqNHfWGH8CgA66LrHMWxWOXVTD4145QwYqiWNCdLwpJ1Ew==",
"path": "microsoft.entityframeworkcore.abstractions/9.0.10",
"hashPath": "microsoft.entityframeworkcore.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Analyzers/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-mXNl0Gg3l3zGrClLCHepB+b7rYVuFfB9qQJwya0dUSHFuR1T0jMD8KxuNVyhQSfoWIepanhzjcG8TUNGXOcU0Q==",
"path": "microsoft.entityframeworkcore.analyzers/9.0.10",
"hashPath": "microsoft.entityframeworkcore.analyzers.9.0.10.nupkg.sha512"
},
"Microsoft.EntityFrameworkCore.Relational/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-IJNrG5vdmFUvHR8FLLFg9AWpuE8qW1DTEN+fNLGbNTu6cnpZzzqU6+aknAGtTSAEVWosJ3BZ3TOO9wpifUvv3A==",
"path": "microsoft.entityframeworkcore.relational/9.0.10",
"hashPath": "microsoft.entityframeworkcore.relational.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-cL6iTxgJ4u5zP3eFOdBiDAtmE/B2WKTRhyJfEne7n6qvHpsMwwIDxljs210mWSO1ucBy7lR1Lo7/7kjeZeLcqQ==",
"path": "microsoft.extensions.caching.abstractions/9.0.10",
"hashPath": "microsoft.extensions.caching.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Caching.Memory/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-2iuzwIoCoqZJfH2VLk1xvlQS4PQDEuhj4dWiGb+Qpt1vHFHyffp497cTO6ucsV54W/h4JmM1vzDBv8pVAFazZg==",
"path": "microsoft.extensions.caching.memory/9.0.10",
"hashPath": "microsoft.extensions.caching.memory.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ad3JxmFj0uxuFa1CT6oxTCC1lQ0xeRuOvzBRFT/I/ofIXVOnNsH/v2GZkAJWhlpZqKUvSexQZzp3EEAB2CdtJg==",
"path": "microsoft.extensions.configuration.abstractions/9.0.10",
"hashPath": "microsoft.extensions.configuration.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Configuration.Binder/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-D6Kng+9I+w1SQPxJybc6wzw9nnnyUQPutycjtI0svv1RHaWOpUk9PPlwIRfhhoQZ3yihejkEI2wNv/7VnVtkGA==",
"path": "microsoft.extensions.configuration.binder/9.0.10",
"hashPath": "microsoft.extensions.configuration.binder.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-iEtXCkNd5XhjNJAOb/wO4IhDRdLIE2CsPxZggZQWJ/q2+sa8dmEPC393nnsiqdH8/4KV8Xn25IzgKPR1UEQ0og==",
"path": "microsoft.extensions.dependencyinjection/9.0.10",
"hashPath": "microsoft.extensions.dependencyinjection.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.DependencyInjection.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-r9waLiOPe9ZF1PvzUT+RDoHvpMmY8MW+lb4lqjYGObwKpnyPMLI3odVvlmshwuZcdoHynsGWOrCPA0hxZ63lIA==",
"path": "microsoft.extensions.dependencyinjection.abstractions/9.0.10",
"hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Logging/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UBXHqE9vyptVhaFnT1R7YJKCve7TqVI10yjjUZBNGMlW2lZ4c031Slt9hxsOzWCzlpPxxIFyf1Yk4a6Iubxx7w==",
"path": "microsoft.extensions.logging/9.0.10",
"hashPath": "microsoft.extensions.logging.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Logging.Abstractions/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-MFUPv/nN1rAQ19w43smm6bbf0JDYN/1HEPHoiMYY50pvDMFpglzWAuoTavByDmZq7UuhjaxwrET3joU69ZHoHQ==",
"path": "microsoft.extensions.logging.abstractions/9.0.10",
"hashPath": "microsoft.extensions.logging.abstractions.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Options/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zMNABt8eBv0B0XrWjFy9nZNgddavaOeq3ZdaD5IlHhRH65MrU7HM+Hd8GjWE3e2VDGFPZFfSAc6XVXC17f9fOA==",
"path": "microsoft.extensions.options/9.0.10",
"hashPath": "microsoft.extensions.options.9.0.10.nupkg.sha512"
},
"Microsoft.Extensions.Primitives/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3pl8D1O5ZwMpDkZAT2uXrhQ6NipkwEgDLMFuURiHTf72TvkoMP61QYH3Vk1yrzVHnHBdNZk3cQACz8Zc7YGNhQ==",
"path": "microsoft.extensions.primitives/9.0.10",
"hashPath": "microsoft.extensions.primitives.9.0.10.nupkg.sha512"
},
"Microsoft.NETCore.Platforms/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==",
"path": "microsoft.netcore.platforms/1.1.0",
"hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512"
},
"Microsoft.NETCore.Targets/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
"path": "microsoft.netcore.targets/1.1.0",
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
},
"NEbml/1.1.0.5": {
"type": "package",
"serviceable": true,
"sha512": "sha512-svtqDc+hue9kbnqNN2KkK4om/hDrc7K127cNb5FIYfgKgzo+JNDPXNLp8NioCchHhBO3lxWd4Cp/iiZZ3aoUqg==",
"path": "nebml/1.1.0.5",
"hashPath": "nebml.1.1.0.5.nupkg.sha512"
},
"Polly/8.6.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-uuBsDoBw0oYrMe3uTWRjkT2sIkKh+ZZnnDrLb4Z+QANfeA4+7FJacx6E8CY5GAxXRoSgFrvUADEAQ7DPF6fGiw==",
"path": "polly/8.6.4",
"hashPath": "polly.8.6.4.nupkg.sha512"
},
"Polly.Core/8.6.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-4AWqYnQ2TME0E+Mzovt1Uu+VyvpR84ymUldMcPw7Mbj799Phaag14CKrMtlJGx5jsvYP+S3oR1QmysgmXoD5cw==",
"path": "polly.core/8.6.4",
"hashPath": "polly.core.8.6.4.nupkg.sha512"
},
"System.Globalization/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
"path": "system.globalization/4.3.0",
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
},
"System.Runtime/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"path": "system.runtime/4.3.0",
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
},
"System.Text.Json/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-XM02ZBnzxk7Ti6l9YRy8Bp639wANqJzJzw4W4VYiCh+HXY9hBOWkGB4k89OLP/s/RxgM02P4a/mWcJTgFiLf1Q==",
"path": "system.text.json/9.0.10",
"hashPath": "system.text.json.9.0.10.nupkg.sha512"
},
"System.Threading.Tasks.Dataflow/9.0.10": {
"type": "package",
"serviceable": true,
"sha512": "sha512-k1o6v6V3+4mznSnPnO0FBaRjiAPL1ouKPfPQH7hO9Z2SwJHt8E45F4wX5yQh1aeja1JHPYEungQedXibng654Q==",
"path": "system.threading.tasks.dataflow/9.0.10",
"hashPath": "system.threading.tasks.dataflow.9.0.10.nupkg.sha512"
}
}
}

View File

@@ -18,7 +18,7 @@ This plugin is based on my manual mod (see the [legacy branch](https://github.co
- [Configuration](#configuration) - [Configuration](#configuration)
- [Automatic Theme Selection](#automatic-theme-selection) - [Automatic Theme Selection](#automatic-theme-selection)
- [Theme Settings](#theme-settings) - [Theme Settings](#theme-settings)
- [Build Process](#build-process) - [Build The Plugin By Yourself](#build-the-plugin-by-yourself)
- [Troubleshooting](#troubleshooting) - [Troubleshooting](#troubleshooting)
- [Effects Not Showing](#effects-not-showing) - [Effects Not Showing](#effects-not-showing)
- [Docker Permission Issues](#docker-permission-issues) - [Docker Permission Issues](#docker-permission-issues)
@@ -77,9 +77,9 @@ To install this plugin, you will first need to add the repository in Jellyfin.
1. Open your Jellyfin Dashboard. 1. Open your Jellyfin Dashboard.
2. Navigate to **Plugins** > **Manage Repositories**. 2. Navigate to **Plugins** > **Manage Repositories**.
3. Click the **+ New Repository** button to add a new repository. 3. Click the **+ New Repository** button to add a new repository.
4. Enter a name (e.g., "Seasonals") and paste the following URL into the 'Repository URL' field: 4. Enter a name for the repo and paste the following URL into the 'Repository URL' field:
```bash ```bash
https://raw.githubusercontent.com/CodeDevMLH/Jellyfin-Seasonals/refs/heads/main/manifest.json https://raw.githubusercontent.com/CodeDevMLH/jellyfin-plugin-manifest/refs/heads/main/manifest.json
``` ```
5. Click **Add**. 5. Click **Add**.
6. Go to the **Available** tab at the top. 6. Go to the **Available** tab at the top.
@@ -135,7 +135,7 @@ If automatic selection is enabled, the following themes are applied based on the
## Theme Settings ## Theme Settings
Each theme contains additional settings to customize its behavior. Expand the advanced configuration section to configure each theme, adjust parameters like particle count, animation speed etc. Each theme contains additional settings to customize its behavior. Expand the advanced configuration section to configure each theme, adjust parameters like particle count, animation speed etc.
## Build Process ## Build The Plugin By Yourself
If you want to build the plugin yourself: If you want to build the plugin yourself:

View File

@@ -9,10 +9,7 @@ Bevor du baust, musst du die Versionsnummer in den folgenden Dateien aktualisier
1. **`Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj`** 1. **`Jellyfin.Plugin.Seasonals/Jellyfin.Plugin.Seasonals.csproj`**
Suche nach `<Version>...</Version>` und ändere die Nummer. Suche nach `<Version>...</Version>` und ändere die Nummer.
2. **`build.yaml`** 2. **`manifest.json`**
Ändere den Wert bei `version: "..."`.
3. **`manifest.json`**
Füge einen neuen Eintrag oben in die `versions`-Liste ein (oder bearbeite den vorhandenen, wenn es noch kein Release gab). Füge einen neuen Eintrag oben in die `versions`-Liste ein (oder bearbeite den vorhandenen, wenn es noch kein Release gab).
* `version`: Deine neue Nummer. * `version`: Deine neue Nummer.
* `changelog`: Was hat sich geändert? * `changelog`: Was hat sich geändert?

View File

@@ -1,15 +0,0 @@
---
name: "Seasonals"
guid: "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4"
version: "1.3.4.0"
targetAbi: "10.11.0.0"
framework: "net9.0"
overview: "Seasonal effects for Jellyfin"
description: >
Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.
category: "General"
owner: "CodeDevMLH"
artifacts:
- "Jellyfin.Plugin.Seasonals.dll"
changelog: >
Added Advanced Configuration UI for customizing individual seasonal effects.

106
git_cheat_sheet.md Normal file
View File

@@ -0,0 +1,106 @@
# 🚀 Git Cheat Sheet: Branches, Merge, Rebase & VS Code
## 🌳 Branches (Zweige)
* **`main`** → Stabiler Stand, Production-ready (Deployment).
* **`dev`** → Aktive Entwicklung, Sammelbecken für Features.
---
## 🛠 Zusammenführen von Änderungen
### Git Merge
Führt zwei Zweige zusammen. Git sucht den letzten gemeinsamen Basispunkt und erstellt einen neuen **Merge-Commit**.
```bash
git checkout dev
git merge main
```
| Details | |
|-----------|--------|
| Wann? | In Team-Branches, bei bereits gepushten Branches, wenn Stabilität wichtiger als eine saubere History ist. |
| Vorteile | Sicher, einfach nachvollziehbar, schreibt die History nicht um. |
| Nachteile | Die History kann bei vielen Merges unübersichtlich ("Spaghetti-Graph") werden. |
### Git Rebase
Setzt deine Commits neu auf die Spitze eines anderen Branches. Die Commit-IDs werden dabei neu geschrieben.
```bash
git checkout dev
git rebase main
```
| Details | |
|-----------|--------|
| Wann? | In lokalen Feature-Branches (bevor sie geteilt werden), um die History sauber zu halten. |
| Vorteile | Erzeugt eine perfekt lineare, leicht lesbare History. |
| Nachteile | ⚠️ Gefährlich auf geteilten/öffentlichen Branches. Konflikte müssen ggf. für jeden einzelnen Commit gelöst werden. |
---
## 📦 Temporäres Speichern & Spezialbefehle
### Stash (Das "Regal")
Speichert uncommitted Changes temporär, um das Arbeitsverzeichnis sauber zu machen, ohne zu committen.
```bash
git stash # Änderungen "parken".
git stash pop # Änderungen zurückholen und Stash leeren.
```
**Wann?** Wenn du schnell den Branch wechseln musst, aber deine Arbeit noch nicht fertig ist.
### Cherry-pick
Kopiert einen ganz gezielten Commit von einem Branch in deinen aktuellen.
```bash
git cherry-pick <commit-hash>
```
**Wann?** Wenn ein Bugfix auf dem falschen Branch gelandet ist oder du nur eine einzige Funktion aus einem Feature-Branch brauchst.
---
## 🔄 Checkout & Switch
```bash
git checkout dev # oder git switch dev Wechselt zum Branch.
git checkout -f dev # Force Checkout: Wechselt den Branch und verwirft alle ungespeicherten lokalen Änderungen unwiderruflich! ⚠️
```
---
## 💻 VS Code Git-Optionen (UI)
VS Code bietet beim Branch-Wechsel oft drei intelligente Optionen an:
* **Migrate Changes ⭐**
* Nimmt deine aktuellen Änderungen einfach mit in den neuen Branch.
* (Intern: stash → switch → stash pop).
* **Stash & Checkout**
* Parkt deine Änderungen sicher im Stash und wechselt den Branch. Die Änderungen bleiben im Stash, bis du sie manuell wieder herausholst.
* **Force Checkout ⚠️**
* Wechselt den Branch und löscht deine aktuellen, ungespeicherten Änderungen. Nur nutzen, wenn die Arbeit weggeworfen werden kann.
---
## 🔄 Typischer Sync-Workflow
Um den Entwicklungs-Branch aktuell zu halten, nachdem dev in main gemerged wurde:
1. Auf dev entwickeln.
2. Merge dev → main für das Release.
3. Zurück auf dev wechseln:
```bash
git checkout dev
git merge main # (oder rebase), um den neuesten Stand vom Main wieder in Dev zu haben.
```
---
## 🧠 Merksätze
* **Merge** = Historien verbinden (Sicher & Dokumentiert).
* **Rebase** = Historie neu schreiben (Linear & Sauber).
* **Stash** = "Ich parke das mal kurz hier."
* **Migrate Changes** = Sicherer Branch-Wechsel mit "Gepäck".

BIN
logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

View File

@@ -2,19 +2,43 @@
{ {
"guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4", "guid": "ef1e863f-cbb0-4e47-9f23-f0cbb1826ad4",
"name": "Seasonals", "name": "Seasonals",
"description": "Adds seasonal effects like snow, leaves, etc. to the Jellyfin web interface.", "description": "Adds atmospheric, animated seasonal effects like snow, falling leaves, hearts, fireworks and more. The plugin provides a settings page for configuration. If you do not have write permissions to the web folder, please also install the file-transformation plugin.",
"overview": "Seasonal effects for Jellyfin", "overview": "Atmospheric, configurable seasonal effects for the Jellyfin web interface",
"owner": "CodeDevMLH", "owner": "CodeDevMLH",
"category": "General", "category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png", "imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/raw/branch/main/logo.png",
"versions": [ "versions": [
{
"version": "1.6.2.0",
"changelog": "- feat: Add client-side toggle option for seasonal settings",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.6.2.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "22e876cf0a20118e14f8e2a2c35bdd50",
"timestamp": "2026-02-03T18:18:34Z"
},
{
"version": "1.5.1.0",
"changelog": "- fix: snowfall effect sometimes not scaling on window resize, which leads to clustering and rain effect of snowflakes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.5.1.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "a66b5c24c1f4bfff14bba5e93576bb80",
"timestamp": "2026-01-28T22:40:49Z"
},
{
"version": "1.5.0.0",
"changelog": "- Refactor script injection logic",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.5.0.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "ba8d3c358df3e0546b99113b43f54fea",
"timestamp": "2026-01-08T23:31:52Z"
},
{ {
"version": "1.4.1.0", "version": "1.4.1.0",
"changelog": "- fix fireworks display issue on scroll", "changelog": "- fix fireworks display issue on scroll",
"targetAbi": "10.11.0.0", "targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.1.0/Jellyfin.Plugin.Seasonals.zip", "sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/Jellyfin-Seasonals-Plugin/releases/download/v1.4.1.0/Jellyfin.Plugin.Seasonals.zip",
"checksum": "13fa16ff190da6acc537690d5ee99264", "checksum": "bd8d9c6af064de011a708ea85e9b08c0",
"timestamp": "2025-12-29T22:05:02Z" "timestamp": "2026-01-06T21:12:46Z"
}, },
{ {
"version": "1.4.0.0", "version": "1.4.0.0",
@@ -73,5 +97,160 @@
"timestamp": "2025-12-15T15:33:15Z" "timestamp": "2025-12-15T15:33:15Z"
} }
] ]
},
{
"guid": "d7e11d57-819b-4bdd-a88d-53c5f5560225",
"name": "Media Bar Enhanced",
"description": "A feature-rich fork of the original Media Bar script by MakD that brings your home screen to life.\n\n-> 100% Configurable via Web UI: Manage all features, lists, and settings effortlessly through the plugin configuration page.\n\nKey Highlights:\n- Cinematic Video Backdrops: Supports local & YouTube trailers (incl. SponsorBlock)\n- Custom Content: Curate your slideshow with specific Collections, Playlists, or seasonal events\n\nAdditional Features:\n- Full-width immersive mode\n- Smart resolution handling (up to 4K)\n- Full keyboard navigation & playback control\n- Wait-for-trailer options\n- Customizable pagination & animations\n\nIf you do not have write permissions to the web folder, please also install the file-transformation plugin.",
"overview": "Transforms your Jellyfin home screen with an immersive, fully configurable media slideshow featuring video backdrops.",
"owner": "CodeDevMLH",
"category": "General",
"imageUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/raw/branch/main/logo.png",
"versions": [
{
"version": "1.2.3.7",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.\n- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 3.0.9 from original repo",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.7/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "fa1bf48cff159cc7dbf0aab48511a37c",
"timestamp": "2026-01-28T22:39:54Z"
},
{
"version": "1.2.3.6",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.6/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "da73bb490548c122906419d2762a2d00",
"timestamp": "2026-01-28T21:31:54Z"
},
{
"version": "1.2.3.5",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.5/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "b5efea79ec465522dad31e4ee5f1710c",
"timestamp": "2026-01-28T20:21:20Z"
},
{
"version": "1.2.3.4",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.4/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "4683f75e2df2590db663303bdd329ccd",
"timestamp": "2026-01-28T01:09:38Z"
},
{
"version": "1.2.3.3",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "3c18a84b2f59c86c130e91da83f980a2",
"timestamp": "2026-01-28T01:05:45Z"
},
{
"version": "1.2.3.2",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "4109a3ea10eb3145217b24ee8f8b37b5",
"timestamp": "2026-01-28T00:30:36Z"
},
{
"version": "1.2.3.1",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "e73029ac767e24d36742a27678758b6f",
"timestamp": "2026-01-28T00:17:28Z"
},
{
"version": "1.2.3.0",
"changelog": "- Fixes the issue where buttons were cut off on smaller screens such as on S24/S25.",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.3.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "ae6f34bee76f9d7873964a71ca191bf3",
"timestamp": "2026-01-27T23:54:42Z"
},
{
"version": "1.2.2.0",
"changelog": "- Fixes issues with persistent slides-container visibility",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.2.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "3362f93815845c4e85b66b31bcd0f52c",
"timestamp": "2026-01-24T22:53:55Z"
},
{
"version": "1.2.1.0",
"changelog": "- Update mediaBarEnhanced.js and mediaBarEnhanced.css with version 3.0.8 from original repo",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "70defc1fb29a17ff4c9362bf7bdc53b5",
"timestamp": "2026-01-22T23:50:56Z"
},
{
"version": "1.2.0.0",
"changelog": "- Add video quality preference setting (Auto / 1080p / Highres)\n- Set preferred video quality on YouTube player based on setting",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.2.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "0b6379f68990026240d97fe8f77fbef1",
"timestamp": "2026-01-08T23:30:58Z"
},
{
"version": "1.1.2.0",
"changelog": "- Add method to resume video playback when slideshow is active",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.2.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "a0e8ff5e59b22a1bdedc916cd5e1c16a",
"timestamp": "2026-01-08T15:26:55Z"
},
{
"version": "1.1.1.0",
"changelog": "- Add method to pause all video playback when navigating away from home screen",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.1.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "09da95fc561b11191d23a5cfa30ea731",
"timestamp": "2026-01-08T14:54:57Z"
},
{
"version": "1.1.0.0",
"changelog": "- 'custom media IDs' setting is now enabled by default (no input --> random selection)\n- improve GUID handling in slideshow manager to handle seperator and description",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.1.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "32305d72b8d704acf8eef0c22277fee9",
"timestamp": "2026-01-08T02:15:50Z"
},
{
"version": "1.0.0.3",
"changelog": "fixes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.3/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "e6180c42836069029072e96ac4860c42",
"timestamp": "2026-01-06T23:26:29Z"
},
{
"version": "1.0.0.2",
"changelog": "fixes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.2/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "1041b403ec0193c2172a6fe15501afd3",
"timestamp": "2026-01-06T21:21:37Z"
},
{
"version": "1.0.0.1",
"changelog": "fixes",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.1/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "f4e6194a9cc72fdda7436161c73832de",
"timestamp": "2026-01-06T21:18:33Z"
},
{
"version": "1.0.0.0",
"changelog": "Initial release",
"targetAbi": "10.11.0.0",
"sourceUrl": "https://git.mahom03-spacecloud.de/CodeDevMLH/jellyfin-plugin-media-bar-enhanced/releases/download/v1.0.0.0/Jellyfin.Plugin.MediaBarEnhanced.zip",
"checksum": "2ba7cc7f238f6aa7097628797935b903",
"timestamp": "2026-01-06T18:56:30Z"
}
]
} }
] ]