Fix warnings when compiling with GCC 7
[ncmpcpp.git] / src / screens / media_library.cpp
blob24045b4ae455bb71882177ba32757fee0d449e31
1 /***************************************************************************
2 * Copyright (C) 2008-2017 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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 ***************************************************************************/
21 #include <boost/date_time/posix_time/posix_time.hpp>
22 #include <boost/locale/conversion.hpp>
23 #include <algorithm>
24 #include <array>
25 #include <cassert>
27 #include "charset.h"
28 #include "display.h"
29 #include "helpers.h"
30 #include "global.h"
31 #include "curses/menu_impl.h"
32 #include "mpdpp.h"
33 #include "screens/playlist.h"
34 #include "screens/media_library.h"
35 #include "status.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"
42 #include "title.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;
53 namespace {
55 bool hasTwoColumns;
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)
69 date.clear();
70 return 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);
96 struct SortSongs {
97 typedef NC::Menu<MPD::Song>::Item SongItem;
99 static const std::array<MPD::Song::GetFunction, 3> GetFuns;
101 LocaleStringComparison m_cmp;
103 public:
104 SortSongs()
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) {
111 int ret;
112 for (auto get : GetFuns) {
113 ret = m_cmp(a.getTags(get), b.getTags(get));
114 if (ret != 0)
115 return ret < 0;
118 // Sort by track numbers.
119 try {
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());
125 if (ret != 0)
126 return ret < 0;
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 = {{
135 &MPD::Song::getDate,
136 &MPD::Song::getAlbum,
137 &MPD::Song::getDisc
140 class SortAlbumEntries {
141 typedef MediaLibrary::Album Album;
143 LocaleStringComparison m_cmp;
145 public:
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();
155 else
157 int result;
158 result = m_cmp(a.tag(), b.tag());
159 if (result != 0)
160 return result < 0;
161 result = m_cmp(a.date(), b.date());
162 if (result != 0)
163 return result < 0;
164 return m_cmp(a.album(), b.album()) < 0;
169 class SortPrimaryTags {
170 LocaleStringComparison m_cmp;
172 public:
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();
178 else
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))
190 hasTwoColumns = 0;
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();
205 if (tag.empty())
206 menu << Config.empty_tag;
207 else
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)
231 w = &Tags;
234 void MediaLibrary::resize()
236 size_t x_offset, width;
237 getWindowResizeParams(x_offset, width);
238 if (!hasTwoColumns)
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;
247 else
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);
263 hasToBeResized = 0;
266 void MediaLibrary::refresh()
268 Tags.display();
269 drawSeparator(itsMiddleColStartX-1);
270 Albums.display();
271 drawSeparator(itsRightColStartX-1);
272 Songs.display();
273 if (Albums.empty())
275 Albums << NC::XY(0, 0) << "No albums found.";
276 Albums.Window::refresh();
280 void MediaLibrary::switchTo()
282 SwitchTo::execute(this);
283 drawHeader();
284 refresh();
287 std::wstring MediaLibrary::title()
289 return L"Media library";
292 void MediaLibrary::update()
294 if (hasTwoColumns)
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)
304 std::string tag;
305 unsigned idx = 0;
306 while (!(tag = s->get(Config.media_lib_primary_tag, idx++)).empty())
308 auto key = std::make_tuple(
309 std::move(tag),
310 s->getAlbum(),
311 Date_(s->getDate()));
312 auto it = albums.find(key);
313 if (it == albums.end())
314 albums[std::move(key)] = s->getMTime();
315 else
316 it->second = s->getMTime();
319 size_t idx = 0;
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)),
326 album.second));
327 if (idx < Albums.size())
328 Albums[idx].value() = std::move(entry);
329 else
330 Albums.addItem(std::move(entry));
331 ++idx;
333 if (idx < Albums.size())
334 Albums.resizeList(idx);
335 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
338 else
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)
351 std::string tag;
352 unsigned idx = 0;
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();
358 else
359 it->second = std::max(it->second, s->getMTime());
363 else
365 MPD::StringIterator tag = Mpd.GetList(Config.media_lib_primary_tag), end;
366 for (; tag != end; ++tag)
367 tags[std::move(*tag)] = 0;
369 size_t idx = 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);
375 else
376 Tags.addItem(std::move(ptag));
377 ++idx;
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);
387 if (!Tags.empty()
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();
403 else
404 it->second = std::max(it->second, s->getMTime());
406 size_t idx = 0;
407 for (const auto &album : albums)
409 auto entry = AlbumEntry(
410 Album(primary_tag,
411 std::move(std::get<0>(album.first)),
412 std::move(std::get<1>(album.first)),
413 album.second));
414 if (idx < Albums.size())
416 Albums[idx].value() = std::move(entry);
417 Albums[idx].setSeparator(false);
419 else
420 Albums.addItem(std::move(entry));
421 ++idx;
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);
436 if (!Albums.empty()
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();
443 size_t idx = 0;
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);
449 else
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;
464 else
465 return Screen<WindowType>::windowTimeout();
468 void MediaLibrary::mouseButtonPressed(MEVENT me)
470 auto tryNextColumn = [this]() -> bool {
471 bool result = true;
472 if (!isActiveWindow(Songs))
474 if (nextColumnAvailable())
475 nextColumn();
476 else
477 result = false;
479 return result;
481 auto tryPreviousColumn = [this]() -> bool {
482 bool result = true;
483 if (!isActiveWindow(Tags))
485 if (previousColumnAvailable())
486 previousColumn();
487 else
488 result = false;
490 return result;
492 if (Tags.hasCoords(me.x, me.y))
494 if (!tryPreviousColumn() || !tryPreviousColumn())
495 return;
496 if (size_t(me.y) < Tags.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
498 Tags.Goto(me.y);
499 if (me.bstate & BUTTON3_PRESSED)
500 addItemToPlaylist(false);
502 else
503 Screen<WindowType>::mouseButtonPressed(me);
504 Albums.clear();
505 Songs.clear();
507 else if (Albums.hasCoords(me.x, me.y))
509 if (!isActiveWindow(Albums))
511 bool success;
512 if (isActiveWindow(Tags))
513 success = tryNextColumn();
514 else
515 success = tryPreviousColumn();
516 if (!success)
517 return;
519 if (size_t(me.y) < Albums.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
521 Albums.Goto(me.y);
522 if (me.bstate & BUTTON3_PRESSED)
523 addItemToPlaylist(false);
525 else
526 Screen<WindowType>::mouseButtonPressed(me);
527 Songs.clear();
529 else if (Songs.hasCoords(me.x, me.y))
531 if (!tryNextColumn() || !tryNextColumn())
532 return;
533 if (size_t(me.y) < Songs.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
535 Songs.Goto(me.y);
536 bool play = me.bstate & BUTTON3_PRESSED;
537 addItemToPlaylist(play);
539 else
540 Screen<WindowType>::mouseButtonPressed(me);
544 /***********************************************************************/
546 bool MediaLibrary::allowsSearching()
548 return true;
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>(
567 constraint,
568 Config.regex_type,
569 TagEntryMatcher);
571 else if (isActiveWindow(Albums))
573 m_albums_search_predicate = Regex::ItemFilter<AlbumEntry>(
574 constraint,
575 Config.regex_type,
576 std::bind(AlbumEntryMatcher, ph::_1, ph::_2, false));
578 else if (isActiveWindow(Songs))
580 m_songs_search_predicate = Regex::Filter<MPD::Song>(
581 constraint,
582 Config.regex_type,
583 SongEntryMatcher);
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)
599 bool result = false;
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);
606 return result;
609 /***********************************************************************/
611 bool MediaLibrary::allowsFiltering()
613 return allowsSearching();
616 std::string MediaLibrary::currentFilter()
618 std::string result;
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();
634 return result;
637 void MediaLibrary::applyFilter(const std::string &constraint)
639 if (isActiveWindow(Tags))
641 if (!constraint.empty())
643 Tags.applyFilter(Regex::Filter<PrimaryTag>(
644 constraint,
645 Config.regex_type,
646 TagEntryMatcher));
648 else
649 Tags.clearFilter();
651 else if (isActiveWindow(Albums))
653 if (!constraint.empty())
655 Albums.applyFilter(Regex::ItemFilter<AlbumEntry>(
656 constraint,
657 Config.regex_type,
658 std::bind(AlbumEntryMatcher, ph::_1, ph::_2, true)));
660 else
661 Albums.clearFilter();
663 else if (isActiveWindow(Songs))
665 if (!constraint.empty())
667 Songs.applyFilter(Regex::Filter<MPD::Song>(
668 constraint,
669 Config.regex_type,
670 SongEntryMatcher));
672 else
673 Songs.clearFilter();
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();
688 return false;
691 bool MediaLibrary::addItemToPlaylist(bool play)
693 bool result = false;
694 if (isActiveWindow(Songs))
695 result = addSongToPlaylist(Songs.current()->value(), play);
696 else
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));
724 return 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();
736 std::copy(
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;
743 for (auto &e : Tags)
745 if (e.isSelected())
747 any_selected = true;
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())
762 any_selected = true;
763 auto &sc = it->value();
764 Mpd.StartSearch(true);
765 if (hasTwoColumns)
766 Mpd.AddSearch(Config.media_lib_primary_tag, sc.entry().tag());
767 else
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();
774 std::copy(
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();
786 std::copy(
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();
796 return result;
799 /***********************************************************************/
801 bool MediaLibrary::previousColumnAvailable()
803 assert(!hasTwoColumns || !isActiveWindow(Tags));
804 if (isActiveWindow(Songs))
806 ScopedUnfilteredMenu<AlbumEntry> sunfilter_albums(ReapplyFilter::No, Albums);
807 if (!Albums.empty())
808 return true;
810 else if (isActiveWindow(Albums))
812 ScopedUnfilteredMenu<PrimaryTag> sunfilter_tags(ReapplyFilter::No, Tags);
813 if (!hasTwoColumns && !Tags.empty())
814 return true;
816 return false;
819 void MediaLibrary::previousColumn()
821 if (isActiveWindow(Songs))
823 setHighlightInactiveColumnFixes(Songs);
824 w->refresh();
825 w = &Albums;
826 setHighlightFixes(Albums);
828 else if (isActiveWindow(Albums) && !hasTwoColumns)
830 setHighlightInactiveColumnFixes(Albums);
831 w->refresh();
832 w = &Tags;
833 setHighlightFixes(Tags);
837 bool MediaLibrary::nextColumnAvailable()
839 assert(!hasTwoColumns || !isActiveWindow(Tags));
840 if (isActiveWindow(Tags))
842 ScopedUnfilteredMenu<AlbumEntry> sunfilter_albums(ReapplyFilter::No, Albums);
843 if (!Albums.empty())
844 return true;
846 else if (isActiveWindow(Albums))
848 ScopedUnfilteredMenu<MPD::Song> sunfilter_songs(ReapplyFilter::No, Songs);
849 if (!Songs.empty())
850 return true;
852 return false;
855 void MediaLibrary::nextColumn()
857 if (isActiveWindow(Tags))
859 setHighlightInactiveColumnFixes(Tags);
860 w->refresh();
861 w = &Albums;
862 setHighlightFixes(Albums);
864 else if (isActiveWindow(Albums))
866 setHighlightInactiveColumnFixes(Albums);
867 w->refresh();
868 w = &Songs;
869 setHighlightFixes(Songs);
873 /***********************************************************************/
875 void MediaLibrary::updateTimer()
877 m_timer = Global::Timer;
880 void MediaLibrary::toggleColumnsMode()
882 hasTwoColumns = !hasTwoColumns;
883 Tags.clear();
884 Albums.clear();
885 Albums.reset();
886 Songs.clear();
887 if (hasTwoColumns)
889 if (isActiveWindow(Tags))
890 nextColumn();
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 + ")");
899 else
900 Albums.setTitle(Config.titles_visibility ? "Albums" : "");
901 resize();
904 int MediaLibrary::columns()
906 if (hasTwoColumns)
907 return 2;
908 else
909 return 3;
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");
917 if (hasTwoColumns)
919 ScopedUnfilteredMenu<AlbumEntry> sunfilter_albums(ReapplyFilter::No, Albums);
920 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
921 Albums.refresh();
922 Songs.clear();
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 + ")");
931 else
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());
938 Tags.refresh();
940 else
942 Tags.clear();
944 Albums.clear();
945 Songs.clear();
947 update();
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);
958 return;
961 if (!s.isFromDatabase())
963 Statusbar::print("Song is not from the database");
964 return;
967 if (myScreen != this)
968 switchTo();
969 Statusbar::put() << "Jumping to song...";
970 Global::wFooter->refresh();
972 if (!hasTwoColumns)
974 Tags.clearFilter();
975 if (Tags.empty())
977 requestTagsUpdate();
978 update();
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());
990 Tags.refresh();
991 MoveToTag(Tags, primary_tag);
993 Albums.clear();
996 Albums.clearFilter();
997 if (Albums.empty())
999 requestAlbumsUpdate();
1000 update();
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,
1030 s.getAlbum(),
1031 Date_(s.getDate()),
1032 s.getMTime())));
1033 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
1034 Albums.refresh();
1035 MoveToAlbum(Albums, primary_tag, s);
1038 Songs.clearFilter();
1039 requestSongsUpdate();
1040 update();
1042 if (!Songs.empty())
1044 if (s != Songs.current()->value())
1046 auto begin = Songs.beginV(), end = Songs.endV();
1047 auto it = std::find(begin, end, s);
1048 if (it != end)
1049 Songs.highlight(it-begin);
1051 nextColumn();
1052 nextColumn();
1054 else // invalid album was added, clear the list
1055 Albums.clear();
1057 else // invalid tag was added, clear the list
1058 Tags.clear();
1059 refresh();
1062 namespace {
1064 std::string AlbumToString(const AlbumEntry &ae)
1066 std::string result;
1067 if (ae.isAllTracksEntry())
1068 result = "All tracks";
1069 else
1071 if (hasTwoColumns)
1073 if (ae.entry().tag().empty())
1074 result += Config.empty_tag;
1075 else
1076 result += ae.entry().tag();
1077 result += " - ";
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();
1083 return result;
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())
1101 return filter;
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)
1112 if (tags.empty())
1113 return false;
1115 auto equals_fun_argument = [&](PrimaryTag &e) {
1116 return e.tag() == primary_tag;
1119 if (equals_fun_argument(*tags.currentV()))
1120 return true;
1122 auto begin = tags.beginV(), end = tags.endV();
1123 auto it = std::find_if(begin, end, equals_fun_argument);
1124 if (it != end)
1126 tags.highlight(it-begin);
1127 return true;
1130 return false;
1133 bool MoveToAlbum(NC::Menu<AlbumEntry> &albums, const std::string &primary_tag, const MPD::Song &s)
1135 if (albums.empty())
1136 return false;
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()))
1148 return true;
1150 auto begin = albums.beginV(), end = albums.endV();
1151 auto it = std::find_if(begin, end, equals_fun_argument);
1152 if (it != end)
1154 albums.highlight(it-begin);
1155 return true;
1158 return false;