From a2700e087cfd304401040c7aca9ad227fe1b97da Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Sun, 11 Apr 2021 12:34:41 -0500 Subject: [PATCH] show release notes on home page (#165) --- .../Interfaces/GitHub/IGitHubApiClient.cs | 11 ++ .../GitHub/GitHubApiClient.cs | 38 ++++++ ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs | 17 +++ .../GitHub/Models/GitHubTag.cs | 7 + ErsatzTV/ErsatzTV.csproj | 2 + ErsatzTV/Pages/Index.razor | 129 +++++++++--------- ErsatzTV/Pages/_Host.cshtml | 5 + ErsatzTV/Shared/MarkdownView.razor | 1 + ErsatzTV/Shared/MarkdownView.razor.cs | 55 ++++++++ ErsatzTV/Startup.cs | 11 ++ ErsatzTV/wwwroot/css/site.css | 16 +++ 11 files changed, 226 insertions(+), 66 deletions(-) create mode 100644 ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs create mode 100644 ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs create mode 100644 ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs create mode 100644 ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs create mode 100644 ErsatzTV/Shared/MarkdownView.razor create mode 100644 ErsatzTV/Shared/MarkdownView.razor.cs diff --git a/ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs b/ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs new file mode 100644 index 000000000..9f0e51e86 --- /dev/null +++ b/ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using LanguageExt; + +namespace ErsatzTV.Core.Interfaces.GitHub +{ + public interface IGitHubApiClient + { + Task> GetLatestReleaseNotes(); + Task> GetReleaseNotes(string tag); + } +} diff --git a/ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs b/ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs new file mode 100644 index 000000000..f24c25f4c --- /dev/null +++ b/ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; +using ErsatzTV.Core; +using ErsatzTV.Core.Interfaces.GitHub; +using LanguageExt; +using Refit; + +namespace ErsatzTV.Infrastructure.GitHub +{ + public class GitHubApiClient : IGitHubApiClient + { + public async Task> GetLatestReleaseNotes() + { + try + { + IGitHubApi service = RestService.For("https://api.github.com"); + return await service.GetReleases().Map(releases => releases.Head().Body); + } + catch (Exception ex) + { + return BaseError.New(ex.ToString()); + } + } + + public async Task> GetReleaseNotes(string tag) + { + try + { + IGitHubApi service = RestService.For("https://api.github.com"); + return await service.GetTag(tag).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..e11b5004a --- /dev/null +++ b/ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using ErsatzTV.Infrastructure.GitHub.Models; +using Refit; + +namespace ErsatzTV.Infrastructure.GitHub +{ + [Headers("Accept: application/vnd.github.v3+json", "User-Agent: jasongdove/ErsatzTV")] + public interface IGitHubApi + { + [Get("/repos/jasongdove/ErsatzTV/releases")] + public Task> GetReleases(); + + [Get("/repos/jasongdove/ErsatzTV/releases/tags/{tag}")] + public Task GetTag(string tag); + } +} diff --git a/ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs b/ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs new file mode 100644 index 000000000..67271a9a4 --- /dev/null +++ b/ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs @@ -0,0 +1,7 @@ +namespace ErsatzTV.Infrastructure.GitHub.Models +{ + public class GitHubTag + { + public string Body { get; set; } + } +} diff --git a/ErsatzTV/ErsatzTV.csproj b/ErsatzTV/ErsatzTV.csproj index 82a7ddc47..982caa906 100644 --- a/ErsatzTV/ErsatzTV.csproj +++ b/ErsatzTV/ErsatzTV.csproj @@ -19,7 +19,9 @@ + + diff --git a/ErsatzTV/Pages/Index.razor b/ErsatzTV/Pages/Index.razor index 62491598f..d50f1a27f 100644 --- a/ErsatzTV/Pages/Index.razor +++ b/ErsatzTV/Pages/Index.razor @@ -1,72 +1,69 @@ @page "/" +@using System.Reflection +@using ErsatzTV.Core.Interfaces.GitHub +@using Microsoft.Extensions.Caching.Memory +@inject IGitHubApiClient _gitHubApiClient +@inject IMemoryCache _memoryCache - - Welcome to ErsatzTV! - - Channels - - Channels are not directly associated with any media. Channels have a number, a name, and a streaming mode that indicates how the channel will play media. - - - In TransportStream mode, the channel will also require an FFmpeg profile to configure transcoding and normalization. - In HttpLiveStreaming mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format. - - - - FFmpeg Profiles - - FFmpeg Profiles are collections of FFmpeg settings that are applied at the channel level. - All content on a given channel will use the same FFmpeg settings. This also means the same content on different channels can use different settings. - - - - Libraries - - Two local libraries are available, one for each media kind: Shows and Movies. Libraries contain paths (folders) to regularly scan for media items. - Support for Plex libraries is under active development; Jellyfin and Emby library support is planned. - - - - Collections - - Collections have a name and contain a logical grouping of media items. - Collections may contain shows, seasons, episodes or movies. - Collections containing shows and seasons are automatically updated as media is added or removed from the linked shows and seasons. - - - - Schedules - - Schedules have a name, a collection playback order and items to continually loop through. - - Three collection playback orders are supported: -
    -
  • Random - to randomly play collection items; repeating is allowed before all collection items have been played.
  • -
  • Shuffle - to randomly play collection items; repeating is not allowed until all collection items have been played.
  • -
  • Chronological - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).
  • -
