mirror of https://github.com/ErsatzTV/ErsatzTV.git
11 changed files with 226 additions and 66 deletions
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
namespace ErsatzTV.Infrastructure.GitHub.Models |
||||||
|
{ |
||||||
|
public class GitHubTag |
||||||
|
{ |
||||||
|
public string Body { get; set; } |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue