Space treatment, a bit more memory safe handling, exclude option, typo fixes
All checks were successful
MLC Release workflow / Archlinux (release) Successful in 1m35s

This commit is contained in:
Blue 2025-03-30 22:07:20 +03:00
parent ceab08a26d
commit bf88e05a13
Signed by: blue
GPG Key ID: 9B203B252A63EE38
10 changed files with 111 additions and 73 deletions

View File

@ -1,7 +1,10 @@
# Changelog
## MLC 1.3.4 (UNRELEASED)
## MLC 1.3.4 (March 30, 2025)
- Build fixes
- Source and Destination paths now can contain spaces
- Regex to copy non music files can have spaces
- A feature to define a regex to exclude paths from rendering
## MLC 1.3.3 (October 13, 2023)
- Regex to specify non-music files to copy
@ -28,7 +31,7 @@
- New logging system
- Artist, Album artist, Album and Title are now utf16 encoded, should fix broken titles
- BPM tag is now rounded, as it supposed to be by spec
- Lyrics is not set now for USLT tag for unsychronized lyrics is not supported in LAME
- Lyrics is not set now for USLT tag for unsynchronized lyrics is not supported in LAME
- Date bug fix, it was MMDD instead of standard DDMM
## MLC 1.0.1 (July 21, 2023)

View File

