mpd: redesign Item and adjust browser

This commit is contained in:
Andrzej Rybczak
2014-11-02 00:22:02 +01:00
parent 30d57afcac
commit 0457af36fe
19 changed files with 783 additions and 557 deletions

View File

@@ -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> SupportedExtensions;
bool hasSupportedExtension(const std::string &file);
std::set<std::string> lm_supported_extensions;
std::string ItemToString(const MPD::Item &item);
bool BrowserEntryMatcher(const boost::regex &rx, const MPD::Item &item, bool filter);
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);
}
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)
MPD::SongList songs;
auto item_handler = [this, &songs](const MPD::Item &item) {
switch (item.type())
{
# 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)
{
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()
{
SupportedExtensions.clear();
Mpd.GetSupportedExtensions(SupportedExtensions);
return isRootDirectory(m_current_directory);
}
void Browser::LocateSong(const MPD::Song &s)
bool Browser::isParentDirectory(const MPD::Item &item)
{
return isItemParentDirectory(item);
}
const std::string& Browser::currentDirectory()
{
return m_current_directory;
}
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))
{
switch (it->type)
// make it so that display function doesn't have to handle special cases
w.addItem(MPD::Directory(directory + "/.."));
}
for (const auto &item : items)
{
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);
m_current_directory = directory;
}
#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)
);
}
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() == "/")
{
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;
}
}
throw std::runtime_error("deletion of parent directory is forbidden");
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
{
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;
}
}
catch (boost::filesystem::filesystem_error &err)
{
errmsg = err.what();
rv = false;
}
return rv;
}
#endif // !WIN32
namespace {//
bool hasSupportedExtension(const std::string &file)
{
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();
}
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) + "]";
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;
}
}
/***********************************************************************/
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)
{
Tags::setAttribute(s, "Last-Modified",
timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry.path()))
);
// read tags
Tags::read(s);
}
# 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)
{
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));
}
}
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)
);
}
}
void clearDirectory(const std::string &directory)
{
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 result;
switch (item.type())
{
case MPD::Item::Type::Directory:
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);
}
}