using System; using System.Globalization; using System.IO; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Jellyfin.Plugin.RedditComments.Models; using Jellyfin.Plugin.RedditComments.Services; using MediaBrowser.Controller.Library; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; namespace Jellyfin.Plugin.RedditComments.Api; /// /// API endpoints for the Reddit Comments plugin. /// [ApiController] [Route("RedditComments")] public class RedditCommentsController : ControllerBase { private readonly ILogger _logger; private readonly ILibraryManager _libraryManager; private readonly RedditCommentsService _commentsService; private readonly RedditClient _redditClient; /// /// Initializes a new instance of the class. /// /// Logger. /// Library manager. /// Comments service. /// Reddit client. public RedditCommentsController( ILogger logger, ILibraryManager libraryManager, RedditCommentsService commentsService, RedditClient redditClient) { _logger = logger; _libraryManager = libraryManager; _commentsService = commentsService; _redditClient = redditClient; } /// /// Gets the Reddit discussion comments for a Jellyfin item. Served from the local cache /// unless this is the first lookup or the cache expired. /// /// The Jellyfin item id. /// Cancellation token. /// The comments response. [HttpGet("Item/{itemId}")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetComments([FromRoute] Guid itemId, CancellationToken cancellationToken) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } try { return await _commentsService.GetForItemAsync(item, forceRefresh: false, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError(ex, "Failed to get Reddit comments for item {ItemId}", itemId); return new CommentsResponse { Found = false, ItemId = itemId.ToString("N", CultureInfo.InvariantCulture), Message = "Failed to fetch Reddit comments: " + ex.Message }; } } /// /// Forces a fresh lookup for an item, bypassing and replacing the cache. /// /// The Jellyfin item id. /// Cancellation token. /// The comments response. [HttpPost("Item/{itemId}/Refresh")] [Authorize] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> RefreshComments([FromRoute] Guid itemId, CancellationToken cancellationToken) { var item = _libraryManager.GetItemById(itemId); if (item is null) { return NotFound(); } try { return await _commentsService.GetForItemAsync(item, forceRefresh: true, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logger.LogError(ex, "Failed to refresh Reddit comments for item {ItemId}", itemId); return new CommentsResponse { Found = false, ItemId = itemId.ToString("N", CultureInfo.InvariantCulture), Message = "Failed to fetch Reddit comments: " + ex.Message }; } } /// /// Tests the configured Reddit API credentials. Admin only. /// /// Cancellation token. /// Whether the credentials work. [HttpGet("Test")] [Authorize(Policy = "RequiresElevation")] [ProducesResponseType(StatusCodes.Status200OK)] public async Task> TestCredentials(CancellationToken cancellationToken) { if (!RedditClient.IsConfigured) { return new { ok = false, message = "Client id and secret are not configured." }; } try { await _redditClient.TestCredentialsAsync(cancellationToken).ConfigureAwait(false); return new { ok = true, message = "Successfully authenticated with Reddit." }; } catch (Exception ex) { return new { ok = false, message = ex.Message }; } } /// /// Serves the web client script that adds the comments button/sidebar to the player. /// /// The JavaScript file. [HttpGet("Static/reddit-comments.js")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] public ActionResult GetScript() { var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Jellyfin.Plugin.RedditComments.Web.reddit-comments.js"); if (stream is null) { return NotFound(); } // Cache for an hour so restarts/plugin updates pick up changes reasonably fast. Response.Headers.CacheControl = "public, max-age=3600"; return File(stream, "text/javascript"); } }