mirror of https://github.com/ErsatzTV/ErsatzTV.git
11 changed files with 226 additions and 66 deletions
@ -0,0 +1,11 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,7 @@
|
||||
namespace ErsatzTV.Infrastructure.GitHub.Models |
||||
{ |
||||
public class GitHubTag |
||||
{ |
||||
public string Body { get; set; } |
||||
} |
||||
} |
||||
@ -1,72 +1,69 @@
@@ -1,72 +1,69 @@
|
||||
@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"> |
||||
<MudCard> |
||||
<MudCardContent> |
||||
<MudText Typo="Typo.h3">Welcome to ErsatzTV!</MudText> |
||||
<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 Class="release-notes mud-typography mud-typography-body1"> |
||||
<MarkdownView Content="@_releaseNotes"/> |
||||
</MudCardContent> |
||||
</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 @@
@@ -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