settings: add option to disable data fetching delay in media library and playlist...
[ncmpcpp.git] / src / media_library.cpp
blob496cc3a61dba8289adac568fb353f19ee605d294
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 bool hasTwoColumns;
52 size_t itsLeftColStartX;
53 size_t itsLeftColWidth;
54 size_t itsMiddleColWidth;
55 size_t itsMiddleColStartX;
56 size_t itsRightColWidth;
57 size_t itsRightColStartX;
59 typedef MediaLibrary::AlbumEntry AlbumEntry;
61 std::string AlbumToString(const AlbumEntry &ae);
62 std::string SongToString(const MPD::Song &s);
64 bool TagEntryMatcher(const boost::regex &rx, const MediaLibrary::PrimaryTag &tagmtime);
65 bool AlbumEntryMatcher(const boost::regex &rx, const NC::Menu<AlbumEntry>::Item &item, bool filter);
66 bool SongEntryMatcher(const boost::regex &rx, const MPD::Song &s);
68 struct SortSongs {
69 typedef NC::Menu<MPD::Song>::Item SongItem;
71 static const std::array<MPD::Song::GetFunction, 3> GetFuns;
73 LocaleStringComparison m_cmp;
74 std::ptrdiff_t m_offset;
76 public:
77 SortSongs(bool disc_only)
78 : m_cmp(std::locale(), Config.ignore_leading_the), m_offset(disc_only ? 2 : 0) { }
80 bool operator()(const SongItem &a, const SongItem &b) {
81 return (*this)(a.value(), b.value());
83 bool operator()(const MPD::Song &a, const MPD::Song &b) {
84 for (auto get = GetFuns.begin()+m_offset; get != GetFuns.end(); ++get) {
85 int ret = m_cmp(a.getTags(*get, Config.tags_separator),
86 b.getTags(*get, Config.tags_separator));
87 if (ret != 0)
88 return ret < 0;
90 return a.getTrack() < b.getTrack();
94 const std::array<MPD::Song::GetFunction, 3> SortSongs::GetFuns = {{
95 &MPD::Song::getDate,
96 &MPD::Song::getAlbum,
97 &MPD::Song::getDisc
98 }};
100 class SortAlbumEntries {
101 typedef MediaLibrary::Album Album;
103 LocaleStringComparison m_cmp;
105 public:
106 SortAlbumEntries() : m_cmp(std::locale(), Config.ignore_leading_the) { }
108 bool operator()(const AlbumEntry &a, const AlbumEntry &b) const {
109 return (*this)(a.entry(), b.entry());
112 bool operator()(const Album &a, const Album &b) const {
113 if (Config.media_library_sort_by_mtime)
114 return a.mtime() > b.mtime();
115 else
117 int result;
118 result = m_cmp(a.tag(), b.tag());
119 if (result != 0)
120 return result < 0;
121 result = m_cmp(a.date(), b.date());
122 if (result != 0)
123 return result < 0;
124 return m_cmp(a.album(), b.album()) < 0;
129 class SortPrimaryTags {
130 typedef MediaLibrary::PrimaryTag PrimaryTag;
132 LocaleStringComparison m_cmp;
134 public:
135 SortPrimaryTags() : m_cmp(std::locale(), Config.ignore_leading_the) { }
137 bool operator()(const PrimaryTag &a, const PrimaryTag &b) const {
138 if (Config.media_library_sort_by_mtime)
139 return a.mtime() > b.mtime();
140 else
141 return m_cmp(a.tag(), b.tag()) < 0;
147 MediaLibrary::MediaLibrary()
148 : m_timer(boost::posix_time::from_time_t(0))
149 , m_window_timeout(Config.data_fetching_delay ? 250 : 500)
150 , m_fetching_delay(boost::posix_time::milliseconds(Config.data_fetching_delay ? 250 : -1))
152 hasTwoColumns = 0;
153 itsLeftColWidth = COLS/3-1;
154 itsMiddleColWidth = COLS/3;
155 itsMiddleColStartX = itsLeftColWidth+1;
156 itsRightColWidth = COLS-COLS/3*2-1;
157 itsRightColStartX = itsLeftColWidth+itsMiddleColWidth+2;
159 Tags = NC::Menu<PrimaryTag>(0, MainStartY, itsLeftColWidth, MainHeight, Config.titles_visibility ? tagTypeToString(Config.media_lib_primary_tag) + "s" : "", Config.main_color, NC::Border::None);
160 Tags.setHighlightColor(Config.active_column_color);
161 Tags.cyclicScrolling(Config.use_cyclic_scrolling);
162 Tags.centeredCursor(Config.centered_cursor);
163 Tags.setSelectedPrefix(Config.selected_item_prefix);
164 Tags.setSelectedSuffix(Config.selected_item_suffix);
165 Tags.setItemDisplayer([](NC::Menu<PrimaryTag> &menu) {
166 const std::string &tag = menu.drawn()->value().tag();
167 if (tag.empty())
168 menu << Config.empty_tag;
169 else
170 menu << Charset::utf8ToLocale(tag);
173 Albums = NC::Menu<AlbumEntry>(itsMiddleColStartX, MainStartY, itsMiddleColWidth, MainHeight, Config.titles_visibility ? "Albums" : "", Config.main_color, NC::Border::None);
174 Albums.setHighlightColor(Config.main_highlight_color);
175 Albums.cyclicScrolling(Config.use_cyclic_scrolling);
176 Albums.centeredCursor(Config.centered_cursor);
177 Albums.setSelectedPrefix(Config.selected_item_prefix);
178 Albums.setSelectedSuffix(Config.selected_item_suffix);
179 Albums.setItemDisplayer([](NC::Menu<AlbumEntry> &menu) {
180 menu << Charset::utf8ToLocale(AlbumToString(menu.drawn()->value()));
183 Songs = NC::Menu<MPD::Song>(itsRightColStartX, MainStartY, itsRightColWidth, MainHeight, Config.titles_visibility ? "Songs" : "", Config.main_color, NC::Border::None);
184 Songs.setHighlightColor(Config.main_highlight_color);
185 Songs.cyclicScrolling(Config.use_cyclic_scrolling);
186 Songs.centeredCursor(Config.centered_cursor);
187 Songs.setSelectedPrefix(Config.selected_item_prefix);
188 Songs.setSelectedSuffix(Config.selected_item_suffix);
189 Songs.setItemDisplayer(boost::bind(Display::Songs, _1, songsProxyList(), Config.song_library_format));
191 w = &Tags;
194 void MediaLibrary::resize()
196 size_t x_offset, width;
197 getWindowResizeParams(x_offset, width);
198 if (!hasTwoColumns)
200 itsLeftColStartX = x_offset;
201 itsLeftColWidth = width/3-1;
202 itsMiddleColStartX = itsLeftColStartX+itsLeftColWidth+1;
203 itsMiddleColWidth = width/3;
204 itsRightColStartX = itsMiddleColStartX+itsMiddleColWidth+1;
205 itsRightColWidth = width-width/3*2-1;
207 else
209 itsMiddleColStartX = x_offset;
210 itsMiddleColWidth = width/2;
211 itsRightColStartX = x_offset+itsMiddleColWidth+1;
212 itsRightColWidth = width-itsMiddleColWidth-1;
215 Tags.resize(itsLeftColWidth, MainHeight);
216 Albums.resize(itsMiddleColWidth, MainHeight);
217 Songs.resize(itsRightColWidth, MainHeight);
219 Tags.moveTo(itsLeftColStartX, MainStartY);
220 Albums.moveTo(itsMiddleColStartX, MainStartY);
221 Songs.moveTo(itsRightColStartX, MainStartY);
223 hasToBeResized = 0;
226 void MediaLibrary::refresh()
228 Tags.display();
229 drawSeparator(itsMiddleColStartX-1);
230 Albums.display();
231 drawSeparator(itsRightColStartX-1);
232 Songs.display();
233 if (Albums.empty())
235 Albums << NC::XY(0, 0) << "No albums found.";
236 Albums.Window::refresh();
240 void MediaLibrary::switchTo()
242 SwitchTo::execute(this);
243 markSongsInPlaylist(songsProxyList());
244 drawHeader();
245 refresh();
248 std::wstring MediaLibrary::title()
250 return L"Media library";
253 void MediaLibrary::update()
255 if (hasTwoColumns)
257 if (Albums.reallyEmpty() || m_albums_update_request)
259 Albums.clearSearchResults();
260 m_albums_update_request = false;
261 std::map<std::tuple<std::string, std::string, std::string>, time_t> albums;
262 Mpd.GetDirectoryRecursive("/", [&albums](MPD::Song s) {
263 unsigned idx = 0;
264 std::string tag = s.get(Config.media_lib_primary_tag, idx);
267 auto key = std::make_tuple(tag, s.getAlbum(), s.getDate());
268 auto it = albums.find(key);
269 if (it == albums.end())
270 albums[key] = s.getMTime();
271 else
272 it->second = s.getMTime();
274 while (!(tag = s.get(Config.media_lib_primary_tag, ++idx)).empty());
276 withUnfilteredMenuReapplyFilter(Albums, [this, &albums]() {
277 size_t idx = 0;
278 for (auto it = albums.begin(); it != albums.end(); ++it, ++idx)
280 auto &&entry = AlbumEntry(Album(
281 std::move(std::get<0>(it->first)),
282 std::move(std::get<1>(it->first)),
283 std::move(std::get<2>(it->first)),
284 it->second));
285 if (idx < Albums.size())
286 Albums[idx].value() = entry;
287 else
288 Albums.addItem(entry);
290 if (idx < Albums.size())
291 Albums.resizeList(idx);
292 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
294 Albums.refresh();
297 else
299 if (Tags.reallyEmpty() || m_tags_update_request)
301 Tags.clearSearchResults();
302 m_tags_update_request = false;
303 std::map<std::string, time_t> tags;
304 if (Config.media_library_sort_by_mtime)
306 Mpd.GetDirectoryRecursive("/", [&tags](MPD::Song s) {
307 unsigned idx = 0;
308 std::string tag = s.get(Config.media_lib_primary_tag, idx);
311 auto it = tags.find(tag);
312 if (it == tags.end())
313 tags[tag] = s.getMTime();
314 else
315 it->second = std::max(it->second, s.getMTime());
317 while (!(tag = s.get(Config.media_lib_primary_tag, ++idx)).empty());
320 else
322 Mpd.GetList(Config.media_lib_primary_tag, [&tags](std::string tag) {
323 tags[tag] = 0;
326 withUnfilteredMenuReapplyFilter(Tags, [this, &tags]() {
327 size_t idx = 0;
328 for (auto it = tags.begin(); it != tags.end(); ++it, ++idx)
330 auto &&tag = PrimaryTag(std::move(it->first), it->second);
331 if (idx < Tags.size())
332 Tags[idx].value() = tag;
333 else
334 Tags.addItem(tag);
336 if (idx < Tags.size())
337 Tags.resizeList(idx);
338 std::sort(Tags.beginV(), Tags.endV(), SortPrimaryTags());
340 Tags.refresh();
343 if (!Tags.empty()
344 && ((Albums.reallyEmpty() && Global::Timer - m_timer > m_fetching_delay) || m_albums_update_request)
347 Albums.clearSearchResults();
348 m_albums_update_request = false;
349 auto &primary_tag = Tags.current().value().tag();
350 Mpd.StartSearch(true);
351 Mpd.AddSearch(Config.media_lib_primary_tag, primary_tag);
352 std::map<std::tuple<std::string, std::string>, time_t> albums;
353 Mpd.CommitSearchSongs([&albums](MPD::Song s) {
354 auto key = std::make_tuple(s.getAlbum(), s.getDate());
355 auto it = albums.find(key);
356 if (it == albums.end())
357 albums[key] = s.getMTime();
358 else
359 it->second = s.getMTime();
361 withUnfilteredMenuReapplyFilter(Albums, [this, &albums, &primary_tag]() {
362 size_t idx = 0;
363 for (auto it = albums.begin(); it != albums.end(); ++it, ++idx)
365 auto &&entry = AlbumEntry(Album(
366 primary_tag,
367 std::move(std::get<0>(it->first)),
368 std::move(std::get<1>(it->first)),
369 it->second));
370 if (idx < Albums.size())
372 Albums[idx].value() = entry;
373 Albums[idx].setSeparator(false);
375 else
376 Albums.addItem(entry);
378 if (idx < Albums.size())
379 Albums.resizeList(idx);
380 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
381 if (albums.size() > 1)
383 Albums.addSeparator();
384 Albums.addItem(AlbumEntry::mkAllTracksEntry(primary_tag));
387 Albums.refresh();
391 if (!Albums.empty()
392 && ((Songs.reallyEmpty() && Global::Timer - m_timer > m_fetching_delay) || m_songs_update_request)
395 Songs.clearSearchResults();
396 m_songs_update_request = false;
397 auto &album = Albums.current().value();
398 Mpd.StartSearch(true);
399 Mpd.AddSearch(Config.media_lib_primary_tag, album.entry().tag());
400 if (!album.isAllTracksEntry())
402 Mpd.AddSearch(MPD_TAG_ALBUM, album.entry().album());
403 Mpd.AddSearch(MPD_TAG_DATE, album.entry().date());
405 withUnfilteredMenuReapplyFilter(Songs, [this, &album]() {
406 size_t idx = 0;
407 Mpd.CommitSearchSongs([this, &idx](MPD::Song s) {
408 bool is_playlist = myPlaylist->checkForSong(s);
409 if (idx < Songs.size())
411 Songs[idx].value() = s;
412 Songs[idx].setBold(is_playlist);
414 else
415 Songs.addItem(s, is_playlist);
416 ++idx;
418 if (idx < Songs.size())
419 Songs.resizeList(idx);
420 std::sort(Songs.begin(), Songs.end(), SortSongs(!album.isAllTracksEntry()));
422 Songs.refresh();
426 int MediaLibrary::windowTimeout()
428 if (Albums.reallyEmpty() || Songs.reallyEmpty())
429 return m_window_timeout;
430 else
431 return Screen<WindowType>::windowTimeout();
434 void MediaLibrary::enterPressed()
436 AddToPlaylist(true);
439 void MediaLibrary::spacePressed()
441 if (Config.space_selects)
443 if (isActiveWindow(Tags))
445 size_t idx = Tags.choice();
446 Tags[idx].setSelected(!Tags[idx].isSelected());
447 Tags.scroll(NC::Scroll::Down);
448 Albums.clear();
449 Songs.clear();
451 else if (isActiveWindow(Albums))
453 if (!Albums.current().value().isAllTracksEntry())
455 size_t idx = Albums.choice();
456 Albums[idx].setSelected(!Albums[idx].isSelected());
457 Albums.scroll(NC::Scroll::Down);
458 Songs.clear();
461 else if (isActiveWindow(Songs))
463 size_t idx = Songs.choice();
464 Songs[idx].setSelected(!Songs[idx].isSelected());
465 Songs.scroll(NC::Scroll::Down);
468 else
469 AddToPlaylist(false);
472 void MediaLibrary::mouseButtonPressed(MEVENT me)
474 auto tryNextColumn = [this]() -> bool {
475 bool result = true;
476 if (!isActiveWindow(Songs))
478 if (nextColumnAvailable())
479 nextColumn();
480 else
481 result = false;
483 return result;
485 auto tryPreviousColumn = [this]() -> bool {
486 bool result = true;
487 if (!isActiveWindow(Tags))
489 if (previousColumnAvailable())
490 previousColumn();
491 else
492 result = false;
494 return result;
496 if (!Tags.empty() && Tags.hasCoords(me.x, me.y))
498 if (!tryPreviousColumn() || !tryPreviousColumn())
499 return;
500 if (size_t(me.y) < Tags.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
502 Tags.Goto(me.y);
503 if (me.bstate & BUTTON3_PRESSED)
505 size_t pos = Tags.choice();
506 spacePressed();
507 if (pos < Tags.size()-1)
508 Tags.scroll(NC::Scroll::Up);
511 else
512 Screen<WindowType>::mouseButtonPressed(me);
513 Albums.clear();
514 Songs.clear();
516 else if (!Albums.empty() && Albums.hasCoords(me.x, me.y))
518 if (!isActiveWindow(Albums))
520 bool success;
521 if (isActiveWindow(Tags))
522 success = tryNextColumn();
523 else
524 success = tryPreviousColumn();
525 if (!success)
526 return;
528 if (size_t(me.y) < Albums.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
530 Albums.Goto(me.y);
531 if (me.bstate & BUTTON3_PRESSED)
533 size_t pos = Albums.choice();
534 spacePressed();
535 if (pos < Albums.size()-1)
536 Albums.scroll(NC::Scroll::Up);
539 else
540 Screen<WindowType>::mouseButtonPressed(me);
541 Songs.clear();
543 else if (!Songs.empty() && Songs.hasCoords(me.x, me.y))
545 if (!tryNextColumn() || !tryNextColumn())
546 return;
547 if (size_t(me.y) < Songs.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
549 Songs.Goto(me.y);
550 if (me.bstate & BUTTON1_PRESSED)
552 size_t pos = Songs.choice();
553 spacePressed();
554 if (pos < Songs.size()-1)
555 Songs.scroll(NC::Scroll::Up);
557 else
558 enterPressed();
560 else
561 Screen<WindowType>::mouseButtonPressed(me);
565 /***********************************************************************/
567 bool MediaLibrary::allowsFiltering()
569 return true;
572 std::string MediaLibrary::currentFilter()
574 std::string filter;
575 if (isActiveWindow(Tags))
576 filter = RegexFilter<PrimaryTag>::currentFilter(Tags);
577 else if (isActiveWindow(Albums))
578 filter = RegexItemFilter<AlbumEntry>::currentFilter(Albums);
579 else if (isActiveWindow(Songs))
580 filter = RegexFilter<MPD::Song>::currentFilter(Songs);
581 return filter;
584 void MediaLibrary::applyFilter(const std::string &filter)
586 if (filter.empty())
588 if (isActiveWindow(Tags))
590 Tags.clearFilter();
591 Tags.clearFilterResults();
593 else if (isActiveWindow(Albums))
595 Albums.clearFilter();
596 Albums.clearFilterResults();
598 else if (isActiveWindow(Songs))
600 Songs.clearFilter();
601 Songs.clearFilterResults();
603 return;
607 if (isActiveWindow(Tags))
609 Tags.showAll();
610 auto rx = RegexFilter<PrimaryTag>(
611 boost::regex(filter, Config.regex_type), TagEntryMatcher);
612 Tags.filter(Tags.begin(), Tags.end(), rx);
614 else if (isActiveWindow(Albums))
616 Albums.showAll();
617 auto fun = boost::bind(AlbumEntryMatcher, _1, _2, true);
618 auto rx = RegexItemFilter<AlbumEntry>(
619 boost::regex(filter, Config.regex_type), fun);
620 Albums.filter(Albums.begin(), Albums.end(), rx);
622 else if (isActiveWindow(Songs))
624 Songs.showAll();
625 auto rx = RegexFilter<MPD::Song>(
626 boost::regex(filter, Config.regex_type), SongEntryMatcher);
627 Songs.filter(Songs.begin(), Songs.end(), rx);
630 catch (boost::bad_expression &) { }
633 /***********************************************************************/
635 bool MediaLibrary::allowsSearching()
637 return true;
640 bool MediaLibrary::search(const std::string &constraint)
642 if (constraint.empty())
644 if (isActiveWindow(Tags))
645 Tags.clearSearchResults();
646 else if (isActiveWindow(Albums))
647 Albums.clearSearchResults();
648 else if (isActiveWindow(Songs))
649 Songs.clearSearchResults();
650 return false;
654 bool result = false;
655 if (isActiveWindow(Tags))
657 auto rx = RegexFilter<PrimaryTag>(
658 boost::regex(constraint, Config.regex_type), TagEntryMatcher);
659 result = Tags.search(Tags.begin(), Tags.end(), rx);
661 else if (isActiveWindow(Albums))
663 auto fun = boost::bind(AlbumEntryMatcher, _1, _2, false);
664 auto rx = RegexItemFilter<AlbumEntry>(
665 boost::regex(constraint, Config.regex_type), fun);
666 result = Albums.search(Albums.begin(), Albums.end(), rx);
668 else if (isActiveWindow(Songs))
670 auto rx = RegexFilter<MPD::Song>(
671 boost::regex(constraint, Config.regex_type), SongEntryMatcher);
672 result = Songs.search(Songs.begin(), Songs.end(), rx);
674 return result;
676 catch (boost::bad_expression &)
678 return false;
682 void MediaLibrary::nextFound(bool wrap)
684 if (isActiveWindow(Tags))
685 Tags.nextFound(wrap);
686 else if (isActiveWindow(Albums))
687 Albums.nextFound(wrap);
688 else if (isActiveWindow(Songs))
689 Songs.nextFound(wrap);
692 void MediaLibrary::prevFound(bool wrap)
694 if (isActiveWindow(Tags))
695 Tags.prevFound(wrap);
696 else if (isActiveWindow(Albums))
697 Albums.prevFound(wrap);
698 else if (isActiveWindow(Songs))
699 Songs.prevFound(wrap);
702 /***********************************************************************/
704 ProxySongList MediaLibrary::proxySongList()
706 auto ptr = ProxySongList();
707 if (isActiveWindow(Songs))
708 ptr = songsProxyList();
709 return ptr;
712 bool MediaLibrary::allowsSelection()
714 return true;
717 void MediaLibrary::reverseSelection()
719 if (isActiveWindow(Tags))
720 reverseSelectionHelper(Tags.begin(), Tags.end());
721 else if (isActiveWindow(Albums))
723 // omit "All tracks"
724 if (Albums.size() > 1)
725 reverseSelectionHelper(Albums.begin(), Albums.end()-2);
726 else
727 reverseSelectionHelper(Albums.begin(), Albums.end());
729 else if (isActiveWindow(Songs))
730 reverseSelectionHelper(Songs.begin(), Songs.end());
733 MPD::SongList MediaLibrary::getSelectedSongs()
735 MPD::SongList result;
736 if (isActiveWindow(Tags))
738 auto tag_handler = [&result](const std::string &tag) {
739 Mpd.StartSearch(true);
740 Mpd.AddSearch(Config.media_lib_primary_tag, tag);
741 Mpd.CommitSearchSongs(vectorMoveInserter(result));
743 bool any_selected = false;
744 for (auto &e : Tags)
746 if (e.isSelected())
748 any_selected = true;
749 tag_handler(e.value().tag());
752 // if no item is selected, add current one
753 if (!any_selected && !Tags.empty())
754 tag_handler(Tags.current().value().tag());
756 else if (isActiveWindow(Albums))
758 bool any_selected = false;
759 for (auto it = Albums.begin(); it != Albums.end() && !it->isSeparator(); ++it)
761 if (it->isSelected())
763 any_selected = true;
764 auto &sc = it->value();
765 Mpd.StartSearch(true);
766 if (hasTwoColumns)
767 Mpd.AddSearch(Config.media_lib_primary_tag, sc.entry().tag());
768 else
769 Mpd.AddSearch(Config.media_lib_primary_tag,
770 Tags.current().value().tag());
771 Mpd.AddSearch(MPD_TAG_ALBUM, sc.entry().album());
772 Mpd.AddSearch(MPD_TAG_DATE, sc.entry().date());
773 size_t begin = result.size();
774 Mpd.CommitSearchSongs(vectorMoveInserter(result));
775 std::sort(result.begin()+begin, result.end(), SortSongs(false));
778 // if no item is selected, add songs from right column
779 if (!any_selected && !Albums.empty())
781 withUnfilteredMenu(Songs, [this, &result]() {
782 result.insert(result.end(), Songs.beginV(), Songs.endV());
786 else if (isActiveWindow(Songs))
788 for (auto it = Songs.begin(); it != Songs.end(); ++it)
789 if (it->isSelected())
790 result.push_back(it->value());
791 // if no item is selected, add current one
792 if (result.empty() && !Songs.empty())
793 result.push_back(Songs.current().value());
795 return result;
798 /***********************************************************************/
800 bool MediaLibrary::previousColumnAvailable()
802 assert(!hasTwoColumns || !isActiveWindow(Tags));
803 if (isActiveWindow(Songs))
805 if (!Albums.reallyEmpty() && (hasTwoColumns || !Tags.reallyEmpty()))
806 return true;
808 else if (isActiveWindow(Albums))
810 if (!hasTwoColumns && !Tags.reallyEmpty())
811 return true;
813 return false;
816 void MediaLibrary::previousColumn()
818 if (isActiveWindow(Songs))
820 Songs.setHighlightColor(Config.main_highlight_color);
821 w->refresh();
822 w = &Albums;
823 Albums.setHighlightColor(Config.active_column_color);
825 else if (isActiveWindow(Albums) && !hasTwoColumns)
827 Albums.setHighlightColor(Config.main_highlight_color);
828 w->refresh();
829 w = &Tags;
830 Tags.setHighlightColor(Config.active_column_color);
834 bool MediaLibrary::nextColumnAvailable()
836 assert(!hasTwoColumns || !isActiveWindow(Tags));
837 if (isActiveWindow(Tags))
839 if (!Albums.reallyEmpty() && !Songs.reallyEmpty())
840 return true;
842 else if (isActiveWindow(Albums))
844 if (!Songs.reallyEmpty())
845 return true;
847 return false;
850 void MediaLibrary::nextColumn()
852 if (isActiveWindow(Tags))
854 Tags.setHighlightColor(Config.main_highlight_color);
855 w->refresh();
856 w = &Albums;
857 Albums.setHighlightColor(Config.active_column_color);
859 else if (isActiveWindow(Albums))
861 Albums.setHighlightColor(Config.main_highlight_color);
862 w->refresh();
863 w = &Songs;
864 Songs.setHighlightColor(Config.active_column_color);
868 /***********************************************************************/
870 void MediaLibrary::updateTimer()
872 m_timer = Global::Timer;
875 void MediaLibrary::toggleColumnsMode()
877 hasTwoColumns = !hasTwoColumns;
878 Tags.clear();
879 Albums.clear();
880 Albums.reset();
881 Songs.clear();
882 if (hasTwoColumns)
884 if (isActiveWindow(Tags))
885 nextColumn();
886 if (Config.titles_visibility)
888 std::string item_type = boost::locale::to_lower(
889 tagTypeToString(Config.media_lib_primary_tag));
890 std::string and_mtime = Config.media_library_sort_by_mtime ? " and mtime" : "";
891 Albums.setTitle("Albums (sorted by " + item_type + and_mtime + ")");
894 else
895 Albums.setTitle(Config.titles_visibility ? "Albums" : "");
896 resize();
899 int MediaLibrary::Columns()
901 if (hasTwoColumns)
902 return 2;
903 else
904 return 3;
907 ProxySongList MediaLibrary::songsProxyList()
909 return ProxySongList(Songs, [](NC::Menu<MPD::Song>::Item &item) {
910 return &item.value();
914 void MediaLibrary::toggleSortMode()
916 Config.media_library_sort_by_mtime = !Config.media_library_sort_by_mtime;
917 Statusbar::printf("Sorting library by: %1%",
918 Config.media_library_sort_by_mtime ? "modification time" : "name");
919 if (hasTwoColumns)
921 withUnfilteredMenuReapplyFilter(Albums, [this]() {
922 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
924 Albums.refresh();
925 Songs.clear();
926 if (Config.titles_visibility)
928 std::string item_type = boost::locale::to_lower(
929 tagTypeToString(Config.media_lib_primary_tag));
930 std::string and_mtime = Config.media_library_sort_by_mtime ? (" " "and mtime") : "";
931 Albums.setTitle("Albums (sorted by " + item_type + and_mtime + ")");
934 else
936 withUnfilteredMenuReapplyFilter(Tags, [this]() {
937 // if we already have modification times,
938 // just resort. otherwise refetch the list.
939 if (!Tags.empty() && Tags[0].value().mtime() > 0)
941 std::sort(Tags.beginV(), Tags.endV(), SortPrimaryTags());
942 Tags.refresh();
944 else
945 Tags.clear();
947 Albums.clear();
948 Songs.clear();
950 update();
953 void MediaLibrary::LocateSong(const MPD::Song &s)
955 std::string primary_tag = s.get(Config.media_lib_primary_tag);
956 if (primary_tag.empty())
958 std::string item_type = boost::locale::to_lower(
959 tagTypeToString(Config.media_lib_primary_tag));
960 Statusbar::printf("Can't use this function because the song has no %s tag", item_type);
961 return;
963 if (!s.isFromDatabase())
965 Statusbar::print("Song is not from the database");
966 return;
969 if (myScreen != this)
970 switchTo();
971 Statusbar::put() << "Jumping to song...";
972 Global::wFooter->refresh();
974 if (!hasTwoColumns)
976 Tags.showAll();
977 if (Tags.empty())
978 update();
979 if (primary_tag != Tags.current().value().tag())
981 for (size_t i = 0; i < Tags.size(); ++i)
983 if (primary_tag == Tags[i].value().tag())
985 Tags.highlight(i);
986 Albums.clear();
987 Songs.clear();
988 break;
994 Albums.showAll();
995 if (Albums.empty())
996 update();
998 std::string album = s.getAlbum();
999 std::string date = s.getDate();
1000 if ((hasTwoColumns && Albums.current().value().entry().tag() != primary_tag)
1001 || album != Albums.current().value().entry().album()
1002 || date != Albums.current().value().entry().date())
1004 for (size_t i = 0; i < Albums.size(); ++i)
1006 if ((!hasTwoColumns || Albums[i].value().entry().tag() == primary_tag)
1007 && album == Albums[i].value().entry().album()
1008 && date == Albums[i].value().entry().date())
1010 Albums.highlight(i);
1011 Songs.clear();
1012 break;
1017 Songs.showAll();
1018 if (Songs.empty())
1019 update();
1021 if (s != Songs.current().value())
1023 for (size_t i = 0; i < Songs.size(); ++i)
1025 if (s == Songs[i].value())
1027 Songs.highlight(i);
1028 break;
1033 Tags.setHighlightColor(Config.main_highlight_color);
1034 Albums.setHighlightColor(Config.main_highlight_color);
1035 Songs.setHighlightColor(Config.active_column_color);
1036 w = &Songs;
1037 refresh();
1040 void MediaLibrary::AddToPlaylist(bool add_n_play)
1042 if (isActiveWindow(Songs) && !Songs.empty())
1043 addSongToPlaylist(Songs.current().value(), add_n_play);
1044 else
1046 if ((!Tags.empty() && isActiveWindow(Tags))
1047 || (isActiveWindow(Albums) && Albums.current().value().isAllTracksEntry()))
1049 MPD::SongList list;
1050 Mpd.StartSearch(true);
1051 Mpd.AddSearch(Config.media_lib_primary_tag, Tags.current().value().tag());
1052 Mpd.CommitSearchSongs(vectorMoveInserter(list));
1053 bool success = addSongsToPlaylist(list.begin(), list.end(), add_n_play, -1);
1054 std::string tag_type = boost::locale::to_lower(
1055 tagTypeToString(Config.media_lib_primary_tag));
1056 Statusbar::printf("Songs with %1% \"%2%\" added%3%",
1057 tag_type, Tags.current().value().tag(), withErrors(success)
1060 else if (isActiveWindow(Albums))
1062 bool success;
1063 withUnfilteredMenu(Songs, [&]() {
1064 success = addSongsToPlaylist(Songs.beginV(), Songs.endV(), add_n_play, -1);
1066 Statusbar::printf("Songs from album \"%1%\" added%2%",
1067 Albums.current().value().entry().album(), withErrors(success)
1072 if (!add_n_play)
1074 w->scroll(NC::Scroll::Down);
1075 if (isActiveWindow(Tags))
1077 Albums.clear();
1078 Songs.clear();
1080 else if (isActiveWindow(Albums))
1081 Songs.clear();
1085 namespace {//
1087 std::string AlbumToString(const AlbumEntry &ae)
1089 std::string result;
1090 if (ae.isAllTracksEntry())
1091 result = "All tracks";
1092 else
1094 if (hasTwoColumns)
1096 if (ae.entry().tag().empty())
1097 result += Config.empty_tag;
1098 else
1099 result += ae.entry().tag();
1100 result += " - ";
1102 if (Config.media_lib_primary_tag != MPD_TAG_DATE && !ae.entry().date().empty())
1103 result += "(" + ae.entry().date() + ") ";
1104 result += ae.entry().album().empty() ? "<no album>" : ae.entry().album();
1106 return result;
1109 std::string SongToString(const MPD::Song &s)
1111 return s.toString(Config.song_library_format, Config.tags_separator);
1114 bool TagEntryMatcher(const boost::regex &rx, const MediaLibrary::PrimaryTag &pt)
1116 return boost::regex_search(pt.tag(), rx);
1119 bool AlbumEntryMatcher(const boost::regex &rx, const NC::Menu<AlbumEntry>::Item &item, bool filter)
1121 if (item.isSeparator() || item.value().isAllTracksEntry())
1122 return filter;
1123 return boost::regex_search(AlbumToString(item.value()), rx);
1126 bool SongEntryMatcher(const boost::regex &rx, const MPD::Song &s)
1128 return boost::regex_search(SongToString(s), rx);