Add GUI variant of mcopy.
This commit is contained in:
parent
5a121fc062
commit
720d93f43d
|
@ -1,3 +1,3 @@
|
||||||
# mcopy
|
# mcopy
|
||||||
|
|
||||||
Rename music by specifying a format
|
Rename/copy audio files based on their metadata.
|
||||||
|
|
38
include/core.hpp
Normal file
38
include/core.hpp
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/* mcopy
|
||||||
|
*
|
||||||
|
* Rename music by specifying a format
|
||||||
|
* Licensed under the GNU General Public License version 3.
|
||||||
|
* See included LICENSE file for copyright and license details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace mcopy {
|
||||||
|
struct Settings {
|
||||||
|
bool ask{true};
|
||||||
|
bool quiet{false};
|
||||||
|
bool strict{false};
|
||||||
|
bool skip_existing{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct FileData {
|
||||||
|
std::string title{};
|
||||||
|
std::string album{};
|
||||||
|
std::string artist{};
|
||||||
|
std::string track{};
|
||||||
|
std::string year{};
|
||||||
|
std::string genre{};
|
||||||
|
std::string comment{};
|
||||||
|
std::string extension{};
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FileStatus {
|
||||||
|
Success,
|
||||||
|
Failure,
|
||||||
|
};
|
||||||
|
|
||||||
|
std::pair<FileStatus, FileData> get_metadata_from_file(const std::string& file);
|
||||||
|
void write_to_file(const Settings& settings, const std::string& format, const std::string& file);
|
||||||
|
};
|
28
meson.build
28
meson.build
|
@ -4,14 +4,24 @@ project(
|
||||||
version : '"0.0.1"',
|
version : '"0.0.1"',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
include_directories = [
|
||||||
|
include_directories('include'),
|
||||||
|
]
|
||||||
|
|
||||||
project_source_files = [
|
project_source_files = [
|
||||||
'src/mcopy.cpp',
|
'src/mcopy.cpp',
|
||||||
|
'src/core.cpp',
|
||||||
]
|
]
|
||||||
|
|
||||||
project_dependencies = [
|
project_dependencies = [
|
||||||
dependency('taglib'),
|
dependency('taglib'),
|
||||||
|
dependency('wxwidgets'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if get_option('gui')
|
||||||
|
project_dependencies += [ dependency('wxwidgets') ]
|
||||||
|
endif
|
||||||
|
|
||||||
build_args = [
|
build_args = [
|
||||||
'-DMCOPY_VERSION=' + meson.project_version(),
|
'-DMCOPY_VERSION=' + meson.project_version(),
|
||||||
]
|
]
|
||||||
|
@ -21,6 +31,22 @@ project_target = executable(
|
||||||
project_source_files, install : true,
|
project_source_files, install : true,
|
||||||
dependencies: project_dependencies,
|
dependencies: project_dependencies,
|
||||||
cpp_args : build_args,
|
cpp_args : build_args,
|
||||||
|
include_directories : include_directories,
|
||||||
)
|
)
|
||||||
|
|
||||||
test('mcopy', project_target)
|
test(meson.project_name(), project_target)
|
||||||
|
|
||||||
|
if get_option('gui')
|
||||||
|
gui_project_source_files = [
|
||||||
|
'src/core.cpp',
|
||||||
|
'src/mcopy_gui.cpp',
|
||||||
|
]
|
||||||
|
|
||||||
|
project_gui_target = executable(
|
||||||
|
'mcopy_gui',
|
||||||
|
gui_project_source_files, install : true,
|
||||||
|
dependencies : project_dependencies,
|
||||||
|
cpp_args : build_args,
|
||||||
|
include_directories : include_directories,
|
||||||
|
)
|
||||||
|
endif
|
||||||
|
|
1
meson_options.txt
Normal file
1
meson_options.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
option('gui', type : 'boolean', value : true, description : 'Enable GUI')
|
139
src/core.cpp
Normal file
139
src/core.cpp
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
/* mcopy
|
||||||
|
*
|
||||||
|
* Rename music by specifying a format
|
||||||
|
* Licensed under the GNU General Public License version 3.
|
||||||
|
* See included LICENSE file for copyright and license details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <tag.h>
|
||||||
|
#include <fileref.h>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <fstream>
|
||||||
|
#include <core.hpp>
|
||||||
|
|
||||||
|
std::pair<mcopy::FileStatus, mcopy::FileData> mcopy::get_metadata_from_file(const std::string& file) {
|
||||||
|
TagLib::FileRef _file = TagLib::FileRef(TagLib::FileName(file.c_str()));
|
||||||
|
|
||||||
|
if (_file.isNull() || !_file.audioProperties()) {
|
||||||
|
return {mcopy::FileStatus::Failure, {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
TagLib::Tag* tag = _file.tag();
|
||||||
|
|
||||||
|
const auto get_title = [&tag]() -> std::string { return tag->title().to8Bit(true); };
|
||||||
|
const auto get_album = [&tag]() -> std::string { return tag->album().to8Bit(true); };
|
||||||
|
const auto get_artist = [&tag]() -> std::string { return tag->artist().to8Bit(true); };
|
||||||
|
const auto get_track = [&tag]() -> std::string { return std::to_string(tag->track()); };
|
||||||
|
const auto get_year = [&tag]() -> std::string { return std::to_string(tag->year()); };
|
||||||
|
const auto get_genre = [&tag]() -> std::string { return tag->genre().to8Bit(true); };
|
||||||
|
const auto get_comment = [&tag]() -> std::string { return tag->comment().to8Bit(true); };
|
||||||
|
const auto get_extension = [&file]() -> std::string { return file.find_last_of(".") + 1 != file.npos ? file.substr(file.find_last_of(".") + 1) : ""; };
|
||||||
|
|
||||||
|
mcopy::FileData data{
|
||||||
|
get_title(),
|
||||||
|
get_album(),
|
||||||
|
get_artist(),
|
||||||
|
get_track(),
|
||||||
|
get_year(),
|
||||||
|
get_genre(),
|
||||||
|
get_comment(),
|
||||||
|
get_extension(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {mcopy::FileStatus::Success, data};
|
||||||
|
}
|
||||||
|
|
||||||
|
void mcopy::write_to_file(const mcopy::Settings& settings, const std::string& format, const std::string& file) {
|
||||||
|
if (!std::filesystem::is_regular_file(file)) {
|
||||||
|
std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n";
|
||||||
|
if (settings.strict) {
|
||||||
|
std::exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<mcopy::FileStatus, mcopy::FileData> pair = mcopy::get_metadata_from_file(file);
|
||||||
|
if (pair.first == mcopy::FileStatus::Failure) {
|
||||||
|
std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n";
|
||||||
|
if (settings.strict) {
|
||||||
|
std::exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mcopy::FileData& data = pair.second;
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::vector<char> the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'};
|
||||||
|
#else
|
||||||
|
std::vector<char> the_forbidden_characters{'/'};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const std::vector<std::string*> data_members = {&data.title, &data.album, &data.artist, &data.track, &data.year, &data.genre, &data.comment, &data.extension};
|
||||||
|
for (const auto& it : data_members) {
|
||||||
|
for (const auto& c : the_forbidden_characters) {
|
||||||
|
it->erase(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; }), it->end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> delim_map{
|
||||||
|
{"@t", data.title},
|
||||||
|
{"@a", data.album},
|
||||||
|
{"@A", data.artist},
|
||||||
|
{"@n", data.track},
|
||||||
|
{"@y", data.year},
|
||||||
|
{"@g", data.genre},
|
||||||
|
{"@c", data.comment},
|
||||||
|
{"@e", data.extension},
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string output_filename = format;
|
||||||
|
for (auto& it : delim_map) {
|
||||||
|
while (output_filename.find(it.first) != std::string::npos) {
|
||||||
|
if (it.second.empty()) {
|
||||||
|
it.second = "Unknown";
|
||||||
|
|
||||||
|
if (settings.ask) {
|
||||||
|
std::cerr << "mcopy: failed to retrieve data to represent " << it.first << ", enter an appropriate replacement: ";
|
||||||
|
std::getline(std::cin, it.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output_filename.replace(output_filename.find(it.first), it.first.length(), it.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ifstream ef(output_filename);
|
||||||
|
if (ef.good() && settings.skip_existing) {
|
||||||
|
std::cerr << "mcopy: file << '" << file << "' already exists, skipping\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string dir{std::filesystem::path{output_filename}.remove_filename()};
|
||||||
|
if (dir.empty()) {
|
||||||
|
std::cerr << "mcopy: failed to retrieve directory: " << output_filename << "\n";
|
||||||
|
if (settings.strict) {
|
||||||
|
std::exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(dir) && !std::filesystem::create_directories(dir)) {
|
||||||
|
std::cerr << "mcopy: failed to create directory '" << dir << "'\n";
|
||||||
|
if (settings.strict) {
|
||||||
|
std::exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::copy_file(file, output_filename, std::filesystem::copy_options::overwrite_existing)) {
|
||||||
|
std::cerr << "mcopy: failed to copy input file '" << file << "' to '" << output_filename << "'\n";
|
||||||
|
if (settings.strict) std::exit(EXIT_FAILURE);
|
||||||
|
} else if (!settings.quiet) {
|
||||||
|
std::cout << "mcopy: copied input file '" << file << "' to '" << output_filename << "'\n";
|
||||||
|
}
|
||||||
|
}
|
149
src/mcopy.cpp
149
src/mcopy.cpp
|
@ -6,25 +6,16 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <unordered_map>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <tag.h>
|
#include <core.hpp>
|
||||||
#include <fileref.h>
|
|
||||||
|
|
||||||
#ifndef MCOPY_VERSION
|
#ifndef MCOPY_VERSION
|
||||||
#define MCOPY_VERSION "0.0.1"
|
#define MCOPY_VERSION "0.0.1"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct Settings {
|
|
||||||
bool ask{true};
|
|
||||||
bool quiet{false};
|
|
||||||
bool strict{false};
|
|
||||||
bool skip_existing{false};
|
|
||||||
};
|
|
||||||
|
|
||||||
void help() {
|
void help() {
|
||||||
std::cout <<
|
std::cout <<
|
||||||
"mcopy [options]\n" <<
|
"mcopy [options]\n" <<
|
||||||
|
@ -50,144 +41,14 @@ void help() {
|
||||||
"@n Track number\n" <<
|
"@n Track number\n" <<
|
||||||
"@y Year\n" <<
|
"@y Year\n" <<
|
||||||
"@g Genre\n" <<
|
"@g Genre\n" <<
|
||||||
|
"@c Comment\n" <<
|
||||||
"@e File extension\n";
|
"@e File extension\n";
|
||||||
|
|
||||||
std::exit(EXIT_SUCCESS);
|
std::exit(EXIT_SUCCESS);
|
||||||
}
|
}
|
||||||
|
|
||||||
TagLib::FileRef get_file(const std::string& f) {
|
|
||||||
return TagLib::FileRef(TagLib::FileName(f.c_str()));
|
|
||||||
}
|
|
||||||
|
|
||||||
void get_metadata_from_file(const Settings& settings, const std::string& format, const std::string& file) {
|
|
||||||
if (!std::filesystem::is_regular_file(file)) {
|
|
||||||
std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n";
|
|
||||||
if (settings.strict) {
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TagLib::FileRef _file = get_file(file);
|
|
||||||
|
|
||||||
if (_file.isNull() || !_file.audioProperties()) {
|
|
||||||
std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n";
|
|
||||||
if (settings.strict) {
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TagLib::Tag* tag = _file.tag();
|
|
||||||
|
|
||||||
struct FileData {
|
|
||||||
std::string title{};
|
|
||||||
std::string album{};
|
|
||||||
std::string artist{};
|
|
||||||
std::string track{};
|
|
||||||
std::string year{};
|
|
||||||
std::string genre{};
|
|
||||||
std::string comment{};
|
|
||||||
std::string extension{};
|
|
||||||
};
|
|
||||||
|
|
||||||
const auto get_title = [&tag]() -> std::string { return tag->title().to8Bit(true); };
|
|
||||||
const auto get_album = [&tag]() -> std::string { return tag->album().to8Bit(true); };
|
|
||||||
const auto get_artist = [&tag]() -> std::string { return tag->artist().to8Bit(true); };
|
|
||||||
const auto get_track = [&tag]() -> std::string { return std::to_string(tag->track()); };
|
|
||||||
const auto get_year = [&tag]() -> std::string { return std::to_string(tag->year()); };
|
|
||||||
const auto get_genre = [&tag]() -> std::string { return tag->genre().to8Bit(true); };
|
|
||||||
const auto get_comment = [&tag]() -> std::string { return tag->comment().to8Bit(true); };
|
|
||||||
const auto get_extension = [&file]() -> std::string { return file.find_last_of(".") + 1 != file.npos ? file.substr(file.find_last_of(".") + 1) : ""; };
|
|
||||||
|
|
||||||
FileData data{
|
|
||||||
get_title(),
|
|
||||||
get_album(),
|
|
||||||
get_artist(),
|
|
||||||
get_track(),
|
|
||||||
get_year(),
|
|
||||||
get_genre(),
|
|
||||||
get_comment(),
|
|
||||||
get_extension(),
|
|
||||||
};
|
|
||||||
#ifdef _WIN32
|
|
||||||
std::vector<char> the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'};
|
|
||||||
#else
|
|
||||||
std::vector<char> the_forbidden_characters{'/'};
|
|
||||||
#endif
|
|
||||||
|
|
||||||
const std::vector<std::string*> data_members = {&data.title, &data.album, &data.artist, &data.track, &data.year, &data.genre, &data.comment, &data.extension};
|
|
||||||
|
|
||||||
for (const auto& it : data_members) {
|
|
||||||
for (const auto& c : the_forbidden_characters) {
|
|
||||||
it->erase(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; }), it->end());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unordered_map<std::string, std::string> delim_map{
|
|
||||||
{"@t", data.title},
|
|
||||||
{"@a", data.album},
|
|
||||||
{"@A", data.artist},
|
|
||||||
{"@n", data.track},
|
|
||||||
{"@y", data.year},
|
|
||||||
{"@g", data.genre},
|
|
||||||
{"@c", data.comment},
|
|
||||||
{"@e", data.extension},
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string output_filename = format;
|
|
||||||
for (auto& it : delim_map) {
|
|
||||||
while (output_filename.find(it.first) != std::string::npos) {
|
|
||||||
if (it.second.empty()) {
|
|
||||||
it.second = "Unknown";
|
|
||||||
|
|
||||||
if (settings.ask) {
|
|
||||||
std::cerr << "mcopy: failed to retrieve data to represent " << it.first << ", enter an appropriate replacement: ";
|
|
||||||
std::getline(std::cin, it.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
output_filename.replace(output_filename.find(it.first), it.first.length(), it.second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ifstream ef(output_filename);
|
|
||||||
if (ef.good() && settings.skip_existing) {
|
|
||||||
std::cerr << "mcopy: file << '" << file << "' already exists, skipping\n";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string dir{std::filesystem::path{output_filename}.remove_filename()};
|
|
||||||
if (dir.empty()) {
|
|
||||||
std::cerr << "mcopy: failed to retrieve directory\n";
|
|
||||||
if (settings.strict) {
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!std::filesystem::exists(dir) && !std::filesystem::create_directories(dir)) {
|
|
||||||
std::cerr << "mcopy: failed to create directory '" << dir << "'\n";
|
|
||||||
if (settings.strict) {
|
|
||||||
std::exit(EXIT_FAILURE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!std::filesystem::copy_file(file, output_filename, std::filesystem::copy_options::overwrite_existing)) {
|
|
||||||
std::cerr << "mcopy: failed to copy input file '" << file << "' to '" << output_filename << "'\n";
|
|
||||||
if (settings.strict) std::exit(EXIT_FAILURE);
|
|
||||||
} else if (!settings.quiet) {
|
|
||||||
std::cout << "mcopy: copied input file '" << file << "' to '" << output_filename << "'\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
Settings settings{};
|
mcopy::Settings settings{};
|
||||||
std::string format{};
|
std::string format{};
|
||||||
|
|
||||||
std::vector<std::string> unknown_args{};
|
std::vector<std::string> unknown_args{};
|
||||||
|
@ -237,7 +98,7 @@ int main(int argc, char **argv) {
|
||||||
std::ifstream f(it);
|
std::ifstream f(it);
|
||||||
|
|
||||||
if (f.good()) {
|
if (f.good()) {
|
||||||
get_metadata_from_file(settings, format, it);
|
mcopy::write_to_file(settings, format, it);
|
||||||
} else {
|
} else {
|
||||||
std::cerr << "mcopy: input file '" << it << "' not found\n";
|
std::cerr << "mcopy: input file '" << it << "' not found\n";
|
||||||
|
|
||||||
|
|
376
src/mcopy_gui.cpp
Normal file
376
src/mcopy_gui.cpp
Normal file
|
@ -0,0 +1,376 @@
|
||||||
|
/* mcopy
|
||||||
|
*
|
||||||
|
* Copy music by specifying a format
|
||||||
|
* Licensed under the GNU General Public License version 3.
|
||||||
|
* See included LICENSE file for copyright and license details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <core.hpp>
|
||||||
|
#include <wx/wx.h>
|
||||||
|
#include <wx/list.h>
|
||||||
|
#include <wx/listctrl.h>
|
||||||
|
#include <wx/stdpaths.h>
|
||||||
|
#include <wx/filename.h>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#ifndef MCOPY_VERSION
|
||||||
|
#define MCOPY_VERSION "0.0.1"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
class Application : public wxApp {
|
||||||
|
public:
|
||||||
|
bool OnInit() override;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
wxIMPLEMENT_APP(Application);
|
||||||
|
|
||||||
|
enum {
|
||||||
|
ActionID_Import = wxID_OPEN,
|
||||||
|
ActionID_About = wxID_ABOUT,
|
||||||
|
ActionID_Exit = wxID_EXIT,
|
||||||
|
ActionID_Copy = 2,
|
||||||
|
ActionID_Remove = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum {
|
||||||
|
Property_File = 0,
|
||||||
|
Property_Artist = 1,
|
||||||
|
Property_Album = 2,
|
||||||
|
Property_Title = 3,
|
||||||
|
Property_Track = 4,
|
||||||
|
Property_Year = 5,
|
||||||
|
Property_Genre = 6,
|
||||||
|
Property_Comment = 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Global {
|
||||||
|
std::vector<std::pair<bool, std::string>> imported_files{};
|
||||||
|
std::string format_str{};
|
||||||
|
mcopy::Settings settings{};
|
||||||
|
};
|
||||||
|
|
||||||
|
Global global{};
|
||||||
|
|
||||||
|
class Window : public wxFrame {
|
||||||
|
public:
|
||||||
|
Window();
|
||||||
|
virtual ~Window() = default;
|
||||||
|
|
||||||
|
void import_handler(wxCommandEvent& event);
|
||||||
|
void about_handler(wxCommandEvent& event);
|
||||||
|
void exit_handler(wxCommandEvent& event);
|
||||||
|
void rename_handler(wxCommandEvent& event);
|
||||||
|
void append_handler(wxCommandEvent& event);
|
||||||
|
void context_handler(wxContextMenuEvent& event);
|
||||||
|
void remove_handler(wxCommandEvent& event);
|
||||||
|
void selection_handler(wxListEvent& event);
|
||||||
|
|
||||||
|
void handle_file(const std::string& format, const std::string& file);
|
||||||
|
void rebuild_list();
|
||||||
|
private:
|
||||||
|
wxListCtrl* list = nullptr;
|
||||||
|
wxTextCtrl* format = nullptr;
|
||||||
|
|
||||||
|
wxDECLARE_EVENT_TABLE();
|
||||||
|
};
|
||||||
|
|
||||||
|
void Window::import_handler(wxCommandEvent& event) {
|
||||||
|
wxFileDialog open_file_dialog(this, _("Open audio files"), "", "", "All files (*.*)|*.*", wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST);
|
||||||
|
|
||||||
|
if (open_file_dialog.ShowModal() == wxID_CANCEL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wxArrayString paths{};
|
||||||
|
open_file_dialog.GetPaths(paths);
|
||||||
|
|
||||||
|
for (const auto& it : paths) {
|
||||||
|
const std::string f{it.ToStdString()};
|
||||||
|
if (std::filesystem::exists(f) == false) {
|
||||||
|
wxMessageBox("Imported file '" + f + "' doesn't exist.", "File does not exist.", wxICON_ERROR | wxOK);
|
||||||
|
global.imported_files.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
global.imported_files.push_back({true, f});
|
||||||
|
}
|
||||||
|
|
||||||
|
wxMenuItem* copy_item = GetMenuBar()->FindItem(ActionID_Copy);
|
||||||
|
if (copy_item) {
|
||||||
|
copy_item->Enable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
append_handler(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::about_handler(wxCommandEvent& event) {
|
||||||
|
wxMessageBox("mcopy_gui\n\nLicensed under the GNU General Public License version 3.\nCopyright (c) 2024 speedie\nhttps://git.speedie.site/speedie/mcopy\nAlso available as a command-line application.", "About mcopy_gui", wxOK | wxICON_INFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::exit_handler(wxCommandEvent& event) {
|
||||||
|
Close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::rename_handler(wxCommandEvent& event) {
|
||||||
|
if (this->format == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->format->IsEmpty()) {
|
||||||
|
wxMessageBox("You must specify a format in order to rename.", "Error", wxICON_ERROR | wxOK);
|
||||||
|
global.imported_files.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long _index = -1;
|
||||||
|
_index = list->GetNextItem(_index, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
|
||||||
|
|
||||||
|
if (_index == wxNOT_FOUND) {
|
||||||
|
for (auto& it : global.imported_files) {
|
||||||
|
it.first = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
global.format_str = this->format->GetValue().ToStdString();
|
||||||
|
|
||||||
|
int i{0};
|
||||||
|
for (const auto& it : global.imported_files) {
|
||||||
|
if (!it.first) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatusText("Copying '" + it.second + "'.");
|
||||||
|
handle_file(global.format_str, it.second);
|
||||||
|
SetStatusText("Copied '" + it.second + "'.");
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
|
||||||
|
wxMessageBox("Successfully copied " + std::to_string(i) + " file(s).", "Complete", wxOK | wxICON_INFORMATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::handle_file(const std::string& format, const std::string& file) {
|
||||||
|
std::pair<mcopy::FileStatus, mcopy::FileData> pair = mcopy::get_metadata_from_file(file);
|
||||||
|
mcopy::FileData& data = pair.second;
|
||||||
|
#ifdef _WIN32
|
||||||
|
std::vector<char> the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'};
|
||||||
|
#else
|
||||||
|
std::vector<char> the_forbidden_characters{'/'};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const std::vector<std::string*> data_members = {&data.title, &data.album, &data.artist, &data.track, &data.year, &data.genre, &data.comment, &data.extension};
|
||||||
|
for (const auto& it : data_members) {
|
||||||
|
for (const auto& c : the_forbidden_characters) {
|
||||||
|
it->erase(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; }), it->end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> delim_map{
|
||||||
|
{"@t", data.title},
|
||||||
|
{"@a", data.album},
|
||||||
|
{"@A", data.artist},
|
||||||
|
{"@n", data.track},
|
||||||
|
{"@y", data.year},
|
||||||
|
{"@g", data.genre},
|
||||||
|
{"@c", data.comment},
|
||||||
|
{"@e", data.extension},
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string output_filename = format;
|
||||||
|
for (auto& it : delim_map) {
|
||||||
|
while (output_filename.find(it.first) != std::string::npos) {
|
||||||
|
if (it.second.empty()) {
|
||||||
|
it.second = "Unknown";
|
||||||
|
|
||||||
|
wxTextEntryDialog dialog(this, "Failed to retrieve data to represent " + it.first + ", enter an appropriate replacement: ", "Please input data", wxEmptyString);
|
||||||
|
if (dialog.ShowModal() == wxID_OK) {
|
||||||
|
it.second = dialog.GetValue().ToStdString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output_filename.replace(output_filename.find(it.first), it.first.length(), it.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string dir{std::filesystem::path{output_filename}.remove_filename()};
|
||||||
|
if (dir.empty()) {
|
||||||
|
wxMessageBox("Failed to retrieve directory.", "Error", wxOK | wxICON_ERROR);
|
||||||
|
global.imported_files.clear();
|
||||||
|
list->ClearAll();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::exists(dir) && !std::filesystem::create_directories(dir)) {
|
||||||
|
wxMessageBox("Failed to create directory.", "Error", wxOK | wxICON_ERROR);
|
||||||
|
global.imported_files.clear();
|
||||||
|
list->ClearAll();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::filesystem::copy_file(file, output_filename, std::filesystem::copy_options::overwrite_existing)) {
|
||||||
|
std::cerr << "mcopy: failed to copy input file '" << file << "' to '" << output_filename << "'\n";
|
||||||
|
|
||||||
|
wxMessageBox("Failed to copy input file.", "Error", wxOK | wxICON_ERROR);
|
||||||
|
global.imported_files.clear();
|
||||||
|
list->ClearAll();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::remove_handler(wxCommandEvent& event) {
|
||||||
|
long index{list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)};
|
||||||
|
if (index != wxNOT_FOUND) {
|
||||||
|
list->DeleteItem(index);
|
||||||
|
global.imported_files.erase(global.imported_files.begin() + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuild_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::rebuild_list() {
|
||||||
|
if (list == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list->ClearAll();
|
||||||
|
|
||||||
|
list->InsertColumn(Property_File, "File", wxLIST_FORMAT_LEFT, 300);
|
||||||
|
list->InsertColumn(Property_Artist, "Artist", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
list->InsertColumn(Property_Album, "Album", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
list->InsertColumn(Property_Title, "Title", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
list->InsertColumn(Property_Track, "Track", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
list->InsertColumn(Property_Year, "Year", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
list->InsertColumn(Property_Genre, "Genre", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
list->InsertColumn(Property_Comment, "Comment", wxLIST_FORMAT_RIGHT, 100);
|
||||||
|
|
||||||
|
for (std::size_t i{0}; i < global.imported_files.size(); i++) {
|
||||||
|
const auto& data = mcopy::get_metadata_from_file(global.imported_files.at(i).second);
|
||||||
|
|
||||||
|
if (data.first == mcopy::FileStatus::Failure) {
|
||||||
|
wxMessageBox("Imported file '" + global.imported_files.at(i).second + "' is invalid.", "Invalid file", wxICON_ERROR | wxOK);
|
||||||
|
list->ClearAll();
|
||||||
|
global.imported_files.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list->InsertItem(i, global.imported_files.at(i).second);
|
||||||
|
|
||||||
|
list->SetItem(i, Property_Year, data.second.year);
|
||||||
|
list->SetItem(i, Property_Album, data.second.album);
|
||||||
|
list->SetItem(i, Property_Artist, data.second.artist);
|
||||||
|
list->SetItem(i, Property_Title, data.second.title);
|
||||||
|
list->SetItem(i, Property_Track, data.second.track);
|
||||||
|
list->SetItem(i, Property_Comment, data.second.comment);
|
||||||
|
list->SetItem(i, Property_Genre, data.second.genre);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::append_handler(wxCommandEvent& event) {
|
||||||
|
rebuild_list();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::context_handler(wxContextMenuEvent& event) {
|
||||||
|
wxMenu menu;
|
||||||
|
|
||||||
|
long index{list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)};
|
||||||
|
if (index != wxNOT_FOUND) {
|
||||||
|
menu.Append(ActionID_Remove, "Delete");
|
||||||
|
} else {
|
||||||
|
menu.Append(ActionID_Import, "Import");
|
||||||
|
}
|
||||||
|
|
||||||
|
wxPoint pos = event.GetPosition();
|
||||||
|
pos = ScreenToClient(pos);
|
||||||
|
|
||||||
|
PopupMenu(&menu, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Window::selection_handler(wxListEvent& event) {
|
||||||
|
long index = event.GetIndex();
|
||||||
|
if (index == wxNOT_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string file_text = list->GetItemText(index).ToStdString();
|
||||||
|
|
||||||
|
for (auto& it : global.imported_files) {
|
||||||
|
it.first = false;
|
||||||
|
if (it.second == file_text) {
|
||||||
|
it.first = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Window::Window() : wxFrame(nullptr, wxID_ANY, "mcopy_gui") {
|
||||||
|
const auto create_menu = [this]() -> void {
|
||||||
|
wxMenu* menu_file = new wxMenu;
|
||||||
|
menu_file->Append(ActionID_Import, "&Import\tCtrl-I", "Import files to copy.");
|
||||||
|
menu_file->Append(ActionID_Copy, "&Copy\tCtrl-Shift-C", "Copy imported files");
|
||||||
|
|
||||||
|
wxMenuItem* copy_item = menu_file->FindItem(ActionID_Copy);
|
||||||
|
if (copy_item && global.imported_files.empty()) {
|
||||||
|
copy_item->Enable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
wxMenu* menu_help = new wxMenu;
|
||||||
|
menu_help->Append(ActionID_About);
|
||||||
|
|
||||||
|
wxMenuBar* bar = new wxMenuBar;
|
||||||
|
bar->Append(menu_file, "&File");
|
||||||
|
bar->Append(menu_help, "&Help");
|
||||||
|
|
||||||
|
SetMenuBar(bar);
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto create_file_table = [this]() -> void {
|
||||||
|
wxPanel* panel = new wxPanel(this, wxID_ANY);
|
||||||
|
|
||||||
|
list = new wxListCtrl(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT);
|
||||||
|
|
||||||
|
if (list == nullptr) {
|
||||||
|
throw std::runtime_error{"list == nullptr"};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& home_sweet_home{wxFileName(wxStandardPaths::Get().GetUserDataDir()).GetPath().ToStdString()};
|
||||||
|
|
||||||
|
wxStaticText* label = new wxStaticText(panel, wxID_ANY, "Format:", wxDefaultPosition, wxSize(70, 30));
|
||||||
|
#ifdef _WIN32
|
||||||
|
this->format = new wxTextCtrl(panel, wxID_ANY, home_sweet_home + "\\Music\\@A\\@a\\@n. @A - @t.@e", wxDefaultPosition, wxSize(500, 30));
|
||||||
|
#else
|
||||||
|
this->format = new wxTextCtrl(panel, wxID_ANY, home_sweet_home + "/Music/@A/@a/@n. @A - @t.@e", wxDefaultPosition, wxSize(500, 30));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL);
|
||||||
|
wxBoxSizer* input_sizer = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
|
||||||
|
input_sizer->Add(label, 0, wxALL | wxCENTER, 5);
|
||||||
|
input_sizer->Add(this->format, 1, wxALL | wxCENTER, 5);
|
||||||
|
|
||||||
|
top_sizer->Add(list, 1, wxEXPAND | wxALL, 5); // Add wxListCtrl to top_sizer
|
||||||
|
top_sizer->Add(input_sizer, 0, wxEXPAND | wxALL, 5); // Add input_sizer (with label and textbox) to top_sizer
|
||||||
|
|
||||||
|
panel->SetSizer(top_sizer);
|
||||||
|
};
|
||||||
|
|
||||||
|
create_menu();
|
||||||
|
create_file_table();
|
||||||
|
|
||||||
|
CreateStatusBar();
|
||||||
|
|
||||||
|
Bind(wxEVT_MENU, &Window::import_handler, this, ActionID_Import);
|
||||||
|
Bind(wxEVT_MENU, &Window::rename_handler, this, ActionID_Copy);
|
||||||
|
Bind(wxEVT_MENU, &Window::about_handler, this, ActionID_About);
|
||||||
|
Bind(wxEVT_MENU, &Window::exit_handler, this, ActionID_Exit);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Application::OnInit() {
|
||||||
|
Window* win = new Window();
|
||||||
|
return win->Show(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
wxBEGIN_EVENT_TABLE(Window, wxFrame)
|
||||||
|
EVT_CONTEXT_MENU(Window::context_handler)
|
||||||
|
EVT_MENU(ActionID_Import, Window::import_handler)
|
||||||
|
EVT_MENU(ActionID_Remove, Window::remove_handler)
|
||||||
|
EVT_LIST_ITEM_SELECTED(wxID_ANY, Window::selection_handler)
|
||||||
|
wxEND_EVENT_TABLE()
|
Loading…
Reference in a new issue