Browse Source

show release notes on home page (#165)

pull/166/head v0.0.33-prealpha
Jason Dove 5 years ago committed by GitHub
parent
commit
a2700e087c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      ErsatzTV.Core/Interfaces/GitHub/IGitHubApiClient.cs
  2. 38
      ErsatzTV.Infrastructure/GitHub/GitHubApiClient.cs
  3. 17
      ErsatzTV.Infrastructure/GitHub/IGitHubApi.cs
  4. 7
      ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs
  5. 2
      ErsatzTV/ErsatzTV.csproj
  6. 129
      ErsatzTV/Pages/Index.razor
  7. 5
      ErsatzTV/Pages/_Host.cshtml
  8. 1
      ErsatzTV/Shared/MarkdownView.razor
  9. 55
      ErsatzTV/Shared/MarkdownView.razor.cs
  10. 11
      ErsatzTV/Startup.cs
  11. 16
      ErsatzTV/wwwroot/css/site.css

11
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<Either<BaseError, string>> GetLatestReleaseNotes();
Task<Either<BaseError, string>> GetReleaseNotes(string tag);
}
}

38
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<Either<BaseError, string>> GetLatestReleaseNotes()
{
try
{
IGitHubApi service = RestService.For<IGitHubApi>("https://api.github.com");
return await service.GetReleases().Map(releases => releases.Head().Body);
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
public async Task<Either<BaseError, string>> GetReleaseNotes(string tag)
{
try
{
IGitHubApi service = RestService.For<IGitHubApi>("https://api.github.com");
return await service.GetTag(tag).Map(t => t.Body);
}
catch (Exception ex)
{
return BaseError.New(ex.ToString());
}
}
}
}

17
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<List<GitHubTag>> GetReleases();
[Get("/repos/jasongdove/ErsatzTV/releases/tags/{tag}")]
public Task<GitHubTag> GetTag(string tag);
}
}

7
ErsatzTV.Infrastructure/GitHub/Models/GitHubTag.cs

@ -0,0 +1,7 @@
namespace ErsatzTV.Infrastructure.GitHub.Models
{
public class GitHubTag
{
public string Body { get; set; }
}
}

2
ErsatzTV/ErsatzTV.csproj

@ -19,7 +19,9 @@
<PackageReference Include="Blazored.LocalStorage" Version="3.0.0" /> <PackageReference Include="Blazored.LocalStorage" Version="3.0.0" />
<PackageReference Include="FluentValidation" Version="9.5.3" /> <PackageReference Include="FluentValidation" Version="9.5.3" />
<PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" /> <PackageReference Include="FluentValidation.AspNetCore" Version="9.5.3" />
<PackageReference Include="HtmlSanitizer" Version="5.0.376" />
<PackageReference Include="LanguageExt.Core" Version="3.4.15" /> <PackageReference Include="LanguageExt.Core" Version="3.4.15" />
<PackageReference Include="Markdig" Version="0.24.0" />
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" /> <PackageReference Include="MediatR.Courier.DependencyInjection" Version="3.0.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" /> <PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="5.0.4" />

129
ErsatzTV/Pages/Index.razor

@ -1,72 +1,69 @@
@page "/" @page "/"
@using System.Reflection
@using ErsatzTV.Core.Interfaces.GitHub
@using Microsoft.Extensions.Caching.Memory
@inject IGitHubApiClient _gitHubApiClient
@inject IMemoryCache _memoryCache
<MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8"> <MudContainer MaxWidth="MaxWidth.ExtraLarge" Class="pt-8">
<MudCard> <MudCard>
<MudCardContent> <MudCardContent Class="release-notes mud-typography mud-typography-body1">
<MudText Typo="Typo.h3">Welcome to ErsatzTV!</MudText> <MarkdownView Content="@_releaseNotes"/>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Channels</MudText>
<MudText>
<MudLink Href="/channels">Channels</MudLink> are not directly associated with any media. Channels have a <b>number</b>, a <b>name</b>, and a <b>streaming mode</b> that indicates how the channel will play media.
</MudText>
<MudText Class="mt-3">
In <b>TransportStream</b> mode, the channel will also require an <b>FFmpeg profile</b> to configure transcoding and normalization.
In <b>HttpLiveStreaming</b> mode, the channel will attempt to serve the channel's media without transcoding or normalization beyond the container format.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">FFmpeg Profiles</MudText>
<MudText>
<MudLink Href="/ffmpeg">FFmpeg Profiles</MudLink> 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.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Libraries</MudText>
<MudText>
Two local <MudLink Href="/media/libraries">libraries</MudLink> are available, one for each <b>media kind</b>: Shows and Movies. Libraries contain <b>paths</b> (folders) to regularly scan for media items.
Support for Plex libraries is under active development; Jellyfin and Emby library support is planned.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Collections</MudText>
<MudText>
<MudLink Href="/media/collections">Collections</MudLink> have a <b>name</b> 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.
</MudText>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Schedules</MudText>
<MudText>
<MudLink Href="/schedules">Schedules</MudLink> have a <b>name</b>, a <b>collection playback order</b> and <b>items</b> to continually loop through.
</MudText>
<MudText Class="mt-3 mb-2">Three <b>collection playback orders</b> are supported:</MudText>
<ul class="mud-typography-body1">
<li><b>Random</b> - to randomly play collection items; repeating is allowed before all collection items have been played.</li>
<li><b>Shuffle</b> - to randomly play collection items; repeating is <i>not</i> allowed until all collection items have been played.</li>
<li><b>Chronological</b> - to play collection items sorted by air date and then by season and episode number (for when multiple episodes aired on a single day).</li>
</ul>
<MudText Class="mt-3">
Schedule items have a <b>start type</b>, a <b>start time</b>, a <b>collection</b> and a <b>playout mode</b>.
</MudText>
<MudText Class="mt-3">
A <b>fixed</b> start type requires a <b>start time</b>, while a <b>dynamic</b> start type means the schedule item will start immediately after the preceding schedule item.
</MudText>
<MudText Class="mt-3 mb-2">Four <b>playout modes</b> are supported:</MudText>
<ul class="mud-typography-body1">
<li><b>One</b> - to play one media item from the collection before advancing to the next schedule item.</li>
<li><b>Multiple</b> - to play a specified <b>count</b> of media items from the collection before advancing to the next schedule item.</li>
<li><b>Duration</b> - to play the maximum number of complete media items that will fit in the specified <b>playout duration</b>, before either going offline for the remainder of the <b>playout duration</b> (an <b>offline tail</b>), or immediately advancing to the next schedule item.</li>
<li><b>Flood</b> - to play media items from the collection forever, or until the next schedule item's <b>start time</b> if one exists.</li>
</ul>
</MudElement>
<MudElement HtmlTag="div" Class="mt-6">
<MudText Typo="Typo.h4" GutterBottom="true">Playouts</MudText>
<MudText>
<MudLink Href="/playouts">Playouts</MudLink> assign a <b>schedule</b> to a <b>channel</b> and individually track the ordered playback of collection items.
</MudText>
</MudElement>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
</MudContainer> </MudContainer>
@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<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (version != null)
{
Either<BaseError, string> 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
}
}
}

