Írjunk FFmpeg frontend-et – 3. rész
Az előző részben beállítottuk a GIT tárolónkat és a CI/CD pipeline környezetet. Ebben a cikkben belevágunk a problémakör megoldásának megvalósításába, a preset modellezésbe.
A presetek részletes specifikációja alapján viszonylag könnyen készíthető egy modell a leírásra. Az egyes presetek leírásait tárolhatjuk kódban is, vagy valami adatfájlban. A programban tárolás ellen szól, hogy így minden egyes preset módosítás esetén a programot újra kellene fordítanunk, ami nem feltétlen a legjobb megoldás, mivel a későbbiekben limitálja a bővíthetőséget. Adatfájlban tárolás esetén problémát az okoz, hogy az egyes presetek esetén például hogyan írjuk le a paraméterek validálását, vagy mondjuk hogyan valósítjuk meg azt, hogy egyes beállítási lehetőségek opcionálisak lehetnek?
Ennek a kivitelezése kódban triviális, hiszen egy if-else
elágazással megoldható a probléma, de az adat esetén már nem feltétlen evidens. Ha a logika leírását is beletesszük a preset definícióba, akkor nagyon könnyen abban a szituációban találhatjuk magunkat, hogy egy domain specifikus nyelvet (DSL) fejlesztünk a megoldandó probléma modellezésére.
A DSL nyelvek lényegében programozási nyelvek, amelyeknek nem feltétlen kell Turing teljesnek lenniük. A probléma általában az, hogy ezek a nyelvek egyszerűnek indulnak, de általában az idő előrehaladtával mindenre is jók lesznek, ami csak problémákat szül. Ezek miatt a problémák miatt az esetek nagyon kis százalékában éri meg saját DSL nyelvet fejleszteni. Ez is egy ilyen szituáció.
Persze adódhat a jogos felvetés, hogy miért nem LUA alapon valósítjuk meg a preseteket? Játékfejlesztők előszeretettel használják objektum leírásra és logika megvalósításra. Jelen esetben is használható lenne, de a KISS elv mentén egyenlőre nem látom ennek szükségességét.
Ezért végső soron az XML formátum mellett döntöttem, mivel ez kézzel is szépen formázható, illetve szerintem beszédesebb egy picit leírásra, mint egy JSON fáj, mivel közelebb áll a HTML-hez. Ez alapvetően egy szubjektív döntés.
A modell alapvetően két fő osztályból áll. Egy osztály felelős a paraméterek leírásáért, míg egy másik magáért a központi preset leírásért.
A paraméterek leírására szolgáló osztály:
using System.Xml.Serialization;
namespace FFConvert.Domain;
[Serializable]
public sealed class PresetParameter
{
[XmlAttribute]
public string ParameterName { get; set; }
[XmlAttribute]
public string ParameterDescription { get; set; }
[XmlIgnore]
public string Value { get; set; }
[XmlAttribute("Validator")]
public string? ValidatorName { get; set; }
[XmlAttribute]
public string? ValidatorParameters { get; set; }
[XmlAttribute("Converter")]
public string? ConverterName { get; set; }
[XmlAttribute]
public bool IsOptional { get; set; }
[XmlAttribute]
public string? OptionalContent { get; set; }
public PresetParameter()
{
IsOptional = false;
ParameterName = string.Empty;
ParameterDescription = string.Empty;
Value = string.Empty;
}
}
A paraméterek esetén a paraméter neve és a leírása kötelezően megadandó tulajdonságok, minden más opcionális. Ezeket a paramétereket elképzelhető, hogy validálni kell, ezért a ValidatorName
a bevitt szövegen futtatandó validáló osztály nevét fogja megadni. Ezeknek a validációs osztályoknak elképzelhető, hogy paramétereket is szeretnénk átadni. Erre szolgál a ValidatorParameters
tulajdonság. A ConverterName
egy konvertáló osztályt ad meg, ami az ellenőrzés után fog majd lefutni. Ezzel olyan funkciók valósíthatóak majd meg, hogy a megadott időt mindig másodperc formátumra hozzuk. Az IsOptional
és a OptionalContent
pedig olyan paraméterek leírására szolgálnak, amelyek csak akkor adandóak majd a generált parancssorhoz, ha nem üres az értékük.
A preset leírására pedig az alábbi osztály szolgál:
using System.Xml.Serialization;
namespace FFConvert.Domain;
[Serializable]
public sealed class Preset
{
[XmlAttribute]
public string ActivatorName { get; set; }
public string Description { get; set; }
public string CommandLine { get; set; }
[XmlAttribute]
public string TargetExtension { get; set; }
public List<PresetParameter> ParametersToAsk { get; set; }
public Preset()
{
ActivatorName = string.Empty;
Description = string.Empty;
CommandLine = string.Empty;
TargetExtension = string.Empty;
ParametersToAsk = new List<PresetParameter>();
}
}
Itt opcionális tulajdonság nincs, mindegyik megadása kötelező. Az ActivatorName
a preset neve, amit a parancssorban kell majd megírni. A Description
mező a leírásra szolgál, ami majd a dinamikus súgó generáláshoz lesz használva. A CommandLine
és TargetExtension
pedig a parancssor generáláshoz lesz használva, míg a ParametersToAsk
a paramétereket írja le.
Preset validáció
Mivel a preset beállítások egy külső XML fájlból fognak jönni, ezért a programban ellenőrizni kell használat előtt, hogy valóban rendben vannak-e, ki van-e töltve az összes szükséges tulajdonság megfelelően.
Ehhez szintén a domain services rétegben készítettem egy extension metódust, ami egy preset kapcsán ellenőrzi, hogy az rendben van-e.
A logika a paraméterek esetén is végez ellenőrzéseket. Például, ha a van megadva OptionalContent
, akkor az IsOptional
tulajdonságnak igaznak kell lennie. Hasonló módon ha van validációs paraméter, akkor a validációhoz használt osztály nevét meg kell adni.
Ez a fajta ellenőrzés egy statikus ellenőrzésnek fogható fel, mivel csak a szintaxist figyeli, de azt már nem, hogy a preset esetén a megadott validációs osztály és konverziós osztály létezik-e egyáltalán.
A későbbiek folyamán majd ez is ellenőrzésre kerül.
using FFConvert.Domain;
namespace FFConvert.DomainServices;
internal static class PresetExtensions
{
public static bool IsValid(this Preset preset)
{
bool baseValid = !string.IsNullOrEmpty(preset.ActivatorName)
&& !string.IsNullOrEmpty(preset.Description)
&& !string.IsNullOrEmpty(preset.CommandLine)
&& !string.IsNullOrEmpty(preset.TargetExtension);
if (preset.ParametersToAsk.Count == 0)
return baseValid;
return baseValid
&& preset.ParametersToAsk.All(x => x.IsValid());
}
public static bool IsValid(this PresetParameter parameter)
{
bool baseValid = !string.IsNullOrEmpty(parameter.ParameterName)
&& !string.IsNullOrEmpty(parameter.ParameterDescription);
if (!string.IsNullOrEmpty(parameter.OptionalContent))
return baseValid && parameter.IsOptional == true;
if (!string.IsNullOrEmpty(parameter.ValidatorParameters))
return baseValid && !string.IsNullOrEmpty(parameter.ValidatorName);
return baseValid;
}
}
Mivel XML fájlban tároljuk a preseteket, ezért a validációt megvalósíthatnánk egy XSD fájl segítségével. Az XSD az XML Schema Document rövdítése. Ennek segítségével leírhatóak olyan szabályrendszerek, melynek meg kell feleljen egy XML dokumentum ahhoz, hogy „érvényes” legyen az adott sémában. Ezt minden XML olvasónak támogatnia kell.
C# és .NET 6.0 alatt azonban van egy pici bökkenő az eszköz támogatottságban, mégpedig az, hogy az XSD.exe eszköz még mindig nem került átírásra. Ez gyakorlatban azt jelenti számunkra, hogy a C# osztályainkból nem tudunk generálni XSD fájlt.
Éppen ezért a fejlesztésnek ezen pontján úgy voltam, hogy azt a minimális szabályrendszert inkább kódban valósítom meg. Ennek a megoldásnak a további előnye, hogy később akár JSON-re vagy YAML-re is átültethető a preset leírás anélkül, hogy a validációs logika törne.
Később persze egyszerűbb szerkeszthetőség miatt az XML dokumentum alapján generáltattam egy XSD sémát. Ehhez a https://www.liquid-technologies.com/online-xml-to-xsd-converter eszközt használtam.
Ha egy XML dokumentumhoz van egy beállított XSD fájlunk, akkor az XML szerkesztése közben a Visual Studio tud számunkra IntelliSense segítséget ajánlani, ami nagymértékben megkönnyítette a preset fájlok fejlesztését.
Preset betöltés
A preset definíciónk alapján elkészíthetjük a hozzá kapcsolódó deszerializáló kódot. Ezért a PresetManager
osztály lesz felelős, ami az Infrastructure
rétegben kapott helyet. A TryLoadPresets
metódus felel a betöltésért. Ez igaz értéket ad vissza, ha sikeres volt a betöltés, hamisat pedig akkor, ha nem. A betöltött presetek a presets
kimeneti argumentumban kerülnek visszaadásra.
using FFConvert.Domain;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace FFConvert.Infrastructure;
internal class PresetManager
{
private readonly XmlSerializer _serializer;
private readonly string _file;
public PresetManager()
{
_serializer = new XmlSerializer(typeof(Preset[]), new XmlRootAttribute("Presets"));
_file = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "presets.xml");
}
public bool TryLoadPresets([NotNullWhen(true)] out Preset[]? presets)
{
try
{
using var stream = File.OpenRead(_file);
presets = (Preset[]?)_serializer.Deserialize(stream);
return presets != null;
}
catch (Exception)
{
presets = Array.Empty<Preset>();
return false;
}
}
public bool PresetsExist
{
get
{
return File.Exists(_file);
}
}
public bool CreateSamplePreset()
{
Preset sample = new Preset
{
Description = "Preset description",
ActivatorName = "preset activator",
CommandLine = "command line string",
TargetExtension = ".mp4",
ParametersToAsk = new List<PresetParameter>
{
new PresetParameter
{
ParameterDescription = "Description",
ParameterName = "name",
}
}
};
try
{
string sampleName = Path.ChangeExtension(_file, ".sample.xml");
using XmlTextWriter writer = new(sampleName, encoding: Encoding.UTF8);
writer.Formatting = Formatting.Indented;
writer.Indentation = 4;
_serializer.Serialize(writer, new Preset[] { sample });
return true;
}
catch (Exception)
{
return false;
}
}
}
Emellett az osztály rendelkezik egy PresetsExist
tulajdonsággal, aminek a segítségével lekérdezhető, hogy egyáltalán létezik-e a presets.xml
fájl. Ezzel majd a főprogramban logikát tudunk megvalósítani, hogy ha nem létezne, akkor létrehozunk egy minta fájlt.
A minta fájl létrehozásáért a CreateSamplePreset
metódus felel. Ez egy presets.sample.xml
fájlban létrehoz egy minimális preset fájlt. Ideális esetben erre sosem lesz szükség, de előfordulhat, hogy a felhasználó majd törli a programmal szállított fájlt és sajátot akar létrehozni.
Ezen metódus által létrehozott mintafájl alapján készült el egyébként a programmal szállított presets.xml
fájl tartalma is.
Folytatása következik…