From 838d0cbbec22714eea0e9659324afa5bdf6fd191 Mon Sep 17 00:00:00 2001 From: "Engineering (agent-engineering)" Date: Fri, 15 May 2026 21:38:35 -0700 Subject: [PATCH] feat: SQLite lesson data store with schema and C# access layer - Lesson.cs and LessonMedia.cs model classes - LessonStore.cs with CRUD operations - Microsoft.Data.Sqlite package reference - Default DB path: ~/.local/share/jellyfin-lessons/lessons.db Closes #4 --- plugin/Data/LessonStore.cs | 223 ++++++++++++++++++++++++++ plugin/Data/Models/Lesson.cs | 17 ++ plugin/Data/Models/LessonMedia.cs | 8 + plugin/Jellyfin.Plugin.Lessons.csproj | 3 + 4 files changed, 251 insertions(+) create mode 100644 plugin/Data/LessonStore.cs create mode 100644 plugin/Data/Models/Lesson.cs create mode 100644 plugin/Data/Models/LessonMedia.cs diff --git a/plugin/Data/LessonStore.cs b/plugin/Data/LessonStore.cs new file mode 100644 index 0000000..af90313 --- /dev/null +++ b/plugin/Data/LessonStore.cs @@ -0,0 +1,223 @@ +using Microsoft.Data.Sqlite; +using Jellyfin.Plugin.Lessons.Data.Models; + +namespace Jellyfin.Plugin.Lessons.Data; + +public class LessonStore +{ + private readonly string _connectionString; + + public LessonStore(string dbPath) + { + if (dbPath.StartsWith("~")) + dbPath = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile) + dbPath[1..]; + + var dir = Path.GetDirectoryName(dbPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + _connectionString = $"Data Source={dbPath}"; + + CreateTables(); + } + + private void CreateTables() + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS lessons ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + grade INTEGER NOT NULL, + subject TEXT NOT NULL, + lesson_date TEXT NOT NULL, + description TEXT, + source_path TEXT NOT NULL, + compiled_html TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ); + CREATE TABLE IF NOT EXISTS lesson_media ( + lesson_id TEXT NOT NULL, + jellyfin_item_id TEXT NOT NULL, + sort_order INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (lesson_id, jellyfin_item_id), + FOREIGN KEY (lesson_id) REFERENCES lessons(id) ON DELETE CASCADE + );"; + cmd.ExecuteNonQuery(); + } + + public List GetAllLessons() + { + var lessons = new List(); + + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT * FROM lessons ORDER BY lesson_date DESC, subject, title"; + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + lessons.Add(ReadLesson(reader)); + + return lessons; + } + + public Lesson? GetLessonById(string id) + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT * FROM lessons WHERE id = @id"; + cmd.Parameters.AddWithValue("@id", id); + + using var reader = cmd.ExecuteReader(); + if (!reader.Read()) + return null; + + var lesson = ReadLesson(reader); + lesson.MediaItems = GetMediaForLesson(lesson.Id); + return lesson; + } + + public List GetLessonsByGrade(int grade) + { + var lessons = new List(); + + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT * FROM lessons WHERE grade = @grade ORDER BY lesson_date DESC, subject, title"; + cmd.Parameters.AddWithValue("@grade", grade); + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + lessons.Add(ReadLesson(reader)); + + return lessons; + } + + public void UpsertLesson(Lesson lesson, List mediaItems) + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var tx = conn.BeginTransaction(); + + var now = DateTime.UtcNow.ToString("o"); + + if (string.IsNullOrEmpty(lesson.Id)) + lesson.Id = Guid.NewGuid().ToString(); + + lesson.CreatedAt = string.IsNullOrEmpty(lesson.CreatedAt) ? now : lesson.CreatedAt; + lesson.UpdatedAt = now; + + using (var cmd = conn.CreateCommand()) + { + cmd.CommandText = @" + INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at) + VALUES (@id, @title, @grade, @subject, @lesson_date, @description, @source_path, @compiled_html, @created_at, @updated_at) + ON CONFLICT(id) DO UPDATE SET + title = excluded.title, + grade = excluded.grade, + subject = excluded.subject, + lesson_date = excluded.lesson_date, + description = excluded.description, + source_path = excluded.source_path, + compiled_html = excluded.compiled_html, + updated_at = excluded.updated_at"; + cmd.Parameters.AddWithValue("@id", lesson.Id); + cmd.Parameters.AddWithValue("@title", lesson.Title); + cmd.Parameters.AddWithValue("@grade", lesson.Grade); + cmd.Parameters.AddWithValue("@subject", lesson.Subject); + cmd.Parameters.AddWithValue("@lesson_date", lesson.LessonDate); + cmd.Parameters.AddWithValue("@description", (object?)lesson.Description ?? DBNull.Value); + cmd.Parameters.AddWithValue("@source_path", lesson.SourcePath); + cmd.Parameters.AddWithValue("@compiled_html", (object?)lesson.CompiledHtml ?? DBNull.Value); + cmd.Parameters.AddWithValue("@created_at", lesson.CreatedAt); + cmd.Parameters.AddWithValue("@updated_at", lesson.UpdatedAt); + cmd.ExecuteNonQuery(); + } + + // Replace media items + using (var delCmd = conn.CreateCommand()) + { + delCmd.CommandText = "DELETE FROM lesson_media WHERE lesson_id = @lesson_id"; + delCmd.Parameters.AddWithValue("@lesson_id", lesson.Id); + delCmd.ExecuteNonQuery(); + } + + foreach (var m in mediaItems) + { + using var insCmd = conn.CreateCommand(); + insCmd.CommandText = @" + INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) + VALUES (@lesson_id, @jellyfin_item_id, @sort_order)"; + insCmd.Parameters.AddWithValue("@lesson_id", lesson.Id); + insCmd.Parameters.AddWithValue("@jellyfin_item_id", m.JellyfinItemId); + insCmd.Parameters.AddWithValue("@sort_order", m.SortOrder); + insCmd.ExecuteNonQuery(); + } + + tx.Commit(); + } + + public void DeleteLesson(string id) + { + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "DELETE FROM lessons WHERE id = @id"; + cmd.Parameters.AddWithValue("@id", id); + cmd.ExecuteNonQuery(); + } + + private List GetMediaForLesson(string lessonId) + { + var items = new List(); + + using var conn = new SqliteConnection(_connectionString); + conn.Open(); + + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT * FROM lesson_media WHERE lesson_id = @id ORDER BY sort_order"; + cmd.Parameters.AddWithValue("@id", lessonId); + + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + items.Add(new LessonMedia + { + LessonId = reader.GetString(0), + JellyfinItemId = reader.GetString(1), + SortOrder = reader.GetInt32(2) + }); + } + + return items; + } + + private static Lesson ReadLesson(SqliteDataReader reader) + { + return new Lesson + { + Id = reader.GetString(0), + Title = reader.GetString(1), + Grade = reader.GetInt32(2), + Subject = reader.GetString(3), + LessonDate = reader.GetString(4), + Description = reader.IsDBNull(5) ? null : reader.GetString(5), + SourcePath = reader.GetString(6), + CompiledHtml = reader.IsDBNull(7) ? null : reader.GetString(7), + CreatedAt = reader.GetString(8), + UpdatedAt = reader.GetString(9) + }; + } +} diff --git a/plugin/Data/Models/Lesson.cs b/plugin/Data/Models/Lesson.cs new file mode 100644 index 0000000..ee5ee64 --- /dev/null +++ b/plugin/Data/Models/Lesson.cs @@ -0,0 +1,17 @@ +namespace Jellyfin.Plugin.Lessons.Data.Models; + +public class Lesson +{ + public string Id { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public int Grade { get; set; } + public string Subject { get; set; } = string.Empty; + public string LessonDate { get; set; } = string.Empty; + public string? Description { get; set; } + public string SourcePath { get; set; } = string.Empty; + public string? CompiledHtml { get; set; } + public string CreatedAt { get; set; } = string.Empty; + public string UpdatedAt { get; set; } = string.Empty; + + public List MediaItems { get; set; } = new(); +} diff --git a/plugin/Data/Models/LessonMedia.cs b/plugin/Data/Models/LessonMedia.cs new file mode 100644 index 0000000..35d88f6 --- /dev/null +++ b/plugin/Data/Models/LessonMedia.cs @@ -0,0 +1,8 @@ +namespace Jellyfin.Plugin.Lessons.Data.Models; + +public class LessonMedia +{ + public string LessonId { get; set; } = string.Empty; + public string JellyfinItemId { get; set; } = string.Empty; + public int SortOrder { get; set; } +} diff --git a/plugin/Jellyfin.Plugin.Lessons.csproj b/plugin/Jellyfin.Plugin.Lessons.csproj index 37bd5cc..faba00e 100644 --- a/plugin/Jellyfin.Plugin.Lessons.csproj +++ b/plugin/Jellyfin.Plugin.Lessons.csproj @@ -12,5 +12,8 @@ runtime + + runtime + -- 2.49.1