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 "format_impl.h"
38 #include "helpers/song_iterator_maker.h"
39 #include "utility/comparators.h"
40 #include "utility/functional.h"
41 #include "utility/type_conversions.h"
43 #include "screens/screen_switcher.h"
45 using Global::MainHeight
;
46 using Global::MainStartY
;
47 using Global::myScreen
;
49 namespace ph
= std::placeholders
;
51 MediaLibrary
*myLibrary
;
56 size_t itsLeftColStartX
;
57 size_t itsLeftColWidth
;
58 size_t itsMiddleColWidth
;
59 size_t itsMiddleColStartX
;
60 size_t itsRightColWidth
;
61 size_t itsRightColStartX
;
63 typedef MediaLibrary::PrimaryTag PrimaryTag
;
64 typedef MediaLibrary::AlbumEntry AlbumEntry
;
66 std::string
Date_(std::string date
)
68 if (!Config
.media_library_albums_split_by_date
)
73 MPD::SongIterator
getSongsFromAlbum(const AlbumEntry
&album
)
75 Mpd
.StartSearch(true);
76 Mpd
.AddSearch(Config
.media_lib_primary_tag
, album
.entry().tag());
77 if (!album
.isAllTracksEntry())
79 Mpd
.AddSearch(MPD_TAG_ALBUM
, album
.entry().album());
80 if (Config
.media_library_albums_split_by_date
)
81 Mpd
.AddSearch(MPD_TAG_DATE
, album
.entry().date());
83 return Mpd
.CommitSearchSongs();
86 std::string
AlbumToString(const AlbumEntry
&ae
);
87 std::string
SongToString(const MPD::Song
&s
);
89 bool TagEntryMatcher(const Regex::Regex
&rx
, const MediaLibrary::PrimaryTag
&tagmtime
);
90 bool AlbumEntryMatcher(const Regex::Regex
&rx
, const NC::Menu
<AlbumEntry
>::Item
&item
, bool filter
);
91 bool SongEntryMatcher(const Regex::Regex
&rx
, const MPD::Song
&s
);
93 bool MoveToTag(NC::Menu
<PrimaryTag
> &tags
, const std::string
&primary_tag
);
94 bool MoveToAlbum(NC::Menu
<AlbumEntry
> &albums
, const std::string
&primary_tag
, const MPD::Song
&s
);
97 typedef NC::Menu
<MPD::Song
>::Item SongItem
;
99 static const std::array
<MPD::Song::GetFunction
, 3> GetFuns
;
101 LocaleStringComparison m_cmp
;
105 : m_cmp(std::locale(), Config
.ignore_leading_the
) { }
107 bool operator()(const SongItem
&a
, const SongItem
&b
) {
108 return (*this)(a
.value(), b
.value());
110 bool operator()(const MPD::Song
&a
, const MPD::Song
&b
) {
112 for (auto get
: GetFuns
) {
113 ret
= m_cmp(a
.getTags(get
), b
.getTags(get
));
118 // Sort by track numbers.
120 ret
= boost::lexical_cast
<int>(a
.getTags(&MPD::Song::getTrackNumber
))
121 - boost::lexical_cast
<int>(b
.getTags(&MPD::Song::getTrackNumber
));
122 } catch (boost::bad_lexical_cast
&) {
123 ret
= a
.getTrackNumber().compare(b
.getTrackNumber());
128 // If track numbers are equal, sort by the display format.
129 return Format::stringify
<char>(Config
.song_library_format
, &a
)
130 < Format::stringify
<char>(Config
.song_library_format
, &b
);
134 const std::array
<MPD::Song::GetFunction
, 3> SortSongs::GetFuns
= {{
136 &MPD::Song::getAlbum
,
140 class SortAlbumEntries
{
141 typedef MediaLibrary::Album Album
;
143 LocaleStringComparison m_cmp
;
146 SortAlbumEntries() : m_cmp(std::locale(), Config
.ignore_leading_the
) { }
148 bool operator()(const AlbumEntry
&a
, const AlbumEntry
&b
) const {
149 return (*this)(a
.entry(), b
.entry());
152 bool operator()(const Album
&a
, const Album
&b
) const {
153 if (Config
.media_library_sort_by_mtime
)
154 return a
.mtime() > b
.mtime();
158 result
= m_cmp(a
.tag(), b
.tag());
161 result
= m_cmp(a
.date(), b
.date());
164 return m_cmp(a
.album(), b
.album()) < 0;
169 class SortPrimaryTags
{
170 LocaleStringComparison m_cmp
;
173 SortPrimaryTags() : m_cmp(std::locale(), Config
.ignore_leading_the
) { }
175 bool operator()(const PrimaryTag
&a
, const PrimaryTag
&b
) const {
176 if (Config
.media_library_sort_by_mtime
)
177 return a
.mtime() > b
.mtime();
179 return m_cmp(a
.tag(), b
.tag()) < 0;
185 MediaLibrary::MediaLibrary()
186 : m_timer(boost::posix_time::from_time_t(0))
187 , m_window_timeout(Config
.data_fetching_delay
? 250 : BaseScreen::defaultWindowTimeout
)
188 , m_fetching_delay(boost::posix_time::milliseconds(Config
.data_fetching_delay
? 250 : -1))
191 itsLeftColWidth
= COLS
/3-1;
192 itsMiddleColWidth
= COLS
/3;
193 itsMiddleColStartX
= itsLeftColWidth
+1;
194 itsRightColWidth
= COLS
-COLS
/3*2-1;
195 itsRightColStartX
= itsLeftColWidth
+itsMiddleColWidth
+2;
197 Tags
= NC::Menu
<PrimaryTag
>(0, MainStartY
, itsLeftColWidth
, MainHeight
, Config
.titles_visibility
? tagTypeToString(Config
.media_lib_primary_tag
) + "s" : "", Config
.main_color
, NC::Border());
198 setHighlightFixes(Tags
);
199 Tags
.cyclicScrolling(Config
.use_cyclic_scrolling
);
200 Tags
.centeredCursor(Config
.centered_cursor
);
201 Tags
.setSelectedPrefix(Config
.selected_item_prefix
);
202 Tags
.setSelectedSuffix(Config
.selected_item_suffix
);
203 Tags
.setItemDisplayer([](NC::Menu
<PrimaryTag
> &menu
) {
204 const std::string
&tag
= menu
.drawn()->value().tag();
206 menu
<< Config
.empty_tag
;
208 menu
<< Charset::utf8ToLocale(tag
);
211 Albums
= NC::Menu
<AlbumEntry
>(itsMiddleColStartX
, MainStartY
, itsMiddleColWidth
, MainHeight
, Config
.titles_visibility
? "Albums" : "", Config
.main_color
, NC::Border());
212 setHighlightInactiveColumnFixes(Albums
);
213 Albums
.cyclicScrolling(Config
.use_cyclic_scrolling
);
214 Albums
.centeredCursor(Config
.centered_cursor
);
215 Albums
.setSelectedPrefix(Config
.selected_item_prefix
);
216 Albums
.setSelectedSuffix(Config
.selected_item_suffix
);
217 Albums
.setItemDisplayer([](NC::Menu
<AlbumEntry
> &menu
) {
218 menu
<< Charset::utf8ToLocale(AlbumToString(menu
.drawn()->value()));
221 Songs
= NC::Menu
<MPD::Song
>(itsRightColStartX
, MainStartY
, itsRightColWidth
, MainHeight
, Config
.titles_visibility
? "Songs" : "", Config
.main_color
, NC::Border());
222 setHighlightInactiveColumnFixes(Songs
);
223 Songs
.cyclicScrolling(Config
.use_cyclic_scrolling
);
224 Songs
.centeredCursor(Config
.centered_cursor
);
225 Songs
.setSelectedPrefix(Config
.selected_item_prefix
);
226 Songs
.setSelectedSuffix(Config
.selected_item_suffix
);
227 Songs
.setItemDisplayer(std::bind(
228 Display::Songs
, ph::_1
, std::cref(Songs
), std::cref(Config
.song_library_format
)
234 void MediaLibrary::resize()
236 size_t x_offset
, width
;
237 getWindowResizeParams(x_offset
, width
);
240 itsLeftColStartX
= x_offset
;
241 itsLeftColWidth
= width
/3-1;
242 itsMiddleColStartX
= itsLeftColStartX
+itsLeftColWidth
+1;
243 itsMiddleColWidth
= width
/3;
244 itsRightColStartX
= itsMiddleColStartX
+itsMiddleColWidth
+1;
245 itsRightColWidth
= width
-width
/3*2-1;
249 itsMiddleColStartX
= x_offset
;
250 itsMiddleColWidth
= width
/2;
251 itsRightColStartX
= x_offset
+itsMiddleColWidth
+1;
252 itsRightColWidth
= width
-itsMiddleColWidth
-1;
255 Tags
.resize(itsLeftColWidth
, MainHeight
);
256 Albums
.resize(itsMiddleColWidth
, MainHeight
);
257 Songs
.resize(itsRightColWidth
, MainHeight
);
259 Tags
.moveTo(itsLeftColStartX
, MainStartY
);
260 Albums
.moveTo(itsMiddleColStartX
, MainStartY
);
261 Songs
.moveTo(itsRightColStartX
, MainStartY
);
266 void MediaLibrary::refresh()
269 drawSeparator(itsMiddleColStartX
-1);
271 drawSeparator(itsRightColStartX
-1);
275 Albums
<< NC::XY(0, 0) << "No albums found.";
276 Albums
.Window::refresh();
280 void MediaLibrary::switchTo()
282 SwitchTo::execute(this);
287 std::wstring
MediaLibrary::title()
289 return L
"Media library";
292 void MediaLibrary::update()
296 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
297 if (Albums
.empty() || m_albums_update_request
)
299 m_albums_update_request
= false;
300 sunfilter_albums
.set(ReapplyFilter::Yes
, true);
301 std::map
<std::tuple
<std::string
, std::string
, std::string
>, time_t> albums
;
302 for (MPD::SongIterator s
= getDatabaseIterator(Mpd
), end
; s
!= end
; ++s
)
306 while (!(tag
= s
->get(Config
.media_lib_primary_tag
, idx
++)).empty())
308 auto key
= std::make_tuple(
311 Date_(s
->getDate()));
312 auto it
= albums
.find(key
);
313 if (it
== albums
.end())
314 albums
[std::move(key
)] = s
->getMTime();
316 it
->second
= s
->getMTime();
320 for (const auto &album
: albums
)
322 auto entry
= AlbumEntry(
323 Album(std::move(std::get
<0>(album
.first
)),
324 std::move(std::get
<1>(album
.first
)),
325 std::move(std::get
<2>(album
.first
)),
327 if (idx
< Albums
.size())
328 Albums
[idx
].value() = std::move(entry
);
330 Albums
.addItem(std::move(entry
));
333 if (idx
< Albums
.size())
334 Albums
.resizeList(idx
);
335 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
341 ScopedUnfilteredMenu
<PrimaryTag
> sunfilter_tags(ReapplyFilter::No
, Tags
);
342 if (Tags
.empty() || m_tags_update_request
)
344 m_tags_update_request
= false;
345 sunfilter_tags
.set(ReapplyFilter::Yes
, true);
346 std::map
<std::string
, time_t> tags
;
347 if (Config
.media_library_sort_by_mtime
)
349 for (MPD::SongIterator s
= getDatabaseIterator(Mpd
), end
; s
!= end
; ++s
)
353 while (!(tag
= s
->get(Config
.media_lib_primary_tag
, idx
++)).empty())
355 auto it
= tags
.find(tag
);
356 if (it
== tags
.end())
357 tags
[std::move(tag
)] = s
->getMTime();
359 it
->second
= std::max(it
->second
, s
->getMTime());
365 MPD::StringIterator tag
= Mpd
.GetList(Config
.media_lib_primary_tag
), end
;
366 for (; tag
!= end
; ++tag
)
367 tags
[std::move(*tag
)] = 0;
370 for (const auto &tag
: tags
)
372 auto ptag
= PrimaryTag(std::move(tag
.first
), tag
.second
);
373 if (idx
< Tags
.size())
374 Tags
[idx
].value() = std::move(ptag
);
376 Tags
.addItem(std::move(ptag
));
379 if (idx
< Tags
.size())
380 Tags
.resizeList(idx
);
381 std::sort(Tags
.beginV(), Tags
.endV(), SortPrimaryTags());
386 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
388 && ((Albums
.empty() && Global::Timer
- m_timer
> m_fetching_delay
)
389 || m_albums_update_request
))
391 m_albums_update_request
= false;
392 sunfilter_albums
.set(ReapplyFilter::Yes
, true);
393 auto &primary_tag
= Tags
.current()->value().tag();
394 Mpd
.StartSearch(true);
395 Mpd
.AddSearch(Config
.media_lib_primary_tag
, primary_tag
);
396 std::map
<std::tuple
<std::string
, std::string
>, time_t> albums
;
397 for (MPD::SongIterator s
= Mpd
.CommitSearchSongs(), end
; s
!= end
; ++s
)
399 auto key
= std::make_tuple(s
->getAlbum(), Date_(s
->getDate()));
400 auto it
= albums
.find(key
);
401 if (it
== albums
.end())
402 albums
[std::move(key
)] = s
->getMTime();
404 it
->second
= std::max(it
->second
, s
->getMTime());
407 for (const auto &album
: albums
)
409 auto entry
= AlbumEntry(
411 std::move(std::get
<0>(album
.first
)),
412 std::move(std::get
<1>(album
.first
)),
414 if (idx
< Albums
.size())
416 Albums
[idx
].value() = std::move(entry
);
417 Albums
[idx
].setSeparator(false);
420 Albums
.addItem(std::move(entry
));
423 if (idx
< Albums
.size())
424 Albums
.resizeList(idx
);
425 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
426 if (albums
.size() > 1)
428 Albums
.addSeparator();
429 Albums
.addItem(AlbumEntry::mkAllTracksEntry(primary_tag
));
435 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
437 && ((Songs
.empty() && Global::Timer
- m_timer
> m_fetching_delay
)
438 || m_songs_update_request
))
440 m_songs_update_request
= false;
441 sunfilter_songs
.set(ReapplyFilter::Yes
, true);
442 auto &album
= Albums
.current()->value();
444 for (MPD::SongIterator s
= getSongsFromAlbum(album
), end
;
445 s
!= end
; ++s
, ++idx
)
447 if (idx
< Songs
.size())
448 Songs
[idx
].value() = std::move(*s
);
450 Songs
.addItem(std::move(*s
));
452 if (idx
< Songs
.size())
453 Songs
.resizeList(idx
);
454 std::sort(Songs
.begin(), Songs
.end(), SortSongs());
458 int MediaLibrary::windowTimeout()
460 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
461 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
462 if (Albums
.empty() || Songs
.empty())
463 return m_window_timeout
;
465 return Screen
<WindowType
>::windowTimeout();
468 void MediaLibrary::mouseButtonPressed(MEVENT me
)
470 auto tryNextColumn
= [this]() -> bool {
472 if (!isActiveWindow(Songs
))
474 if (nextColumnAvailable())
481 auto tryPreviousColumn
= [this]() -> bool {
483 if (!isActiveWindow(Tags
))
485 if (previousColumnAvailable())
492 if (Tags
.hasCoords(me
.x
, me
.y
))
494 if (!tryPreviousColumn() || !tryPreviousColumn())
496 if (size_t(me
.y
) < Tags
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
499 if (me
.bstate
& BUTTON3_PRESSED
)
500 addItemToPlaylist(false);
503 Screen
<WindowType
>::mouseButtonPressed(me
);
507 else if (Albums
.hasCoords(me
.x
, me
.y
))
509 if (!isActiveWindow(Albums
))
512 if (isActiveWindow(Tags
))
513 success
= tryNextColumn();
515 success
= tryPreviousColumn();
519 if (size_t(me
.y
) < Albums
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
522 if (me
.bstate
& BUTTON3_PRESSED
)
523 addItemToPlaylist(false);
526 Screen
<WindowType
>::mouseButtonPressed(me
);
529 else if (Songs
.hasCoords(me
.x
, me
.y
))
531 if (!tryNextColumn() || !tryNextColumn())
533 if (size_t(me
.y
) < Songs
.size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
536 bool play
= me
.bstate
& BUTTON3_PRESSED
;
537 addItemToPlaylist(play
);
540 Screen
<WindowType
>::mouseButtonPressed(me
);
544 /***********************************************************************/
546 bool MediaLibrary::allowsSearching()
551 const std::string
&MediaLibrary::searchConstraint()
553 if (isActiveWindow(Tags
))
554 return m_tags_search_predicate
.constraint();
555 else if (isActiveWindow(Albums
))
556 return m_albums_search_predicate
.constraint();
557 else if (isActiveWindow(Songs
))
558 return m_songs_search_predicate
.constraint();
559 throw std::runtime_error("no active window");
562 void MediaLibrary::setSearchConstraint(const std::string
&constraint
)
564 if (isActiveWindow(Tags
))
566 m_tags_search_predicate
= Regex::Filter
<PrimaryTag
>(
571 else if (isActiveWindow(Albums
))
573 m_albums_search_predicate
= Regex::ItemFilter
<AlbumEntry
>(
576 std::bind(AlbumEntryMatcher
, ph::_1
, ph::_2
, false));
578 else if (isActiveWindow(Songs
))
580 m_songs_search_predicate
= Regex::Filter
<MPD::Song
>(
587 void MediaLibrary::clearSearchConstraint()
589 if (isActiveWindow(Tags
))
590 m_tags_search_predicate
.clear();
591 else if (isActiveWindow(Albums
))
592 m_albums_search_predicate
.clear();
593 else if (isActiveWindow(Songs
))
594 m_songs_search_predicate
.clear();
597 bool MediaLibrary::search(SearchDirection direction
, bool wrap
, bool skip_current
)
600 if (isActiveWindow(Tags
))
601 result
= ::search(Tags
, m_tags_search_predicate
, direction
, wrap
, skip_current
);
602 else if (isActiveWindow(Albums
))
603 result
= ::search(Albums
, m_albums_search_predicate
, direction
, wrap
, skip_current
);
604 else if (isActiveWindow(Songs
))
605 result
= ::search(Songs
, m_songs_search_predicate
, direction
, wrap
, skip_current
);
609 /***********************************************************************/
611 bool MediaLibrary::allowsFiltering()
613 return allowsSearching();
616 std::string
MediaLibrary::currentFilter()
619 if (isActiveWindow(Tags
))
621 if (auto pred
= Tags
.filterPredicate
<Regex::Filter
<PrimaryTag
>>())
622 result
= pred
->constraint();
624 else if (isActiveWindow(Albums
))
626 if (auto pred
= Albums
.filterPredicate
<Regex::ItemFilter
<AlbumEntry
>>())
627 result
= pred
->constraint();
629 else if (isActiveWindow(Songs
))
631 if (auto pred
= Songs
.filterPredicate
<Regex::Filter
<MPD::Song
>>())
632 result
= pred
->constraint();
637 void MediaLibrary::applyFilter(const std::string
&constraint
)
639 if (isActiveWindow(Tags
))
641 if (!constraint
.empty())
643 Tags
.applyFilter(Regex::Filter
<PrimaryTag
>(
651 else if (isActiveWindow(Albums
))
653 if (!constraint
.empty())
655 Albums
.applyFilter(Regex::ItemFilter
<AlbumEntry
>(
658 std::bind(AlbumEntryMatcher
, ph::_1
, ph::_2
, true)));
661 Albums
.clearFilter();
663 else if (isActiveWindow(Songs
))
665 if (!constraint
.empty())
667 Songs
.applyFilter(Regex::Filter
<MPD::Song
>(
678 /***********************************************************************/
680 bool MediaLibrary::itemAvailable()
682 if (isActiveWindow(Tags
))
683 return !Tags
.empty();
684 if (isActiveWindow(Albums
))
685 return !Albums
.empty();
686 if (isActiveWindow(Songs
))
687 return !Songs
.empty();
691 bool MediaLibrary::addItemToPlaylist(bool play
)
694 if (isActiveWindow(Songs
))
695 result
= addSongToPlaylist(Songs
.current()->value(), play
);
698 if (isActiveWindow(Tags
)
699 || (isActiveWindow(Albums
) && Albums
.current()->value().isAllTracksEntry()))
701 Mpd
.StartSearch(true);
702 Mpd
.AddSearch(Config
.media_lib_primary_tag
, Tags
.current()->value().tag());
703 std::vector
<MPD::Song
> list(
704 std::make_move_iterator(Mpd
.CommitSearchSongs()),
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 std::string tag_type
= boost::locale::to_lower(
709 tagTypeToString(Config
.media_lib_primary_tag
));
710 Statusbar::printf("Songs with %1% \"%2%\" added%3%",
711 tag_type
, Tags
.current()->value().tag(), withErrors(result
));
713 else if (isActiveWindow(Albums
))
715 std::vector
<MPD::Song
> list(
716 std::make_move_iterator(getSongsFromAlbum(Albums
.current()->value())),
717 std::make_move_iterator(MPD::SongIterator()));
718 std::sort(list
.begin(), list
.end(), SortSongs());
719 result
= addSongsToPlaylist(list
.begin(), list
.end(), play
, -1);
720 Statusbar::printf("Songs from album \"%1%\" added%2%",
721 Albums
.current()->value().entry().album(), withErrors(result
));
727 std::vector
<MPD::Song
> MediaLibrary::getSelectedSongs()
729 std::vector
<MPD::Song
> result
;
730 if (isActiveWindow(Tags
))
732 auto tag_handler
= [&result
](const std::string
&tag
) {
733 Mpd
.StartSearch(true);
734 Mpd
.AddSearch(Config
.media_lib_primary_tag
, tag
);
735 size_t begin
= result
.size();
737 std::make_move_iterator(Mpd
.CommitSearchSongs()),
738 std::make_move_iterator(MPD::SongIterator()),
739 std::back_inserter(result
));
740 std::sort(result
.begin()+begin
, result
.end(), SortSongs());
742 bool any_selected
= false;
748 tag_handler(e
.value().tag());
751 // if no item is selected, add current one
752 if (!any_selected
&& !Tags
.empty())
753 tag_handler(Tags
.current()->value().tag());
755 else if (isActiveWindow(Albums
))
757 bool any_selected
= false;
758 for (auto it
= Albums
.begin(); it
!= Albums
.end() && !it
->isSeparator(); ++it
)
760 if (it
->isSelected())
763 auto &sc
= it
->value();
764 Mpd
.StartSearch(true);
766 Mpd
.AddSearch(Config
.media_lib_primary_tag
, sc
.entry().tag());
768 Mpd
.AddSearch(Config
.media_lib_primary_tag
,
769 Tags
.current()->value().tag());
770 Mpd
.AddSearch(MPD_TAG_ALBUM
, sc
.entry().album());
771 if (Config
.media_library_albums_split_by_date
)
772 Mpd
.AddSearch(MPD_TAG_DATE
, sc
.entry().date());
773 size_t begin
= result
.size();
775 std::make_move_iterator(Mpd
.CommitSearchSongs()),
776 std::make_move_iterator(MPD::SongIterator()),
777 std::back_inserter(result
));
778 std::sort(result
.begin()+begin
, result
.end(), SortSongs());
781 // if no item is selected, add songs from right column
782 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
783 if (!any_selected
&& !Albums
.empty())
785 size_t begin
= result
.size();
787 std::make_move_iterator(getSongsFromAlbum(Albums
.current()->value())),
788 std::make_move_iterator(MPD::SongIterator()),
789 std::back_inserter(result
)
791 std::sort(result
.begin()+begin
, result
.end(), SortSongs());
794 else if (isActiveWindow(Songs
))
795 result
= Songs
.getSelectedSongs();
799 /***********************************************************************/
801 bool MediaLibrary::previousColumnAvailable()
803 assert(!hasTwoColumns
|| !isActiveWindow(Tags
));
804 if (isActiveWindow(Songs
))
806 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
810 else if (isActiveWindow(Albums
))
812 ScopedUnfilteredMenu
<PrimaryTag
> sunfilter_tags(ReapplyFilter::No
, Tags
);
813 if (!hasTwoColumns
&& !Tags
.empty())
819 void MediaLibrary::previousColumn()
821 if (isActiveWindow(Songs
))
823 setHighlightInactiveColumnFixes(Songs
);
826 setHighlightFixes(Albums
);
828 else if (isActiveWindow(Albums
) && !hasTwoColumns
)
830 setHighlightInactiveColumnFixes(Albums
);
833 setHighlightFixes(Tags
);
837 bool MediaLibrary::nextColumnAvailable()
839 assert(!hasTwoColumns
|| !isActiveWindow(Tags
));
840 if (isActiveWindow(Tags
))
842 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
846 else if (isActiveWindow(Albums
))
848 ScopedUnfilteredMenu
<MPD::Song
> sunfilter_songs(ReapplyFilter::No
, Songs
);
855 void MediaLibrary::nextColumn()
857 if (isActiveWindow(Tags
))
859 setHighlightInactiveColumnFixes(Tags
);
862 setHighlightFixes(Albums
);
864 else if (isActiveWindow(Albums
))
866 setHighlightInactiveColumnFixes(Albums
);
869 setHighlightFixes(Songs
);
873 /***********************************************************************/
875 void MediaLibrary::updateTimer()
877 m_timer
= Global::Timer
;
880 void MediaLibrary::toggleColumnsMode()
882 hasTwoColumns
= !hasTwoColumns
;
889 if (isActiveWindow(Tags
))
891 if (Config
.titles_visibility
)
893 std::string item_type
= boost::locale::to_lower(
894 tagTypeToString(Config
.media_lib_primary_tag
));
895 std::string and_mtime
= Config
.media_library_sort_by_mtime
? " and mtime" : "";
896 Albums
.setTitle("Albums (sorted by " + item_type
+ and_mtime
+ ")");
900 Albums
.setTitle(Config
.titles_visibility
? "Albums" : "");
904 int MediaLibrary::columns()
912 void MediaLibrary::toggleSortMode()
914 Config
.media_library_sort_by_mtime
= !Config
.media_library_sort_by_mtime
;
915 Statusbar::printf("Sorting library by: %1%",
916 Config
.media_library_sort_by_mtime
? "modification time" : "name");
919 ScopedUnfilteredMenu
<AlbumEntry
> sunfilter_albums(ReapplyFilter::No
, Albums
);
920 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
923 if (Config
.titles_visibility
)
925 std::string item_type
= boost::locale::to_lower(
926 tagTypeToString(Config
.media_lib_primary_tag
));
927 std::string and_mtime
= Config
.media_library_sort_by_mtime
? (" " "and mtime") : "";
928 Albums
.setTitle("Albums (sorted by " + item_type
+ and_mtime
+ ")");
933 ScopedUnfilteredMenu
<PrimaryTag
> sunfilter_tags(ReapplyFilter::No
, Tags
);
934 // if we already have modification times, just resort. otherwise refetch the list.
935 if (!Tags
.empty() && Tags
[0].value().mtime() > 0)
937 std::sort(Tags
.beginV(), Tags
.endV(), SortPrimaryTags());
950 void MediaLibrary::locateSong(const MPD::Song
&s
)
952 std::string primary_tag
= s
.get(Config
.media_lib_primary_tag
);
953 if (primary_tag
.empty())
955 std::string item_type
= boost::locale::to_lower(
956 tagTypeToString(Config
.media_lib_primary_tag
));
957 Statusbar::printf("Can't use this function because the song has no %s tag", item_type
);
961 if (!s
.isFromDatabase())
963 Statusbar::print("Song is not from the database");
967 if (myScreen
!= this)
969 Statusbar::put() << "Jumping to song...";
970 Global::wFooter
->refresh();
981 if (!MoveToTag(Tags
, primary_tag
))
983 // The tag could not be found. Since this was called from an existing
984 // song, the tag should exist in the library, but it was not listed by
985 // list/listallinfo. This is the case with some players where it is not
986 // possible to list all of the library, e.g. mopidy with mopidy-spotify.
987 // To workaround this we simply insert the missing tag.
988 Tags
.addItem(PrimaryTag(primary_tag
, s
.getMTime()));
989 std::sort(Tags
.beginV(), Tags
.endV(), SortPrimaryTags());
991 MoveToTag(Tags
, primary_tag
);
996 Albums
.clearFilter();
999 requestAlbumsUpdate();
1003 // When you locate a song in the media library, if no albums or no songs
1004 // are found, set the active column to the previous one (tags if no albums,
1005 // and albums if no songs). This makes sure that the active column is not
1006 // empty, which may make it impossible to move out of.
1008 // The problem was if you highlight some song in the rightmost column in
1009 // the media browser and then go to some other window and select locate
1010 // song. If the tag or album it looked up in the media library was
1011 // empty, the selection would stay in the songs column while it was empty.
1012 // This made the selection impossible to change.
1014 // This only is a problem if a song has some tag or album for which the
1015 // find command doesn't return any results. This should never really happen
1016 // unless there is some inconsistency in the player. However, it may
1017 // happen, so we need to handle it.
1019 // Note: We don't want to return when no albums are found in two column
1020 // mode. In this case, we try to insert the album, as we do with tags when
1021 // they are not found.
1022 if (hasTwoColumns
|| !Albums
.empty())
1024 if (!MoveToAlbum(Albums
, primary_tag
, s
))
1026 // The album could not be found, insert it if in two column mode.
1027 // See comment about tags not found above. This is the equivalent
1028 // for two column mode.
1029 Albums
.addItem(AlbumEntry(Album(primary_tag
,
1033 std::sort(Albums
.beginV(), Albums
.endV(), SortAlbumEntries());
1035 MoveToAlbum(Albums
, primary_tag
, s
);
1038 Songs
.clearFilter();
1039 requestSongsUpdate();
1044 if (s
!= Songs
.current()->value())
1046 auto begin
= Songs
.beginV(), end
= Songs
.endV();
1047 auto it
= std::find(begin
, end
, s
);
1049 Songs
.highlight(it
-begin
);
1054 else // invalid album was added, clear the list
1057 else // invalid tag was added, clear the list
1064 std::string
AlbumToString(const AlbumEntry
&ae
)
1067 if (ae
.isAllTracksEntry())
1068 result
= "All tracks";
1073 if (ae
.entry().tag().empty())
1074 result
+= Config
.empty_tag
;
1076 result
+= ae
.entry().tag();
1079 if (Config
.media_lib_primary_tag
!= MPD_TAG_DATE
&& !ae
.entry().date().empty())
1080 result
+= "(" + ae
.entry().date() + ") ";
1081 result
+= ae
.entry().album().empty() ? "<no album>" : ae
.entry().album();
1086 std::string
SongToString(const MPD::Song
&s
)
1088 return Format::stringify
<char>(
1089 Config
.song_library_format
, &s
1093 bool TagEntryMatcher(const Regex::Regex
&rx
, const PrimaryTag
&pt
)
1095 return Regex::search(pt
.tag(), rx
, Config
.ignore_diacritics
);
1098 bool AlbumEntryMatcher(const Regex::Regex
&rx
, const NC::Menu
<AlbumEntry
>::Item
&item
, bool filter
)
1100 if (item
.isSeparator() || item
.value().isAllTracksEntry())
1102 return Regex::search(AlbumToString(item
.value()), rx
, Config
.ignore_diacritics
);
1105 bool SongEntryMatcher(const Regex::Regex
&rx
, const MPD::Song
&s
)
1107 return Regex::search(SongToString(s
), rx
, Config
.ignore_diacritics
);
1110 bool MoveToTag(NC::Menu
<PrimaryTag
> &tags
, const std::string
&primary_tag
)
1115 auto equals_fun_argument
= [&](PrimaryTag
&e
) {
1116 return e
.tag() == primary_tag
;
1119 if (equals_fun_argument(*tags
.currentV()))
1122 auto begin
= tags
.beginV(), end
= tags
.endV();
1123 auto it
= std::find_if(begin
, end
, equals_fun_argument
);
1126 tags
.highlight(it
-begin
);
1133 bool MoveToAlbum(NC::Menu
<AlbumEntry
> &albums
, const std::string
&primary_tag
, const MPD::Song
&s
)
1138 std::string album
= s
.getAlbum();
1139 std::string date
= s
.getDate();
1141 auto equals_fun_argument
= [&](AlbumEntry
&e
) {
1142 return (!hasTwoColumns
|| e
.entry().tag() == primary_tag
)
1143 && e
.entry().album() == album
1144 && (!Config
.media_library_albums_split_by_date
|| e
.entry().date() == date
);
1147 if (equals_fun_argument(*albums
.currentV()))
1150 auto begin
= albums
.beginV(), end
= albums
.endV();
1151 auto it
= std::find_if(begin
, end
, equals_fun_argument
);
1154 albums
.highlight(it
-begin
);