1 /***************************************************************************
2 * Copyright (C) 2008-2017 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 ***************************************************************************/
21 #include <boost/date_time/posix_time/posix_time.hpp>
22 #include <boost/locale/conversion.hpp>
31 #include "curses/menu_impl.h"
33 #include "screens/playlist.h"
34 #include "screens/media_library.h"
36 #include "statusbar.h"
37 #include "helpers/song_iterator_maker.h"
38 #include "utility/comparators.h"
39 #include "utility/functional.h"
40 #include "utility/type_conversions.h"
42 #include "screens/screen_switcher.h"
44 using Global::MainHeight
;
45 using Global::MainStartY
;
46 using Global::myScreen
;
48 namespace ph
= std::placeholders
;
50 MediaLibrary
*myLibrary
;
55 size_t itsLeftColStartX
;
56 size_t itsLeftColWidth
;
57 size_t itsMiddleColWidth
;
58 size_t itsMiddleColStartX
;
59 size_t itsRightColWidth
;
60 size_t itsRightColStartX
;
62 typedef MediaLibrary::PrimaryTag PrimaryTag
;
63 typedef MediaLibrary::AlbumEntry AlbumEntry
;
65 MPD::SongIterator
getSongsFromAlbum(const AlbumEntry
&album
)
67 Mpd
.StartSearch(true);
68 Mpd
.AddSearch(Config
.media_lib_primary_tag
, album
.entry().tag());
69 if (!album
.isAllTracksEntry())
71 Mpd
.AddSearch(MPD_TAG_ALBUM
, album
.entry().album());
72 Mpd
.AddSearch(MPD_TAG_DATE
, album
.entry().date());
74 return Mpd
.CommitSearchSongs();
77 std::string
AlbumToString(const AlbumEntry
&ae
);
78 std::string
SongToString(const MPD::Song
&s
);
80 bool TagEntryMatcher(const Regex::Regex
&rx
, const MediaLibrary::PrimaryTag
&tagmtime
);
81 bool AlbumEntryMatcher(const Regex::Regex
&rx
, const NC::Menu
<AlbumEntry
>::Item
&item
, bool filter
);
82 bool SongEntryMatcher(const Regex::Regex
&rx
, const MPD::Song
&s
);
84 bool MoveToTag(NC::Menu
<PrimaryTag
> &tags
, const std::string
&primary_tag
);
85 bool MoveToAlbum(NC::Menu
<AlbumEntry
> &albums
, const std::string
&primary_tag
, const MPD::Song
&s
);
88 typedef NC::Menu
<MPD::Song
>::Item SongItem
;
90 static const std::array
<MPD::Song::GetFunction
, 3> GetFuns
;
92 LocaleStringComparison m_cmp
;
93 std::ptrdiff_t m_offset
;
96 SortSongs(bool disc_only
= false)
97 : m_cmp(std::locale(), Config
.ignore_leading_the
), m_offset(disc_only
? 2 : 0) { }
99 bool operator()(const SongItem
&a
, const SongItem
&b
) {
100 return (*this)(a
.value(), b
.value());
102 bool operator()(const MPD::Song
&a
, const MPD::Song
&b
) {
103 for (auto get
= GetFuns
.begin()+m_offset
; get
!= GetFuns
.end(); ++get
) {
104 int ret
= m_cmp(a
.getTags(*get
),
110 int ret
= boost::lexical_cast
<int>(a
.getTags(&MPD::Song::getTrackNumber
))
111 - boost::lexical_cast
<int>(b
.getTags(&MPD::Song::getTrackNumber
));
113 } catch (boost::bad_lexical_cast
&) {
114 return a
.getTrackNumber() < b
.getTrackNumber();
119 const std::array
<MPD::Song::GetFunction
, 3> SortSongs::GetFuns
= {{
121 &MPD::Song::getAlbum
,
125 class SortAlbumEntries
{
126 typedef MediaLibrary::Album Album
;
128 LocaleStringComparison m_cmp
;
131 SortAlbumEntries() : m_cmp(std::locale(), Config
.ignore_leading_the
) { }
133 bool operator()(const AlbumEntry
&a
, const AlbumEntry
&b
) const {
134 return (*this)(a
.entry(), b
.entry());
137 bool operator()(const Album
&a
, const Album
&b
) const {
138 if (Config
.media_library_sort_by_mtime
)
139 return a
.mtime() > b
.mtime();
143 result
= m_cmp(a
.tag(), b
.tag());
146 result
= m_cmp(a
.date(), b
.date());
149 return m_cmp(a
.album(), b
.album()) < 0;
154 class SortPrimaryTags
{
155 LocaleStringComparison m_cmp
;
158 SortPrimaryTags() : m_cmp(std::locale(), Config
.ignore_leading_the
) { }
160 bool operator()(const PrimaryTag
&a
, const PrimaryTag
&b
) const {
161 if (Config
.media_library_sort_by_mtime
)
162 return a
.mtime() > b
.mtime();
164 return m_cmp(a
.tag(), b
.tag()) < 0;
170 MediaLibrary::MediaLibrary()
171 : m_timer(boost::posix_time::from_time_t(0))
172 , m_window_timeout(Config
.data_fetching_delay
? 250 : BaseScreen::defaultWindowTimeout
)
173 , m_fetching_delay(boost::posix_time::milliseconds(Config
.data_fetching_delay
? 250 : -1))
176 itsLeftColWidth
= COLS
/3-1;
177 itsMiddleColWidth
= COLS
/3;
178 itsMiddleColStartX
= itsLeftColWidth
+1;
179 itsRightColWidth
= COLS
-COLS
/3*2-1;
180 itsRightColStartX
= itsLeftColWidth
+itsMiddleColWidth
+2;
182 Tags
= NC::Menu
<PrimaryTag
>(0, MainStartY
, itsLeftColWidth
, MainHeight
, Config
.titles_visibility
? tagTypeToString(Config
.media_lib_primary_tag
) + "s" : "", Config
.main_color
, NC::Border());
183 Tags
.setHighlightColor(Config
.active_column_color
);
184 Tags
.cyclicScrolling(Config
.use_cyclic_scrolling
);
185 Tags
.centeredCursor(Config
.centered_cursor
);
186 Tags
.setSelectedPrefix(Config
.selected_item_prefix
);
187 Tags
.setSelectedSuffix(Config
.selected_item_suffix
);
188 Tags
.setItemDisplayer([](NC::Menu
<PrimaryTag
> &menu
) {
189 const std::string
&tag
= menu
.drawn()->value().tag();
191 menu
<< Config
.empty_tag
;
193 menu
<< Charset::utf8ToLocale(tag
);
196 Albums
= NC::Menu
<AlbumEntry
>(itsMiddleColStartX
, MainStartY
, itsMiddleColWidth
, MainHeight
, Config
.titles_visibility
? "Albums" : "", Config
.main_color
, NC::Border());
197 Albums
.setHighlightColor(Config
.main_highlight_color
);
198 Albums
.cyclicScrolling(Config
.use_cyclic_scrolling
);
199 Albums
.centeredCursor(Config
.centered_cursor
);
200 Albums
.setSelectedPrefix(Config
.selected_item_prefix
);
201 Albums
.setSelectedSuffix(Config
.selected_item_suffix
);
202 Albums
.setItemDisplayer([](NC::Menu
<AlbumEntry
> &menu
) {
203 menu
<< Charset::utf8ToLocale(AlbumToString(menu
.drawn()->value()));
206 Songs
= NC::Menu
<MPD::Song
>(itsRightColStartX
, MainStartY
, itsRightColWidth
, MainHeight
, Config
.titles_visibility
? "Songs" : "", Config
.main_color
, NC::Border());
207 Songs
.setHighlightColor(Config
.main_highlight_color
);
208 Songs
.cyclicScrolling(Config
.use_cyclic_scrolling
);
209 Songs
.centeredCursor(Config
.centered_cursor
);
210 Songs
.setSelectedPrefix(Config
.selected_item_prefix
);
211 Songs
.setSelectedSuffix(Config
.selected_item_suffix
);
212 Songs
.setItemDisplayer(std::bind(
213 Display::Songs
, ph::_1
, std::cref(Songs
), std::cref(Config
.song_library_format
)
219 void MediaLibrary::resize()
221 size_t x_offset
, width
;
222 getWindowResizeParams(x_offset
, width
);
225 itsLeftColStartX
= x_offset
;
226 itsLeftColWidth
= width
/3-1;
227 itsMiddleColStartX
= itsLeftColStartX
+itsLeftColWidth
+1;
228 itsMiddleColWidth
= width
/3;
229 itsRightColStartX
= itsMiddleColStartX
+itsMiddleColWidth
+1;
230 itsRightColWidth
= width
-width
/3*2-1;
234 itsMiddleColStartX
= x_offset
;
235 itsMiddleColWidth
= width
/2;
236 itsRightColStartX
= x_offset
+itsMiddleColWidth
+1;
237 itsRightColWidth
= width
-itsMiddleColWidth
-1;
240 Tags
.resize(itsLeftColWidth
, MainHeight
);
241 Albums
.resize(itsMiddleColWidth
, MainHeight
);
242 Songs
.resize(itsRightColWidth
, MainHeight
);
244 Tags
.moveTo(itsLeftColStartX
, MainStartY
);
245 Albums
.moveTo(itsMiddleColStartX
, MainStartY
);
246 Songs
.moveTo(itsRightColStartX
, MainStartY
);
251 void MediaLibrary::refresh()
254 drawSeparator(itsMiddleColStartX
-1);
256 drawSeparator(itsRightColStartX
-1);
260 Albums
<< NC::XY(0, 0) << "No albums found.";
261 Albums
.Window::refresh();
265 void MediaLibrary::switchTo()
267 SwitchTo::execute(this);
272 std::wstring
MediaLibrary::title()
274 return L
"Media library";
277 void MediaLibrary::update()
281 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
282 if (Albums
.empty() || m_albums_update_request
)
284 m_albums_update_request
= false;
285 sunfilter_albums
.set(ReapplyFilter::Yes
, true);
286 std::map
<std::tuple
<std::string
, std::string
, std::string
>, time_t> albums
;
287 for (MPD::SongIterator s
= getDatabaseIterator(Mpd
), end
; s
!= end
; ++s
)
291 while (!(tag
= s
->get(Config
.media_lib_primary_tag
, idx
++)).empty())
293 auto key
= std::make_tuple(std::move(tag
), s
->getAlbum(), s
->getDate());
294 auto it
= albums
.find(key
);
295 if (it
== albums
.end())
296 albums
[std::move(key
)] = s
->getMTime();
298 it
->second
= s
->getMTime();
302 for (const auto &album
: albums
)
304 auto entry
= AlbumEntry(
305 Album(std::move(std::get
<0>(album
.first
)),
306 std::move(std::get
<1>(album
.first
)),
307 std::move(std::get
<2>(album
.first
)),
309 if (idx
< Albums
.size())
310 Albums
[idx
].value() = std::move(entry
);
312 Albums
.addItem(std::move(entry
));
315 if (idx
< Albums
.size())
316 Albums
.resizeList(idx
);
317 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
323 ScopedUnfilteredMenu
<PrimaryTag
> sunfilter_tags(ReapplyFilter::No
, Tags
);
324 if (Tags
.empty() || m_tags_update_request
)
326 m_tags_update_request
= false;
327 sunfilter_tags
.set(ReapplyFilter::Yes
, true);
328 std::map
<std::string
, time_t> tags
;
329 if (Config
.media_library_sort_by_mtime
)
331 for (MPD::SongIterator s
= getDatabaseIterator(Mpd
), end
; s
!= end
; ++s
)
335 while (!(tag
= s
->get(Config
.media_lib_primary_tag
, idx
++)).empty())
337 auto it
= tags
.find(tag
);
338 if (it
== tags
.end())
339 tags
[std::move(tag
)] = s
->getMTime();
341 it
->second
= std::max(it
->second
, s
->getMTime());
347 MPD::StringIterator tag
= Mpd
.GetList(Config
.media_lib_primary_tag
), end
;
348 for (; tag
!= end
; ++tag
)
349 tags
[std::move(*tag
)] = 0;
352 for (const auto &tag
: tags
)
354 auto ptag
= PrimaryTag(std::move(tag
.first
), tag
.second
);
355 if (idx
< Tags
.size())
356 Tags
[idx
].value() = std::move(ptag
);
358 Tags
.addItem(std::move(ptag
));
361 if (idx
< Tags
.size())
362 Tags
.resizeList(idx
);
363 std::sort(Tags
.beginV(), Tags
.endV(), SortPrimaryTags());
368 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
370 && ((Albums
.empty() && Global::Timer
- m_timer
> m_fetching_delay
)
371 || m_albums_update_request
))
373 m_albums_update_request
= false;
374 sunfilter_albums
.set(ReapplyFilter::Yes
, true);
375 auto &primary_tag
= Tags
.current()->value().tag();
376 Mpd
.StartSearch(true);
377 Mpd
.AddSearch(Config
.media_lib_primary_tag
, primary_tag
);
378 std::map
<std::tuple
<std::string
, std::string
>, time_t> albums
;
379 for (MPD::SongIterator s
= Mpd
.CommitSearchSongs(), end
; s
!= end
; ++s
)
381 auto key
= std::make_tuple(s
->getAlbum(), s
->getDate());
382 auto it
= albums
.find(key
);
383 if (it
== albums
.end())
384 albums
[std::move(key
)] = s
->getMTime();
386 it
->second
= std::max(it
->second
, s
->getMTime());
389 for (const auto &album
: albums
)
391 auto entry
= AlbumEntry(
393 std::move(std::get
<0>(album
.first
)),
394 std::move(std::get
<1>(album
.first
)),
396 if (idx
< Albums
.size())
398 Albums
[idx
].value() = std::move(entry
);
399 Albums
[idx
].setSeparator(false);
402 Albums
.addItem(std::move(entry
));
405 if (idx
< Albums
.size())
406 Albums
.resizeList(idx
);
407 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
408 if (albums
.size() > 1)
410 Albums
.addSeparator();
411 Albums
.addItem(AlbumEntry::mkAllTracksEntry(primary_tag
));
417 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
419 && ((Songs
.empty() && Global::Timer
- m_timer
> m_fetching_delay
)
420 || m_songs_update_request
))
422 m_songs_update_request
= false;
423 sunfilter_songs
.set(ReapplyFilter::Yes
, true);
424 auto &album
= Albums
.current()->value();
425 Mpd
.StartSearch(true);
426 Mpd
.AddSearch(Config
.media_lib_primary_tag
, album
.entry().tag());
427 if (!album
.isAllTracksEntry())
429 Mpd
.AddSearch(MPD_TAG_ALBUM
, album
.entry().album());
430 Mpd
.AddSearch(MPD_TAG_DATE
, album
.entry().date());
433 for (MPD::SongIterator s
= Mpd
.CommitSearchSongs(), end
; s
!= end
; ++s
, ++idx
)
435 if (idx
< Songs
.size())
436 Songs
[idx
].value() = std::move(*s
);
438 Songs
.addItem(std::move(*s
));
440 if (idx
< Songs
.size())
441 Songs
.resizeList(idx
);
442 std::sort(Songs
.begin(), Songs
.end(), SortSongs(!album
.isAllTracksEntry()));
446 int MediaLibrary::windowTimeout()
448 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
449 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
450 if (Albums
.empty() || Songs
.empty())
451 return m_window_timeout
;
453 return Screen
<WindowType
>::windowTimeout();
456 void MediaLibrary::mouseButtonPressed(MEVENT me
)
458 auto tryNextColumn
= [this]() -> bool {
460 if (!isActiveWindow(Songs
))
462 if (nextColumnAvailable())
469 auto tryPreviousColumn
= [this]() -> bool {
471 if (!isActiveWindow(Tags
))
473 if (previousColumnAvailable())
480 if (Tags
.hasCoords(me
.x
, me
.y
))
482 if (!tryPreviousColumn() || !tryPreviousColumn())
484 if (size_t(me
.y
) < Tags
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
487 if (me
.bstate
& BUTTON3_PRESSED
)
488 addItemToPlaylist(false);
491 Screen
<WindowType
>::mouseButtonPressed(me
);
495 else if (Albums
.hasCoords(me
.x
, me
.y
))
497 if (!isActiveWindow(Albums
))
500 if (isActiveWindow(Tags
))
501 success
= tryNextColumn();
503 success
= tryPreviousColumn();
507 if (size_t(me
.y
) < Albums
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
510 if (me
.bstate
& BUTTON3_PRESSED
)
511 addItemToPlaylist(false);
514 Screen
<WindowType
>::mouseButtonPressed(me
);
517 else if (Songs
.hasCoords(me
.x
, me
.y
))
519 if (!tryNextColumn() || !tryNextColumn())
521 if (size_t(me
.y
) < Songs
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
524 bool play
= me
.bstate
& BUTTON3_PRESSED
;
525 addItemToPlaylist(play
);
528 Screen
<WindowType
>::mouseButtonPressed(me
);
532 /***********************************************************************/
534 bool MediaLibrary::allowsSearching()
539 const std::string
&MediaLibrary::searchConstraint()
541 if (isActiveWindow(Tags
))
542 return m_tags_search_predicate
.constraint();
543 else if (isActiveWindow(Albums
))
544 return m_albums_search_predicate
.constraint();
545 else if (isActiveWindow(Songs
))
546 return m_songs_search_predicate
.constraint();
547 throw std::runtime_error("no active window");
550 void MediaLibrary::setSearchConstraint(const std::string
&constraint
)
552 if (isActiveWindow(Tags
))
554 m_tags_search_predicate
= Regex::Filter
<PrimaryTag
>(
559 else if (isActiveWindow(Albums
))
561 m_albums_search_predicate
= Regex::ItemFilter
<AlbumEntry
>(
564 std::bind(AlbumEntryMatcher
, ph::_1
, ph::_2
, false));
566 else if (isActiveWindow(Songs
))
568 m_songs_search_predicate
= Regex::Filter
<MPD::Song
>(
575 void MediaLibrary::clearSearchConstraint()
577 if (isActiveWindow(Tags
))
578 m_tags_search_predicate
.clear();
579 else if (isActiveWindow(Albums
))
580 m_albums_search_predicate
.clear();
581 else if (isActiveWindow(Songs
))
582 m_songs_search_predicate
.clear();
585 bool MediaLibrary::search(SearchDirection direction
, bool wrap
, bool skip_current
)
588 if (isActiveWindow(Tags
))
589 result
= ::search(Tags
, m_tags_search_predicate
, direction
, wrap
, skip_current
);
590 else if (isActiveWindow(Albums
))
591 result
= ::search(Albums
, m_albums_search_predicate
, direction
, wrap
, skip_current
);
592 else if (isActiveWindow(Songs
))
593 result
= ::search(Songs
, m_songs_search_predicate
, direction
, wrap
, skip_current
);
597 /***********************************************************************/
599 bool MediaLibrary::allowsFiltering()
601 return allowsSearching();
604 std::string
MediaLibrary::currentFilter()
607 if (isActiveWindow(Tags
))
609 if (auto pred
= Tags
.filterPredicate
<Regex::Filter
<PrimaryTag
>>())
610 result
= pred
->constraint();
612 else if (isActiveWindow(Albums
))
614 if (auto pred
= Albums
.filterPredicate
<Regex::ItemFilter
<AlbumEntry
>>())
615 result
= pred
->constraint();
617 else if (isActiveWindow(Songs
))
619 if (auto pred
= Songs
.filterPredicate
<Regex::Filter
<MPD::Song
>>())
620 result
= pred
->constraint();
625 void MediaLibrary::applyFilter(const std::string
&constraint
)
627 if (isActiveWindow(Tags
))
629 if (!constraint
.empty())
631 Tags
.applyFilter(Regex::Filter
<PrimaryTag
>(
639 else if (isActiveWindow(Albums
))
641 if (!constraint
.empty())
643 Albums
.applyFilter(Regex::ItemFilter
<AlbumEntry
>(
646 std::bind(AlbumEntryMatcher
, ph::_1
, ph::_2
, true)));
649 Albums
.clearFilter();
651 else if (isActiveWindow(Songs
))
653 if (!constraint
.empty())
655 Songs
.applyFilter(Regex::Filter
<MPD::Song
>(
666 /***********************************************************************/
668 bool MediaLibrary::itemAvailable()
670 if (isActiveWindow(Tags
))
671 return !Tags
.empty();
672 if (isActiveWindow(Albums
))
673 return !Albums
.empty();
674 if (isActiveWindow(Songs
))
675 return !Songs
.empty();
679 bool MediaLibrary::addItemToPlaylist(bool play
)
682 if (isActiveWindow(Songs
))
683 result
= addSongToPlaylist(Songs
.current()->value(), play
);
686 if (isActiveWindow(Tags
)
687 || (isActiveWindow(Albums
) && Albums
.current()->value().isAllTracksEntry()))
689 Mpd
.StartSearch(true);
690 Mpd
.AddSearch(Config
.media_lib_primary_tag
, Tags
.current()->value().tag());
691 std::vector
<MPD::Song
> list(
692 std::make_move_iterator(Mpd
.CommitSearchSongs()),
693 std::make_move_iterator(MPD::SongIterator()));
694 std::sort(list
.begin(), list
.end(), SortSongs());
695 result
= addSongsToPlaylist(list
.begin(), list
.end(), play
, -1);
696 std::string tag_type
= boost::locale::to_lower(
697 tagTypeToString(Config
.media_lib_primary_tag
));
698 Statusbar::printf("Songs with %1% \"%2%\" added%3%",
699 tag_type
, Tags
.current()->value().tag(), withErrors(result
));
701 else if (isActiveWindow(Albums
))
703 std::vector
<MPD::Song
> list(
704 std::make_move_iterator(getSongsFromAlbum(Albums
.current()->value())),
705 std::make_move_iterator(MPD::SongIterator()));
706 std::sort(list
.begin(), list
.end(), SortSongs());
707 result
= addSongsToPlaylist(list
.begin(), list
.end(), play
, -1);
708 Statusbar::printf("Songs from album \"%1%\" added%2%",
709 Albums
.current()->value().entry().album(), withErrors(result
));
715 std::vector
<MPD::Song
> MediaLibrary::getSelectedSongs()
717 std::vector
<MPD::Song
> result
;
718 if (isActiveWindow(Tags
))
720 auto tag_handler
= [&result
](const std::string
&tag
) {
721 Mpd
.StartSearch(true);
722 Mpd
.AddSearch(Config
.media_lib_primary_tag
, tag
);
723 size_t begin
= result
.size();
725 std::make_move_iterator(Mpd
.CommitSearchSongs()),
726 std::make_move_iterator(MPD::SongIterator()),
727 std::back_inserter(result
));
728 std::sort(result
.begin()+begin
, result
.end(), SortSongs());
730 bool any_selected
= false;
736 tag_handler(e
.value().tag());
739 // if no item is selected, add current one
740 if (!any_selected
&& !Tags
.empty())
741 tag_handler(Tags
.current()->value().tag());
743 else if (isActiveWindow(Albums
))
745 bool any_selected
= false;
746 for (auto it
= Albums
.begin(); it
!= Albums
.end() && !it
->isSeparator(); ++it
)
748 if (it
->isSelected())
751 auto &sc
= it
->value();
752 Mpd
.StartSearch(true);
754 Mpd
.AddSearch(Config
.media_lib_primary_tag
, sc
.entry().tag());
756 Mpd
.AddSearch(Config
.media_lib_primary_tag
,
757 Tags
.current()->value().tag());
758 Mpd
.AddSearch(MPD_TAG_ALBUM
, sc
.entry().album());
759 Mpd
.AddSearch(MPD_TAG_DATE
, sc
.entry().date());
760 size_t begin
= result
.size();
762 std::make_move_iterator(Mpd
.CommitSearchSongs()),
763 std::make_move_iterator(MPD::SongIterator()),
764 std::back_inserter(result
));
765 std::sort(result
.begin()+begin
, result
.end(), SortSongs());
768 // if no item is selected, add songs from right column
769 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
770 if (!any_selected
&& !Albums
.empty())
772 size_t begin
= result
.size();
774 std::make_move_iterator(getSongsFromAlbum(Albums
.current()->value().entry())),
775 std::make_move_iterator(MPD::SongIterator()),
776 std::back_inserter(result
)
778 std::sort(result
.begin()+begin
, result
.end(), SortSongs());
781 else if (isActiveWindow(Songs
))
782 result
= Songs
.getSelectedSongs();
786 /***********************************************************************/
788 bool MediaLibrary::previousColumnAvailable()
790 assert(!hasTwoColumns
|| !isActiveWindow(Tags
));
791 if (isActiveWindow(Songs
))
793 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
797 else if (isActiveWindow(Albums
))
799 ScopedUnfilteredMenu
<PrimaryTag
> sunfilter_tags(ReapplyFilter::No
, Tags
);
800 if (!hasTwoColumns
&& !Tags
.empty())
806 void MediaLibrary::previousColumn()
808 if (isActiveWindow(Songs
))
810 Songs
.setHighlightColor(Config
.main_highlight_color
);
813 Albums
.setHighlightColor(Config
.active_column_color
);
815 else if (isActiveWindow(Albums
) && !hasTwoColumns
)
817 Albums
.setHighlightColor(Config
.main_highlight_color
);
820 Tags
.setHighlightColor(Config
.active_column_color
);
824 bool MediaLibrary::nextColumnAvailable()
826 assert(!hasTwoColumns
|| !isActiveWindow(Tags
));
827 if (isActiveWindow(Tags
))
829 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
833 else if (isActiveWindow(Albums
))
835 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
842 void MediaLibrary::nextColumn()
844 if (isActiveWindow(Tags
))
846 Tags
.setHighlightColor(Config
.main_highlight_color
);
849 Albums
.setHighlightColor(Config
.active_column_color
);
851 else if (isActiveWindow(Albums
))
853 Albums
.setHighlightColor(Config
.main_highlight_color
);
856 Songs
.setHighlightColor(Config
.active_column_color
);
860 /***********************************************************************/
862 void MediaLibrary::updateTimer()
864 m_timer
= Global::Timer
;
867 void MediaLibrary::toggleColumnsMode()
869 hasTwoColumns
= !hasTwoColumns
;
876 if (isActiveWindow(Tags
))
878 if (Config
.titles_visibility
)
880 std::string item_type
= boost::locale::to_lower(
881 tagTypeToString(Config
.media_lib_primary_tag
));
882 std::string and_mtime
= Config
.media_library_sort_by_mtime
? " and mtime" : "";
883 Albums
.setTitle("Albums (sorted by " + item_type
+ and_mtime
+ ")");
887 Albums
.setTitle(Config
.titles_visibility
? "Albums" : "");
891 int MediaLibrary::columns()
899 void MediaLibrary::toggleSortMode()
901 Config
.media_library_sort_by_mtime
= !Config
.media_library_sort_by_mtime
;
902 Statusbar::printf("Sorting library by: %1%",
903 Config
.media_library_sort_by_mtime
? "modification time" : "name");
906 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
907 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
910 if (Config
.titles_visibility
)
912 std::string item_type
= boost::locale::to_lower(
913 tagTypeToString(Config
.media_lib_primary_tag
));
914 std::string and_mtime
= Config
.media_library_sort_by_mtime
? (" " "and mtime") : "";
915 Albums
.setTitle("Albums (sorted by " + item_type
+ and_mtime
+ ")");
920 ScopedUnfilteredMenu
<PrimaryTag
> sunfilter_tags(ReapplyFilter::No
, Tags
);
921 // if we already have modification times, just resort. otherwise refetch the list.
922 if (!Tags
.empty() && Tags
[0].value().mtime() > 0)
924 std::sort(Tags
.beginV(), Tags
.endV(), SortPrimaryTags());
937 void MediaLibrary::locateSong(const MPD::Song
&s
)
939 std::string primary_tag
= s
.get(Config
.media_lib_primary_tag
);
940 if (primary_tag
.empty())
942 std::string item_type
= boost::locale::to_lower(
943 tagTypeToString(Config
.media_lib_primary_tag
));
944 Statusbar::printf("Can't use this function because the song has no %s tag", item_type
);
948 if (!s
.isFromDatabase())
950 Statusbar::print("Song is not from the database");
954 if (myScreen
!= this)
956 Statusbar::put() << "Jumping to song...";
957 Global::wFooter
->refresh();
968 if (!MoveToTag(Tags
, primary_tag
))
970 // The tag could not be found. Since this was called from an existing
971 // song, the tag should exist in the library, but it was not listed by
972 // list/listallinfo. This is the case with some players where it is not
973 // possible to list all of the library, e.g. mopidy with mopidy-spotify.
974 // To workaround this we simply insert the missing tag.
975 Tags
.addItem(PrimaryTag(primary_tag
, s
.getMTime()));
976 std::sort(Tags
.beginV(), Tags
.endV(), SortPrimaryTags());
978 MoveToTag(Tags
, primary_tag
);
983 Albums
.clearFilter();
986 requestAlbumsUpdate();
990 // When you locate a song in the media library, if no albums or no songs
991 // are found, set the active column to the previous one (tags if no albums,
992 // and albums if no songs). This makes sure that the active column is not
993 // empty, which may make it impossible to move out of.
995 // The problem was if you highlight some song in the rightmost column in
996 // the media browser and then go to some other window and select locate
997 // song. If the tag or album it looked up in the media library was
998 // empty, the selection would stay in the songs column while it was empty.
999 // This made the selection impossible to change.
1001 // This only is a problem if a song has some tag or album for which the
1002 // find command doesn't return any results. This should never really happen
1003 // unless there is some inconsistency in the player. However, it may
1004 // happen, so we need to handle it.
1006 // Note: We don't want to return when no albums are found in two column
1007 // mode. In this case, we try to insert the album, as we do with tags when
1008 // they are not found.
1009 if (hasTwoColumns
|| !Albums
.empty())
1011 if (!MoveToAlbum(Albums
, primary_tag
, s
))
1013 // The album could not be found, insert it if in two column mode.
1014 // See comment about tags not found above. This is the equivalent
1015 // for two column mode.
1016 Albums
.addItem(AlbumEntry(
1017 Album(primary_tag
, s
.getAlbum(), s
.getDate(), s
.getMTime())
1019 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
1021 MoveToAlbum(Albums
, primary_tag
, s
);
1024 Songs
.clearFilter();
1025 requestSongsUpdate();
1030 if (s
!= Songs
.current()->value())
1032 auto begin
= Songs
.beginV(), end
= Songs
.endV();
1033 auto it
= std::find(begin
, end
, s
);
1035 Songs
.highlight(it
-begin
);
1040 else // invalid album was added, clear the list
1043 else // invalid tag was added, clear the list
1050 std::string
AlbumToString(const AlbumEntry
&ae
)
1053 if (ae
.isAllTracksEntry())
1054 result
= "All tracks";
1059 if (ae
.entry().tag().empty())
1060 result
+= Config
.empty_tag
;
1062 result
+= ae
.entry().tag();
1065 if (Config
.media_lib_primary_tag
!= MPD_TAG_DATE
&& !ae
.entry().date().empty())
1066 result
+= "(" + ae
.entry().date() + ") ";
1067 result
+= ae
.entry().album().empty() ? "<no album>" : ae
.entry().album();
1072 std::string
SongToString(const MPD::Song
&s
)
1074 return Format::stringify
<char>(
1075 Config
.song_library_format
, &s
1079 bool TagEntryMatcher(const Regex::Regex
&rx
, const PrimaryTag
&pt
)
1081 return Regex::search(pt
.tag(), rx
);
1084 bool AlbumEntryMatcher(const Regex::Regex
&rx
, const NC::Menu
<AlbumEntry
>::Item
&item
, bool filter
)
1086 if (item
.isSeparator() || item
.value().isAllTracksEntry())
1088 return Regex::search(AlbumToString(item
.value()), rx
);
1091 bool SongEntryMatcher(const Regex::Regex
&rx
, const MPD::Song
&s
)
1093 return Regex::search(SongToString(s
), rx
);
1096 bool MoveToTag(NC::Menu
<PrimaryTag
> &tags
, const std::string
&primary_tag
)
1101 auto equals_fun_argument
= [&](PrimaryTag
&e
) {
1102 return e
.tag() == primary_tag
;
1105 if (equals_fun_argument(*tags
.currentV()))
1108 auto begin
= tags
.beginV(), end
= tags
.endV();
1109 auto it
= std::find_if(begin
, end
, equals_fun_argument
);
1112 tags
.highlight(it
-begin
);
1119 bool MoveToAlbum(NC::Menu
<AlbumEntry
> &albums
, const std::string
&primary_tag
, const MPD::Song
&s
)
1124 std::string album
= s
.getAlbum();
1125 std::string date
= s
.getDate();
1127 auto equals_fun_argument
= [&](AlbumEntry
&e
) {
1128 return (!hasTwoColumns
|| e
.entry().tag() == primary_tag
)
1129 && e
.entry().album() == album
1130 && e
.entry().date() == date
;
1133 if (equals_fun_argument(*albums
.currentV()))
1136 auto begin
= albums
.beginV(), end
= albums
.endV();
1137 auto it
= std::find_if(begin
, end
, equals_fun_argument
);
1140 albums
.highlight(it
-begin
);