diff --git a/Dockerfile.base b/Dockerfile.base index 0015fd0d0..47c640fae 100644 --- a/Dockerfile.base +++ b/Dockerfile.base @@ -1,7 +1,8 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/aspnet:8.0 AS base ARG TARGETPLATFORM ARG TARGETARCH ARG BUILDPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} RUN apt-get update -yq \ && apt-get upgrade -yq \ diff --git a/Dockerfile.build b/Dockerfile.build index 422cb4079..f9f989a4c 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -1,7 +1,8 @@ -FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build +FROM --platform=$TARGETPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build ARG TARGETPLATFORM ARG TARGETARCH ARG BUILDPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} RUN apt-get update -yq \ && apt-get upgrade -yq diff --git a/Dockerfile.sm.template b/Dockerfile.sm.template index 859f8fce8..9be6cdfae 100644 --- a/Dockerfile.sm.template +++ b/Dockerfile.sm.template @@ -1,6 +1,7 @@ ARG TARGETPLATFORM ARG TARGETARCH ARG BUILDPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} WORKDIR /src COPY . . diff --git a/Dockerfile.template b/Dockerfile.template index 1ebea74e7..9cafe088c 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -1,7 +1,8 @@ ARG TARGETPLATFORM -ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} ARG TARGETARCH ARG BUILDPLATFORM +ENV TARGETPLATFORM=${TARGETPLATFORM:-linux/amd64} + LABEL org.opencontainers.image.url="https://hub.docker.com/r/SenexCrenshaw/streammaster/" \ org.opencontainers.image.source="https://github.com/SenexCrenshaw/StreamMaster" \ org.opencontainers.image.vendor="SenexCrenshaw" \ @@ -18,9 +19,10 @@ COPY docker-entrypoint.sh docker-ensure-initdb.sh /usr/local/bin/ RUN chmod +x /usr/local/bin/docker-entrypoint.sh /usr/local/bin/docker-ensure-initdb.sh RUN ln -sT docker-ensure-initdb.sh /usr/local/bin/docker-enforce-initdb.sh +COPY backup.sh /usr/local/bin/backup.sh COPY entrypoint.sh /entrypoint.sh COPY env.sh /env.sh -RUN chmod +x /entrypoint.sh /env.sh +RUN chmod +x /entrypoint.sh /env.sh /usr/local/bin/backup.sh RUN mkdir /config EXPOSE 5432 diff --git a/StreamMaster.API/Controllers/MiscController.cs b/StreamMaster.API/Controllers/MiscController.cs index 33adf6608..0382ffd9a 100644 --- a/StreamMaster.API/Controllers/MiscController.cs +++ b/StreamMaster.API/Controllers/MiscController.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Mvc; +using StreamMaster.Domain.Common; using StreamMaster.Domain.Services; using System.Text; @@ -43,4 +44,12 @@ public IActionResult GetTestM3U(int numberOfStreams) FileDownloadName = $"m3u-test-{numberOfStreams}.m3u" }; } + + [HttpPut] + [Route("[action]")] + public async Task Backup() + { + await FileUtil.Backup(); + return Ok(); + } } \ No newline at end of file diff --git a/StreamMaster.API/Controllers/VideoStreamsController.cs b/StreamMaster.API/Controllers/VideoStreamsController.cs index cc42bf24e..9e491c3b2 100644 --- a/StreamMaster.API/Controllers/VideoStreamsController.cs +++ b/StreamMaster.API/Controllers/VideoStreamsController.cs @@ -6,6 +6,8 @@ using StreamMaster.Application.VideoStreams.Commands; using StreamMaster.Application.VideoStreams.Queries; using StreamMaster.Domain.Authentication; +using StreamMaster.Domain.Cache; +using StreamMaster.Domain.Common; using StreamMaster.Domain.Dto; using StreamMaster.Domain.Enums; using StreamMaster.Domain.Pagination; @@ -84,6 +86,15 @@ public async Task>> GetPagedVideoStre return Ok(res); } + private StreamingProxyTypes GetStreamingProxyType(VideoStreamDto videoStream) + { + Setting setting = MemoryCache.GetSetting(); + + return videoStream.StreamingProxyType == StreamingProxyTypes.SystemDefault + ? setting.StreamingProxyType + : videoStream.StreamingProxyType; + } + [Authorize(Policy = "SGLinks")] [HttpGet] [HttpHead] @@ -123,11 +134,8 @@ public async Task GetVideoStreamStream(string encodedIds, string n HttpContext.Session.Remove("ClientId"); - bool redirect = videoStream.StreamingProxyType == StreamingProxyTypes.None; - if (!redirect && videoStream.StreamingProxyType == StreamingProxyTypes.SystemDefault && Settings.StreamingProxyType == StreamingProxyTypes.None) - { - redirect = true; - } + StreamingProxyTypes proxyType = GetStreamingProxyType(videoStream); + bool redirect = proxyType == StreamingProxyTypes.None; if (redirect) { diff --git a/StreamMaster.API/Program.cs b/StreamMaster.API/Program.cs index ac412f167..a51e1475f 100644 --- a/StreamMaster.API/Program.cs +++ b/StreamMaster.API/Program.cs @@ -101,12 +101,24 @@ var lifetime = app.Services.GetService(); if (lifetime != null) { - lifetime.ApplicationStopped.Register(OnShutdown); + lifetime.ApplicationStopping.Register(OnShutdown); } void OnShutdown() { SqliteConnection.ClearAllPools(); + PGSQLRepositoryContext repositoryContext = app.Services.GetRequiredService(); + repositoryContext.Dispose(); + SQLiteRepositoryContext sQLiteRepositoryContext = app.Services.GetRequiredService(); + sQLiteRepositoryContext.Dispose(); + IImageDownloadService imageDownloadService = app.Services.GetRequiredService(); + imageDownloadService.StopAsync(CancellationToken.None).Wait(); + + FileUtil.Backup().Wait(); + //LogDbContext logDbContext = app.Services.GetRequiredService(); + //logDbContext.Dispose(); + //LogDbContextInitialiser logInitialiser = app.Services.GetRequiredService(); + //logInitialiser.Dispose(); } app.UseOpenApi(); diff --git a/StreamMaster.Application/EPGFiles/Commands/ProcessEPGFileRequest.cs b/StreamMaster.Application/EPGFiles/Commands/ProcessEPGFileRequest.cs index 7d647d81d..aa3f67b91 100644 --- a/StreamMaster.Application/EPGFiles/Commands/ProcessEPGFileRequest.cs +++ b/StreamMaster.Application/EPGFiles/Commands/ProcessEPGFileRequest.cs @@ -56,7 +56,7 @@ public class ProcessEPGFileRequestHandler(ILogger logger, } finally { - jobStatusService.SetEPGIsRunning(false); + jobStatusService.SetEPGStop(); } return null; } diff --git a/StreamMaster.Application/EPGFiles/Commands/RefreshEPGFileRequest.cs b/StreamMaster.Application/EPGFiles/Commands/RefreshEPGFileRequest.cs index 852b6e9a2..ce50803cb 100644 --- a/StreamMaster.Application/EPGFiles/Commands/RefreshEPGFileRequest.cs +++ b/StreamMaster.Application/EPGFiles/Commands/RefreshEPGFileRequest.cs @@ -26,7 +26,7 @@ public class RefreshEPGFileRequestHandler(ILogger Logger, { return null; } - jobStatusService.SetEPGIsRunning(true); + jobStatusService.SetEPGStart(); } diff --git a/StreamMaster.Application/M3UFiles/Commands/ProcessM3UFileRequest.cs b/StreamMaster.Application/M3UFiles/Commands/ProcessM3UFileRequest.cs index f2fc795d4..f1ea338e4 100644 --- a/StreamMaster.Application/M3UFiles/Commands/ProcessM3UFileRequest.cs +++ b/StreamMaster.Application/M3UFiles/Commands/ProcessM3UFileRequest.cs @@ -65,7 +65,7 @@ public class ProcessM3UFileRequestHandler(ILogger logger, } finally { - jobStatusService.SetM3UIsRunning(false); + jobStatusService.SetM3UStop(); } } diff --git a/StreamMaster.Application/M3UFiles/Commands/RefreshM3UFileRequest.cs b/StreamMaster.Application/M3UFiles/Commands/RefreshM3UFileRequest.cs index af7974002..ffb1b0bd7 100644 --- a/StreamMaster.Application/M3UFiles/Commands/RefreshM3UFileRequest.cs +++ b/StreamMaster.Application/M3UFiles/Commands/RefreshM3UFileRequest.cs @@ -26,7 +26,7 @@ public class RefreshM3UFileRequestHandler(ILogger Logger, { return null; } - jobStatusService.SetM3UIsRunning(true); + jobStatusService.SetM3UStart(); } M3UFile? m3uFile = await Repository.M3UFile.GetM3UFileById(request.Id).ConfigureAwait(false); diff --git a/StreamMaster.Application/Settings/Commands/UpdateSettingRequest.cs b/StreamMaster.Application/Settings/Commands/UpdateSettingRequest.cs index cc5e034ed..f62c47327 100644 --- a/StreamMaster.Application/Settings/Commands/UpdateSettingRequest.cs +++ b/StreamMaster.Application/Settings/Commands/UpdateSettingRequest.cs @@ -7,6 +7,9 @@ namespace StreamMaster.Application.Settings.Commands; public class UpdateSettingRequest : IRequest { + public bool? BackupEnabled { get; set; } + public int? BackupVersionsToKeep { get; set; } + public int? BackupInterval { get; set; } public SDSettingsRequest? SDSettings { get; set; } public bool? ShowClientHostNames { get; set; } public string? AdminPassword { get; set; } @@ -274,6 +277,21 @@ private async Task UpdateSetting(Setting currentSetting, UpdateSettingRequ currentSetting.M3UUseChnoForId = (bool)request.M3UUseChnoForId; } + if (request.BackupEnabled != null) + { + currentSetting.BackupEnabled = (bool)request.BackupEnabled; + } + + if (request.BackupVersionsToKeep.HasValue) + { + currentSetting.BackupVersionsToKeep = request.BackupVersionsToKeep.Value; + } + + if (request.BackupInterval.HasValue) + { + currentSetting.BackupInterval = request.BackupInterval.Value; + } + if (request.ShowClientHostNames != null) { currentSetting.ShowClientHostNames = (bool)request.ShowClientHostNames; diff --git a/StreamMaster.Domain/Common/BuildInfo.cs b/StreamMaster.Domain/Common/BuildInfo.cs index d95a23a7e..eff9a837e 100644 --- a/StreamMaster.Domain/Common/BuildInfo.cs +++ b/StreamMaster.Domain/Common/BuildInfo.cs @@ -27,6 +27,21 @@ static BuildInfo() InitializePaths(); } + private static DateTime _startTime; + + public static DateTime StartTime + { + get + { + if (_startTime == DateTime.MinValue) + { + _startTime = DateTime.Now; + } + + return _startTime; + } + } + #region Database Configuration Properties /// @@ -118,6 +133,7 @@ public static DateTime BuildDateTime public static readonly string IconDefault = Path.Combine("images", "default.png"); public static readonly string FFMPEGDefaultOptions = "-hide_banner -loglevel error -i {streamUrl} -c copy -f mpegts pipe:1"; public static readonly string LogFilePath = Path.Combine(LogFolder, "StreamMasterAPI.log"); + public static readonly string BackupPath = Path.Combine(AppDataFolder, "backups"); #endregion diff --git a/StreamMaster.Domain/Common/FileUtil.cs b/StreamMaster.Domain/Common/FileUtil.cs index 6576766cc..3a5dedd20 100644 --- a/StreamMaster.Domain/Common/FileUtil.cs +++ b/StreamMaster.Domain/Common/FileUtil.cs @@ -2,6 +2,7 @@ using StreamMaster.Domain.Models; using StreamMaster.SchedulesDirect.Domain.XmltvXml; +using System.Diagnostics; using System.IO.Compression; using System.Net; using System.Text; @@ -247,6 +248,57 @@ public static Stream GetFileDataStream(string source) FileStream fs = File.OpenRead(source); return new GZipStream(fs, CompressionMode.Decompress); } + public static async Task Backup(int? versionsToKeep = null) + { + Setting? setting = GetSetting(); + if (setting.BackupEnabled == false) + { + return; + } + + try + { + versionsToKeep ??= GetSetting()?.BackupVersionsToKeep ?? 5; + using Process process = new(); + process.StartInfo.FileName = "/bin/bash"; + process.StartInfo.Arguments = $"/usr/local/bin/backup.sh {versionsToKeep}"; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + + process.Start(); + + string output = await process.StandardOutput.ReadToEndAsync(); + string error = await process.StandardError.ReadToEndAsync(); + + await process.WaitForExitAsync(); + + if (!string.IsNullOrEmpty(output)) + { + Console.WriteLine($"Backup Output: {output}"); + } + + if (!string.IsNullOrEmpty(error)) + { + Console.WriteLine($"Backup Error: {error}"); + } + + if (process.ExitCode == 0) + { + Console.WriteLine("Backup executed successfully."); + } + else + { + Console.WriteLine($"Backup execution failed with exit code: {process.ExitCode}"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Backup Exception occurred: {ex.Message}"); + } + + } public static async Task GetFileData(string source) { @@ -436,6 +488,7 @@ public static void SetupDirectories(bool alwaysRun = false) CreateDir(BuildInfo.SDStationLogosCache); CreateDir(BuildInfo.SDJSONFolder); CreateDir(BuildInfo.LogFolder); + CreateDir(BuildInfo.BackupPath); for (char c = '0'; c <= '9'; c++) { diff --git a/StreamMaster.Domain/Common/Setting.cs b/StreamMaster.Domain/Common/Setting.cs index e0850590d..93f269999 100644 --- a/StreamMaster.Domain/Common/Setting.cs +++ b/StreamMaster.Domain/Common/Setting.cs @@ -89,6 +89,9 @@ public class TestSettings public class BaseSettings : M3USettings { + public bool BackupEnabled { get; set; } = true; + public int BackupVersionsToKeep { get; set; } = 18; + public int BackupInterval { get; set; } = 4; public bool PrettyEPG { get; set; } = false; public int MaxLogFiles { get; set; } = 10; diff --git a/StreamMaster.Domain/Models/JobStatus.cs b/StreamMaster.Domain/Models/JobStatus.cs index 7fdb79678..f7525f93a 100644 --- a/StreamMaster.Domain/Models/JobStatus.cs +++ b/StreamMaster.Domain/Models/JobStatus.cs @@ -47,8 +47,13 @@ public void ClearForce() Extra = false; } - public void SetIsRunning(bool isRunning) + public void SetStart() { - IsRunning = isRunning; + IsRunning = true; + } + + public void SetStop() + { + IsRunning = false; } } diff --git a/StreamMaster.Domain/Services/IImageDownloadService.cs b/StreamMaster.Domain/Services/IImageDownloadService.cs index 8db9f5c58..aa4a01d17 100644 --- a/StreamMaster.Domain/Services/IImageDownloadService.cs +++ b/StreamMaster.Domain/Services/IImageDownloadService.cs @@ -4,6 +4,7 @@ namespace StreamMaster.Domain.Services { public interface IImageDownloadService { + Task StopAsync(CancellationToken cancellationToken); void Start(); ImageDownloadServiceStatus GetStatus(); } diff --git a/StreamMaster.Domain/Services/IJobStatusService.cs b/StreamMaster.Domain/Services/IJobStatusService.cs index b6e81fbff..610d86b84 100644 --- a/StreamMaster.Domain/Services/IJobStatusService.cs +++ b/StreamMaster.Domain/Services/IJobStatusService.cs @@ -8,7 +8,8 @@ public interface IJobStatusService JobStatus GetSyncJobStatus(); void SetSyncError(); void SetSyncForceNextRun(bool Extra = false); - void SetSyncIsRunning(bool v); + void SetSyncStop(); + void SetSyncStart(); void SetSyncSuccessful(); @@ -16,7 +17,8 @@ public interface IJobStatusService void SetM3USuccessful(); void SetM3UError(); void SetM3UForceNextRun(bool Extra = false); - void SetM3UIsRunning(bool v); + void SetM3UStop(); + void SetM3UStart(); JobStatus GetM3UJobStatus(); void ClearM3UForce(); @@ -25,8 +27,18 @@ public interface IJobStatusService void SetEPGSuccessful(); void SetEPGError(); void SetEPGForceNextRun(bool Extra = false); - void SetEPGIsRunning(bool v); + void SetEPGStop(); + void SetEPGStart(); JobStatus GetEPGJobStatus(); void ClearEPGForce(); + + // Backup + void SetBackupSuccessful(); + void SetBackupError(); + void SetBackupForceNextRun(bool Extra = false); + void SetBackupStop(); + void SetBackupStart(); + JobStatus GetBackupJobStatus(); + void ClearBackupForce(); } } \ No newline at end of file diff --git a/StreamMaster.Infrastructure/Services/JobStatusService.Backup.cs b/StreamMaster.Infrastructure/Services/JobStatusService.Backup.cs new file mode 100644 index 000000000..09f6dca3e --- /dev/null +++ b/StreamMaster.Infrastructure/Services/JobStatusService.Backup.cs @@ -0,0 +1,46 @@ +using StreamMaster.Domain.Services; + +namespace StreamMaster.Infrastructure.Services; + +public partial class JobStatusService : IJobStatusService +{ + + private static readonly string BackupSyncKey = "BackupSync"; + + public void SetBackupSuccessful() + { + SetSuccessful(BackupSyncKey); + } + + public void SetBackupError() + { + SetError(BackupSyncKey); + } + + public void SetBackupForceNextRun(bool Extra = false) + { + SetForceNextRun(BackupSyncKey, Extra); + } + + public void SetBackupStart() + { + SetIsRunning(BackupSyncKey, true); + } + + public void SetBackupStop() + { + SetIsRunning(BackupSyncKey, false); + } + + public JobStatus GetBackupJobStatus() + { + return GetStatus(BackupSyncKey); + } + + public void ClearBackupForce() + { + ClearForce(BackupSyncKey); + } + + +} diff --git a/StreamMaster.Infrastructure/Services/JobStatusService.EPG.cs b/StreamMaster.Infrastructure/Services/JobStatusService.EPG.cs index 4395db836..524948c2d 100644 --- a/StreamMaster.Infrastructure/Services/JobStatusService.EPG.cs +++ b/StreamMaster.Infrastructure/Services/JobStatusService.EPG.cs @@ -22,9 +22,14 @@ public void SetEPGForceNextRun(bool Extra = false) SetForceNextRun(RefreshEPGSyncKey, Extra); } - public void SetEPGIsRunning(bool isRunning) + public void SetEPGStart() { - SetIsRunning(RefreshEPGSyncKey, isRunning); + SetIsRunning(RefreshEPGSyncKey, true); + } + + public void SetEPGStop() + { + SetIsRunning(RefreshEPGSyncKey, false); } public JobStatus GetEPGJobStatus() diff --git a/StreamMaster.Infrastructure/Services/JobStatusService.M3U.cs b/StreamMaster.Infrastructure/Services/JobStatusService.M3U.cs index 998371016..74378708f 100644 --- a/StreamMaster.Infrastructure/Services/JobStatusService.M3U.cs +++ b/StreamMaster.Infrastructure/Services/JobStatusService.M3U.cs @@ -22,9 +22,14 @@ public void SetM3UForceNextRun(bool Extra = false) SetForceNextRun(RefreshM3USyncKey, Extra); } - public void SetM3UIsRunning(bool isRunning) + public void SetM3UStart() { - SetIsRunning(RefreshM3USyncKey, isRunning); + SetIsRunning(RefreshM3USyncKey, true); + } + + public void SetM3UStop() + { + SetIsRunning(RefreshM3USyncKey, false); } public JobStatus GetM3UJobStatus() diff --git a/StreamMaster.Infrastructure/Services/JobStatusService.Sync.cs b/StreamMaster.Infrastructure/Services/JobStatusService.Sync.cs index 94ac3ae27..593463105 100644 --- a/StreamMaster.Infrastructure/Services/JobStatusService.Sync.cs +++ b/StreamMaster.Infrastructure/Services/JobStatusService.Sync.cs @@ -22,9 +22,14 @@ public void SetSyncForceNextRun(bool Extra = false) SetForceNextRun(EPGSyncKey, Extra); } - public void SetSyncIsRunning(bool isRunning) + public void SetSyncStart() { - SetIsRunning(EPGSyncKey, isRunning); + SetIsRunning(EPGSyncKey, true); + } + + public void SetSyncStop() + { + SetIsRunning(EPGSyncKey, false); } public JobStatus GetSyncJobStatus() diff --git a/StreamMaster.Infrastructure/Services/JobStatusService.cs b/StreamMaster.Infrastructure/Services/JobStatusService.cs index 2329b8230..ed5578d50 100644 --- a/StreamMaster.Infrastructure/Services/JobStatusService.cs +++ b/StreamMaster.Infrastructure/Services/JobStatusService.cs @@ -53,7 +53,14 @@ private void SetForceNextRun(string key, bool Extra = false) private void SetIsRunning(string key, bool isRunning) { - UpdateStatus(key, status => status.SetIsRunning(isRunning), _locks.GetOrAdd(key, new object())); + if (isRunning) + { + UpdateStatus(key, status => status.SetStart(), _locks.GetOrAdd(key, new object())); + } + else + { + UpdateStatus(key, status => status.SetStop(), _locks.GetOrAdd(key, new object())); + } } private void ClearForce(string key) diff --git a/StreamMaster.Infrastructure/Services/TimerService.cs b/StreamMaster.Infrastructure/Services/TimerService.cs index e7c8bdb14..a02215805 100644 --- a/StreamMaster.Infrastructure/Services/TimerService.cs +++ b/StreamMaster.Infrastructure/Services/TimerService.cs @@ -30,7 +30,7 @@ public class TimerService(IServiceProvider serviceProvider, IMemoryCache memoryC private Timer? _timer; private bool isActive = false; - + private static DateTime LastBackupTime = DateTime.UtcNow; public void Dispose() { GC.SuppressFinalize(this); @@ -70,8 +70,6 @@ private async Task DoWorkAsync(object? _, CancellationToken cancellationToken) isActive = true; } - - using IServiceScope scope = serviceProvider.CreateScope(); IMediator mediator = scope.ServiceProvider.GetRequiredService(); IHubContext hubContext = scope.ServiceProvider.GetRequiredService>(); @@ -101,7 +99,7 @@ private async Task DoWorkAsync(object? _, CancellationToken cancellationToken) JobStatus jobStatus = jobStatusService.GetSyncJobStatus(); if (!jobStatus.IsRunning) { - jobStatus.SetIsRunning(true); + jobStatus.SetStart(); if (jobStatus.ForceNextRun || (now - jobStatus.LastRun).TotalMinutes > 15 || (now - jobStatus.LastSuccessful).TotalMinutes > 60) { @@ -122,8 +120,10 @@ private async Task DoWorkAsync(object? _, CancellationToken cancellationToken) } } - if (!jobStatusService.GetEPGJobStatus().IsRunning) + jobStatus = jobStatusService.GetEPGJobStatus(); + if (!jobStatus.IsRunning) { + jobStatus.SetStart(); IEnumerable epgFilesToUpdated = await mediator.Send(new GetEPGFilesNeedUpdating(), cancellationToken).ConfigureAwait(false); if (epgFilesToUpdated.Any()) { @@ -134,10 +134,13 @@ private async Task DoWorkAsync(object? _, CancellationToken cancellationToken) _ = await mediator.Send(new RefreshEPGFileRequest(epg.Id), cancellationToken).ConfigureAwait(false); } } + jobStatus.SetStop(); } - if (!jobStatusService.GetM3UJobStatus().IsRunning) + jobStatus = jobStatusService.GetM3UJobStatus(); + if (!jobStatus.IsRunning) { + jobStatus.SetStart(); IEnumerable m3uFilesToUpdated = await mediator.Send(new GetM3UFilesNeedUpdating(), cancellationToken).ConfigureAwait(false); if (m3uFilesToUpdated.Any()) { @@ -148,6 +151,26 @@ private async Task DoWorkAsync(object? _, CancellationToken cancellationToken) await mediator.Send(new RefreshM3UFileRequest(m3uFile.Id), cancellationToken).ConfigureAwait(false); } } + jobStatus.SetStop(); + } + + jobStatus = jobStatusService.GetBackupJobStatus(); + if (setting.BackupEnabled && !jobStatus.IsRunning) + { + jobStatus.SetStart(); + + if (LastBackupTime.AddHours(setting.BackupInterval) <= DateTime.UtcNow) + { + logger.LogInformation("Backup started. {status}", jobStatusService.GetBackupJobStatus()); + + await FileUtil.Backup().ConfigureAwait(false); + + logger.LogInformation("Backup completed. {status}", jobStatusService.GetBackupJobStatus()); + LastBackupTime = DateTime.UtcNow; + jobStatus.SetStop(); + } + + jobStatus.SetStop(); } lock (Lock) diff --git a/StreamMaster.SchedulesDirect/SchedulesDirect.cs b/StreamMaster.SchedulesDirect/SchedulesDirect.cs index a083e18f3..ae2ca6e55 100644 --- a/StreamMaster.SchedulesDirect/SchedulesDirect.cs +++ b/StreamMaster.SchedulesDirect/SchedulesDirect.cs @@ -63,7 +63,7 @@ public async Task SDSync(int EPGNumber, CancellationToken cancellationToke return false; } - jobStatusService.SetSyncIsRunning(true); + jobStatusService.SetSyncStart(); //ResetEPGCache(); int maxRetry = 3; int retryCount = 0; diff --git a/StreamMaster.Streams.Domain/Helpers/ProxyHelper.cs b/StreamMaster.Streams.Domain/Helpers/ProxyHelper.cs new file mode 100644 index 000000000..33b68a3e2 --- /dev/null +++ b/StreamMaster.Streams.Domain/Helpers/ProxyHelper.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace StreamMaster.Streams.Domain.Helpers +{ + internal class ProxyHelper + { + } +} diff --git a/StreamMaster.Streams/Buffers/CircularRingBuffer.cs b/StreamMaster.Streams/Buffers/CircularRingBuffer.cs index 25ce6ea83..d8e27a504 100644 --- a/StreamMaster.Streams/Buffers/CircularRingBuffer.cs +++ b/StreamMaster.Streams/Buffers/CircularRingBuffer.cs @@ -15,18 +15,11 @@ public sealed partial class CircularRingBuffer : ICircularRingBuffer private TaskCompletionSource _writeSignal = new(TaskCreationOptions.RunContinuationsAsynchronously); private TaskCompletionSource _pauseSignal = new(TaskCreationOptions.RunContinuationsAsynchronously); - private readonly CancellationTokenRegistration _registration; - public long GetNextReadIndex() { return WriteBytes; } - public void UnregisterCancellation() - { - _registration.Unregister(); - } - private int CalculateClientReadIndex(long readByteIndex) { if (_buffer.Length == 0) diff --git a/StreamMaster.Streams/Factories/ProxyFactory.cs b/StreamMaster.Streams/Factories/ProxyFactory.cs index a6fc4a213..e32f883fd 100644 --- a/StreamMaster.Streams/Factories/ProxyFactory.cs +++ b/StreamMaster.Streams/Factories/ProxyFactory.cs @@ -12,30 +12,33 @@ namespace StreamMaster.Streams.Factories; public sealed class ProxyFactory(ILogger logger, IHttpClientFactory httpClientFactory, IMemoryCache memoryCache) : IProxyFactory { - public async Task<(Stream? stream, int processId, ProxyStreamError? error)> GetProxy(string streamUrl, string streamName, StreamingProxyTypes streamProxyType, CancellationToken cancellationToken) + private StreamingProxyTypes GetStreamingProxyType(StreamingProxyTypes videoStreamStreamingProxyType) { Setting setting = memoryCache.GetSetting(); + return videoStreamStreamingProxyType == StreamingProxyTypes.SystemDefault + ? setting.StreamingProxyType + : videoStreamStreamingProxyType; + } + + public async Task<(Stream? stream, int processId, ProxyStreamError? error)> GetProxy(string streamUrl, string streamName, StreamingProxyTypes streamProxyType, CancellationToken cancellationToken) + { Stream? stream; ProxyStreamError? error; int processId; - StreamingProxyTypes proxyType = setting.StreamingProxyType; + StreamingProxyTypes proxyType = GetStreamingProxyType(streamProxyType); - if (proxyType.Equals(StreamingProxyTypes.None)) + if (proxyType == StreamingProxyTypes.None) { logger.LogInformation("No proxy stream needed for {StreamUrl} {streamName}", streamUrl, streamName); return (null, -1, null); } - if (proxyType.Equals(StreamingProxyTypes.SystemDefault)) - { - proxyType = setting.StreamingProxyType; - } - if (proxyType == StreamingProxyTypes.FFMpeg) { - (stream, processId, error) = await GetFFMpegStream(streamUrl); + + (stream, processId, error) = await GetFFMpegStream(streamUrl, streamName); LogErrorIfAny(stream, error, streamUrl, streamName); } else @@ -56,7 +59,7 @@ private void LogErrorIfAny(Stream? stream, ProxyStreamError? error, string strea } } - private async Task<(Stream? stream, int processId, ProxyStreamError? error)> GetFFMpegStream(string streamUrl) + private async Task<(Stream? stream, int processId, ProxyStreamError? error)> GetFFMpegStream(string streamUrl, string streamName) { Setting settings = memoryCache.GetSetting(); @@ -74,7 +77,7 @@ private void LogErrorIfAny(Stream? stream, ProxyStreamError? error, string strea try { - return await CreateFFMpegStream(ffmpegExec, streamUrl).ConfigureAwait(false); + return await CreateFFMpegStream(ffmpegExec, streamUrl, streamName).ConfigureAwait(false); } catch (IOException ex) { @@ -86,10 +89,11 @@ private void LogErrorIfAny(Stream? stream, ProxyStreamError? error, string strea } } - private async Task<(Stream? stream, int processId, ProxyStreamError? error)> CreateFFMpegStream(string ffmpegExec, string streamUrl) + private async Task<(Stream? stream, int processId, ProxyStreamError? error)> CreateFFMpegStream(string ffmpegExec, string streamUrl, string streamName) { try { + Stopwatch stopwatch = Stopwatch.StartNew(); Setting settings = memoryCache.GetSetting(); string options = string.IsNullOrEmpty(settings.FFMpegOptions) ? BuildInfo.FFMPEGDefaultOptions : settings.FFMpegOptions; @@ -103,17 +107,22 @@ private void LogErrorIfAny(Stream? stream, ProxyStreamError? error, string strea process.StartInfo.CreateNoWindow = true; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; bool processStarted = process.Start(); + stopwatch.Stop(); if (!processStarted) { // Log and return an error if the process couldn't be started ProxyStreamError error = new() { ErrorCode = ProxyStreamErrorCode.ProcessStartFailed, Message = "Failed to start FFmpeg process" }; logger.LogError("CreateFFMpegStream Error: {ErrorMessage}", error.Message); + return (null, -1, error); } // Return the standard output stream of the process + + logger.LogInformation("Opened ffmpeg stream for {streamName} with args \"{formattedArgs}\" in {ElapsedMilliseconds} ms", streamName, formattedArgs, stopwatch.ElapsedMilliseconds); return (await Task.FromResult(process.StandardOutput.BaseStream).ConfigureAwait(false), process.Id, null); } catch (Exception ex) @@ -134,7 +143,7 @@ private void LogErrorIfAny(Stream? stream, ProxyStreamError? error, string strea private async Task<(Stream? stream, int processId, ProxyStreamError? error)> GetProxyStream(string sourceUrl, string streamName, CancellationToken cancellationToken) { - Stopwatch stopwatch = Stopwatch.StartNew(); // Start the stopwatch + Stopwatch stopwatch = Stopwatch.StartNew(); try { @@ -158,12 +167,12 @@ private void LogErrorIfAny(Stream? stream, ProxyStreamError? error, string strea ) { logger.LogInformation("Stream URL has HLS content, using FFMpeg for streaming: {StreamUrl} {streamName}", sourceUrl, streamName); - return await GetFFMpegStream(sourceUrl).ConfigureAwait(false); + return await GetFFMpegStream(sourceUrl, streamName).ConfigureAwait(false); } Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); stopwatch.Stop(); // Stop the stopwatch when the stream is retrieved - logger.LogInformation("Successfully retrieved stream for: {StreamUrl} {streamName} in {ElapsedMilliseconds} ms", sourceUrl, streamName, stopwatch.ElapsedMilliseconds); + logger.LogInformation("Opened stream for {streamName} in {ElapsedMilliseconds} ms", streamName, stopwatch.ElapsedMilliseconds); return (stream, -1, null); } diff --git a/StreamMaster.Streams/Streams/StreamHandler.cs b/StreamMaster.Streams/Streams/StreamHandler.cs index 90036f99c..ec7c2c434 100644 --- a/StreamMaster.Streams/Streams/StreamHandler.cs +++ b/StreamMaster.Streams/Streams/StreamHandler.cs @@ -416,10 +416,23 @@ public async Task Stop() if (ProcessId > 0) { + //try + //{ + // KillProcess(); + + //} + //catch (Exception ex) + //{ + // logger.LogError(ex, "Error killing process {ProcessId}.", ProcessId); + //} try { - KillProcess(); - + string? procName = CheckProcessExists(); + if (procName != null) + { + Process process = Process.GetProcessById(ProcessId); + process.Kill(); + } } catch (Exception ex) { @@ -478,6 +491,18 @@ private void KillProcess() } + private string? CheckProcessExists() + { + try + { + Process process = Process.GetProcessById(ProcessId); + return process.ProcessName; + } + catch (ArgumentException) + { + return null; + } + } public IEnumerable GetClientStreamerClientIds() diff --git a/backup.ps1 b/backup.ps1 new file mode 100644 index 000000000..a56c1aa7e --- /dev/null +++ b/backup.ps1 @@ -0,0 +1,81 @@ +# Source environment variables +# PowerShell equivalent might require directly setting variables or using a .ps1 file for environment variables. + +# Configuration +$backupDirs = @("/config/PlayLists", "/config/Logs", "/config/Cache/SDJson") +$backupFiles = @("/config/settings.json", "/config/logsettings.json") +$backupPath = "/config/backups" +$versionsToKeep = 5 +$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss" +$dbBackupFile = "db_$timestamp.gz" +$filesBackupFile = "files_$timestamp.tar.gz" +$backupFile = "$backupPath/backup_$timestamp.tar.gz" + +$env:BACKUP_VERSIONS_TO_KEEP = 18 +$env:POSTGRES_USER = "sm" +$env:POSTGRES_DB = "StreamMaster" +$env:POSTGRES_PASSWORD = "sm123" + +# Determine versions to keep from command line argument, environment variable, or default +if ($args.Count -gt 0) { + $versionsToKeep = $args[0] +} +elseif ([Environment]::GetEnvironmentVariable("BACKUP_VERSIONS_TO_KEEP")) { + $versionsToKeep = [Environment]::GetEnvironmentVariable("BACKUP_VERSIONS_TO_KEEP") +} + +# Check if backup directory exists, if not, create it +if (-not (Test-Path $backupPath)) { + New-Item -Path $backupPath -ItemType Directory +} + +# Backup PostgreSQL database +function Backup-Database { + & pg_dump -U $env:POSTGRES_USER $env:POSTGRES_DB | Compress-Archive -DestinationPath "$backupPath/$dbBackupFile" + Write-Host "Database backup completed: $dbBackupFile" +} + +# Backup files and directories +function Backup-FilesAndDirs { + $itemsToBackup = @() + foreach ($item in $backupDirs + $backupFiles) { + if (Test-Path $item) { + $itemsToBackup += $item + } + else { + Write-Host "Warning: $item does not exist and will not be included in the backup." + } + } + + if ($itemsToBackup.Count -gt 0) { + Compress-Archive -Path $itemsToBackup -DestinationPath "$backupPath/$filesBackupFile" + Write-Host "Directories and files backup completed: $filesBackupFile" + } + else { + Write-Host "No files or directories exist for backup." + } +} + +# Create one backup file +function Create-Backup { + Compress-Archive -Path "$backupPath/$dbBackupFile", "$backupPath/$filesBackupFile" -DestinationPath $backupFile + Write-Host "Backup file created: $backupFile" +} + +# Function to limit the number of backups +function Limit-Backups { + Set-Location $backupPath + # Remove individual db and files backups to clean up + Remove-Item $dbBackupFile, $filesBackupFile -ErrorAction SilentlyContinue + # Keep only the specified number of backup versions + Get-ChildItem -Filter "backup_*.tar.gz" | Sort-Object CreationTime -Descending | Select-Object -Skip $versionsToKeep | Remove-Item + Write-Host "Old backups cleanup completed." +} + +# Main script execution +Write-Host "Starting backup process..." +Backup-Database +Backup-FilesAndDirs +Create-Backup +Limit-Backups +Write-Host "Backup process finished." diff --git a/backup.sh b/backup.sh new file mode 100644 index 000000000..6b583d2e7 --- /dev/null +++ b/backup.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Source environment variables +. /env.sh + +# Configuration +backup_dirs="/config/PlayLists /config/Logs /config/Cache/SDJson" # Space-separated list of directories +backup_files="/config/settings.json /config/logsettings.json" # Space-separated list of files +backup_path="/config/backups" +versions_to_keep=5 +timestamp=$(date +"%Y-%m-%d_%H-%M-%S") +db_backup_file="db_$timestamp.gz" +files_backup_file="files_$timestamp.tar.gz" +backup_file="$backup_path/backup_$timestamp.tar.gz" + +# Determine versions to keep from command line argument, environment variable, or default +if [ ! -z "$1" ]; then + versions_to_keep=$1 +elif [ ! -z "$BACKUP_VERSIONS_TO_KEEP" ]; then + versions_to_keep=$BACKUP_VERSIONS_TO_KEEP +else + versions_to_keep=5 +fi + +# Check if backup directory exists, if not, create it +if [ ! -d "$backup_path" ]; then + mkdir -p "$backup_path" +fi + +# Backup PostgreSQL database +backup_database() { + pg_dump -U $POSTGRES_USER $POSTGRES_DB | gzip > "$backup_path/$db_backup_file" + echo "Database backup completed: $db_backup_file" +} + +# Backup files and directories +backup_files_and_dirs() { + local items_to_backup=() + for item in $backup_dirs $backup_files; do + if [ -e "$item" ]; then + items_to_backup+=("$item") + else + echo "Warning: $item does not exist and will not be included in the backup." + fi + done + + if [ ${#items_to_backup[@]} -gt 0 ]; then + tar -czf "$backup_path/$files_backup_file" "${items_to_backup[@]}" 2>/dev/null + echo "Directories and files backup completed: $files_backup_file" + else + echo "No files or directories exist for backup." + fi +} + +# Create one backup file +create_backup() { + tar -czf "$backup_file" -C "$backup_path" $db_backup_file $files_backup_file 2>/dev/null + echo "Backup file created: $backup_file" +} + +# Function to limit the number of backups +limit_backups() { + cd $backup_path + # Remove individual db and files backups to clean up + rm -f $db_backup_file $files_backup_file + # Keep only the specified number of backup versions + (ls -t backup_*.tar.gz | head -n $versions_to_keep; ls backup_*.tar.gz) | sort | uniq -u | xargs --no-run-if-empty rm + echo "Old backups cleanup completed." +} + +# Main script execution +echo "Starting backup process..." +backup_database +backup_files_and_dirs +create_backup +limit_backups +echo "Backup process finished." diff --git a/build_docker.ps1 b/build_docker.ps1 index 87cc4495a..acd578f32 100644 --- a/build_docker.ps1 +++ b/build_docker.ps1 @@ -16,6 +16,35 @@ param ( $global:tags +function Write-StringToFile { + param ( + [string]$Path, + [string]$Content + ) + try { + $Content | Out-File -FilePath $Path -Encoding UTF8 -Force + Write-Host "Content written to file: $Path" + } + catch { + Write-Host "An error occurred: $_" + } +} +function Read-StringFromFile { + param ( + [string]$Path + ) + try { + $Content = Get-Content -Path $Path -Raw + Write-Host "Content read from file: $Path" + return $Content + } + catch { + Write-Host "An error occurred: $_" + return $null + } +} + + function Main { Set-EnvironmentVariables @@ -30,57 +59,67 @@ function Main { # DownloadFiles $imageName = "docker.io/senexcrenshaw/streammaster" + $buildName = $imageName + "-builds" $result = Get-AssemblyInfo -assemblyInfoPath "./StreamMaster.API/AssemblyInfo.cs" $processedAssemblyInfo = ProcessAssemblyInfo $result if ($BuildBase -or $BuildAll) { $dockerFile = "Dockerfile.base" - $global:tags = @("$("${imageName}:"+$processedAssemblyInfo.BranchName)-base") - BuildImage -result $processedAssemblyInfo -imageName $imageName -dockerFile $dockerFile + $global:tags = @("$("${buildName}:"+$processedAssemblyInfo.BranchName)-base") + BuildImage -result $processedAssemblyInfo -imageName $buildName -dockerFile $dockerFile + Write-StringToFile -Path "basever" -Content $processedAssemblyInfo.BranchName } if ($BuildBuild -or $BuildAll) { $dockerFile = "Dockerfile.build" - $global:tags = @("$("${imageName}:"+$processedAssemblyInfo.BranchName)-build") - BuildImage -result $processedAssemblyInfo -imageName $imageName -dockerFile $dockerFile + $global:tags = @("$("${buildName}:"+$processedAssemblyInfo.BranchName)-build") + BuildImage -result $processedAssemblyInfo -imageName $buildName -dockerFile $dockerFile + Write-StringToFile -Path "buildver" -Content $processedAssemblyInfo.BranchName } if ($BuildSM -or $BuildBuild -or $BuildAll) { $dockerFile = "Dockerfile.sm" $global:tags = @("$("${imageName}:"+$processedAssemblyInfo.BranchName)-sm") - + if ( $BuildBuildVer -ne '' ) { $contentArray = @('FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($BuildBuildVer)-build" + ' AS build'); + $smver = $BuildBuildVer; } else { - $contentArray = @('FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($processedAssemblyInfo.BranchName)-build" + ' AS build'); + $buildver = Read-StringFromFile -Path "buildver"; + $smver = $buildver; + $contentArray = @('FROM --platform=$BUILDPLATFORM ' + "${imageName}:{$buildver}-build" + ' AS build'); } Add-ContentAtTop -filePath $dockerFile -contentArray $contentArray BuildImage -result $processedAssemblyInfo -imageName $imageName -dockerFile $dockerFile + Write-StringToFile -Path "smver" -Content $smver } if ( -not $SkipMainBuild -or $BuildAll) { $dockerFile = "Dockerfile" $contentArray = @(); - if ( $BuildSMVer -ne '' ) { - $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($BuildSMVer)-sm" + ' AS sm' - } - else { - $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($processedAssemblyInfo.BranchName)-sm" + ' AS sm' - } - - if ( $BuildBaseVer -ne '' ) { - $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($BuildBaseVer)-base" + ' AS base' - } - else { - $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($processedAssemblyInfo.BranchName)-base" + ' AS base' - } - + # if ( $BuildSMVer -ne '' ) { + # $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($BuildSMVer)-sm" + ' AS sm' + # } + # else { + # $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($processedAssemblyInfo.BranchName)-sm" + ' AS sm' + # } + + # if ( $BuildBaseVer -ne '' ) { + # $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($BuildBaseVer)-base" + ' AS base' + # } + # else { + # $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${imageName}:$($processedAssemblyInfo.BranchName)-base" + ' AS base' + # } + $basever = Read-StringFromFile -Path "basever"; + $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${buildName}:$($basever)-base" + ' AS base' + $smver = Read-StringFromFile -Path "smver"; + $contentArray += 'FROM --platform=$BUILDPLATFORM ' + "${buildName}:$($smver)-sm" + ' AS sm' Add-ContentAtTop -filePath $dockerFile -contentArray $contentArray $global:tags = DetermineTags -result $processedAssemblyInfo -imageName $imageName diff --git a/env.sh b/env.sh index bfc452a5f..6e4b1ab78 100644 --- a/env.sh +++ b/env.sh @@ -5,6 +5,7 @@ default_dbuser="sm" default_dbpassword="sm123" default_db="StreamMaster" default_set_perms=1 +default_backup_versions_to_keep=5 # Set and export environment variables with defaults if they are not set export PGADMIN_SETUP_EMAIL="${PGADMIN_SETUP_EMAIL:-$default_email}" @@ -15,4 +16,5 @@ export POSTGRES_PASSWORD="${POSTGRES_PASSWORD:-$default_dbpassword}" export PGDATA=/config/DB export POSTGRES_DB="${POSTGRES_DB:-$default_db}" export POSTGRES_SET_PERMS="${POSTGRES_SET_PERMS:-$default_set_perms}" -export PATH=$PATH:/usr/lib/postgresql/15/bin \ No newline at end of file +export BACKUP_VERSIONS_TO_KEEP="${BACKUP_VERSIONS_TO_KEEP:-$default_backup_versions_to_keep}" +export PATH=$PATH:/usr/lib/postgresql/15/bin diff --git a/streammasterwebui/features/settings/BackupSettings.tsx b/streammasterwebui/features/settings/BackupSettings.tsx new file mode 100644 index 000000000..6496e314f --- /dev/null +++ b/streammasterwebui/features/settings/BackupSettings.tsx @@ -0,0 +1,28 @@ +import { GetMessage } from '@lib/common/common'; +import React from 'react'; + +import { Fieldset } from 'primereact/fieldset'; +import { getCheckBoxLine } from './getCheckBoxLine'; +import { getInputNumberLine } from './getInputNumberLine'; + +import { useSettingChangeHandler } from './useSettingChangeHandler'; + +export function BackupSettings(): React.ReactElement { + const { onChange, selectedCurrentSettingDto } = useSettingChangeHandler(); + + if (selectedCurrentSettingDto === null || selectedCurrentSettingDto === undefined) { + return ( +
+
{GetMessage('loading')}
+
+ ); + } + + return ( +
+ {getCheckBoxLine({ field: 'backupEnabled', selectedCurrentSettingDto, onChange })} + {getInputNumberLine({ field: 'backupVersionsToKeep', selectedCurrentSettingDto, onChange })} + {getInputNumberLine({ field: 'backupInterval', selectedCurrentSettingDto, onChange })} +
+ ); +} diff --git a/streammasterwebui/features/settings/SettingsEditor.tsx b/streammasterwebui/features/settings/SettingsEditor.tsx index c3bb1927c..f683099af 100644 --- a/streammasterwebui/features/settings/SettingsEditor.tsx +++ b/streammasterwebui/features/settings/SettingsEditor.tsx @@ -11,6 +11,7 @@ import { UpdateSetting } from '@lib/smAPI/Settings/SettingsMutateAPI'; import { ScrollPanel } from 'primereact/scrollpanel'; import { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { AuthenticationSettings } from './AuthenticationSettings'; +import { BackupSettings } from './BackupSettings'; import { DevelopmentSettings } from './DevelopmentSettings'; import { FilesEPGM3USettings } from './FilesEPGM3USettings'; import { GeneralSettings } from './GeneralSettings'; @@ -91,6 +92,8 @@ export const SettingsEditor = () => { + + diff --git a/streammasterwebui/lib/iptvApi.ts b/streammasterwebui/lib/iptvApi.ts index 6d1c12f99..d8e03b52d 100644 --- a/streammasterwebui/lib/iptvApi.ts +++ b/streammasterwebui/lib/iptvApi.ts @@ -806,6 +806,7 @@ export type EpgFilesCreateEpgFileFromFormApiResponse = unknown; export type EpgFilesCreateEpgFileFromFormApiArg = { FormFile?: Blob | null; Name?: string; + FileName?: string; EPGNumber?: number; UrlSource?: string | null; Color?: string | null; @@ -1223,6 +1224,7 @@ export type GetChannelGroupsForStreamGroupRequest = { export type CreateEpgFileRequest = { formFile?: Blob | null; name?: string; + fileName?: string; epgNumber?: number; urlSource?: string | null; color?: string | null; @@ -1801,6 +1803,9 @@ export type SdSettings = { }; export type AuthenticationType = 0 | 2; export type BaseSettings = M3USettings & { + backupEnabled?: boolean; + backupVersionsToKeep?: number; + backupInterval?: number; prettyEPG?: boolean; maxLogFiles?: number; maxLogFileSizeMB?: number; @@ -1879,6 +1884,9 @@ export type SdSettingsRequest = { xmltvSingleImage?: boolean | null; }; export type UpdateSettingRequest = { + backupEnabled?: boolean | null; + backupVersionsToKeep?: number | null; + backupInterval?: number | null; sdSettings?: SdSettingsRequest | null; showClientHostNames?: boolean | null; adminPassword?: string | null; diff --git a/streammasterwebui/lib/locales/MessagesEn.tsx b/streammasterwebui/lib/locales/MessagesEn.tsx index 073fc4583..43c2fc423 100644 --- a/streammasterwebui/lib/locales/MessagesEn.tsx +++ b/streammasterwebui/lib/locales/MessagesEn.tsx @@ -134,8 +134,11 @@ const MessagesEn: messages_enType = { set: 'Set', m3UUseCUIDForChannelID: 'Use CUID for channel id', settings: 'Settings', - + backups: 'Backups', show: 'Show', + backupEnabled: 'Backup Enabled', + backupVersionsToKeep: 'Backup Versions to Keep', + backupInterval: 'Backup Interval (hours)', showClientHostNames: 'Show Client Hostnames',