Compare commits

...

9 Commits

Author SHA1 Message Date
Blue 3971a5b662
pthreads for compatibility 2023-10-20 18:12:26 -03:00
Blue 2cce5f52f0
build scenario changes 2023-10-20 17:39:27 -03:00
Blue 5c3a4a592e
encoding settings 2023-10-13 16:10:08 -03:00
Blue 03e7f29d84
regex for additional files copying 2023-10-12 22:00:16 -03:00
Blue eb85b71651
CI, try 12 2023-10-11 17:11:23 -03:00
Blue a337cc7ec3
CI, try 11 2023-10-11 17:09:54 -03:00
Blue 97ffe45b24
CI, try 9 2023-10-11 17:07:18 -03:00
Blue ca9f67c223
CI, try 9 2023-10-11 17:00:31 -03:00
Blue 45f3e1d6dd
CI, try 8 2023-10-11 16:57:54 -03:00
16 changed files with 364 additions and 82 deletions

View File

@ -20,8 +20,9 @@ jobs:
- name: Clone the AUR repository
run: |
echo "${{ secrets.DEPLOY_TO_AUR_PRIVATE_KEY }}" > key
GIT_SSH_COMMAND="ssh -i key -o 'IdentitiesOnly yes'" git clone ssh://aur@aur.archlinux.org/mlc.git aur
chmod 777 -R aur/.SRCINFO
chmod 600 key
GIT_SSH_COMMAND="ssh -i key -o 'IdentitiesOnly yes' -o 'StrictHostKeyChecking no'" git clone ssh://aur@aur.archlinux.org/mlc.git aur
chmod 777 -R aur
cd aur
git config user.name ${{ secrets.DEPLOY_TO_AUR_USER_NAME }}
git config user.email ${{ secrets.DEPLOY_TO_AUR_EMAIL }}
@ -41,4 +42,4 @@ jobs:
run: |
git add PKGBUILD .SRCINFO
git commit -m "${{ gitea.event.release.body }}"
GIT_SSH_COMMAND="ssh -i ../key -o 'IdentitiesOnly yes'" git push
GIT_SSH_COMMAND="ssh -i ../key -o 'IdentitiesOnly yes' -o 'StrictHostKeyChecking no'" git push

View File

