Initial shell project for the Recyclarr GUI. This is super bare bones and not ready for prime time.gui
commit
466a06003e
@ -0,0 +1,11 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<LayoutView Layout="@typeof(MainLayout)">
|
||||
<p role="alert">Sorry, there's nothing at this address.</p>
|
||||
</LayoutView>
|
||||
</NotFound>
|
||||
</Router>
|
@ -0,0 +1,23 @@
|
||||
using System.IO.Abstractions;
|
||||
using Autofac;
|
||||
using AutofacSerilogIntegration;
|
||||
using Common;
|
||||
using TrashLib.Startup;
|
||||
|
||||
namespace Recyclarr.Gui;
|
||||
|
||||
public static class CompositionRoot
|
||||
{
|
||||
public static void Setup(ContainerBuilder builder)
|
||||
{
|
||||
builder.RegisterLogger();
|
||||
|
||||
builder.RegisterModule<CommonAutofacModule>();
|
||||
|
||||
builder.RegisterType<FileSystem>().As<IFileSystem>();
|
||||
builder.RegisterType<DefaultAppDataSetup>();
|
||||
builder.Register(c => c.Resolve<DefaultAppDataSetup>().CreateAppPaths())
|
||||
.As<IAppPaths>()
|
||||
.SingleInstance();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
@page
|
||||
@model Recyclarr.Gui.Pages.ErrorModel
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<title>Error</title>
|
||||
<link href="~/css/bootstrap/bootstrap.min.css" rel="stylesheet" />
|
||||
<link href="~/css/app.css" rel="stylesheet" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="main">
|
||||
<div class="content px-4">
|
||||
<h1 class="text-danger">Error.</h1>
|
||||
<h2 class="text-danger">An error occurred while processing your request.</h2>
|
||||
|
||||
@if (Model.ShowRequestId)
|
||||
{
|
||||
<p>
|
||||
<strong>Request ID:</strong> <code>@Model.RequestId</code>
|
||||
</p>
|
||||
}
|
||||
|
||||
<h3>Development Mode</h3>
|
||||
<p>
|
||||
Swapping to the <strong>Development</strong> environment displays detailed information about the error that occurred.
|
||||
</p>
|
||||
<p>
|
||||
<strong>The Development environment shouldn't be enabled for deployed applications.</strong>
|
||||
It can result in displaying sensitive information from exceptions to end users.
|
||||
For local debugging, enable the <strong>Development</strong> environment by setting the <strong>ASPNETCORE_ENVIRONMENT</strong> environment variable to <strong>Development</strong>
|
||||
and restarting the app.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -0,0 +1,7 @@
|
||||
@page "/"
|
||||
|
||||
<PageTitle>Index</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h3" GutterBottom="true">Hello, world!</MudText>
|
||||
<MudText Class="mb-8">Welcome to your new app, powered by MudBlazor!</MudText>
|
||||
<MudAlert Severity="Severity.Normal">You can find documentation and examples on our website here: <MudLink Href="https://mudblazor.com" Typo="Typo.body2" Color="Color.Inherit"><b>www.mudblazor.com</b></MudLink></MudAlert>
|
@ -0,0 +1,9 @@
|
||||
@page "/"
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@namespace Recyclarr.Gui.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@{
|
||||
Layout = "_Layout";
|
||||
}
|
||||
|
||||
<component type="typeof(App)" render-mode="ServerPrerendered" />
|
@ -0,0 +1,37 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@namespace Recyclarr.Gui.Pages
|
||||
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="~/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<component type="typeof(HeadOutlet)" render-mode="ServerPrerendered" />
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
</head>
|
||||
<body>
|
||||
@RenderBody()
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
<environment include="Staging,Production">
|
||||
An error has occurred. This application may no longer respond until reloaded.
|
||||
</environment>
|
||||
<environment include="Development">
|
||||
An unhandled exception has occurred. See browser dev tools for details.
|
||||
</environment>
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.server.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,49 @@
|
||||
// [CA1506] '<Main>$' is coupled with '54' different types from '34' different namespaces. Rewrite or refactor the code
|
||||
// to decrease its class coupling below '41'.
|
||||
#pragma warning disable CA1506
|
||||
|
||||
using System.IO.Abstractions;
|
||||
using Autofac.Extensions.DependencyInjection;
|
||||
using MudBlazor.Services;
|
||||
using Recyclarr.Gui;
|
||||
using Serilog;
|
||||
using TrashLib.Startup;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddServerSideBlazor();
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
builder.Host
|
||||
.ConfigureServices(x => x.AddAutofac())
|
||||
.UseServiceProviderFactory(new AutofacServiceProviderFactory(CompositionRoot.Setup))
|
||||
.UseSerilog((_, provider, config) =>
|
||||
{
|
||||
var paths = provider.GetRequiredService<IAppPaths>();
|
||||
var logFile = paths.LogDirectory.SubDirectory("gui").File("gui.log");
|
||||
config
|
||||
.MinimumLevel.Debug()
|
||||
.WriteTo.File(logFile.FullName);
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
var paths = app.Services.GetRequiredService<IAppPaths>();
|
||||
var log = app.Services.GetRequiredService<ILogger>();
|
||||
log.Debug("App Data Dir: {AppData}", paths.AppDataDirectory);
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseExceptionHandler("/Error");
|
||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
|
||||
app.Run();
|
@ -0,0 +1,13 @@
|
||||
{
|
||||
"profiles": {
|
||||
"Recyclarr.Gui": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5000",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<Using Remove="Microsoft.Extensions.Logging" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Autofac" />
|
||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" />
|
||||
<PackageReference Include="AutofacSerilogIntegration" Version="5.*" />
|
||||
<PackageReference Include="MudBlazor" Version="6.*" />
|
||||
<PackageReference Include="ReactiveUI.Blazor" Version="18.*" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="5.*" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\TrashLib\TrashLib.csproj" />
|
||||
<ProjectReference Include="..\VersionControl\VersionControl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
@ -0,0 +1,34 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider IsDarkMode="true" />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<PageTitle>Recyclarr</PageTitle>
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="0">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="@(_ => DrawerToggle())" />
|
||||
</MudAppBar>
|
||||
<MudDrawer @bind-Open="_drawerOpen" Elevation="1">
|
||||
<MudDrawerHeader>
|
||||
<MudImage Src="android-chrome-512x512.png" Width="32" Height="32" Class="mr-2" />
|
||||
<MudText Typo="Typo.h6">Recyclarr</MudText>
|
||||
</MudDrawerHeader>
|
||||
<NavMenu />
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.Large" Class="my-16 pt-16">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
bool _drawerOpen = true;
|
||||
|
||||
void DrawerToggle()
|
||||
{
|
||||
_drawerOpen = !_drawerOpen;
|
||||
}
|
||||
}
|
@ -0,0 +1,4 @@
|
||||
<MudNavMenu>
|
||||
<MudNavLink Href="" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Home">Home</MudNavLink>
|
||||
<MudNavLink Href="custom-format" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Add">Custom Formats</MudNavLink>
|
||||
</MudNavMenu>
|
@ -0,0 +1,11 @@
|
||||
@using System.Net.Http
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using MudBlazor
|
||||
@using Recyclarr.Gui
|
||||
@using Recyclarr.Gui.Shared
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"DetailedErrors": true
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft": "Warning",
|
||||
"Microsoft.Hosting.Lifetime": "Information"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 39 KiB |
After Width: | Height: | Size: 9.9 KiB |
After Width: | Height: | Size: 624 B |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 15 KiB |
@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
@ -0,0 +1,87 @@
|
||||
using System.IO.Abstractions;
|
||||
using System.IO.Abstractions.TestingHelpers;
|
||||
using AutoFixture.NUnit3;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using TestLibrary.AutoFixture;
|
||||
using TrashLib.Radarr.CustomFormat.Guide;
|
||||
using TrashLib.Startup;
|
||||
|
||||
namespace TrashLib.Tests.Radarr.CustomFormat.Guide;
|
||||
|
||||
[TestFixture]
|
||||
[Parallelizable(ParallelScope.All)]
|
||||
public class CustomFormatGroupParserTest
|
||||
{
|
||||
[Test, AutoMockData]
|
||||
public void It_works(
|
||||
[Frozen] IAppPaths paths,
|
||||
[Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs,
|
||||
CustomFormatGroupParser sut)
|
||||
{
|
||||
const string markdown = @"
|
||||
## INDEX
|
||||
|
||||
------
|
||||
|
||||
| Audio Advanced #1 | Audio Advanced #2 |
|
||||
| ----------------------------------------- | ------------------------------- |
|
||||
| [TrueHD ATMOS](#truehd-atmos) | [FLAC](#flac) |
|
||||
| [DTS X](#dts-x) | [PCM](#pcm) |
|
||||
| [ATMOS (undefined)](#atmos-undefined) | [DTS-HD HRA](#dts-hd-hra) |
|
||||
| [DD+ ATMOS](#dd-atmos) | [AAC](#aac) |
|
||||
| [TrueHD](#truehd) | [DD](#dd) |
|
||||
| [DTS-HD MA](#dts-hd-ma) | [MP3](#mp3) |
|
||||
| [DD+](#ddplus) | [Opus](#opus) |
|
||||
| [DTS-ES](#dts-es) | |
|
||||
| [DTS](#dts) | |
|
||||
| | |
|
||||
|
||||
------
|
||||
|
||||
| Movie Versions | Unwanted |
|
||||
| --------------------------------------------- | ---------------------------------- |
|
||||
| [Hybrid](#hybrid) | [BR-DISK](#br-disk) |
|
||||
| [Remaster](#remaster) | [EVO (no WEBDL)](#evo-no-webdl) |
|
||||
| [4K Remaster](#4k-remaster) | [LQ](#lq) |
|
||||
| [Special Editions](#special-edition) | [x265 (720/1080p)](#x265-7201080p) |
|
||||
| [Criterion Collection](#criterion-collection) | [3D](#3d) |
|
||||
| [Theatrical Cut](#theatrical-cut) | [No-RlsGroup](#no-rlsgroup) |
|
||||
| [IMAX](#imax) | [Obfuscated](#obfuscated) |
|
||||
| [IMAX Enhanced](#imax-enhanced) | [DV (WEBDL)](#dv-webdl) |
|
||||
| | |
|
||||
|
||||
------
|
||||
";
|
||||
|
||||
var file = paths.RepoDirectory
|
||||
.SubDirectory("docs")
|
||||
.SubDirectory("Radarr")
|
||||
.File("Radarr-collection-of-custom-formats.md");
|
||||
|
||||
fs.AddFile(file.FullName, new MockFileData(markdown));
|
||||
|
||||
var result = sut.Parse();
|
||||
|
||||
result.Keys.Should().BeEquivalentTo(
|
||||
"Audio Advanced #1",
|
||||
"Audio Advanced #2",
|
||||
"Movie Versions",
|
||||
"Unwanted"
|
||||
);
|
||||
|
||||
result.Should().ContainKey("Audio Advanced #1")
|
||||
.WhoseValue.Should().BeEquivalentTo(new[]
|
||||
{
|
||||
new CustomFormatGroupItem("TrueHD ATMOS", "truehd-atmos"),
|
||||
new CustomFormatGroupItem("DTS X", "dts-x"),
|
||||
new CustomFormatGroupItem("ATMOS (undefined)", "atmos-undefined"),
|
||||
new CustomFormatGroupItem("DD+ ATMOS", "dd-atmos"),
|
||||
new CustomFormatGroupItem("TrueHD", "truehd"),
|
||||
new CustomFormatGroupItem("DTS-HD MA", "dts-hd-ma"),
|
||||
new CustomFormatGroupItem("DD+", "ddplus"),
|
||||
new CustomFormatGroupItem("DTS-ES", "dts-es"),
|
||||
new CustomFormatGroupItem("DTS", "dts")
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO.Abstractions;
|
||||
using System.Text.RegularExpressions;
|
||||
using Common.Extensions;
|
||||
using TrashLib.Startup;
|
||||
|
||||
namespace TrashLib.Radarr.CustomFormat.Guide;
|
||||
|
||||
public record CustomFormatGroupItem(string Name, string Anchor);
|
||||
|
||||
public class CustomFormatGroupParser
|
||||
{
|
||||
private readonly IAppPaths _paths;
|
||||
private static readonly Regex TableRegex = new(@"^\s*\|(.*)\|\s*$");
|
||||
private static readonly Regex LinkRegex = new(@"^\[(.+?)\]\(#(.+?)\)$");
|
||||
|
||||
public CustomFormatGroupParser(IAppPaths paths)
|
||||
{
|
||||
_paths = paths;
|
||||
}
|
||||
|
||||
public IDictionary<string, ReadOnlyCollection<CustomFormatGroupItem>> Parse()
|
||||
{
|
||||
var mdFile = _paths.RepoDirectory
|
||||
.SubDirectory("docs")
|
||||
.SubDirectory("Radarr")
|
||||
.File("Radarr-collection-of-custom-formats.md");
|
||||
|
||||
var columns = new List<List<string>>();
|
||||
|
||||
using var md = mdFile.OpenText();
|
||||
while (!md.EndOfStream)
|
||||
{
|
||||
var rows = ParseTable(md);
|
||||
|
||||
// Pivot the data so that we have lists of columns instead of lists of rows
|
||||
// Taken from: https://stackoverflow.com/a/39485441/157971
|
||||
columns.AddRange(rows
|
||||
.SelectMany(x => x.Select((value, index) => (value, index)))
|
||||
.GroupBy(x => x.index, x => x.value)
|
||||
.Select(x => x.ToList()));
|
||||
}
|
||||
|
||||
return columns.ToDictionary(
|
||||
x => x[0],
|
||||
x => x.Skip(1).Select(ParseLink).NotNull().ToList().AsReadOnly());
|
||||
}
|
||||
|
||||
private static CustomFormatGroupItem? ParseLink(string markdownLink)
|
||||
{
|
||||
var match = LinkRegex.Match(markdownLink);
|
||||
return match.Success ? new CustomFormatGroupItem(match.Groups[1].Value, match.Groups[2].Value) : null;
|
||||
}
|
||||
|
||||
private static IEnumerable<List<string>> ParseTable(TextReader stream)
|
||||
{
|
||||
var tableRows = new List<List<string>>();
|
||||
|
||||
while (true)
|
||||
{
|
||||
var line = stream.ReadLine();
|
||||
if (line is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!line.Any())
|
||||
{
|
||||
if (tableRows.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = TableRegex.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
if (tableRows.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var tableRow = match.Groups[1].Value;
|
||||
var fields = tableRow.Split('|').Select(x => x.Trim()).ToList();
|
||||
if (!fields.Any())
|
||||
{
|
||||
if (tableRows.Any())
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
tableRows.Add(fields);
|
||||
}
|
||||
|
||||
return tableRows
|
||||
// Filter out the `|---|---|---|` part of the table between the heading & data rows.
|
||||
.Where(x => !Regex.IsMatch(x[0], @"^-+$"));
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
using System.IO.Abstractions;
|
||||
using CliFx.Exceptions;
|
||||
using Common;
|
||||
using TrashLib;
|
||||
|
||||
namespace Recyclarr.Command.Initialization;
|
||||
namespace TrashLib.Startup;
|
||||
|
||||
public class DefaultAppDataSetup
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using System.IO.Abstractions;
|
||||
|
||||
namespace TrashLib;
|
||||
namespace TrashLib.Startup;
|
||||
|
||||
public interface IAppPaths
|
||||
{
|
Loading…
Reference in new issue