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/locale/conversion.hpp>
35 #include "regex_filter.h"
36 #include "screen_switcher.h"
39 #include "statusbar.h"
40 #include "tag_editor.h"
43 #include "utility/comparators.h"
44 #include "utility/string.h"
46 using namespace std::placeholders
;
48 using Global::MainHeight
;
49 using Global::MainStartY
;
50 using Global::myScreen
;
52 using MPD::itDirectory
;
54 using MPD::itPlaylist
;
60 std::set
<std::string
> SupportedExtensions
;
61 bool hasSupportedExtension(const std::string
&file
);
63 std::string
ItemToString(const MPD::Item
&item
);
64 bool BrowserEntryMatcher(const boost::regex
&rx
, const MPD::Item
&item
, bool filter
);
68 Browser::Browser() : itsBrowseLocally(0), itsScrollBeginning(0), itsBrowsedDir("/")
70 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
);
71 w
.setHighlightColor(Config
.main_highlight_color
);
72 w
.cyclicScrolling(Config
.use_cyclic_scrolling
);
73 w
.centeredCursor(Config
.centered_cursor
);
74 w
.setSelectedPrefix(Config
.selected_item_prefix
);
75 w
.setSelectedSuffix(Config
.selected_item_suffix
);
76 w
.setItemDisplayer(std::bind(Display::Items
, _1
, proxySongList()));
79 void Browser::resize()
81 size_t x_offset
, width
;
82 getWindowResizeParams(x_offset
, width
);
83 w
.resize(width
, MainHeight
);
84 w
.moveTo(x_offset
, MainStartY
);
85 w
.setTitle(Config
.columns_in_browser
&& Config
.titles_visibility
? Display::Columns(w
.getWidth()) : "");
89 void Browser::switchTo()
91 SwitchTo::execute(this);
93 // local browser doesn't support sorting by mtime
94 if (isLocal() && Config
.browser_sort_mode
== smMTime
)
95 Config
.browser_sort_mode
= smName
;
98 GetDirectory(itsBrowsedDir
);
100 markSongsInPlaylist(proxySongList());
105 std::wstring
Browser::title()
107 std::wstring result
= L
"Browse: ";
108 result
+= Scroller(ToWString(itsBrowsedDir
), itsScrollBeginning
, COLS
-result
.length()-(Config
.new_design
? 2 : Global::VolumeState
.length()));
112 void Browser::enterPressed()
117 const MPD::Item
&item
= w
.current().value();
122 if (isParentDirectory(item
))
123 GetDirectory(getParentDirectory(itsBrowsedDir
), itsBrowsedDir
);
125 GetDirectory(item
.name
, itsBrowsedDir
);
131 addSongToPlaylist(*item
.song
, true, -1);
136 Mpd
.LoadPlaylist(item
.name
);
137 Statusbar::msg("Playlist \"%s\" loaded", item
.name
.c_str());
138 myPlaylist
->PlayNewlyAddedSongs();
143 void Browser::spacePressed()
148 size_t i
= itsBrowsedDir
!= "/" ? 1 : 0;
149 if (Config
.space_selects
&& w
.choice() >= i
)
152 w
.at(i
).setSelected(!w
.at(i
).isSelected());
153 w
.scroll(NC::Scroll::Down
);
157 const MPD::Item
&item
= w
.current().value();
159 if (isParentDirectory(item
))
171 Statusbar::msg("Scanning directory \"%s\"...", item
.name
.c_str());
172 myBrowser
->GetLocalDirectory(items
, item
.name
, 1);
173 list
.reserve(items
.size());
174 for (MPD::ItemList::const_iterator it
= items
.begin(); it
!= items
.end(); ++it
)
175 list
.push_back(*it
->song
);
176 addSongsToPlaylist(list
, false, -1);
181 Statusbar::msg("Directory \"%s\" added", item
.name
.c_str());
186 addSongToPlaylist(*item
.song
, false);
191 Mpd
.LoadPlaylist(item
.name
);
192 Statusbar::msg("Playlist \"%s\" loaded", item
.name
.c_str());
196 w
.scroll(NC::Scroll::Down
);
199 void Browser::mouseButtonPressed(MEVENT me
)
201 if (w
.empty() || !w
.hasCoords(me
.x
, me
.y
) || size_t(me
.y
) >= w
.size())
203 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
206 switch (w
.current().value().type
)
209 if (me
.bstate
& BUTTON1_PRESSED
)
211 GetDirectory(w
.current().value().name
);
216 size_t pos
= w
.choice();
218 if (pos
< w
.size()-1)
219 w
.scroll(NC::Scroll::Up
);
224 if (me
.bstate
& BUTTON1_PRESSED
)
226 size_t pos
= w
.choice();
228 if (pos
< w
.size()-1)
229 w
.scroll(NC::Scroll::Up
);
237 Screen
<WindowType
>::mouseButtonPressed(me
);
240 /***********************************************************************/
242 bool Browser::allowsFiltering()
247 std::string
Browser::currentFilter()
249 return RegexFilter
<MPD::Item
>::currentFilter(w
);
252 void Browser::applyFilter(const std::string
&filter
)
257 auto fun
= std::bind(BrowserEntryMatcher
, _1
, _2
, true);
258 auto rx
= RegexFilter
<MPD::Item
>(
259 boost::regex(filter
, Config
.regex_type
), fun
);
260 w
.filter(w
.begin(), w
.end(), rx
);
262 catch (boost::bad_expression
&) { }
265 /***********************************************************************/
267 bool Browser::allowsSearching()
272 bool Browser::search(const std::string
&constraint
)
276 auto fun
= std::bind(BrowserEntryMatcher
, _1
, _2
, false);
277 auto rx
= RegexFilter
<MPD::Item
>(
278 boost::regex(constraint
, Config
.regex_type
), fun
);
279 return w
.search(w
.begin(), w
.end(), rx
);
281 catch (boost::bad_expression
&)
287 void Browser::nextFound(bool wrap
)
292 void Browser::prevFound(bool wrap
)
297 /***********************************************************************/
299 ProxySongList
Browser::proxySongList()
301 return ProxySongList(w
, [](NC::Menu
<MPD::Item
>::Item
&item
) -> MPD::Song
* {
303 if (item
.value().type
== itSong
)
304 ptr
= item
.value().song
.get();
309 bool Browser::allowsSelection()
314 void Browser::reverseSelection()
316 reverseSelectionHelper(w
.begin()+(itsBrowsedDir
== "/" ? 0 : 1), w
.end());
319 MPD::SongList
Browser::getSelectedSongs()
321 MPD::SongList result
;
322 auto item_handler
= [this, &result
](const MPD::Item
&item
) {
323 if (item
.type
== itDirectory
)
329 GetLocalDirectory(list
, item
.name
, true);
330 for (auto it
= list
.begin(); it
!= list
.end(); ++it
)
331 result
.push_back(*it
->song
);
336 Mpd
.GetDirectoryRecursive(item
.name
, [&result
](MPD::Song
&&s
) {
341 else if (item
.type
== itSong
)
342 result
.push_back(*item
.song
);
343 else if (item
.type
== itPlaylist
)
345 Mpd
.GetPlaylistContent(item
.name
, [&result
](MPD::Song
&&s
) {
350 for (auto it
= w
.begin(); it
!= w
.end(); ++it
)
351 if (it
->isSelected())
352 item_handler(it
->value());
353 // if no item is selected, add current one
354 if (result
.empty() && !w
.empty())
355 item_handler(w
.current().value());
359 void Browser::fetchSupportedExtensions()
361 SupportedExtensions
.clear();
362 Mpd
.GetSupportedExtensions(SupportedExtensions
);
365 void Browser::LocateSong(const MPD::Song
&s
)
367 if (s
.getDirectory().empty())
370 itsBrowseLocally
= !s
.isFromDatabase();
372 if (myScreen
!= this)
375 if (itsBrowsedDir
!= s
.getDirectory())
376 GetDirectory(s
.getDirectory());
377 for (size_t i
= 0; i
< w
.size(); ++i
)
379 if (w
[i
].value().type
== itSong
&& s
.getHash() == w
[i
].value().song
->getHash())
388 void Browser::GetDirectory(std::string dir
, std::string subdir
)
393 int highlightme
= -1;
394 itsScrollBeginning
= 0;
395 if (itsBrowsedDir
!= dir
)
405 parent
.type
= itDirectory
;
412 GetLocalDirectory(list
);
414 Mpd
.GetDirectory(dir
, [&list
](MPD::Item
&&item
) {
415 list
.push_back(item
);
418 list
= Mpd
.GetDirectory(dir
);
420 if (!isLocal()) // local directory is already sorted
421 std::sort(list
.begin(), list
.end(),
422 LocaleBasedItemSorting(std::locale(), Config
.ignore_leading_the
, Config
.browser_sort_mode
));
424 for (MPD::ItemList::iterator it
= list
.begin(); it
!= list
.end(); ++it
)
435 if (it
->name
== subdir
)
436 highlightme
= w
.size();
443 for (size_t i
= 0; i
< myPlaylist
->main().size(); ++i
)
445 if (myPlaylist
->main().at(i
).value().getHash() == it
->song
->getHash())
451 w
.addItem(*it
, bold
);
456 if (highlightme
>= 0)
457 w
.highlight(highlightme
);
461 void Browser::GetLocalDirectory(MPD::ItemList
&v
, const std::string
&directory
, bool recursively
) const
463 DIR *dir
= opendir((directory
.empty() ? itsBrowsedDir
: directory
).c_str());
470 struct stat file_stat
;
471 std::string full_path
;
473 size_t old_size
= v
.size();
474 while ((file
= readdir(dir
)))
477 if (file
->d_name
[0] == '.' && (file
->d_name
[1] == '\0' || (file
->d_name
[1] == '.' && file
->d_name
[2] == '\0')))
480 if (!Config
.local_browser_show_hidden_files
&& file
->d_name
[0] == '.')
483 full_path
= directory
.empty() ? itsBrowsedDir
: directory
;
484 if (itsBrowsedDir
!= "/")
486 full_path
+= file
->d_name
;
487 stat(full_path
.c_str(), &file_stat
);
488 if (S_ISDIR(file_stat
.st_mode
))
492 GetLocalDirectory(v
, full_path
, 1);
497 new_item
.type
= itDirectory
;
498 new_item
.name
= full_path
;
499 v
.push_back(new_item
);
502 else if (hasSupportedExtension(file
->d_name
))
504 new_item
.type
= itSong
;
505 mpd_pair file_pair
= { "file", full_path
.c_str() };
506 MPD::MutableSong
*s
= new MPD::MutableSong(mpd_song_begin(&file_pair
));
507 new_item
.song
= std::shared_ptr
<MPD::Song
>(s
);
508 # ifdef HAVE_TAGLIB_H
511 # endif // HAVE_TAGLIB_H
512 v
.push_back(new_item
);
516 std::sort(v
.begin()+old_size
, v
.end(),
517 LocaleBasedItemSorting(std::locale(), Config
.ignore_leading_the
, Config
.browser_sort_mode
));
520 void Browser::ClearDirectory(const std::string
&path
) const
522 DIR *dir
= opendir(path
.c_str());
527 struct stat file_stat
;
528 std::string full_path
;
530 while ((file
= readdir(dir
)))
533 if (file
->d_name
[0] == '.' && (file
->d_name
[1] == '\0' || (file
->d_name
[1] == '.' && file
->d_name
[2] == '\0')))
537 if (*full_path
.rbegin() != '/')
539 full_path
+= file
->d_name
;
540 lstat(full_path
.c_str(), &file_stat
);
541 if (S_ISDIR(file_stat
.st_mode
))
542 ClearDirectory(full_path
);
543 if (remove(full_path
.c_str()) == 0)
545 const char msg
[] = "Deleting \"%ls\"...";
546 Statusbar::msg(msg
, wideShorten(ToWString(full_path
), COLS
-const_strlen(msg
)).c_str());
550 const char msg
[] = "Couldn't remove \"%ls\": %s";
551 Statusbar::msg(msg
, wideShorten(ToWString(full_path
), COLS
-const_strlen(msg
)-25).c_str(), strerror(errno
));
557 void Browser::ChangeBrowseMode()
559 if (Mpd
.GetHostname()[0] != '/')
561 Statusbar::msg("For browsing local filesystem connection to MPD via UNIX Socket is required");
565 itsBrowseLocally
= !itsBrowseLocally
;
566 Statusbar::msg("Browse mode: %s", itsBrowseLocally
? "Local filesystem" : "MPD database");
567 itsBrowsedDir
= itsBrowseLocally
? Config
.GetHomeDirectory() : "/";
568 if (itsBrowseLocally
&& *itsBrowsedDir
.rbegin() == '/')
569 itsBrowsedDir
.resize(itsBrowsedDir
.length()-1);
571 GetDirectory(itsBrowsedDir
);
575 bool Browser::deleteItem(const MPD::Item
&item
)
577 if (isParentDirectory((item
)))
578 FatalError("Parent directory passed to Browser::deleteItem");
580 // playlist created by mpd
581 if (!isLocal() && item
.type
== itPlaylist
&& CurrentDir() == "/")
582 Mpd
.DeletePlaylist(item
.name
);
586 path
= Config
.mpd_music_dir
;
587 path
+= item
.type
== itSong
? item
.song
->getURI() : item
.name
;
589 if (item
.type
== itDirectory
)
590 ClearDirectory(path
);
592 return std::remove(path
.c_str()) == 0;
598 bool hasSupportedExtension(const std::string
&file
)
600 size_t last_dot
= file
.rfind(".");
601 if (last_dot
> file
.length())
604 std::string ext
= boost::locale::to_lower(file
.substr(last_dot
+1));
605 return SupportedExtensions
.find(ext
) != SupportedExtensions
.end();
608 std::string
ItemToString(const MPD::Item
&item
)
613 case MPD::itDirectory
:
614 result
= "[" + getBasename(item
.name
) + "]";
617 if (Config
.columns_in_browser
)
618 result
= item
.song
->toString(Config
.song_in_columns_to_string_format
, Config
.tags_separator
);
620 result
= item
.song
->toString(Config
.song_list_format_dollar_free
, Config
.tags_separator
);
622 case MPD::itPlaylist
:
623 result
= Config
.browser_playlist_prefix
.str() + getBasename(item
.name
);
629 bool BrowserEntryMatcher(const boost::regex
&rx
, const MPD::Item
&item
, bool filter
)
631 if (Browser::isParentDirectory(item
))
633 return boost::regex_search(ItemToString(item
), rx
);