diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9bd8ced
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,14 @@
+# .NET build artifacts
+bin/
+obj/
+*.dll
+
+# Nix
+.result/
+result
+
+# IDE
+.vs/
+.vscode/
+*.user
+*.suo
diff --git a/deps.json b/deps.json
new file mode 100644
index 0000000..0243907
--- /dev/null
+++ b/deps.json
@@ -0,0 +1,152 @@
+[
+ {
+ "pname": "Diacritics",
+ "version": "3.3.29",
+ "hash": "sha256-sIbdJ3yMthnmJHly3WheUdYjtwPakcczTJx9ycxtgrY="
+ },
+ {
+ "pname": "ICU4N",
+ "version": "60.1.0-alpha.356",
+ "hash": "sha256-1QyOgO7pNMeoEgBtl6o8IG4o91wD2hFUgRI0jM0ltxY="
+ },
+ {
+ "pname": "ICU4N.Transliterator",
+ "version": "60.1.0-alpha.356",
+ "hash": "sha256-RLNwQNVqNz8Omfb/mC/rzQWVq8c7uCnNdG2qi4xJmds="
+ },
+ {
+ "pname": "J2N",
+ "version": "2.0.0",
+ "hash": "sha256-YvtIWErlm2O49hg3lIRm5Ha8/owkQkfMudzuldC3EhA="
+ },
+ {
+ "pname": "Jellyfin.Common",
+ "version": "10.10.7",
+ "hash": "sha256-9EIigrDheob4vRP+UBAoIPHH4fyz6Cl27GUpelEGpBg="
+ },
+ {
+ "pname": "Jellyfin.Controller",
+ "version": "10.10.7",
+ "hash": "sha256-/obWAuxWpSn+NlMES+fjyrf1g+qbxmSYQUBvgXQYltQ="
+ },
+ {
+ "pname": "Jellyfin.Data",
+ "version": "10.10.7",
+ "hash": "sha256-lRQjg/HkFAtCN0woL1gX6j5dMfVLP/fzQTKjFTcRth4="
+ },
+ {
+ "pname": "Jellyfin.Extensions",
+ "version": "10.10.7",
+ "hash": "sha256-AOGJ2IoT2v+LnlEqnb2O4FnXapcQiH0V9ny8GUysNHg="
+ },
+ {
+ "pname": "Jellyfin.Model",
+ "version": "10.10.7",
+ "hash": "sha256-ubsClGTLq/aFnMiBCaHVUX2b88beuL07Yn5VXNbTCjQ="
+ },
+ {
+ "pname": "Jellyfin.Naming",
+ "version": "10.10.7",
+ "hash": "sha256-/jlIS0X4FuBo6Ac1cjLGzgdJ3vR6VotBspxpNrqfMzo="
+ },
+ {
+ "pname": "Microsoft.Extensions.Caching.Abstractions",
+ "version": "2.0.0",
+ "hash": "sha256-Eg1MES40kzkGW9tZmjaKtbWI00Kbv7fLJQmjrigjxqk="
+ },
+ {
+ "pname": "Microsoft.Extensions.Caching.Memory",
+ "version": "2.0.0",
+ "hash": "sha256-1fnNvp62KrviVwYlqVl1CbdaZVpCDah9eCZeNDGDbWM="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration.Abstractions",
+ "version": "8.0.0",
+ "hash": "sha256-4eBpDkf7MJozTZnOwQvwcfgRKQGcNXe0K/kF+h5Rl8o="
+ },
+ {
+ "pname": "Microsoft.Extensions.Configuration.Binder",
+ "version": "8.0.2",
+ "hash": "sha256-aGB0VuoC34YadAEqrwoaXLc5qla55pswDV2xLSmR7SE="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection",
+ "version": "8.0.1",
+ "hash": "sha256-O9g0jWS+jfGoT3yqKwZYJGL+jGSIeSbwmvomKDC3hTU="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
+ "version": "2.0.0",
+ "hash": "sha256-H1rEnq/veRWvmp8qmUsrQkQIcVlKilUNzmmKsxJ0md8="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
+ "version": "8.0.0",
+ "hash": "sha256-75KzEGWjbRELczJpCiJub+ltNUMMbz5A/1KQU+5dgP8="
+ },
+ {
+ "pname": "Microsoft.Extensions.DependencyInjection.Abstractions",
+ "version": "8.0.2",
+ "hash": "sha256-UfLfEQAkXxDaVPC7foE/J3FVEXd31Pu6uQIhTic3JgY="
+ },
+ {
+ "pname": "Microsoft.Extensions.Logging",
+ "version": "8.0.1",
+ "hash": "sha256-vkfVw4tQEg86Xg18v6QO0Qb4Ysz0Njx57d1XcNuj6IU="
+ },
+ {
+ "pname": "Microsoft.Extensions.Logging.Abstractions",
+ "version": "8.0.2",
+ "hash": "sha256-cHpe8X2BgYa5DzulZfq24rg8O2K5Lmq2OiLhoyAVgJc="
+ },
+ {
+ "pname": "Microsoft.Extensions.Options",
+ "version": "2.0.0",
+ "hash": "sha256-EMvaXxGzueI8lT97bYJQr0kAj1IK0pjnAcWN82hTnzw="
+ },
+ {
+ "pname": "Microsoft.Extensions.Options",
+ "version": "8.0.2",
+ "hash": "sha256-AjcldddddtN/9aH9pg7ClEZycWtFHLi9IPe1GGhNQys="
+ },
+ {
+ "pname": "Microsoft.Extensions.Primitives",
+ "version": "2.0.0",
+ "hash": "sha256-q44LtMvyNEKSvgERvA+BrasKapP92Sc91QR4u2TJ9/Y="
+ },
+ {
+ "pname": "Microsoft.Extensions.Primitives",
+ "version": "8.0.0",
+ "hash": "sha256-FU8qj3DR8bDdc1c+WeGZx/PCZeqqndweZM9epcpXjSo="
+ },
+ {
+ "pname": "Microsoft.NETCore.Platforms",
+ "version": "1.1.0",
+ "hash": "sha256-FeM40ktcObQJk4nMYShB61H/E8B7tIKfl9ObJ0IOcCM="
+ },
+ {
+ "pname": "Microsoft.NETCore.Targets",
+ "version": "1.1.0",
+ "hash": "sha256-0AqQ2gMS8iNlYkrD+BxtIg7cXMnr9xZHtKAuN4bjfaQ="
+ },
+ {
+ "pname": "System.Globalization",
+ "version": "4.3.0",
+ "hash": "sha256-caL0pRmFSEsaoeZeWN5BTQtGrAtaQPwFi8YOZPZG5rI="
+ },
+ {
+ "pname": "System.Runtime",
+ "version": "4.3.0",
+ "hash": "sha256-51813WXpBIsuA6fUtE5XaRQjcWdQ2/lmEokJt97u0Rg="
+ },
+ {
+ "pname": "System.Text.Json",
+ "version": "8.0.5",
+ "hash": "sha256-yKxo54w5odWT6nPruUVsaX53oPRe+gKzGvLnnxtwP68="
+ },
+ {
+ "pname": "System.Threading.Tasks.Dataflow",
+ "version": "8.0.1",
+ "hash": "sha256-hgCfF91BDd/eOtLEd5jhjzgJdvwmVv4/b42fXRr3nvo="
+ }
+]
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..0b0400a
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1778443072,
+ "narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-unstable",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..2645a68
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,72 @@
+{
+ description = "Jellyfin Lessons — a Jellyfin plugin";
+
+ inputs = {
+ nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+ };
+
+ outputs =
+ { self, nixpkgs }:
+ let
+ forAllSystems =
+ fn: nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed fn;
+ in
+ {
+ devShells = forAllSystems (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ {
+ default = pkgs.mkShell {
+ packages = [
+ pkgs.dotnetCorePackages.sdk_9_0
+ pkgs.typst
+ pkgs.nodejs
+ ];
+
+ shellHook = ''
+ echo "jellyfin-lessons dev shell"
+ echo " dotnet: $(dotnet --version)"
+ echo " typst: $(typst --version)"
+ echo " node: $(node --version)"
+ '';
+ };
+ }
+ );
+
+ packages = forAllSystems (
+ system:
+ let
+ pkgs = nixpkgs.legacyPackages.${system};
+ in
+ {
+ default = pkgs.callPackage (
+ {
+ buildDotnetModule,
+ dotnetCorePackages,
+ lib,
+ }:
+ buildDotnetModule {
+ pname = "jellyfin-lessons";
+ version = "0.1.0";
+
+ src = self;
+
+ projectFile = "plugin/Jellyfin.Plugin.Lessons.csproj";
+ nugetDeps = ./deps.json;
+
+ dotnet-sdk = dotnetCorePackages.sdk_9_0;
+ dotnet-runtime = dotnetCorePackages.aspnetcore_9_0;
+
+ meta = {
+ description = "Jellyfin Lessons plugin";
+ license = lib.licenses.mit;
+ platforms = lib.platforms.all;
+ };
+ }
+ ) { };
+ }
+ );
+ };
+}
diff --git a/plugin/Jellyfin.Plugin.Lessons.csproj b/plugin/Jellyfin.Plugin.Lessons.csproj
new file mode 100644
index 0000000..37bd5cc
--- /dev/null
+++ b/plugin/Jellyfin.Plugin.Lessons.csproj
@@ -0,0 +1,16 @@
+
+
+ net9.0
+ JellyfinLessons
+ jellyfin-lessons
+
+
+
+
+ runtime
+
+
+ runtime
+
+
+
diff --git a/templates/example-lesson.typ b/templates/example-lesson.typ
deleted file mode 100644
index 44122e7..0000000
--- a/templates/example-lesson.typ
+++ /dev/null
@@ -1,83 +0,0 @@
-// example-lesson.typ — Complete example demonstrating the lesson template
-// ============================================================
-// Frontmatter:
-// title: God's Creation: The Water Cycle
-// grade: 3
-// subject: Science
-// date: 2026-01-15
-// media: jellyfin://uuid-water-cycle, jellyfin://uuid-cloud-formation
-// description: Learn how God designed water to move through His creation —
-// evaporation, condensation, and precipitation working together to sustain life.
-
-#import "lesson.typ": lesson-template, media-ref
-
-#show: doc => lesson-template(
- doc,
- title: "God's Creation: The Water Cycle",
- grade: "3",
- subject: "Science",
- date: "2026-01-15",
- media: (
- ("uuid-water-cycle", "The Water Cycle Explained"),
- ("uuid-cloud-formation", "Cloud Formation and Weather"),
- ),
- description: "Learn how God designed water to move through His creation — evaporation, condensation, and precipitation working together to sustain life on Earth.",
-)
-
-= What Is the Water Cycle?
-
-Water is one of God's most amazing creations! The water cycle is how water moves
-all around our planet. It never runs out — it just keeps going around and around.
-There are four main steps: evaporation, condensation, precipitation, and collection.
-
-== Evaporation
-
-When the sun shines on oceans, lakes, and rivers, the water gets warm and turns
-into a gas called *water vapor*. This gas rises up into the sky. You can't see it,
-but it's there! Evaporation happens every day, especially on warm, sunny days.
-
-== Condensation
-
-When the water vapor goes high up where it's cold, it turns back into tiny water
-droplets. These droplets come together to make clouds. Have you ever looked at
-clouds and seen shapes? Those shapes are made of millions of tiny water droplets
-sticking together!
-
-== Precipitation
-
-When the clouds get too heavy with water droplets, the water falls back down to
-Earth. This is called *precipitation*. It can be:
-
-- Rain — liquid water falling from the sky
-- Snow — frozen crystals that fall in cold weather
-- Sleet — tiny ice pellets that bounce when they land
-- Hail — hard balls of ice that form in big storms
-
-God sends precipitation to water the plants and fill the rivers, lakes, and oceans.
-
-== Collection
-
-The water that falls onto the ground collects in rivers, lakes, and oceans. Some
-of it soaks into the ground and becomes *groundwater* — water stored underground
-that plants can reach with their roots. Then the sun warms the water, and the
-whole cycle starts all over again!
-
-= Why Is the Water Cycle Important?
-
-God designed the water cycle to do important work:
-
-- Give plants the water they need to grow
-- Provide fresh water for animals and people to drink
-- Keep our rivers and lakes full of water
-- Help control the temperature of our whole planet
-- Clean the water naturally as it moves through the cycle
-
-*"He causes the vapors to ascend from the ends of the earth; He makes lightning
-for the rain; He brings the wind out of His treasuries."* — Psalm 135:7
-
-= Think About It
-
-1. What happens to water when the sun shines on a lake?
-2. Why do clouds form high up in the sky?
-3. What are the four types of precipitation?
-4. How does the water cycle show God's care for His creation?
diff --git a/templates/lesson.typ b/templates/lesson.typ
deleted file mode 100644
index a85cab3..0000000
--- a/templates/lesson.typ
+++ /dev/null
@@ -1,90 +0,0 @@
-// lesson.typ — Typst template for homeschool lessons
-// ============================================================
-// This template produces clean, structured lesson documents.
-// Designed to compile to HTML for rendering inside Jellyfin.
-//
-// Frontmatter convention (documented as comments at top of each lesson):
-// // title: The Lesson Title
-// // grade: 3
-// // subject: Science
-// // date: 2026-01-15
-// // media: jellyfin://uuid-1, jellyfin://uuid-2
-// // description: Short description of the lesson
-//
-// Usage:
-// #import "lesson.typ": lesson-template, media-ref
-// #show: doc => lesson-template(
-// doc,
-// title: "The Water Cycle",
-// grade: "3",
-// subject: "Science",
-// date: "2026-01-15",
-// media: (),
-// description: "Learn about evaporation and condensation.",
-// )
-
-// Render a single Jellyfin media reference.
-// Produces a element in HTML output
-// that the Jellyfin plugin can parse and link to the player.
-#let media-ref(id, title) = {
- raw(lang: "jellyfin-media", id + "|" + title)
-}
-
-// Render a list of media references as a formatted section.
-#let media-section(media-items) = {
- if media-items.len() > 0 {
- heading(level: 2)[Media]
- for item in media-items {
- list.item(media-ref(item.at(0), item.at(1)))
- }
- }
-}
-
-// The main lesson template.
-// - body: The lesson content (passed via show rule)
-// - title: Lesson title (string)
-// - grade: Grade level (string, e.g. "3")
-// - subject: Subject name (string)
-// - date: Lesson date in YYYY-MM-DD format
-// - media: Array of (id, title) pairs for linked Jellyfin media
-// - description: Short description of the lesson
-#let lesson-template(
- body,
- title: "Untitled Lesson",
- grade: none,
- subject: none,
- date: none,
- media: (),
- description: none,
-) = {
- // Document metadata for PDF output
- set document(title: title)
-
- // Title — prominent heading
- heading(level: 1, title)
-
- // Metadata line: Grade X — Subject — Date
- let meta-parts = ()
- if grade != none { meta-parts.push([Grade ] + grade) }
- if subject != none { meta-parts.push(subject) }
- if date != none { meta-parts.push(date) }
- if meta-parts.len() > 0 {
- let sep = [ — ]
- let meta-text = meta-parts.first()
- for i in range(1, meta-parts.len()) {
- meta-text += sep + meta-parts.at(i)
- }
- par(text(style: "italic", size: 0.9em, meta-text))
- }
-
- // Description paragraph
- if description != none {
- par([*Description:* ] + description)
- }
-
- // Lesson body content (renders user-authored sections, paragraphs, lists, etc.)
- body
-
- // Media references section — listed with identifiable jellyfin-media codes
- media-section(media)
-}