feat: SQLite lesson data store with schema and C# access layer #13

Merged
sscoble merged 1 commit from issue-4-data-store into main 2026-05-16 04:38:51 +00:00
4 changed files with 251 additions and 0 deletions

223
plugin/Data/LessonStore.cs Normal file
View file

@ -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<Lesson> GetAllLessons()
{
var lessons = new List<Lesson>();
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<Lesson> GetLessonsByGrade(int grade)
{
var lessons = new List<Lesson>();
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<LessonMedia> 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<LessonMedia> GetMediaForLesson(string lessonId)
{
var items = new List<LessonMedia>();
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)
};
}
}

View file

@ -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<LessonMedia> MediaItems { get; set; } = new();
}

View file

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

View file

@ -12,5 +12,8 @@
<PackageReference Include="Jellyfin.Model" Version="10.10.*">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.*">
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
</Project>