add v2 ffmpeg profile page (#768)
* wip * remove transcode property; use i18n * add api * use computed table headers for i18n
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
using ErsatzTV.Core.Api.FFmpegProfiles;
|
||||
|
||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||
|
||||
public record GetAllFFmpegProfilesForApi : IRequest<List<FFmpegProfileResponseModel>>;
|
||||
@@ -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<GetAllFFmpegProfilesForApi, List<FFmpegProfileResponseModel>>
|
||||
{
|
||||
private readonly IDbContextFactory<TvContext> _dbContextFactory;
|
||||
|
||||
public GetAllFFmpegProfilesForApiHandler(IDbContextFactory<TvContext> dbContextFactory) =>
|
||||
_dbContextFactory = dbContextFactory;
|
||||
|
||||
public async Task<List<FFmpegProfileResponseModel>> Handle(
|
||||
GetAllFFmpegProfilesForApi request,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
await using TvContext dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||
List<FFmpegProfile> ffmpegProfiles = await dbContext.FFmpegProfiles
|
||||
.AsNoTracking()
|
||||
.Include(p => p.Resolution)
|
||||
.ToListAsync(cancellationToken);
|
||||
return ffmpegProfiles.Map(ProjectToResponseModel).ToList();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace ErsatzTV.Core.Api.FFmpegProfiles;
|
||||
|
||||
public record FFmpegProfileResponseModel(
|
||||
int Id,
|
||||
string Name,
|
||||
string Resolution,
|
||||
string Video,
|
||||
string Audio);
|
||||
18
ErsatzTV/Controllers/Api/FFmpegProfileController.cs
Normal file
18
ErsatzTV/Controllers/Api/FFmpegProfileController.cs
Normal file
@@ -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<List<FFmpegProfileResponseModel>> GetAll() =>
|
||||
await _mediator.Send(new GetAllFFmpegProfilesForApi());
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,10 @@
|
||||
"title": "Início"
|
||||
},
|
||||
"ffmpeg-profiles": {
|
||||
"title": "Perfis FFmpeg"
|
||||
"title": "Perfis FFmpeg",
|
||||
"table": {
|
||||
"name": "Nome"
|
||||
}
|
||||
},
|
||||
"watermarks": {
|
||||
"title": "Marcas d'água"
|
||||
|
||||
7
ErsatzTV/client-app/src/models/FFmpegProfile.ts
Normal file
7
ErsatzTV/client-app/src/models/FFmpegProfile.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface FFmpegProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
resolution: string;
|
||||
video: string;
|
||||
audio: string;
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
18
ErsatzTV/client-app/src/services/FFmpegProfileService.ts
Normal file
18
ErsatzTV/client-app/src/services/FFmpegProfileService.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { AbstractApiService } from './AbstractApiService';
|
||||
import { FFmpegProfile } from '@/models/FFmpegProfile';
|
||||
|
||||
class FFmpegProfileApiService extends AbstractApiService {
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public getAll(): Promise<FFmpegProfile[]> {
|
||||
return this.http
|
||||
.get('/api/ffmpeg/profiles')
|
||||
.then(this.handleResponse.bind(this))
|
||||
.catch(this.handleError.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
export const ffmpegProfileApiService: FFmpegProfileApiService =
|
||||
new FFmpegProfileApiService();
|
||||
@@ -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';
|
||||
|
||||
|
||||
40
ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue
Normal file
40
ErsatzTV/client-app/src/views/FFmpegProfilesPage.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
:items="ffmpegProfiles"
|
||||
:sort-by="['name']"
|
||||
class="elevation-1"
|
||||
>
|
||||
</v-data-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Vue, Component } from 'vue-property-decorator';
|
||||
import { FFmpegProfile } from '@/models/FFmpegProfile';
|
||||
import { ffmpegProfileApiService } from '@/services/FFmpegProfileService';
|
||||
|
||||
@Component
|
||||
export default class FFmpegProfiles extends Vue {
|
||||
private ffmpegProfiles: FFmpegProfile[] = [];
|
||||
|
||||
get headers() {
|
||||
return [
|
||||
{ text: this.$t('ffmpeg-profiles.table.name'), value: 'name' },
|
||||
{
|
||||
text: this.$t('ffmpeg-profiles.table.resolution'),
|
||||
value: 'resolution'
|
||||
},
|
||||
{ text: this.$t('ffmpeg-profiles.table.video'), value: 'video' },
|
||||
{ text: this.$t('ffmpeg-profiles.table.audio'), value: 'audio' }
|
||||
];
|
||||
}
|
||||
|
||||
title: string = 'FFMpeg Profiles';
|
||||
|
||||
async mounted(): Promise<void> {
|
||||
this.ffmpegProfiles = await ffmpegProfileApiService.getAll();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -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": "",
|
||||
|
||||
Reference in New Issue
Block a user