status: store status fields seperately
[ncmpcpp.git] / src / media_library.cpp
blob2ec1acf9b5138d726d779474acce8082bb999962
1 /***************************************************************************
2 * Copyright (C) 2008-2014 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/bind.hpp>
22 #include <boost/date_time/posix_time/posix_time.hpp>
23 #include <boost/locale/conversion.hpp>
24 #include <algorithm>
25 #include <array>
26 #include <cassert>
28 #include "charset.h"
29 #include "display.h"
30 #include "helpers.h"
31 #include "global.h"
32 #include "media_library.h"
33 #include "mpdpp.h"
34 #include "playlist.h"
35 #include "regex_filter.h"
36 #include "status.h"
37 #include "statusbar.h"
38 #include "utility/comparators.h"
39 #include "utility/type_conversions.h"
40 #include "title.h"
41 #include "screen_switcher.h"
43 using Global::MainHeight;
44 using Global::MainStartY;
45 using Global::myScreen;
47 MediaLibrary *myLibrary;
49 namespace {
51 const auto ml_wtimeout = 250;
52 const auto fetch_delay = boost::posix_time::milliseconds(ml_wtimeout);
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::AlbumEntry AlbumEntry;
64 std::string AlbumToString(const AlbumEntry &ae);
65 std::string SongToString(const MPD::Song &s);
67 bool TagEntryMatcher(const boost::regex &rx, const MediaLibrary::PrimaryTag &tagmtime);
68 bool AlbumEntryMatcher(const boost::regex &rx, const NC::Menu<AlbumEntry>::Item &item, bool filter);
69 bool SongEntryMatcher(const boost::regex &rx, const MPD::Song &s);
71 struct SortSongs {
72 typedef NC::Menu<MPD::Song>::Item SongItem;
74 static const std::array<MPD::Song::GetFunction, 3> GetFuns;
76 LocaleStringComparison m_cmp;
77 std::ptrdiff_t m_offset;
79 public:
80 SortSongs(bool disc_only)
81 : m_cmp(std::locale(), Config.ignore_leading_the), m_offset(disc_only ? 2 : 0) { }
83 bool operator()(const SongItem &a, const SongItem &b) {
84 return (*this)(a.value(), b.value());
86 bool operator()(const MPD::Song &a, const MPD::Song &b) {
87 for (auto get = GetFuns.begin()+m_offset; get != GetFuns.end(); ++get) {
88 int ret = m_cmp(a.getTags(*get, Config.tags_separator),
89 b.getTags(*get, Config.tags_separator));
90 if (ret != 0)
91 return ret < 0;
93 return a.getTrack() < b.getTrack();
97 const std::array<MPD::Song::GetFunction, 3> SortSongs::GetFuns = {{
98 &MPD::Song::getDate,
99 &MPD::Song::getAlbum,
100 &MPD::Song::getDisc
103 class SortAlbumEntries {
104 typedef MediaLibrary::Album Album;
106 LocaleStringComparison m_cmp;
108 public:
109 SortAlbumEntries() : m_cmp(std::locale(), Config.ignore_leading_the) { }
111 bool operator()(const AlbumEntry &a, const AlbumEntry &b) const {
112 return (*this)(a.entry(), b.entry());
115 bool operator()(const Album &a, const Album &b) const {
116 if (Config.media_library_sort_by_mtime)
117 return a.mtime() > b.mtime();
118 else
120 int result;
121 result = m_cmp(a.tag(), b.tag());
122 if (result != 0)
123 return result < 0;
124 result = m_cmp(a.date(), b.date());
125 if (result != 0)
126 return result < 0;
127 return m_cmp(a.album(), b.album()) < 0;
132 class SortPrimaryTags {
133 typedef MediaLibrary::PrimaryTag PrimaryTag;
135 LocaleStringComparison m_cmp;
137 public:
138 SortPrimaryTags() : m_cmp(std::locale(), Config.ignore_leading_the) { }
140 bool operator()(const PrimaryTag &a, const PrimaryTag &b) const {
141 if (Config.media_library_sort_by_mtime)
142 return a.mtime() > b.mtime();
143 else
144 return m_cmp(a.tag(), b.tag()) < 0;
150 MediaLibrary::MediaLibrary()
151 : m_timer(boost::posix_time::from_time_t(0))
153 hasTwoColumns = 0;
154 itsLeftColWidth = COLS/3-1;
155 itsMiddleColWidth = COLS/3;
156 itsMiddleColStartX = itsLeftColWidth+1;
157 itsRightColWidth = COLS-COLS/3*2-1;
158 itsRightColStartX = itsLeftColWidth+itsMiddleColWidth+2;
160 Tags = NC::Menu<PrimaryTag>(0, MainStartY, itsLeftColWidth, MainHeight, Config.titles_visibility ? tagTypeToString(Config.media_lib_primary_tag) + "s" : "", Config.main_color, NC::Border::None);
161 Tags.setHighlightColor(Config.active_column_color);
162 Tags.cyclicScrolling(Config.use_cyclic_scrolling);
163 Tags.centeredCursor(Config.centered_cursor);
164 Tags.setSelectedPrefix(Config.selected_item_prefix);
165 Tags.setSelectedSuffix(Config.selected_item_suffix);
166 Tags.setItemDisplayer([](NC::Menu<PrimaryTag> &menu) {
167 const std::string &tag = menu.drawn()->value().tag();
168 if (tag.empty())
169 menu << Config.empty_tag;
170 else
171 menu << Charset::utf8ToLocale(tag);
174 Albums = NC::Menu<AlbumEntry>(itsMiddleColStartX, MainStartY, itsMiddleColWidth, MainHeight, Config.titles_visibility ? "Albums" : "", Config.main_color, NC::Border::None);
175 Albums.setHighlightColor(Config.main_highlight_color);
176 Albums.cyclicScrolling(Config.use_cyclic_scrolling);
177 Albums.centeredCursor(Config.centered_cursor);
178 Albums.setSelectedPrefix(Config.selected_item_prefix);
179 Albums.setSelectedSuffix(Config.selected_item_suffix);
180 Albums.setItemDisplayer([](NC::Menu<AlbumEntry> &menu) {
181 menu << Charset::utf8ToLocale(AlbumToString(menu.drawn()->value()));
184 Songs = NC::Menu<MPD::Song>(itsRightColStartX, MainStartY, itsRightColWidth, MainHeight, Config.titles_visibility ? "Songs" : "", Config.main_color, NC::Border::None);
185 Songs.setHighlightColor(Config.main_highlight_color);
186 Songs.cyclicScrolling(Config.use_cyclic_scrolling);
187 Songs.centeredCursor(Config.centered_cursor);
188 Songs.setSelectedPrefix(Config.selected_item_prefix);
189 Songs.setSelectedSuffix(Config.selected_item_suffix);
190 Songs.setItemDisplayer(boost::bind(Display::Songs, _1, songsProxyList(), Config.song_library_format));
192 w = &Tags;
195 void MediaLibrary::resize()
197 size_t x_offset, width;
198 getWindowResizeParams(x_offset, width);
199 if (!hasTwoColumns)
201 itsLeftColStartX = x_offset;
202 itsLeftColWidth = width/3-1;
203 itsMiddleColStartX = itsLeftColStartX+itsLeftColWidth+1;
204 itsMiddleColWidth = width/3;
205 itsRightColStartX = itsMiddleColStartX+itsMiddleColWidth+1;
206 itsRightColWidth = width-width/3*2-1;
208 else
210 itsMiddleColStartX = x_offset;
211 itsMiddleColWidth = width/2;
212 itsRightColStartX = x_offset+itsMiddleColWidth+1;
213 itsRightColWidth = width-itsMiddleColWidth-1;
216 Tags.resize(itsLeftColWidth, MainHeight);
217 Albums.resize(itsMiddleColWidth, MainHeight);
218 Songs.resize(itsRightColWidth, MainHeight);
220 Tags.moveTo(itsLeftColStartX, MainStartY);
221 Albums.moveTo(itsMiddleColStartX, MainStartY);
222 Songs.moveTo(itsRightColStartX, MainStartY);
224 hasToBeResized = 0;
227 void MediaLibrary::refresh()
229 Tags.display();
230 drawSeparator(itsMiddleColStartX-1);
231 Albums.display();
232 drawSeparator(itsRightColStartX-1);
233 Songs.display();
234 if (Albums.empty())
236 Albums << NC::XY(0, 0) << "No albums found.";
237 Albums.Window::refresh();
241 void MediaLibrary::switchTo()
243 SwitchTo::execute(this);
244 markSongsInPlaylist(songsProxyList());
245 drawHeader();
246 refresh();
249 std::wstring MediaLibrary::title()
251 return L"Media library";
254 void MediaLibrary::update()
256 if (hasTwoColumns)
258 if (Albums.reallyEmpty() || m_albums_update_request)
260 Albums.clearSearchResults();
261 m_albums_update_request = false;
262 std::map<std::tuple<std::string, std::string, std::string>, time_t> albums;
263 Mpd.GetDirectoryRecursive("/", [&albums](MPD::Song s) {
264 unsigned idx = 0;
265 std::string tag = s.get(Config.media_lib_primary_tag, idx);
268 auto key = std::make_tuple(tag, s.getAlbum(), s.getDate());
269 auto it = albums.find(key);
270 if (it == albums.end())
271 albums[key] = s.getMTime();
272 else
273 it->second = s.getMTime();
275 while (!(tag = s.get(Config.media_lib_primary_tag, ++idx)).empty());
277 withUnfilteredMenuReapplyFilter(Albums, [this, &albums]() {
278 size_t idx = 0;
279 for (auto it = albums.begin(); it != albums.end(); ++it, ++idx)
281 auto &&entry = AlbumEntry(Album(
282 std::move(std::get<0>(it->first)),
283 std::move(std::get<1>(it->first)),
284 std::move(std::get<2>(it->first)),
285 it->second));
286 if (idx < Albums.size())
287 Albums[idx].value() = entry;
288 else
289 Albums.addItem(entry);
291 if (idx < Albums.size())
292 Albums.resizeList(idx);
293 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
295 Albums.refresh();
298 else
300 if (Tags.reallyEmpty() || m_tags_update_request)
302 Tags.clearSearchResults();
303 m_tags_update_request = false;
304 std::map<std::string, time_t> tags;
305 if (Config.media_library_sort_by_mtime)
307 Mpd.GetDirectoryRecursive("/", [&tags](MPD::Song s) {
308 unsigned idx = 0;
309 std::string tag = s.get(Config.media_lib_primary_tag, idx);
312 auto it = tags.find(tag);
313 if (it == tags.end())
314 tags[tag] = s.getMTime();
315 else
316 it->second = std::max(it->second, s.getMTime());
318 while (!(tag = s.get(Config.media_lib_primary_tag, ++idx)).empty());
321 else
323 Mpd.GetList(Config.media_lib_primary_tag, [&tags](std::string tag) {
324 tags[tag] = 0;
327 withUnfilteredMenuReapplyFilter(Tags, [this, &tags]() {
328 size_t idx = 0;
329 for (auto it = tags.begin(); it != tags.end(); ++it, ++idx)
331 auto &&tag = PrimaryTag(std::move(it->first), it->second);
332 if (idx < Tags.size())
333 Tags[idx].value() = tag;
334 else
335 Tags.addItem(tag);
337 if (idx < Tags.size())
338 Tags.resizeList(idx);
339 std::sort(Tags.beginV(), Tags.endV(), SortPrimaryTags());
341 Tags.refresh();
344 if (!Tags.empty()
345 && ((Albums.reallyEmpty() && Global::Timer - m_timer > fetch_delay) || m_albums_update_request)
348 Albums.clearSearchResults();
349 m_albums_update_request = false;
350 auto &primary_tag = Tags.current().value().tag();
351 Mpd.StartSearch(true);
352 Mpd.AddSearch(Config.media_lib_primary_tag, primary_tag);
353 std::map<std::tuple<std::string, std::string>, time_t> albums;
354 Mpd.CommitSearchSongs([&albums](MPD::Song s) {
355 auto key = std::make_tuple(s.getAlbum(), s.getDate());
356 auto it = albums.find(key);
357 if (it == albums.end())
358 albums[key] = s.getMTime();
359 else
360 it->second = s.getMTime();
362 withUnfilteredMenuReapplyFilter(Albums, [this, &albums, &primary_tag]() {
363 size_t idx = 0;
364 for (auto it = albums.begin(); it != albums.end(); ++it, ++idx)
366 auto &&entry = AlbumEntry(Album(
367 primary_tag,
368 std::move(std::get<0>(it->first)),
369 std::move(std::get<1>(it->first)),
370 it->second));
371 if (idx < Albums.size())
373 Albums[idx].value() = entry;
374 Albums[idx].setSeparator(false);
376 else
377 Albums.addItem(entry);
379 if (idx < Albums.size())
380 Albums.resizeList(idx);
381 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
382 if (albums.size() > 1)
384 Albums.addSeparator();
385 Albums.addItem(AlbumEntry::mkAllTracksEntry(primary_tag));
388 Albums.refresh();
392 if (!Albums.empty()
393 && ((Songs.reallyEmpty() && Global::Timer - m_timer > fetch_delay) || m_songs_update_request)
396 Songs.clearSearchResults();
397 m_songs_update_request = false;
398 auto &album = Albums.current().value();
399 Mpd.StartSearch(true);
400 Mpd.AddSearch(Config.media_lib_primary_tag, album.entry().tag());
401 if (!album.isAllTracksEntry())
403 Mpd.AddSearch(MPD_TAG_ALBUM, album.entry().album());
404 Mpd.AddSearch(MPD_TAG_DATE, album.entry().date());
406 withUnfilteredMenuReapplyFilter(Songs, [this, &album]() {
407 size_t idx = 0;
408 Mpd.CommitSearchSongs([this, &idx](MPD::Song s) {
409 bool is_playlist = myPlaylist->checkForSong(s);
410 if (idx < Songs.size())
412 Songs[idx].value() = s;
413 Songs[idx].setBold(is_playlist);
415 else
416 Songs.addItem(s, is_playlist);
417 ++idx;
419 if (idx < Songs.size())
420 Songs.resizeList(idx);
421 std::sort(Songs.begin(), Songs.end(), SortSongs(!album.isAllTracksEntry()));
423 Songs.refresh();
427 int MediaLibrary::windowTimeout()
429 if (Albums.reallyEmpty() || Songs.reallyEmpty())
430 return ml_wtimeout;
431 else
432 return Screen<WindowType>::windowTimeout();
435 void MediaLibrary::enterPressed()
437 AddToPlaylist(true);
440 void MediaLibrary::spacePressed()
442 if (Config.space_selects)
444 if (isActiveWindow(Tags))
446 size_t idx = Tags.choice();
447 Tags[idx].setSelected(!Tags[idx].isSelected());
448 Tags.scroll(NC::Scroll::Down);
449 Albums.clear();
450 Songs.clear();
452 else if (isActiveWindow(Albums))
454 if (!Albums.current().value().isAllTracksEntry())
456 size_t idx = Albums.choice();
457 Albums[idx].setSelected(!Albums[idx].isSelected());
458 Albums.scroll(NC::Scroll::Down);
459 Songs.clear();
462 else if (isActiveWindow(Songs))
464 size_t idx = Songs.choice();
465 Songs[idx].setSelected(!Songs[idx].isSelected());
466 Songs.scroll(NC::Scroll::Down);
469 else
470 AddToPlaylist(false);
473 void MediaLibrary::mouseButtonPressed(MEVENT me)
475 auto tryNextColumn = [this]() -> bool {
476 bool result = true;
477 if (!isActiveWindow(Songs))
479 if (nextColumnAvailable())
480 nextColumn();
481 else
482 result = false;
484 return result;
486 auto tryPreviousColumn = [this]() -> bool {
487 bool result = true;
488 if (!isActiveWindow(Tags))
490 if (previousColumnAvailable())
491 previousColumn();
492 else
493 result = false;
495 return result;
497 if (!Tags.empty() && Tags.hasCoords(me.x, me.y))
499 if (!tryPreviousColumn() || !tryPreviousColumn())
500 return;
501 if (size_t(me.y) < Tags.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
503 Tags.Goto(me.y);
504 if (me.bstate & BUTTON3_PRESSED)
506 size_t pos = Tags.choice();
507 spacePressed();
508 if (pos < Tags.size()-1)
509 Tags.scroll(NC::Scroll::Up);
512 else
513 Screen<WindowType>::mouseButtonPressed(me);
514 Albums.clear();
515 Songs.clear();
517 else if (!Albums.empty() && Albums.hasCoords(me.x, me.y))
519 if (!isActiveWindow(Albums))
521 bool success;
522 if (isActiveWindow(Tags))
523 success = tryNextColumn();
524 else
525 success = tryPreviousColumn();
526 if (!success)
527 return;
529 if (size_t(me.y) < Albums.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
531 Albums.Goto(me.y);
532 if (me.bstate & BUTTON3_PRESSED)
534 size_t pos = Albums.choice();
535 spacePressed();
536 if (pos < Albums.size()-1)
537 Albums.scroll(NC::Scroll::Up);
540 else
541 Screen<WindowType>::mouseButtonPressed(me);
542 Songs.clear();
544 else if (!Songs.empty() && Songs.hasCoords(me.x, me.y))
546 if (!tryNextColumn() || !tryNextColumn())
547 return;
548 if (size_t(me.y) < Songs.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
550 Songs.Goto(me.y);
551 if (me.bstate & BUTTON1_PRESSED)
553 size_t pos = Songs.choice();
554 spacePressed();
555 if (pos < Songs.size()-1)
556 Songs.scroll(NC::Scroll::Up);
558 else
559 enterPressed();
561 else
562 Screen<WindowType>::mouseButtonPressed(me);
566 /***********************************************************************/
568 bool MediaLibrary::allowsFiltering()
570 return true;
573 std::string MediaLibrary::currentFilter()
575 std::string filter;
576 if (isActiveWindow(Tags))
577 filter = RegexFilter<PrimaryTag>::currentFilter(Tags);
578 else if (isActiveWindow(Albums))
579 filter = RegexItemFilter<AlbumEntry>::currentFilter(Albums);
580 else if (isActiveWindow(Songs))
581 filter = RegexFilter<MPD::Song>::currentFilter(Songs);
582 return filter;
585 void MediaLibrary::applyFilter(const std::string &filter)
587 if (filter.empty())
589 if (isActiveWindow(Tags))
591 Tags.clearFilter();
592 Tags.clearFilterResults();
594 else if (isActiveWindow(Albums))
596 Albums.clearFilter();
597 Albums.clearFilterResults();
599 else if (isActiveWindow(Songs))
601 Songs.clearFilter();
602 Songs.clearFilterResults();
604 return;
608 if (isActiveWindow(Tags))
610 Tags.showAll();
611 auto rx = RegexFilter<PrimaryTag>(
612 boost::regex(filter, Config.regex_type), TagEntryMatcher);
613 Tags.filter(Tags.begin(), Tags.end(), rx);
615 else if (isActiveWindow(Albums))
617 Albums.showAll();
618 auto fun = boost::bind(AlbumEntryMatcher, _1, _2, true);
619 auto rx = RegexItemFilter<AlbumEntry>(
620 boost::regex(filter, Config.regex_type), fun);
621 Albums.filter(Albums.begin(), Albums.end(), rx);
623 else if (isActiveWindow(Songs))
625 Songs.showAll();
626 auto rx = RegexFilter<MPD::Song>(
627 boost::regex(filter, Config.regex_type), SongEntryMatcher);
628 Songs.filter(Songs.begin(), Songs.end(), rx);
631 catch (boost::bad_expression &) { }
634 /***********************************************************************/
636 bool MediaLibrary::allowsSearching()
638 return true;
641 bool MediaLibrary::search(const std::string &constraint)
643 if (constraint.empty())
645 if (isActiveWindow(Tags))
646 Tags.clearSearchResults();
647 else if (isActiveWindow(Albums))
648 Albums.clearSearchResults();
649 else if (isActiveWindow(Songs))
650 Songs.clearSearchResults();
651 return false;
655 bool result = false;
656 if (isActiveWindow(Tags))
658 auto rx = RegexFilter<PrimaryTag>(
659 boost::regex(constraint, Config.regex_type), TagEntryMatcher);
660 result = Tags.search(Tags.begin(), Tags.end(), rx);
662 else if (isActiveWindow(Albums))
664 auto fun = boost::bind(AlbumEntryMatcher, _1, _2, false);
665 auto rx = RegexItemFilter<AlbumEntry>(
666 boost::regex(constraint, Config.regex_type), fun);
667 result = Albums.search(Albums.begin(), Albums.end(), rx);
669 else if (isActiveWindow(Songs))
671 auto rx = RegexFilter<MPD::Song>(
672 boost::regex(constraint, Config.regex_type), SongEntryMatcher);
673 result = Songs.search(Songs.begin(), Songs.end(), rx);
675 return result;
677 catch (boost::bad_expression &)
679 return false;
683 void MediaLibrary::nextFound(bool wrap)
685 if (isActiveWindow(Tags))
686 Tags.nextFound(wrap);
687 else if (isActiveWindow(Albums))
688 Albums.nextFound(wrap);
689 else if (isActiveWindow(Songs))
690 Songs.nextFound(wrap);
693 void MediaLibrary::prevFound(bool wrap)
695 if (isActiveWindow(Tags))
696 Tags.prevFound(wrap);
697 else if (isActiveWindow(Albums))
698 Albums.prevFound(wrap);
699 else if (isActiveWindow(Songs))
700 Songs.prevFound(wrap);
703 /***********************************************************************/
705 ProxySongList MediaLibrary::proxySongList()
707 auto ptr = ProxySongList();
708 if (isActiveWindow(Songs))
709 ptr = songsProxyList();
710 return ptr;
713 bool MediaLibrary::allowsSelection()
715 return true;
718 void MediaLibrary::reverseSelection()
720 if (isActiveWindow(Tags))
721 reverseSelectionHelper(Tags.begin(), Tags.end());
722 else if (isActiveWindow(Albums))
724 // omit "All tracks"
725 if (Albums.size() > 1)
726 reverseSelectionHelper(Albums.begin(), Albums.end()-2);
727 else
728 reverseSelectionHelper(Albums.begin(), Albums.end());
730 else if (isActiveWindow(Songs))
731 reverseSelectionHelper(Songs.begin(), Songs.end());
734 MPD::SongList MediaLibrary::getSelectedSongs()
736 MPD::SongList result;
737 if (isActiveWindow(Tags))
739 auto tag_handler = [&result](const std::string &tag) {
740 Mpd.StartSearch(true);
741 Mpd.AddSearch(Config.media_lib_primary_tag, tag);
742 Mpd.CommitSearchSongs(vectorMoveInserter(result));
744 bool any_selected = false;
745 for (auto &e : Tags)
747 if (e.isSelected())
749 any_selected = true;
750 tag_handler(e.value().tag());
753 // if no item is selected, add current one
754 if (!any_selected && !Tags.empty())
755 tag_handler(Tags.current().value().tag());
757 else if (isActiveWindow(Albums))
759 bool any_selected = false;
760 for (auto it = Albums.begin(); it != Albums.end() && !it->isSeparator(); ++it)
762 if (it->isSelected())
764 any_selected = true;
765 auto &sc = it->value();
766 Mpd.StartSearch(true);
767 if (hasTwoColumns)
768 Mpd.AddSearch(Config.media_lib_primary_tag, sc.entry().tag());
769 else
770 Mpd.AddSearch(Config.media_lib_primary_tag,
771 Tags.current().value().tag());
772 Mpd.AddSearch(MPD_TAG_ALBUM, sc.entry().album());
773 Mpd.AddSearch(MPD_TAG_DATE, sc.entry().date());
774 size_t begin = result.size();
775 Mpd.CommitSearchSongs(vectorMoveInserter(result));
776 std::sort(result.begin()+begin, result.end(), SortSongs(false));
779 // if no item is selected, add songs from right column
780 if (!any_selected && !Albums.empty())
782 withUnfilteredMenu(Songs, [this, &result]() {
783 result.insert(result.end(), Songs.beginV(), Songs.endV());
787 else if (isActiveWindow(Songs))
789 for (auto it = Songs.begin(); it != Songs.end(); ++it)
790 if (it->isSelected())
791 result.push_back(it->value());
792 // if no item is selected, add current one
793 if (result.empty() && !Songs.empty())
794 result.push_back(Songs.current().value());
796 return result;
799 /***********************************************************************/
801 bool MediaLibrary::previousColumnAvailable()
803 assert(!hasTwoColumns || !isActiveWindow(Tags));
804 if (isActiveWindow(Songs))
806 if (!Albums.reallyEmpty() && (hasTwoColumns || !Tags.reallyEmpty()))
807 return true;
809 else if (isActiveWindow(Albums))
811 if (!hasTwoColumns && !Tags.reallyEmpty())
812 return true;
814 return false;
817 void MediaLibrary::previousColumn()
819 if (isActiveWindow(Songs))
821 Songs.setHighlightColor(Config.main_highlight_color);
822 w->refresh();
823 w = &Albums;
824 Albums.setHighlightColor(Config.active_column_color);
826 else if (isActiveWindow(Albums) && !hasTwoColumns)
828 Albums.setHighlightColor(Config.main_highlight_color);
829 w->refresh();
830 w = &Tags;
831 Tags.setHighlightColor(Config.active_column_color);
835 bool MediaLibrary::nextColumnAvailable()
837 assert(!hasTwoColumns || !isActiveWindow(Tags));
838 if (isActiveWindow(Tags))
840 if (!Albums.reallyEmpty() && !Songs.reallyEmpty())
841 return true;
843 else if (isActiveWindow(Albums))
845 if (!Songs.reallyEmpty())
846 return true;
848 return false;
851 void MediaLibrary::nextColumn()
853 if (isActiveWindow(Tags))
855 Tags.setHighlightColor(Config.main_highlight_color);
856 w->refresh();
857 w = &Albums;
858 Albums.setHighlightColor(Config.active_column_color);
860 else if (isActiveWindow(Albums))
862 Albums.setHighlightColor(Config.main_highlight_color);
863 w->refresh();
864 w = &Songs;
865 Songs.setHighlightColor(Config.active_column_color);
869 /***********************************************************************/
871 void MediaLibrary::updateTimer()
873 m_timer = Global::Timer;
876 void MediaLibrary::toggleColumnsMode()
878 hasTwoColumns = !hasTwoColumns;
879 Tags.clear();
880 Albums.clear();
881 Albums.reset();
882 Songs.clear();
883 if (hasTwoColumns)
885 if (isActiveWindow(Tags))
886 nextColumn();
887 if (Config.titles_visibility)
889 std::string item_type = boost::locale::to_lower(
890 tagTypeToString(Config.media_lib_primary_tag));
891 std::string and_mtime = Config.media_library_sort_by_mtime ? " and mtime" : "";
892 Albums.setTitle("Albums (sorted by " + item_type + and_mtime + ")");
895 else
896 Albums.setTitle(Config.titles_visibility ? "Albums" : "");
897 resize();
900 int MediaLibrary::Columns()
902 if (hasTwoColumns)
903 return 2;
904 else
905 return 3;
908 ProxySongList MediaLibrary::songsProxyList()
910 return ProxySongList(Songs, [](NC::Menu<MPD::Song>::Item &item) {
911 return &item.value();
915 void MediaLibrary::toggleSortMode()
917 Config.media_library_sort_by_mtime = !Config.media_library_sort_by_mtime;
918 Statusbar::printf("Sorting library by: %1%",
919 Config.media_library_sort_by_mtime ? "modification time" : "name");
920 if (hasTwoColumns)
922 withUnfilteredMenuReapplyFilter(Albums, [this]() {
923 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
925 Albums.refresh();
926 Songs.clear();
927 if (Config.titles_visibility)
929 std::string item_type = boost::locale::to_lower(
930 tagTypeToString(Config.media_lib_primary_tag));
931 std::string and_mtime = Config.media_library_sort_by_mtime ? (" " "and mtime") : "";
932 Albums.setTitle("Albums (sorted by " + item_type + and_mtime + ")");
935 else
937 withUnfilteredMenuReapplyFilter(Tags, [this]() {
938 // if we already have modification times,
939 // just resort. otherwise refetch the list.
940 if (!Tags.empty() && Tags[0].value().mtime() > 0)
942 std::sort(Tags.beginV(), Tags.endV(), SortPrimaryTags());
943 Tags.refresh();
945 else
946 Tags.clear();
948 Albums.clear();
949 Songs.clear();
951 update();
954 void MediaLibrary::LocateSong(const MPD::Song &s)
956 std::string primary_tag = s.get(Config.media_lib_primary_tag);
957 if (primary_tag.empty())
959 std::string item_type = boost::locale::to_lower(
960 tagTypeToString(Config.media_lib_primary_tag));
961 Statusbar::printf("Can't use this function because the song has no %s tag", item_type);
962 return;
964 if (!s.isFromDatabase())
966 Statusbar::print("Song is not from the database");
967 return;
970 if (myScreen != this)
971 switchTo();
972 Statusbar::put() << "Jumping to song...";
973 Global::wFooter->refresh();
975 if (!hasTwoColumns)
977 Tags.showAll();
978 if (Tags.empty())
979 update();
980 if (primary_tag != Tags.current().value().tag())
982 for (size_t i = 0; i < Tags.size(); ++i)
984 if (primary_tag == Tags[i].value().tag())
986 Tags.highlight(i);
987 Albums.clear();
988 Songs.clear();
989 break;
995 Albums.showAll();
996 if (Albums.empty())
997 update();
999 std::string album = s.getAlbum();
1000 std::string date = s.getDate();
1001 if ((hasTwoColumns && Albums.current().value().entry().tag() != primary_tag)
1002 || album != Albums.current().value().entry().album()
1003 || date != Albums.current().value().entry().date())
1005 for (size_t i = 0; i < Albums.size(); ++i)
1007 if ((!hasTwoColumns || Albums[i].value().entry().tag() == primary_tag)
1008 && album == Albums[i].value().entry().album()
1009 && date == Albums[i].value().entry().date())
1011 Albums.highlight(i);
1012 Songs.clear();
1013 break;
1018 Songs.showAll();
1019 if (Songs.empty())
1020 update();
1022 if (s != Songs.current().value())
1024 for (size_t i = 0; i < Songs.size(); ++i)
1026 if (s == Songs[i].value())
1028 Songs.highlight(i);
1029 break;
1034 Tags.setHighlightColor(Config.main_highlight_color);
1035 Albums.setHighlightColor(Config.main_highlight_color);
1036 Songs.setHighlightColor(Config.active_column_color);
1037 w = &Songs;
1038 refresh();
1041 void MediaLibrary::AddToPlaylist(bool add_n_play)
1043 if (isActiveWindow(Songs) && !Songs.empty())
1044 addSongToPlaylist(Songs.current().value(), add_n_play);
1045 else
1047 if ((!Tags.empty() && isActiveWindow(Tags))
1048 || (isActiveWindow(Albums) && Albums.current().value().isAllTracksEntry()))
1050 MPD::SongList list;
1051 Mpd.StartSearch(true);
1052 Mpd.AddSearch(Config.media_lib_primary_tag, Tags.current().value().tag());
1053 Mpd.CommitSearchSongs(vectorMoveInserter(list));
1054 bool success = addSongsToPlaylist(list.begin(), list.end(), add_n_play, -1);
1055 std::string tag_type = boost::locale::to_lower(
1056 tagTypeToString(Config.media_lib_primary_tag));
1057 Statusbar::printf("Songs with %1% \"%2%\" added%3%",
1058 tag_type, Tags.current().value().tag(), withErrors(success)
1061 else if (isActiveWindow(Albums))
1063 bool success;
1064 withUnfilteredMenu(Songs, [&]() {
1065 success = addSongsToPlaylist(Songs.beginV(), Songs.endV(), add_n_play, -1);
1067 Statusbar::printf("Songs from album \"%1%\" added%2%",
1068 Albums.current().value().entry().album(), withErrors(success)
1073 if (!add_n_play)
1075 w->scroll(NC::Scroll::Down);
1076 if (isActiveWindow(Tags))
1078 Albums.clear();
1079 Songs.clear();
1081 else if (isActiveWindow(Albums))
1082 Songs.clear();
1086 namespace {//
1088 std::string AlbumToString(const AlbumEntry &ae)
1090 std::string result;
1091 if (ae.isAllTracksEntry())
1092 result = "All tracks";
1093 else
1095 if (hasTwoColumns)
1097 if (ae.entry().tag().empty())
1098 result += Config.empty_tag;
1099 else
1100 result += ae.entry().tag();
1101 result += " - ";
1103 if (Config.media_lib_primary_tag != MPD_TAG_DATE && !ae.entry().date().empty())
1104 result += "(" + ae.entry().date() + ") ";
1105 result += ae.entry().album().empty() ? "<no album>" : ae.entry().album();
1107 return result;
1110 std::string SongToString(const MPD::Song &s)
1112 return s.toString(Config.song_library_format, Config.tags_separator);
1115 bool TagEntryMatcher(const boost::regex &rx, const MediaLibrary::PrimaryTag &pt)
1117 return boost::regex_search(pt.tag(), rx);
1120 bool AlbumEntryMatcher(const boost::regex &rx, const NC::Menu<AlbumEntry>::Item &item, bool filter)
1122 if (item.isSeparator() || item.value().isAllTracksEntry())
1123 return filter;
1124 return boost::regex_search(AlbumToString(item.value()), rx);
1127 bool SongEntryMatcher(const boost::regex &rx, const MPD::Song &s)
1129 return boost::regex_search(SongToString(s), rx);