@ -1,5 +1,9 @@
# Changelog
## MLC 1.3.3 (October 13, 2023)
- Regex to specify non-music files to copy
- Encoding settings (VBR/CBR, encoding quality, output quality)
## MLC 1.3.2 (October 10, 2023)
- A release purely for CI

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.5)
project(
mlc
VERSION 1.3.25
VERSION 1.3.3
DESCRIPTION "Media Library Compiler: rips your media library to a lossy compilation"
LANGUAGES CXX
)
@ -25,24 +25,25 @@ message("Compilation options: " ${COMPILE_OPTIONS})
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(PkgConfig REQUIRED)
find_package(FLAC REQUIRED)
find_package(JPEG REQUIRED)
find_package(LAME REQUIRED)
find_package(TAGLIB REQUIRED)
find_package(Threads REQUIRED)
pkg_check_modules(LAME REQUIRED IMPORTED_TARGET lame)
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib)
add_executable(mlc)
add_executable(${PROJECT_NAME})
target_compile_options(${PROJECT_NAME} PRIVATE ${COMPILE_OPTIONS})
add_subdirectory(src)
target_link_libraries(mlc
target_link_libraries(${PROJECT_NAME}
FLAC::FLAC
PkgConfig::LAME
LAME::LAME
JPEG::JPEG
PkgConfig::TAGLIB
TAGLIB::TAGLIB
Threads::Threads
)
install(TARGETS mlc RUNTIME DESTINATION bin)
install(TARGETS ${PROJECT_NAME} RUNTIME DESTINATION bin)

23
cmake/FindFLAC.cmake Normal file
View File

@ -0,0 +1,23 @@
find_path(FLAC_INCLUDE_DIR FLAC/stream_decoder.h)
find_library(FLAC_LIBRARIES FLAC NAMES flac)
if(FLAC_INCLUDE_DIR AND FLAC_LIBRARIES)
set(FLAC_FOUND TRUE)
endif()
if(FLAC_FOUND)
add_library(FLAC::FLAC SHARED IMPORTED)
set_target_properties(FLAC::FLAC PROPERTIES
IMPORTED_LOCATION "${FLAC_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${FLAC_INCLUDE_DIR}/FLAC"
INTERFACE_LINK_LIBRARIES "${FLAC_LIBRARIES}"
)
if (NOT FLAC_FIND_QUIETLY)
message(STATUS "Found FLAC includes: ${FLAC_INCLUDE_DIR}/FLAC")
message(STATUS "Found FLAC library: ${FLAC_LIBRARIES}")
endif ()
else()
if (FLAC_FIND_REQUIRED)
message(FATAL_ERROR "Could NOT find FLAC development files")
endif ()
endif()

View File

@ -1,20 +1,26 @@
#copied from here, thank you
#https://github.com/sipwise/sems/blob/master/cmake/FindLame.cmake
FIND_PATH(LAME_INCLUDE_DIR lame/lame.h)
FIND_LIBRARY(LAME_LIBRARIES NAMES mp3lame)
find_path(LAME_INCLUDE_DIR lame/lame.h)
find_library(LAME_LIBRARIES lame NAMES mp3lame)
IF(LAME_INCLUDE_DIR AND LAME_LIBRARIES)
SET(LAME_FOUND TRUE)
ENDIF(LAME_INCLUDE_DIR AND LAME_LIBRARIES)
if(LAME_INCLUDE_DIR AND LAME_LIBRARIES)
set(LAME_FOUND TRUE)
endif(LAME_INCLUDE_DIR AND LAME_LIBRARIES)
IF(LAME_FOUND)
IF (NOT Lame_FIND_QUIETLY)
MESSAGE(STATUS "Found lame includes: ${LAME_INCLUDE_DIR}/lame/lame.h")
MESSAGE(STATUS "Found lame library: ${LAME_LIBRARIES}")
ENDIF (NOT Lame_FIND_QUIETLY)
ELSE(LAME_FOUND)
IF (Lame_FIND_REQUIRED)
MESSAGE(FATAL_ERROR "Could NOT find lame development files")
ENDIF (Lame_FIND_REQUIRED)
ENDIF(LAME_FOUND)
if(LAME_FOUND)
add_library(LAME::LAME SHARED IMPORTED)
set_target_properties(LAME::LAME PROPERTIES
IMPORTED_LOCATION "${LAME_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${LAME_INCLUDE_DIR}/lame"
INTERFACE_LINK_LIBRARIES "${LAME_LIBRARIES}"
)
if (NOT Lame_FIND_QUIETLY)
message(STATUS "Found lame includes: ${LAME_INCLUDE_DIR}/lame")
message(STATUS "Found lame library: ${LAME_LIBRARIES}")
endif (NOT Lame_FIND_QUIETLY)
else(LAME_FOUND)
if (Lame_FIND_REQUIRED)
message(FATAL_ERROR "Could NOT find lame development files")
endif (Lame_FIND_REQUIRED)
endif(LAME_FOUND)

24
cmake/FindTAGLIB.cmake Normal file
View File

@ -0,0 +1,24 @@
find_path(TAGLIB_INCLUDE_DIR taglib/id3v2tag.h)
find_library(TAGLIB_LIBRARIES taglib NAMES TAGLIB tag)
if(TAGLIB_INCLUDE_DIR AND TAGLIB_LIBRARIES)
set(TAGLIB_FOUND TRUE)
endif()
if(TAGLIB_FOUND)
add_library(TAGLIB::TAGLIB SHARED IMPORTED)
set_target_properties(TAGLIB::TAGLIB PROPERTIES
IMPORTED_LOCATION "${TAGLIB_LIBRARIES}"
INTERFACE_INCLUDE_DIRECTORIES "${FLAC_INCLUDE_DIR}/taglib"
INTERFACE_LINK_LIBRARIES "${TAGLIB_LIBRARIES}"
)
if (NOT TAGLIB_FIND_QUIETLY)
message(STATUS "Found TAGLIB includes: ${FLAC_INCLUDE_DIR}/taglib")
message(STATUS "Found TAGLIB library: ${TAGLIB_LIBRARIES}")
endif ()
else()
if (TAGLIB_FIND_REQUIRED)
message(FATAL_ERROR "Could NOT find TAGLIB development files")
endif ()
endif()

View File

@ -1,6 +1,6 @@
# Maintainer: Yury Gubich <blue@macaw.me>
pkgname=mlc
pkgver=1.3.25
pkgver=1.3.3
pkgrel=1
pkgdesc="Media Library Compiler: rips your media library to a lossy compilation"
arch=('i686' 'x86_64')

View File

@ -6,23 +6,16 @@ namespace fs = std::filesystem;
static const std::string flac(".flac");
Collection::Collection(const std::string& path, TaskManager* tm) :
path(fs::canonical(path)),
Collection::Collection(const std::filesystem::path& path, TaskManager* tm) :
path(path),
countMusical(0),
counted(false),
taskManager(tm)
{}
Collection::Collection(const std::filesystem::path& path, TaskManager* tm):
path(fs::canonical(path)),
countMusical(0),
counted(false),
taskManager(tm)
Collection::~Collection()
{}
Collection::~Collection() {
}
void Collection::list() const {
if (fs::is_regular_file(path))
return;
@ -71,13 +64,10 @@ void Collection::convert(const std::string& outPath) {
switch (entry.status().type()) {
case fs::file_type::regular: {
fs::path sourcePath = entry.path();
fs::path dstPath = out / sourcePath.stem();
if (isMusic(sourcePath))
taskManager->queueJob(sourcePath, dstPath);
taskManager->queueConvert(sourcePath, out / sourcePath.stem());
else
fs::copy_file(sourcePath, dstPath, fs::copy_options::overwrite_existing);
//std::cout << sourcePath << " => " << dstPath << std::endl;
taskManager->queueCopy(sourcePath, out / sourcePath.filename());
} break;
case fs::file_type::directory: {
fs::path sourcePath = entry.path();

View File

@ -10,7 +10,6 @@ class TaskManager;
class Collection {
public:
Collection(const std::string& path, TaskManager* tm = nullptr);
Collection(const std::filesystem::path& path, TaskManager* tm = nullptr);
~Collection();

View File

@ -44,3 +44,55 @@
# If it's set to 0 - amount of threads is going to be
# as high as your processor can effectively handle
#parallel 0
# Non music files
# MLC copies any non-music file it finds in source directory
# if it matches the following regex
# Allowed value are: [all, none] or regex without any additional syntax,
# for example: filesToCopy cover\.jpe?g
#filesToCopy all
# Encoding quality
# Sets up encoding quality (NOT OUTPUT QUALITY)
# The higher quality the slower the encoding process
# 0 is the highest quality and slowest process
# 9 is the lowest quality and the fastest process fastest
# Allowed values are: [0, 1, 2, ... 9]
#encodingQuality 0
# Output quality
# Sets up output quality
# The higher quality the less information is lonst in compression
# 0 is the highest possible quality for selected mode and type and results in the biggest file
# 9 is the lowest quality but results in the smalest file
# Allowed values are [0, 1, 2, ... 9]
# For the constant bitrate modes (CBR) the following table is valid
# Quality | MP3 |
# --------+-----+--
# 0 | 320 |
# --------+-----+--
# 1 | 288 |
# --------+-----+--
# 2 | 256 |
# --------+-----+--
# 3 | 224 |
# --------+-----+--
# 4 | 192 |
# --------+-----+--
# 5 | 160 |
# --------+-----+--
# 6 | 128 |
# --------+-----+--
# 7 | 96 |
# --------+-----+--
# 8 | 64 |
# --------+-----+--
# 9 | 32 |
#outputQuality 0
# Variable bitrate
# Switches on or off variable bitrate
# VBR files are usually smaller, but not supped to be worse
# in terms of quality. VBR files might be a bit more tricky for the player
# Allowedvalues are: [true, false]
#vbr true

View File

@ -12,6 +12,18 @@ constexpr std::string_view jpeg ("image/jpeg");
const std::map<std::string, std::string> textIdentificationReplacements({
{"PUBLISHER", "TPUB"}
});
constexpr std::array<int, 10> bitrates({
320,
288,
256,
224,
192,
160,
128,
96,
64,
32
});
FLACtoMP3::FLACtoMP3(Logger::Severity severity, uint8_t size) :
logger(severity),
@ -46,7 +58,7 @@ bool FLACtoMP3::run() {
if (pcmCounter > 0)
flush();
int nwrite = lame_encode_flush(encoder, outputBuffer, pcmSize * 2);
int nwrite = lame_encode_flush(encoder, outputBuffer, outputBufferSize);
fwrite((char*)outputBuffer, nwrite, 1, output);
@ -98,9 +110,21 @@ void FLACtoMP3::setOutputFile(const std::string& path) {
outPath = path;
lame_set_VBR(encoder, vbr_default);
lame_set_VBR_quality(encoder, 0);
lame_set_quality(encoder, 0);
}
void FLACtoMP3::setParameters(unsigned char encodingQuality, unsigned char outputQuality, bool vbr) {
if (vbr) {
logger.info("Encoding to VBR with quality " + std::to_string(outputQuality));
lame_set_VBR(encoder, vbr_default);
lame_set_VBR_quality(encoder, outputQuality);
} else {
int bitrate = bitrates[outputQuality];
logger.info("Encoding to CBR " + std::to_string(bitrate));
lame_set_VBR(encoder, vbr_off);
lame_set_brate(encoder, bitrate);
}
lame_set_quality(encoder, encodingQuality);
}
bool FLACtoMP3::initializeOutput() {
@ -306,7 +330,7 @@ bool FLACtoMP3::flush() {
nwrite = lame_encode_buffer_interleaved(
encoder,
pcm,
pcmCounter,
pcmCounter / 2,
outputBuffer,
outputBufferSize
);

View File

@ -1,6 +1,6 @@
#pragma once
#include <FLAC/stream_decoder.h>
#include <stream_decoder.h>
#include <lame.h>
#include <jpeglib.h>
#include <id3v2tag.h>
@ -8,6 +8,7 @@
#include <string>
#include <string_view>
#include <map>
#include <array>
#include <stdio.h>
#include "logger/accumulator.h"
@ -19,6 +20,7 @@ public:
void setInputFile(const std::string& path);
void setOutputFile(const std::string& path);
void setParameters(unsigned char encodingQuality, unsigned char outputQuality, bool vbr);
bool run();
std::list<Logger::Message> getHistory() const;

View File

@ -16,6 +16,10 @@ enum class Option {
source,
destination,
parallel,
filesToCopy,
encodingQuality,
outputQuality,
vbr,
_optionsSize
};
@ -36,13 +40,20 @@ constexpr std::array<std::string_view, static_cast<int>(Option::_optionsSize)> o
"type",
"source",
"destination",
"parallel"
"parallel",
"filesToCopy",
"encodingQuality",
"outputQuality",
"vbr"
});
constexpr std::array<std::string_view, Settings::_typesSize> types({
"mp3"
});
constexpr unsigned int maxQuality = 9;
constexpr unsigned int minQuality = 0;
bool is_space(char ch){
return std::isspace(static_cast<unsigned char>(ch));
}
@ -72,7 +83,13 @@ Settings::Settings(int argc, char ** argv):
outputType(std::nullopt),
input(std::nullopt),
output(std::nullopt),
logLevel(std::nullopt)
logLevel(std::nullopt),
configPath(std::nullopt),
threads(std::nullopt),
nonMusic(std::nullopt),
encodingQuality(std::nullopt),
outputQuality(std::nullopt),
vbr(std::nullopt)
{
for (int i = 1; i < argc; ++i)
arguments.push_back(argv[i]);
@ -182,6 +199,27 @@ unsigned int Settings::getThreads() const {
return 0;
}
unsigned char Settings::getOutputQuality() const {
if (outputQuality.has_value())
return outputQuality.value();
else
return minQuality; //actually, it means max possible quality
}
unsigned char Settings::getEncodingQuality() const {
if (encodingQuality.has_value())
return encodingQuality.value();
else
return minQuality; //actually, it means max possible quality
}
bool Settings::getVBR() const {
if (vbr.has_value())
return vbr.value();
else
return true;
}
void Settings::strip(std::string& line) {
line.erase(line.begin(), std::find_if(line.begin(), line.end(), std::not_fn(is_space)));
line.erase(std::find_if(line.rbegin(), line.rend(), std::not_fn(is_space)).base(), line.end());
@ -264,6 +302,32 @@ void Settings::readConfigLine(const std::string& line) {
if (!threads.has_value() && stream >> count)
threads = count;
} break;
case Option::filesToCopy: {
std::string regex;
if (!nonMusic.has_value() && stream >> regex) {
if (regex == "all")
regex = "";
else if (regex == "none")
regex = "a^";
nonMusic = regex;
}
} break;
case Option::outputQuality: {
unsigned int value;
if (!outputQuality.has_value() && stream >> value)
outputQuality = std::clamp(value, minQuality, maxQuality);
} break;
case Option::encodingQuality: {
unsigned int value;
if (!encodingQuality.has_value() && stream >> value)
encodingQuality = std::clamp(value, minQuality, maxQuality);
} break;
case Option::vbr: {
bool value;
if (!vbr.has_value() && stream >> std::boolalpha >> value)
vbr = value;
} break;
default:
break;
}
@ -299,3 +363,11 @@ std::string Settings::resolvePath(const std::string& line) {
else
return line;
}
bool Settings::matchNonMusic(const std::string& fileName) const {
if (nonMusic.has_value())
return std::regex_search(fileName, nonMusic.value());
else
return true;
}

View File

@ -11,6 +11,7 @@
#include <functional>
#include <cctype>
#include <sstream>
#include <regex>
#include "logger/logger.h"
@ -38,6 +39,10 @@ public:
Type getType() const;
Action getAction() const;
unsigned int getThreads() const;
bool matchNonMusic(const std::string& fileName) const;
unsigned char getEncodingQuality() const;
unsigned char getOutputQuality() const;
bool getVBR() const;
bool readConfigFile();
void readConfigLine(const std::string& line);
@ -63,4 +68,8 @@ private:
std::optional<Logger::Severity> logLevel;
std::optional<std::string> configPath;
std::optional<unsigned int> threads;
std::optional<std::regex> nonMusic;
std::optional<unsigned char> encodingQuality;
std::optional<unsigned char> outputQuality;
std::optional<bool> vbr;
};

View File

@ -21,9 +21,9 @@ TaskManager::TaskManager(const std::shared_ptr<Settings>& settings, const std::s
TaskManager::~TaskManager() {
}
void TaskManager::queueJob(const std::filesystem::path& source, const std::filesystem::path& destination) {
void TaskManager::queueConvert(const std::filesystem::path& source, const std::filesystem::path& destination) {
std::unique_lock<std::mutex> lock(queueMutex);
jobs.emplace(source, destination);
jobs.emplace(Job::convert, source, destination);
++maxTasks;
logger->setStatusMessage(std::to_string(completeTasks) + "/" + std::to_string(maxTasks));
@ -32,6 +32,19 @@ void TaskManager::queueJob(const std::filesystem::path& source, const std::files
loopConditional.notify_one();
}
void TaskManager::queueCopy(const std::filesystem::path& source, const std::filesystem::path& destination) {
if (!settings->matchNonMusic(source.filename()))
return;
std::unique_lock<std::mutex> lock(queueMutex);
jobs.emplace(Job::copy, source, destination);
++maxTasks;
logger->setStatusMessage(std::to_string(completeTasks) + "/" + std::to_string(maxTasks));
lock.unlock();
loopConditional.notify_one();
}
bool TaskManager::busy() const {
std::lock_guard lock(queueMutex);
return !jobs.empty();
@ -61,28 +74,16 @@ void TaskManager::loop() {
if (terminate)
return;
std::pair<std::string, std::string> pair = jobs.front();
Job job = jobs.front();
++busyThreads;
jobs.pop();
lock.unlock();
JobResult result;
switch (settings->getType()) {
case Settings::mp3:
result = mp3Job(pair.first, pair.second + ".mp3", settings->getLogLevel());
break;
default:
break;
}
JobResult result = execute(job);
lock.lock();
++completeTasks;
logger->printNested(
result.first ? "Encoding complete but there are messages about it" : "Encoding failed!",
{"Source: \t" + pair.first, "Destination: \t" + pair.second},
result.second,
std::to_string(completeTasks) + "/" + std::to_string(maxTasks)
);
printResilt(job, result);
--busyThreads;
lock.unlock();
waitConditional.notify_all();
@ -118,14 +119,72 @@ unsigned int TaskManager::getCompleteTasks() const {
return completeTasks;
}
TaskManager::JobResult TaskManager::mp3Job(
const std::filesystem::path& source,
const std::filesystem::path& destination,
Logger::Severity logLevel)
{
FLACtoMP3 convertor(logLevel);
convertor.setInputFile(source);
convertor.setOutputFile(destination);
TaskManager::JobResult TaskManager::execute(Job& job) {
switch (job.type) {
case Job::copy:
return copyJob(job, settings);
case Job::convert:
switch (settings->getType()) {
case Settings::mp3:
job.destination.replace_extension("mp3");
return mp3Job(job, settings);
default:
break;
}
}
return {false, {
{Logger::Severity::error, "Unknown job type: " + std::to_string(job.type)}
}};
}
void TaskManager::printResilt(const TaskManager::Job& job, const TaskManager::JobResult& result) {
std::string msg;
switch (job.type) {
case Job::copy:
if (result.first)
msg = "File copy complete, but there are messages about it:";
else
msg = "File copy failed!";
break;
case Job::convert:
if (result.first)
msg = "Encoding complete but there are messages about it:";
else
msg = "Encoding failed!";
break;
}
logger->printNested(
msg,
{"Source: \t" + job.source.string(), "Destination: \t" + job.destination.string()},
result.second,
std::to_string(completeTasks) + "/" + std::to_string(maxTasks)
);
}
TaskManager::JobResult TaskManager::mp3Job(const TaskManager::Job& job, const std::shared_ptr<Settings>& settings) {
FLACtoMP3 convertor(settings->getLogLevel());
convertor.setInputFile(job.source);
convertor.setOutputFile(job.destination);
convertor.setParameters(settings->getEncodingQuality(), settings->getOutputQuality(), settings->getVBR());
bool result = convertor.run();
return {result, convertor.getHistory()};
}
TaskManager::JobResult TaskManager::copyJob(const TaskManager::Job& job, const std::shared_ptr<Settings>& settings) {
(void)(settings);
bool success = std::filesystem::copy_file(
job.source,
job.destination,
std::filesystem::copy_options::overwrite_existing
);
return {success, {}};
}
TaskManager::Job::Job(Type type, const std::filesystem::path& source, std::filesystem::path destination):
type(type),
source(source),
destination(destination) {}

View File

@ -18,13 +18,15 @@
#include "logger/printer.h"
class TaskManager {
typedef std::pair<bool, std::list<Logger::Message>> JobResult;
using JobResult = std::pair<bool, std::list<Logger::Message>>;
struct Job;
public:
TaskManager(const std::shared_ptr<Settings>& settings, const std::shared_ptr<Printer>& logger);
~TaskManager();
void start();
void queueJob(const std::filesystem::path& source, const std::filesystem::path& destination);
void queueConvert(const std::filesystem::path& source, const std::filesystem::path& destination);
void queueCopy(const std::filesystem::path& source, const std::filesystem::path& destination);
void stop();
bool busy() const;
void wait();
@ -33,7 +35,11 @@ public:
private:
void loop();
static JobResult mp3Job(const std::filesystem::path& source, const std::filesystem::path& destination, Logger::Severity logLevel);
JobResult execute(Job& job);
void printResilt(const Job& job, const JobResult& result);
static JobResult mp3Job(const Job& job, const std::shared_ptr<Settings>& settings);
static JobResult copyJob(const Job& job, const std::shared_ptr<Settings>& settings);
private:
std::shared_ptr<Settings> settings;
std::shared_ptr<Printer> logger;
@ -46,7 +52,17 @@ private:
std::condition_variable loopConditional;
std::condition_variable waitConditional;
std::vector<std::thread> threads;
std::queue<std::pair<std::filesystem::path, std::filesystem::path>> jobs;
std::queue<Job> jobs;
};
struct TaskManager::Job {
enum Type {
copy,
convert
};
Job(Type type, const std::filesystem::path& source, std::filesystem::path destination);
Type type;
std::filesystem::path source;
std::filesystem::path destination;
};