Config: reformat all descriptions to fill 80 columns
[ncmpcpp.git] / src / screens / media_library.cpp
blob31efb57ed30a210126d28ee3783cce7f44035526
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 "helpers/song_iterator_maker.h"
38 #include "utility/comparators.h"
39 #include "utility/functional.h"
40 #include "utility/type_conversions.h"
41 #include "title.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;
52 namespace {
54 bool hasTwoColumns;
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);
87 struct SortSongs {
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;
95 public:
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),
105 b.getTags(*get));
106 if (ret != 0)
107 return ret < 0;
109 try {
110 int ret = boost::lexical_cast<int>(a.getTags(&MPD::Song::getTrackNumber))
111 - boost::lexical_cast<int>(b.getTags(&MPD::Song::getTrackNumber));
112 return ret < 0;
113 } catch (boost::bad_lexical_cast &) {
114 return a.getTrackNumber() < b.getTrackNumber();
119 const std::array<MPD::Song::GetFunction, 3> SortSongs::GetFuns = {{
120 &MPD::Song::getDate,
121 &MPD::Song::getAlbum,
122 &MPD::Song::getDisc
125 class SortAlbumEntries {
126 typedef MediaLibrary::Album Album;
128 LocaleStringComparison m_cmp;
130 public:
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();
140 else
142 int result;
143 result = m_cmp(a.tag(), b.tag());
144 if (result != 0)
145 return result < 0;
146 result = m_cmp(a.date(), b.date());
147 if (result != 0)
148 return result < 0;
149 return m_cmp(a.album(), b.album()) < 0;
154 class SortPrimaryTags {
155 LocaleStringComparison m_cmp;
157 public:
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();
163 else
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))
175 hasTwoColumns = 0;
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();
190 if (tag.empty())
191 menu << Config.empty_tag;
192 else
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)
216 w = &Tags;
219 void MediaLibrary::resize()
221 size_t x_offset, width;
222 getWindowResizeParams(x_offset, width);
223 if (!hasTwoColumns)
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;
232 else
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);
248 hasToBeResized = 0;
251 void MediaLibrary::refresh()
253 Tags.display();
254 drawSeparator(itsMiddleColStartX-1);
255 Albums.display();
256 drawSeparator(itsRightColStartX-1);
257 Songs.display();
258 if (Albums.empty())
260 Albums << NC::XY(0, 0) << "No albums found.";
261 Albums.Window::refresh();
265 void MediaLibrary::switchTo()
267 SwitchTo::execute(this);
268 drawHeader();
269 refresh();
272 std::wstring MediaLibrary::title()
274 return L"Media library";
277 void MediaLibrary::update()
279 if (hasTwoColumns)
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)
289 std::string tag;
290 unsigned idx = 0;
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();
297 else
298 it->second = s->getMTime();
301 size_t idx = 0;
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)),
308 album.second));
309 if (idx < Albums.size())
310 Albums[idx].value() = std::move(entry);
311 else
312 Albums.addItem(std::move(entry));
313 ++idx;
315 if (idx < Albums.size())
316 Albums.resizeList(idx);
317 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
320 else
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)
333 std::string tag;
334 unsigned idx = 0;
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();
340 else
341 it->second = std::max(it->second, s->getMTime());
345 else
347 MPD::StringIterator tag = Mpd.GetList(Config.media_lib_primary_tag), end;
348 for (; tag != end; ++tag)
349 tags[std::move(*tag)] = 0;
351 size_t idx = 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);
357 else
358 Tags.addItem(std::move(ptag));
359 ++idx;
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);
369 if (!Tags.empty()
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();
385 else
386 it->second = std::max(it->second, s->getMTime());
388 size_t idx = 0;
389 for (const auto &album : albums)
391 auto entry = AlbumEntry(
392 Album(primary_tag,
393 std::move(std::get<0>(album.first)),
394 std::move(std::get<1>(album.first)),
395 album.second));
396 if (idx < Albums.size())
398 Albums[idx].value() = std::move(entry);
399 Albums[idx].setSeparator(false);
401 else
402 Albums.addItem(std::move(entry));
403 ++idx;
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);
418 if (!Albums.empty()
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());
432 size_t idx = 0;
433 for (MPD::SongIterator s = Mpd.CommitSearchSongs(), end; s != end; ++s, ++idx)
435 if (idx < Songs.size())
436 Songs[idx].value() = std::move(*s);
437 else
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;
452 else
453 return Screen<WindowType>::windowTimeout();
456 void MediaLibrary::mouseButtonPressed(MEVENT me)
458 auto tryNextColumn = [this]() -> bool {
459 bool result = true;
460 if (!isActiveWindow(Songs))
462 if (nextColumnAvailable())
463 nextColumn();
464 else
465 result = false;
467 return result;
469 auto tryPreviousColumn = [this]() -> bool {
470 bool result = true;
471 if (!isActiveWindow(Tags))
473 if (previousColumnAvailable())
474 previousColumn();
475 else
476 result = false;
478 return result;
480 if (Tags.hasCoords(me.x, me.y))
482 if (!tryPreviousColumn() || !tryPreviousColumn())
483 return;
484 if (size_t(me.y) < Tags.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
486 Tags.Goto(me.y);
487 if (me.bstate & BUTTON3_PRESSED)
488 addItemToPlaylist(false);
490 else
491 Screen<WindowType>::mouseButtonPressed(me);
492 Albums.clear();
493 Songs.clear();
495 else if (Albums.hasCoords(me.x, me.y))
497 if (!isActiveWindow(Albums))
499 bool success;
500 if (isActiveWindow(Tags))
501 success = tryNextColumn();
502 else
503 success = tryPreviousColumn();
504 if (!success)
505 return;
507 if (size_t(me.y) < Albums.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
509 Albums.Goto(me.y);
510 if (me.bstate & BUTTON3_PRESSED)
511 addItemToPlaylist(false);
513 else
514 Screen<WindowType>::mouseButtonPressed(me);
515 Songs.clear();
517 else if (Songs.hasCoords(me.x, me.y))
519 if (!tryNextColumn() || !tryNextColumn())
520 return;
521 if (size_t(me.y) < Songs.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
523 Songs.Goto(me.y);
524 bool play = me.bstate & BUTTON3_PRESSED;
525 addItemToPlaylist(play);
527 else
528 Screen<WindowType>::mouseButtonPressed(me);
532 /***********************************************************************/
534 bool MediaLibrary::allowsSearching()
536 return true;
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>(
555 constraint,
556 Config.regex_type,
557 TagEntryMatcher);
559 else if (isActiveWindow(Albums))
561 m_albums_search_predicate = Regex::ItemFilter<AlbumEntry>(
562 constraint,
563 Config.regex_type,
564 std::bind(AlbumEntryMatcher, ph::_1, ph::_2, false));
566 else if (isActiveWindow(Songs))
568 m_songs_search_predicate = Regex::Filter<MPD::Song>(
569 constraint,
570 Config.regex_type,
571 SongEntryMatcher);
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)
587 bool result = false;
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);
594 return result;
597 /***********************************************************************/
599 bool MediaLibrary::allowsFiltering()
601 return allowsSearching();
604 std::string MediaLibrary::currentFilter()
606 std::string result;
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();
622 return result;
625 void MediaLibrary::applyFilter(const std::string &constraint)
627 if (isActiveWindow(Tags))
629 if (!constraint.empty())
631 Tags.applyFilter(Regex::Filter<PrimaryTag>(
632 constraint,
633 Config.regex_type,
634 TagEntryMatcher));
636 else
637 Tags.clearFilter();
639 else if (isActiveWindow(Albums))
641 if (!constraint.empty())
643 Albums.applyFilter(Regex::ItemFilter<AlbumEntry>(
644 constraint,
645 Config.regex_type,
646 std::bind(AlbumEntryMatcher, ph::_1, ph::_2, true)));
648 else
649 Albums.clearFilter();
651 else if (isActiveWindow(Songs))
653 if (!constraint.empty())
655 Songs.applyFilter(Regex::Filter<MPD::Song>(
656 constraint,
657 Config.regex_type,
658 SongEntryMatcher));
660 else
661 Songs.clearFilter();
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();
676 return false;
679 bool MediaLibrary::addItemToPlaylist(bool play)
681 bool result = false;
682 if (isActiveWindow(Songs))
683 result = addSongToPlaylist(Songs.current()->value(), play);
684 else
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));
712 return 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();
724 std::copy(
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;
731 for (auto &e : Tags)
733 if (e.isSelected())
735 any_selected = true;
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())
750 any_selected = true;
751 auto &sc = it->value();
752 Mpd.StartSearch(true);
753 if (hasTwoColumns)
754 Mpd.AddSearch(Config.media_lib_primary_tag, sc.entry().tag());
755 else
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();
761 std::copy(
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();
773 std::copy(
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();
783 return result;
786 /***********************************************************************/
788 bool MediaLibrary::previousColumnAvailable()
790 assert(!hasTwoColumns || !isActiveWindow(Tags));
791 if (isActiveWindow(Songs))
793 ScopedUnfilteredMenu<AlbumEntry> sunfilter_albums(ReapplyFilter::No, Albums);
794 if (!Albums.empty())
795 return true;
797 else if (isActiveWindow(Albums))
799 ScopedUnfilteredMenu<PrimaryTag> sunfilter_tags(ReapplyFilter::No, Tags);
800 if (!hasTwoColumns && !Tags.empty())
801 return true;
803 return false;
806 void MediaLibrary::previousColumn()
808 if (isActiveWindow(Songs))
810 Songs.setHighlightColor(Config.main_highlight_color);
811 w->refresh();
812 w = &Albums;
813 Albums.setHighlightColor(Config.active_column_color);
815 else if (isActiveWindow(Albums) && !hasTwoColumns)
817 Albums.setHighlightColor(Config.main_highlight_color);
818 w->refresh();
819 w = &Tags;
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);
830 if (!Albums.empty())
831 return true;
833 else if (isActiveWindow(Albums))
835 ScopedUnfilteredMenu<MPD::Song> sunfilter_songs(ReapplyFilter::No, Songs);
836 if (!Songs.empty())
837 return true;
839 return false;
842 void MediaLibrary::nextColumn()
844 if (isActiveWindow(Tags))
846 Tags.setHighlightColor(Config.main_highlight_color);
847 w->refresh();
848 w = &Albums;
849 Albums.setHighlightColor(Config.active_column_color);
851 else if (isActiveWindow(Albums))
853 Albums.setHighlightColor(Config.main_highlight_color);
854 w->refresh();
855 w = &Songs;
856 Songs.setHighlightColor(Config.active_column_color);
860 /***********************************************************************/
862 void MediaLibrary::updateTimer()
864 m_timer = Global::Timer;
867 void MediaLibrary::toggleColumnsMode()
869 hasTwoColumns = !hasTwoColumns;
870 Tags.clear();
871 Albums.clear();
872 Albums.reset();
873 Songs.clear();
874 if (hasTwoColumns)
876 if (isActiveWindow(Tags))
877 nextColumn();
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 + ")");
886 else
887 Albums.setTitle(Config.titles_visibility ? "Albums" : "");
888 resize();
891 int MediaLibrary::columns()
893 if (hasTwoColumns)
894 return 2;
895 else
896 return 3;
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");
904 if (hasTwoColumns)
906 ScopedUnfilteredMenu<AlbumEntry> sunfilter_albums(ReapplyFilter::No, Albums);
907 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
908 Albums.refresh();
909 Songs.clear();
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 + ")");
918 else
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());
925 Tags.refresh();
927 else
929 Tags.clear();
931 Albums.clear();
932 Songs.clear();
934 update();
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);
945 return;
948 if (!s.isFromDatabase())
950 Statusbar::print("Song is not from the database");
951 return;
954 if (myScreen != this)
955 switchTo();
956 Statusbar::put() << "Jumping to song...";
957 Global::wFooter->refresh();
959 if (!hasTwoColumns)
961 Tags.clearFilter();
962 if (Tags.empty())
964 requestTagsUpdate();
965 update();
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());
977 Tags.refresh();
978 MoveToTag(Tags, primary_tag);
980 Albums.clear();
983 Albums.clearFilter();
984 if (Albums.empty())
986 requestAlbumsUpdate();
987 update();
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());
1020 Albums.refresh();
1021 MoveToAlbum(Albums, primary_tag, s);
1024 Songs.clearFilter();
1025 requestSongsUpdate();
1026 update();
1028 if (!Songs.empty())
1030 if (s != Songs.current()->value())
1032 auto begin = Songs.beginV(), end = Songs.endV();
1033 auto it = std::find(begin, end, s);
1034 if (it != end)
1035 Songs.highlight(it-begin);
1037 nextColumn();
1038 nextColumn();
1040 else // invalid album was added, clear the list
1041 Albums.clear();
1043 else // invalid tag was added, clear the list
1044 Tags.clear();
1045 refresh();
1048 namespace {
1050 std::string AlbumToString(const AlbumEntry &ae)
1052 std::string result;
1053 if (ae.isAllTracksEntry())
1054 result = "All tracks";
1055 else
1057 if (hasTwoColumns)
1059 if (ae.entry().tag().empty())
1060 result += Config.empty_tag;
1061 else
1062 result += ae.entry().tag();
1063 result += " - ";
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();
1069 return result;
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())
1087 return filter;
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)
1098 if (tags.empty())
1099 return false;
1101 auto equals_fun_argument = [&](PrimaryTag &e) {
1102 return e.tag() == primary_tag;
1105 if (equals_fun_argument(*tags.currentV()))
1106 return true;
1108 auto begin = tags.beginV(), end = tags.endV();
1109 auto it = std::find_if(begin, end, equals_fun_argument);
1110 if (it != end)
1112 tags.highlight(it-begin);
1113 return true;
1116 return false;
1119 bool MoveToAlbum(NC::Menu<AlbumEntry> &albums, const std::string &primary_tag, const MPD::Song &s)
1121 if (albums.empty())
1122 return false;
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()))
1134 return true;
1136 auto begin = albums.beginV(), end = albums.endV();
1137 auto it = std::find_if(begin, end, equals_fun_argument);
1138 if (it != end)
1140 albums.highlight(it-begin);
1141 return true;
1144 return false;