diff --git a/CHANGELOG.md b/CHANGELOG.md index a539953de..1fe7487b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Changed - Upgrade Intel driver in docker containers to support latest Battlemage devices (e.g. B70) +- Restore release notes section on home page, since bugs continue to be fixed ## [26.5.1] - 2026-05-08 ### Fixed diff --git a/ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs b/ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs new file mode 100644 index 000000000..c663452cf --- /dev/null +++ b/ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.Core.Interfaces.GitHub; + +public interface IGitHubApiClient +{ + Task> GetLatestReleaseNotes(CancellationToken cancellationToken); + Task> GetReleaseNotes(string tag, CancellationToken cancellationToken); +} diff --git a/ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs b/ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs new file mode 100644 index 000000000..a01beca65 --- /dev/null +++ b/ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs @@ -0,0 +1,34 @@ +using ErsatzTV.Core; +using ErsatzTV.Core.Interfaces.GitHub; +using Refit; + +namespace ErsatzTV.Infrastructure.GitHub; + +public class GitHubApiClient : IGitHubApiClient +{ + public async Task> GetLatestReleaseNotes(CancellationToken cancellationToken) + { + try + { + IGitHubApi service = RestService.For("https://api.github.com"); + return await service.GetReleases(cancellationToken).Map(releases => releases.Head().Body); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + public async Task> GetReleaseNotes(string tag, CancellationToken cancellationToken) + { + try + { + IGitHubApi service = RestService.For("https://api.github.com"); + return await service.GetTag(tag, cancellationToken).Map(t => t.Body); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } +} diff --git a/ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs b/ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs new file mode 100644 index 000000000..e30408056 --- /dev/null +++ b/ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs @@ -0,0 +1,14 @@ +using ErsatzTV.Infrastructure.GitHub.Models; +using Refit; + +namespace ErsatzTV.Infrastructure.GitHub; + +[Headers("Accept: application/vnd.github.v3+json", "User-Agent: ErsatzTV/ErsatzTV")] +public interface IGitHubApi +{ + [Get("/repos/ErsatzTV/legacy/releases")] + Task> GetReleases(CancellationToken cancellationToken); + + [Get("/repos/ErsatzTV/legacy/releases/tags/{tag}")] + Task GetTag(string tag, CancellationToken cancellationToken); +} diff --git a/ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs b/ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs new file mode 100644 index 000000000..638a780ea --- /dev/null +++ b/ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs @@ -0,0 +1,6 @@ +namespace ErsatzTV.Infrastructure.GitHub.Models; + +public class GitHubTag +{ + public string Body { get; set; } +} diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index bcae12394..0387fd85d 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -38,6 +38,7 @@ + diff --git a/ErsatzTV/Pages/Index.razor b/ErsatzTV/Pages/Index.razor index e4893d87e..abb3697b4 100644 --- a/ErsatzTV/Pages/Index.razor +++ b/ErsatzTV/Pages/Index.razor @@ -1,8 +1,13 @@ -@page "/" +@page "/" @page "/system/health" @using ErsatzTV.Application.Health @using ErsatzTV.Core.Health +@using System.Reflection +@using ErsatzTV.Core.Interfaces.GitHub +@using Microsoft.Extensions.Caching.Memory @implements IDisposable +@inject IGitHubApiClient GitHubApiClient +@inject IMemoryCache MemoryCache @inject IMediator Mediator @inject SystemStartup SystemStartup; @@ -124,12 +129,24 @@ + + + Full changelog is available on GitHub + + + + + } @code { + private CancellationTokenSource _cts; + + private string _releaseNotes; + protected override void OnInitialized() { SystemStartup.OnDatabaseReady += OnStartupProgress; @@ -140,6 +157,9 @@ { SystemStartup.OnDatabaseReady -= OnStartupProgress; SystemStartup.OnSearchIndexReady -= OnStartupProgress; + + _cts?.Cancel(); + _cts?.Dispose(); } private async void OnStartupProgress(object sender, EventArgs e) @@ -154,6 +174,66 @@ } } + protected override async Task OnParametersSetAsync() + { + _cts?.Cancel(); + _cts?.Dispose(); + _cts = new CancellationTokenSource(); + var token = _cts.Token; + + try + { + if (MemoryCache.TryGetValue("Index.ReleaseNotesHtml", out string releaseNotesHtml)) + { + _releaseNotes = releaseNotesHtml; + } + else + { + var assembly = Assembly.GetEntryAssembly(); + if (assembly != null) + { + string version = assembly.GetCustomAttribute()?.InformationalVersion; + if (version != null) + { + Either maybeNotes; + + if (version != "develop") + { + string gitHubVersion = version.Split("-").Head(); + if (!gitHubVersion.StartsWith("v")) + { + gitHubVersion = $"v{gitHubVersion}"; + } + + maybeNotes = await GitHubApiClient.GetReleaseNotes(gitHubVersion, token); + foreach (string notes in maybeNotes.RightToSeq()) + { + _releaseNotes = notes; + } + } + else + { + maybeNotes = await GitHubApiClient.GetLatestReleaseNotes(token); + foreach (string notes in maybeNotes.RightToSeq()) + { + _releaseNotes = notes; + } + } + } + } + + if (_releaseNotes != null) + { + MemoryCache.Set("Index.ReleaseNotesHtml", _releaseNotes); + } + } + } + catch (Exception) + { + // ignore + } + } + private async Task> ServerReload(TableState state, CancellationToken cancellationToken) { List healthCheckResults = await Mediator.Send(new GetAllHealthCheckResults(), cancellationToken); diff --git a/ErsatzTV/Shared/MarkdownView.razor b/ErsatzTV/Shared/MarkdownView.razor new file mode 100644 index 000000000..d3d0be9a4 --- /dev/null +++ b/ErsatzTV/Shared/MarkdownView.razor @@ -0,0 +1 @@ +@HtmlContent diff --git a/ErsatzTV/Shared/MarkdownView.razor.cs b/ErsatzTV/Shared/MarkdownView.razor.cs new file mode 100644 index 000000000..71b3c5871 --- /dev/null +++ b/ErsatzTV/Shared/MarkdownView.razor.cs @@ -0,0 +1,69 @@ +using Ganss.Xss; +using Markdig; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace ErsatzTV.Shared; + +public partial class MarkdownView +{ + private MarkupString? _markupContent; + + [Inject] + public IHtmlSanitizer HtmlSanitizer { get; set; } + + [Inject] + public IJSRuntime JsRuntime { get; set; } + + [Parameter] + public string Content { get; set; } + + public MarkupString? HtmlContent + { + get + { + if (string.IsNullOrWhiteSpace(Content)) + { + return null; + } + + return _markupContent ?? (_markupContent = ConvertStringToMarkupString(Content)).Value; + } + } + + private MarkupString ConvertStringToMarkupString(string value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + // Convert markdown string to HTML + string html = Markdown.ToHtml( + value, + new MarkdownPipelineBuilder() + .UseAdvancedExtensions() + .UseSoftlineBreakAsHardlineBreak() + .Build()); + + // Sanitize HTML before rendering + string sanitizedHtml = HtmlSanitizer.Sanitize(html); + + // Return sanitized HTML as a MarkupString that Blazor can render + return new MarkupString(sanitizedHtml); + } + + return new MarkupString(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + try + { + await JsRuntime.InvokeVoidAsync("styleMarkdown"); + } + catch (Exception) + { + // ignored + } + + await base.OnAfterRenderAsync(true); + } +} diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index ed8fec951..5fb9b272f 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -21,6 +21,7 @@ using ErsatzTV.Core.Images; using ErsatzTV.Core.Interfaces.Database; using ErsatzTV.Core.Interfaces.Emby; using ErsatzTV.Core.Interfaces.FFmpeg; +using ErsatzTV.Core.Interfaces.GitHub; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Jellyfin; using ErsatzTV.Core.Interfaces.Locking; @@ -55,6 +56,7 @@ using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Database; using ErsatzTV.Infrastructure.Emby; using ErsatzTV.Infrastructure.FFmpeg; +using ErsatzTV.Infrastructure.GitHub; using ErsatzTV.Infrastructure.Health; using ErsatzTV.Infrastructure.Health.Checks; using ErsatzTV.Infrastructure.Images; @@ -833,6 +835,7 @@ public class Startup services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(_ => { var sanitizer = new HtmlSanitizer();