@ -13,13 +13,13 @@ This is a program to compile your local lossless music library to lossy formats
### Building
```sh
$ git clone https://git.macaw.me/blue/mlc
$ cd mlc
$ mkdir build
$ cd build
$ cmake ..
$ cmake --build .
```shell
git clone https://git.macaw.me/blue/mlc
cd mlc
mkdir build
cd build
cmake ..
cmake --build .
```
### Usage
@ -27,19 +27,19 @@ $ cmake --build .
Just to compile lossless library to lossy you can use this command
assuming you are in the same directory you have just built `MLC`:
```
```shell
./mlc path/to/lossless/library path/to/store/lossy/library
```
There are more ways to use `MLC`, please refer to help for more options:
```sh
$ ./mlc help
```shell
./mlc help
# or
$ ./mlc --help
./mlc --help
#
# or
$ ./mlc -h
./mlc -h
```
`MLC` has a way to configure conversion process, it will generate global for you user config on the first launch.
@ -49,14 +49,16 @@ You can also make local configs for each directory you launch mlc from.
To output the default config run the following, assuming you are in the same directory you have just built `MLC`:
```sh
$ ./mlc config
```shell
./mlc config #to print
#or
./mlc config > config.conf #to save to config.conf in current directory
```
To use non default config run the following, assuming you are in the same directory you have just built `MLC`:
```sh
$ ./mlc path/to/lossless/library path/to/store/lossy/library -c path/to/config/file
```shell
./mlc path/to/lossless/library path/to/store/lossy/library -c path/to/config/file
```
### About

View File

@ -6,11 +6,12 @@ namespace fs = std::filesystem;
static const std::string flac(".flac");
Collection::Collection(const std::filesystem::path& path, TaskManager* tm) :
Collection::Collection(const std::filesystem::path& path, const std::shared_ptr<TaskManager>& tm, const std::shared_ptr<Settings>& st):
path(path),
countMusical(0),
counted(false),
taskManager(tm)
taskManager(tm),
settings(st)
{}
Collection::~Collection()
@ -33,13 +34,16 @@ uint32_t Collection::countMusicFiles() const {
++countMusical;
} else if (fs::is_directory(path)) {
for (const fs::directory_entry& entry : fs::directory_iterator(path)) {
if (settings->isExcluded(entry.path()))
continue;
switch (entry.status().type()) {
case fs::file_type::regular:
if (isMusic(entry.path()))
++countMusical;
break;
case fs::file_type::directory: {
Collection collection(entry.path());
Collection collection(entry.path(), taskManager, settings);
countMusical += collection.countMusicFiles();
} break;
default:
@ -53,25 +57,24 @@ uint32_t Collection::countMusicFiles() const {
}
void Collection::convert(const std::string& outPath) {
if (taskManager == nullptr)
throw 6;
fs::path out = fs::absolute(outPath);
fs::create_directories(out);
out = fs::canonical(outPath);
for (const fs::directory_entry& entry : fs::directory_iterator(path)) {
fs::path sourcePath = entry.path();
if (settings->isExcluded(sourcePath))
continue;
switch (entry.status().type()) {
case fs::file_type::regular: {
fs::path sourcePath = entry.path();
if (isMusic(sourcePath))
taskManager->queueConvert(sourcePath, out / sourcePath.stem());
else
taskManager->queueCopy(sourcePath, out / sourcePath.filename());
} break;
case fs::file_type::directory: {
fs::path sourcePath = entry.path();
Collection collection(sourcePath, taskManager);
Collection collection(sourcePath, taskManager, settings);
fs::path::iterator itr = sourcePath.end();
--itr;
collection.convert(std::string(out / *itr));

View File

@ -3,14 +3,16 @@
#include <string>
#include <iostream>
#include <filesystem>
#include <memory>
#include "settings.h"
#include "flactomp3.h"
class TaskManager;
class Collection {
public:
Collection(const std::filesystem::path& path, TaskManager* tm = nullptr);
Collection(const std::filesystem::path& path, const std::shared_ptr<TaskManager>& tm, const std::shared_ptr<Settings>& st);
~Collection();
void list() const;
@ -24,6 +26,7 @@ private:
std::filesystem::path path;
mutable uint32_t countMusical;
mutable bool counted;
TaskManager* taskManager;
std::shared_ptr<TaskManager> taskManager;
std::shared_ptr<Settings> settings;
};

View File

@ -92,7 +92,15 @@
# Variable bitrate
# Switches on or off variable bitrate
# VBR files are usually smaller, but not supped to be worse
# VBR files are usually smaller, but not supposed to be worse
# in terms of quality. VBR files might be a bit more tricky for the player
# Allowed values are: [true, false]
#vbr true
# Exclude
# MLC renders any music file it finds in source directory
# UNLESS its path matches the following regex
# Allowed value is the regex without any additional syntax,
# for example: exclude [Ss]hamefull?\s[Ss]ong[Ss]
# If you don't want to exclude anything leave this option blank
#exclude

View File

@ -57,16 +57,16 @@ int main(int argc, char **argv) {
}
logger->setSeverity(settings->getLogLevel());
TaskManager taskManager(settings, logger);
taskManager.start();
std::shared_ptr<TaskManager> taskManager = std::make_shared<TaskManager>(settings, logger);
taskManager->start();
std::chrono::time_point start = std::chrono::system_clock::now();
Collection collection(input, &taskManager);
Collection collection(input, taskManager, settings);
collection.convert(output);
taskManager.wait();
taskManager->wait();
std::cout << std::endl;
taskManager.stop();
taskManager->stop();
std::chrono::time_point end = std::chrono::system_clock::now();
std::chrono::duration<double> seconds = end - start;

View File

@ -17,6 +17,7 @@ enum class Option {
destination,
parallel,
filesToCopy,
exclude,
encodingQuality,
outputQuality,
vbr,
@ -42,6 +43,7 @@ constexpr std::array<std::string_view, static_cast<int>(Option::_optionsSize)> o
"destination",
"parallel",
"filesToCopy",
"exclude",
"encodingQuality",
"outputQuality",
"vbr"
@ -264,69 +266,74 @@ void Settings::readConfigLine(const std::string& line) {
if (option == Option::_optionsSize)
return;
std::string value;
std::getline(stream >> std::ws, value);
strip(value);
if (value.empty())
return;
switch (option) {
case Option::level: {
std::string lv;
if (!logLevel.has_value() && stream >> lv) {
if (!logLevel.has_value() && std::istringstream(value) >> lv) {
Logger::Severity level = Logger::stringToSeverity(lv);
if (level < Logger::Severity::_severitySize)
logLevel = level;
}
} break;
case Option::type: {
std::string lv;
if (!outputType.has_value() && stream >> lv) {
Type type = stringToType(lv);
std::string tp;
if (!outputType.has_value() && std::istringstream(value) >> tp) {
Type type = stringToType(tp);
if (type < _typesSize)
outputType = type;
}
} break;
case Option::source: {
std::string path;
if (stream >> path) {
if (!input.has_value()) {
input = path;
input = value;
} else if (!output.has_value()) {
output = input;
input = path;
}
input = value;
}
} break;
case Option::destination: {
std::string path;
if (!output.has_value() && stream >> path)
output = path;
if (!output.has_value())
output = value;
} break;
case Option::parallel: {
unsigned int count;
if (!threads.has_value() && stream >> count)
if (!threads.has_value() && std::istringstream(value) >> 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^";
if (!nonMusic.has_value()) {
if (value == "all")
value = "";
else if (value == "none")
value = "a^";
nonMusic = regex;
nonMusic = value;
}
} break;
case Option::exclude: {
if (!excluded.has_value())
excluded = value;
} break;
case Option::outputQuality: {
unsigned int value;
if (!outputQuality.has_value() && stream >> value)
outputQuality = std::clamp(value, minQuality, maxQuality);
unsigned int quality;
if (!outputQuality.has_value() && std::istringstream(value) >> quality)
outputQuality = std::clamp(quality, minQuality, maxQuality);
} break;
case Option::encodingQuality: {
unsigned int value;
if (!encodingQuality.has_value() && stream >> value)
encodingQuality = std::clamp(value, minQuality, maxQuality);
unsigned int quality;
if (!encodingQuality.has_value() && std::istringstream(value) >> quality)
encodingQuality = std::clamp(quality, minQuality, maxQuality);
} break;
case Option::vbr: {
bool value;
if (!vbr.has_value() && stream >> std::boolalpha >> value)
vbr = value;
bool vb;
if (!vbr.has_value() && std::istringstream(value) >> std::boolalpha >> vb)
vbr = vb;
} break;
default:
break;
@ -365,9 +372,16 @@ std::string Settings::resolvePath(const std::string& line) {
}
bool Settings::matchNonMusic(const std::string& fileName) const {
if (nonMusic.has_value())
return std::regex_search(fileName, nonMusic.value());
else
if (!nonMusic.has_value())
return true;
return std::regex_search(fileName, nonMusic.value());
}
bool Settings::isExcluded(const std::string& path) const {
if (!excluded.has_value())
return false;
return std::regex_search(path, excluded.value());
}

View File

@ -40,6 +40,7 @@ public:
Action getAction() const;
unsigned int getThreads() const;
bool matchNonMusic(const std::string& fileName) const;
bool isExcluded(const std::string& path) const;
unsigned char getEncodingQuality() const;
unsigned char getOutputQuality() const;
bool getVBR() const;
@ -69,6 +70,7 @@ private:
std::optional<std::string> configPath;
std::optional<unsigned int> threads;
std::optional<std::regex> nonMusic;
std::optional<std::regex> excluded;
std::optional<unsigned char> encodingQuality;
std::optional<unsigned char> outputQuality;
std::optional<bool> vbr;

View File

@ -22,6 +22,9 @@ TaskManager::~TaskManager() {
}
void TaskManager::queueConvert(const std::filesystem::path& source, const std::filesystem::path& destination) {
if (settings->isExcluded(source))
return;
std::unique_lock<std::mutex> lock(queueMutex);
jobs.emplace(Job::convert, source, destination);
@ -83,7 +86,7 @@ void TaskManager::loop() {
lock.lock();
++completeTasks;
printResilt(job, result);
printResult(job, result);
--busyThreads;
lock.unlock();
waitConditional.notify_all();
@ -138,7 +141,7 @@ TaskManager::JobResult TaskManager::execute(Job& job) {
}};
}
void TaskManager::printResilt(const TaskManager::Job& job, const TaskManager::JobResult& result) {
void TaskManager::printResult(const TaskManager::Job& job, const TaskManager::JobResult& result) {
std::string msg;
switch (job.type) {
case Job::copy:

View File

@ -36,7 +36,7 @@ public:
private:
void loop();
JobResult execute(Job& job);
void printResilt(const Job& job, const JobResult& result);
void printResult(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);