Space treatment, a bit more memory safe handling, exclude option, typo fixes
All checks were successful
MLC Release workflow / Archlinux (release) Successful in 1m35s
All checks were successful
MLC Release workflow / Archlinux (release) Successful in 1m35s
This commit is contained in:
parent
ceab08a26d
commit
bf88e05a13
@ -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)
|
||||
|
34
README.md
34
README.md
@ -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
|
||||
|
@ -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));
|
||||
|
@ -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;
|
||||
};
|
||||
|
||||
|
@ -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
|
10
src/main.cpp
10
src/main.cpp
@ -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;
|
||||
|
@ -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;
|
||||
} else if (!output.has_value()) {
|
||||
output = input;
|
||||
input = path;
|
||||
}
|
||||
if (!input.has_value()) {
|
||||
input = value;
|
||||
} else if (!output.has_value()) {
|
||||
output = input;
|
||||
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());
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user