1 /***************************************************************************
2 * Copyright (C) 2008-2016 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
22 #include <boost/algorithm/string/predicate.hpp>
23 #include <boost/filesystem.hpp>
24 #include <boost/locale/conversion.hpp>
33 #include "menu_impl.h"
34 #include "screen_switcher.h"
37 #include "statusbar.h"
38 #include "tag_editor.h"
41 #include "helpers/song_iterator_maker.h"
42 #include "utility/comparators.h"
43 #include "utility/string.h"
44 #include "configuration.h"
46 using Global::MainHeight
;
47 using Global::MainStartY
;
48 using Global::myScreen
;
50 namespace fs
= boost::filesystem
;
51 namespace ph
= std::placeholders
;
57 std::set
<std::string
> lm_supported_extensions
;
59 std::string
realPath(bool local_browser
, std::string path
);
60 bool isStringParentDirectory(const std::string
&directory
);
61 bool isItemParentDirectory(const MPD::Item
&item
);
62 bool isRootDirectory(const std::string
&directory
);
63 bool isHidden(const fs::directory_iterator
&entry
);
64 bool hasSupportedExtension(const fs::directory_entry
&entry
);
65 MPD::Song
getLocalSong(const fs::directory_entry
&entry
, bool read_tags
);
66 void getLocalDirectory(std::vector
<MPD::Item
> &items
, const std::string
&directory
);
67 void getLocalDirectoryRecursively(std::vector
<MPD::Song
> &songs
, const std::string
&directory
);
68 void clearDirectory(const std::string
&directory
);
70 std::string
itemToString(const MPD::Item
&item
);
71 bool browserEntryMatcher(const Regex::Regex
&rx
, const MPD::Item
&item
, bool filter
);
76 typedef SongExtractor type
;
78 typedef typename
NC::Menu
<MPD::Item
>::Item MenuItem
;
79 typedef typename
std::conditional
<Const
, const MenuItem
, MenuItem
>::type Item
;
80 typedef typename
std::conditional
<Const
, const MPD::Song
, MPD::Song
>::type Song
;
82 Song
*operator()(Item
&item
) const
85 if (item
.value().type() == MPD::Item::Type::Song
)
86 ptr
= const_cast<Song
*>(&item
.value().song());
93 SongIterator
BrowserWindow::currentS()
95 return makeSongIterator_
<MPD::Item
>(current(), SongExtractor
<false>());
98 ConstSongIterator
BrowserWindow::currentS() const
100 return makeConstSongIterator_
<MPD::Item
>(current(), SongExtractor
<true>());
103 SongIterator
BrowserWindow::beginS()
105 return makeSongIterator_
<MPD::Item
>(begin(), SongExtractor
<false>());
108 ConstSongIterator
BrowserWindow::beginS() const
110 return makeConstSongIterator_
<MPD::Item
>(begin(), SongExtractor
<true>());
113 SongIterator
BrowserWindow::endS()
115 return makeSongIterator_
<MPD::Item
>(end(), SongExtractor
<false>());
118 ConstSongIterator
BrowserWindow::endS() const
120 return makeConstSongIterator_
<MPD::Item
>(end(), SongExtractor
<true>());
123 std::vector
<MPD::Song
> BrowserWindow::getSelectedSongs()
128 /**********************************************************************/
131 : m_update_request(true)
132 , m_local_browser(false)
133 , m_scroll_beginning(0)
134 , m_current_directory("/")
136 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());
137 w
.setHighlightColor(Config
.main_highlight_color
);
138 w
.cyclicScrolling(Config
.use_cyclic_scrolling
);
139 w
.centeredCursor(Config
.centered_cursor
);
140 w
.setSelectedPrefix(Config
.selected_item_prefix
);
141 w
.setSelectedSuffix(Config
.selected_item_suffix
);
142 w
.setItemDisplayer(std::bind(Display::Items
, ph::_1
, std::cref(w
)));
145 void Browser::resize()
147 size_t x_offset
, width
;
148 getWindowResizeParams(x_offset
, width
);
149 w
.resize(width
, MainHeight
);
150 w
.moveTo(x_offset
, MainStartY
);
151 switch (Config
.browser_display_mode
)
153 case DisplayMode::Columns
:
154 if (Config
.titles_visibility
)
156 w
.setTitle(Display::Columns(w
.getWidth()));
159 case DisplayMode::Classic
:
166 void Browser::switchTo()
168 SwitchTo::execute(this);
169 markSongsInPlaylist(w
);
173 std::wstring
Browser::title()
175 std::wstring result
= L
"Browse: ";
176 result
+= Scroller(ToWString(m_current_directory
), m_scroll_beginning
, COLS
-result
.length()-(Config
.design
== Design::Alternative
? 2 : Global::VolumeState
.length()));
180 void Browser::update()
182 if (m_update_request
)
184 m_update_request
= false;
185 bool directory_changed
= false;
190 getDirectory(m_current_directory
);
193 catch (MPD::ServerError
&err
)
195 // If current directory doesn't exist, try getting its
196 // parent until we either succeed or reach the root.
197 if (err
.code() == MPD_SERVER_ERROR_NO_EXIST
)
199 m_current_directory
= getParentDirectory(m_current_directory
);
200 directory_changed
= true;
206 while (w
.empty() && !inRootDirectory());
207 if (directory_changed
)
212 void Browser::mouseButtonPressed(MEVENT me
)
214 if (w
.empty() || !w
.hasCoords(me
.x
, me
.y
) || size_t(me
.y
) >= w
.size())
216 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
219 switch (w
.current()->value().type())
221 case MPD::Item::Type::Directory
:
222 if (me
.bstate
& BUTTON1_PRESSED
)
225 addItemToPlaylist(false);
227 case MPD::Item::Type::Playlist
:
228 case MPD::Item::Type::Song
:
230 bool play
= me
.bstate
& BUTTON3_PRESSED
;
231 addItemToPlaylist(play
);
237 Screen
<WindowType
>::mouseButtonPressed(me
);
240 /***********************************************************************/
242 bool Browser::allowsSearching()
247 const std::string
&Browser::searchConstraint()
249 return m_search_predicate
.constraint();
252 void Browser::setSearchConstraint(const std::string
&constraint
)
254 m_search_predicate
= Regex::Filter
<MPD::Item
>(
257 std::bind(browserEntryMatcher
, ph::_1
, ph::_2
, false));
260 void Browser::clearSearchConstraint()
262 m_search_predicate
.clear();
265 bool Browser::search(SearchDirection direction
, bool wrap
, bool skip_current
)
267 return ::search(w
, m_search_predicate
, direction
, wrap
, skip_current
);
270 /***********************************************************************/
272 bool Browser::allowsFiltering()
274 return allowsSearching();
277 std::string
Browser::currentFilter()
280 if (auto pred
= w
.filterPredicate
<Regex::Filter
<MPD::Item
>>())
281 result
= pred
->constraint();
285 void Browser::applyFilter(const std::string
&constraint
)
287 if (!constraint
.empty())
289 w
.applyFilter(Regex::Filter
<MPD::Item
>(
292 std::bind(browserEntryMatcher
, ph::_1
, ph::_2
, true)));
299 /***********************************************************************/
301 bool Browser::itemAvailable()
304 // ignore parent directory
305 && !isParentDirectory(w
.current()->value());
308 bool Browser::addItemToPlaylist(bool play
)
312 auto tryToPlay
= [] {
313 // Cheap trick that might fail in presence of multiple
314 // clients modifying the playlist at the same time, but
315 // oh well, this approach correctly loads cue playlists
316 // and is much faster in general as it doesn't require
317 // fetching song data.
320 Mpd
.Play(Status::State::playlistLength());
322 catch (MPD::ServerError
&e
)
324 // If not bad index, rethrow.
325 if (e
.code() != MPD_SERVER_ERROR_ARG
)
330 const MPD::Item
&item
= w
.current()->value();
333 case MPD::Item::Type::Directory
:
337 std::vector
<MPD::Song
> songs
;
338 getLocalDirectoryRecursively(songs
, item
.directory().path());
339 result
= addSongsToPlaylist(songs
.begin(), songs
.end(), play
, -1);
343 Mpd
.Add(item
.directory().path());
348 Statusbar::printf("Directory \"%1%\" added%2%",
349 item
.directory().path(), withErrors(result
));
352 case MPD::Item::Type::Song
:
353 result
= addSongToPlaylist(item
.song(), play
);
355 case MPD::Item::Type::Playlist
:
356 Mpd
.LoadPlaylist(item
.playlist().path());
359 Statusbar::printf("Playlist \"%1%\" loaded", item
.playlist().path());
366 std::vector
<MPD::Song
> Browser::getSelectedSongs()
368 std::vector
<MPD::Song
> songs
;
369 auto item_handler
= [this, &songs
](const MPD::Item
&item
) {
372 case MPD::Item::Type::Directory
:
374 getLocalDirectoryRecursively(songs
, item
.directory().path());
378 std::make_move_iterator(Mpd
.GetDirectoryRecursive(item
.directory().path())),
379 std::make_move_iterator(MPD::SongIterator()),
380 std::back_inserter(songs
)
384 case MPD::Item::Type::Song
:
385 songs
.push_back(item
.song());
387 case MPD::Item::Type::Playlist
:
389 std::make_move_iterator(Mpd
.GetPlaylistContent(item
.playlist().path())),
390 std::make_move_iterator(MPD::SongIterator()),
391 std::back_inserter(songs
)
396 for (const auto &item
: w
)
397 if (item
.isSelected())
398 item_handler(item
.value());
399 // if no item is selected, add current one
400 if (songs
.empty() && !w
.empty())
401 item_handler(w
.current()->value());
405 /***********************************************************************/
407 bool Browser::inRootDirectory()
409 return isRootDirectory(m_current_directory
);
412 bool Browser::isParentDirectory(const MPD::Item
&item
)
414 return isItemParentDirectory(item
);
417 const std::string
& Browser::currentDirectory()
419 return m_current_directory
;
422 void Browser::locateSong(const MPD::Song
&s
)
424 if (s
.getDirectory().empty())
425 throw std::runtime_error("Song's directory is empty");
427 m_local_browser
= !s
.isFromDatabase();
429 if (myScreen
!= this)
434 // change to relevant directory
435 if (m_current_directory
!= s
.getDirectory())
437 getDirectory(s
.getDirectory());
441 // highlight the item
442 auto begin
= w
.beginV(), end
= w
.endV();
443 auto it
= std::find(begin
, end
, MPD::Item(s
));
445 w
.highlight(it
-begin
);
448 bool Browser::enterDirectory()
453 const auto &item
= w
.current()->value();
454 if (item
.type() == MPD::Item::Type::Directory
)
456 getDirectory(item
.directory().path());
464 void Browser::getDirectory(std::string directory
)
466 ScopedUnfilteredMenu
<MPD::Item
> sunfilter(ReapplyFilter::Yes
, w
);
468 m_scroll_beginning
= 0;
471 // reset the position if we change directories
472 if (m_current_directory
!= directory
)
475 // check if it's a parent directory
476 if (isStringParentDirectory(directory
))
478 directory
.resize(directory
.length()-3);
479 directory
= getParentDirectory(directory
);
481 // when we go down to root, it can be empty
482 if (directory
.empty())
485 std::vector
<MPD::Item
> items
;
487 getLocalDirectory(items
, directory
);
491 std::make_move_iterator(Mpd
.GetDirectory(directory
)),
492 std::make_move_iterator(MPD::ItemIterator()),
493 std::back_inserter(items
)
498 if (Config
.browser_sort_mode
!= SortMode::NoOp
)
500 std::sort(items
.begin(), items
.end(),
501 LocaleBasedItemSorting(std::locale(), Config
.ignore_leading_the
, Config
.browser_sort_mode
)
505 // if the requested directory is not root, add parent directory
506 if (!isRootDirectory(directory
))
508 // make it so that display function doesn't have to handle special cases
509 w
.addItem(MPD::Directory(directory
+ "/.."), NC::List::Properties::None
);
512 for (const auto &item
: items
)
516 case MPD::Item::Type::Playlist
:
518 w
.addItem(std::move(item
));
521 case MPD::Item::Type::Directory
:
523 bool is_current
= item
.directory().path() == m_current_directory
;
524 w
.addItem(std::move(item
));
526 w
.highlight(w
.size()-1);
529 case MPD::Item::Type::Song
:
531 auto properties
= NC::List::Properties::Selectable
;
532 if (myPlaylist
->checkForSong(item
.song()))
533 properties
|= NC::List::Properties::Bold
;
534 w
.addItem(std::move(item
), properties
);
539 m_current_directory
= directory
;
542 void Browser::changeBrowseMode()
544 if (Mpd
.GetHostname()[0] != '/')
546 Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
550 m_local_browser
= !m_local_browser
;
551 Statusbar::printf("Browse mode: %1%",
552 m_local_browser
? "local filesystem" : "MPD database"
556 m_current_directory
= "~";
557 expand_home(m_current_directory
);
560 m_current_directory
= "/";
562 getDirectory(m_current_directory
);
566 void Browser::remove(const MPD::Item
&item
)
568 if (!Config
.allow_for_physical_item_deletion
)
569 throw std::runtime_error("physical deletion is forbidden");
570 if (isParentDirectory((item
)))
571 throw std::runtime_error("deletion of parent directory is forbidden");
576 case MPD::Item::Type::Directory
:
577 path
= realPath(m_local_browser
, item
.directory().path());
578 clearDirectory(path
);
581 case MPD::Item::Type::Song
:
582 path
= realPath(m_local_browser
, item
.song().getURI());
585 case MPD::Item::Type::Playlist
:
586 path
= item
.playlist().path();
588 Mpd
.DeletePlaylist(path
);
589 } catch (MPD::ServerError
&e
) {
590 // if there is no such mpd playlist, it's a local one
591 if (e
.code() == MPD_SERVER_ERROR_NO_EXIST
)
593 path
= realPath(m_local_browser
, std::move(path
));
603 /***********************************************************************/
605 void Browser::fetchSupportedExtensions()
607 lm_supported_extensions
.clear();
608 MPD::StringIterator extension
= Mpd
.GetSupportedExtensions(), end
;
609 for (; extension
!= end
; ++extension
)
610 lm_supported_extensions
.insert("." + std::move(*extension
));
613 /***********************************************************************/
617 std::string
realPath(bool local_browser
, std::string path
)
620 path
= Config
.mpd_music_dir
+ path
;
624 bool isStringParentDirectory(const std::string
&directory
)
626 return boost::algorithm::ends_with(directory
, "/..");
629 bool isItemParentDirectory(const MPD::Item
&item
)
631 return item
.type() == MPD::Item::Type::Directory
632 && isStringParentDirectory(item
.directory().path());
635 bool isRootDirectory(const std::string
&directory
)
637 return directory
== "/";
640 bool isHidden(const fs::directory_iterator
&entry
)
642 return entry
->path().filename().native()[0] == '.';
645 bool hasSupportedExtension(const fs::directory_entry
&entry
)
647 return lm_supported_extensions
.find(entry
.path().extension().native())
648 != lm_supported_extensions
.end();
651 MPD::Song
getLocalSong(const fs::directory_entry
&entry
, bool read_tags
)
653 mpd_pair pair
= { "file", entry
.path().c_str() };
654 mpd_song
*s
= mpd_song_begin(&pair
);
656 throw std::runtime_error("invalid path: " + entry
.path().native());
660 Tags::setAttribute(s
, "Last-Modified",
661 timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry
.path()))
665 #endif // HAVE_TAGLIB_H
670 void getLocalDirectory(std::vector
<MPD::Item
> &items
, const std::string
&directory
)
672 for (fs::directory_iterator
entry(directory
), end
; entry
!= end
; ++entry
)
674 if (!Config
.local_browser_show_hidden_files
&& isHidden(entry
))
677 if (fs::is_directory(*entry
))
679 items
.push_back(MPD::Directory(
680 entry
->path().native(),
681 fs::last_write_time(entry
->path())
684 else if (hasSupportedExtension(*entry
))
685 items
.push_back(getLocalSong(*entry
, true));
689 void getLocalDirectoryRecursively(std::vector
<MPD::Song
> &songs
, const std::string
&directory
)
691 size_t sort_offset
= songs
.size();
692 for (fs::directory_iterator
entry(directory
), end
; entry
!= end
; ++entry
)
694 if (!Config
.local_browser_show_hidden_files
&& isHidden(entry
))
697 if (fs::is_directory(*entry
))
699 getLocalDirectoryRecursively(songs
, entry
->path().native());
700 sort_offset
= songs
.size();
702 else if (hasSupportedExtension(*entry
))
703 songs
.push_back(getLocalSong(*entry
, false));
706 if (Config
.browser_sort_mode
!= SortMode::NoOp
)
708 std::sort(songs
.begin()+sort_offset
, songs
.end(),
709 LocaleBasedSorting(std::locale(), Config
.ignore_leading_the
)
714 void clearDirectory(const std::string
&directory
)
716 for (fs::directory_iterator
entry(directory
), end
; entry
!= end
; ++entry
)
718 if (!fs::is_symlink(*entry
) && fs::is_directory(*entry
))
719 clearDirectory(entry
->path().native());
720 const char msg
[] = "Deleting \"%1%\"...";
721 Statusbar::printf(msg
, wideShorten(entry
->path().native(), COLS
-const_strlen(msg
)));
722 fs::remove(entry
->path());
726 /***********************************************************************/
728 std::string
itemToString(const MPD::Item
&item
)
733 case MPD::Item::Type::Directory
:
734 result
= "[" + getBasename(item
.directory().path()) + "]";
736 case MPD::Item::Type::Song
:
737 switch (Config
.browser_display_mode
)
739 case DisplayMode::Classic
:
740 result
= Format::stringify
<char>(Config
.song_list_format
, &item
.song());
742 case DisplayMode::Columns
:
743 result
= Format::stringify
<char>(Config
.song_columns_mode_format
, &item
.song());
747 case MPD::Item::Type::Playlist
:
748 result
= Config
.browser_playlist_prefix
.str();
749 result
+= getBasename(item
.playlist().path());
755 bool browserEntryMatcher(const Regex::Regex
&rx
, const MPD::Item
&item
, bool filter
)
757 if (isItemParentDirectory(item
))
759 return Regex::search(itemToString(item
), rx
);