set SIGWINCH handler before initializing ncurses to avoid races
[ncmpcpp.git] / src / media_library.cpp
blobc8944db1dec123cb4eb8953023ec5636df2355ba
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 template <typename FunT>
62 void withSongsFromAlbum(const AlbumEntry &album, FunT &&f)
64 Mpd.StartSearch(true);
65 Mpd.AddSearch(Config.media_lib_primary_tag, album.entry().tag());
66 if (!album.isAllTracksEntry())
68 Mpd.AddSearch(MPD_TAG_ALBUM, album.entry().album());
69 Mpd.AddSearch(MPD_TAG_DATE, album.entry().date());
71 Mpd.CommitSearchSongs(std::forward<FunT>(f));
74 std::string AlbumToString(const AlbumEntry &ae);
75 std::string SongToString(const MPD::Song &s);
77 bool TagEntryMatcher(const boost::regex &rx, const MediaLibrary::PrimaryTag &tagmtime);
78 bool AlbumEntryMatcher(const boost::regex &rx, const NC::Menu<AlbumEntry>::Item &item, bool filter);
79 bool SongEntryMatcher(const boost::regex &rx, const MPD::Song &s);
81 struct SortSongs {
82 typedef NC::Menu<MPD::Song>::Item SongItem;
84 static const std::array<MPD::Song::GetFunction, 3> GetFuns;
86 LocaleStringComparison m_cmp;
87 std::ptrdiff_t m_offset;
89 public:
90 SortSongs(bool disc_only)
91 : m_cmp(std::locale(), Config.ignore_leading_the), m_offset(disc_only ? 2 : 0) { }
93 bool operator()(const SongItem &a, const SongItem &b) {
94 return (*this)(a.value(), b.value());
96 bool operator()(const MPD::Song &a, const MPD::Song &b) {
97 for (auto get = GetFuns.begin()+m_offset; get != GetFuns.end(); ++get) {
98 int ret = m_cmp(a.getTags(*get, Config.tags_separator),
99 b.getTags(*get, Config.tags_separator));
100 if (ret != 0)
101 return ret < 0;
103 return a.getTrack() < b.getTrack();
107 const std::array<MPD::Song::GetFunction, 3> SortSongs::GetFuns = {{
108 &MPD::Song::getDate,
109 &MPD::Song::getAlbum,
110 &MPD::Song::getDisc
113 class SortAlbumEntries {
114 typedef MediaLibrary::Album Album;
116 LocaleStringComparison m_cmp;
118 public:
119 SortAlbumEntries() : m_cmp(std::locale(), Config.ignore_leading_the) { }
121 bool operator()(const AlbumEntry &a, const AlbumEntry &b) const {
122 return (*this)(a.entry(), b.entry());
125 bool operator()(const Album &a, const Album &b) const {
126 if (Config.media_library_sort_by_mtime)
127 return a.mtime() > b.mtime();
128 else
130 int result;
131 result = m_cmp(a.tag(), b.tag());
132 if (result != 0)
133 return result < 0;
134 result = m_cmp(a.date(), b.date());
135 if (result != 0)
136 return result < 0;
137 return m_cmp(a.album(), b.album()) < 0;
142 class SortPrimaryTags {
143 typedef MediaLibrary::PrimaryTag PrimaryTag;
145 LocaleStringComparison m_cmp;
147 public:
148 SortPrimaryTags() : m_cmp(std::locale(), Config.ignore_leading_the) { }
150 bool operator()(const PrimaryTag &a, const PrimaryTag &b) const {
151 if (Config.media_library_sort_by_mtime)
152 return a.mtime() > b.mtime();
153 else
154 return m_cmp(a.tag(), b.tag()) < 0;
160 MediaLibrary::MediaLibrary()
161 : m_timer(boost::posix_time::from_time_t(0))
162 , m_window_timeout(Config.data_fetching_delay ? 250 : 500)
163 , m_fetching_delay(boost::posix_time::milliseconds(Config.data_fetching_delay ? 250 : -1))
165 hasTwoColumns = 0;
166 itsLeftColWidth = COLS/3-1;
167 itsMiddleColWidth = COLS/3;
168 itsMiddleColStartX = itsLeftColWidth+1;
169 itsRightColWidth = COLS-COLS/3*2-1;
170 itsRightColStartX = itsLeftColWidth+itsMiddleColWidth+2;
172 Tags = NC::Menu<PrimaryTag>(0, MainStartY, itsLeftColWidth, MainHeight, Config.titles_visibility ? tagTypeToString(Config.media_lib_primary_tag) + "s" : "", Config.main_color, NC::Border::None);
173 Tags.setHighlightColor(Config.active_column_color);
174 Tags.cyclicScrolling(Config.use_cyclic_scrolling);
175 Tags.centeredCursor(Config.centered_cursor);
176 Tags.setSelectedPrefix(Config.selected_item_prefix);
177 Tags.setSelectedSuffix(Config.selected_item_suffix);
178 Tags.setItemDisplayer([](NC::Menu<PrimaryTag> &menu) {
179 const std::string &tag = menu.drawn()->value().tag();
180 if (tag.empty())
181 menu << Config.empty_tag;
182 else
183 menu << Charset::utf8ToLocale(tag);
186 Albums = NC::Menu<AlbumEntry>(itsMiddleColStartX, MainStartY, itsMiddleColWidth, MainHeight, Config.titles_visibility ? "Albums" : "", Config.main_color, NC::Border::None);
187 Albums.setHighlightColor(Config.main_highlight_color);
188 Albums.cyclicScrolling(Config.use_cyclic_scrolling);
189 Albums.centeredCursor(Config.centered_cursor);
190 Albums.setSelectedPrefix(Config.selected_item_prefix);
191 Albums.setSelectedSuffix(Config.selected_item_suffix);
192 Albums.setItemDisplayer([](NC::Menu<AlbumEntry> &menu) {
193 menu << Charset::utf8ToLocale(AlbumToString(menu.drawn()->value()));
196 Songs = NC::Menu<MPD::Song>(itsRightColStartX, MainStartY, itsRightColWidth, MainHeight, Config.titles_visibility ? "Songs" : "", Config.main_color, NC::Border::None);
197 Songs.setHighlightColor(Config.main_highlight_color);
198 Songs.cyclicScrolling(Config.use_cyclic_scrolling);
199 Songs.centeredCursor(Config.centered_cursor);
200 Songs.setSelectedPrefix(Config.selected_item_prefix);
201 Songs.setSelectedSuffix(Config.selected_item_suffix);
202 Songs.setItemDisplayer(boost::bind(Display::Songs, _1, songsProxyList(), Config.song_library_format));
204 w = &Tags;
207 void MediaLibrary::resize()
209 size_t x_offset, width;
210 getWindowResizeParams(x_offset, width);
211 if (!hasTwoColumns)
213 itsLeftColStartX = x_offset;
214 itsLeftColWidth = width/3-1;
215 itsMiddleColStartX = itsLeftColStartX+itsLeftColWidth+1;
216 itsMiddleColWidth = width/3;
217 itsRightColStartX = itsMiddleColStartX+itsMiddleColWidth+1;
218 itsRightColWidth = width-width/3*2-1;
220 else
222 itsMiddleColStartX = x_offset;
223 itsMiddleColWidth = width/2;
224 itsRightColStartX = x_offset+itsMiddleColWidth+1;
225 itsRightColWidth = width-itsMiddleColWidth-1;
228 Tags.resize(itsLeftColWidth, MainHeight);
229 Albums.resize(itsMiddleColWidth, MainHeight);
230 Songs.resize(itsRightColWidth, MainHeight);
232 Tags.moveTo(itsLeftColStartX, MainStartY);
233 Albums.moveTo(itsMiddleColStartX, MainStartY);
234 Songs.moveTo(itsRightColStartX, MainStartY);
236 hasToBeResized = 0;
239 void MediaLibrary::refresh()
241 Tags.display();
242 drawSeparator(itsMiddleColStartX-1);
243 Albums.display();
244 drawSeparator(itsRightColStartX-1);
245 Songs.display();
246 if (Albums.empty())
248 Albums << NC::XY(0, 0) << "No albums found.";
249 Albums.Window::refresh();
253 void MediaLibrary::switchTo()
255 SwitchTo::execute(this);
256 markSongsInPlaylist(songsProxyList());
257 drawHeader();
258 refresh();
261 std::wstring MediaLibrary::title()
263 return L"Media library";
266 void MediaLibrary::update()
268 if (hasTwoColumns)
270 if (Albums.reallyEmpty() || m_albums_update_request)
272 Albums.clearSearchResults();
273 m_albums_update_request = false;
274 std::map<std::tuple<std::string, std::string, std::string>, time_t> albums;
275 Mpd.GetDirectoryRecursive("/", [&albums](MPD::Song s) {
276 unsigned idx = 0;
277 std::string tag = s.get(Config.media_lib_primary_tag, idx);
280 auto key = std::make_tuple(tag, s.getAlbum(), s.getDate());
281 auto it = albums.find(key);
282 if (it == albums.end())
283 albums[key] = s.getMTime();
284 else
285 it->second = s.getMTime();
287 while (!(tag = s.get(Config.media_lib_primary_tag, ++idx)).empty());
289 withUnfilteredMenuReapplyFilter(Albums, [this, &albums]() {
290 size_t idx = 0;
291 for (auto it = albums.begin(); it != albums.end(); ++it, ++idx)
293 auto &&entry = AlbumEntry(Album(
294 std::move(std::get<0>(it->first)),
295 std::move(std::get<1>(it->first)),
296 std::move(std::get<2>(it->first)),
297 it->second));
298 if (idx < Albums.size())
299 Albums[idx].value() = entry;
300 else
301 Albums.addItem(entry);
303 if (idx < Albums.size())
304 Albums.resizeList(idx);
305 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
307 Albums.refresh();
310 else
312 if (Tags.reallyEmpty() || m_tags_update_request)
314 Tags.clearSearchResults();
315 m_tags_update_request = false;
316 std::map<std::string, time_t> tags;
317 if (Config.media_library_sort_by_mtime)
319 Mpd.GetDirectoryRecursive("/", [&tags](MPD::Song s) {
320 unsigned idx = 0;
321 std::string tag = s.get(Config.media_lib_primary_tag, idx);
324 auto it = tags.find(tag);
325 if (it == tags.end())
326 tags[tag] = s.getMTime();
327 else
328 it->second = std::max(it->second, s.getMTime());
330 while (!(tag = s.get(Config.media_lib_primary_tag, ++idx)).empty());
333 else
335 Mpd.GetList(Config.media_lib_primary_tag, [&tags](std::string tag) {
336 tags[tag] = 0;
339 withUnfilteredMenuReapplyFilter(Tags, [this, &tags]() {
340 size_t idx = 0;
341 for (auto it = tags.begin(); it != tags.end(); ++it, ++idx)
343 auto &&tag = PrimaryTag(std::move(it->first), it->second);
344 if (idx < Tags.size())
345 Tags[idx].value() = tag;
346 else
347 Tags.addItem(tag);
349 if (idx < Tags.size())
350 Tags.resizeList(idx);
351 std::sort(Tags.beginV(), Tags.endV(), SortPrimaryTags());
353 Tags.refresh();
356 if (!Tags.empty()
357 && ((Albums.reallyEmpty() && Global::Timer - m_timer > m_fetching_delay) || m_albums_update_request)
360 Albums.clearSearchResults();
361 m_albums_update_request = false;
362 auto &primary_tag = Tags.current().value().tag();
363 Mpd.StartSearch(true);
364 Mpd.AddSearch(Config.media_lib_primary_tag, primary_tag);
365 std::map<std::tuple<std::string, std::string>, time_t> albums;
366 Mpd.CommitSearchSongs([&albums](MPD::Song s) {
367 auto key = std::make_tuple(s.getAlbum(), s.getDate());
368 auto it = albums.find(key);
369 if (it == albums.end())
370 albums[key] = s.getMTime();
371 else
372 it->second = s.getMTime();
374 withUnfilteredMenuReapplyFilter(Albums, [this, &albums, &primary_tag]() {
375 size_t idx = 0;
376 for (auto it = albums.begin(); it != albums.end(); ++it, ++idx)
378 auto &&entry = AlbumEntry(Album(
379 primary_tag,
380 std::move(std::get<0>(it->first)),
381 std::move(std::get<1>(it->first)),
382 it->second));
383 if (idx < Albums.size())
385 Albums[idx].value() = entry;
386 Albums[idx].setSeparator(false);
388 else
389 Albums.addItem(entry);
391 if (idx < Albums.size())
392 Albums.resizeList(idx);
393 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
394 if (albums.size() > 1)
396 Albums.addSeparator();
397 Albums.addItem(AlbumEntry::mkAllTracksEntry(primary_tag));
400 Albums.refresh();
404 if (!Albums.empty()
405 && ((Songs.reallyEmpty() && Global::Timer - m_timer > m_fetching_delay) || m_songs_update_request)
408 Songs.clearSearchResults();
409 m_songs_update_request = false;
410 auto &album = Albums.current().value();
411 Mpd.StartSearch(true);
412 Mpd.AddSearch(Config.media_lib_primary_tag, album.entry().tag());
413 if (!album.isAllTracksEntry())
415 Mpd.AddSearch(MPD_TAG_ALBUM, album.entry().album());
416 Mpd.AddSearch(MPD_TAG_DATE, album.entry().date());
418 withUnfilteredMenuReapplyFilter(Songs, [this, &album]() {
419 size_t idx = 0;
420 Mpd.CommitSearchSongs([this, &idx](MPD::Song s) {
421 bool is_playlist = myPlaylist->checkForSong(s);
422 if (idx < Songs.size())
424 Songs[idx].value() = s;
425 Songs[idx].setBold(is_playlist);
427 else
428 Songs.addItem(s, is_playlist);
429 ++idx;
431 if (idx < Songs.size())
432 Songs.resizeList(idx);
433 std::sort(Songs.begin(), Songs.end(), SortSongs(!album.isAllTracksEntry()));
435 Songs.refresh();
439 int MediaLibrary::windowTimeout()
441 if (Albums.reallyEmpty() || Songs.reallyEmpty())
442 return m_window_timeout;
443 else
444 return Screen<WindowType>::windowTimeout();
447 void MediaLibrary::enterPressed()
449 AddToPlaylist(true);
452 void MediaLibrary::spacePressed()
454 if (Config.space_selects)
456 if (isActiveWindow(Tags))
458 size_t idx = Tags.choice();
459 Tags[idx].setSelected(!Tags[idx].isSelected());
460 Tags.scroll(NC::Scroll::Down);
461 Albums.clear();
462 Songs.clear();
464 else if (isActiveWindow(Albums))
466 if (!Albums.current().value().isAllTracksEntry())
468 size_t idx = Albums.choice();
469 Albums[idx].setSelected(!Albums[idx].isSelected());
470 Albums.scroll(NC::Scroll::Down);
471 Songs.clear();
474 else if (isActiveWindow(Songs))
476 size_t idx = Songs.choice();
477 Songs[idx].setSelected(!Songs[idx].isSelected());
478 Songs.scroll(NC::Scroll::Down);
481 else
482 AddToPlaylist(false);
485 void MediaLibrary::mouseButtonPressed(MEVENT me)
487 auto tryNextColumn = [this]() -> bool {
488 bool result = true;
489 if (!isActiveWindow(Songs))
491 if (nextColumnAvailable())
492 nextColumn();
493 else
494 result = false;
496 return result;
498 auto tryPreviousColumn = [this]() -> bool {
499 bool result = true;
500 if (!isActiveWindow(Tags))
502 if (previousColumnAvailable())
503 previousColumn();
504 else
505 result = false;
507 return result;
509 if (!Tags.empty() && Tags.hasCoords(me.x, me.y))
511 if (!tryPreviousColumn() || !tryPreviousColumn())
512 return;
513 if (size_t(me.y) < Tags.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
515 Tags.Goto(me.y);
516 if (me.bstate & BUTTON3_PRESSED)
518 size_t pos = Tags.choice();
519 spacePressed();
520 if (pos < Tags.size()-1)
521 Tags.scroll(NC::Scroll::Up);
524 else
525 Screen<WindowType>::mouseButtonPressed(me);
526 Albums.clear();
527 Songs.clear();
529 else if (!Albums.empty() && Albums.hasCoords(me.x, me.y))
531 if (!isActiveWindow(Albums))
533 bool success;
534 if (isActiveWindow(Tags))
535 success = tryNextColumn();
536 else
537 success = tryPreviousColumn();
538 if (!success)
539 return;
541 if (size_t(me.y) < Albums.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
543 Albums.Goto(me.y);
544 if (me.bstate & BUTTON3_PRESSED)
546 size_t pos = Albums.choice();
547 spacePressed();
548 if (pos < Albums.size()-1)
549 Albums.scroll(NC::Scroll::Up);
552 else
553 Screen<WindowType>::mouseButtonPressed(me);
554 Songs.clear();
556 else if (!Songs.empty() && Songs.hasCoords(me.x, me.y))
558 if (!tryNextColumn() || !tryNextColumn())
559 return;
560 if (size_t(me.y) < Songs.size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
562 Songs.Goto(me.y);
563 if (me.bstate & BUTTON1_PRESSED)
565 size_t pos = Songs.choice();
566 spacePressed();
567 if (pos < Songs.size()-1)
568 Songs.scroll(NC::Scroll::Up);
570 else
571 enterPressed();
573 else
574 Screen<WindowType>::mouseButtonPressed(me);
578 /***********************************************************************/
580 bool MediaLibrary::allowsFiltering()
582 return true;
585 std::string MediaLibrary::currentFilter()
587 std::string filter;
588 if (isActiveWindow(Tags))
589 filter = RegexFilter<PrimaryTag>::currentFilter(Tags);
590 else if (isActiveWindow(Albums))
591 filter = RegexItemFilter<AlbumEntry>::currentFilter(Albums);
592 else if (isActiveWindow(Songs))
593 filter = RegexFilter<MPD::Song>::currentFilter(Songs);
594 return filter;
597 void MediaLibrary::applyFilter(const std::string &filter)
599 if (filter.empty())
601 if (isActiveWindow(Tags))
603 Tags.clearFilter();
604 Tags.clearFilterResults();
606 else if (isActiveWindow(Albums))
608 Albums.clearFilter();
609 Albums.clearFilterResults();
611 else if (isActiveWindow(Songs))
613 Songs.clearFilter();
614 Songs.clearFilterResults();
616 return;
620 if (isActiveWindow(Tags))
622 Tags.showAll();
623 auto rx = RegexFilter<PrimaryTag>(
624 boost::regex(filter, Config.regex_type), TagEntryMatcher);
625 Tags.filter(Tags.begin(), Tags.end(), rx);
627 else if (isActiveWindow(Albums))
629 Albums.showAll();
630 auto fun = boost::bind(AlbumEntryMatcher, _1, _2, true);
631 auto rx = RegexItemFilter<AlbumEntry>(
632 boost::regex(filter, Config.regex_type), fun);
633 Albums.filter(Albums.begin(), Albums.end(), rx);
635 else if (isActiveWindow(Songs))
637 Songs.showAll();
638 auto rx = RegexFilter<MPD::Song>(
639 boost::regex(filter, Config.regex_type), SongEntryMatcher);
640 Songs.filter(Songs.begin(), Songs.end(), rx);
643 catch (boost::bad_expression &) { }
646 /***********************************************************************/
648 bool MediaLibrary::allowsSearching()
650 return true;
653 bool MediaLibrary::search(const std::string &constraint)
655 if (constraint.empty())
657 if (isActiveWindow(Tags))
658 Tags.clearSearchResults();
659 else if (isActiveWindow(Albums))
660 Albums.clearSearchResults();
661 else if (isActiveWindow(Songs))
662 Songs.clearSearchResults();
663 return false;
667 bool result = false;
668 if (isActiveWindow(Tags))
670 auto rx = RegexFilter<PrimaryTag>(
671 boost::regex(constraint, Config.regex_type), TagEntryMatcher);
672 result = Tags.search(Tags.begin(), Tags.end(), rx);
674 else if (isActiveWindow(Albums))
676 auto fun = boost::bind(AlbumEntryMatcher, _1, _2, false);
677 auto rx = RegexItemFilter<AlbumEntry>(
678 boost::regex(constraint, Config.regex_type), fun);
679 result = Albums.search(Albums.begin(), Albums.end(), rx);
681 else if (isActiveWindow(Songs))
683 auto rx = RegexFilter<MPD::Song>(
684 boost::regex(constraint, Config.regex_type), SongEntryMatcher);
685 result = Songs.search(Songs.begin(), Songs.end(), rx);
687 return result;
689 catch (boost::bad_expression &)
691 return false;
695 void MediaLibrary::nextFound(bool wrap)
697 if (isActiveWindow(Tags))
698 Tags.nextFound(wrap);
699 else if (isActiveWindow(Albums))
700 Albums.nextFound(wrap);
701 else if (isActiveWindow(Songs))
702 Songs.nextFound(wrap);
705 void MediaLibrary::prevFound(bool wrap)
707 if (isActiveWindow(Tags))
708 Tags.prevFound(wrap);
709 else if (isActiveWindow(Albums))
710 Albums.prevFound(wrap);
711 else if (isActiveWindow(Songs))
712 Songs.prevFound(wrap);
715 /***********************************************************************/
717 ProxySongList MediaLibrary::proxySongList()
719 auto ptr = ProxySongList();
720 if (isActiveWindow(Songs))
721 ptr = songsProxyList();
722 return ptr;
725 bool MediaLibrary::allowsSelection()
727 return true;
730 void MediaLibrary::reverseSelection()
732 if (isActiveWindow(Tags))
733 reverseSelectionHelper(Tags.begin(), Tags.end());
734 else if (isActiveWindow(Albums))
736 // omit "All tracks"
737 if (Albums.size() > 1)
738 reverseSelectionHelper(Albums.begin(), Albums.end()-2);
739 else
740 reverseSelectionHelper(Albums.begin(), Albums.end());
742 else if (isActiveWindow(Songs))
743 reverseSelectionHelper(Songs.begin(), Songs.end());
746 MPD::SongList MediaLibrary::getSelectedSongs()
748 MPD::SongList result;
749 if (isActiveWindow(Tags))
751 auto tag_handler = [&result](const std::string &tag) {
752 Mpd.StartSearch(true);
753 Mpd.AddSearch(Config.media_lib_primary_tag, tag);
754 Mpd.CommitSearchSongs(vectorMoveInserter(result));
756 bool any_selected = false;
757 for (auto &e : Tags)
759 if (e.isSelected())
761 any_selected = true;
762 tag_handler(e.value().tag());
765 // if no item is selected, add current one
766 if (!any_selected && !Tags.empty())
767 tag_handler(Tags.current().value().tag());
769 else if (isActiveWindow(Albums))
771 bool any_selected = false;
772 for (auto it = Albums.begin(); it != Albums.end() && !it->isSeparator(); ++it)
774 if (it->isSelected())
776 any_selected = true;
777 auto &sc = it->value();
778 Mpd.StartSearch(true);
779 if (hasTwoColumns)
780 Mpd.AddSearch(Config.media_lib_primary_tag, sc.entry().tag());
781 else
782 Mpd.AddSearch(Config.media_lib_primary_tag,
783 Tags.current().value().tag());
784 Mpd.AddSearch(MPD_TAG_ALBUM, sc.entry().album());
785 Mpd.AddSearch(MPD_TAG_DATE, sc.entry().date());
786 size_t begin = result.size();
787 Mpd.CommitSearchSongs(vectorMoveInserter(result));
788 std::sort(result.begin()+begin, result.end(), SortSongs(false));
791 // if no item is selected, add songs from right column
792 if (!any_selected && !Albums.empty())
794 size_t begin = result.size();
795 withSongsFromAlbum(Albums.current().value().entry(), vectorMoveInserter(result));
796 std::sort(result.begin()+begin, result.end(), SortSongs(false));
799 else if (isActiveWindow(Songs))
801 for (auto it = Songs.begin(); it != Songs.end(); ++it)
802 if (it->isSelected())
803 result.push_back(it->value());
804 // if no item is selected, add current one
805 if (result.empty() && !Songs.empty())
806 result.push_back(Songs.current().value());
808 return result;
811 /***********************************************************************/
813 bool MediaLibrary::previousColumnAvailable()
815 assert(!hasTwoColumns || !isActiveWindow(Tags));
816 if (isActiveWindow(Songs))
818 if (!Albums.reallyEmpty() && (hasTwoColumns || !Tags.reallyEmpty()))
819 return true;
821 else if (isActiveWindow(Albums))
823 if (!hasTwoColumns && !Tags.reallyEmpty())
824 return true;
826 return false;
829 void MediaLibrary::previousColumn()
831 if (isActiveWindow(Songs))
833 Songs.setHighlightColor(Config.main_highlight_color);
834 w->refresh();
835 w = &Albums;
836 Albums.setHighlightColor(Config.active_column_color);
838 else if (isActiveWindow(Albums) && !hasTwoColumns)
840 Albums.setHighlightColor(Config.main_highlight_color);
841 w->refresh();
842 w = &Tags;
843 Tags.setHighlightColor(Config.active_column_color);
847 bool MediaLibrary::nextColumnAvailable()
849 assert(!hasTwoColumns || !isActiveWindow(Tags));
850 if (isActiveWindow(Tags))
852 if (!Albums.reallyEmpty() && !Songs.reallyEmpty())
853 return true;
855 else if (isActiveWindow(Albums))
857 if (!Songs.reallyEmpty())
858 return true;
860 return false;
863 void MediaLibrary::nextColumn()
865 if (isActiveWindow(Tags))
867 Tags.setHighlightColor(Config.main_highlight_color);
868 w->refresh();
869 w = &Albums;
870 Albums.setHighlightColor(Config.active_column_color);
872 else if (isActiveWindow(Albums))
874 Albums.setHighlightColor(Config.main_highlight_color);
875 w->refresh();
876 w = &Songs;
877 Songs.setHighlightColor(Config.active_column_color);
881 /***********************************************************************/
883 void MediaLibrary::updateTimer()
885 m_timer = Global::Timer;
888 void MediaLibrary::toggleColumnsMode()
890 hasTwoColumns = !hasTwoColumns;
891 Tags.clear();
892 Albums.clear();
893 Albums.reset();
894 Songs.clear();
895 if (hasTwoColumns)
897 if (isActiveWindow(Tags))
898 nextColumn();
899 if (Config.titles_visibility)
901 std::string item_type = boost::locale::to_lower(
902 tagTypeToString(Config.media_lib_primary_tag));
903 std::string and_mtime = Config.media_library_sort_by_mtime ? " and mtime" : "";
904 Albums.setTitle("Albums (sorted by " + item_type + and_mtime + ")");
907 else
908 Albums.setTitle(Config.titles_visibility ? "Albums" : "");
909 resize();
912 int MediaLibrary::Columns()
914 if (hasTwoColumns)
915 return 2;
916 else
917 return 3;
920 ProxySongList MediaLibrary::songsProxyList()
922 return ProxySongList(Songs, [](NC::Menu<MPD::Song>::Item &item) {
923 return &item.value();
927 void MediaLibrary::toggleSortMode()
929 Config.media_library_sort_by_mtime = !Config.media_library_sort_by_mtime;
930 Statusbar::printf("Sorting library by: %1%",
931 Config.media_library_sort_by_mtime ? "modification time" : "name");
932 if (hasTwoColumns)
934 withUnfilteredMenuReapplyFilter(Albums, [this]() {
935 std::sort(Albums.beginV(), Albums.endV(), SortAlbumEntries());
937 Albums.refresh();
938 Songs.clear();
939 if (Config.titles_visibility)
941 std::string item_type = boost::locale::to_lower(
942 tagTypeToString(Config.media_lib_primary_tag));
943 std::string and_mtime = Config.media_library_sort_by_mtime ? (" " "and mtime") : "";
944 Albums.setTitle("Albums (sorted by " + item_type + and_mtime + ")");
947 else
949 withUnfilteredMenuReapplyFilter(Tags, [this]() {
950 // if we already have modification times,
951 // just resort. otherwise refetch the list.
952 if (!Tags.empty() && Tags[0].value().mtime() > 0)
954 std::sort(Tags.beginV(), Tags.endV(), SortPrimaryTags());
955 Tags.refresh();
957 else
958 Tags.clear();
960 Albums.clear();
961 Songs.clear();
963 update();
966 void MediaLibrary::LocateSong(const MPD::Song &s)
968 std::string primary_tag = s.get(Config.media_lib_primary_tag);
969 if (primary_tag.empty())
971 std::string item_type = boost::locale::to_lower(
972 tagTypeToString(Config.media_lib_primary_tag));
973 Statusbar::printf("Can't use this function because the song has no %s tag", item_type);
974 return;
976 if (!s.isFromDatabase())
978 Statusbar::print("Song is not from the database");
979 return;
982 if (myScreen != this)
983 switchTo();
984 Statusbar::put() << "Jumping to song...";
985 Global::wFooter->refresh();
987 if (!hasTwoColumns)
989 Tags.showAll();
990 if (Tags.empty())
991 update();
992 if (primary_tag != Tags.current().value().tag())
994 for (size_t i = 0; i < Tags.size(); ++i)
996 if (primary_tag == Tags[i].value().tag())
998 Tags.highlight(i);
999 Albums.clear();
1000 Songs.clear();
1001 break;
1007 Albums.showAll();
1008 if (Albums.empty())
1009 update();
1011 std::string album = s.getAlbum();
1012 std::string date = s.getDate();
1013 if ((hasTwoColumns && Albums.current().value().entry().tag() != primary_tag)
1014 || album != Albums.current().value().entry().album()
1015 || date != Albums.current().value().entry().date())
1017 for (size_t i = 0; i < Albums.size(); ++i)
1019 if ((!hasTwoColumns || Albums[i].value().entry().tag() == primary_tag)
1020 && album == Albums[i].value().entry().album()
1021 && date == Albums[i].value().entry().date())
1023 Albums.highlight(i);
1024 Songs.clear();
1025 break;
1030 Songs.showAll();
1031 if (Songs.empty())
1032 update();
1034 if (s != Songs.current().value())
1036 for (size_t i = 0; i < Songs.size(); ++i)
1038 if (s == Songs[i].value())
1040 Songs.highlight(i);
1041 break;
1046 Tags.setHighlightColor(Config.main_highlight_color);
1047 Albums.setHighlightColor(Config.main_highlight_color);
1048 Songs.setHighlightColor(Config.active_column_color);
1049 w = &Songs;
1050 refresh();
1053 void MediaLibrary::AddToPlaylist(bool add_n_play)
1055 if (isActiveWindow(Songs) && !Songs.empty())
1056 addSongToPlaylist(Songs.current().value(), add_n_play);
1057 else
1059 if ((!Tags.empty() && isActiveWindow(Tags))
1060 || (isActiveWindow(Albums) && Albums.current().value().isAllTracksEntry()))
1062 MPD::SongList list;
1063 Mpd.StartSearch(true);
1064 Mpd.AddSearch(Config.media_lib_primary_tag, Tags.current().value().tag());
1065 Mpd.CommitSearchSongs(vectorMoveInserter(list));
1066 bool success = addSongsToPlaylist(list.begin(), list.end(), add_n_play, -1);
1067 std::string tag_type = boost::locale::to_lower(
1068 tagTypeToString(Config.media_lib_primary_tag));
1069 Statusbar::printf("Songs with %1% \"%2%\" added%3%",
1070 tag_type, Tags.current().value().tag(), withErrors(success)
1073 else if (isActiveWindow(Albums))
1075 MPD::SongList list;
1076 withSongsFromAlbum(Albums.current().value(), vectorMoveInserter(list));
1077 bool success = addSongsToPlaylist(list.begin(), list.end(), add_n_play, -1);
1078 Statusbar::printf("Songs from album \"%1%\" added%2%",
1079 Albums.current().value().entry().album(), withErrors(success)
1084 if (!add_n_play)
1086 w->scroll(NC::Scroll::Down);
1087 if (isActiveWindow(Tags))
1089 Albums.clear();
1090 Songs.clear();
1092 else if (isActiveWindow(Albums))
1093 Songs.clear();
1097 namespace {//
1099 std::string AlbumToString(const AlbumEntry &ae)
1101 std::string result;
1102 if (ae.isAllTracksEntry())
1103 result = "All tracks";
1104 else
1106 if (hasTwoColumns)
1108 if (ae.entry().tag().empty())
1109 result += Config.empty_tag;
1110 else
1111 result += ae.entry().tag();
1112 result += " - ";
1114 if (Config.media_lib_primary_tag != MPD_TAG_DATE && !ae.entry().date().empty())
1115 result += "(" + ae.entry().date() + ") ";
1116 result += ae.entry().album().empty() ? "<no album>" : ae.entry().album();
1118 return result;
1121 std::string SongToString(const MPD::Song &s)
1123 return s.toString(Config.song_library_format, Config.tags_separator);
1126 bool TagEntryMatcher(const boost::regex &rx, const MediaLibrary::PrimaryTag &pt)
1128 return boost::regex_search(pt.tag(), rx);
1131 bool AlbumEntryMatcher(const boost::regex &rx, const NC::Menu<AlbumEntry>::Item &item, bool filter)
1133 if (item.isSeparator() || item.value().isAllTracksEntry())
1134 return filter;
1135 return boost::regex_search(AlbumToString(item.value()), rx);
1138 bool SongEntryMatcher(const boost::regex &rx, const MPD::Song &s)
1140 return boost::regex_search(SongToString(s), rx);