diff options
Diffstat (limited to 'src/browser.cpp')
-rw-r--r-- | src/browser.cpp | 589 |
1 files changed, 324 insertions, 265 deletions
diff --git a/src/browser.cpp b/src/browser.cpp index 79074610..62898489 100644 --- a/src/browser.cpp +++ b/src/browser.cpp @@ -18,10 +18,12 @@ * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. * ***************************************************************************/ +#include <algorithm> +#include <boost/algorithm/string/predicate.hpp> #include <boost/bind.hpp> #include <boost/filesystem.hpp> #include <boost/locale/conversion.hpp> -#include <algorithm> +#include <time.h> #include "browser.h" #include "charset.h" @@ -50,17 +52,27 @@ namespace fs = boost::filesystem; Browser *myBrowser; -namespace {// +namespace { + +std::set<std::string> lm_supported_extensions; -std::set<std::string> SupportedExtensions; -bool hasSupportedExtension(const std::string &file); +std::string realPath(bool local_browser, std::string path); +bool isStringParentDirectory(const std::string &directory); +bool isItemParentDirectory(const MPD::Item &item); +bool isRootDirectory(const std::string &directory); +bool isHidden(const fs::directory_iterator &entry); +bool hasSupportedExtension(const fs::directory_entry &entry); +MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags); +void getLocalDirectory(MPD::ItemList &items, const std::string &directory); +void getLocalDirectoryRecursively(MPD::SongList &songs, const std::string &directory); +void clearDirectory(const std::string &directory); -std::string ItemToString(const MPD::Item &item); -bool BrowserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter); +std::string itemToString(const MPD::Item &item); +bool browserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter); } -Browser::Browser() : itsBrowseLocally(0), itsScrollBeginning(0), itsBrowsedDir("/") +Browser::Browser() : m_local_browser(false), m_scroll_beginning(0), m_current_directory("/") { w = NC::Menu<MPD::Item>(0, MainStartY, COLS, MainHeight, Config.browser_display_mode == DisplayMode::Columns && Config.titles_visibility ? Display::Columns(COLS) : "", Config.main_color, NC::Border::None); w.setHighlightColor(Config.main_highlight_color); @@ -97,7 +109,7 @@ void Browser::switchTo() SwitchTo::execute(this); if (w.empty()) - GetDirectory(itsBrowsedDir); + getDirectory(m_current_directory); else markSongsInPlaylist(proxySongList()); @@ -107,7 +119,7 @@ void Browser::switchTo() std::wstring Browser::title() { std::wstring result = L"Browse: "; - result += Scroller(ToWString(itsBrowsedDir), itsScrollBeginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length())); + result += Scroller(ToWString(m_current_directory), m_scroll_beginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length())); return result; } @@ -117,31 +129,29 @@ void Browser::enterPressed() return; const MPD::Item &item = w.current().value(); - switch (item.type) + switch (item.type()) { case MPD::Item::Type::Directory: { - if (isParentDirectory(item)) - GetDirectory(getParentDirectory(itsBrowsedDir), itsBrowsedDir); - else - GetDirectory(item.name, itsBrowsedDir); + getDirectory(item.directory().path()); drawHeader(); break; } case MPD::Item::Type::Song: { - addSongToPlaylist(item.song, true, -1); + addSongToPlaylist(item.song(), true, -1); break; } case MPD::Item::Type::Playlist: { MPD::SongList list( - std::make_move_iterator(Mpd.GetPlaylistContentNoInfo(item.name)), + std::make_move_iterator(Mpd.GetPlaylistContentNoInfo(item.playlist().path())), std::make_move_iterator(MPD::SongIterator()) ); + // TODO: ask on failure if we want to continue bool success = addSongsToPlaylist(list.begin(), list.end(), true, -1); Statusbar::printf("Playlist \"%1%\" loaded%2%", - item.name, withErrors(success) + item.playlist().path(), withErrors(success) ); } } @@ -152,60 +162,47 @@ void Browser::spacePressed() if (w.empty()) return; - size_t i = itsBrowsedDir != "/" ? 1 : 0; + size_t i = inRootDirectory() ? 0 : 1; if (Config.space_selects && w.choice() >= i) { i = w.choice(); - w.at(i).setSelected(!w.at(i).isSelected()); + w[i].setSelected(!w[i].isSelected()); w.scroll(NC::Scroll::Down); return; } - + const MPD::Item &item = w.current().value(); - + // ignore parent directory if (isParentDirectory(item)) return; - - switch (item.type) + + switch (item.type()) { case MPD::Item::Type::Directory: { - bool success; -# ifndef WIN32 - if (isLocal()) + bool success = true; + if (m_local_browser) { - MPD::SongList list; - MPD::ItemList items; - Statusbar::printf("Scanning directory \"%1%\"...", item.name); - myBrowser->GetLocalDirectory(items, item.name, 1); - list.reserve(items.size()); - for (MPD::ItemList::const_iterator it = items.begin(); it != items.end(); ++it) - list.push_back(it->song); - success = addSongsToPlaylist(list.begin(), list.end(), false, -1); + MPD::SongList songs; + getLocalDirectoryRecursively(songs, item.directory().path()); + success = addSongsToPlaylist(songs.begin(), songs.end(), false, -1); } else -# endif // !WIN32 - { - Mpd.Add(item.name); - success = true; - } + Mpd.Add(item.directory().path()); Statusbar::printf("Directory \"%1%\" added%2%", - item.name, withErrors(success) + item.directory().path(), withErrors(success) ); break; } case MPD::Item::Type::Song: - { - addSongToPlaylist(item.song, false); + addSongToPlaylist(item.song(), false); break; - } case MPD::Item::Type::Playlist: - { - Mpd.LoadPlaylist(item.name); - Statusbar::printf("Playlist \"%1%\" loaded", item.name); + Mpd.LoadPlaylist(item.playlist().path()); + Statusbar::printf("Playlist \"%1%\" loaded", item.playlist().path()); break; - } } + w.scroll(NC::Scroll::Down); } @@ -216,12 +213,12 @@ void Browser::mouseButtonPressed(MEVENT me) if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)) { w.Goto(me.y); - switch (w.current().value().type) + switch (w.current().value().type()) { case MPD::Item::Type::Directory: if (me.bstate & BUTTON1_PRESSED) { - GetDirectory(w.current().value().name); + getDirectory(w.current().value().directory().path()); drawHeader(); } else @@ -273,7 +270,7 @@ void Browser::applyFilter(const std::string &filter) try { w.showAll(); - auto fun = boost::bind(BrowserEntryMatcher, _1, _2, true); + auto fun = boost::bind(browserEntryMatcher, _1, _2, true); auto rx = RegexFilter<MPD::Item>( boost::regex(filter, Config.regex_type), fun); w.filter(w.begin(), w.end(), rx); @@ -297,7 +294,7 @@ bool Browser::search(const std::string &constraint) } try { - auto fun = boost::bind(BrowserEntryMatcher, _1, _2, false); + auto fun = boost::bind(browserEntryMatcher, _1, _2, false); auto rx = RegexFilter<MPD::Item>( boost::regex(constraint, Config.regex_type), fun); return w.search(w.begin(), w.end(), rx); @@ -324,8 +321,8 @@ ProxySongList Browser::proxySongList() { return ProxySongList(w, [](NC::Menu<MPD::Item>::Item &item) -> MPD::Song * { MPD::Song *ptr = 0; - if (item.value().type == MPD::Item::Type::Song) - ptr = &item.value().song; + if (item.value().type() == MPD::Item::Type::Song) + ptr = const_cast<MPD::Song *>(&item.value().song()); return ptr; }); } @@ -337,201 +334,167 @@ bool Browser::allowsSelection() void Browser::reverseSelection() { - reverseSelectionHelper(w.begin()+(itsBrowsedDir == "/" ? 0 : 1), w.end()); + size_t offset = inRootDirectory() ? 0 : 1; + reverseSelectionHelper(w.begin()+offset, w.end()); } MPD::SongList Browser::getSelectedSongs() { - MPD::SongList result; - auto item_handler = [this, &result](const MPD::Item &item) { - if (item.type == MPD::Item::Type::Directory) - { -# ifndef WIN32 - if (isLocal()) - { - MPD::ItemList list; - GetLocalDirectory(list, item.name, true); - for (auto it = list.begin(); it != list.end(); ++it) - result.push_back(it->song); - } - else -# endif // !WIN32 - { - Mpd.GetDirectoryRecursive(item.name, vectorMoveInserter(result)); - } - } - else if (item.type == MPD::Item::Type::Song) - result.push_back(item.song); - else if (item.type == MPD::Item::Type::Playlist) + MPD::SongList songs; + auto item_handler = [this, &songs](const MPD::Item &item) { + switch (item.type()) { - std::copy( - std::make_move_iterator(Mpd.GetPlaylistContent(item.name)), - std::make_move_iterator(MPD::SongIterator()), - std::back_inserter(result) - ); + case MPD::Item::Type::Directory: + if (m_local_browser) + { + MPD::ItemList items; + getLocalDirectoryRecursively(songs, item.directory().path()); + } + else + { + MPD::ItemIterator it = Mpd.GetDirectoryRecursive(item.directory().path()), end; + for (; it != end; ++it) + if (it->type() == MPD::Item::Type::Song) + songs.push_back(std::move(it->song())); + } + break; + case MPD::Item::Type::Song: + songs.push_back(item.song()); + break; + case MPD::Item::Type::Playlist: + std::copy( + std::make_move_iterator(Mpd.GetPlaylistContent(item.playlist().path())), + std::make_move_iterator(MPD::SongIterator()), + std::back_inserter(songs) + ); + break; } }; - for (auto it = w.begin(); it != w.end(); ++it) - if (it->isSelected()) - item_handler(it->value()); + for (const auto &item : w) + if (item.isSelected()) + item_handler(item.value()); // if no item is selected, add current one - if (result.empty() && !w.empty()) + if (songs.empty() && !w.empty()) item_handler(w.current().value()); - return result; + return songs; } -void Browser::fetchSupportedExtensions() +/***********************************************************************/ + +bool Browser::inRootDirectory() +{ + return isRootDirectory(m_current_directory); +} + +bool Browser::isParentDirectory(const MPD::Item &item) +{ + return isItemParentDirectory(item); +} + +const std::string& Browser::currentDirectory() { - SupportedExtensions.clear(); - Mpd.GetSupportedExtensions(SupportedExtensions); + return m_current_directory; } -void Browser::LocateSong(const MPD::Song &s) +void Browser::locateSong(const MPD::Song &s) { if (s.getDirectory().empty()) - return; + throw std::runtime_error("Song's directory is empty"); - itsBrowseLocally = !s.isFromDatabase(); + m_local_browser = !s.isFromDatabase(); if (myScreen != this) switchTo(); - if (itsBrowsedDir != s.getDirectory()) - GetDirectory(s.getDirectory()); - for (size_t i = 0; i < w.size(); ++i) + // change to relevant directory + if (m_current_directory != s.getDirectory()) { - if (w[i].value().type == MPD::Item::Type::Song && s == w[i].value().song) - { - w.highlight(i); - break; - } + getDirectory(s.getDirectory()); + drawHeader(); } - drawHeader(); + + // highlight the item + auto begin = w.beginV(), end = w.endV(); + auto it = std::find(begin, end, MPD::Item(s)); + if (it != end) + w.highlight(it-begin); } -void Browser::GetDirectory(std::string dir, std::string subdir) +void Browser::getDirectory(std::string directory) { - if (dir.empty()) - dir = "/"; - - int highlightme = -1; - itsScrollBeginning = 0; - if (itsBrowsedDir != dir) - w.reset(); - itsBrowsedDir = dir; - + m_scroll_beginning = 0; w.clear(); - - if (dir != "/") + + // reset the position if we change directories + if (m_current_directory != directory) + w.reset(); + + // check if it's a parent directory + if (isStringParentDirectory(directory)) { - MPD::Item parent; - parent.name = ".."; - parent.type = MPD::Item::Type::Directory; - w.addItem(parent); + directory.resize(directory.length()-3); + directory = getParentDirectory(directory); } - - MPD::ItemList list; -# ifndef WIN32 - if (isLocal()) - GetLocalDirectory(list, itsBrowsedDir, false); + // when we go down to root, it can be empty + if (directory.empty()) + directory = "/"; + + MPD::ItemList items; + if (m_local_browser) + getLocalDirectory(items, directory); else - Mpd.GetDirectory(dir, vectorMoveInserter(list)); -# else - list = Mpd.GetDirectory(dir); -# endif // !WIN32 - if (Config.browser_sort_mode != SortMode::NoOp && !isLocal()) // local directory is already sorted - std::sort(list.begin(), list.end(), + { + std::copy( + std::make_move_iterator(Mpd.GetDirectory(directory)), + std::make_move_iterator(MPD::ItemIterator()), + std::back_inserter(items) + ); + } + + // sort items + if (Config.browser_sort_mode != SortMode::NoOp) + { + std::sort(items.begin(), items.end(), LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode) ); - - for (MPD::ItemList::iterator it = list.begin(); it != list.end(); ++it) + } + + // if the requested directory is not root, add parent directory + if (!isRootDirectory(directory)) + { + // make it so that display function doesn't have to handle special cases + w.addItem(MPD::Directory(directory + "/..")); + } + + for (const auto &item : items) { - switch (it->type) + switch (item.type()) { case MPD::Item::Type::Playlist: { - w.addItem(*it); + w.addItem(std::move(item)); break; } case MPD::Item::Type::Directory: { - if (it->name == subdir) - highlightme = w.size(); - w.addItem(*it); + bool is_current = item.directory().path() == m_current_directory; + w.addItem(std::move(item)); + if (is_current) + w.highlight(w.size()-1); break; } case MPD::Item::Type::Song: { - w.addItem(*it, myPlaylist->checkForSong(it->song)); + bool is_bold = myPlaylist->checkForSong(item.song()); + w.addItem(std::move(item), is_bold); break; } } } - if (highlightme >= 0) - w.highlight(highlightme); -} - -#ifndef WIN32 -void Browser::GetLocalDirectory(MPD::ItemList &v, const std::string &directory, bool recursively) const -{ - size_t start_size = v.size(); - fs::path dir(directory); - std::for_each(fs::directory_iterator(dir), fs::directory_iterator(), [&](fs::directory_entry &e) { - if (!Config.local_browser_show_hidden_files && e.path().filename().native()[0] == '.') - return; - MPD::Item item; - if (fs::is_directory(e)) - { - if (recursively) - { - GetLocalDirectory(v, e.path().native(), true); - start_size = v.size(); - } - else - { - item.type = MPD::Item::Type::Directory; - item.name = e.path().native(); - v.push_back(item); - } - } - else if (hasSupportedExtension(e.path().native())) - { - item.type = MPD::Item::Type::Song; - mpd_pair file_pair = { "file", e.path().native().c_str() }; - item.song = mpd_song_begin(&file_pair); - // FIXME no tag reading for now - /* -# ifdef HAVE_TAGLIB_H - if (!recursively) - { - s->setMTime(fs::last_write_time(e.path())); - Tags::read(*s); - } -# endif // HAVE_TAGLIB_H - */ - v.push_back(item); - } - }); - - if (Config.browser_sort_mode != SortMode::NoOp) - std::sort(v.begin()+start_size, v.end(), - LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the, Config.browser_sort_mode) - ); + m_current_directory = directory; } -void Browser::ClearDirectory(const std::string &path) const -{ - fs::path dir(path); - std::for_each(fs::directory_iterator(dir), fs::directory_iterator(), [&](fs::directory_entry &e) { - if (!fs::is_symlink(e) && fs::is_directory(e)) - ClearDirectory(e.path().native()); - const char msg[] = "Deleting \"%1%\"..."; - Statusbar::printf(msg, wideShorten(e.path().native(), COLS-const_strlen(msg))); - fs::remove(e.path()); - }); -} - -void Browser::ChangeBrowseMode() +void Browser::changeBrowseMode() { if (Mpd.GetHostname()[0] != '/') { @@ -539,120 +502,216 @@ void Browser::ChangeBrowseMode() return; } - itsBrowseLocally = !itsBrowseLocally; + m_local_browser = !m_local_browser; Statusbar::printf("Browse mode: %1%", - itsBrowseLocally ? "local filesystem" : "MPD database" + m_local_browser ? "local filesystem" : "MPD database" ); - if (itsBrowseLocally) + if (m_local_browser) { - itsBrowsedDir = "~"; - expand_home(itsBrowsedDir); - if (*itsBrowsedDir.rbegin() == '/') - itsBrowsedDir.resize(itsBrowsedDir.length()-1); + m_current_directory = "~"; + expand_home(m_current_directory); } else - itsBrowsedDir = "/"; + m_current_directory = "/"; w.reset(); - GetDirectory(itsBrowsedDir); + getDirectory(m_current_directory); drawHeader(); } -bool Browser::deleteItem(const MPD::Item &item, std::string &errmsg) +void Browser::remove(const MPD::Item &item) { if (!Config.allow_for_physical_item_deletion) - FatalError("Browser::deleteItem invoked with allow_for_physical_item_deletion = false"); + throw std::runtime_error("physical deletion is forbidden"); if (isParentDirectory((item))) - FatalError("Parent directory passed to Browser::deleteItem"); - - // playlist created by mpd - if (!isLocal() && item.type == MPD::Item::Type::Playlist && CurrentDir() == "/") + throw std::runtime_error("deletion of parent directory is forbidden"); + + std::string path; + switch (item.type()) { - try - { - Mpd.DeletePlaylist(item.name); - return true; - } - catch (MPD::ServerError &e) - { - // if there is no such mpd playlist, we assume it's users's playlist. - if (e.code() != MPD_SERVER_ERROR_NO_EXIST) - throw; - } + case MPD::Item::Type::Directory: + path = realPath(m_local_browser, item.directory().path()); + clearDirectory(path); + fs::remove(path); + break; + case MPD::Item::Type::Song: + path = realPath(m_local_browser, item.song().getURI()); + fs::remove(path); + break; + case MPD::Item::Type::Playlist: + path = item.playlist().path(); + try { + Mpd.DeletePlaylist(path); + } catch (MPD::ServerError &e) { + // if there is no such mpd playlist, it's a local one + if (e.code() == MPD_SERVER_ERROR_NO_EXIST) + { + path = realPath(m_local_browser, std::move(path)); + fs::remove(path); + } + else + throw; + } + break; } - - std::string path; - if (!isLocal()) - path = Config.mpd_music_dir; - path += item.type == MPD::Item::Type::Song ? item.song.getURI() : item.name; +} - bool rv; - try +/***********************************************************************/ + +void Browser::fetchSupportedExtensions() +{ + lm_supported_extensions.clear(); + Mpd.GetSupportedExtensions([&](std::string extension) { + lm_supported_extensions.insert("." + std::move(extension)); + }); +} + +/***********************************************************************/ + +namespace { + +std::string realPath(bool local_browser, std::string path) +{ + if (!local_browser) + path = Config.mpd_music_dir + path; + return path; +} + +bool isStringParentDirectory(const std::string &directory) +{ + return boost::algorithm::ends_with(directory, "/.."); +} + +bool isItemParentDirectory(const MPD::Item &item) +{ + return item.type() == MPD::Item::Type::Directory + && isStringParentDirectory(item.directory().path()); +} + +bool isRootDirectory(const std::string &directory) +{ + return directory == "/"; +} + +bool isHidden(const fs::directory_iterator &entry) +{ + return entry->path().filename().native()[0] == '.'; +} + +bool hasSupportedExtension(const fs::directory_entry &entry) +{ + return lm_supported_extensions.find(entry.path().extension().native()) + != lm_supported_extensions.end(); +} + +MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags) +{ + mpd_pair pair = { "file", entry.path().c_str() }; + mpd_song *s = mpd_song_begin(&pair); + if (s == nullptr) + throw std::runtime_error("invalid path: " + entry.path().native()); +# ifdef HAVE_TAGLIB_H + if (read_tags) { - if (item.type == MPD::Item::Type::Directory) - ClearDirectory(path); - if (!boost::filesystem::exists(path)) - { - errmsg = "No such item: " + path; - rv = false; - } - else - { - boost::filesystem::remove(path); - rv = true; - } + Tags::setAttribute(s, "Last-Modified", + timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry.path())) + ); + // read tags + Tags::read(s); } - catch (boost::filesystem::filesystem_error &err) +# endif // HAVE_TAGLIB_H + return s; +} + +void getLocalDirectory(MPD::ItemList &items, const std::string &directory) +{ + for (fs::directory_iterator entry(directory), end; entry != end; ++entry) { - errmsg = err.what(); - rv = false; + if (!Config.local_browser_show_hidden_files && isHidden(entry)) + continue; + + if (fs::is_directory(*entry)) + { + items.push_back(MPD::Directory( + entry->path().native(), + fs::last_write_time(entry->path()) + )); + } + else if (hasSupportedExtension(*entry)) + items.push_back(getLocalSong(*entry, true)); } - return rv; } -#endif // !WIN32 -namespace {// +void getLocalDirectoryRecursively(MPD::SongList &songs, const std::string &directory) +{ + size_t sort_offset = songs.size(); + for (fs::directory_iterator entry(directory), end; entry != end; ++entry) + { + if (!Config.local_browser_show_hidden_files && isHidden(entry)) + continue; + + if (fs::is_directory(*entry)) + { + getLocalDirectoryRecursively(songs, entry->path().native()); + sort_offset = songs.size(); + } + else if (hasSupportedExtension(*entry)) + songs.push_back(getLocalSong(*entry, false)); + }; + + if (Config.browser_sort_mode != SortMode::NoOp) + { + std::sort(songs.begin()+sort_offset, songs.end(), + LocaleBasedSorting(std::locale(), Config.ignore_leading_the) + ); + } +} -bool hasSupportedExtension(const std::string &file) +void clearDirectory(const std::string &directory) { - size_t last_dot = file.rfind("."); - if (last_dot > file.length()) - return false; - - std::string ext = boost::locale::to_lower(file.substr(last_dot+1)); - return SupportedExtensions.find(ext) != SupportedExtensions.end(); + for (fs::directory_iterator entry(directory), end; entry != end; ++entry) + { + if (!fs::is_symlink(*entry) && fs::is_directory(*entry)) + clearDirectory(entry->path().native()); + const char msg[] = "Deleting \"%1%\"..."; + Statusbar::printf(msg, wideShorten(entry->path().native(), COLS-const_strlen(msg))); + fs::remove(entry->path()); + }; } -std::string ItemToString(const MPD::Item &item) +/***********************************************************************/ + +std::string itemToString(const MPD::Item &item) { std::string result; - switch (item.type) + switch (item.type()) { case MPD::Item::Type::Directory: - result = "[" + getBasename(item.name) + "]"; + result = "[" + getBasename(item.directory().path()) + "]"; break; case MPD::Item::Type::Song: switch (Config.browser_display_mode) { case DisplayMode::Classic: - result = item.song.toString(Config.song_list_format_dollar_free, Config.tags_separator); + result = item.song().toString(Config.song_list_format_dollar_free, Config.tags_separator); break; case DisplayMode::Columns: - result = item.song.toString(Config.song_in_columns_to_string_format, Config.tags_separator); + result = item.song().toString(Config.song_in_columns_to_string_format, Config.tags_separator); break; } break; case MPD::Item::Type::Playlist: - result = Config.browser_playlist_prefix.str() + getBasename(item.name); + result = Config.browser_playlist_prefix.str(); + result += getBasename(item.playlist().path()); break; } return result; } -bool BrowserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter) +bool browserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter) { - if (Browser::isParentDirectory(item)) + if (isItemParentDirectory(item)) return filter; - return boost::regex_search(ItemToString(item), rx); + return boost::regex_search(itemToString(item), rx); } } |