Files
ncmpcpp/src/browser.cpp
2015-05-17 21:51:15 +02:00

721 lines
18 KiB
C++

/***************************************************************************
* Copyright (C) 2008-2014 by Andrzej Rybczak *
* electricityispower@gmail.com *
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License *
* along with this program; if not, write to the *
* Free Software Foundation, Inc., *
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
***************************************************************************/
#include <algorithm>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem.hpp>
#include <boost/locale/conversion.hpp>
#include <time.h>
#include "browser.h"
#include "charset.h"
#include "display.h"
#include "global.h"
#include "helpers.h"
#include "playlist.h"
#include "menu_impl.h"
#include "screen_switcher.h"
#include "settings.h"
#include "status.h"
#include "statusbar.h"
#include "tag_editor.h"
#include "title.h"
#include "tags.h"
#include "helpers/song_iterator_maker.h"
#include "utility/comparators.h"
#include "utility/string.h"
#include "configuration.h"
using Global::MainHeight;
using Global::MainStartY;
using Global::myScreen;
namespace fs = boost::filesystem;
namespace ph = std::placeholders;
Browser *myBrowser;
namespace {
std::set<std::string> lm_supported_extensions;
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(std::vector<MPD::Item> &items, const std::string &directory);
void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory);
void clearDirectory(const std::string &directory);
std::string itemToString(const MPD::Item &item);
bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter);
template <bool Const>
struct SongExtractor
{
typedef SongExtractor type;
typedef typename NC::Menu<MPD::Item>::Item MenuItem;
typedef typename std::conditional<Const, const MenuItem, MenuItem>::type Item;
typedef typename std::conditional<Const, const MPD::Song, MPD::Song>::type Song;
Song *operator()(Item &item) const
{
Song *ptr = nullptr;
if (item.value().type() == MPD::Item::Type::Song)
ptr = const_cast<Song *>(&item.value().song());
return ptr;
}
};
}
SongIterator BrowserWindow::currentS()
{
return makeSongIterator_<MPD::Item>(current(), SongExtractor<false>());
}
ConstSongIterator BrowserWindow::currentS() const
{
return makeConstSongIterator_<MPD::Item>(current(), SongExtractor<true>());
}
SongIterator BrowserWindow::beginS()
{
return makeSongIterator_<MPD::Item>(begin(), SongExtractor<false>());
}
ConstSongIterator BrowserWindow::beginS() const
{
return makeConstSongIterator_<MPD::Item>(begin(), SongExtractor<true>());
}
SongIterator BrowserWindow::endS()
{
return makeSongIterator_<MPD::Item>(end(), SongExtractor<false>());
}
ConstSongIterator BrowserWindow::endS() const
{
return makeConstSongIterator_<MPD::Item>(end(), SongExtractor<true>());
}
std::vector<MPD::Song> BrowserWindow::getSelectedSongs()
{
return {}; // TODO
}
/**********************************************************************/
Browser::Browser()
: m_update_request(true)
, 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());
w.setHighlightColor(Config.main_highlight_color);
w.cyclicScrolling(Config.use_cyclic_scrolling);
w.centeredCursor(Config.centered_cursor);
w.setSelectedPrefix(Config.selected_item_prefix);
w.setSelectedSuffix(Config.selected_item_suffix);
w.setItemDisplayer(std::bind(Display::Items, ph::_1, std::cref(w)));
}
void Browser::resize()
{
size_t x_offset, width;
getWindowResizeParams(x_offset, width);
w.resize(width, MainHeight);
w.moveTo(x_offset, MainStartY);
switch (Config.browser_display_mode)
{
case DisplayMode::Columns:
if (Config.titles_visibility)
{
w.setTitle(Display::Columns(w.getWidth()));
break;
}
case DisplayMode::Classic:
w.setTitle("");
break;
}
hasToBeResized = 0;
}
void Browser::switchTo()
{
SwitchTo::execute(this);
markSongsInPlaylist(w);
drawHeader();
}
std::wstring Browser::title()
{
std::wstring result = L"Browse: ";
result += Scroller(ToWString(m_current_directory), m_scroll_beginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length()));
return result;
}
void Browser::update()
{
if (m_update_request)
{
m_update_request = false;
bool directory_changed = false;
do
{
try
{
getDirectory(m_current_directory);
w.refresh();
}
catch (MPD::ServerError &err)
{
// If current directory doesn't exist, try getting its
// parent until we either succeed or reach the root.
if (err.code() == MPD_SERVER_ERROR_NO_EXIST)
{
m_current_directory = getParentDirectory(m_current_directory);
directory_changed = true;
}
else
throw;
}
}
while (w.empty() && !inRootDirectory());
if (directory_changed)
drawHeader();
}
}
void Browser::enterPressed()
{
if (w.empty())
return;
const MPD::Item &item = w.current()->value();
switch (item.type())
{
case MPD::Item::Type::Directory:
{
getDirectory(item.directory().path());
drawHeader();
break;
}
case MPD::Item::Type::Song:
{
addSongToPlaylist(item.song(), true, -1);
break;
}
case MPD::Item::Type::Playlist:
{
std::vector<MPD::Song> list(
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.playlist().path(), withErrors(success)
);
}
}
}
void Browser::mouseButtonPressed(MEVENT me)
{
if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
return;
if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
{
w.Goto(me.y);
switch (w.current()->value().type())
{
case MPD::Item::Type::Directory:
if (me.bstate & BUTTON1_PRESSED)
{
getDirectory(w.current()->value().directory().path());
drawHeader();
}
else
addItemToPlaylist();
break;
case MPD::Item::Type::Playlist:
case MPD::Item::Type::Song:
if (me.bstate & BUTTON1_PRESSED)
addItemToPlaylist();
else
enterPressed();
break;
}
}
else
Screen<WindowType>::mouseButtonPressed(me);
}
/***********************************************************************/
bool Browser::allowsSearching()
{
return true;
}
void Browser::setSearchConstraint(const std::string &constraint)
{
m_search_predicate = Regex::Filter<MPD::Item>(
Regex::make(constraint, Config.regex_type),
std::bind(browserEntryMatcher, ph::_1, ph::_2, false)
);
}
void Browser::clearConstraint()
{
m_search_predicate.clear();
}
bool Browser::find(SearchDirection direction, bool wrap, bool skip_current)
{
return search(w, m_search_predicate, direction, wrap, skip_current);
}
/***********************************************************************/
bool Browser::addItemToPlaylist()
{
bool result = false;
if (w.empty())
return result;
const MPD::Item &item = w.current()->value();
// ignore parent directory
if (isParentDirectory(item))
return result;
switch (item.type())
{
case MPD::Item::Type::Directory:
{
if (m_local_browser)
{
std::vector<MPD::Song> songs;
getLocalDirectoryRecursively(songs, item.directory().path());
result = addSongsToPlaylist(songs.begin(), songs.end(), false, -1);
}
else
Mpd.Add(item.directory().path());
Statusbar::printf("Directory \"%1%\" added%2%",
item.directory().path(), withErrors(result)
);
break;
}
case MPD::Item::Type::Song:
result = addSongToPlaylist(item.song(), false);
break;
case MPD::Item::Type::Playlist:
Mpd.LoadPlaylist(item.playlist().path());
Statusbar::printf("Playlist \"%1%\" loaded", item.playlist().path());
result = true;
break;
}
return result;
}
std::vector<MPD::Song> Browser::getSelectedSongs()
{
std::vector<MPD::Song> songs;
auto item_handler = [this, &songs](const MPD::Item &item) {
switch (item.type())
{
case MPD::Item::Type::Directory:
if (m_local_browser)
getLocalDirectoryRecursively(songs, item.directory().path());
else
{
std::copy(
std::make_move_iterator(Mpd.GetDirectoryRecursive(item.directory().path())),
std::make_move_iterator(MPD::SongIterator()),
std::back_inserter(songs)
);
}
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 (const auto &item : w)
if (item.isSelected())
item_handler(item.value());
// if no item is selected, add current one
if (songs.empty() && !w.empty())
item_handler(w.current()->value());
return songs;
}
/***********************************************************************/
bool Browser::inRootDirectory()
{
return isRootDirectory(m_current_directory);
}
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())
throw std::runtime_error("Song's directory is empty");
m_local_browser = !s.isFromDatabase();
if (myScreen != this)
switchTo();
// change to relevant directory
if (m_current_directory != s.getDirectory())
{
getDirectory(s.getDirectory());
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 directory)
{
m_scroll_beginning = 0;
w.clear();
// reset the position if we change directories
if (m_current_directory != directory)
w.reset();
// check if it's a parent directory
if (isStringParentDirectory(directory))
{
directory.resize(directory.length()-3);
directory = getParentDirectory(directory);
}
// when we go down to root, it can be empty
if (directory.empty())
directory = "/";
std::vector<MPD::Item> items;
if (m_local_browser)
getLocalDirectory(items, directory);
else
{
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)
);
}
// 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 + "/.."), NC::List::Properties::None);
}
for (const auto &item : items)
{
switch (item.type())
{
case MPD::Item::Type::Playlist:
{
w.addItem(std::move(item));
break;
}
case MPD::Item::Type::Directory:
{
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:
{
auto properties = NC::List::Properties::Selectable;
if (myPlaylist->checkForSong(item.song()))
properties |= NC::List::Properties::Bold;
w.addItem(std::move(item), properties);
break;
}
}
}
m_current_directory = directory;
}
void Browser::changeBrowseMode()
{
if (Mpd.GetHostname()[0] != '/')
{
Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
return;
}
m_local_browser = !m_local_browser;
Statusbar::printf("Browse mode: %1%",
m_local_browser ? "local filesystem" : "MPD database"
);
if (m_local_browser)
{
m_current_directory = "~";
expand_home(m_current_directory);
}
else
m_current_directory = "/";
w.reset();
getDirectory(m_current_directory);
drawHeader();
}
void Browser::remove(const MPD::Item &item)
{
if (!Config.allow_for_physical_item_deletion)
throw std::runtime_error("physical deletion is forbidden");
if (isParentDirectory((item)))
throw std::runtime_error("deletion of parent directory is forbidden");
std::string path;
switch (item.type())
{
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;
}
}
/***********************************************************************/
void Browser::fetchSupportedExtensions()
{
lm_supported_extensions.clear();
MPD::StringIterator extension = Mpd.GetSupportedExtensions(), end;
for (; extension != end; ++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(std::vector<MPD::Item> &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(std::vector<MPD::Song> &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 = Format::stringify<char>(Config.song_list_format, &item.song());
break;
case DisplayMode::Columns:
result = Format::stringify<char>(Config.song_columns_mode_format, &item.song());
break;
}
break;
case MPD::Item::Type::Playlist:
result = Config.browser_playlist_prefix.str();
result += getBasename(item.playlist().path());
break;
}
return result;
}
bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter)
{
if (isItemParentDirectory(item))
return filter;
return Regex::search(itemToString(item), rx);
}
}