1 /***************************************************************************
2  *   Copyright (C) 2008-2021 by Andrzej Rybczak                            *
3  *   andrzej@rybczak.net                                                   *
4  *                                                                         *
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.                                   *
9  *                                                                         *
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.                          *
14  *                                                                         *
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  ***************************************************************************/
20 
21 #include <algorithm>
22 #include <boost/algorithm/string/predicate.hpp>
23 #include <boost/filesystem.hpp>
24 #include <boost/locale/conversion.hpp>
25 #include <time.h>
26 
27 #include "screens/browser.h"
28 #include "charset.h"
29 #include "display.h"
30 #include "global.h"
31 #include "helpers.h"
32 #include "screens/playlist.h"
33 #include "curses/menu_impl.h"
34 #include "screens/screen_switcher.h"
35 #include "settings.h"
36 #include "status.h"
37 #include "statusbar.h"
38 #include "screens/tag_editor.h"
39 #include "title.h"
40 #include "tags.h"
41 #include "format_impl.h"
42 #include "helpers/song_iterator_maker.h"
43 #include "utility/comparators.h"
44 #include "utility/string.h"
45 #include "configuration.h"
46 
47 using Global::MainHeight;
48 using Global::MainStartY;
49 using Global::myScreen;
50 
51 namespace fs = boost::filesystem;
52 namespace ph = std::placeholders;
53 
54 Browser *myBrowser;
55 
56 namespace {
57 
58 std::set<std::string> lm_supported_extensions;
59 
60 std::string realPath(bool local_browser, std::string path);
61 bool isStringParentDirectory(const std::string &directory);
62 bool isItemParentDirectory(const MPD::Item &item);
63 bool isRootDirectory(const std::string &directory);
64 bool isHidden(const fs::directory_iterator &entry);
65 bool hasSupportedExtension(const fs::directory_entry &entry);
66 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags);
67 void getLocalDirectory(NC::Menu<MPD::Item> &menu, const std::string &directory);
68 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs,
69                                   const std::string &directory);
70 void clearDirectory(const std::string &directory);
71 
72 std::string itemToString(const MPD::Item &item);
73 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter);
74 
75 }
76 
77 template <>
78 struct SongPropertiesExtractor<MPD::Item>
79 {
80 	template <typename ItemT>
operator ()SongPropertiesExtractor81 	auto &operator()(ItemT &item) const
82 	{
83 		auto s = item.value().type() == MPD::Item::Type::Song
84 			? &item.value().song()
85 			: nullptr;
86 		m_cache.assign(&item.properties(), s);
87 		return m_cache;
88 	}
89 
90 private:
91 	mutable SongProperties m_cache;
92 };
93 
currentS()94 SongIterator BrowserWindow::currentS()
95 {
96 	return makeSongIterator(current());
97 }
98 
currentS() const99 ConstSongIterator BrowserWindow::currentS() const
100 {
101 	return makeConstSongIterator(current());
102 }
103 
beginS()104 SongIterator BrowserWindow::beginS()
105 {
106 	return makeSongIterator(begin());
107 }
108 
beginS() const109 ConstSongIterator BrowserWindow::beginS() const
110 {
111 	return makeConstSongIterator(begin());
112 }
113 
endS()114 SongIterator BrowserWindow::endS()
115 {
116 	return makeSongIterator(end());
117 }
118 
endS() const119 ConstSongIterator BrowserWindow::endS() const
120 {
121 	return makeConstSongIterator(end());
122 }
123 
getSelectedSongs()124 std::vector<MPD::Song> BrowserWindow::getSelectedSongs()
125 {
126 	return {}; // TODO
127 }
128 
129 /**********************************************************************/
130 
Browser()131 Browser::Browser()
132 : m_redraw_header(false)
133 , m_update_request(true)
134 , m_local_browser(false)
135 , m_scroll_beginning(0)
136 , m_current_directory("/")
137 {
138 	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());
139 	setHighlightFixes(w);
140 	w.cyclicScrolling(Config.use_cyclic_scrolling);
141 	w.centeredCursor(Config.centered_cursor);
142 	w.setSelectedPrefix(Config.selected_item_prefix);
143 	w.setSelectedSuffix(Config.selected_item_suffix);
144 	w.setItemDisplayer(std::bind(Display::Items, ph::_1, std::cref(w)));
145 }
146 
resize()147 void Browser::resize()
148 {
149 	size_t x_offset, width;
150 	getWindowResizeParams(x_offset, width);
151 	w.resize(width, MainHeight);
152 	w.moveTo(x_offset, MainStartY);
153 	switch (Config.browser_display_mode)
154 	{
155 		case DisplayMode::Columns:
156 			if (Config.titles_visibility)
157 				w.setTitle(Display::Columns(w.getWidth()));
158 			break;
159 		case DisplayMode::Classic:
160 			w.setTitle("");
161 			break;
162 	}
163 	hasToBeResized = 0;
164 }
165 
switchTo()166 void Browser::switchTo()
167 {
168 	SwitchTo::execute(this);
169 	m_redraw_header = true;
170 }
171 
title()172 std::wstring Browser::title()
173 {
174 	std::wstring result = L"Browse: ";
175 	result += Scroller(ToWString(m_current_directory), m_scroll_beginning, COLS-result.length()-(Config.design == Design::Alternative ? 2 : Global::VolumeState.length()));
176 	return result;
177 }
178 
update()179 void Browser::update()
180 {
181 	if (m_update_request)
182 	{
183 		m_update_request = false;
184 		do
185 		{
186 			try
187 			{
188 				getDirectory(m_current_directory);
189 				w.refresh();
190 			}
191 			catch (MPD::ServerError &err)
192 			{
193 				// If current directory doesn't exist, try getting its
194 				// parent until we either succeed or reach the root.
195 				if (err.code() == MPD_SERVER_ERROR_NO_EXIST)
196 					m_current_directory = getParentDirectory(m_current_directory);
197 				else
198 					throw;
199 			}
200 		}
201 		while (w.empty() && !inRootDirectory());
202 	}
203 	if (m_redraw_header)
204 	{
205 		drawHeader();
206 		m_redraw_header = false;
207 	}
208 }
209 
mouseButtonPressed(MEVENT me)210 void Browser::mouseButtonPressed(MEVENT me)
211 {
212 	if (w.empty() || !w.hasCoords(me.x, me.y) || size_t(me.y) >= w.size())
213 		return;
214 	if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
215 	{
216 		w.Goto(me.y);
217 		switch (w.current()->value().type())
218 		{
219 			case MPD::Item::Type::Directory:
220 				if (me.bstate & BUTTON1_PRESSED)
221 					enterDirectory();
222 				else
223 					addItemToPlaylist(false);
224 				break;
225 			case MPD::Item::Type::Playlist:
226 			case MPD::Item::Type::Song:
227 			{
228 				bool play = me.bstate & BUTTON3_PRESSED;
229 				addItemToPlaylist(play);
230 				break;
231 			}
232 		}
233 	}
234 	else
235 		Screen<WindowType>::mouseButtonPressed(me);
236 }
237 
238 /***********************************************************************/
239 
allowsSearching()240 bool Browser::allowsSearching()
241 {
242 	return true;
243 }
244 
searchConstraint()245 const std::string &Browser::searchConstraint()
246 {
247 	return m_search_predicate.constraint();
248 }
249 
setSearchConstraint(const std::string & constraint)250 void Browser::setSearchConstraint(const std::string &constraint)
251 {
252 	m_search_predicate = Regex::Filter<MPD::Item>(
253 		constraint,
254 		Config.regex_type,
255 		std::bind(browserEntryMatcher, ph::_1, ph::_2, false));
256 }
257 
clearSearchConstraint()258 void Browser::clearSearchConstraint()
259 {
260 	m_search_predicate.clear();
261 }
262 
search(SearchDirection direction,bool wrap,bool skip_current)263 bool Browser::search(SearchDirection direction, bool wrap, bool skip_current)
264 {
265 	return ::search(w, m_search_predicate, direction, wrap, skip_current);
266 }
267 
268 /***********************************************************************/
269 
allowsFiltering()270 bool Browser::allowsFiltering()
271 {
272 	return allowsSearching();
273 }
274 
currentFilter()275 std::string Browser::currentFilter()
276 {
277 	std::string result;
278 	if (auto pred = w.filterPredicate<Regex::Filter<MPD::Item>>())
279 		result = pred->constraint();
280 	return result;
281 }
282 
applyFilter(const std::string & constraint)283 void Browser::applyFilter(const std::string &constraint)
284 {
285 	if (!constraint.empty())
286 	{
287 		w.applyFilter(Regex::Filter<MPD::Item>(
288 			              constraint,
289 			              Config.regex_type,
290 			              std::bind(browserEntryMatcher, ph::_1, ph::_2, true)));
291 	}
292 	else
293 		w.clearFilter();
294 }
295 
296 
297 /***********************************************************************/
298 
itemAvailable()299 bool Browser::itemAvailable()
300 {
301 	return !w.empty()
302 		// ignore parent directory
303 		&& !isParentDirectory(w.current()->value());
304 }
305 
addItemToPlaylist(bool play)306 bool Browser::addItemToPlaylist(bool play)
307 {
308 	bool success = false;
309 
310 	auto tryToPlay = [] {
311 		// Cheap trick that might fail in presence of multiple clients modifying the
312 		// playlist at the same time, but oh well, this approach correctly loads cue
313 		// playlists and is much faster in general as it doesn't require fetching
314 		// song data.
315 		try
316 		{
317 			Mpd.Play(Status::State::playlistLength());
318 		}
319 		catch (MPD::ServerError &e)
320 		{
321 			// If not bad index, rethrow.
322 			if (e.code() != MPD_SERVER_ERROR_ARG)
323 				throw;
324 		}
325 	};
326 
327 	const MPD::Item &item = w.current()->value();
328 	switch (item.type())
329 	{
330 		case MPD::Item::Type::Directory:
331 		{
332 			if (m_local_browser)
333 			{
334 				std::vector<MPD::Song> songs;
335 				getLocalDirectoryRecursively(songs, item.directory().path());
336 				success = addSongsToPlaylist(songs.begin(), songs.end(), play, -1);
337 			}
338 			else
339 			{
340 				success = Mpd.Add(item.directory().path());
341 				if (play)
342 					tryToPlay();
343 			}
344 			Statusbar::printf("Directory \"%1%\" added%2%",
345 				item.directory().path(), withErrors(success));
346 			break;
347 		}
348 		case MPD::Item::Type::Song:
349 			success = addSongToPlaylist(item.song(), play);
350 			break;
351 		case MPD::Item::Type::Playlist:
352 			success = Mpd.LoadPlaylist(item.playlist().path());
353 			if (play)
354 				tryToPlay();
355 			if (success)
356 				Statusbar::printf("Playlist \"%1%\" loaded", item.playlist().path());
357 			break;
358 	}
359 	return success;
360 }
361 
getSelectedSongs()362 std::vector<MPD::Song> Browser::getSelectedSongs()
363 {
364 	std::vector<MPD::Song> songs;
365 	auto item_handler = [this, &songs](const MPD::Item &item) {
366 		switch (item.type())
367 		{
368 			case MPD::Item::Type::Directory:
369 				if (m_local_browser)
370 					getLocalDirectoryRecursively(songs, item.directory().path());
371 				else
372 				{
373 					std::copy(
374 						std::make_move_iterator(Mpd.GetDirectoryRecursive(item.directory().path())),
375 						std::make_move_iterator(MPD::SongIterator()),
376 						std::back_inserter(songs)
377 					);
378 				}
379 				break;
380 			case MPD::Item::Type::Song:
381 				songs.push_back(item.song());
382 				break;
383 			case MPD::Item::Type::Playlist:
384 				std::copy(
385 					std::make_move_iterator(Mpd.GetPlaylistContent(item.playlist().path())),
386 					std::make_move_iterator(MPD::SongIterator()),
387 					std::back_inserter(songs)
388 				);
389 				break;
390 		}
391 	};
392 	for (const auto &item : w)
393 		if (item.isSelected())
394 			item_handler(item.value());
395 	// if no item is selected, add current one
396 	if (songs.empty() && !w.empty())
397 		item_handler(w.current()->value());
398 	return songs;
399 }
400 
401 /***********************************************************************/
402 
inRootDirectory()403 bool Browser::inRootDirectory()
404 {
405 	return isRootDirectory(m_current_directory);
406 }
407 
isParentDirectory(const MPD::Item & item)408 bool Browser::isParentDirectory(const MPD::Item &item)
409 {
410 	return isItemParentDirectory(item);
411 }
412 
currentDirectory()413 const std::string& Browser::currentDirectory()
414 {
415 	return m_current_directory;
416 }
417 
locateSong(const MPD::Song & s)418 void Browser::locateSong(const MPD::Song &s)
419 {
420 	if (s.getDirectory().empty())
421 		throw std::runtime_error("Song's directory is empty");
422 
423 	m_local_browser = !s.isFromDatabase();
424 
425 	if (myScreen != this)
426 		switchTo();
427 
428 	w.clearFilter();
429 
430 	try
431 	{
432 		// Try to change to relevant directory.
433 		if (m_current_directory != s.getDirectory())
434 			getDirectory(s.getDirectory());
435 
436 		// Highlight the item.
437 		auto begin = w.beginV(), end = w.endV();
438 		auto it = std::find(begin, end, MPD::Item(s));
439 		if (it != end)
440 			w.highlight(it-begin);
441 	}
442 	catch (MPD::ServerError &err)
443 	{
444 		// If the directory is invalid, recover by invoking the update.
445 		if (err.code() == MPD_SERVER_ERROR_NO_EXIST)
446 			requestUpdate();
447 		else
448 			throw;
449 	}
450 }
451 
enterDirectory()452 bool Browser::enterDirectory()
453 {
454 	bool result = false;
455 	if (!w.empty())
456 	{
457 		const auto &item = w.current()->value();
458 		if (item.type() == MPD::Item::Type::Directory)
459 		{
460 			getDirectory(item.directory().path());
461 			result = true;
462 		}
463 	}
464 	return result;
465 }
466 
getDirectory(std::string directory)467 void Browser::getDirectory(std::string directory)
468 {
469 	{
470 		ScopedUnfilteredMenu<MPD::Item> sunfilter(ReapplyFilter::Yes, w);
471 
472 		m_scroll_beginning = 0;
473 		w.clear();
474 
475 		// Reset the position if we change directories.
476 		if (m_current_directory != directory)
477 		{
478 			w.reset();
479 			m_redraw_header = true;
480 		}
481 
482 		// Check if it's a parent directory.
483 		if (isStringParentDirectory(directory))
484 		{
485 			directory.resize(directory.length()-3);
486 			directory = getParentDirectory(directory);
487 		}
488 		// When we go down to root, it can be empty.
489 		if (directory.empty())
490 			directory = "/";
491 
492 		bool is_root = isRootDirectory(directory);
493 		// If the requested directory is not root, add parent directory.
494 		if (!is_root)
495 		{
496 			// Make it so that display function doesn't have to handle special cases.
497 			w.addItem(MPD::Directory(directory + "/.."), NC::List::Properties::None);
498 		}
499 
500 		if (m_local_browser)
501 		{
502 			getLocalDirectory(w, directory);
503 			if (Config.browser_sort_mode == SortMode::None
504 			    || Config.browser_sort_mode == SortMode::Type)
505 			{
506 				Statusbar::print("Switching to sorting songs by name for the local browser");
507 				Config.browser_sort_mode = SortMode::Name;
508 			}
509 		}
510 		else
511 		{
512 			MPD::ItemIterator end;
513 			for (auto dir = Mpd.GetDirectory(directory); dir != end; ++dir)
514 				w.addItem(std::move(*dir));
515 		}
516 
517 		if (Config.browser_sort_mode != SortMode::None)
518 		{
519 			std::stable_sort(
520 				w.begin() + (is_root ? 0 : 1), w.end(),
521 				LocaleBasedItemSorting(std::locale(), Config.ignore_leading_the,
522 				                       Config.browser_sort_mode));
523 		}
524 	}
525 
526 	for (size_t i = 0; i < w.size(); ++i)
527 	{
528 		if (w[i].value().type() == MPD::Item::Type::Directory
529 		    && w[i].value().directory().path() == m_current_directory)
530 		{
531 			w.highlight(i);
532 			break;
533 		}
534 	}
535 	m_current_directory = directory;
536 }
537 
changeBrowseMode()538 void Browser::changeBrowseMode()
539 {
540 	if (Mpd.GetHostname()[0] != '/')
541 	{
542 		Statusbar::print("For browsing local filesystem connection to MPD via UNIX Socket is required");
543 		return;
544 	}
545 
546 	m_local_browser = !m_local_browser;
547 	Statusbar::printf("Browse mode: %1%",
548 		m_local_browser ? "local filesystem" : "MPD database"
549 	);
550 	if (m_local_browser)
551 	{
552 		m_current_directory = "~";
553 		expand_home(m_current_directory);
554 	}
555 	else
556 		m_current_directory = "/";
557 	w.reset();
558 	getDirectory(m_current_directory);
559 }
560 
remove(const MPD::Item & item)561 void Browser::remove(const MPD::Item &item)
562 {
563 	if (!Config.allow_for_physical_item_deletion)
564 		throw std::runtime_error("physical deletion is forbidden");
565 	if (isParentDirectory((item)))
566 		throw std::runtime_error("deletion of parent directory is forbidden");
567 
568 	std::string path;
569 	switch (item.type())
570 	{
571 		case MPD::Item::Type::Directory:
572 			path = realPath(m_local_browser, item.directory().path());
573 			clearDirectory(path);
574 			fs::remove(path);
575 			break;
576 		case MPD::Item::Type::Song:
577 			path = realPath(m_local_browser, item.song().getURI());
578 			fs::remove(path);
579 			break;
580 		case MPD::Item::Type::Playlist:
581 			path = item.playlist().path();
582 			try {
583 				Mpd.DeletePlaylist(path);
584 			} catch (MPD::ServerError &e) {
585 				// if there is no such mpd playlist, it's a local one
586 				if (e.code() == MPD_SERVER_ERROR_NO_EXIST)
587 				{
588 					path = realPath(m_local_browser, std::move(path));
589 					fs::remove(path);
590 				}
591 				else
592 					throw;
593 			}
594 			break;
595 	}
596 }
597 
598 /***********************************************************************/
599 
fetchSupportedExtensions()600 void Browser::fetchSupportedExtensions()
601 {
602 	lm_supported_extensions.clear();
603 	MPD::StringIterator extension = Mpd.GetSupportedExtensions(), end;
604 	for (; extension != end; ++extension)
605 		lm_supported_extensions.insert("." + std::move(*extension));
606 }
607 
608 /***********************************************************************/
609 
610 namespace {
611 
realPath(bool local_browser,std::string path)612 std::string realPath(bool local_browser, std::string path)
613 {
614 	if (!local_browser)
615 		path = Config.mpd_music_dir + path;
616 	return path;
617 }
618 
isStringParentDirectory(const std::string & directory)619 bool isStringParentDirectory(const std::string &directory)
620 {
621 	return boost::algorithm::ends_with(directory, "/..");
622 }
623 
isItemParentDirectory(const MPD::Item & item)624 bool isItemParentDirectory(const MPD::Item &item)
625 {
626 	return item.type() == MPD::Item::Type::Directory
627 	    && isStringParentDirectory(item.directory().path());
628 }
629 
isRootDirectory(const std::string & directory)630 bool isRootDirectory(const std::string &directory)
631 {
632 	return directory == "/";
633 }
634 
isHidden(const fs::directory_iterator & entry)635 bool isHidden(const fs::directory_iterator &entry)
636 {
637 	return entry->path().filename().native()[0] == '.';
638 }
639 
hasSupportedExtension(const fs::directory_entry & entry)640 bool hasSupportedExtension(const fs::directory_entry &entry)
641 {
642 	return lm_supported_extensions.find(entry.path().extension().native())
643 	    != lm_supported_extensions.end();
644 }
645 
getLocalSong(const fs::directory_entry & entry,bool read_tags)646 MPD::Song getLocalSong(const fs::directory_entry &entry, bool read_tags)
647 {
648 	mpd_pair pair = { "file", entry.path().c_str() };
649 	mpd_song *s = mpd_song_begin(&pair);
650 	if (s == nullptr)
651 		throw std::runtime_error("invalid path: " + entry.path().native());
652 	if (read_tags)
653 	{
654 #ifdef HAVE_TAGLIB_H
655 		Tags::setAttribute(s, "Last-Modified",
656 			timeFormat("%Y-%m-%dT%H:%M:%SZ", fs::last_write_time(entry.path()))
657 		);
658 		// read tags
659 		Tags::read(s);
660 #endif // HAVE_TAGLIB_H
661 	}
662 	return s;
663 }
664 
getLocalDirectory(NC::Menu<MPD::Item> & menu,const std::string & directory)665 void getLocalDirectory(NC::Menu<MPD::Item> &menu, const std::string &directory)
666 {
667 	for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
668 	{
669 		if (!Config.local_browser_show_hidden_files && isHidden(entry))
670 			continue;
671 
672 	if (fs::is_directory(*entry))
673 	{
674 		menu.addItem(MPD::Directory(entry->path().native(),
675 		                            fs::last_write_time(entry->path())));
676 	}
677 	else if (hasSupportedExtension(*entry))
678 		menu.addItem(getLocalSong(*entry, true));
679 	}
680 }
681 
getLocalDirectoryRecursively(std::vector<MPD::Song> & songs,const std::string & directory)682 void getLocalDirectoryRecursively(std::vector<MPD::Song> &songs, const std::string &directory)
683 {
684 	size_t sort_offset = songs.size();
685 	for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
686 	{
687 		if (!Config.local_browser_show_hidden_files && isHidden(entry))
688 			continue;
689 
690 		if (fs::is_directory(*entry))
691 		{
692 			getLocalDirectoryRecursively(songs, entry->path().native());
693 			sort_offset = songs.size();
694 		}
695 		else if (hasSupportedExtension(*entry))
696 			songs.push_back(getLocalSong(*entry, false));
697 	};
698 
699 	if (Config.browser_sort_mode != SortMode::None)
700 	{
701 		std::stable_sort(songs.begin()+sort_offset, songs.end(),
702 		                 LocaleBasedSorting(std::locale(), Config.ignore_leading_the)
703 		);
704 	}
705 }
706 
clearDirectory(const std::string & directory)707 void clearDirectory(const std::string &directory)
708 {
709 	for (fs::directory_iterator entry(directory), end; entry != end; ++entry)
710 	{
711 		if (!fs::is_symlink(*entry) && fs::is_directory(*entry))
712 			clearDirectory(entry->path().native());
713 		const char msg[] = "Deleting \"%1%\"...";
714 		Statusbar::printf(msg, wideShorten(entry->path().native(), COLS-const_strlen(msg)));
715 		fs::remove(entry->path());
716 	};
717 }
718 
719 /***********************************************************************/
720 
itemToString(const MPD::Item & item)721 std::string itemToString(const MPD::Item &item)
722 {
723 	std::string result;
724 	switch (item.type())
725 	{
726 		case MPD::Item::Type::Directory:
727 			result = "[" + getBasename(item.directory().path()) + "]";
728 			break;
729 		case MPD::Item::Type::Song:
730 			switch (Config.browser_display_mode)
731 			{
732 				case DisplayMode::Classic:
733 					result = Format::stringify<char>(Config.song_list_format, &item.song());
734 					break;
735 				case DisplayMode::Columns:
736 					result = Format::stringify<char>(Config.song_columns_mode_format, &item.song());
737 					break;
738 			}
739 			break;
740 		case MPD::Item::Type::Playlist:
741 			result = Config.browser_playlist_prefix.str();
742 			result += getBasename(item.playlist().path());
743 			break;
744 	}
745 	return result;
746 }
747 
browserEntryMatcher(const Regex::Regex & rx,const MPD::Item & item,bool filter)748 bool browserEntryMatcher(const Regex::Regex &rx, const MPD::Item &item, bool filter)
749 {
750 	if (isItemParentDirectory(item))
751 		return filter;
752 	return Regex::search(itemToString(item), rx, Config.ignore_diacritics);
753 }
754 
755 }
756