mirror of https://github.com/ErsatzTV/ErsatzTV.git
12 changed files with 0 additions and 906 deletions
@ -1,106 +0,0 @@
@@ -1,106 +0,0 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands |
||||
{ |
||||
[Command("channel", Description = "Create or rename a channel")] |
||||
public class ChannelCommand : ICommand |
||||
{ |
||||
private readonly ChannelsApi _channelsApi; |
||||
private readonly FFmpegProfileApi _ffmpegProfileApi; |
||||
private readonly ILogger<ChannelCommand> _logger; |
||||
|
||||
public ChannelCommand(IConfiguration configuration, ILogger<ChannelCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_channelsApi = new ChannelsApi(configuration["ServerUrl"]); |
||||
_ffmpegProfileApi = new FFmpegProfileApi(configuration["ServerUrl"]); |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "channel-number", Description = "The channel number")] |
||||
public int Number { get; set; } |
||||
|
||||
[CommandParameter(1, Name = "channel-name", Description = "The channel name")] |
||||
public string Name { get; set; } |
||||
|
||||
[CommandParameter(2, Name = "streaming-mode", Description = "The streaming mode")] |
||||
public StreamingMode StreamingMode { get; set; } |
||||
|
||||
[CommandOption("ffmpeg-profile", Description = "The ffmpeg profile name")] |
||||
public string FFmpegProfileName { get; set; } |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
Option<ChannelViewModel> maybeChannel = await _channelsApi.ApiChannelsGetAsync() |
||||
.Map(list => Optional(list.SingleOrDefault(c => c.Number == Number))); |
||||
|
||||
FFmpegProfileViewModel ffmpegProfile = await _ffmpegProfileApi.ApiFfmpegProfilesGetAsync() |
||||
.Map( |
||||
list => Optional(list.SingleOrDefault(p => p.Name == FFmpegProfileName)) |
||||
.IfNone(new FFmpegProfileViewModel { Id = 1 })); |
||||
|
||||
await maybeChannel.Match( |
||||
channel => RenameChannel(channel, ffmpegProfile), |
||||
() => AddChannel(ffmpegProfile)); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to synchronize channel: {Message}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async ValueTask RenameChannel(ChannelViewModel existing, FFmpegProfileViewModel ffmpegProfile) |
||||
{ |
||||
int newFFmpegProfileId = string.IsNullOrWhiteSpace(FFmpegProfileName) |
||||
? existing.FfmpegProfileId |
||||
: ffmpegProfile.Id; |
||||
|
||||
if (existing.Name != Name || existing.FfmpegProfileId != newFFmpegProfileId || |
||||
existing.StreamingMode != StreamingMode) |
||||
{ |
||||
var updateChannel = new UpdateChannel( |
||||
existing.Id, |
||||
Name, |
||||
existing.Number, |
||||
newFFmpegProfileId, |
||||
existing.Logo, |
||||
StreamingMode); |
||||
|
||||
await _channelsApi.ApiChannelsPatchAsync(updateChannel); |
||||
} |
||||
|
||||
_logger.LogInformation( |
||||
"Successfully synchronized channel {ChannelNumber} - {ChannelName}", |
||||
Number, |
||||
Name); |
||||
} |
||||
|
||||
private async ValueTask AddChannel(FFmpegProfileViewModel ffmpegProfile) |
||||
{ |
||||
var createChannel = new CreateChannel( |
||||
Name, |
||||
Number, |
||||
ffmpegProfile.Id, |
||||
null, |
||||
StreamingMode); |
||||
|
||||
await _channelsApi.ApiChannelsPostAsync(createChannel); |
||||
|
||||
_logger.LogInformation( |
||||
"Successfully created channel {ChannelNumber} - {ChannelName}", |
||||
Number, |
||||
Name); |
||||
} |
||||
} |
||||
} |
||||
@ -1,31 +0,0 @@
@@ -1,31 +0,0 @@
|
||||
using System; |
||||
using System.IO; |
||||
using System.Text.Json; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands |
||||
{ |
||||
[Command("config", Description = "Configure ErsatzTV server url")] |
||||
public class ConfigCommand : ICommand |
||||
{ |
||||
[CommandParameter(0, Name = "server-url", Description = "The url of the ErsatzTV server")] |
||||
public string ServerUrl { get; set; } |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
// TODO: validate URL
|
||||
|
||||
string configFolder = Path.Combine( |
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), |
||||
"ersatztv"); |
||||
|
||||
string configFile = Path.Combine(configFolder, "cli.json"); |
||||
|
||||
var config = new Config { ServerUrl = ServerUrl }; |
||||
string contents = JsonSerializer.Serialize(config); |
||||
await File.WriteAllTextAsync(configFile, contents); |
||||
} |
||||
} |
||||
} |
||||
@ -1,151 +0,0 @@
@@ -1,151 +0,0 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands |
||||
{ |
||||
[Command("ffmpeg-profile", Description = "Synchronize an ffmpeg profile")] |
||||
public class FFmpegProfileCommand : ICommand |
||||
{ |
||||
private readonly FFmpegProfileApi _ffmpegProfileApi; |
||||
private readonly ILogger<FFmpegProfileCommand> _logger; |
||||
|
||||
public FFmpegProfileCommand(IConfiguration configuration, ILogger<FFmpegProfileCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_ffmpegProfileApi = new FFmpegProfileApi(configuration["ServerUrl"]); |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "profile-name", Description = "The ffmpeg profile name")] |
||||
public string Name { get; set; } |
||||
|
||||
[CommandOption("thread-count", Description = "The number of threads")] |
||||
public int ThreadCount { get; set; } = 0; |
||||
|
||||
[CommandOption("transcode", Description = "Whether to transcode all media")] |
||||
public bool Transcode { get; set; } = true; |
||||
|
||||
// public int ResolutionId { get; set; } = resolution.Id;
|
||||
// Resolution { get; set; } = resolution;
|
||||
[CommandOption("resolution", Description = "The resolution")] |
||||
public DesiredResolution Resolution { get; set; } = DesiredResolution.W1920H1080; |
||||
|
||||
[CommandOption("video-codec", Description = "The video codec")] |
||||
public string VideoCodec { get; set; } = "libx264"; |
||||
|
||||
[CommandOption("audio-codec", Description = "The audio codec")] |
||||
public string AudioCodec { get; set; } = "ac3"; |
||||
|
||||
[CommandOption("video-bitrate", Description = "The video bitrate in kBit/s")] |
||||
public int VideoBitrate { get; set; } = 2000; |
||||
|
||||
[CommandOption("video-buffer-size", Description = "The video buffer size in kBit")] |
||||
public int VideoBufferSize { get; set; } = 2000; |
||||
|
||||
[CommandOption("audio-bitrate", Description = "The audio bitrate in kBit/s")] |
||||
public int AudioBitrate { get; set; } = 192; |
||||
|
||||
[CommandOption("audio-buffer-size", Description = "The audio buffer size in kBits")] |
||||
public int AudioBufferSize { get; set; } = 50; |
||||
|
||||
[CommandOption("audio-volume", Description = "The audio volume as a whole number percent")] |
||||
public int AudioVolume { get; set; } = 100; |
||||
|
||||
[CommandOption("audio-channels", Description = "The number of audio channels")] |
||||
public int AudioChannels { get; set; } = 2; |
||||
|
||||
[CommandOption("audio-sample-rate", Description = "The audio sample rate in kHz")] |
||||
public int AudioSampleRate { get; set; } = 48; |
||||
|
||||
[CommandOption("normalize-resolution", Description = "Whether to normalize the resolution of all media")] |
||||
public bool NormalizeResolution { get; set; } = true; |
||||
|
||||
[CommandOption("normalize-video-codec", Description = "Whether to normalize the video codec of all media")] |
||||
public bool NormalizeVideoCodec { get; set; } = true; |
||||
|
||||
[CommandOption("normalize-audio-codec", Description = "Whether to normalize the audio codec of all media")] |
||||
public bool NormalizeAudioCodec { get; set; } = true; |
||||
|
||||
[CommandOption( |
||||
"normalize-audio", |
||||
Description = "Whether to normalize audio channels and sample rate of all media")] |
||||
public bool NormalizeAudio { get; set; } = true; |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
Option<FFmpegProfileViewModel> maybeFFmpegProfile = await _ffmpegProfileApi.ApiFfmpegProfilesGetAsync() |
||||
.Map(list => Optional(list.SingleOrDefault(p => p.Name == Name))); |
||||
|
||||
await maybeFFmpegProfile.Match(UpdateProfile, AddProfile); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to synchronize ffmpeg profile: {Message}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async ValueTask UpdateProfile(FFmpegProfileViewModel existing) |
||||
{ |
||||
var updateFFmpegProfile = new UpdateFFmpegProfile( |
||||
existing.Id, |
||||
Name, |
||||
ThreadCount, |
||||
Transcode, |
||||
(int) Resolution, |
||||
NormalizeResolution, |
||||
VideoCodec, |
||||
NormalizeVideoCodec, |
||||
VideoBitrate, |
||||
VideoBufferSize, |
||||
AudioCodec, |
||||
NormalizeAudioCodec, |
||||
AudioBitrate, |
||||
AudioBufferSize, |
||||
AudioVolume, |
||||
AudioChannels, |
||||
AudioSampleRate, |
||||
NormalizeAudio); |
||||
|
||||
await _ffmpegProfileApi.ApiFfmpegProfilesPatchAsync(updateFFmpegProfile); |
||||
|
||||
_logger.LogInformation("Successfully synchronized ffmpeg profile {ProfileName}", Name); |
||||
} |
||||
|
||||
private async ValueTask AddProfile() |
||||
{ |
||||
var createFFmpegProfile = new CreateFFmpegProfile( |
||||
Name, |
||||
ThreadCount, |
||||
Transcode, |
||||
(int) Resolution, |
||||
NormalizeResolution, |
||||
VideoCodec, |
||||
NormalizeVideoCodec, |
||||
VideoBitrate, |
||||
VideoBufferSize, |
||||
AudioCodec, |
||||
NormalizeAudioCodec, |
||||
AudioBitrate, |
||||
AudioBufferSize, |
||||
AudioVolume, |
||||
AudioChannels, |
||||
AudioSampleRate, |
||||
NormalizeAudio); |
||||
|
||||
|
||||
await _ffmpegProfileApi.ApiFfmpegProfilesPostAsync(createFFmpegProfile); |
||||
|
||||
_logger.LogInformation("Successfully created ffmpeg profile {ProfileName}", Name); |
||||
} |
||||
} |
||||
} |
||||
@ -1,86 +0,0 @@
@@ -1,86 +0,0 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using LanguageExt.Common; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands.MediaCollections |
||||
{ |
||||
[Command("collection clear", Description = "Removes all items from a media collection")] |
||||
public class MediaCollectionClearCommand : ICommand |
||||
{ |
||||
private readonly ILogger<MediaCollectionClearCommand> _logger; |
||||
private readonly string _serverUrl; |
||||
|
||||
public MediaCollectionClearCommand(IConfiguration configuration, ILogger<MediaCollectionClearCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_serverUrl = configuration["ServerUrl"]; |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "collection-name", Description = "The name of the media collection")] |
||||
public string Name { get; set; } |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
CancellationToken cancellationToken = console.GetCancellationToken(); |
||||
|
||||
Either<Error, Unit> result = await ClearMediaCollection(cancellationToken); |
||||
|
||||
result.Match( |
||||
_ => _logger.LogInformation("Successfully cleared media collection {MediaCollection}", Name), |
||||
error => _logger.LogError( |
||||
"Unable to clear media collection: {Error}", |
||||
error.Message)); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to clear media collection: {Error}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async Task<Either<Error, Unit>> ClearMediaCollection(CancellationToken cancellationToken) => |
||||
await EnsureMediaCollectionExists(cancellationToken) |
||||
.BindAsync(mediaCollectionId => ClearMediaCollectionImpl(mediaCollectionId, cancellationToken)); |
||||
|
||||
private async Task<Either<Error, int>> EnsureMediaCollectionExists(CancellationToken cancellationToken) |
||||
{ |
||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl); |
||||
Option<MediaCollectionViewModel> maybeExisting = |
||||
(await mediaCollectionsApi.ApiMediaCollectionsGetAsync(cancellationToken)) |
||||
.SingleOrDefault(mc => mc.Name == Name); |
||||
return await maybeExisting.MatchAsync( |
||||
existing => existing.Id, |
||||
async () => |
||||
{ |
||||
var data = new CreateSimpleMediaCollection(Name); |
||||
MediaCollectionViewModel result = |
||||
await mediaCollectionsApi.ApiMediaCollectionsPostAsync(data, cancellationToken); |
||||
return result.Id; |
||||
}); |
||||
} |
||||
|
||||
private async Task<Either<Error, Unit>> ClearMediaCollectionImpl( |
||||
int mediaCollectionId, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl); |
||||
await mediaCollectionsApi.ApiMediaCollectionsIdItemsPutAsync( |
||||
mediaCollectionId, |
||||
new List<int>(), |
||||
cancellationToken); |
||||
return unit; |
||||
} |
||||
} |
||||
} |
||||
@ -1,72 +0,0 @@
@@ -1,72 +0,0 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using LanguageExt.Common; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands.MediaCollections |
||||
{ |
||||
[Command("collection create", Description = "Creates a new media collection")] |
||||
public class MediaCollectionCreateCommand : ICommand |
||||
{ |
||||
private readonly ILogger<MediaCollectionCreateCommand> _logger; |
||||
private readonly string _serverUrl; |
||||
|
||||
public MediaCollectionCreateCommand(IConfiguration configuration, ILogger<MediaCollectionCreateCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_serverUrl = configuration["ServerUrl"]; |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "collection-name", Description = "The name of the media collection")] |
||||
public string Name { get; set; } |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
CancellationToken cancellationToken = console.GetCancellationToken(); |
||||
|
||||
Either<Error, Unit> result = await CreateMediaCollection(cancellationToken); |
||||
result.IfLeft(error => _logger.LogError("Unable to create media collection: {Error}", error.Message)); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to create media collection: {Error}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async Task<Either<Error, Unit>> CreateMediaCollection(CancellationToken cancellationToken) => |
||||
await EnsureMediaCollectionExists(cancellationToken); |
||||
|
||||
private async Task<Either<Error, Unit>> EnsureMediaCollectionExists(CancellationToken cancellationToken) |
||||
{ |
||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl); |
||||
|
||||
bool needToAdd = await mediaCollectionsApi |
||||
.ApiMediaCollectionsGetAsync(cancellationToken) |
||||
.Map(list => list.All(mc => mc.Name != Name)); |
||||
|
||||
if (needToAdd) |
||||
{ |
||||
var data = new CreateSimpleMediaCollection(Name); |
||||
await mediaCollectionsApi.ApiMediaCollectionsPostAsync(data, cancellationToken); |
||||
_logger.LogInformation("Successfully created media collection {MediaCollection}", Name); |
||||
} |
||||
else |
||||
{ |
||||
_logger.LogInformation("Media collection {MediaCollection} is already present", Name); |
||||
} |
||||
|
||||
return unit; |
||||
} |
||||
} |
||||
} |
||||
@ -1,107 +0,0 @@
@@ -1,107 +0,0 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands |
||||
{ |
||||
[Command("playout build", Description = "Builds a playout with the requested channel and schedule")] |
||||
public class PlayoutCommand : ICommand |
||||
{ |
||||
private readonly ILogger<PlayoutCommand> _logger; |
||||
private readonly string _serverUrl; |
||||
|
||||
public PlayoutCommand(IConfiguration configuration, ILogger<PlayoutCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_serverUrl = configuration["ServerUrl"]; |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "channel-number", Description = "The channel number")] |
||||
public int ChannelNumber { get; set; } |
||||
|
||||
[CommandParameter(1, Name = "schedule-name", Description = "The schedule name")] |
||||
public string ScheduleName { get; set; } |
||||
|
||||
// [Option("--type <type>")]
|
||||
// [Required]
|
||||
// public ProgramSchedulePlayoutType PlayoutType { get; set; }
|
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
CancellationToken cancellationToken = console.GetCancellationToken(); |
||||
|
||||
var channelsApi = new ChannelsApi(_serverUrl); |
||||
Option<ChannelViewModel> maybeChannel = await channelsApi.ApiChannelsGetAsync(cancellationToken) |
||||
.Map(list => list.SingleOrDefault(c => c.Number == ChannelNumber)); |
||||
|
||||
await maybeChannel.Match( |
||||
channel => BuildPlayout(cancellationToken, channel), |
||||
() => |
||||
{ |
||||
_logger.LogError("Unable to locate channel number {ChannelNumber}", ChannelNumber); |
||||
return ValueTask.CompletedTask; |
||||
}); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to build playout: {Error}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async ValueTask BuildPlayout(CancellationToken cancellationToken, ChannelViewModel channel) |
||||
{ |
||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl); |
||||
Option<ProgramScheduleViewModel> maybeSchedule = await programScheduleApi |
||||
.ApiSchedulesGetAsync(cancellationToken) |
||||
.Map(list => list.SingleOrDefault(s => s.Name == ScheduleName)); |
||||
|
||||
await maybeSchedule.Match( |
||||
schedule => SynchronizePlayoutAsync(channel.Id, schedule.Id, cancellationToken), |
||||
() => |
||||
{ |
||||
_logger.LogError("Unable to locate schedule {Schedule}", ScheduleName); |
||||
return ValueTask.CompletedTask; |
||||
}); |
||||
} |
||||
|
||||
private async ValueTask SynchronizePlayoutAsync( |
||||
int channelId, |
||||
int scheduleId, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var playoutApi = new PlayoutApi(_serverUrl); |
||||
Option<PlayoutViewModel> maybeExisting = await playoutApi.ApiPlayoutsGetAsync(cancellationToken) |
||||
.Map(list => list.SingleOrDefault(p => p.Channel.Id == channelId)); |
||||
await maybeExisting.Match( |
||||
existing => |
||||
{ |
||||
var data = new UpdatePlayout(existing.Id, channelId, scheduleId, ProgramSchedulePlayoutType.Flood); |
||||
if (existing.Channel.Id != data.ChannelId || |
||||
existing.ProgramSchedule.Id != data.ProgramScheduleId || |
||||
existing.ProgramSchedulePlayoutType != data.ProgramSchedulePlayoutType) |
||||
{ |
||||
return playoutApi.ApiPlayoutsPatchAsync(data, cancellationToken); |
||||
} |
||||
|
||||
return Task.CompletedTask; |
||||
}, |
||||
() => |
||||
{ |
||||
var data = new CreatePlayout(channelId, scheduleId, ProgramSchedulePlayoutType.Flood); |
||||
return playoutApi.ApiPlayoutsPostAsync(data, cancellationToken); |
||||
}); |
||||
|
||||
_logger.LogInformation("Successfully built playout for schedule {Schedule}", ScheduleName); |
||||
} |
||||
} |
||||
} |
||||
@ -1,140 +0,0 @@
@@ -1,140 +0,0 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands.Schedules |
||||
{ |
||||
[Command("schedule add-item", Description = "Adds an item to the end of a schedule")] |
||||
public class ScheduleAddItemCommand : ICommand |
||||
{ |
||||
private readonly ILogger<ScheduleAddItemCommand> _logger; |
||||
private readonly string _serverUrl; |
||||
|
||||
public ScheduleAddItemCommand(IConfiguration configuration, ILogger<ScheduleAddItemCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_serverUrl = configuration["ServerUrl"]; |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "schedule-name", Description = "The schedule name")] |
||||
public string ScheduleName { get; set; } |
||||
|
||||
[CommandParameter(1, Name = "collection-name", Description = "The media collection name")] |
||||
public string CollectionName { get; set; } |
||||
|
||||
// [CommandParameter(2, Description = "The collection playback order")]
|
||||
// public PlaybackOrder Order { get; set; }
|
||||
|
||||
[CommandOption("start-type", 's', Description = "The playout start type")] |
||||
public StartType StartType { get; set; } = StartType.Dynamic; |
||||
|
||||
[CommandOption("start-time", 't', Description = "The playout start time (of day)")] |
||||
public string StartTime { get; set; } = null; |
||||
|
||||
[CommandOption("playout-mode", 'm', Description = "The playout mode")] |
||||
public PlayoutMode PlayoutMode { get; set; } = PlayoutMode.Flood; |
||||
|
||||
[CommandOption( |
||||
"multiple-count", |
||||
'c', |
||||
Description = "How many items to play from the collection (for Multiple playout mode)")] |
||||
public int? MultipleCount { get; set; } = null; |
||||
|
||||
[CommandOption( |
||||
"playout-duration", |
||||
'd', |
||||
Description = "How long to play items from the collection (for Duration playout mode)")] |
||||
public string PlayoutDuration { get; set; } = null; |
||||
|
||||
[CommandOption( |
||||
"offline-tail", |
||||
'o', |
||||
Description = |
||||
"Whether to remain offline for the entire duration, or to start the next item immediately (for Duration playout mode)")] |
||||
public bool? OfflineTail { get; set; } = null; |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
CancellationToken cancellationToken = console.GetCancellationToken(); |
||||
|
||||
Option<ProgramScheduleViewModel> maybeSchedule = await GetSchedule(cancellationToken); |
||||
await maybeSchedule.Match( |
||||
programSchedule => AddItemToSchedule(cancellationToken, programSchedule), |
||||
() => |
||||
{ |
||||
_logger.LogError("Unable to locate schedule {Schedule}", ScheduleName); |
||||
return ValueTask.CompletedTask; |
||||
}); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to add item to schedule: {Error}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async ValueTask AddItemToSchedule( |
||||
CancellationToken cancellationToken, |
||||
ProgramScheduleViewModel programSchedule) |
||||
{ |
||||
var mediaCollectionsApi = new MediaCollectionsApi(_serverUrl); |
||||
Option<MediaCollectionViewModel> maybeMediaCollection = await mediaCollectionsApi |
||||
.ApiMediaCollectionsGetAsync(cancellationToken) |
||||
.Map(list => list.SingleOrDefault(mc => mc.Name == CollectionName)); |
||||
|
||||
await maybeMediaCollection.Match( |
||||
collection => |
||||
AddScheduleItem(programSchedule.Id, collection.Id, cancellationToken), |
||||
() => |
||||
{ |
||||
_logger.LogError( |
||||
"Unable to locate collection {Collection}", |
||||
CollectionName); |
||||
return Task.CompletedTask; |
||||
}); |
||||
} |
||||
|
||||
private async Task<Option<ProgramScheduleViewModel>> GetSchedule(CancellationToken cancellationToken) |
||||
{ |
||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl); |
||||
return await programScheduleApi.ApiSchedulesGetAsync(cancellationToken) |
||||
.Map(list => list.SingleOrDefault(schedule => schedule.Name == ScheduleName)); |
||||
} |
||||
|
||||
private async Task AddScheduleItem( |
||||
int programScheduleId, |
||||
int mediaCollectionId, |
||||
CancellationToken cancellationToken) |
||||
{ |
||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl); |
||||
|
||||
var request = new AddProgramScheduleItem |
||||
{ |
||||
ProgramScheduleId = programScheduleId, |
||||
StartType = StartType, |
||||
StartTime = StartTime, |
||||
PlayoutMode = PlayoutMode, |
||||
MediaCollectionId = mediaCollectionId, |
||||
PlayoutDuration = PlayoutDuration, |
||||
MultipleCount = MultipleCount, |
||||
OfflineTail = OfflineTail |
||||
}; |
||||
|
||||
await programScheduleApi.ApiSchedulesItemsAddPostAsync(request, cancellationToken); |
||||
|
||||
_logger.LogInformation( |
||||
"Collection {Collection} has been added to schedule {Schedule}", |
||||
CollectionName, |
||||
ScheduleName); |
||||
} |
||||
} |
||||
} |
||||
@ -1,86 +0,0 @@
@@ -1,86 +0,0 @@
|
||||
using System; |
||||
using System.Linq; |
||||
using System.Threading; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using CliFx.Attributes; |
||||
using ErsatzTV.Api.Sdk.Api; |
||||
using ErsatzTV.Api.Sdk.Model; |
||||
using LanguageExt; |
||||
using LanguageExt.Common; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.Logging; |
||||
using static LanguageExt.Prelude; |
||||
|
||||
namespace ErsatzTV.CommandLine.Commands.Schedules |
||||
{ |
||||
[Command("schedule create", Description = "Creates a new schedule")] |
||||
public class ScheduleCreateCommand : ICommand |
||||
{ |
||||
private readonly ILogger<ScheduleCreateCommand> _logger; |
||||
private readonly string _serverUrl; |
||||
|
||||
public ScheduleCreateCommand(IConfiguration configuration, ILogger<ScheduleCreateCommand> logger) |
||||
{ |
||||
_logger = logger; |
||||
_serverUrl = configuration["ServerUrl"]; |
||||
} |
||||
|
||||
[CommandParameter(0, Name = "schedule-name", Description = "The schedule name")] |
||||
public string Name { get; set; } |
||||
|
||||
[CommandParameter(1, Name = "playback-order", Description = "The collection playback order")] |
||||
public PlaybackOrder Order { get; set; } |
||||
|
||||
[CommandOption("reset", Description = "Resets the schedule to contain no items")] |
||||
public bool Reset { get; set; } |
||||
|
||||
public async ValueTask ExecuteAsync(IConsole console) |
||||
{ |
||||
try |
||||
{ |
||||
CancellationToken cancellationToken = console.GetCancellationToken(); |
||||
|
||||
Either<Error, Unit> result = await EnsureScheduleExistsAsync(cancellationToken); |
||||
result.IfLeft(error => _logger.LogError("Unable to create schedule: {Error}", error.Message)); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
_logger.LogError("Unable to create schedule: {Error}", ex.Message); |
||||
} |
||||
} |
||||
|
||||
private async Task<Either<Error, Unit>> EnsureScheduleExistsAsync(CancellationToken cancellationToken) |
||||
{ |
||||
var programScheduleApi = new ProgramScheduleApi(_serverUrl); |
||||
|
||||
Option<ProgramScheduleViewModel> maybeExisting = await programScheduleApi |
||||
.ApiSchedulesGetAsync(cancellationToken) |
||||
.Map(list => list.SingleOrDefault(schedule => schedule.Name == Name)); |
||||
|
||||
await maybeExisting.Match( |
||||
existing => |
||||
{ |
||||
// TODO: update playback order if changed?
|
||||
_logger.LogInformation("Schedule {Schedule} is already present", Name); |
||||
|
||||
if (Reset) |
||||
{ |
||||
return programScheduleApi |
||||
.ApiSchedulesProgramScheduleIdItemsDeleteAsync(existing.Id, cancellationToken) |
||||
.Iter(_ => _logger.LogInformation("Successfully reset schedule {Schedule}", Name)); |
||||
} |
||||
|
||||
return Task.CompletedTask; |
||||
}, |
||||
() => |
||||
{ |
||||
var data = new CreateProgramSchedule(Name, Order); |
||||
return programScheduleApi.ApiSchedulesPostAsync(data, cancellationToken) |
||||
.Iter(_ => _logger.LogInformation("Successfully created schedule {Schedule}", Name)); |
||||
}); |
||||
|
||||
return unit; |
||||
} |
||||
} |
||||
} |
||||
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
namespace ErsatzTV.CommandLine |
||||
{ |
||||
public class Config |
||||
{ |
||||
public string ServerUrl { get; set; } |
||||
} |
||||
} |
||||
@ -1,10 +0,0 @@
@@ -1,10 +0,0 @@
|
||||
namespace ErsatzTV.CommandLine |
||||
{ |
||||
public enum DesiredResolution |
||||
{ |
||||
W720H480 = 1, |
||||
W1280H720 = 2, |
||||
W1920H1080 = 3, |
||||
W3840H2160 = 4 |
||||
} |
||||
} |
||||
@ -1,35 +0,0 @@
@@ -1,35 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk"> |
||||
|
||||
<PropertyGroup> |
||||
<OutputType>Exe</OutputType> |
||||
<TargetFramework>net5.0</TargetFramework> |
||||
<AssemblyName>ersatztv-cli</AssemblyName> |
||||
<LangVersion>9</LangVersion> |
||||
<PackageVersion>0.0.1</PackageVersion> |
||||
<AssemblyVersion>0.0.1</AssemblyVersion> |
||||
</PropertyGroup> |
||||
|
||||
<ItemGroup> |
||||
<PackageReference Include="CliFx" Version="1.6.0" /> |
||||
<PackageReference Include="LanguageExt.Core" Version="3.4.15" /> |
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" /> |
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="5.0.0" /> |
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" /> |
||||
<PackageReference Include="RestSharp" Version="106.11.7" /> |
||||
<PackageReference Include="Serilog" Version="2.10.0" /> |
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="4.1.2" /> |
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" /> |
||||
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<ProjectReference Include="..\generated\ErsatzTV.Api.Sdk\src\ErsatzTV.Api.Sdk\ErsatzTV.Api.Sdk.csproj" /> |
||||
</ItemGroup> |
||||
|
||||
<ItemGroup> |
||||
<Reference Include="Microsoft.Extensions.Hosting.Abstractions, Version=5.0.0.0, Culture=neutral, PublicKeyToken=adb9793829ddae60"> |
||||
<HintPath>..\..\..\..\..\..\usr\share\dotnet\packs\Microsoft.AspNetCore.App.Ref\5.0.0\ref\net5.0\Microsoft.Extensions.Hosting.Abstractions.dll</HintPath> |
||||
</Reference> |
||||
</ItemGroup> |
||||
|
||||
</Project> |
||||
@ -1,75 +0,0 @@
@@ -1,75 +0,0 @@
|
||||
using System; |
||||
using System.Collections.Generic; |
||||
using System.IO; |
||||
using System.Linq; |
||||
using System.Threading.Tasks; |
||||
using CliFx; |
||||
using Microsoft.Extensions.Configuration; |
||||
using Microsoft.Extensions.DependencyInjection; |
||||
using Microsoft.Extensions.Hosting; |
||||
using Serilog; |
||||
using Serilog.Events; |
||||
|
||||
namespace ErsatzTV.CommandLine |
||||
{ |
||||
public class Program |
||||
{ |
||||
public static async Task<int> Main(string[] args) |
||||
{ |
||||
Log.Logger = new LoggerConfiguration() |
||||
.MinimumLevel.Information() |
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning) |
||||
.Enrich.FromLogContext() |
||||
.WriteTo.Console() |
||||
.CreateLogger(); |
||||
|
||||
IHost host = CreateHostBuilder(args).Build(); |
||||
try |
||||
{ |
||||
return await new CliApplicationBuilder() |
||||
.AddCommandsFromThisAssembly() |
||||
.UseTypeActivator(host.Services.GetService) |
||||
.Build() |
||||
.RunAsync(args); |
||||
} |
||||
catch (Exception ex) |
||||
{ |
||||
Log.Fatal(ex, "Host terminated unexpectedly"); |
||||
return 1; |
||||
} |
||||
finally |
||||
{ |
||||
Log.CloseAndFlush(); |
||||
} |
||||
} |
||||
|
||||
public static IHostBuilder CreateHostBuilder(string[] args) => |
||||
Host.CreateDefaultBuilder(args) |
||||
.ConfigureServices( |
||||
(_, services) => |
||||
{ |
||||
services.AddSingleton<IConsole, SystemConsole>(); |
||||
IEnumerable<Type> typesThatImplementICommand = typeof(Program).Assembly.GetTypes() |
||||
.Where(x => typeof(ICommand).IsAssignableFrom(x)) |
||||
.Where(x => !x.IsAbstract); |
||||
foreach (Type t in typesThatImplementICommand) |
||||
{ |
||||
services.AddTransient(t); |
||||
} |
||||
}) |
||||
.ConfigureAppConfiguration( |
||||
(_, configuration) => |
||||
{ |
||||
configuration.Sources.Clear(); |
||||
|
||||
string configFolder = Path.Combine( |
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), |
||||
"ersatztv"); |
||||
|
||||
configuration.SetBasePath(configFolder); |
||||
configuration.AddJsonFile("cli.json", true, true); |
||||
}) |
||||
.UseSerilog() |
||||
.UseConsoleLifetime(); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue