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.Application.Resolutions;
|
||||||
|
using ErsatzTV.Core.Api.FFmpegProfiles;
|
||||||
using ErsatzTV.Core.Domain;
|
using ErsatzTV.Core.Domain;
|
||||||
|
|
||||||
namespace ErsatzTV.Application.FFmpegProfiles;
|
namespace ErsatzTV.Application.FFmpegProfiles;
|
||||||
@@ -26,6 +27,14 @@ internal static class Mapper
|
|||||||
profile.NormalizeFramerate,
|
profile.NormalizeFramerate,
|
||||||
profile.DeinterlaceVideo == true);
|
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) =>
|
private static ResolutionViewModel Project(Resolution resolution) =>
|
||||||
new(resolution.Id, resolution.Name, resolution.Width, resolution.Height);
|
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(
|
public record ChannelResponseModel(
|
||||||
int Id,
|
int Id,
|
||||||
string Number,
|
string Number,
|
||||||
string Name,
|
string Name,
|
||||||
string FFmpegProfile,
|
[property: JsonProperty("ffmpegProfile")] string FFmpegProfile,
|
||||||
string Language,
|
string Language,
|
||||||
string StreamingMode);
|
string StreamingMode);
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ErsatzTV.Core.Api.FFmpegProfiles;
|
||||||
|
|
||||||
|
public record FFmpegProfileResponseModel(
|
||||||
|
int Id,
|
||||||
|
string Name,
|
||||||
|
string Resolution,
|
||||||
|
string Video,
|
||||||
|
string Audio);
|
||||||
@@ -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"
|
"title": "Home"
|
||||||
},
|
},
|
||||||
"ffmpeg-profiles": {
|
"ffmpeg-profiles": {
|
||||||
"title": "FFmpeg Profiles"
|
"title": "FFmpeg Profiles",
|
||||||
|
"table": {
|
||||||
|
"name": "Name",
|
||||||
|
"resolution": "Resolution",
|
||||||
|
"video": "Video",
|
||||||
|
"audio": "Audio"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"watermarks": {
|
"watermarks": {
|
||||||
"title": "Watermarks"
|
"title": "Watermarks"
|
||||||
@@ -165,4 +171,4 @@
|
|||||||
"ffmpeg-profile": "FFmpeg Profile"
|
"ffmpeg-profile": "FFmpeg Profile"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,10 @@
|
|||||||
"title": "Início"
|
"title": "Início"
|
||||||
},
|
},
|
||||||
"ffmpeg-profiles": {
|
"ffmpeg-profiles": {
|
||||||
"title": "Perfis FFmpeg"
|
"title": "Perfis FFmpeg",
|
||||||
|
"table": {
|
||||||
|
"name": "Nome"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"watermarks": {
|
"watermarks": {
|
||||||
"title": "Marcas d'água"
|
"title": "Marcas d'água"
|
||||||
|
|||||||
@@ -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 VueRouter from 'vue-router';
|
||||||
import HomePage from '../views/HomePage.vue';
|
import HomePage from '../views/HomePage.vue';
|
||||||
import ChannelsPage from '../views/ChannelsPage.vue';
|
import ChannelsPage from '../views/ChannelsPage.vue';
|
||||||
|
import FFmpegProfilesPage from '../views/FFmpegProfilesPage.vue';
|
||||||
|
|
||||||
Vue.use(VueRouter);
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
@@ -27,9 +28,10 @@ const routes = [
|
|||||||
{
|
{
|
||||||
path: '/ffmpeg-profiles',
|
path: '/ffmpeg-profiles',
|
||||||
name: 'ffmpeg-profiles.title',
|
name: 'ffmpeg-profiles.title',
|
||||||
|
component: FFmpegProfilesPage,
|
||||||
meta: {
|
meta: {
|
||||||
icon: 'mdi-video-input-component',
|
icon: 'mdi-video-input-component',
|
||||||
disabled: true
|
disabled: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 {
|
export default class Channels extends Vue {
|
||||||
private channels: Channel[] = [];
|
private channels: Channel[] = [];
|
||||||
|
|
||||||
private headers = [
|
get headers() {
|
||||||
{ text: this.$t('channels.table.number'), value: 'number' },
|
return [
|
||||||
{ text: this.$t('channels.table.logo'), value: 'logo' },
|
{ text: this.$t('channels.table.number'), value: 'number' },
|
||||||
{ text: this.$t('channels.table.name'), value: 'name' },
|
{ text: this.$t('channels.table.logo'), value: 'logo' },
|
||||||
{ text: this.$t('channels.table.language'), value: 'language' },
|
{ text: this.$t('channels.table.name'), value: 'name' },
|
||||||
{ text: this.$t('channels.table.mode'), value: 'streamingMode' },
|
{ 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'
|
text: this.$t('channels.table.ffmpeg-profile'),
|
||||||
}
|
value: 'ffmpegProfile'
|
||||||
];
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
title: string = 'Channels';
|
title: string = 'Channels';
|
||||||
|
|
||||||
|
|||||||
@@ -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": [
|
"responses": [
|
||||||
{
|
{
|
||||||
"uuid": "2f42cd38-2591-475f-a4bf-e5fb3455a8b3",
|
"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,
|
"latency": 0,
|
||||||
"statusCode": 200,
|
"statusCode": 200,
|
||||||
"label": "",
|
"label": "",
|
||||||
|
|||||||
Reference in New Issue
Block a user