- - Schedule items have a start type, a start time, a collection and a playout mode. - - - A fixed start type requires a start time, while a dynamic start type means the schedule item will start immediately after the preceding schedule item. - - Four playout modes are supported: -
    -
  • One - to play one media item from the collection before advancing to the next schedule item.
  • -
  • Multiple - to play a specified count of media items from the collection before advancing to the next schedule item.
  • -
  • Duration - to play the maximum number of complete media items that will fit in the specified playout duration, before either going offline for the remainder of the playout duration (an offline tail), or immediately advancing to the next schedule item.
  • -
  • Flood - to play media items from the collection forever, or until the next schedule item's start time if one exists.
  • -
-
- - Playouts - - Playouts assign a schedule to a channel and individually track the ordered playback of collection items. - - + +
-
\ No newline at end of file + + +@code { + + private string _releaseNotes; + + protected override async Task OnParametersSetAsync() + { + 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() + "-prealpha"; + if (!gitHubVersion.StartsWith("v")) + { + gitHubVersion = $"v{gitHubVersion}"; + } + + maybeNotes = await _gitHubApiClient.GetReleaseNotes(gitHubVersion); + maybeNotes.IfRight(notes => _releaseNotes = notes); + } + else + { + maybeNotes = await _gitHubApiClient.GetLatestReleaseNotes(); + maybeNotes.IfRight(notes => _releaseNotes = notes); + } + } + } + + if (_releaseNotes != null) + { + _memoryCache.Set("Index.ReleaseNotesHtml", _releaseNotes); + } + } + } + catch (Exception ex) + { + // ignore + } + } + +} \ No newline at end of file diff --git a/ErsatzTV/Pages/_Host.cshtml b/ErsatzTV/Pages/_Host.cshtml index 00edcedee..02eae4871 100644 --- a/ErsatzTV/Pages/_Host.cshtml +++ b/ErsatzTV/Pages/_Host.cshtml @@ -43,6 +43,11 @@ function enableSorting() { $("#sortable-collection").sortable("option", "disabled", false); } + + function styleMarkdown() { + $("h2").addClass("mud-typography mud-typography-h4"); + $("h3").addClass("mud-typography mud-typography-h5"); + } diff --git a/ErsatzTV/Shared/MarkdownView.razor b/ErsatzTV/Shared/MarkdownView.razor new file mode 100644 index 000000000..9a558554b --- /dev/null +++ b/ErsatzTV/Shared/MarkdownView.razor @@ -0,0 +1 @@ +@HtmlContent \ No newline at end of file diff --git a/ErsatzTV/Shared/MarkdownView.razor.cs b/ErsatzTV/Shared/MarkdownView.razor.cs new file mode 100644 index 000000000..690d4569d --- /dev/null +++ b/ErsatzTV/Shared/MarkdownView.razor.cs @@ -0,0 +1,55 @@ +using System.Threading.Tasks; +using Ganss.XSS; +using Markdig; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace ErsatzTV.Shared +{ + public partial class MarkdownView + { + private string _content; + + [Inject] + public IHtmlSanitizer HtmlSanitizer { get; set; } + + [Inject] + public IJSRuntime JsRuntime { get; set; } + + [Parameter] + public string Content + { + get => _content; + set + { + _content = value; + HtmlContent = ConvertStringToMarkupString(_content); + } + } + + public MarkupString HtmlContent { get; private set; } + + private MarkupString ConvertStringToMarkupString(string value) + { + if (!string.IsNullOrWhiteSpace(_content)) + { + // Convert markdown string to HTML + string html = Markdown.ToHtml(value, new MarkdownPipelineBuilder().UseAdvancedExtensions().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) + { + await JsRuntime.InvokeVoidAsync("styleMarkdown"); + await base.OnAfterRenderAsync(firstRender); + } + } +} diff --git a/ErsatzTV/Startup.cs b/ErsatzTV/Startup.cs index 41c34c04a..44c8465c2 100644 --- a/ErsatzTV/Startup.cs +++ b/ErsatzTV/Startup.cs @@ -10,6 +10,7 @@ using ErsatzTV.Application.Channels.Queries; using ErsatzTV.Core; using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg; +using ErsatzTV.Core.Interfaces.GitHub; using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Metadata; @@ -24,6 +25,7 @@ using ErsatzTV.Core.Scheduling; using ErsatzTV.Formatters; using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data.Repositories; +using ErsatzTV.Infrastructure.GitHub; using ErsatzTV.Infrastructure.Images; using ErsatzTV.Infrastructure.Locking; using ErsatzTV.Infrastructure.Plex; @@ -33,6 +35,7 @@ using ErsatzTV.Serialization; using ErsatzTV.Services; using ErsatzTV.Services.RunOnce; using FluentValidation.AspNetCore; +using Ganss.XSS; using MediatR; using MediatR.Courier.DependencyInjection; using Microsoft.AspNetCore.Builder; @@ -225,6 +228,14 @@ namespace ErsatzTV services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped( + _ => + { + var sanitizer = new HtmlSanitizer(); + sanitizer.AllowedAttributes.Add("class"); + return sanitizer; + }); services.AddHostedService(); services.AddHostedService(); diff --git a/ErsatzTV/wwwroot/css/site.css b/ErsatzTV/wwwroot/css/site.css index 900b79a7d..c881b83cb 100644 --- a/ErsatzTV/wwwroot/css/site.css +++ b/ErsatzTV/wwwroot/css/site.css @@ -120,4 +120,20 @@ flex-direction: column; height: 100%; justify-content: space-around; +} + +.release-notes ul { + list-style: unset; +} + +.release-notes > ul { + margin-top: 10px; +} + +.release-notes ul > li { + margin-left: 30px; +} + +.release-notes h3 { + margin-top: 20px; } \ No newline at end of file