btag-1.1.3/LICENSE000644 001750 001750 00000002467 11727756360 015117 0ustar00fernandofernando000000 000000 Copyright (c) 2010-2012 Fernando Tarlá Cardoso Lemos All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. btag-1.1.3/man/btag.1000644 001750 001750 00000012443 11727756360 015657 0ustar00fernandofernando000000 000000 .TH BTAG 1 2011-06-04 "btag" "btag Manual" .SH NAME btag \- A command line based multimedia tagger .SH SYNOPSIS .B btag [\fIoptions\fR] \fIpath1\fR [\fIpath2\fR] [\fIpath3\fR] ... .SH DESCRIPTION btag is a TagLib-based command line multimedia tag editor that attempts to automate the process of tagging a lot of files at once. It uses the tags found in the supplied files as well as interactive user input to determine new values for the tags. It can also optionally rename files and directories based on those new values. You can supply paths to files or directories to btag. Directories are recursively traversed and all files found are tagged. Directories are also handled differently in the sense that btag will attempt to keep information about the previously tagged files to provide sane defaults for all other files in the same parent directory. Only files with file extensions supported by TagLib are considered. .SH OPTIONS .TP 33 .B \-D\fR/\fB\-\-dry\-run Don't do anything, just show what would have been done (dry run mode) .TP .B \-d\fR/\fB\-\-dir\-rename\-format \fIformat Use \fIformat\fR to rename the directories where the multimedia files were found .TP .B \-i\fR/\fB\-\-input\-filter \fIfilter Use \fIfilter\fR as the input filter .TP .B \-f\fR/\fB\-\-filter \fIfilter Use \fIfilter\fR as both the input and the output filter .TP .B \-h\fR/\fB\-\-help Display usage information and exit .TP .B \-n\fR/\fB\-\-renaming\-filter \fIfilter Use \fIfilter\fR as the renaming filter .TP .B \-o\fR/\fB\-\-output\-filter \fIfilter Use \fIfilter\fR as the input filter .TP .B \-r\fR/\fB\-\-file\-rename\-format \fIformat Use \fIformat\fR to rename the multimedia files .TP .B \-t\fR/\fB\-\-title\-locale \fIlocale Use \fIlocale\fR for proper (although lax) locale\-specific title casing .SH INPUT AND OUTPUT FILTERS btag supports input and output filters that are applied to the text fields (artist, album and song title). Those filters can protect against basic mistakes such as duplicate whitespace. Input filters are used on the tags as they are loaded from the multimedia files. This filtered information is used to provide suggestions to the user when the interactive tagger requests information for those text fields. If an output filter is configured, the user input is then filtered, and if the filtered text does not match the user input, the user is asked for confirmation. In most cases, the input filter should match the output filter (which is why the \fB\-f\fR option is handy). You may choose to specify only an input filter, in which case the user input is not filtered. If you don't specify an input filter, though, the default input filter will be used. The currently available filters are: .TP 14 .B basic Provides basic filtering by removing duplicate or trailing whitespace, is the default input filter and the base for all other filters .TP .B first_upper The first character in the field is uppercased, while all others are lowercased .TP .B lower All characters are lowercased .TP .B title The first character of each word is uppercased (with exceptions), while all others are lowercased .TP .B upper All characters are uppercased .PP The title capitalization algorithm will follow locale\-specific context\-insensitive rules depending on the value of the \fB\-t\fR parameter. Note that strict title capitalization rules often depend on the context in which the words are used, the precise analysis of which is much beyond the scope of btag. The currently supported title locale specifications are: .TP 4 .B en English (default) .TP .B es Spanish .SH RENAMING FORMATS If a format is specified with the \fB\-r\fR option, the tagged multimedia files are renamed accordingly. Likewise, if the \fB\-d\fR option is used, the directory in which multimedia files were tagged is renamed according to the specified format. The specified format is converted to a file or directory name using the following substitutions: .TP 9 .B %artist Artist name .TP .B %album Album name .TP .B %year Year of release .TP .B %track Track number (only replaced by the \fB\-r\fR option) .TP .B %title Song title (only replaced by the \fB\-r\fR option) .PP Renaming happens after the tags are written, and it's relative to btag's working directory. For directory renaming, the last known artist, album and year information is used. Only directories that contain files that were tagged by btag are renamed. btag does not prevent you from overwriting existing files using the formats described here. .SH RENAMING FILTERS Renaming filters are used to ensure that the file and directory names generated using the renaming formats (if specified) are valid (safe) in the context of the current file system. The following renaming filters are currently available: .TP 14 .B conservative Conservative character replacements are performed, recommended for FAT32 file systems .TP .B unix Generates file and directory names that should be valid in an Unix environment (default) .SH EXAMPLE Using title casing with English rules and sensible renaming formats generating FAT32\-safe file and directory names: .nf $ btag \-\-file\-rename\-format '%track. %title' \\ \-\-dir\-rename\-format '%album (%year)' \\ \-\-filter title \-\-title\-locale en \\ \-\-renaming\-format conservative /path/to/myalbum .fi Using an input filter only: .nf $ btag \-\-input\-filter lower /path/to/myalbum .fi btag-1.1.3/src/TitleLocalizationHandler.h000644 001750 001750 00000002007 11727756360 021770 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef TITLE_LOCALIZATION_HANDLER_H #define TITLE_LOCALIZATION_HANDLER_H #include #include #include class TitleLocalizationHandler { public: enum word_style { WORD_STYLE_LOWER, WORD_STYLE_UPPER, WORD_STYLE_FIRST_UPPER, WORD_STYLE_CUSTOM }; struct word_hint { word_style style; boost::optional > uppercase; word_hint(word_style s) : style(s) {} word_hint(const std::vector &u) : style(WORD_STYLE_CUSTOM), uppercase(u) {} }; virtual word_hint word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const = 0; virtual bool is_acronym(const std::wstring &word) const; virtual wchar_t *punctuation_list() const; }; #endif btag-1.1.3/src/SimpleCapitalizationFilter.h000644 001750 001750 00000001271 11727756360 022335 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef SIMPLE_CAPITALIZATION_FILTER_H #define SIMPLE_CAPITALIZATION_FILTER_H #include "CapitalizationFilter.h" class SimpleCapitalizationFilter : public CapitalizationFilter { public: enum capitalization_mode { CAPITALIZATION_MODE_ALL_LOWER, CAPITALIZATION_MODE_ALL_UPPER, CAPITALIZATION_MODE_FIRST_UPPER }; SimpleCapitalizationFilter(capitalization_mode mode); std::wstring filter(const std::wstring &input) const; private: capitalization_mode m_mode; }; #endif btag-1.1.3/src/SimpleCapitalizationFilter.cpp000644 001750 001750 00000002306 11727756360 022670 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include "SimpleCapitalizationFilter.h" SimpleCapitalizationFilter::SimpleCapitalizationFilter(capitalization_mode mode) : m_mode(mode) { } std::wstring SimpleCapitalizationFilter::filter(const std::wstring &input) const { std::wstring processed = BasicStringFilter::filter(input); std::wstring res; res.reserve(processed.size()); boost::char_separator separator(L" "); boost::tokenizer, std::wstring::const_iterator, std::wstring > words(processed, separator); BOOST_FOREACH(const std::wstring &word, words) { std::wstring new_word; new_word.reserve(word.size()); for (std::wstring::size_type i = 0; i < word.size(); ++i) new_word += m_mode == CAPITALIZATION_MODE_ALL_UPPER ? uppercase(word[i]) : lowercase(word[i]); if (!res.empty()) res += L' '; res += new_word; } if (m_mode == CAPITALIZATION_MODE_FIRST_UPPER && res.size()) res[0] = uppercase(res[0]); return res; } btag-1.1.3/src/RenamingFilter.h000644 001750 001750 00000000737 11727756360 017756 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef RENAMING_FILTER_H #define RENAMING_FILTER_H #include class RenamingFilter { public: std::wstring filter(const std::wstring &path_component) const; private: virtual bool is_character_allowed(wchar_t c) const = 0; virtual wchar_t replacement_character(wchar_t c) const = 0; }; #endif btag-1.1.3/src/InteractiveTagger.cpp000644 001750 001750 00000032030 11727756360 021001 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include #include #include #include #include #include "InteractiveTagger.h" #include "validators.h" #include "wide_string_cast.h" namespace fs = boost::filesystem; InteractiveTagger::InteractiveTagger() : m_input_filter(NULL), m_output_filter(NULL), m_dry_run(false) { // Load the extensions supported by TagLib TagLib::StringList extensions = TagLib::FileRef::defaultFileExtensions(); for (TagLib::StringList::ConstIterator it = extensions.begin(); it != extensions.end(); ++it) m_supported_extensions.push_back(L'.' + it->toWString()); } void InteractiveTagger::set_file_rename_format(const std::string &format) { m_file_rename_format = boost::lexical_cast(format); } void InteractiveTagger::set_dir_rename_format(const std::string &format) { m_dir_rename_format = boost::lexical_cast(format); } void InteractiveTagger::tag(int num_paths, const char **paths) { assert(m_terminal); // Build a list with the normalized paths std::list path_list; for (int i = 0; i < num_paths; ++i) { char *real_path = realpath(paths[i], NULL); if (!real_path) { std::string errstr("\""); errstr += paths[i]; errstr += "\" is not a valid path"; m_terminal->display_warning_message(errstr); continue; } path_list.push_back(fs::path(boost::lexical_cast(std::string(real_path)))); free(real_path); } // Sort it and tag the paths path_list.sort(); BOOST_FOREACH(const fs::path &path, path_list) { // Check if the path exists if (!fs::exists(path)) { m_terminal->display_warning_message(L"Path \"" + path.string() + L"\" not found, skipping..."); continue; } try { // If it's a regular file, just tag it if (fs::is_regular_file(path)) { if (!is_supported_extension(path)) { m_terminal->display_warning_message(L"Path \"" + path.string() + L"\" has no supported extension, skipping..."); continue; } tag_file(path); } // If it's a directory, tag its contents else if (fs::is_directory(path)) { tag_directory(path); } // It's none of the above, do something about it else { m_terminal->display_warning_message(L"Path \"" + path.string() + L"\" is not a regular file or directory, skipping..."); } } catch (std::exception &e) { m_terminal->display_warning_message(e.what()); } } // Save the unsaved files if (!m_unsaved_files.empty() && (m_dry_run || m_terminal->ask_yes_no_question(L"=== OK to save the changes to the files?"))) { if (m_dry_run) m_terminal->display_info_message("=== Not saving changes (dry run mode)"); BOOST_FOREACH(TagLib::FileRef &f, m_unsaved_files) { std::string message(m_dry_run ? "Not saving" : "Saving"); message += " \"" + std::string(f.file()->name()) + '\"'; m_terminal->display_info_message(message); if (!m_dry_run && !f.save()) m_terminal->display_warning_message("Unable to save " + std::string(f.file()->name()) + "\""); } } // Perform the pending renames if (!m_pending_renames.empty() && (m_dry_run || m_terminal->ask_yes_no_question(L"=== OK to rename the files?"))) { if (m_dry_run) m_terminal->display_info_message("=== Not renaming files (dry run mode)"); std::list >::const_iterator it; for (it = m_pending_renames.begin(); it != m_pending_renames.end(); ++it) { const fs::path &from((*it).first); const fs::path &to((*it).second); m_terminal->display_info_message(from.string()); m_terminal->display_info_message(L"-> " + to.string()); if (!m_dry_run) { try { fs::rename(from, to); } catch (std::exception &e) { m_terminal->display_warning_message(e.what()); } } } } m_terminal->display_info_message("=== All done!"); } bool InteractiveTagger::is_supported_extension(const fs::path &path) { std::wstring extension(path.extension().string()); boost::to_lower(extension); BOOST_FOREACH(const std::wstring &supported_extension, m_supported_extensions) { if (extension == supported_extension) return true; } return false; } std::wstring InteractiveTagger::replace_tokens(const std::wstring &str, const std::map &replacements) { std::wstring res; res.reserve(str.size() * 3); bool parsing_token = false; std::wstring current_token; BOOST_FOREACH(char c, str) { if (c == '%') { if (parsing_token) { std::map::const_iterator it = replacements.find(current_token); res += it == replacements.end() ? current_token : it->second; current_token.erase(); } parsing_token = true; } else if (parsing_token) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) { current_token += c; } else { std::map::const_iterator it = replacements.find(current_token); res += it == replacements.end() ? current_token : it->second; res += c; parsing_token = false; current_token.erase(); } } else { res += c; } } if (parsing_token) { std::map::const_iterator it = replacements.find(current_token); res += it == replacements.end() ? current_token : it->second; } return res; } void InteractiveTagger::tag_file(const fs::path &path, ConfirmationHandler &artist_confirmation, ConfirmationHandler &album_confirmation, int *year, int track) { m_terminal->display_info_message(L"=== Tagging \"" + path.filename().string() + L"\""); // Get a reference to the file TagLib::FileRef f(path.c_str()); // Ask for the artist artist_confirmation.reset(); if (!f.tag()->artist().isNull()) artist_confirmation.set_local_default(m_input_filter->filter(f.tag()->artist().toWString())); artist_confirmation.ask(L"Artist:"); while (!artist_confirmation.complies()) artist_confirmation.ask(L"Artist (confirmation):"); f.tag()->setArtist(artist_confirmation.answer()); // Ask for the album album_confirmation.reset(); if (!f.tag()->album().isNull()) album_confirmation.set_local_default(m_input_filter->filter(f.tag()->album().toWString())); album_confirmation.ask(L"Album:"); while (!album_confirmation.complies()) album_confirmation.ask(L"Album (confirmation):"); f.tag()->setAlbum(album_confirmation.answer()); // Ask for the year boost::optional default_year; if (year && *year != -1) default_year = *year; else if (f.tag()->year()) default_year = f.tag()->year(); YearValidator year_validator; int new_year = m_terminal->ask_number_question(L"Year:", default_year, &year_validator); f.tag()->setYear(new_year); if (year) *year = new_year; // Ask for the track if (track == -1) track = f.tag()->track(); TrackValidator track_validator; int new_track = m_terminal->ask_number_question(L"Track:", track > 0 ? track : boost::optional(), &track_validator); f.tag()->setTrack(new_track); // Ask for the song title ConfirmationHandler title_confirmation(*m_terminal, m_input_filter, m_output_filter); title_confirmation.reset(); if (!f.tag()->title().isNull()) title_confirmation.set_local_default(m_input_filter->filter(f.tag()->title().toWString())); title_confirmation.ask(L"Title:"); while (!title_confirmation.complies()) title_confirmation.ask(L"Title (confirmation):"); f.tag()->setTitle(title_confirmation.answer()); // Reset the comment and genre fields f.tag()->setComment(TagLib::String::null); f.tag()->setGenre(TagLib::String::null); // Add it to the list of unsaved files m_unsaved_files.push_back(f); // Add it to the list of pending renames based on the supplied format if (m_file_rename_format) { std::map tokens; tokens[L"artist"] = m_renaming_filter->filter(artist_confirmation.answer()); tokens[L"album"] = m_renaming_filter->filter(album_confirmation.answer()); tokens[L"year"] = boost::lexical_cast(new_year); std::wstring track_str(boost::lexical_cast(new_track)); if (track_str.size() == 1) track_str = L"0" + track_str; tokens[L"track"] = track_str; tokens[L"title"] = m_renaming_filter->filter(title_confirmation.answer()); fs::path new_path = path.parent_path(); new_path /= replace_tokens(*m_file_rename_format, tokens) + boost::to_lower_copy(path.extension().string()); if (new_path != path) m_pending_renames.push_back(std::pair(path, new_path)); } } void InteractiveTagger::tag_file(const boost::filesystem::path &path) { // Tag a file without recording the global default ConfirmationHandler amnesiac_artist(*m_terminal, m_input_filter, m_output_filter); ConfirmationHandler amnesiac_album(*m_terminal, m_input_filter, m_output_filter); tag_file(path, amnesiac_artist, amnesiac_album, NULL, -1); } void InteractiveTagger::tag_directory(const fs::path &path) { m_terminal->display_info_message(L"=== Entering \"" + path.string() + L"\""); // Create lists of files and subdirectories std::list file_list, dir_list; fs::directory_iterator end_it; for (fs::directory_iterator it(path); it != end_it; ++it) { // Normalize the path (needed here so that we follow symlinks) char *real_path = realpath(it->path().c_str(), NULL); if (!real_path) { m_terminal->display_warning_message(L"\"" + it->path().filename().string() + L"\" is not a valid path, skipping..."); continue; } fs::path boost_path(real_path); free(real_path); // Add to the right list if (fs::is_regular_file(boost_path)) { if (is_supported_extension(boost_path)) { file_list.push_back(boost_path); } else { m_terminal->display_warning_message(L"\"" + it->path().filename().string() + L"\" has no supported extension, skipping..."); } } else if (fs::is_directory(boost_path)) { dir_list.push_back(boost_path); } else { m_terminal->display_warning_message(L"\"" + boost_path.filename().string() + L"\" is not a regular file or directory, skipping..."); } } // Sort those lists file_list.sort(); dir_list.sort(); // Tag all individual files ConfirmationHandler artist(*m_terminal, m_input_filter, m_output_filter); ConfirmationHandler album(*m_terminal, m_input_filter, m_output_filter); int year = -1; if (!file_list.empty()) { int track = 1; BOOST_FOREACH(const fs::path &p, file_list) tag_file(p, artist, album, &year, track++); } // We'll ask confirmation to descend into the subdirectories only if there are files BOOST_FOREACH(const fs::path &p, dir_list) { if (file_list.empty() || m_terminal->ask_yes_no_question(L"Descend into subdirectory \"" + boost::lexical_cast(p.filename()) + L"\"?", false)) tag_directory(p); } // Add it to the list of pending renames based on the supplied format if (!artist.answer().empty() && m_dir_rename_format) { std::map tokens; tokens[L"artist"] = m_renaming_filter->filter(artist.answer()); tokens[L"album"] = m_renaming_filter->filter(album.answer()); tokens[L"year"] = boost::lexical_cast(year); fs::path new_path = path.parent_path(); new_path /= replace_tokens(*m_dir_rename_format, tokens); if (new_path != path) m_pending_renames.push_back(std::pair(path, new_path)); } } btag-1.1.3/src/BasicStringFilter.cpp000644 001750 001750 00000001655 11727756360 020761 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include "BasicStringFilter.h" std::wstring BasicStringFilter::filter(const std::wstring &input) const { std::wstring res; res.reserve(input.size()); bool started = false, in_whitespace = false; BOOST_FOREACH(wchar_t c, input) { if (started) { if (c == '\n' || c == '\r' || c == '\t' || c == ' ' || c == '\v') { in_whitespace = true; } else { if (in_whitespace) { res += ' '; in_whitespace = false; } res += c; } } else if (c != '\n' && c != '\r' && c != '\t' && c != ' ' && c != '\v') { res += c; started = true; } } return res; } btag-1.1.3/src/TitleLocalizationHandler.cpp000644 001750 001750 00000002564 11727756360 022333 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include "TitleLocalizationHandler.h" bool TitleLocalizationHandler::is_acronym(const std::wstring &word) const { if (word.size() < 4 || word[word.size() - 1] != L'.') return false; bool should_be_dot = false; BOOST_FOREACH(wchar_t c, word) { if (should_be_dot && c != L'.') return false; else if (!should_be_dot && c == L'.') return false; else should_be_dot = !should_be_dot; } return true; } wchar_t *TitleLocalizationHandler::punctuation_list() const { static wchar_t punctuation[] = { L',', L'.', L':', L'-', L';', L'?', L'\u00bf', // Unicode INVERTED QUESTION MARK (¿) L'!', L'\u00a1', // Unicode INVERTED EXCLAMATION MARK (¡) L'-', L'\u2014', // Unicode EM DASH (—) L')', L'(', L'"', L'\'', L'\u201c', // Unicode LEFT DOUBLE QUOTATION MARK (“) L'\u201d', // Unicode RIGHT DOUBLE QUOTATION MARK (”) L'\u2018', // Unicode LEFT SINGLE QUOTATION MARK (‘) L'\u2019', // Unicode RIGHT SINGLE QUOTATION MARK (’) 0 }; return punctuation; } btag-1.1.3/src/ConservativeRenamingFilter.h000644 001750 001750 00000002053 11727756360 022340 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CONSERVATIVE_RENAMING_FILTER_H #define CONSERVATIVE_RENAMING_FILTER_H #include "RenamingFilter.h" class ConservativeRenamingFilter : public RenamingFilter { private: bool is_character_allowed(wchar_t c) const { if (c >= L'a' && c <= L'z') return true; if (c >= L'A' && c <= L'Z') return true; if (c >= L'0' && c <= L'9') return true; static wchar_t allowed_chars[] = { L' ', L'%', L'-', L'_', L'@', L'~', L'`', L'!', L'(', L')', L'{', L'}', L'^', L'#', L'&', L'.' }; for (unsigned int i = 0; i < sizeof(allowed_chars) / sizeof(wchar_t); ++i) { if (c == allowed_chars[i]) return true; } return false; } wchar_t replacement_character(wchar_t c) const { return L'_'; } }; #endif btag-1.1.3/src/wide_string_cast.h000644 001750 001750 00000001202 11727756360 020364 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef WIDE_STRING_CAST_H #define WIDE_STRING_CAST_H #include #include #include #include #include namespace boost { template<> inline std::wstring lexical_cast(const std::string &arg) { std::wstring result; BOOST_FOREACH(char c, arg) result += std::use_facet >(std::locale()).widen(c); return result; } } #endif btag-1.1.3/src/StandardConsole.cpp000644 001750 001750 00000010032 11727756360 020453 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include "StandardConsole.h" bool StandardConsole::ask_yes_no_question(const std::wstring &question, const boost::optional &default_answer) { for (;;) { std::wcout << question; if (default_answer) std::wcout << (*default_answer ? L" [Y/n] " : L" [y/N] "); else std::wcout << L" [y/n] "; std::wstring a; std::getline(std::wcin, a); if (default_answer && a.empty()) { return *default_answer; } else if (a.size() == 1) { if (a[0] == 'y' || a[0] == 'Y') return true; else if (a[0] == 'n' || a[0] == 'y') return false; } else if (a.size() == 2 && (a[0] == 'n' || a[0] == 'N') && (a[1] == 'o' || a[1] == 'O')) { return false; } else if (a.size() == 3 && (a[0] == 'y' || a[0] == 'Y') && (a[1] == 'e' || a[1] == 'E') && (a[2] == 's' || a[2] == 'S')) { return true; } std::wcout << L"Please answer \"y\" or \"n\"" << std::endl; } } std::wstring StandardConsole::ask_string_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator) { for (;;) { std::wcout << question << (default_answer ? L" [" + *default_answer + L"] " : L" "); std::wstring answer; std::getline(std::wcin, answer); if (answer.empty()) { if (default_answer) return *default_answer; } else { if (validator) { boost::optional error_message; if (validator->validate(answer, error_message)) return answer; std::wcout << (error_message ? *error_message : L"Unknown validation error") << std::endl; continue; } else { return answer; } } std::wcout << L"Please answer the question" << std::endl; } } int StandardConsole::ask_number_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator) { for (;;) { std::wcout << question; if (default_answer) std::wcout << L" [" << *default_answer << L"] "; else std::wcout << L" "; std::wstring answer; std::getline(std::wcin, answer); if (answer.empty()) { if (default_answer) return *default_answer; } else { int res; try { res = boost::lexical_cast(answer); } catch (boost::bad_lexical_cast &) { std::wcout << L"Please answer the question with a valid number" << std::endl; continue; } if (validator) { boost::optional error_message; if (validator->validate(res, error_message)) return res; std::wcout << (error_message ? *error_message : L"Unknown validation error") << std::endl; continue; } else { return res; } } std::wcout << L"Please answer the question" << std::endl; } } void StandardConsole::display_info_message(const std::string &message) { std::cout << message << std::endl; } void StandardConsole::display_info_message(const std::wstring &message) { std::wcout << message << std::endl; } void StandardConsole::display_warning_message(const std::string &message) { std::cerr << "WARNING: " << message << std::endl; } void StandardConsole::display_warning_message(const std::wstring &message) { std::wcerr << L"WARNING: " << message << std::endl; } btag-1.1.3/src/validators.h000644 001750 001750 00000002103 11727756360 017205 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef VALIDATORS_H #define VALIDATORS_H #include "InteractiveTerminal.h" class YearValidator : public InteractiveTerminal::Validator { public: bool validate(const int &year, boost::optional &error_message) const { if (year > 2500 || year < 1000) { error_message = L"The year should be a number between 1000 and 2500"; return false; } else { return true; } } }; class TrackValidator : public InteractiveTerminal::Validator { public: bool validate(const int &track, boost::optional &error_message) const { if (track > 1000 || track < 1) { error_message = L"The track should be a number between 1 and 1000"; return false; } else { return true; } } }; #endif btag-1.1.3/src/InteractiveTagger.h000644 001750 001750 00000004144 11727756360 020453 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef INTERACTIVE_TAGGER_H #define INTERACTIVE_TAGGER_H #include #include #include #include #include #include #include #include "BasicStringFilter.h" #include "ConfirmationHandler.h" #include "InteractiveTerminal.h" #include "RenamingFilter.h" class InteractiveTagger { public: InteractiveTagger(); void set_terminal(InteractiveTerminal *terminal) { m_terminal = terminal; } void set_file_rename_format(const std::string &format); void set_dir_rename_format(const std::string &format); void set_input_filter(BasicStringFilter *filter) { m_input_filter = filter; } void set_output_filter(BasicStringFilter *filter) { m_output_filter = filter; } void set_renaming_filter(RenamingFilter *filter) { m_renaming_filter = filter; } void set_dry_run(bool dry_run = true) { m_dry_run = dry_run; } void tag(int num_paths, const char **paths); private: bool is_supported_extension(const boost::filesystem::path &path); std::wstring replace_tokens(const std::wstring &str, const std::map &replacements); void tag_file(const boost::filesystem::path &path, ConfirmationHandler &artist_confirmation, ConfirmationHandler &album_confirmation, int *year, int track); void tag_file(const boost::filesystem::path &path); void tag_directory(const boost::filesystem::path &path); BasicStringFilter *m_input_filter, *m_output_filter; RenamingFilter *m_renaming_filter; InteractiveTerminal *m_terminal; boost::optional m_file_rename_format, m_dir_rename_format; bool m_dry_run; std::list m_unsaved_files; std::list > m_pending_renames; std::list m_supported_extensions; }; #endif btag-1.1.3/src/CapitalizationFilter.h000644 001750 001750 00000001325 11727756360 021163 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CAPITALIZATION_FILTER_H #define CAPITALIZATION_FILTER_H #include #include "BasicStringFilter.h" class CapitalizationFilter : public BasicStringFilter { public: bool requires_confirmation_as_output_filter() const { return true; } protected: wchar_t lowercase(wchar_t c) const { return std::use_facet >(std::locale()).tolower(c); } wchar_t uppercase(wchar_t c) const { return std::use_facet >(std::locale()).toupper(c); } }; #endif btag-1.1.3/src/EnglishTitleLocalizationHandler.cpp000644 001750 001750 00000005637 11727756360 023651 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "EnglishTitleLocalizationHandler.h" TitleLocalizationHandler::word_hint EnglishTitleLocalizationHandler::word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const { // Uppercase acronyms if (is_acronym(word)) return WORD_STYLE_UPPER; // Check for the "O'" prefix if (word.size() > 2 && word[0] == L'o' && word[1] == L'\'') { std::vector uppercase(word.size(), false); uppercase[0] = true; uppercase[2] = true; return uppercase; } // Check for hyphens if (word.size() > 2 && word[0] != L'-' && word[word.size() - 1] != L'-') { boost::optional > uppercase; for (size_t i = 1; i < word.size() - 1; ++i) { if (word[i] == L'-') { if (!uppercase) { uppercase = std::vector(word.size(), false); (*uppercase)[0] = true; } (*uppercase)[i + 1] = true; } } if (uppercase) return *uppercase; } // Uppercase the first letter of the first and last words if (index == 0 || index == count - 1) return WORD_STYLE_FIRST_UPPER; // Uppercase the first letter of a word after major punctuation static const wchar_t major_punctuation[] = { L'.', L':', L'?', L'!', L'\u2014', // Unicode EM DASH (—) L'-', L')', L'(', L'"', L'\'', L'\u201c', // Unicode LEFT DOUBLE QUOTATION MARK (“) L'\u201d', // Unicode RIGHT DOUBLE QUOTATION MARK (”) L'\u2018', // Unicode LEFT SINGLE QUOTATION MARK (‘) L'\u2019', // Unicode RIGHT SINGLE QUOTATION MARK (’) 0 }; if (after_punctuation != 0) { for (int i = 0; major_punctuation[i]; ++i) { if (after_punctuation == major_punctuation[i]) return WORD_STYLE_FIRST_UPPER; } } // Lowercase some specific words (context insensitive) static const wchar_t *lowercase_words[] = { L"a", L"an", L"the", // articles L"and", L"but", L"for", L"nor", L"or", L"so", L"yet", // coordinated conjunctions L"as", L"at", L"by", L"for", L"from", L"in", L"of", L"on", L"to", L"with", // short prepositions L"'n'", L"o'", // contractions L"is", L"vs.", L"etc.", NULL // some exceptions }; for (int i = 0; lowercase_words[i]; ++i) { if (word == lowercase_words[i]) return WORD_STYLE_LOWER; } return WORD_STYLE_FIRST_UPPER; } btag-1.1.3/src/InteractiveTerminal.h000644 001750 001750 00000002640 11727756360 021014 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef INTERACTIVE_TERMINAL_H #define INTERACTIVE_TERMINAL_H #include #include class InteractiveTerminal { public: template class Validator { public: virtual bool validate(const T &, boost::optional &error_message) const = 0; }; virtual bool ask_yes_no_question(const std::wstring &question, const boost::optional &default_answer = boost::optional()) = 0; virtual std::wstring ask_string_question(const std::wstring &question, const boost::optional &default_answer = boost::optional(), const Validator *validator = NULL) = 0; virtual int ask_number_question(const std::wstring &question, const boost::optional &default_answer = boost::optional(), const Validator *validator = NULL) = 0; virtual void display_info_message(const std::string &message) = 0; virtual void display_info_message(const std::wstring &message) = 0; virtual void display_warning_message(const std::string &message) = 0; virtual void display_warning_message(const std::wstring &message) = 0; }; #endif btag-1.1.3/src/ConfirmationHandler.h000644 001750 001750 00000001760 11727756360 020773 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef CONFIRMATION_HANDLER_H #define CONFIRMATION_HANDLER_H #include #include "BasicStringFilter.h" #include "InteractiveTerminal.h" class ConfirmationHandler { public: ConfirmationHandler(InteractiveTerminal &terminal, BasicStringFilter *input_filter = NULL, BasicStringFilter *output_filter = NULL); void reset(); void set_local_default(const std::wstring &local); bool ask(const std::wstring &question); bool complies() const { return m_complies; } const std::wstring &answer() const { return m_output; } private: bool do_ask(const std::wstring &question); InteractiveTerminal &m_terminal; BasicStringFilter *m_input_filter, *m_output_filter; std::wstring m_global_def, m_local_def, m_output; bool m_retry, m_complies; }; #endif btag-1.1.3/src/ConfirmationHandler.cpp000644 001750 001750 00000004110 11727756360 021316 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include "BasicStringFilter.h" #include "ConfirmationHandler.h" #include "InteractiveTerminal.h" ConfirmationHandler::ConfirmationHandler(InteractiveTerminal &terminal, BasicStringFilter *input_filter, BasicStringFilter *output_filter) : m_terminal(terminal), m_input_filter(input_filter), m_output_filter(output_filter) { } void ConfirmationHandler::set_local_default(const std::wstring &local) { // Set the filtered local default m_local_def = m_input_filter ? m_input_filter->filter(local) : local; } void ConfirmationHandler::reset() { // Erase the local default and the previous answer m_local_def.erase(); m_output.erase(); } bool ConfirmationHandler::do_ask(const std::wstring &question) { // Ask the question boost::optional default_answer; if (!m_global_def.empty()) default_answer = m_global_def; else if (!m_local_def.empty()) default_answer = m_local_def; std::wstring answer = m_terminal.ask_string_question(question, default_answer); // The answer complies if it's the same as the previously entered answer // (the user confirmed it) or if it's the same as the default answer m_complies = answer == default_answer || answer == m_output; m_output = answer; if (m_complies) return true; // Check if the output filter rejects it if (m_output_filter) { std::wstring filtered = m_output_filter->filter(answer); if (m_output_filter->requires_confirmation_as_output_filter() && answer != filtered) { m_local_def = filtered; return (m_complies = false); } } // Nope, so it's good return (m_complies = true); } bool ConfirmationHandler::ask(const std::wstring &question) { // If the answer complies, we got a global default if (do_ask(question)) { m_global_def = m_output; return true; } return false; } btag-1.1.3/src/SpanishTitleLocalizationHandler.h000644 001750 001750 00000000765 11727756360 023327 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef SPANISH_TITLE_LOCALIZATION_HANDLER_H #define SPANISH_TITLE_LOCALIZATION_HANDLER_H #include "TitleLocalizationHandler.h" class SpanishTitleLocalizationHandler : public TitleLocalizationHandler { public: word_hint word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const; }; #endif btag-1.1.3/src/EnglishTitleLocalizationHandler.h000644 001750 001750 00000000765 11727756360 023313 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef ENGLISH_TITLE_LOCALIZATION_HANDLER_H #define ENGLISH_TITLE_LOCALIZATION_HANDLER_H #include "TitleLocalizationHandler.h" class EnglishTitleLocalizationHandler : public TitleLocalizationHandler { public: word_hint word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const; }; #endif btag-1.1.3/src/RenamingFilter.cpp000644 001750 001750 00000001137 11727756360 020304 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include "RenamingFilter.h" std::wstring RenamingFilter::filter(const std::wstring &path_component) const { std::wstring new_component; new_component.reserve(path_component.size()); BOOST_FOREACH(wchar_t c, path_component) new_component += is_character_allowed(c) ? c : replacement_character(c); if (new_component.empty()) new_component += replacement_character(L'A'); return new_component; } btag-1.1.3/src/StandardConsole.h000644 001750 001750 00000002070 11727756360 020123 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef STANDARD_CONSOLE_H #define STANDARD_CONSOLE_H #include "InteractiveTerminal.h" class StandardConsole : public InteractiveTerminal { public: bool ask_yes_no_question(const std::wstring &question, const boost::optional &default_answer); std::wstring ask_string_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator = NULL); int ask_number_question(const std::wstring &question, const boost::optional &default_answer, const Validator *validator = NULL); void display_info_message(const std::string &message); void display_info_message(const std::wstring &message); void display_warning_message(const std::string &message); void display_warning_message(const std::wstring &message); }; #endif btag-1.1.3/src/SpanishTitleLocalizationHandler.cpp000644 001750 001750 00000005271 11727756360 023657 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "SpanishTitleLocalizationHandler.h" TitleLocalizationHandler::word_hint SpanishTitleLocalizationHandler::word_hint_for_word(const std::wstring &word, size_t index, size_t count, wchar_t after_punctuation) const { // Uppercase acronyms if (is_acronym(word)) return WORD_STYLE_UPPER; // Check for hyphens if (word.size() > 2 && word[0] != L'-' && word[word.size() - 1] != L'-') { boost::optional > uppercase; for (size_t i = 1; i < word.size() - 1; ++i) { if (word[i] == L'-') { if (!uppercase) { uppercase = std::vector(word.size(), false); (*uppercase)[0] = true; } (*uppercase)[i + 1] = true; } } if (uppercase) return *uppercase; } // Uppercase the first letter of the first and last words if (index == 0 || index == count - 1) return WORD_STYLE_FIRST_UPPER; // Uppercase the first letter of a word after major punctuation static const wchar_t major_punctuation[] = { L'.', L':', L'?', L'!', L'\u2014', // Unicode EM DASH (—) L'-', L')', L'(', L'"', L'\'', L'\u201c', // Unicode LEFT DOUBLE QUOTATION MARK (“) L'\u201d', // Unicode RIGHT DOUBLE QUOTATION MARK (”) L'\u2018', // Unicode LEFT SINGLE QUOTATION MARK (‘) L'\u2019', // Unicode RIGHT SINGLE QUOTATION MARK (’) 0 }; if (after_punctuation != 0) { for (int i = 0; major_punctuation[i]; ++i) { if (after_punctuation == major_punctuation[i]) return WORD_STYLE_FIRST_UPPER; } } // Lowercase some specific words (context insensitive) static const wchar_t *lowercase_words[] = { L"el", L"la", L"los", L"las", L"una", L"unas", L"un", L"unos", L"al", L"del", // articles L"a", L"con", L"de", L"en", L"para", L"por", L"sin", // common prepositions L"y", L"e", L"o", L"u", L"que", L"ni", // common conjunctions L"me", L"te", L"se", L"nos", L"os", // reflexive pronouns L"vs.", L"etc.", NULL // some exceptions }; for (int i = 0; lowercase_words[i]; ++i) { if (word == lowercase_words[i]) return WORD_STYLE_LOWER; } return WORD_STYLE_FIRST_UPPER; } btag-1.1.3/src/TitleCapitalizationFilter.cpp000644 001750 001750 00000012350 11727756360 022520 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include "TitleCapitalizationFilter.h" #include "TitleLocalizationHandler.h" bool TitleCapitalizationFilter::is_punctuation(wchar_t c) const { wchar_t *punctuation_list = m_handler->punctuation_list(); for (int i = 0; punctuation_list[i]; ++i) { if (c == punctuation_list[i]) return true; } return false; } std::wstring TitleCapitalizationFilter::filter(const std::wstring &input) const { std::wstring processed = BasicStringFilter::filter(input); boost::char_separator separator(L" "); boost::tokenizer, std::wstring::const_iterator, std::wstring > tokens(processed, separator); int num_words = 0; std::list > elements; BOOST_FOREACH(const std::wstring &token, tokens) { if (token.empty()) continue; // Look for all punctuation to the left of the token std::wstring punctuation_left; BOOST_FOREACH(wchar_t c, token) { if (!is_punctuation(c)) break; punctuation_left += c; } // Handle hanging punctuation if (punctuation_left.size() == token.size()) { boost::shared_ptr e(new element(ELEMENT_TYPE_PUNCTUATION_HANGING, token)); elements.push_back(e); continue; } // Add the punctuation to the left to the list if (!punctuation_left.empty()) { boost::shared_ptr e(new element(ELEMENT_TYPE_PUNCTUATION_LEFT, punctuation_left)); elements.push_back(e); } // Check if this could be an acronym std::wstring acronym(token.substr(punctuation_left.size())); bool is_acronym = m_handler->is_acronym(acronym); // Look for all punctuation to the right of the token std::wstring punctuation_right; if (!is_acronym) { BOOST_REVERSE_FOREACH(wchar_t c, token) { if (!is_punctuation(c)) break; punctuation_right = c + punctuation_right; } } // Find the word between the punctuation and add it to the list size_t word_size = token.size() - punctuation_left.size() - punctuation_right.size(); std::wstring word(token.substr(punctuation_left.size(), word_size)); elements.push_back(boost::shared_ptr(new element(ELEMENT_TYPE_WORD, word))); ++num_words; // Add the punctuation to the right to the list if (!punctuation_right.empty()) { boost::shared_ptr e(new element(ELEMENT_TYPE_PUNCTUATION_RIGHT, punctuation_right)); elements.push_back(e); } } std::wstring res; res.reserve(processed.size()); bool last_was_punctuation_left = false; int word_index = 0; boost::shared_ptr last_e; BOOST_FOREACH(boost::shared_ptr e, elements) { if (e->type == ELEMENT_TYPE_WORD) { // Lowercase the string BOOST_FOREACH(wchar_t &c, e->text) c = lowercase(c); // Get a hint wchar_t after_punctuation = last_e.get() && last_e->type != ELEMENT_TYPE_WORD ? last_e->text[last_e->text.size() - 1] : 0; TitleLocalizationHandler::word_hint hint = m_handler->word_hint_for_word(e->text, word_index++, num_words, after_punctuation); // Apply it switch (hint.style) { case TitleLocalizationHandler::WORD_STYLE_LOWER: break; case TitleLocalizationHandler::WORD_STYLE_UPPER: BOOST_FOREACH(wchar_t &c, e->text) c = uppercase(c); break; case TitleLocalizationHandler::WORD_STYLE_FIRST_UPPER: e->text[0] = uppercase(e->text[0]); break; case TitleLocalizationHandler::WORD_STYLE_CUSTOM: for (size_t i = 0; i < hint.uppercase->size(); ++i) { if ((*hint.uppercase)[i]) e->text[i] = uppercase(e->text[i]); } break; } } // Append to the result switch (e->type) { case ELEMENT_TYPE_WORD: if (!last_was_punctuation_left && !res.empty()) res += L' '; res += e->text; last_was_punctuation_left = false; break; case ELEMENT_TYPE_PUNCTUATION_RIGHT: res += e->text; last_was_punctuation_left = false; break; case ELEMENT_TYPE_PUNCTUATION_LEFT: if (!res.empty()) res += L' '; res += e->text; last_was_punctuation_left = true; break; case ELEMENT_TYPE_PUNCTUATION_HANGING: if (!res.empty()) res += L' '; res += e->text; last_was_punctuation_left = false; break; } last_e = e; } return res; } btag-1.1.3/src/BasicStringFilter.h000644 001750 001750 00000000662 11727756360 020423 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef BASIC_STRING_FILTER_H #define BASIC_STRING_FILTER_H #include class BasicStringFilter { public: virtual std::wstring filter(const std::wstring &input) const; virtual bool requires_confirmation_as_output_filter() const { return false; } }; #endif btag-1.1.3/src/UnixRenamingFilter.h000644 001750 001750 00000001017 11727756360 020612 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef UNIX_RENAMING_FILTER_H #define UNIX_RENAMING_FILTER_H #include "RenamingFilter.h" class UnixRenamingFilter : public RenamingFilter { private: bool is_character_allowed(wchar_t c) const { return c != L'/' && c != L'\0'; } wchar_t replacement_character(wchar_t c) const { return L'_'; } }; #endif btag-1.1.3/src/main.cpp000644 001750 001750 00000015037 11727756360 016326 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010-2011 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include #include #include "BasicStringFilter.h" #include "ConservativeRenamingFilter.h" #include "EnglishTitleLocalizationHandler.h" #include "InteractiveTagger.h" #include "RenamingFilter.h" #include "SimpleCapitalizationFilter.h" #include "SpanishTitleLocalizationHandler.h" #include "StandardConsole.h" #include "TitleCapitalizationFilter.h" #include "TitleLocalizationHandler.h" #include "UnixRenamingFilter.h" static void print_usage(std::ostream &out) { out << "\ Usage: \n\ btag [options] path1 [path2] [path3] ...\n\ btag --help\ " << std::endl; } static BasicStringFilter *select_string_filter(const std::string &filter) { if (filter == "basic") return new BasicStringFilter; else if (filter == "title") return new TitleCapitalizationFilter; else if (filter == "lower") return new SimpleCapitalizationFilter(SimpleCapitalizationFilter::CAPITALIZATION_MODE_ALL_LOWER); else if (filter == "upper") return new SimpleCapitalizationFilter(SimpleCapitalizationFilter::CAPITALIZATION_MODE_ALL_UPPER); else if (filter == "first-upper") return new SimpleCapitalizationFilter(SimpleCapitalizationFilter::CAPITALIZATION_MODE_FIRST_UPPER); else return NULL; } static TitleLocalizationHandler *select_title_localization_handler(const std::string &locale) { if (locale == "en") return new EnglishTitleLocalizationHandler; else if (locale == "es") return new SpanishTitleLocalizationHandler; else return NULL; } static RenamingFilter *select_renaming_filter(const std::string &filter) { if (filter == "unix") return new UnixRenamingFilter; else if (filter == "conservative") return new ConservativeRenamingFilter; else return NULL; } int main(int argc, char **argv) { // Set the global locale for case conversion purposes const char *locale_name = getenv("LANG"); if (locale_name) { std::ios_base::sync_with_stdio(false); std::locale locale(std::locale::classic(), locale_name, std::locale::ctype); std::locale::global(locale); std::wcout.imbue(locale); std::wcin.imbue(locale); } else { std::cerr << "WARNING: $LANG is not defined" << std::endl; } struct option long_options[] = { {"dir-rename-format", required_argument, NULL, 'd'}, {"dry-run", no_argument, NULL, 'D'}, {"input-filter", required_argument, NULL, 'i'}, {"file-rename-format", required_argument, NULL, 'r'}, {"filter", required_argument, NULL, 'f'}, {"help", no_argument, NULL, 'h'}, {"output-filter", required_argument, NULL, 'o'}, {"renaming-filter", required_argument, NULL, 'n'}, {"title-locale", required_argument, NULL, 't'}, {NULL, 0, NULL, 0} }; // Create the interactive tagger InteractiveTagger itag; boost::scoped_ptr input_filter, output_filter; boost::scoped_ptr title_localization_handler; boost::scoped_ptr renaming_filter; // Parse the command line options int opt; while ((opt = getopt_long(argc, argv, "Dd:i:f:o:hn:r:t:", long_options, NULL)) != -1) { switch (opt) { case 'D': itag.set_dry_run(); break; case 'd': itag.set_dir_rename_format(optarg); break; case 'f': input_filter.reset(select_string_filter(optarg)); output_filter.reset(select_string_filter(optarg)); if (!input_filter.get() || !output_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'i': input_filter.reset(select_string_filter(optarg)); if (!input_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'h': print_usage(std::cout); return EXIT_SUCCESS; case 'o': output_filter.reset(select_string_filter(optarg)); if (!output_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'n': renaming_filter.reset(select_renaming_filter(optarg)); if (!renaming_filter.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; case 'r': itag.set_file_rename_format(optarg); break; case 't': title_localization_handler.reset(select_title_localization_handler(optarg)); if (!title_localization_handler.get()) { print_usage(std::cerr); return EXIT_FAILURE; } break; default: print_usage(std::cerr); return EXIT_FAILURE; } } // We need at least one path if (optind >= argc) { print_usage(std::cerr); return EXIT_FAILURE; } // Add the title localization handler if (!title_localization_handler.get()) title_localization_handler.reset(new EnglishTitleLocalizationHandler); TitleCapitalizationFilter *filter = dynamic_cast(input_filter.get()); if (filter) filter->set_localization_handler(title_localization_handler.get()); filter = dynamic_cast(output_filter.get()); if (filter) filter->set_localization_handler(title_localization_handler.get()); // Add the filters if (!input_filter.get()) input_filter.reset(new BasicStringFilter); itag.set_input_filter(input_filter.get()); itag.set_output_filter(output_filter.get()); // Add the renaming filter if (!renaming_filter.get()) renaming_filter.reset(new UnixRenamingFilter); itag.set_renaming_filter(renaming_filter.get()); // Create the interactive terminal StandardConsole console; itag.set_terminal(&console); // Perform the interactive tagging itag.tag(argc - optind, (const char **)&argv[optind]); return EXIT_SUCCESS; } btag-1.1.3/src/TitleCapitalizationFilter.h000644 001750 001750 00000002100 11727756360 022155 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2010 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef TITLE_CAPITALIZATION_FILTER_H #define TITLE_CAPITALIZATION_FILTER_H #include "CapitalizationFilter.h" #include "TitleLocalizationHandler.h" class TitleCapitalizationFilter : public CapitalizationFilter { public: TitleCapitalizationFilter() : m_handler(NULL) {} void set_localization_handler(TitleLocalizationHandler *handler) { m_handler = handler; } std::wstring filter(const std::wstring &input) const; private: enum element_type { ELEMENT_TYPE_WORD, ELEMENT_TYPE_PUNCTUATION_LEFT, ELEMENT_TYPE_PUNCTUATION_RIGHT, ELEMENT_TYPE_PUNCTUATION_HANGING }; struct element { element_type type; std::wstring text; element(element_type t, const std::wstring &x) : type(t), text(x) {} }; bool is_punctuation(wchar_t c) const; TitleLocalizationHandler *m_handler; }; #endif btag-1.1.3/README000644 001750 001750 00000003627 11727756360 014771 0ustar00fernandofernando000000 000000 Introduction ============ btag is a command line based multimedia tagger. It retains information about the filesystem structure so that users can tag a bunch of albums with ease. In some ways, btag can be compared to graphical taggers like EasyTAG[1] as both allow you to tag a lot of songs in batches. The following features are implemented: * Tagging of a vast number of formats supported by TagLib[2] * Tag normalization and filtering * Locale-aware title capitalization * Renaming of files and directories References: [1]: http://easytag.sourceforge.net/ [2]: http://developer.kde.org/~wheeler/taglib.html Configuration ============= All configuration is done through command line switches. Refer to btag(1) for more information on the supported switches. Future goals ============ btag is already usable as of now. It wouldn't hurt to bring more features from EasyTAG or to provide more control over specific tags. Some specific goals (nothing set in stone, though): * Load the configuration from a configuration file (1.2.0) * Deprecate some command line switches (1.2.0, removal in 1.3.0) * Handle some other tags better (such as the comment tag, 1.2.0) * Music Brainz integration (1.3.0) * Read the information in CUE files (1.3.0) Contributing ============ Any help is appreciated, please feel free to contribute. I'll review your patches and commit them if they are in accordance with this project's goals, and you'll be credited for the changes (I may ask you to change a few things things before your patches are accepted, though). For the time being, send your patches to: Fernando Tarlá Cardoso Lemos Please format your patches with "git format-patch" and then attach them to an e-mail instead of sending the patches inlined in the message. This makes it easier for me to apply them. Authors ======= btag was created by: Fernando Tarlá Cardoso Lemos btag-1.1.3/tests/english_titles.cpp000644 001750 001750 00000000550 11727756360 020764 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "EnglishTitleLocalizationHandler.h" #include "titles_base.h" int main(int argc, char **argv) { EnglishTitleLocalizationHandler handler; return run_title_capitalization_tests(handler, argc, argv); } btag-1.1.3/tests/titles_base.h000644 001750 001750 00000000513 11727756360 017711 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #ifndef TITLES_BASE_H #define TITLES_BASE_H #include "TitleCapitalizationFilter.h" int run_title_capitalization_tests(TitleLocalizationHandler &handler, int argc, char **argv); #endif btag-1.1.3/tests/spanish_titles.cpp000644 001750 001750 00000000550 11727756360 021000 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include "SpanishTitleLocalizationHandler.h" #include "titles_base.h" int main(int argc, char **argv) { SpanishTitleLocalizationHandler handler; return run_title_capitalization_tests(handler, argc, argv); } btag-1.1.3/tests/titles_base.cpp000644 001750 001750 00000005426 11727756360 020254 0ustar00fernandofernando000000 000000 /* * This file is part of btag. * * © 2011-2012 Fernando Tarlá Cardoso Lemos * * Refer to the LICENSE file for licensing information. * */ #include #include #include #include #include "titles_base.h" int run_title_capitalization_tests(TitleLocalizationHandler &handler, int argc, char **argv) { // Find the path to the test data if (argc != 2) { std::cerr << "You need to supply the path to test data file in the command line" << std::endl; return EXIT_FAILURE; } // Open the test data file std::wifstream f(argv[1]); if (!f.is_open()) { std::cerr << "Failed to open the test data file" << std::endl; return EXIT_FAILURE; } // Force an UTF-8 locale std::ios_base::sync_with_stdio(false); std::locale locale(std::locale::classic(), "C.UTF-8", std::locale::ctype); f.imbue(locale); std::wcerr.imbue(locale); // Set up the capitalization filter TitleCapitalizationFilter filter; filter.set_localization_handler(&handler); int errors = 0, correct_lines = 0, lineno = 0; std::wstring line; for (;;) { // Get a line std::getline(f, line); if (f.eof()) break; else if (f.fail()) { std::wcerr << L"Error reading line from file" << std::endl; return EXIT_FAILURE; } // Check if we can skip it std::wstring::size_type first = line.find_first_not_of(L" \t\n\r"); if (first == std::wstring::npos || line[first] == L'#') { ++lineno; continue; } // Trim it std::wstring::size_type last = line.find_last_not_of(L" \t\n\r"); std::wstring correctStr(line.substr(first, last - first + 1)); // Get lowercase and uppercase versions of the string std::wstring lowerStr(boost::algorithm::to_lower_copy(correctStr)); std::wstring upperStr(boost::algorithm::to_upper_copy(correctStr)); // Apply the localization handler std::wstring lowerFiltered(filter.filter(lowerStr)); std::wstring upperFiltered(filter.filter(upperStr)); // Make sure they match bool has_error = false; if (lowerFiltered != correctStr) { std::wcerr << lineno << L": Incorrect conversion from lowercase" << std::endl; has_error = true; ++errors; } if (upperFiltered != correctStr) { std::wcerr << lineno << L": Incorrect conversion from uppercase" << std::endl; has_error = true; ++errors; } // Increment the counters ++lineno; if (!has_error) ++correct_lines; } return correct_lines > 10 && errors == 0 ? EXIT_SUCCESS : EXIT_FAILURE; } btag-1.1.3/tests/CMakeLists.txt000644 001750 001750 00000003400 11727756360 020000 0ustar00fernandofernando000000 000000 include_directories(${Boost_INCLUDE_DIRS} ${PROJECT_SOURCE_DIR}/src) add_executable( english_titles_test english_titles.cpp titles_base.cpp titles_base.h ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.cpp ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.h ${PROJECT_SOURCE_DIR}/src/CapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/EnglishTitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/EnglishTitleLocalizationHandler.h ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.cpp ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.h ) target_link_libraries(english_titles_test ${Boost_LIBRARIES}) add_test(english_titles_test ${PROJECT_BINARY_DIR}/tests/english_titles_test ${PROJECT_SOURCE_DIR}/tests/data/english_titles.txt) add_executable( spanish_titles_test spanish_titles.cpp titles_base.cpp titles_base.h ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.cpp ${PROJECT_SOURCE_DIR}/src/BasicStringFilter.h ${PROJECT_SOURCE_DIR}/src/CapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/SpanishTitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/SpanishTitleLocalizationHandler.h ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.cpp ${PROJECT_SOURCE_DIR}/src/TitleCapitalizationFilter.h ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.cpp ${PROJECT_SOURCE_DIR}/src/TitleLocalizationHandler.h ) target_link_libraries(spanish_titles_test ${Boost_LIBRARIES}) add_test(spanish_titles_test ${PROJECT_BINARY_DIR}/tests/spanish_titles_test ${PROJECT_SOURCE_DIR}/tests/data/spanish_titles.txt) add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure) btag-1.1.3/tests/data/english_titles.txt000644 001750 001750 00000002365 11727756360 021740 0ustar00fernandofernando000000 000000 # # This was initialized with a list of best-sellers found on # Wikipedia (though only those that we should handle well). # Feel free to extend it with whatever content. # # Blank lines and lines starting with a hash are ignored, but # our parser is very basic, so don't get creative. # # Keep this file in UTF-8. Not sure if having the BOM is a # good thing (avoid it if possible). # A Tale of Two Cities The Lord of the Rings The Hobbit Dream of the Red Chamber And Then There Were None The Lion, the Witch and the Wardrobe Think and Grow Rich The Catcher in the Rye The Alchemist Steps to Christ Heidi's Years of Wandering and Learning The Common Sense Book of Baby and Child Care Anne of Green Gables The Name of the Rose The Hite Report Shere Hite The Tale of Peter Rabbit The Ginger Man Harry Potter and the Deathly Hallows Jonathan Livingston Seagull A Message to Garcia Angels and Demons How the Steel Was Tempered War and Peace You Can Heal Your Life Kane and Abel Sophie's World The Very Hungry Caterpillar The Girl with the Dragon Tattoo The Thorn Birds The Purpose Driven Life One Hundred Years of Solitude The Diary of a Young Girl Valley of the Dolls In His Steps: What Would Jesus Do? The Revolt of Mamie Stover Gone with the Wind To Kill a Mockingbird btag-1.1.3/tests/data/spanish_titles.txt000644 001750 001750 00000003603 11727756360 021750 0ustar00fernandofernando000000 000000 # # This was initialized with a list of best-sellers found on # Wikipedia (though only those that we should handle well). # Feel free to extend it with whatever content. # # Blank lines and lines starting with a hash are ignored, but # our parser is very basic, so don't get creative. # # Keep this file in UTF-8. Not sure if having the BOM is a # good thing (avoid it if possible). # Historia de Dos Ciudades El Hobbit Sueño en el Pabellón Rojo Triple Representatividad Diez Negritos El León, la Bruja y el Armario Ella El Principito El Guardián Entre el Centeno El Alquimista Heidi El Libro del Sentido del Común del Cuidado de Bebés y Niños El Nombre de la Rosa Ángeles y Demonios Así se Templó el Acero Guerra y Paz Usted Puede Sanar Su Vida Matar a un Ruiseñor El Valle de las Muñecas Lo que el Viento se Llevó Cien Años de Soledad Una Vida con Propósito El Pájaro Espino Piense y Hágase Rico Los Hombres que No Amaban a las Mujeres La Oruguita Glotona ¿Quién se Ha Llevado Mi Queso? El Viento en los Sauces 1984 La Prostituta Feliz Tiburón Siempre te Querré La Habitación de las Mujeres Qué Esperar Cuando se Está Esperando Donde Viven los Monstruos El Secreto Miedo a Volar Adivina Cuánto te Quiero Los Siete Hábitos de las Personas Altamente Efectivas Cómo Ganar Amigos e Influir Sobre las Personas El Perfume: Historia de un Asesino El Hombre que Susurraba al Oído de los Caballos La Sombra del Viento La Cabaña Guía del Autoestopista Galáctico Martes con Mi Viejo Profesor Donde el Corazón te Lleve Rebeldes Charlie y la Fábrica de Chocolate La Peste Indigno de Ser Humano El Mono Desnudo Todo se Desmorona El Profeta El Exorcista El Grúfalo Trampa-22 La Isla de las Tormentas Historia del Tiempo El Gato con Sombrero Desde Mi Cielo Cisnes Salvajes La Noche Cometas en el Cielo La Mujer Total Historia del Futuro, la Sociedad del Conocimiento ¿De Qué Color Es Su Paracaídas? btag-1.1.3/INSTALL000644 001750 001750 00000001115 11727756360 015130 0ustar00fernandofernando000000 000000 Dependencies ============ Those are the dependencies needed to compile btag: * TagLib (libtag1-dev in Debian) * Boost headers >= 1.42.0 (libboost-dev in Debian) * Boost filesystem >= 1.42.0 (libboost-filesystem-dev in Debian) * pkg-config * CMake If you want to compile the manual pages, you'll need something compatible with nroff (groff-base in Debian works great). Installation instructions ========================= btag uses CMake: $ mkdir /path/to/build_dir $ cd /path/to/build_dir $ cmake -DCMAKE_INSTALL_PREFIX=/path/to/install_dir /path/to/source_dir $ make $ make install btag-1.1.3/ChangeLog000644 001750 001750 00000000566 11727756360 015662 0ustar00fernandofernando000000 000000 1.1.3 ===== * Clang++ support. * Bugfix release. 1.1.2 ===== * Tests for capitalization filters. * Bugfix release. 1.1.1 ===== * Bugfix release. 1.1.0 ===== * New dry run mode. * Better confirmation algorithm. * Use the new boost::filesystem API. * Fix building with ld --no-add-needed. 1.0.1 ===== * Bugfix release. * New license. 1.0.0 ===== * Initial release. btag-1.1.3/CMakeLists.txt000644 001750 001750 00000004205 11727756360 016642 0ustar00fernandofernando000000 000000 cmake_minimum_required(VERSION 2.8 FATAL_ERROR) project(btag) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER MATCHES clang\\+\\+) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Werror") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS} -ggdb -O0") endif(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER MATCHES clang\\+\\+) find_package(Boost 1.46.0 COMPONENTS filesystem system REQUIRED) find_package(PkgConfig) pkg_check_modules(TAGLIB REQUIRED taglib) set(BTAG_SOURCES src/BasicStringFilter.cpp src/ConfirmationHandler.cpp src/EnglishTitleLocalizationHandler.cpp src/InteractiveTagger.cpp src/main.cpp src/RenamingFilter.cpp src/SimpleCapitalizationFilter.cpp src/SpanishTitleLocalizationHandler.cpp src/StandardConsole.cpp src/TitleCapitalizationFilter.cpp src/TitleLocalizationHandler.cpp) set(BTAG_HEADERS src/BasicStringFilter.h src/CapitalizationFilter.h src/ConfirmationHandler.h src/ConservativeRenamingFilter.h src/EnglishTitleLocalizationHandler.h src/InteractiveTagger.h src/RenamingFilter.h src/SimpleCapitalizationFilter.h src/SpanishTitleLocalizationHandler.h src/StandardConsole.h src/TitleCapitalizationFilter.h src/TitleLocalizationHandler.h src/UnixRenamingFilter.h src/wide_string_cast.h) include_directories(${Boost_INCLUDE_DIRS} ${TAGLIB_INCLUDE_DIRS}) add_executable(btag ${BTAG_SOURCES} ${BTAG_HEADERS}) target_link_libraries(btag ${Boost_LIBRARIES} ${TAGLIB_LIBRARIES}) install(TARGETS btag DESTINATION bin) set(CPACK_PACKAGE_VERSION_MAJOR "1") set(CPACK_PACKAGE_VERSION_MINOR "1") set(CPACK_PACKAGE_VERSION_PATCH "3") set(CPACK_SOURCE_GENERATOR "TGZ") set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${CPACK_PACKAGE_VERSION_MAJOR}.${CPACK_PACKAGE_VERSION_MINOR}.${CPACK_PACKAGE_VERSION_PATCH}") set(CPACK_SOURCE_IGNORE_FILES "/.git;/.gitignore;${CPACK_SOURCE_IGNORE_FILES}") include(CPack) add_custom_target(dist COMMAND ${CMAKE_MAKE_PROGRAM} package_source) option(ENABLE_TESTS "Whether a \"make check\" target should be made available" OFF) if(ENABLE_TESTS) enable_testing() add_subdirectory(tests) endif(ENABLE_TESTS)