diff --git a/README.md b/README.md index 35b7fc3..1e7a931 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # mcopy -Rename music by specifying a format \ No newline at end of file +Rename music by specifying a format diff --git a/mcopy.cpp b/mcopy.cpp deleted file mode 100644 index bf895ba..0000000 --- a/mcopy.cpp +++ /dev/null @@ -1,195 +0,0 @@ -/* 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 -#include -#include -#include -#include -#include -#include -#include - -#ifndef VERSION -#define VERSION "0.1" -#endif - -bool ask = true; -bool quiet = false; - -void help() { - std::cout << - "mcopy " << VERSION << "\n\n" << - "mcopy -f, --format Specify a format to output files using\n" << - "mcopy -a, --ask Ask the user if metadata cannot be retrieved from file\n" << - "mcopy -na, --no-ask Don't ask if metadata cannot be retrieved from file\n" << - "mcopy -q, --quiet Don't print status messages. Warnings will still be printed\n" << - "mcopy -nq, --no-quiet Print status messages\n" << - "mcopy -h, --help Display help\n" << - "mcopy -v, --version Display version\n" << - "\n" << - "Example: mcopy --format \"/home/anon/Music/Albums/@A/@a/@n. @A - @t.flac\" \"~/Downloads/myflac1.flac\" \"~/Downloads/myflac2.flac\n" << - "\n" << - "Formats:\n" << - "\n" << - "@A Artist\n" << - "@a Album name\n" << - "@t Title\n" << - "@n Track number\n"; -} - -TagLib::FileRef getFile(std::string str) { - const char *cstr = str.c_str(); - TagLib::FileName fn(cstr); - TagLib::FileRef file(fn); - - return file; -} - -void getMetadataFromFile(std::string format, std::string str) { - TagLib::FileRef file = getFile(str); - TagLib::Tag *tag = file.tag(); - - if (file.isNull() || !file.audioProperties()) { - std::cerr << "File '" << str << "' is not valid. Skipping.\n"; - return; - } - - std::string title = tag->title().to8Bit(true); - std::string album = tag->album().to8Bit(true); - std::string artist = tag->artist().to8Bit(true); - std::string track = std::to_string(tag->track()); - - // If information is missing, ask for the missing details - if (title.empty()) { - title = "Unknown"; - - if (ask) { - std::cerr << "mcopy: Could not retrieve title; please specify\n> "; - std::getline(std::cin, title); - } - } - - if (album.empty()) { - album = "Unknown"; - - if (ask) { - std::cerr << "mcopy: Could not retrieve album; please specify\n> "; - std::getline(std::cin, album); - } - } - - if (artist.empty()) { - artist = "Unknown"; - - if (ask) { - std::cerr << "mcopy: Could not retrieve artist; please specify\n> "; - std::getline(std::cin, artist); - } - } - - if (track.empty()) { - track = "Unknown"; - - if (ask) { - std::cerr << "mcopy: Could not retrieve track; please specify\n> "; - std::getline(std::cin, track); - } - } - - if (!std::cin) // Failed to get details, skipping - return; - - std::string filename = format; - - // TODO: Replace this junk - filename = std::regex_replace(filename, std::regex("@t"), title); - filename = std::regex_replace(filename, std::regex("@a"), album); - filename = std::regex_replace(filename, std::regex("@A"), artist); - filename = std::regex_replace(filename, std::regex("@n"), track); - - std::ifstream ef(filename); - - if (static_cast(ef.good())) { - std::cerr << "mcopy: File already exists, skipping.\n"; - return; - } - - std::filesystem::path fs; - std::string dir = (fs = filename).remove_filename(); - - if (dir.empty()) { - std::cerr << "mcopy: Was not able to get a directory.\n"; - return; - } - - if (!std::filesystem::exists(fs.remove_filename())) { - if (!std::filesystem::create_directories(dir)) { - std::cerr << "mcopy: Failed to create directory '" << dir << "'\n"; - } - } - - if (!std::filesystem::copy_file(str, filename)) { - std::cerr << "mcopy: Failed to copy file " << str << " to " << filename << "\n"; - std::exit(1); - } else if (!quiet) { - std::cout << "mcopy: Copied file " << str << " to " << filename << "\n"; - } -} - -int main(int argc, char **argv) { - bool set{false}; - std::string format; - - for (int i{1}; i < argc; i++) { - std::string arg = argv[i]; - - if (!arg.compare("-h") || !arg.compare("--help")) { - help(); - } else if (!arg.compare("-v") || !arg.compare("--version")) { - std::cout << "mcopy " << VERSION << '\n'; - return 0; - } else if (!arg.compare("-a") || !arg.compare("--ask")) { - ask = true; - } else if (!arg.compare("-na") || !arg.compare("--no-ask")) { - ask = false; - } else if (!arg.compare("-q") || !arg.compare("--quiet")) { - quiet = true; - } else if (!arg.compare("-nq") || !arg.compare("--no-quiet")) { - quiet = false; - } else if (!arg.compare("-f") || !arg.compare("--format")) { - if (argc > i+1) { - format = argv[++i]; - } - continue; - } - } - - if (!format.compare("")) { - std::cerr << "mcopy: You must specify a format.\n"; - return 1; - } - - if (argc < 4) { - std::cerr << "mcopy: You must specify a file to copy.\n"; - return 1; - } - - for (int i{1}; i < argc; i++) { - std::ifstream f(argv[i]); - - if (static_cast(f.good())) { - set = true; - getMetadataFromFile(format, argv[i]); - } - } - - if (!set) { - std::cerr << "mcopy: File not found.\n"; - return 1; - } -} diff --git a/meson.build b/meson.build index ba98fd5..d3f6f82 100644 --- a/meson.build +++ b/meson.build @@ -1,13 +1,11 @@ project( 'mcopy', 'cpp', - version : '"0.1"', + version : '"0.0.1"', ) -cc = meson.get_compiler('cpp') - project_source_files = [ - 'mcopy.cpp', + 'src/mcopy.cpp', ] project_dependencies = [ @@ -15,7 +13,7 @@ project_dependencies = [ ] build_args = [ - '-DVERSION=' + meson.project_version(), + '-DMCOPY_VERSION=' + meson.project_version(), ] project_target = executable( diff --git a/src/mcopy.cpp b/src/mcopy.cpp new file mode 100644 index 0000000..e2f8c0b --- /dev/null +++ b/src/mcopy.cpp @@ -0,0 +1,203 @@ +/* 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 +#include +#include +#include +#include +#include +#include +#include + +#ifndef MCOPY_VERSION +#define MCOPY_VERSION "0.0.1" +#endif + +struct Settings { + bool ask{true}; + bool quiet{false}; + bool strict{false}; + bool skip_existing{false}; +}; + +void help() { + std::cout << + "mcopy [options]\n" << + " -f, --format Specify a format to output files using\n" << + " -a, --ask Ask the user if metadata cannot be retrieved from file\n" << + " -na, --no-ask Don't ask if metadata cannot be retrieved from file\n" << + " -q, --quiet Don't print status messages. Warnings will still be printed\n" << + " -nq, --no-quiet Print status messages\n" << + " -s, --strict Error on any invalid files\n" << + " -ns, --no-strict Don't error on any invalid files\n" << + " -sk, --skip-existing Skip existing files\n" << + " -ow, --overwrite-existing Overwrite any existing files\n" << + " -h, --help Display help\n" << + " -v, --version Display version\n" << + "\n" << + "Example: mcopy --format \"~/Music/Albums/@A/@a/@n. @A - @t.flac\" \"~/Downloads/myflac1.flac\" \"~/Downloads/myflac2.flac\n" << + "\n" << + "Formats:\n" << + "\n" << + "@A Artist\n" << + "@a Album name\n" << + "@t Title\n" << + "@n Track number\n"; + + std::exit(EXIT_SUCCESS); +} + +TagLib::FileRef get_file(const std::string& str) { + return TagLib::FileRef(TagLib::FileName(str.c_str())); +} + +void get_metadata_from_file(const Settings& settings, const std::string& format, const std::string& file) { + TagLib::FileRef _file = get_file(file); + TagLib::Tag* tag = _file.tag(); + + if (_file.isNull() || !_file.audioProperties()) { + std::cerr << "mcopy: invalid file: '" << file << "' is skipped.\n"; + return; + } + + struct FileData { + std::string title{}; + std::string album{}; + std::string artist{}; + std::string track{}; + }; + + 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()); }; + + FileData data{get_title(), get_album(), get_artist(), get_track()}; + +#ifdef _WIN32 + std::vector the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'}; +#else + std::vector the_forbidden_characters{'/'}; +#endif + + for (auto& it : std::vector{&data.title, &data.album, &data.artist, &data.track}) { + for (const auto& c : the_forbidden_characters) + static_cast(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; })); + } + + for (const auto& it : std::unordered_map{{"title", data.title}, {"album", data.album}, {"artist", data.artist}, {"track", data.track}}) { + if (it.second.empty()) { + it.second = "Unknown"; + + if (settings.ask) { + std::cerr << "mcopy: failed to retrieve " << it.first << ", enter an appropriate replacement: "; + std::getline(std::cin, it.second); + } + } + } + + std::string output_filename = format; + for (const auto& it : std::unordered_map{{"@t", data.title}, {"@a", data.album}, {"@A", data.artist}, {"@n", data.track}}) { + while (output_filename.find(it.first) != std::string::npos) { + 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) { + Settings settings{}; + std::string format{}; + + std::vector unknown_args{}; + for (int i{1}; i < argc; i++) { + std::string arg = argv[i]; + + if (!arg.compare("-h") || !arg.compare("--help")) { + help(); + } else if (!arg.compare("-v") || !arg.compare("--version")) { + std::cout << "mcopy " << MCOPY_VERSION << '\n'; + return 0; + } else if (!arg.compare("-a") || !arg.compare("--ask")) { + settings.ask = true; + } else if (!arg.compare("-na") || !arg.compare("--no-ask")) { + settings.ask = false; + } else if (!arg.compare("-q") || !arg.compare("--quiet")) { + settings.quiet = true; + } else if (!arg.compare("-nq") || !arg.compare("--no-quiet")) { + settings.quiet = false; + } else if (!arg.compare("-s") || !arg.compare("--strict")) { + settings.strict = true; + } else if (!arg.compare("-ns") || !arg.compare("--no-strict")) { + settings.strict = false; + } else if (!arg.compare("-sk") || !arg.compare("--skip-existing")) { + settings.skip_existing = true; + } else if (!arg.compare("-ow") || !arg.compare("--overwrite-existing")) { + settings.skip_existing = false; + } else if (!arg.compare("-f") || !arg.compare("--format")) { + if (argc > i+1) format = argv[++i]; + continue; + } else { + unknown_args.push_back(argv[i]); + } + } + + if (!format.compare("")) { + std::cerr << "mcopy: format parameter not specified\n"; + return 1; + } + + if (argc < 4) { + std::cerr << "mcopy: input file(s) not specified\n"; + return 1; + } + + for (const auto& it : unknown_args) { + std::ifstream f(it); + + if (f.good()) { + get_metadata_from_file(settings, format, it); + } else { + std::cerr << "mcopy: input file '" << it << "' not found\n"; + + if (settings.strict) std::exit(EXIT_FAILURE); + } + } +}