Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17bc988b49 | ||
|
|
749eea836b | ||
|
|
37c52c4cb4 | ||
|
|
33ba58aa68 | ||
|
|
5f6043e593 | ||
|
|
96e95a21fb | ||
|
|
9168fd6bf2 | ||
|
|
14413f62a7 | ||
|
|
34c71a0c12 | ||
|
|
a487e7fe15 | ||
|
|
cd4ea42597 | ||
|
|
a3d42145f7 | ||
|
|
261cf5052a | ||
|
|
de9af2f0f6 | ||
|
|
8d4e18ed2f | ||
|
|
1ee01c1d78 | ||
|
|
7de50dd916 | ||
|
|
744fd3beaa | ||
|
|
861c95e1bd |
@@ -44,6 +44,7 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v1
|
||||
@@ -75,8 +76,9 @@ jobs:
|
||||
- name: Bundle
|
||||
shell: bash
|
||||
run: |
|
||||
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" scripts/macOS/Info.plist
|
||||
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" scripts/macOS/Info.plist
|
||||
brew install coreutils
|
||||
plutil -replace CFBundleShortVersionString -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
|
||||
plutil -replace CFBundleVersion -string "${{ inputs.info_version }}" ErsatzTV-macOS/ErsatzTV-macOS/Info.plist
|
||||
scripts/macOS/bundle.sh
|
||||
|
||||
- name: Sign
|
||||
@@ -173,10 +175,15 @@ jobs:
|
||||
# Define some variables for things we need
|
||||
release_name="ErsatzTV-${{ inputs.release_version }}-${{ matrix.target }}"
|
||||
echo "RELEASE_NAME=${release_name}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
# Build everything
|
||||
dotnet publish ErsatzTV/ErsatzTV.csproj --framework net6.0 --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
|
||||
|
||||
# Build Windows launcher
|
||||
if [ "${{ matrix.kind }}" == "windows" ]; then
|
||||
dotnet publish ErsatzTV-Windows/ErsatzTV-Windows.csproj --framework net6.0-windows --runtime "${{ matrix.target }}" -c Release -o "$release_name" -p:InformationalVersion="${{ inputs.release_version }}-${{ matrix.target }}" -p:EnableCompressionInSingleFile=true -p:DebugType=Embedded -p:PublishSingleFile=true --self-contained true
|
||||
fi
|
||||
|
||||
# Pack files
|
||||
if [ "${{ matrix.kind }}" == "windows" ]; then
|
||||
7z a -tzip "${release_name}.zip" "./${release_name}/*"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "ErsatzTV-macOS"]
|
||||
path = ErsatzTV-macOS
|
||||
url = git@github.com:jasongdove/ErsatzTV-macOS.git
|
||||
+23
-1
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.0-alpha] - 2022-01-29
|
||||
### Fixed
|
||||
- Fix m3u `mode` query param to properly override streaming mode for all channels
|
||||
- `segmenter` for `HLS Segmenter`
|
||||
- `hls-direct` for `HLS Direct`
|
||||
- `ts` for `MPEG-TS`
|
||||
- `ts-legacy` for `MPEG-TS (Legacy)`
|
||||
- omitting the `mode` parameter returns each channel as configured
|
||||
- Link `File Not Found` health check to `Trash` page to allow deletion
|
||||
- Fix `HLS Segmenter` streaming mode with multiple ffmpeg-based clients
|
||||
- Jellyfin (web) and TiviMate (Android) were specifically tested
|
||||
|
||||
### Added
|
||||
- Hide console window on macOS and Windows; tray menu can be used to access UI, logs and to stop the app
|
||||
- Also write logs to text files in the `logs` config subfolder
|
||||
- Add `added_date` to search index
|
||||
- This requires rebuilding the search index and search results may be empty or incomplete until the rebuild is complete
|
||||
- Add `added_inthelast`, `added_notinthelast` search field for relative added date queries
|
||||
- Syntax is a number and a unit (days, weeks, months, years) like `1 week` or `2 years`
|
||||
|
||||
## [0.3.8-alpha] - 2022-01-23
|
||||
### Fixed
|
||||
- Fix issue preventing some versions of ffmpeg (usually 4.4.x) from streaming MPEG-TS (Legacy) channels at all
|
||||
@@ -924,7 +945,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
- Initial release to facilitate testing outside of Docker.
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...HEAD
|
||||
[Unreleased]: https://github.com/jasongdove/ErsatzTV/compare/v0.4.0-alpha...HEAD
|
||||
[0.4.0-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.8-alpha...v0.4.0-alpha
|
||||
[0.3.8-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.7-alpha...v0.3.8-alpha
|
||||
[0.3.7-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.6-alpha...v0.3.7-alpha
|
||||
[0.3.6-alpha]: https://github.com/jasongdove/ErsatzTV/compare/v0.3.5-alpha...v0.3.6-alpha
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net6.0-windows</TargetFramework>
|
||||
<RootNamespace>ErsatzTV_Windows</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
<UseWindowsForms>true</UseWindowsForms>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<ApplicationIcon>Ersatztv.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Ersatztv.ico">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Asmichi.ChildProcess" Version="0.11.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ErsatzTV.Core\ErsatzTV.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Program.cs">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,14 @@
|
||||
namespace ErsatzTV_Windows;
|
||||
|
||||
public static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The main entry point for the application.
|
||||
/// </summary>
|
||||
[STAThread]
|
||||
public static void Main()
|
||||
{
|
||||
ApplicationConfiguration.Initialize();
|
||||
Application.Run(new TrayApplicationContext());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using ErsatzTV.Core;
|
||||
using System.Diagnostics;
|
||||
using Asmichi.ProcessManagement;
|
||||
using System.Reflection;
|
||||
|
||||
namespace ErsatzTV_Windows;
|
||||
|
||||
public class TrayApplicationContext : ApplicationContext
|
||||
{
|
||||
private readonly NotifyIcon _trayIcon;
|
||||
private readonly IChildProcess? _childProcess;
|
||||
|
||||
public TrayApplicationContext()
|
||||
{
|
||||
_trayIcon = new NotifyIcon
|
||||
{
|
||||
Icon = new Icon("./Ersatztv.ico"),
|
||||
ContextMenuStrip = new ContextMenuStrip(),
|
||||
Visible = true
|
||||
};
|
||||
|
||||
AddMenuItem("Launch Web UI", LaunchWebUI);
|
||||
AddMenuItem("Show Logs", ShowLogs);
|
||||
_trayIcon.ContextMenuStrip.Items.Add(new ToolStripSeparator());
|
||||
AddMenuItem("Exit", Exit);
|
||||
|
||||
string folder = AppContext.BaseDirectory;
|
||||
string exe = Path.Combine(folder, "ErsatzTV.exe");
|
||||
|
||||
if (File.Exists(exe))
|
||||
{
|
||||
var si = new ChildProcessStartInfo(exe);
|
||||
_childProcess = ChildProcess.Start(si);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddMenuItem(string name, EventHandler action)
|
||||
{
|
||||
var item = new ToolStripMenuItem(name);
|
||||
item.Click += action;
|
||||
_trayIcon.ContextMenuStrip.Items.Add(item);
|
||||
}
|
||||
|
||||
private void LaunchWebUI(object? sender, EventArgs e)
|
||||
{
|
||||
var process = new Process();
|
||||
process.StartInfo.UseShellExecute = true;
|
||||
process.StartInfo.FileName = "http://localhost:8409";
|
||||
process.Start();
|
||||
}
|
||||
|
||||
private void ShowLogs(object? sender, EventArgs e)
|
||||
{
|
||||
if (!Directory.Exists(FileSystemLayout.LogsFolder))
|
||||
{
|
||||
Directory.CreateDirectory(FileSystemLayout.LogsFolder);
|
||||
}
|
||||
|
||||
var process = new Process();
|
||||
process.StartInfo.UseShellExecute = true;
|
||||
process.StartInfo.FileName = FileSystemLayout.LogsFolder;
|
||||
process.Start();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
_childProcess?.Dispose();
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
private void Exit(object? sender, EventArgs e)
|
||||
{
|
||||
// Hide tray icon, otherwise it will remain shown until user mouses over it
|
||||
_trayIcon.Visible = false;
|
||||
Application.Exit();
|
||||
}
|
||||
}
|
||||
Submodule
+1
Submodule ErsatzTV-macOS added at 2f3ee16f11
@@ -36,10 +36,14 @@ namespace ErsatzTV.Application.Channels.Queries
|
||||
channel.StreamingMode = StreamingMode.HttpLiveStreamingDirect;
|
||||
result.Add(channel);
|
||||
break;
|
||||
case "ts":
|
||||
case "ts-legacy":
|
||||
channel.StreamingMode = StreamingMode.TransportStream;
|
||||
result.Add(channel);
|
||||
break;
|
||||
case "ts":
|
||||
channel.StreamingMode = StreamingMode.TransportStreamHybrid;
|
||||
result.Add(channel);
|
||||
break;
|
||||
default:
|
||||
result.Add(channel);
|
||||
break;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Timers;
|
||||
@@ -132,12 +133,16 @@ namespace ErsatzTV.Application.Streaming
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMediator mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
|
||||
|
||||
long ptsOffset = await GetPtsOffset(mediator, channelNumber, cancellationToken);
|
||||
// _logger.LogInformation("PTS offset: {PtsOffset}", ptsOffset);
|
||||
|
||||
var request = new GetPlayoutItemProcessByChannelNumber(
|
||||
channelNumber,
|
||||
"segmenter",
|
||||
firstProcess ? DateTimeOffset.Now : _transcodedUntil.AddSeconds(1),
|
||||
!firstProcess,
|
||||
realtime);
|
||||
realtime,
|
||||
ptsOffset);
|
||||
|
||||
// _logger.LogInformation("Request {@Request}", request);
|
||||
|
||||
@@ -231,6 +236,28 @@ namespace ErsatzTV.Application.Streaming
|
||||
_playlistStart = trimResult.PlaylistStart;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<long> GetPtsOffset(IMediator mediator, string channelNumber, CancellationToken cancellationToken)
|
||||
{
|
||||
var directory = new DirectoryInfo(Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber));
|
||||
Option<FileInfo> lastSegment =
|
||||
Optional(directory.GetFiles("*.ts").OrderByDescending(f => f.Name).FirstOrDefault());
|
||||
|
||||
long result = 0;
|
||||
foreach (FileInfo segment in lastSegment)
|
||||
{
|
||||
Either<BaseError, PtsAndDuration> queryResult = await mediator.Send(
|
||||
new GetLastPtsDuration(segment.FullName),
|
||||
cancellationToken);
|
||||
|
||||
foreach (PtsAndDuration ptsAndDuration in queryResult.RightToSeq())
|
||||
{
|
||||
result = ptsAndDuration.Pts + ptsAndDuration.Duration;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async Task<int> GetWorkAheadLimit()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace ErsatzTV.Application.Streaming;
|
||||
|
||||
public record PtsAndDuration(long Pts, long Duration)
|
||||
{
|
||||
public static PtsAndDuration From(string ffprobeLine)
|
||||
{
|
||||
string[] split = ffprobeLine.Split("|");
|
||||
var left = long.Parse(split[0]);
|
||||
var right = long.Parse(split[1]);
|
||||
return new PtsAndDuration(left, right);
|
||||
}
|
||||
}
|
||||
@@ -11,5 +11,6 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
string Mode,
|
||||
DateTimeOffset Now,
|
||||
bool StartAtZero,
|
||||
bool HlsRealtime) : IRequest<Either<BaseError, PlayoutItemProcessModel>>;
|
||||
bool HlsRealtime,
|
||||
long PtsOffset) : IRequest<Either<BaseError, PlayoutItemProcessModel>>;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
"ts-legacy",
|
||||
DateTimeOffset.Now,
|
||||
false,
|
||||
true)
|
||||
true,
|
||||
0)
|
||||
{
|
||||
Scheme = scheme;
|
||||
Host = host;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
using ErsatzTV.Core;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries;
|
||||
|
||||
public record GetLastPtsDuration(string FileName) : IRequest<Either<BaseError, PtsAndDuration>>;
|
||||
@@ -0,0 +1,87 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using ErsatzTV.Core;
|
||||
using ErsatzTV.Core.Domain;
|
||||
using ErsatzTV.Core.Interfaces.Repositories;
|
||||
using LanguageExt;
|
||||
using MediatR;
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries;
|
||||
|
||||
public class GetLastPtsDurationHandler : IRequestHandler<GetLastPtsDuration, Either<BaseError, PtsAndDuration>>
|
||||
{
|
||||
private readonly IConfigElementRepository _configElementRepository;
|
||||
|
||||
public GetLastPtsDurationHandler(IConfigElementRepository configElementRepository)
|
||||
{
|
||||
_configElementRepository = configElementRepository;
|
||||
}
|
||||
|
||||
public async Task<Either<BaseError, PtsAndDuration>> Handle(
|
||||
GetLastPtsDuration request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
Validation<BaseError, RequestParameters> validation = await Validate(request);
|
||||
return await validation.Match(
|
||||
Handle,
|
||||
error => Task.FromResult<Either<BaseError, PtsAndDuration>>(error.Join()));
|
||||
}
|
||||
|
||||
private async Task<Validation<BaseError, RequestParameters>> Validate(GetLastPtsDuration request) =>
|
||||
await ValidateFFprobePath()
|
||||
.MapT(
|
||||
ffprobePath => new RequestParameters(
|
||||
request.FileName,
|
||||
ffprobePath));
|
||||
|
||||
private async Task<Either<BaseError, PtsAndDuration>> Handle(RequestParameters parameters)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = parameters.FFprobePath,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
StandardOutputEncoding = Encoding.UTF8,
|
||||
StandardErrorEncoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
startInfo.ArgumentList.Add("-v");
|
||||
startInfo.ArgumentList.Add("0");
|
||||
startInfo.ArgumentList.Add("-show_entries");
|
||||
startInfo.ArgumentList.Add("packet=pts,duration");
|
||||
startInfo.ArgumentList.Add("-of");
|
||||
startInfo.ArgumentList.Add("compact=p=0:nk=1");
|
||||
startInfo.ArgumentList.Add("-read_intervals");
|
||||
startInfo.ArgumentList.Add("-999999");
|
||||
startInfo.ArgumentList.Add(parameters.FileName);
|
||||
|
||||
var probe = new Process
|
||||
{
|
||||
StartInfo = startInfo
|
||||
};
|
||||
|
||||
probe.Start();
|
||||
return await probe.StandardOutput.ReadToEndAsync().MapAsync<string, Either<BaseError, PtsAndDuration>>(
|
||||
async output =>
|
||||
{
|
||||
await probe.WaitForExitAsync();
|
||||
return probe.ExitCode == 0
|
||||
? PtsAndDuration.From(output.Split("\n").Filter(s => !string.IsNullOrWhiteSpace(s)).Last().Trim())
|
||||
: BaseError.New($"FFprobe at {parameters.FFprobePath} exited with code {probe.ExitCode}");
|
||||
});
|
||||
}
|
||||
|
||||
private Task<Validation<BaseError, string>> ValidateFFprobePath() =>
|
||||
_configElementRepository.GetValue<string>(ConfigElementKey.FFprobePath)
|
||||
.FilterT(File.Exists)
|
||||
.Map(
|
||||
ffprobePath =>
|
||||
ffprobePath.ToValidation<BaseError>("FFprobe path does not exist on the file system"));
|
||||
|
||||
private record RequestParameters(string FileName, string FFprobePath);
|
||||
}
|
||||
@@ -2,20 +2,15 @@
|
||||
|
||||
namespace ErsatzTV.Application.Streaming.Queries
|
||||
{
|
||||
public record GetPlayoutItemProcessByChannelNumber : FFmpegProcessRequest
|
||||
{
|
||||
public GetPlayoutItemProcessByChannelNumber(
|
||||
string channelNumber,
|
||||
string mode,
|
||||
DateTimeOffset now,
|
||||
bool startAtZero,
|
||||
bool hlsRealtime) : base(
|
||||
channelNumber,
|
||||
mode,
|
||||
now,
|
||||
startAtZero,
|
||||
hlsRealtime)
|
||||
{
|
||||
}
|
||||
}
|
||||
public record GetPlayoutItemProcessByChannelNumber(string ChannelNumber,
|
||||
string Mode,
|
||||
DateTimeOffset Now,
|
||||
bool StartAtZero,
|
||||
bool HlsRealtime,
|
||||
long PtsOffset) : FFmpegProcessRequest(ChannelNumber,
|
||||
Mode,
|
||||
Now,
|
||||
StartAtZero,
|
||||
HlsRealtime,
|
||||
PtsOffset);
|
||||
}
|
||||
|
||||
+8
-4
@@ -158,7 +158,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
request.HlsRealtime,
|
||||
playoutItemWithPath.PlayoutItem.FillerKind,
|
||||
playoutItemWithPath.PlayoutItem.InPoint,
|
||||
playoutItemWithPath.PlayoutItem.OutPoint);
|
||||
playoutItemWithPath.PlayoutItem.OutPoint,
|
||||
request.PtsOffset);
|
||||
|
||||
var result = new PlayoutItemProcessModel(process, playoutItemWithPath.PlayoutItem.FinishOffset);
|
||||
|
||||
@@ -193,7 +194,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
channel,
|
||||
maybeDuration,
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime);
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset);
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
}
|
||||
@@ -212,7 +214,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
channel,
|
||||
maybeDuration,
|
||||
error.Value,
|
||||
request.HlsRealtime);
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset);
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
}
|
||||
@@ -231,7 +234,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
channel,
|
||||
maybeDuration,
|
||||
"Channel is Offline",
|
||||
request.HlsRealtime);
|
||||
request.HlsRealtime,
|
||||
request.PtsOffset);
|
||||
|
||||
return new PlayoutItemProcessModel(errorProcess, finish);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ namespace ErsatzTV.Application.Streaming.Queries
|
||||
"ts",
|
||||
DateTimeOffset.Now,
|
||||
false,
|
||||
true)
|
||||
true,
|
||||
0)
|
||||
{
|
||||
Scheme = scheme;
|
||||
Host = host;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.3.0" />
|
||||
<PackageReference Include="FluentAssertions" Version="6.4.0" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||
@@ -16,7 +16,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Moq" Version="4.16.1" />
|
||||
<PackageReference Include="NUnit" Version="3.13.2" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.0" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||
|
||||
@@ -16,6 +16,26 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
private readonly FFmpegPlaybackSettingsCalculator _calculator;
|
||||
|
||||
public CalculateSettings() => _calculator = new FFmpegPlaybackSettingsCalculator();
|
||||
|
||||
[Test]
|
||||
public void Should_Not_GenPts_ForHlsSegmenter()
|
||||
{
|
||||
FFmpegProfile ffmpegProfile = TestProfile();
|
||||
|
||||
FFmpegPlaybackSettings actual = _calculator.CalculateSettings(
|
||||
StreamingMode.HttpLiveStreamingSegmenter,
|
||||
ffmpegProfile,
|
||||
new MediaVersion(),
|
||||
new MediaStream(),
|
||||
new MediaStream(),
|
||||
DateTimeOffset.Now,
|
||||
DateTimeOffset.Now,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.Zero,
|
||||
false);
|
||||
|
||||
actual.FormatFlags.Should().NotContain("+genpts");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Should_Not_UseSpecifiedThreadCount_ForTransportStream()
|
||||
|
||||
@@ -214,7 +214,8 @@ namespace ErsatzTV.Core.Tests.FFmpeg
|
||||
false,
|
||||
FillerKind.None,
|
||||
TimeSpan.Zero,
|
||||
TimeSpan.FromSeconds(5));
|
||||
TimeSpan.FromSeconds(5),
|
||||
0);
|
||||
|
||||
process.StartInfo.RedirectStandardError = true;
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Flurl" Version="3.0.2" />
|
||||
<PackageReference Include="Flurl" Version="3.0.4" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="MediatR" Version="9.0.0" />
|
||||
<PackageReference Include="MediatR" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.0.64">
|
||||
|
||||
@@ -36,6 +36,12 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
"+igndts"
|
||||
};
|
||||
|
||||
private static readonly List<string> SegmenterFormatFlags = new()
|
||||
{
|
||||
"+discardcorrupt",
|
||||
"+igndts"
|
||||
};
|
||||
|
||||
public FFmpegPlaybackSettings ConcatSettings => new()
|
||||
{
|
||||
ThreadCount = 1,
|
||||
@@ -56,7 +62,11 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
var result = new FFmpegPlaybackSettings
|
||||
{
|
||||
FormatFlags = CommonFormatFlags,
|
||||
FormatFlags = streamingMode switch
|
||||
{
|
||||
StreamingMode.HttpLiveStreamingSegmenter => SegmenterFormatFlags,
|
||||
_ => CommonFormatFlags,
|
||||
},
|
||||
RealtimeOutput = streamingMode switch
|
||||
{
|
||||
StreamingMode.HttpLiveStreamingSegmenter => hlsRealtime,
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -387,7 +388,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
return this;
|
||||
}
|
||||
|
||||
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion)
|
||||
public FFmpegProcessBuilder WithHls(string channelNumber, Option<MediaVersion> mediaVersion, long ptsOffset, Option<int> maybeTimeScale)
|
||||
{
|
||||
const int SEGMENT_SECONDS = 4;
|
||||
|
||||
@@ -412,10 +413,15 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
frameRate = fr;
|
||||
}
|
||||
|
||||
foreach (int timescale in maybeTimeScale)
|
||||
{
|
||||
_arguments.Add("-output_ts_offset");
|
||||
_arguments.Add($"{(ptsOffset / (double)timescale).ToString(NumberFormatInfo.InvariantInfo)}");
|
||||
}
|
||||
|
||||
_arguments.AddRange(
|
||||
new[]
|
||||
{
|
||||
"-use_wallclock_as_timestamps", "1",
|
||||
"-g", $"{frameRate * SEGMENT_SECONDS}",
|
||||
"-keyint_min", $"{frameRate * SEGMENT_SECONDS}",
|
||||
"-force_key_frames", $"expr:gte(t,n_forced*{SEGMENT_SECONDS})",
|
||||
@@ -425,7 +431,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
"-segment_list_flags", "+live",
|
||||
"-hls_segment_filename",
|
||||
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live%06d.ts"),
|
||||
"-hls_flags", "program_date_time+append_list+omit_endlist+independent_segments",
|
||||
"-hls_flags", "program_date_time+append_list+discont_start+omit_endlist+independent_segments",
|
||||
"-mpegts_flags", "+initial_discontinuity",
|
||||
Path.Combine(FileSystemLayout.TranscodeFolder, channelNumber, "live.m3u8")
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
bool hlsRealtime,
|
||||
FillerKind fillerKind,
|
||||
TimeSpan inPoint,
|
||||
TimeSpan outPoint)
|
||||
TimeSpan outPoint,
|
||||
long ptsOffset)
|
||||
{
|
||||
MediaStream videoStream = await _ffmpegStreamSelector.SelectVideoStream(channel, videoVersion);
|
||||
Option<MediaStream> maybeAudioStream = await _ffmpegStreamSelector.SelectAudioStream(channel, audioVersion);
|
||||
@@ -158,7 +159,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
// HLS needs to segment and generate playlist
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
return builder.WithHls(channel.Number, videoVersion)
|
||||
return builder.WithHls(channel.Number, videoVersion, ptsOffset, playbackSettings.VideoTrackTimeScale)
|
||||
.Build();
|
||||
default:
|
||||
return builder.WithFormat("mpegts")
|
||||
@@ -173,7 +174,8 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
string errorMessage,
|
||||
bool hlsRealtime)
|
||||
bool hlsRealtime,
|
||||
long ptsOffset)
|
||||
{
|
||||
FFmpegPlaybackSettings playbackSettings =
|
||||
_playbackSettingsCalculator.CalculateErrorSettings(channel.FFmpegProfile);
|
||||
@@ -221,7 +223,7 @@ namespace ErsatzTV.Core.FFmpeg
|
||||
{
|
||||
// HLS needs to segment and generate playlist
|
||||
case StreamingMode.HttpLiveStreamingSegmenter:
|
||||
return builder.WithHls(channel.Number, None)
|
||||
return builder.WithHls(channel.Number, None, ptsOffset, playbackSettings.VideoTrackTimeScale)
|
||||
.Build();
|
||||
default:
|
||||
return builder.WithFormat("mpegts")
|
||||
|
||||
@@ -18,9 +18,12 @@ namespace ErsatzTV.Core
|
||||
Environment.SpecialFolderOption.Create),
|
||||
"etv-transcode");
|
||||
|
||||
public static readonly string LogsFolder = Path.Combine(AppDataFolder, "logs");
|
||||
|
||||
public static readonly string DatabasePath = Path.Combine(AppDataFolder, "ersatztv.sqlite3");
|
||||
|
||||
public static readonly string LogDatabasePath = Path.Combine(AppDataFolder, "logs.sqlite3");
|
||||
public static readonly string LogFilePath = Path.Combine(LogsFolder, "ersatztv.log");
|
||||
|
||||
public static readonly string LegacyImageCacheFolder = Path.Combine(AppDataFolder, "cache", "images");
|
||||
public static readonly string ResourcesCacheFolder = Path.Combine(AppDataFolder, "cache", "resources");
|
||||
|
||||
@@ -27,14 +27,16 @@ namespace ErsatzTV.Core.Interfaces.FFmpeg
|
||||
bool hlsRealtime,
|
||||
FillerKind fillerKind,
|
||||
TimeSpan inPoint,
|
||||
TimeSpan outPoint);
|
||||
TimeSpan outPoint,
|
||||
long ptsOffset);
|
||||
|
||||
Task<Process> ForError(
|
||||
string ffmpegPath,
|
||||
Channel channel,
|
||||
Option<TimeSpan> duration,
|
||||
string errorMessage,
|
||||
bool hlsRealtime);
|
||||
bool hlsRealtime,
|
||||
long ptsOffset);
|
||||
|
||||
Process ConcatChannel(string ffmpegPath, bool saveReports, Channel channel, string scheme, string host);
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Refit" Version="6.1.15" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.1.15" />
|
||||
<PackageReference Include="Refit" Version="6.2.16" />
|
||||
<PackageReference Include="Refit.Newtonsoft.Json" Version="6.2.16" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ public class FileNotFoundHealthCheck : BaseHealthCheck, IFileNotFoundHealthCheck
|
||||
|
||||
return WarningResult(
|
||||
$"There are {all.Count} files that do not exist on disk, including the following: {files}",
|
||||
$"/search?query=state%3AFileNotFound");
|
||||
"/media/trash");
|
||||
}
|
||||
|
||||
return OkResult();
|
||||
|
||||
@@ -60,6 +60,21 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
return base.GetRangeQuery("release_date", "00000000", dateString, false, false);
|
||||
}
|
||||
|
||||
if (field == "added_inthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedStart))
|
||||
{
|
||||
var todayString = DateTime.Today.ToString("yyyyMMdd");
|
||||
var dateString = addedStart.ToString("yyyyMMdd");
|
||||
|
||||
return base.GetRangeQuery("added_date", dateString, todayString, true, true);
|
||||
}
|
||||
|
||||
if (field == "added_notinthelast" && CustomQueryParser.ParseStart(queryText, out DateTime addedFinish))
|
||||
{
|
||||
var dateString = addedFinish.ToString("yyyyMMdd");
|
||||
|
||||
return base.GetRangeQuery("added_date", "00000000", dateString, false, false);
|
||||
}
|
||||
|
||||
return base.GetFieldQuery(field, queryText, slop);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,21 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
return base.GetRangeQuery("release_date", "00000000", dateString, false, false);
|
||||
}
|
||||
|
||||
if (field == "added_inthelast" && ParseStart(queryText, out DateTime addedStart))
|
||||
{
|
||||
var todayString = DateTime.Today.ToString("yyyyMMdd");
|
||||
var dateString = addedStart.ToString("yyyyMMdd");
|
||||
|
||||
return base.GetRangeQuery("added_date", dateString, todayString, true, true);
|
||||
}
|
||||
|
||||
if (field == "added_notinthelast" && ParseStart(queryText, out DateTime addedFinish))
|
||||
{
|
||||
var dateString = addedFinish.ToString("yyyyMMdd");
|
||||
|
||||
return base.GetRangeQuery("added_date", "00000000", dateString, false, false);
|
||||
}
|
||||
|
||||
return base.GetFieldQuery(field, queryText, slop);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
private const string TitleAndYearField = "title_and_year";
|
||||
private const string JumpLetterField = "jump_letter";
|
||||
private const string ReleaseDateField = "release_date";
|
||||
private const string AddedDateField = "added_date";
|
||||
private const string StudioField = "studio";
|
||||
private const string LanguageField = "language";
|
||||
private const string StyleField = "style";
|
||||
@@ -80,7 +81,7 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
_initialized = false;
|
||||
}
|
||||
|
||||
public int Version => 19;
|
||||
public int Version => 20;
|
||||
|
||||
public Task<bool> Initialize(ILocalFileSystem localFileSystem)
|
||||
{
|
||||
@@ -342,6 +343,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Plot))
|
||||
{
|
||||
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
|
||||
@@ -468,6 +471,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Plot))
|
||||
{
|
||||
doc.Add(new TextField(PlotField, metadata.Plot ?? string.Empty, Field.Store.NO));
|
||||
@@ -558,6 +563,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
foreach (TraktListItem item in season.TraktListItems)
|
||||
{
|
||||
doc.Add(new StringField(TraktListField, item.TraktList.TraktId.ToString(), Field.Store.NO));
|
||||
@@ -597,6 +604,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
List<string> languages = await searchRepository.GetLanguagesForArtist(artist);
|
||||
await AddLanguages(searchRepository, doc, languages);
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
foreach (Genre genre in metadata.Genres)
|
||||
{
|
||||
doc.Add(new TextField(GenreField, genre.Name, Field.Store.NO));
|
||||
@@ -659,6 +668,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Album))
|
||||
{
|
||||
@@ -748,6 +759,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
metadata.ReleaseDate.Value.ToString("yyyyMMdd"),
|
||||
Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Plot))
|
||||
{
|
||||
@@ -827,6 +840,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
{
|
||||
doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
foreach (Tag tag in metadata.Tags)
|
||||
{
|
||||
@@ -871,6 +886,8 @@ namespace ErsatzTV.Infrastructure.Search
|
||||
{
|
||||
doc.Add(new Int32Field(MinutesField, (int)Math.Ceiling(version.Duration.TotalMinutes), Field.Store.NO));
|
||||
}
|
||||
|
||||
doc.Add(new StringField(AddedDateField, metadata.DateAdded.ToString("yyyyMMdd"), Field.Store.NO));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(metadata.Album))
|
||||
{
|
||||
|
||||
@@ -27,14 +27,14 @@ namespace ErsatzTV.Controllers
|
||||
public Task<IActionResult> GetConcatPlaylist(string channelNumber) =>
|
||||
_mediator.Send(new GetConcatPlaylistByChannelNumber(Request.Scheme, Request.Host.ToString(), channelNumber))
|
||||
.ToActionResult();
|
||||
|
||||
|
||||
[HttpGet("ffmpeg/stream/{channelNumber}")]
|
||||
public Task<IActionResult> GetStream(
|
||||
string channelNumber,
|
||||
[FromQuery]
|
||||
string mode = "mixed") =>
|
||||
_mediator.Send(
|
||||
new GetPlayoutItemProcessByChannelNumber(channelNumber, mode, DateTimeOffset.Now, false, true))
|
||||
new GetPlayoutItemProcessByChannelNumber(channelNumber, mode, DateTimeOffset.Now, false, true, 0))
|
||||
.Map(
|
||||
result =>
|
||||
result.Match<IActionResult>(
|
||||
|
||||
@@ -8,18 +8,17 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Resources" />
|
||||
<Folder Include="wwwroot\css\bootstrap" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.1.5" />
|
||||
<PackageReference Include="FluentValidation" Version="10.3.6" />
|
||||
<PackageReference Include="FluentValidation.AspNetCore" Version="10.3.6" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="6.0.453" />
|
||||
<PackageReference Include="HtmlSanitizer" Version="7.1.475" />
|
||||
<PackageReference Include="LanguageExt.Core" Version="4.0.3" />
|
||||
<PackageReference Include="Markdig" Version="0.26.0" />
|
||||
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="4.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
|
||||
<PackageReference Include="Markdig" Version="0.27.0" />
|
||||
<PackageReference Include="MediatR.Courier.DependencyInjection" Version="5.0.0" />
|
||||
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
@@ -29,10 +28,10 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="MudBlazor" Version="6.0.4" />
|
||||
<PackageReference Include="MudBlazor" Version="6.0.5" />
|
||||
<PackageReference Include="NaturalSort.Extension" Version="3.2.0" />
|
||||
<PackageReference Include="PPioli.FluentValidation.Blazor" Version="5.0.0" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.1.15" />
|
||||
<PackageReference Include="Refit.HttpClientFactory" Version="6.2.16" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="4.1.0" />
|
||||
<PackageReference Include="Serilog.Settings.Configuration" Version="3.3.0" />
|
||||
|
||||
+12
-11
@@ -11,23 +11,22 @@ namespace ErsatzTV
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
private static readonly string BasePath;
|
||||
|
||||
static Program()
|
||||
{
|
||||
var executablePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
|
||||
var executable = Path.GetFileNameWithoutExtension(executablePath);
|
||||
string executablePath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName;
|
||||
string executable = Path.GetFileNameWithoutExtension(executablePath);
|
||||
|
||||
IConfigurationBuilder builder = new ConfigurationBuilder();
|
||||
|
||||
if ("dotnet".Equals(executable, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
builder = builder.SetBasePath(Path.GetDirectoryName(typeof(Program).Assembly.Location));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder = builder.SetBasePath(Path.GetDirectoryName(executablePath));
|
||||
}
|
||||
BasePath = Path.GetDirectoryName(
|
||||
"dotnet".Equals(executable, StringComparison.InvariantCultureIgnoreCase)
|
||||
? typeof(Program).Assembly.Location
|
||||
: executablePath);
|
||||
|
||||
Configuration = builder
|
||||
.SetBasePath(BasePath)
|
||||
.AddJsonFile("appsettings.json", false, true)
|
||||
.AddJsonFile(
|
||||
$"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json",
|
||||
@@ -44,6 +43,7 @@ namespace ErsatzTV
|
||||
.ReadFrom.Configuration(Configuration)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SQLite(FileSystemLayout.LogDatabasePath, retentionPeriod: TimeSpan.FromDays(1))
|
||||
.WriteTo.File(FileSystemLayout.LogFilePath, rollingInterval: RollingInterval.Day)
|
||||
.CreateLogger();
|
||||
|
||||
try
|
||||
@@ -67,7 +67,8 @@ namespace ErsatzTV
|
||||
.ConfigureWebHostDefaults(
|
||||
webBuilder => webBuilder.UseStartup<Startup>()
|
||||
.UseConfiguration(Configuration)
|
||||
.UseKestrel(options => options.AddServerHeader = false))
|
||||
.UseKestrel(options => options.AddServerHeader = false)
|
||||
.UseContentRoot(BasePath))
|
||||
.UseSerilog();
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="svg8" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 500 500"
|
||||
style="enable-background:new 0 0 500 500;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#C0C000;}
|
||||
.st1{font-family:'Roboto-Bold';}
|
||||
.st2{font-size:144.6417px;}
|
||||
.st3{fill:#00C0C0;}
|
||||
.st4{font-size:312.4454px;}
|
||||
.st5{fill:#00C000;}
|
||||
</style>
|
||||
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" fit-margin-bottom="0" fit-margin-left="0" fit-margin-right="0" fit-margin-top="0" id="base" inkscape:current-layer="layer1" inkscape:cx="199.98633" inkscape:cy="92.251514" inkscape:document-rotation="0" inkscape:document-units="mm" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="1364" inkscape:window-maximized="1" inkscape:window-width="3408" inkscape:window-x="14" inkscape:window-y="36" inkscape:zoom="0.49497475" pagecolor="#ffffff" showgrid="false">
|
||||
</sodipodi:namedview>
|
||||
<g>
|
||||
<text transform="matrix(1 0 0 1 47.5005 181.2363)" class="st0 st1 st2">Ersatz</text>
|
||||
<text transform="matrix(1 0 0 1 53.6772 427.7119)"><tspan x="0" y="0" class="st3 st1 st4">T</tspan><tspan x="193.3" y="0" class="st5 st1 st4">V</tspan></text>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
IFS='.' read -ra ADDR <<< "$1"
|
||||
ICONSET=${ADDR[0]}.iconset
|
||||
mkdir $ICONSET
|
||||
sips -z 16 16 $1 --out $ICONSET/icon_16x16.png
|
||||
sips -z 32 32 $1 --out $ICONSET/icon_16x16@2x.png
|
||||
sips -z 32 32 $1 --out $ICONSET/icon_32x32.png
|
||||
sips -z 64 64 $1 --out $ICONSET/icon_32x32@2x.png
|
||||
sips -z 128 128 $1 --out $ICONSET/icon_128x128.png
|
||||
sips -z 256 256 $1 --out $ICONSET/icon_128x128@2x.png
|
||||
sips -z 256 256 $1 --out $ICONSET/icon_256x256.png
|
||||
sips -z 512 512 $1 --out $ICONSET/icon_256x256@2x.png
|
||||
sips -z 512 512 $1 --out $ICONSET/icon_512x512.png
|
||||
cp $1 $ICONSET/icon_512x512@2x.png
|
||||
iconutil -c icns $ICONSET
|
||||
rm -R $ICONSET
|
||||
|
||||
@@ -93,17 +93,17 @@ As with the docker development tags, the development builds are updated with eve
|
||||
|
||||
1. Create a folder `ersatztv` at your preferred install location.
|
||||
2. Download and extract the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases) to the `ersatztv` folder.
|
||||
3. Run `ErsatzTV.exe`
|
||||
4. Open your browser to `http://[server-ip]:8409`
|
||||
3. Run `ErsatzTV-Windows.exe`
|
||||
4. Use the tray menu to open the UI, view logs or exit
|
||||
|
||||
## MacOS
|
||||
|
||||
### Manual Installation
|
||||
|
||||
1. Create a folder `ersatztv` at your preferred install location.
|
||||
2. Download and extract the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases) to the `ersatztv` folder.
|
||||
3. Run `ErsatzTV`
|
||||
4. Open your browser to `http://[server-ip]:8409`
|
||||
1. Download the .dmg from the [latest release from GitHub](https://github.com/jasongdove/ErsatzTV/releases)
|
||||
2. Open the .dmg and drag `ErsatzTV` to the `Applications` folder
|
||||
3. Double-click the `ErsatzTV` application
|
||||
4. Use the tray menu to open the UI, view logs or exit
|
||||
|
||||
## Linux
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ The following fields are available for searching movies:
|
||||
- `content_rating`: The movie content rating (case-sensitive)
|
||||
- `language`: The movie audio stream language
|
||||
- `release_date`: The movie release date (YYYYMMDD)
|
||||
- `added_date`: The date the movie was added to ErsatzTV (YYYYMMDD)
|
||||
- `minutes`: the rounded-up whole number duration of the movie in minutes
|
||||
- `type`: Always `movie`
|
||||
|
||||
@@ -41,6 +42,7 @@ The following fields are available for searching shows:
|
||||
- `content_rating`: The show content rating (case-sensitive)
|
||||
- `language`: The show audio stream language
|
||||
- `release_date`: The show release date (YYYYMMDD)
|
||||
- `added_date`: The date the show was added to ErsatzTV (YYYYMMDD)
|
||||
- `type`: Always `show`
|
||||
|
||||
### Episodes
|
||||
@@ -54,6 +56,7 @@ The following fields are available for searching episodes:
|
||||
- `library_name`: The name of the library that contains the episode
|
||||
- `language`: The episode audio stream language
|
||||
- `release_date`: The episode release date (YYYYMMDD)
|
||||
- `added_date`: The date the episode was added to ErsatzTV (YYYYMMDD)
|
||||
- `minutes`: the rounded-up whole number duration of the episode in minutes
|
||||
- `type`: Always `episode`
|
||||
|
||||
@@ -65,6 +68,7 @@ The following fields are available for searching artists:
|
||||
- `genre`: The artist genre
|
||||
- `style`: The artist style
|
||||
- `mood`: The artist mood
|
||||
- `added_date`: The date the artist was added to ErsatzTV (YYYYMMDD)
|
||||
- `library_name`: The name of the library that contains the artist
|
||||
- `type`: Always `artist`
|
||||
|
||||
@@ -79,6 +83,7 @@ The following fields are available for searching music videos:
|
||||
- `library_name`: The name of the library that contains the music video
|
||||
- `language`: The music video audio stream language
|
||||
- `release_date`: The music video release date (YYYYMMDD)
|
||||
- `added_date`: The date the music video was added to ErsatzTV (YYYYMMDD)
|
||||
- `minutes`: the rounded-up whole number duration of the music video in minutes
|
||||
- `type`: Always `music_video`
|
||||
|
||||
@@ -89,6 +94,7 @@ The following fields are available for searching other videos:
|
||||
- `title`: The filename of the video (without extension)
|
||||
- `tag`: All of the video's parent folders
|
||||
- `minutes`: the rounded-up whole number duration of the video in minutes
|
||||
- `added_date`: The date the other video was added to ErsatzTV (YYYYMMDD)
|
||||
- `type`: Always `other_video`
|
||||
|
||||
### Songs
|
||||
@@ -101,6 +107,7 @@ The following fields are available for searching songs:
|
||||
- `genre`: The song genre
|
||||
- `tag`: All of the song's parent folders
|
||||
- `minutes`: the rounded-up whole number duration of the song in minutes
|
||||
- `added_date`: The date the song was added to ErsatzTV (YYYYMMDD)
|
||||
- `type`: Always `song`
|
||||
|
||||
## Special Search Fields
|
||||
@@ -108,6 +115,8 @@ The following fields are available for searching songs:
|
||||
- `released_inthelast`: For any media type that supports `release_date`, `released_inthelast` takes a number and a unit (days, weeks, months, years) and returns items released between the specified time ago and now
|
||||
- `released_notinthelast`: For any media type that supports `release_date`, `released_notinthelast` takes a number and a unit (days, weeks, months, years) and returns items released before the specified time ago
|
||||
- `released_onthisday`: For any media type that supports `release_date`, `released_onthisday` takes any value (ignored) and will return items released on this month number and day number in previous years
|
||||
- `added_inthelast`: For any media type that supports `added_date`, `added_inthelast` takes a number and a unit (days, weeks, months, years) and returns items added to ErsatzTV between the specified time ago and now
|
||||
- `added_notinthelast`: For any media type that supports `added_date`, `added_notinthelast` takes a number and a unit (days, weeks, months, years) and returns items added to ErsatzTV before the specified time ago
|
||||
|
||||
## Sample Searches
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>ErsatzTV</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>launcher.sh</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>ErsatzTV.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.jasongdove.ersatztv</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>ErsatzTV</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.7-alpha</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.3.7-alpha</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+5
-12
@@ -1,26 +1,19 @@
|
||||
#! /bin/bash
|
||||
|
||||
SCRIPT_FOLDER=$(dirname ${BASH_SOURCE[0]})
|
||||
REPO_ROOT="$SCRIPT_FOLDER/../.."
|
||||
REPO_ROOT=$(realpath "$SCRIPT_FOLDER/../..")
|
||||
|
||||
APP_NAME="$REPO_ROOT/ErsatzTV.app"
|
||||
PUBLISH_OUTPUT_DIRECTORY="$REPO_ROOT/publish/."
|
||||
INFO_PLIST="$SCRIPT_FOLDER/Info.plist"
|
||||
ICON_SOURCE="$REPO_ROOT/artwork/ErsatzTV.icns"
|
||||
ICON_FILE="ErsatzTV.icns"
|
||||
|
||||
if [ -d "$APP_NAME" ]
|
||||
then
|
||||
rm -rf "$APP_NAME"
|
||||
fi
|
||||
|
||||
mkdir "$APP_NAME"
|
||||
cd "$REPO_ROOT/ErsatzTV-macOS" || exit
|
||||
xcodebuild build
|
||||
cd "$REPO_ROOT" || exit
|
||||
cp -R "$REPO_ROOT/ErsatzTV-macOS/build/Release/ErsatzTV-macOS.app" "$APP_NAME"
|
||||
|
||||
mkdir "$APP_NAME/Contents"
|
||||
mkdir "$APP_NAME/Contents/MacOS"
|
||||
mkdir "$APP_NAME/Contents/Resources"
|
||||
|
||||
cp "$INFO_PLIST" "$APP_NAME/Contents/Info.plist"
|
||||
cp "$ICON_SOURCE" "$APP_NAME/Contents/Resources/$ICON_FILE"
|
||||
cp -a "$PUBLISH_OUTPUT_DIRECTORY" "$APP_NAME/Contents/MacOS"
|
||||
cp "$SCRIPT_FOLDER/launcher.sh" "$APP_NAME/Contents/MacOS/"
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#! /bin/sh
|
||||
|
||||
# Set the working directory
|
||||
DIR=$(cd "$(dirname "$0")"; pwd)
|
||||
|
||||
# Run the application
|
||||
echo "running from $DIR"
|
||||
|
||||
open -a Terminal $DIR/ErsatzTV
|
||||
Reference in New Issue
Block a user