#nullable enable using System; using System.Collections.Generic; using System.IO; using Microsoft.Data.Sqlite; using JellyfinLessons.Data.Models; namespace JellyfinLessons.Data; /// /// SQLite-backed data store for lessons and their media associations. /// public class LessonStore { private readonly string _connectionString; /// /// Creates the data store, expanding ~ in the dbPath to the user's home directory. /// public LessonStore(string dbPath) { if (dbPath.StartsWith("~/") || dbPath == "~") { var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); dbPath = Path.Combine(home, dbPath.TrimStart('~', '/')); } _connectionString = $"Data Source={dbPath}"; CreateTables(); } private void CreateTables() { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.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 ); "; command.ExecuteNonQuery(); using var pragmaCmd = connection.CreateCommand(); pragmaCmd.CommandText = "PRAGMA foreign_keys = ON;"; pragmaCmd.ExecuteNonQuery(); } /// /// Returns all lessons (without media). /// public List GetAllLessons() { var lessons = new List(); using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM lessons ORDER BY lesson_date DESC, title ASC"; using var reader = command.ExecuteReader(); while (reader.Read()) lessons.Add(MapLesson(reader)); return lessons; } /// /// Returns a lesson by its id, or null if not found. /// public Lesson? GetLessonById(string id) { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM lessons WHERE id = @id"; command.Parameters.AddWithValue("@id", id); using var reader = command.ExecuteReader(); if (reader.Read()) { var lesson = MapLesson(reader); lesson.MediaItems = GetMediaForLesson(id); return lesson; } return null; } /// /// Returns all lessons for a given grade level. /// public List GetLessonsByGrade(int grade) { var lessons = new List(); using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM lessons WHERE grade = @grade ORDER BY lesson_date DESC, title ASC"; command.Parameters.AddWithValue("@grade", grade); using var reader = command.ExecuteReader(); while (reader.Read()) lessons.Add(MapLesson(reader)); return lessons; } /// /// Inserts or updates a lesson and its associated media entries. /// public void UpsertLesson(Lesson lesson, List media) { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var transaction = connection.BeginTransaction(); using var command = connection.CreateCommand(); command.CommandText = @" INSERT INTO lessons (id, title, grade, subject, lesson_date, description, source_path, compiled_html, created_at, updated_at) VALUES (@id, @title, @grade, @subject, @lessonDate, @description, @sourcePath, @compiledHtml, @createdAt, @updatedAt) 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; "; command.Parameters.AddWithValue("@id", lesson.Id); command.Parameters.AddWithValue("@title", lesson.Title); command.Parameters.AddWithValue("@grade", lesson.Grade); command.Parameters.AddWithValue("@subject", lesson.Subject); command.Parameters.AddWithValue("@lessonDate", lesson.LessonDate); command.Parameters.AddWithValue("@description", (object?)lesson.Description ?? DBNull.Value); command.Parameters.AddWithValue("@sourcePath", lesson.SourcePath); command.Parameters.AddWithValue("@compiledHtml", (object?)lesson.CompiledHtml ?? DBNull.Value); command.Parameters.AddWithValue("@createdAt", lesson.CreatedAt); command.Parameters.AddWithValue("@updatedAt", lesson.UpdatedAt); command.ExecuteNonQuery(); using var deleteMediaCmd = connection.CreateCommand(); deleteMediaCmd.CommandText = "DELETE FROM lesson_media WHERE lesson_id = @lessonId"; deleteMediaCmd.Parameters.AddWithValue("@lessonId", lesson.Id); deleteMediaCmd.ExecuteNonQuery(); foreach (var item in media) { using var insertMediaCmd = connection.CreateCommand(); insertMediaCmd.CommandText = "INSERT INTO lesson_media (lesson_id, jellyfin_item_id, sort_order) VALUES (@lessonId, @jellyfinItemId, @sortOrder)"; insertMediaCmd.Parameters.AddWithValue("@lessonId", lesson.Id); insertMediaCmd.Parameters.AddWithValue("@jellyfinItemId", item.JellyfinItemId); insertMediaCmd.Parameters.AddWithValue("@sortOrder", item.SortOrder); insertMediaCmd.ExecuteNonQuery(); } transaction.Commit(); } /// /// Deletes a lesson and its associated media entries (cascaded by FK if enabled). /// public void DeleteLesson(string id) { using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "DELETE FROM lessons WHERE id = @id"; command.Parameters.AddWithValue("@id", id); command.ExecuteNonQuery(); } private List GetMediaForLesson(string lessonId) { var items = new List(); using var connection = new SqliteConnection(_connectionString); connection.Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT * FROM lesson_media WHERE lesson_id = @lessonId ORDER BY sort_order ASC"; command.Parameters.AddWithValue("@lessonId", lessonId); using var reader = command.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 MapLesson(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) }; } }