init
This commit is contained in:
210
Jellyfin.Plugin.RedditComments/Services/CommentCache.cs
Normal file
210
Jellyfin.Plugin.RedditComments/Services/CommentCache.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Jellyfin.Plugin.RedditComments.Services;
|
||||
|
||||
/// <summary>
|
||||
/// One cached lookup for a Jellyfin item.
|
||||
/// </summary>
|
||||
public class CachedThread
|
||||
{
|
||||
/// <summary>Gets or sets the Jellyfin item id.</summary>
|
||||
public string ItemId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the Reddit thread id (empty when nothing was found).</summary>
|
||||
public string ThreadId { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the Reddit thread title.</summary>
|
||||
public string ThreadTitle { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the subreddit the thread was found in.</summary>
|
||||
public string Subreddit { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the thread permalink path.</summary>
|
||||
public string Permalink { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Gets or sets the thread score.</summary>
|
||||
public int ThreadScore { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the thread's total comment count.</summary>
|
||||
public int NumComments { get; set; }
|
||||
|
||||
/// <summary>Gets or sets when this entry was fetched (UTC).</summary>
|
||||
public DateTime FetchedAt { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the serialized comments JSON array.</summary>
|
||||
public string CommentsJson { get; set; } = "[]";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SQLite-backed cache of Reddit threads/comments for Jellyfin items.
|
||||
/// Avoids hitting the Reddit API again for episodes that were already looked up.
|
||||
/// </summary>
|
||||
public class CommentCache
|
||||
{
|
||||
private readonly string _dbPath;
|
||||
private readonly ILogger<CommentCache> _logger;
|
||||
private readonly object _lock = new object();
|
||||
private bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CommentCache"/> class.
|
||||
/// </summary>
|
||||
/// <param name="dbPath">Full path of the SQLite database file.</param>
|
||||
/// <param name="logger">Logger.</param>
|
||||
public CommentCache(string dbPath, ILogger<CommentCache> logger)
|
||||
{
|
||||
_dbPath = dbPath;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a cached entry if it exists and is still fresh.
|
||||
/// </summary>
|
||||
/// <param name="itemId">Jellyfin item id.</param>
|
||||
/// <param name="foundTtl">Freshness window for successful lookups.</param>
|
||||
/// <param name="notFoundTtl">Freshness window for negative lookups.</param>
|
||||
/// <returns>The cached entry, or null if missing/stale.</returns>
|
||||
public CachedThread? Get(string itemId, TimeSpan foundTtl, TimeSpan notFoundTtl)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
using var connection = OpenConnection();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT thread_id, thread_title, subreddit, permalink, thread_score, num_comments, fetched_at, comments_json FROM threads WHERE item_id = $itemId";
|
||||
command.Parameters.AddWithValue("$itemId", itemId);
|
||||
|
||||
using var reader = command.ExecuteReader();
|
||||
if (!reader.Read())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entry = new CachedThread
|
||||
{
|
||||
ItemId = itemId,
|
||||
ThreadId = reader.GetString(0),
|
||||
ThreadTitle = reader.GetString(1),
|
||||
Subreddit = reader.GetString(2),
|
||||
Permalink = reader.GetString(3),
|
||||
ThreadScore = reader.GetInt32(4),
|
||||
NumComments = reader.GetInt32(5),
|
||||
FetchedAt = DateTime.Parse(reader.GetString(6), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind),
|
||||
CommentsJson = reader.GetString(7)
|
||||
};
|
||||
|
||||
var found = entry.ThreadId.Length > 0;
|
||||
var ttl = found ? foundTtl : notFoundTtl;
|
||||
if (DateTime.UtcNow - entry.FetchedAt > ttl)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores or replaces the entry for an item. Stamps <see cref="CachedThread.FetchedAt"/> with the current time.
|
||||
/// </summary>
|
||||
/// <param name="entry">The entry to store.</param>
|
||||
public void Set(CachedThread entry)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureInitialized();
|
||||
entry.FetchedAt = DateTime.UtcNow;
|
||||
|
||||
using var connection = OpenConnection();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
INSERT INTO threads (item_id, thread_id, thread_title, subreddit, permalink, thread_score, num_comments, fetched_at, comments_json)
|
||||
VALUES ($itemId, $threadId, $threadTitle, $subreddit, $permalink, $threadScore, $numComments, $fetchedAt, $commentsJson)
|
||||
ON CONFLICT(item_id) DO UPDATE SET
|
||||
thread_id = $threadId,
|
||||
thread_title = $threadTitle,
|
||||
subreddit = $subreddit,
|
||||
permalink = $permalink,
|
||||
thread_score = $threadScore,
|
||||
num_comments = $numComments,
|
||||
fetched_at = $fetchedAt,
|
||||
comments_json = $commentsJson";
|
||||
command.Parameters.AddWithValue("$itemId", entry.ItemId);
|
||||
command.Parameters.AddWithValue("$threadId", entry.ThreadId);
|
||||
command.Parameters.AddWithValue("$threadTitle", entry.ThreadTitle);
|
||||
command.Parameters.AddWithValue("$subreddit", entry.Subreddit);
|
||||
command.Parameters.AddWithValue("$permalink", entry.Permalink);
|
||||
command.Parameters.AddWithValue("$threadScore", entry.ThreadScore);
|
||||
command.Parameters.AddWithValue("$numComments", entry.NumComments);
|
||||
command.Parameters.AddWithValue("$fetchedAt", entry.FetchedAt.ToString("O", CultureInfo.InvariantCulture));
|
||||
command.Parameters.AddWithValue("$commentsJson", entry.CommentsJson);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the cached entry for an item (used by manual refresh).
|
||||
/// </summary>
|
||||
/// <param name="itemId">Jellyfin item id.</param>
|
||||
public void Invalidate(string itemId)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
EnsureInitialized();
|
||||
|
||||
using var connection = OpenConnection();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = "DELETE FROM threads WHERE item_id = $itemId";
|
||||
command.Parameters.AddWithValue("$itemId", itemId);
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureInitialized()
|
||||
{
|
||||
if (_initialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var directory = Path.GetDirectoryName(_dbPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
using var connection = OpenConnection();
|
||||
using var command = connection.CreateCommand();
|
||||
command.CommandText = @"
|
||||
CREATE TABLE IF NOT EXISTS threads (
|
||||
item_id TEXT PRIMARY KEY,
|
||||
thread_id TEXT NOT NULL,
|
||||
thread_title TEXT NOT NULL,
|
||||
subreddit TEXT NOT NULL,
|
||||
permalink TEXT NOT NULL,
|
||||
thread_score INTEGER NOT NULL,
|
||||
num_comments INTEGER NOT NULL,
|
||||
fetched_at TEXT NOT NULL,
|
||||
comments_json TEXT NOT NULL
|
||||
)";
|
||||
command.ExecuteNonQuery();
|
||||
|
||||
_initialized = true;
|
||||
_logger.LogInformation("Reddit comments cache initialized at {Path}", _dbPath);
|
||||
}
|
||||
|
||||
private SqliteConnection OpenConnection()
|
||||
{
|
||||
var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadWriteCreate");
|
||||
connection.Open();
|
||||
using var pragma = connection.CreateCommand();
|
||||
pragma.CommandText = "PRAGMA journal_mode=WAL";
|
||||
pragma.ExecuteNonQuery();
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user