5
ErsatzTV/Pages/_Host.cshtml

@ -43,6 +43,11 @@
function enableSorting() { function enableSorting() {
$("#sortable-collection").sortable("option", "disabled", false); $("#sortable-collection").sortable("option", "disabled", false);
} }
function styleMarkdown() {
$("h2").addClass("mud-typography mud-typography-h4");
$("h3").addClass("mud-typography mud-typography-h5");
}
</script> </script>
</head> </head>
<body> <body>

1
ErsatzTV/Shared/MarkdownView.razor

@ -0,0 +1 @@
@HtmlContent

55
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);
}
}
}

11
ErsatzTV/Startup.cs

@ -10,6 +10,7 @@ using ErsatzTV.Application.Channels.Queries;
using ErsatzTV.Core; using ErsatzTV.Core;
using ErsatzTV.Core.FFmpeg; using ErsatzTV.Core.FFmpeg;
using ErsatzTV.Core.Interfaces.FFmpeg; using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.GitHub;
using ErsatzTV.Core.Interfaces.Images; using ErsatzTV.Core.Interfaces.Images;
using ErsatzTV.Core.Interfaces.Locking; using ErsatzTV.Core.Interfaces.Locking;
using ErsatzTV.Core.Interfaces.Metadata; using ErsatzTV.Core.Interfaces.Metadata;
@ -24,6 +25,7 @@ using ErsatzTV.Core.Scheduling;
using ErsatzTV.Formatters; using ErsatzTV.Formatters;
using ErsatzTV.Infrastructure.Data; using ErsatzTV.Infrastructure.Data;
using ErsatzTV.Infrastructure.Data.Repositories; using ErsatzTV.Infrastructure.Data.Repositories;
using ErsatzTV.Infrastructure.GitHub;
using ErsatzTV.Infrastructure.Images; using ErsatzTV.Infrastructure.Images;
using ErsatzTV.Infrastructure.Locking; using ErsatzTV.Infrastructure.Locking;
using ErsatzTV.Infrastructure.Plex; using ErsatzTV.Infrastructure.Plex;
@ -33,6 +35,7 @@ using ErsatzTV.Serialization;
using ErsatzTV.Services; using ErsatzTV.Services;
using ErsatzTV.Services.RunOnce; using ErsatzTV.Services.RunOnce;
using FluentValidation.AspNetCore; using FluentValidation.AspNetCore;
using Ganss.XSS;
using MediatR; using MediatR;
using MediatR.Courier.DependencyInjection; using MediatR.Courier.DependencyInjection;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -225,6 +228,14 @@ namespace ErsatzTV
services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>(); services.AddScoped<IPlexPathReplacementService, PlexPathReplacementService>();
services.AddScoped<IFFmpegStreamSelector, FFmpegStreamSelector>(); services.AddScoped<IFFmpegStreamSelector, FFmpegStreamSelector>();
services.AddScoped<FFmpegProcessService>(); services.AddScoped<FFmpegProcessService>();
services.AddScoped<IGitHubApiClient, GitHubApiClient>();
services.AddScoped<IHtmlSanitizer, HtmlSanitizer>(
_ =>
{
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.Add("class");
return sanitizer;
});
services.AddHostedService<DatabaseMigratorService>(); services.AddHostedService<DatabaseMigratorService>();
services.AddHostedService<CacheCleanerService>(); services.AddHostedService<CacheCleanerService>();

16
ErsatzTV/wwwroot/css/site.css

@ -120,4 +120,20 @@
flex-direction: column; flex-direction: column;
height: 100%; height: 100%;
justify-content: space-around; 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;
} }
Loading…
Cancel
Save