diff --git a/flake.nix b/flake.nix index 2645a68..114ed09 100644 --- a/flake.nix +++ b/flake.nix @@ -68,5 +68,50 @@ ) { }; } ); + + nixosModules.default = + { config, lib, pkgs, ... }: + let + cfg = config.services.jellyfin-lessons; + in + { + options.services.jellyfin-lessons = { + enable = lib.mkEnableOption "Jellyfin Lessons plugin"; + dataDir = lib.mkOption { + type = lib.types.str; + default = "/var/lib/jellyfin-lessons"; + description = "Directory for lessons database"; + }; + adminPort = lib.mkOption { + type = lib.types.port; + default = 3099; + description = "Port for admin UI"; + }; + }; + + config = lib.mkIf cfg.enable { + # Copy plugin DLL to Jellyfin plugins directory on jellyfin preStart + systemd.services.jellyfin = { + preStart = let + pluginPkg = self.packages.${pkgs.system}.default; + in '' + mkdir -p /var/lib/jellyfin/plugins/Lessons + cp -f ${pluginPkg}/lib/jellyfin-lessons/*.dll /var/lib/jellyfin/plugins/Lessons/ + ''; + }; + + # Admin UI as a user systemd service + systemd.user.services.jellyfin-lessons-admin = { + description = "Jellyfin Lessons Admin UI"; + after = [ "network.target" ]; + wantedBy = [ "default.target" ]; + serviceConfig = { + ExecStart = "${pkgs.nodejs}/bin/node ${self}/admin-ui/server.js"; + Restart = "on-failure"; + Environment = "PORT=${toString cfg.adminPort}"; + }; + }; + }; + }; }; } diff --git a/plugin/Api/LessonsController.cs b/plugin/Api/LessonsController.cs new file mode 100644 index 0000000..d2f53c8 --- /dev/null +++ b/plugin/Api/LessonsController.cs @@ -0,0 +1,65 @@ +using Microsoft.AspNetCore.Mvc; +using Jellyfin.Plugin.Lessons.Data; + +namespace Jellyfin.Plugin.Lessons.Api; + +[ApiController] +[Route("Plugins/Lessons")] +public class LessonsController : ControllerBase +{ + private readonly LessonStore _store; + + public LessonsController(LessonStore store) + { + _store = store; + } + + [HttpGet("List")] + public IActionResult List() + { + var lessons = _store.GetAllLessons(); + + var summaries = lessons.Select(l => new + { + id = l.Id, + title = l.Title, + grade = l.Grade, + subject = l.Subject, + lessonDate = l.LessonDate, + description = l.Description + }); + + return Ok(summaries); + } + + [HttpGet("Detail")] + public IActionResult Detail([FromQuery] string id) + { + if (string.IsNullOrEmpty(id)) + return BadRequest("Lesson id is required."); + + var lesson = _store.GetLessonById(id); + if (lesson == null) + return NotFound(); + + return Ok(new + { + id = lesson.Id, + title = lesson.Title, + grade = lesson.Grade, + subject = lesson.Subject, + lessonDate = lesson.LessonDate, + description = lesson.Description, + sourcePath = lesson.SourcePath, + compiledHtml = lesson.CompiledHtml, + createdAt = lesson.CreatedAt, + updatedAt = lesson.UpdatedAt, + mediaItems = lesson.MediaItems.Select(m => new + { + lessonId = m.LessonId, + jellyfinItemId = m.JellyfinItemId, + sortOrder = m.SortOrder + }) + }); + } +} diff --git a/plugin/Configuration/lessonsPage.html b/plugin/Configuration/lessonsPage.html new file mode 100644 index 0000000..c226509 --- /dev/null +++ b/plugin/Configuration/lessonsPage.html @@ -0,0 +1,460 @@ + + + + + + Lessons + + + +
+

📚 Lessons

+
Loading lessons…
+
+ + + + diff --git a/plugin/Jellyfin.Plugin.Lessons.csproj b/plugin/Jellyfin.Plugin.Lessons.csproj index faba00e..5682323 100644 --- a/plugin/Jellyfin.Plugin.Lessons.csproj +++ b/plugin/Jellyfin.Plugin.Lessons.csproj @@ -3,6 +3,8 @@ net9.0 JellyfinLessons jellyfin-lessons + enable + enable @@ -16,4 +18,12 @@ runtime + + + + + + + + diff --git a/plugin/Plugin.cs b/plugin/Plugin.cs index 59f6f42..52c4269 100644 --- a/plugin/Plugin.cs +++ b/plugin/Plugin.cs @@ -1,12 +1,14 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Plugins; using MediaBrowser.Model.Plugins; using MediaBrowser.Model.Serialization; -using Microsoft.Extensions.Logging; using Jellyfin.Plugin.Lessons.Configuration; namespace Jellyfin.Plugin.Lessons; -public class Plugin : BasePlugin +public class Plugin : BasePlugin, IHasWebPages { public Plugin(IApplicationPaths appPaths, IXmlSerializer xmlSerializer) : base(appPaths, xmlSerializer) @@ -18,4 +20,20 @@ public class Plugin : BasePlugin public override Guid Id => Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); public override string Description => "Homeschool lesson plans with embedded media"; + + public IEnumerable GetPages() + { + return new[] + { + new PluginPageInfo + { + Name = Name, + DisplayName = "Lessons", + EmbeddedResourcePath = GetType().Namespace + ".Configuration.lessonsPage.html", + EnableInMainMenu = true, + MenuSection = "plugins", + MenuIcon = "book" + } + }; + } } diff --git a/plugin/ServiceRegistrator.cs b/plugin/ServiceRegistrator.cs index cae7cc3..378341e 100644 --- a/plugin/ServiceRegistrator.cs +++ b/plugin/ServiceRegistrator.cs @@ -1,11 +1,15 @@ -using MediaBrowser.Common.Plugins; +using Jellyfin.Plugin.Lessons.Data; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Plugins; using Microsoft.Extensions.DependencyInjection; namespace Jellyfin.Plugin.Lessons; public class ServiceRegistrator : IPluginServiceRegistrator { - public void RegisterServices(IServiceCollection services) + public void RegisterServices(IServiceCollection services, IServerApplicationHost appHost) { + services.AddSingleton(sp => + new LessonStore("~/.local/share/jellyfin-lessons/lessons.db")); } }