1 /***************************************************************************
2 * Copyright (C) 2008-2013 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 ***************************************************************************/
25 #include <boost/filesystem.hpp>
26 #include <boost/locale/conversion.hpp>
36 #include "regex_filter.h"
37 #include "screen_switcher.h"
40 #include "statusbar.h"
41 #include "tag_editor.h"
44 #include "utility/comparators.h"
45 #include "utility/string.h"
47 using namespace std::placeholders
;
49 using Global::MainHeight
;
50 using Global::MainStartY
;
51 using Global::myScreen
;
53 using MPD::itDirectory
;
55 using MPD::itPlaylist
;
61 std::set
<std::string
> SupportedExtensions
;
62 bool hasSupportedExtension(const std::string
&file
);
64 std::string
ItemToString(const MPD::Item
&item
);
65 bool BrowserEntryMatcher(const boost::regex
&rx
, const MPD::Item
&item
, bool filter
);
69 Browser::Browser() : itsBrowseLocally(0), itsScrollBeginning(0), itsBrowsedDir("/")
71 w
= NC::Menu
<MPD::Item
>(0, MainStartY
, COLS
, MainHeight
, Config
.columns_in_browser
&& Config
.titles_visibility
? Display::Columns(COLS
) : "", Config
.main_color
, NC::Border::None
);
72 w
.setHighlightColor(Config
.main_highlight_color
);
73 w
.cyclicScrolling(Config
.use_cyclic_scrolling
);
74 w
.centeredCursor(Config
.centered_cursor
);
75 w
.setSelectedPrefix(Config
.selected_item_prefix
);
76 w
.setSelectedSuffix(Config
.selected_item_suffix
);
77 w
.setItemDisplayer(std::bind(Display::Items
, _1
, proxySongList()));
80 void Browser::resize()
82 size_t x_offset
, width
;
83 getWindowResizeParams(x_offset
, width
);
84 w
.resize(width
, MainHeight
);
85 w
.moveTo(x_offset
, MainStartY
);
86 w
.setTitle(Config
.columns_in_browser
&& Config
.titles_visibility
? Display::Columns(w
.getWidth()) : "");
90 void Browser::switchTo()
92 SwitchTo::execute(this);
95 GetDirectory(itsBrowsedDir
);
97 markSongsInPlaylist(proxySongList());
102 std::wstring
Browser::title()
104 std::wstring result
= L
"Browse: ";
105 result
+= Scroller(ToWString(itsBrowsedDir
), itsScrollBeginning
, COLS
-result
.length()-(Config
.new_design
? 2 : Global::VolumeState
.length()));
109 void Browser::enterPressed()
114 const MPD::Item
&item
= w
.current().value();
119 if (isParentDirectory(item
))
120 GetDirectory(getParentDirectory(itsBrowsedDir
), itsBrowsedDir
);
122 GetDirectory(item
.name
, itsBrowsedDir
);
128 addSongToPlaylist(*item
.song
, true, -1);
133 Mpd
.LoadPlaylist(item
.name
);
134 Statusbar::msg("Playlist \"%s\" loaded", item
.name
.c_str());
135 myPlaylist
->PlayNewlyAddedSongs();
140 void Browser::spacePressed()
145 size_t i
= itsBrowsedDir
!= "/" ? 1 : 0;
146 if (Config
.space_selects
&& w
.choice() >= i
)
149 w
.at(i
).setSelected(!w
.at(i
).isSelected());
150 w
.scroll(NC::Scroll::Down
);
154 const MPD::Item
&item
= w
.current().value();
156 if (isParentDirectory(item
))
168 Statusbar::msg("Scanning directory \"%s\"...", item
.name
.c_str());
169 myBrowser
->GetLocalDirectory(items
, item
.name
, 1);
170 list
.reserve(items
.size());
171 for (MPD::ItemList::const_iterator it
= items
.begin(); it
!= items
.end(); ++it
)
172 list
.push_back(*it
->song
);
173 addSongsToPlaylist(list
, false, -1);
178 Statusbar::msg("Directory \"%s\" added", item
.name
.c_str());
183 addSongToPlaylist(*item
.song
, false);
188 Mpd
.LoadPlaylist(item
.name
);
189 Statusbar::msg("Playlist \"%s\" loaded", item
.name
.c_str());
193 w
.scroll(NC::Scroll::Down
);
196 void Browser::mouseButtonPressed(MEVENT me
)
198 if (w
.empty() || !w
.hasCoords(me
.x
, me
.y
) || size_t(me
.y
) >= w
.size())
200 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
203 switch (w
.current().value().type
)
206 if (me
.bstate
& BUTTON1_PRESSED
)
208 GetDirectory(w
.current().value().name
);
213 size_t pos
= w
.choice();
215 if (pos
< w
.size()-1)
216 w
.scroll(NC::Scroll::Up
);
221 if (me
.bstate
& BUTTON1_PRESSED
)
223 size_t pos
= w
.choice();
225 if (pos
< w
.size()-1)
226 w
.scroll(NC::Scroll::Up
);
234 Screen
<WindowType
>::mouseButtonPressed(me
);
237 /***********************************************************************/
239 bool Browser::allowsFiltering()
244 std::string
Browser::currentFilter()
246 return RegexFilter
<MPD::Item
>::currentFilter(w
);
249 void Browser::applyFilter(const std::string
&filter
)
254 w
.clearFilterResults();
260 auto fun
= std::bind(BrowserEntryMatcher
, _1
, _2
, true);
261 auto rx
= RegexFilter
<MPD::Item
>(
262 boost::regex(filter
, Config
.regex_type
), fun
);
263 w
.filter(w
.begin(), w
.end(), rx
);
265 catch (boost::bad_expression
&) { }
268 /***********************************************************************/
270 bool Browser::allowsSearching()
275 bool Browser::search(const std::string
&constraint
)
277 if (constraint
.empty())
279 w
.clearSearchResults();
284 auto fun
= std::bind(BrowserEntryMatcher
, _1
, _2
, false);
285 auto rx
= RegexFilter
<MPD::Item
>(
286 boost::regex(constraint
, Config
.regex_type
), fun
);
287 return w
.search(w
.begin(), w
.end(), rx
);
289 catch (boost::bad_expression
&)
295 void Browser::nextFound(bool wrap
)
300 void Browser::prevFound(bool wrap
)
305 /***********************************************************************/
307 ProxySongList
Browser::proxySongList()
309 return ProxySongList(w
, [](NC::Menu
<MPD::Item
>::Item
&item
) -> MPD::Song
* {
311 if (item
.value().type
== itSong
)
312 ptr
= item
.value().song
.get();
317 bool Browser::allowsSelection()
322 void Browser::reverseSelection()
324 reverseSelectionHelper(w
.begin()+(itsBrowsedDir
== "/" ? 0 : 1), w
.end());
327 MPD::SongList
Browser::getSelectedSongs()
329 MPD::SongList result
;
330 auto item_handler
= [this, &result
](const MPD::Item
&item
) {
331 if (item
.type
== itDirectory
)
337 GetLocalDirectory(list
, item
.name
, true);
338 for (auto it
= list
.begin(); it
!= list
.end(); ++it
)
339 result
.push_back(*it
->song
);
344 Mpd
.GetDirectoryRecursive(item
.name
, [&result
](MPD::Song
&&s
) {
349 else if (item
.type
== itSong
)
350 result
.push_back(*item
.song
);
351 else if (item
.type
== itPlaylist
)
353 Mpd
.GetPlaylistContent(item
.name
, [&result
](MPD::Song
&&s
) {
358 for (auto it
= w
.begin(); it
!= w
.end(); ++it
)
359 if (it
->isSelected())
360 item_handler(it
->value());
361 // if no item is selected, add current one
362 if (result
.empty() && !w
.empty())
363 item_handler(w
.current().value());
367 void Browser::fetchSupportedExtensions()
369 SupportedExtensions
.clear();
370 Mpd
.GetSupportedExtensions(SupportedExtensions
);
373 void Browser::LocateSong(const MPD::Song
&s
)
375 if (s
.getDirectory().empty())
378 itsBrowseLocally
= !s
.isFromDatabase();
380 if (myScreen
!= this)
383 if (itsBrowsedDir
!= s
.getDirectory())
384 GetDirectory(s
.getDirectory());
385 for (size_t i
= 0; i
< w
.size(); ++i
)
387 if (w
[i
].value().type
== itSong
&& s
.getHash() == w
[i
].value().song
->getHash())
396 void Browser::GetDirectory(std::string dir
, std::string subdir
)
401 int highlightme
= -1;
402 itsScrollBeginning
= 0;
403 if (itsBrowsedDir
!= dir
)
413 parent
.type
= itDirectory
;
420 GetLocalDirectory(list
, itsBrowsedDir
, false);
422 Mpd
.GetDirectory(dir
, [&list
](MPD::Item
&&item
) {
423 list
.push_back(item
);
426 list
= Mpd
.GetDirectory(dir
);
428 if (!isLocal()) // local directory is already sorted
429 std::sort(list
.begin(), list
.end(),
430 LocaleBasedItemSorting(std::locale(), Config
.ignore_leading_the
, Config
.browser_sort_mode
));
432 for (MPD::ItemList::iterator it
= list
.begin(); it
!= list
.end(); ++it
)
443 if (it
->name
== subdir
)
444 highlightme
= w
.size();
451 for (size_t i
= 0; i
< myPlaylist
->main().size(); ++i
)
453 if (myPlaylist
->main().at(i
).value().getHash() == it
->song
->getHash())
459 w
.addItem(*it
, bold
);
464 if (highlightme
>= 0)
465 w
.highlight(highlightme
);
469 void Browser::GetLocalDirectory(MPD::ItemList
&v
, const std::string
&directory
, bool recursively
) const
471 namespace fs
= boost::filesystem
;
473 size_t start_size
= v
.size();
474 fs::path
dir(directory
);
475 std::for_each(fs::directory_iterator(dir
), fs::directory_iterator(), [&](fs::directory_entry
&e
) {
476 if (!Config
.local_browser_show_hidden_files
&& e
.path().filename().native()[0] == '.')
479 if (fs::is_directory(e
))
483 GetLocalDirectory(v
, e
.path().native(), true);
484 start_size
= v
.size();
488 item
.type
= itDirectory
;
489 item
.name
= e
.path().native();
493 else if (hasSupportedExtension(e
.path().native()))
496 mpd_pair file_pair
= { "file", e
.path().native().c_str() };
497 MPD::MutableSong
*s
= new MPD::MutableSong(mpd_song_begin(&file_pair
));
498 item
.song
= std::shared_ptr
<MPD::Song
>(s
);
499 # ifdef HAVE_TAGLIB_H
502 # endif // HAVE_TAGLIB_H
507 std::sort(v
.begin()+start_size
, v
.end(), LocaleBasedItemSorting(std::locale(),
508 Config
.ignore_leading_the
, Config
.browser_sort_mode
));
511 void Browser::ClearDirectory(const std::string
&path
) const
513 DIR *dir
= opendir(path
.c_str());
518 struct stat file_stat
;
519 std::string full_path
;
521 while ((file
= readdir(dir
)))
524 if (file
->d_name
[0] == '.' && (file
->d_name
[1] == '\0' || (file
->d_name
[1] == '.' && file
->d_name
[2] == '\0')))
528 if (*full_path
.rbegin() != '/')
530 full_path
+= file
->d_name
;
531 lstat(full_path
.c_str(), &file_stat
);
532 if (S_ISDIR(file_stat
.st_mode
))
533 ClearDirectory(full_path
);
534 if (remove(full_path
.c_str()) == 0)
536 const char msg
[] = "Deleting \"%ls\"...";
537 Statusbar::msg(msg
, wideShorten(ToWString(full_path
), COLS
-const_strlen(msg
)).c_str());
541 const char msg
[] = "Couldn't remove \"%ls\": %s";
542 Statusbar::msg(msg
, wideShorten(ToWString(full_path
), COLS
-const_strlen(msg
)-25).c_str(), strerror(errno
));
548 void Browser::ChangeBrowseMode()
550 if (Mpd
.GetHostname()[0] != '/')
552 Statusbar::msg("For browsing local filesystem connection to MPD via UNIX Socket is required");
556 itsBrowseLocally
= !itsBrowseLocally
;
557 Statusbar::msg("Browse mode: %s", itsBrowseLocally
? "Local filesystem" : "MPD database");
558 itsBrowsedDir
= itsBrowseLocally
? Config
.GetHomeDirectory() : "/";
559 if (itsBrowseLocally
&& *itsBrowsedDir
.rbegin() == '/')
560 itsBrowsedDir
.resize(itsBrowsedDir
.length()-1);
562 GetDirectory(itsBrowsedDir
);
566 bool Browser::deleteItem(const MPD::Item
&item
)
568 if (isParentDirectory((item
)))
569 FatalError("Parent directory passed to Browser::deleteItem");
571 // playlist created by mpd
572 if (!isLocal() && item
.type
== itPlaylist
&& CurrentDir() == "/")
573 Mpd
.DeletePlaylist(item
.name
);
577 path
= Config
.mpd_music_dir
;
578 path
+= item
.type
== itSong
? item
.song
->getURI() : item
.name
;
580 if (item
.type
== itDirectory
)
581 ClearDirectory(path
);
583 return std::remove(path
.c_str()) == 0;
589 bool hasSupportedExtension(const std::string
&file
)
591 size_t last_dot
= file
.rfind(".");
592 if (last_dot
> file
.length())
595 std::string ext
= boost::locale::to_lower(file
.substr(last_dot
+1));
596 return SupportedExtensions
.find(ext
) != SupportedExtensions
.end();
599 std::string
ItemToString(const MPD::Item
&item
)
604 case MPD::itDirectory
:
605 result
= "[" + getBasename(item
.name
) + "]";
608 if (Config
.columns_in_browser
)
609 result
= item
.song
->toString(Config
.song_in_columns_to_string_format
, Config
.tags_separator
);
611 result
= item
.song
->toString(Config
.song_list_format_dollar_free
, Config
.tags_separator
);
613 case MPD::itPlaylist
:
614 result
= Config
.browser_playlist_prefix
.str() + getBasename(item
.name
);
620 bool BrowserEntryMatcher(const boost::regex
&rx
, const MPD::Item
&item
, bool filter
)
622 if (Browser::isParentDirectory(item
))
624 return boost::regex_search(ItemToString(item
), rx
);