Merge remote-tracking branch 'origin/main' into feat/143-transcoding-editors
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 4m10s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 4m30s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped

# Conflicts:
#	web/src/App.tsx
#	web/src/screens/SettingsScreen.tsx
This commit is contained in:
2026-07-07 14:55:55 +02:00
20 changed files with 1346 additions and 4 deletions
@@ -0,0 +1,39 @@
using ErsatzTV.Application.Logs;
using ErsatzTV.Core.Api.Logs;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace ErsatzTV.Controllers.Api;
[ApiController]
public class LogsController(IMediator mediator) : ControllerBase
{
private const int MaxPageSize = 100;
[HttpGet("/api/logs", Name = "GetLogs")]
[Tags("Logs")]
[EndpointSummary("Get recent log entries")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(PagedLogEntriesResponseModel), StatusCodes.Status200OK)]
public async Task<PagedLogEntriesResponseModel> GetLogs(
[FromQuery] int pageNum = 0,
[FromQuery] int pageSize = 100,
[FromQuery] string filter = "",
CancellationToken cancellationToken = default)
{
int clampedPageNum = Math.Max(0, pageNum);
int clampedPageSize = Math.Clamp(pageSize, 1, MaxPageSize);
PagedLogEntriesViewModel result = await mediator.Send(
new GetRecentLogEntries(clampedPageNum, clampedPageSize, filter ?? string.Empty),
cancellationToken);
return new PagedLogEntriesResponseModel(
result.TotalCount,
result.Page.Map(ProjectToResponseModel).ToList());
}
private static LogEntryResponseModel ProjectToResponseModel(LogEntryViewModel viewModel) =>
new(viewModel.Timestamp, viewModel.Level.ToString(), viewModel.Message);
}
@@ -1,15 +1,19 @@
using System.IO.Abstractions;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;
using ErsatzTV.Application;
using ErsatzTV.Application.MediaItems;
using ErsatzTV.Application.Troubleshooting;
using ErsatzTV.Application.Troubleshooting.Queries;
using ErsatzTV.Core;
using ErsatzTV.Core.Api.Troubleshooting;
using ErsatzTV.Core.Domain;
using ErsatzTV.Core.Interfaces.FFmpeg;
using ErsatzTV.Core.Interfaces.Repositories;
using ErsatzTV.Core.Interfaces.Troubleshooting;
using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Serilog.Context;
@@ -23,8 +27,55 @@ public class TroubleshootController(
ITroubleshootingNotifier notifier,
IMediator mediator) : ControllerBase
{
private static readonly JsonSerializerOptions GeneralJsonOptions = new()
{
Converters = { new JsonStringEnumConverter() },
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
[HttpGet("api/troubleshoot/info", Name = "GetTroubleshootingInfo")]
[Tags("Troubleshooting")]
[EndpointSummary("Get troubleshooting diagnostic info")]
[EndpointGroupName("general")]
[ProducesResponseType(typeof(TroubleshootingInfoResponseModel), StatusCodes.Status200OK)]
public async Task<TroubleshootingInfoResponseModel> GetInfo(CancellationToken cancellationToken)
{
TroubleshootingInfo info = await mediator.Send(new GetTroubleshootingInfo(), cancellationToken);
// mirrors the "General" tab JSON blob built by Pages/Troubleshooting/Troubleshooting.razor
string generalJson = JsonSerializer.Serialize(
new
{
info.Version,
Environment = info.Environment.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value),
info.Cpus,
info.VideoControllers,
info.Health,
info.FFmpegSettings,
AviSynth = new
{
Demuxer = info.AviSynthDemuxer,
Installed = info.AviSynthInstalled
},
info.Channels,
info.FFmpegProfiles
},
GeneralJsonOptions);
return new TroubleshootingInfoResponseModel(
generalJson,
info.NvidiaCapabilities,
info.QsvCapabilities,
info.VaapiCapabilities,
info.VideoToolboxCapabilities);
}
[HttpHead("api/troubleshoot/playback.m3u8")]
[HttpGet("api/troubleshoot/playback.m3u8")]
[Tags("Troubleshooting")]
[EndpointSummary("Start a troubleshooting playback session")]
[EndpointGroupName("general")]
public async Task<IActionResult> TroubleshootPlayback(
[FromQuery]
int mediaItem,
@@ -159,6 +210,9 @@ public class TroubleshootController(
[HttpHead("api/troubleshoot/playback/archive")]
[HttpGet("api/troubleshoot/playback/archive")]
[Tags("Troubleshooting")]
[EndpointSummary("Download the last troubleshooting playback session archive")]
[EndpointGroupName("general")]
public async Task<IActionResult> TroubleshootPlaybackArchive(CancellationToken cancellationToken)
{
Option<string> maybeArchivePath = await mediator.Send(new ArchiveTroubleshootingResults(), cancellationToken);
@@ -182,6 +236,9 @@ public class TroubleshootController(
[HttpHead("api/troubleshoot/playback/sample/{mediaItemId:int}")]
[HttpGet("api/troubleshoot/playback/sample/{mediaItemId:int}")]
[Tags("Troubleshooting")]
[EndpointSummary("Download a media sample archive for troubleshooting")]
[EndpointGroupName("general")]
public async Task<IActionResult> TroubleshootPlaybackSample(int mediaItemId, CancellationToken cancellationToken)
{
Option<string> maybeArchivePath = await mediator.Send(new ArchiveMediaSample(mediaItemId), cancellationToken);
+440
View File
@@ -3088,6 +3088,65 @@
}
}
},
"/api/logs": {
"get": {
"tags": [
"Logs"
],
"summary": "Get recent log entries",
"operationId": "GetLogs",
"parameters": [
{
"name": "pageNum",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 0
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"format": "int32",
"default": 100
}
},
{
"name": "filter",
"in": "query",
"schema": {
"type": "string",
"default": ""
}
}
],
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/PagedLogEntriesResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/PagedLogEntriesResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/PagedLogEntriesResponseModel"
}
}
}
}
}
}
},
"/api/maintenance/gc": {
"get": {
"tags": [
@@ -5857,6 +5916,303 @@
}
}
},
"/api/troubleshoot/info": {
"get": {
"tags": [
"Troubleshooting"
],
"summary": "Get troubleshooting diagnostic info",
"operationId": "GetTroubleshootingInfo",
"responses": {
"200": {
"description": "OK",
"content": {
"text/plain": {
"schema": {
"$ref": "#/components/schemas/TroubleshootingInfoResponseModel"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/TroubleshootingInfoResponseModel"
}
},
"text/json": {
"schema": {
"$ref": "#/components/schemas/TroubleshootingInfoResponseModel"
}
}
}
}
}
}
},
"/api/troubleshoot/playback.m3u8": {
"head": {
"tags": [
"Troubleshooting"
],
"summary": "Start a troubleshooting playback session",
"parameters": [
{
"name": "mediaItem",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "channel",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "ffmpegProfile",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "streamingMode",
"in": "query",
"schema": {
"$ref": "#/components/schemas/StreamingMode"
}
},
{
"name": "watermark",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
},
{
"name": "graphicsElement",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
},
{
"name": "streamSelector",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "subtitleId",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "seekSeconds",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "start",
"in": "query",
"schema": {
"type": "string",
"format": "date-time"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"get": {
"tags": [
"Troubleshooting"
],
"summary": "Start a troubleshooting playback session",
"parameters": [
{
"name": "mediaItem",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "channel",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "ffmpegProfile",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "streamingMode",
"in": "query",
"schema": {
"$ref": "#/components/schemas/StreamingMode"
}
},
{
"name": "watermark",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
},
{
"name": "graphicsElement",
"in": "query",
"schema": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
}
},
{
"name": "streamSelector",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "subtitleId",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "seekSeconds",
"in": "query",
"schema": {
"type": "integer",
"format": "int32"
}
},
{
"name": "start",
"in": "query",
"schema": {
"type": "string",
"format": "date-time"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/troubleshoot/playback/archive": {
"head": {
"tags": [
"Troubleshooting"
],
"summary": "Download the last troubleshooting playback session archive",
"responses": {
"200": {
"description": "OK"
}
}
},
"get": {
"tags": [
"Troubleshooting"
],
"summary": "Download the last troubleshooting playback session archive",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/troubleshoot/playback/sample/{mediaItemId}": {
"head": {
"tags": [
"Troubleshooting"
],
"summary": "Download a media sample archive for troubleshooting",
"parameters": [
{
"name": "mediaItemId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"get": {
"tags": [
"Troubleshooting"
],
"summary": "Download a media sample archive for troubleshooting",
"parameters": [
{
"name": "mediaItemId",
"in": "path",
"required": true,
"schema": {
"type": "integer",
"format": "int32"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/version": {
"get": {
"tags": [
@@ -9140,6 +9496,26 @@
}
}
},
"LogEntryResponseModel": {
"required": [
"timestamp",
"level",
"message"
],
"type": "object",
"properties": {
"timestamp": {
"type": "string",
"format": "date-time"
},
"level": {
"type": "string"
},
"message": {
"type": "string"
}
}
},
"LogEventLevel": {
"enum": [
"Verbose",
@@ -9484,6 +9860,25 @@
}
}
},
"PagedLogEntriesResponseModel": {
"required": [
"totalCount",
"page"
],
"type": "object",
"properties": {
"totalCount": {
"type": "integer",
"format": "int32"
},
"page": {
"type": "array",
"items": {
"$ref": "#/components/schemas/LogEntryResponseModel"
}
}
}
},
"PagedPlayoutItemsResponseModel": {
"required": [
"totalCount",
@@ -10569,6 +10964,45 @@
],
"type": "string"
},
"TroubleshootingInfoResponseModel": {
"required": [
"generalJson",
"nvidiaCapabilities",
"qsvCapabilities",
"vaapiCapabilities",
"videoToolboxCapabilities"
],
"type": "object",
"properties": {
"generalJson": {
"type": "string"
},
"nvidiaCapabilities": {
"type": [
"null",
"string"
]
},
"qsvCapabilities": {
"type": [
"null",
"string"
]
},
"vaapiCapabilities": {
"type": [
"null",
"string"
]
},
"videoToolboxCapabilities": {
"type": [
"null",
"string"
]
}
}
},
"UiSettingsResponseModel": {
"required": [
"isDarkMode",
@@ -11812,6 +12246,9 @@
{
"name": "Libraries"
},
{
"name": "Logs"
},
{
"name": "Maintenance"
},
@@ -11836,6 +12273,9 @@
{
"name": "Smart Collections"
},
{
"name": "Troubleshooting"
},
{
"name": "Version"
},