From 780ebc01eeb6966c34c61fa7c0e27d49baf27e7d Mon Sep 17 00:00:00 2001 From: Jason Dove Date: Thu, 28 Apr 2022 06:56:01 -0500 Subject: [PATCH] add v2 ffmpeg profile page (#768) * wip * remove transcode property; use i18n * add api * use computed table headers for i18n --- ErsatzTV.Application/FFmpegProfiles/Mapper.cs | 9 +++++ .../Queries/GetAllFFmpegProfilesForApi.cs | 5 +++ .../GetAllFFmpegProfilesForApiHandler.cs | 28 +++++++++++++ .../Api/Channels/ChannelResponseModel.cs | 6 ++- .../FFmpegProfileResponseModel.cs | 8 ++++ .../Api/FFmpegProfileController.cs | 18 +++++++++ ErsatzTV/client-app/src/locales/en.json | 10 ++++- ErsatzTV/client-app/src/locales/pt-br.json | 5 ++- .../client-app/src/models/FFmpegProfile.ts | 7 ++++ ErsatzTV/client-app/src/router/index.js | 4 +- .../src/services/FFmpegProfileService.ts | 18 +++++++++ .../client-app/src/views/ChannelsPage.vue | 24 ++++++----- .../src/views/FFmpegProfilesPage.vue | 40 +++++++++++++++++++ api/ersatztv-mock-api.json | 2 +- 14 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApi.cs create mode 100644 ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApiHandler.cs create mode 100644 ErsatzTV.Core/Api/FFmpegProfiles/FFmpegProfileResponseModel.cs create mode 100644 ErsatzTV/Controllers/Api/FFmpegProfileController.cs create mode 100644 ErsatzTV/client-app/src/models/FFmpegProfile.ts create mode 100644 ErsatzTV/client-app/src/services/FFmpegProfileService.ts create mode 100644 ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue diff --git a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs index c2b536338..a92d27108 100644 --- a/ErsatzTV.Application/FFmpegProfiles/Mapper.cs +++ b/ErsatzTV.Application/FFmpegProfiles/Mapper.cs @@ -1,4 +1,5 @@ using ErsatzTV.Application.Resolutions; +using ErsatzTV.Core.Api.FFmpegProfiles; using ErsatzTV.Core.Domain; namespace ErsatzTV.Application.FFmpegProfiles; @@ -26,6 +27,14 @@ internal static class Mapper profile.NormalizeFramerate, profile.DeinterlaceVideo == true); + internal static FFmpegProfileResponseModel ProjectToResponseModel(FFmpegProfile ffmpegProfile) => + new( + ffmpegProfile.Id, + ffmpegProfile.Name, + $"{ffmpegProfile.Resolution.Width}x{ffmpegProfile.Resolution.Height}", + ffmpegProfile.VideoFormat.ToString().ToLowerInvariant(), + ffmpegProfile.AudioFormat.ToString().ToLowerInvariant()); + private static ResolutionViewModel Project(Resolution resolution) => new(resolution.Id, resolution.Name, resolution.Width, resolution.Height); } diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApi.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApi.cs new file mode 100644 index 000000000..a020ff83f --- /dev/null +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApi.cs @@ -0,0 +1,5 @@ +using ErsatzTV.Core.Api.FFmpegProfiles; + +namespace ErsatzTV.Application.FFmpegProfiles; + +public record GetAllFFmpegProfilesForApi : IRequest>; diff --git a/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApiHandler.cs b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApiHandler.cs new file mode 100644 index 000000000..9e620e55a --- /dev/null +++ b/ErsatzTV.Application/FFmpegProfiles/Queries/GetAllFFmpegProfilesForApiHandler.cs @@ -0,0 +1,28 @@ +using ErsatzTV.Core.Api.FFmpegProfiles; +using ErsatzTV.Core.Domain; +using ErsatzTV.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using static ErsatzTV.Application.FFmpegProfiles.Mapper; + +namespace ErsatzTV.Application.FFmpegProfiles; + +public class + GetAllFFmpegProfilesForApiHandler : IRequestHandler> +{ + private readonly IDbContextFactory _dbContextFactory; + + public GetAllFFmpegProfilesForApiHandler(IDbContextFactory dbContextFactory) => + _dbContextFactory = dbContextFactory; + + public async Task> Handle( + GetAllFFmpegProfilesForApi request, + CancellationToken cancellationToken) + { + await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + List ffmpegProfiles = await dbContext.FFmpegProfiles + .AsNoTracking() + .Include(p => p.Resolution) + .ToListAsync(cancellationToken); + return ffmpegProfiles.Map(ProjectToResponseModel).ToList(); + } +} diff --git a/ErsatzTV.Core/Api/Channels/ChannelResponseModel.cs b/ErsatzTV.Core/Api/Channels/ChannelResponseModel.cs index 129aca437..bb351ff61 100644 --- a/ErsatzTV.Core/Api/Channels/ChannelResponseModel.cs +++ b/ErsatzTV.Core/Api/Channels/ChannelResponseModel.cs @@ -1,9 +1,11 @@ -namespace ErsatzTV.Core.Api.Channels; +using Newtonsoft.Json; + +namespace ErsatzTV.Core.Api.Channels; public record ChannelResponseModel( int Id, string Number, string Name, - string FFmpegProfile, + [property: JsonProperty("ffmpegProfile")] string FFmpegProfile, string Language, string StreamingMode); diff --git a/ErsatzTV.Core/Api/FFmpegProfiles/FFmpegProfileResponseModel.cs b/ErsatzTV.Core/Api/FFmpegProfiles/FFmpegProfileResponseModel.cs new file mode 100644 index 000000000..0884a2ed8 --- /dev/null +++ b/ErsatzTV.Core/Api/FFmpegProfiles/FFmpegProfileResponseModel.cs @@ -0,0 +1,8 @@ +namespace ErsatzTV.Core.Api.FFmpegProfiles; + +public record FFmpegProfileResponseModel( + int Id, + string Name, + string Resolution, + string Video, + string Audio); diff --git a/ErsatzTV/Controllers/Api/FFmpegProfileController.cs b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs new file mode 100644 index 000000000..95837f37d --- /dev/null +++ b/ErsatzTV/Controllers/Api/FFmpegProfileController.cs @@ -0,0 +1,18 @@ +using ErsatzTV.Application.FFmpegProfiles; +using ErsatzTV.Core.Api.FFmpegProfiles; +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace ErsatzTV.Controllers.Api; + +[ApiController] +public class FFmpegProfileController +{ + private readonly IMediator _mediator; + + public FFmpegProfileController(IMediator mediator) => _mediator = mediator; + + [HttpGet("/api/ffmpeg/profiles")] + public async Task> GetAll() => + await _mediator.Send(new GetAllFFmpegProfilesForApi()); +} diff --git a/ErsatzTV/client-app/src/locales/en.json b/ErsatzTV/client-app/src/locales/en.json index 22b861240..8a8a9a6d1 100644 --- a/ErsatzTV/client-app/src/locales/en.json +++ b/ErsatzTV/client-app/src/locales/en.json @@ -86,7 +86,13 @@ "title": "Home" }, "ffmpeg-profiles": { - "title": "FFmpeg Profiles" + "title": "FFmpeg Profiles", + "table": { + "name": "Name", + "resolution": "Resolution", + "video": "Video", + "audio": "Audio" + } }, "watermarks": { "title": "Watermarks" @@ -165,4 +171,4 @@ "ffmpeg-profile": "FFmpeg Profile" } } -} \ No newline at end of file +} diff --git a/ErsatzTV/client-app/src/locales/pt-br.json b/ErsatzTV/client-app/src/locales/pt-br.json index 53f186b3d..56da83c31 100644 --- a/ErsatzTV/client-app/src/locales/pt-br.json +++ b/ErsatzTV/client-app/src/locales/pt-br.json @@ -86,7 +86,10 @@ "title": "Início" }, "ffmpeg-profiles": { - "title": "Perfis FFmpeg" + "title": "Perfis FFmpeg", + "table": { + "name": "Nome" + } }, "watermarks": { "title": "Marcas d'água" diff --git a/ErsatzTV/client-app/src/models/FFmpegProfile.ts b/ErsatzTV/client-app/src/models/FFmpegProfile.ts new file mode 100644 index 000000000..971468379 --- /dev/null +++ b/ErsatzTV/client-app/src/models/FFmpegProfile.ts @@ -0,0 +1,7 @@ +export interface FFmpegProfile { + id: number; + name: string; + resolution: string; + video: string; + audio: string; +} diff --git a/ErsatzTV/client-app/src/router/index.js b/ErsatzTV/client-app/src/router/index.js index d0928150b..79a738a11 100644 --- a/ErsatzTV/client-app/src/router/index.js +++ b/ErsatzTV/client-app/src/router/index.js @@ -2,6 +2,7 @@ import VueRouter from 'vue-router'; import HomePage from '../views/HomePage.vue'; import ChannelsPage from '../views/ChannelsPage.vue'; +import FFmpegProfilesPage from '../views/FFmpegProfilesPage.vue'; Vue.use(VueRouter); @@ -27,9 +28,10 @@ const routes = [ { path: '/ffmpeg-profiles', name: 'ffmpeg-profiles.title', + component: FFmpegProfilesPage, meta: { icon: 'mdi-video-input-component', - disabled: true + disabled: false } }, { diff --git a/ErsatzTV/client-app/src/services/FFmpegProfileService.ts b/ErsatzTV/client-app/src/services/FFmpegProfileService.ts new file mode 100644 index 000000000..121d235bf --- /dev/null +++ b/ErsatzTV/client-app/src/services/FFmpegProfileService.ts @@ -0,0 +1,18 @@ +import { AbstractApiService } from './AbstractApiService'; +import { FFmpegProfile } from '@/models/FFmpegProfile'; + +class FFmpegProfileApiService extends AbstractApiService { + public constructor() { + super(); + } + + public getAll(): Promise { + return this.http + .get('/api/ffmpeg/profiles') + .then(this.handleResponse.bind(this)) + .catch(this.handleError.bind(this)); + } +} + +export const ffmpegProfileApiService: FFmpegProfileApiService = + new FFmpegProfileApiService(); diff --git a/ErsatzTV/client-app/src/views/ChannelsPage.vue b/ErsatzTV/client-app/src/views/ChannelsPage.vue index 7a1c482b2..76c6ed7e1 100644 --- a/ErsatzTV/client-app/src/views/ChannelsPage.vue +++ b/ErsatzTV/client-app/src/views/ChannelsPage.vue @@ -18,17 +18,19 @@ import { channelApiService } from '@/services/ChannelService'; export default class Channels extends Vue { private channels: Channel[] = []; - private headers = [ - { text: this.$t('channels.table.number'), value: 'number' }, - { text: this.$t('channels.table.logo'), value: 'logo' }, - { text: this.$t('channels.table.name'), value: 'name' }, - { text: this.$t('channels.table.language'), value: 'language' }, - { text: this.$t('channels.table.mode'), value: 'streamingMode' }, - { - text: this.$t('channels.table.ffmpeg-profile'), - value: 'ffmpegProfile' - } - ]; + get headers() { + return [ + { text: this.$t('channels.table.number'), value: 'number' }, + { text: this.$t('channels.table.logo'), value: 'logo' }, + { text: this.$t('channels.table.name'), value: 'name' }, + { text: this.$t('channels.table.language'), value: 'language' }, + { text: this.$t('channels.table.mode'), value: 'streamingMode' }, + { + text: this.$t('channels.table.ffmpeg-profile'), + value: 'ffmpegProfile' + } + ]; + } title: string = 'Channels'; diff --git a/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue b/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue new file mode 100644 index 000000000..b601708ff --- /dev/null +++ b/ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue @@ -0,0 +1,40 @@ + + + diff --git a/api/ersatztv-mock-api.json b/api/ersatztv-mock-api.json index cceb285b2..9fb30bb31 100644 --- a/api/ersatztv-mock-api.json +++ b/api/ersatztv-mock-api.json @@ -40,7 +40,7 @@ "responses": [ { "uuid": "2f42cd38-2591-475f-a4bf-e5fb3455a8b3", - "body": "[\n {{#repeat (faker 'datatype.number' min=2 max=3)}}\n { \n \"id\": {{@index}},\n \"name\": \"{{faker 'hacker.adjective'}} {{faker 'hacker.noun'}}\",\n \"transcode\": {{faker 'datatype.boolean'}},\n \"resolution\": \"{{oneOf (array '1920x1080' '1280x720' '720x480')}}\",\n \"videoCodec\": \"{{oneOf (array 'hevc_nvenc' 'h264_nvenc')}}\",\n \"audioCodec\": \"{{oneOf (array 'aac' 'ac3')}}\"\n }\n {{/repeat}}\n]", + "body": "[\n {{#repeat (faker 'datatype.number' min=2 max=3)}}\n { \n \"id\": {{@index}},\n \"name\": \"{{faker 'hacker.adjective'}} {{faker 'hacker.noun'}}\",\n \"resolution\": \"{{oneOf (array '1920x1080' '1280x720' '720x480')}}\",\n \"video\": \"{{oneOf (array 'hevc' 'h264')}}{{oneOf (array ' / nvenc' ' / qsv' ' / vaapi' '')}}\",\n \"audio\": \"{{oneOf (array 'aac' 'ac3')}}\"\n }\n {{/repeat}}\n]", "latency": 0, "statusCode": 200, "label": "",