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) }; } }