30de3df43d3e5ed67b7c23746040f516026487fc
[ncmpc-debian.git] / src / screen_artist.cxx
1 /* ncmpc (Ncurses MPD Client)
2  * (c) 2004-2018 The Music Player Daemon Project
3  * Project homepage: http://musicpd.org
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 along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18  */
19
20 #include "screen_artist.hxx"
21 #include "screen_interface.hxx"
22 #include "screen_status.hxx"
23 #include "screen_find.hxx"
24 #include "screen_browser.hxx"
25 #include "i18n.h"
26 #include "charset.hxx"
27 #include "mpdclient.hxx"
28 #include "filelist.hxx"
29 #include "options.hxx"
30 #include "util/NulledString.hxx"
31
32 #include <vector>
33 #include <string>
34 #include <algorithm>
35
36 #include <assert.h>
37 #include <string.h>
38 #include <glib.h>
39
40 #define BUFSIZE 1024
41
42 class ArtistBrowserPage final : public FileListPage {
43         enum class Mode {
44                 ARTISTS,
45                 ALBUMS,
46                 SONGS,
47         } mode = Mode::ARTISTS;
48
49         std::vector<std::string> artist_list, album_list;
50         std::string artist;
51
52         /**
53          * The current album filter.  If IsNulled() is true, then the
54          * album filter is not used (i.e. all songs from all albums
55          * are displayed).
56          */
57         std::string album;
58
59 public:
60         ArtistBrowserPage(ScreenManager &_screen, WINDOW *_w,
61                           Size size)
62                 :FileListPage(_screen, _w, size,
63                               options.list_format) {}
64
65 private:
66         void FreeLists();
67         void LoadArtistList(struct mpdclient &c);
68         void LoadAlbumList(struct mpdclient &c);
69         void LoadSongList(struct mpdclient &c);
70         void Reload(struct mpdclient &c);
71
72         void OpenArtistList(struct mpdclient &c);
73         void OpenArtistList(struct mpdclient &c, const char *selected_value);
74         void OpenAlbumList(struct mpdclient &c, std::string _artist);
75         void OpenAlbumList(struct mpdclient &c, std::string _artist,
76                            const char *selected_value);
77         void OpenSongList(struct mpdclient &c, std::string _artist,
78                           std::string _album);
79
80         bool OnListCommand(struct mpdclient &c, command_t cmd);
81
82 public:
83         /* virtual methods from class Page */
84         void OnOpen(struct mpdclient &c) override;
85         void Paint() const override;
86         void Update(struct mpdclient &c, unsigned events) override;
87         bool OnCommand(struct mpdclient &c, command_t cmd) override;
88         const char *GetTitle(char *s, size_t size) const override;
89 };
90
91 gcc_pure
92 static int
93 string_array_find(const std::vector<std::string> &array, const char *value)
94 {
95         for (size_t i = 0; i < array.size(); ++i)
96                 if (array[i] == value)
97                         return i;
98
99         return -1;
100 }
101
102 gcc_pure
103 static bool
104 CompareUTF8(const std::string &a, const std::string &b)
105 {
106         char *key1 = g_utf8_collate_key(a.c_str(), -1);
107         char *key2 = g_utf8_collate_key(b.c_str(), -1);
108         int n = strcmp(key1,key2);
109         g_free(key1);
110         g_free(key2);
111         return n < 0;
112 }
113
114 /* list_window callback */
115 static const char *
116 screen_artist_lw_callback(unsigned idx, void *data)
117 {
118         const auto &list = *(const std::vector<std::string> *)data;
119
120         /*
121         if (mode == Mode::ALBUMS) {
122                 if (idx == 0)
123                         return "..";
124                 else if (idx == list.size() + 1)
125                         return _("All tracks");
126
127                 --idx;
128         }
129         */
130
131         assert(idx < list.size());
132
133         const char *str_utf8 = list[idx].c_str();
134         char *str = utf8_to_locale(str_utf8);
135
136         static char buf[BUFSIZE];
137         g_strlcpy(buf, str, sizeof(buf));
138         g_free(str);
139
140         return buf;
141 }
142
143 /* list_window callback */
144 static const char *
145 AlbumListCallback(unsigned idx, void *data)
146 {
147         const auto &list = *(const std::vector<std::string> *)data;
148
149         if (idx == 0)
150                 return "..";
151         else if (idx == list.size() + 1)
152                 return _("All tracks");
153
154         --idx;
155
156         return screen_artist_lw_callback(idx, data);
157 }
158
159 void
160 ArtistBrowserPage::FreeLists()
161 {
162         artist_list.clear();
163         album_list.clear();
164
165         delete filelist;
166         filelist = nullptr;
167 }
168
169 static void
170 recv_tag_values(struct mpd_connection *connection, enum mpd_tag_type tag,
171                 std::vector<std::string> &list)
172 {
173         struct mpd_pair *pair;
174
175         while ((pair = mpd_recv_pair_tag(connection, tag)) != nullptr) {
176                 list.emplace_back(pair->value);
177                 mpd_return_pair(connection, pair);
178         }
179 }
180
181 void
182 ArtistBrowserPage::LoadArtistList(struct mpdclient &c)
183 {
184         struct mpd_connection *connection = mpdclient_get_connection(&c);
185
186         assert(mode == Mode::ARTISTS);
187         assert(artist_list.empty());
188         assert(album_list.empty());
189         assert(filelist == nullptr);
190
191         artist_list.clear();
192
193         if (connection != nullptr) {
194                 mpd_search_db_tags(connection, MPD_TAG_ARTIST);
195                 mpd_search_commit(connection);
196                 recv_tag_values(connection, MPD_TAG_ARTIST, artist_list);
197
198                 mpdclient_finish_command(&c);
199         }
200
201         /* sort list */
202         std::sort(artist_list.begin(), artist_list.end(), CompareUTF8);
203         lw.SetLength(artist_list.size());
204 }
205
206 void
207 ArtistBrowserPage::LoadAlbumList(struct mpdclient &c)
208 {
209         struct mpd_connection *connection = mpdclient_get_connection(&c);
210
211         assert(mode == Mode::ALBUMS);
212         assert(album_list.empty());
213         assert(filelist == nullptr);
214
215         if (connection != nullptr) {
216                 mpd_search_db_tags(connection, MPD_TAG_ALBUM);
217                 mpd_search_add_tag_constraint(connection,
218                                               MPD_OPERATOR_DEFAULT,
219                                               MPD_TAG_ARTIST, artist.c_str());
220                 mpd_search_commit(connection);
221
222                 recv_tag_values(connection, MPD_TAG_ALBUM, album_list);
223
224                 mpdclient_finish_command(&c);
225         }
226
227         /* sort list */
228         std::sort(album_list.begin(), album_list.end(), CompareUTF8);
229         lw.SetLength(album_list.size() + 2);
230 }
231
232 void
233 ArtistBrowserPage::LoadSongList(struct mpdclient &c)
234 {
235         struct mpd_connection *connection = mpdclient_get_connection(&c);
236
237         assert(mode == Mode::SONGS);
238         assert(filelist == nullptr);
239
240         filelist = new FileList();
241         /* add a dummy entry for ".." */
242         filelist->emplace_back(nullptr);
243
244         if (connection != nullptr) {
245                 mpd_search_db_songs(connection, true);
246                 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
247                                               MPD_TAG_ARTIST, artist.c_str());
248                 if (!IsNulled(album))
249                         mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
250                                                       MPD_TAG_ALBUM, album.c_str());
251                 mpd_search_commit(connection);
252
253                 filelist->Receive(*connection);
254
255                 mpdclient_finish_command(&c);
256         }
257
258         /* fix highlights */
259         screen_browser_sync_highlights(filelist, &c.playlist);
260         lw.SetLength(filelist->size());
261 }
262
263 void
264 ArtistBrowserPage::OpenArtistList(struct mpdclient &c)
265 {
266         FreeLists();
267
268         mode = Mode::ARTISTS;
269         LoadArtistList(c);
270 }
271
272 void
273 ArtistBrowserPage::OpenArtistList(struct mpdclient &c,
274                                   const char *selected_value)
275 {
276         OpenArtistList(c);
277
278         lw.Reset();
279
280         int idx = string_array_find(artist_list, selected_value);
281         if (idx >= 0) {
282                 lw.SetCursor(idx);
283                 lw.Center(idx);
284         }
285 }
286
287 void
288 ArtistBrowserPage::OpenAlbumList(struct mpdclient &c, std::string _artist)
289 {
290         FreeLists();
291
292         mode = Mode::ALBUMS;
293         artist = std::move(_artist);
294         LoadAlbumList(c);
295 }
296
297 void
298 ArtistBrowserPage::OpenAlbumList(struct mpdclient &c, std::string _artist,
299                                  const char *selected_value)
300 {
301         OpenAlbumList(c, std::move(_artist));
302
303         lw.Reset();
304
305         int idx = selected_value == nullptr
306                 ? (int)album_list.size()
307                 : string_array_find(album_list, selected_value);
308         if (idx >= 0) {
309                 ++idx;
310                 lw.SetCursor(idx);
311                 lw.Center(idx);
312         }
313 }
314
315 void
316 ArtistBrowserPage::OpenSongList(struct mpdclient &c,
317                                 std::string _artist, std::string _album)
318 {
319         FreeLists();
320
321         mode = Mode::SONGS;
322         artist = std::move(_artist);
323         album = std::move(_album);
324         LoadSongList(c);
325 }
326
327 void
328 ArtistBrowserPage::Reload(struct mpdclient &c)
329 {
330         FreeLists();
331
332         switch (mode) {
333         case Mode::ARTISTS:
334                 LoadArtistList(c);
335                 break;
336
337         case Mode::ALBUMS:
338                 LoadAlbumList(c);
339                 break;
340
341         case Mode::SONGS:
342                 LoadSongList(c);
343                 break;
344         }
345 }
346
347 static Page *
348 screen_artist_init(ScreenManager &_screen, WINDOW *w, Size size)
349 {
350         return new ArtistBrowserPage(_screen, w, size);
351 }
352
353 void
354 ArtistBrowserPage::OnOpen(struct mpdclient &c)
355 {
356         if (artist_list.empty())
357                 Reload(c);
358 }
359
360 /**
361  * Paint one item in the artist list.
362  */
363 static void
364 paint_artist_callback(WINDOW *w, unsigned i,
365                       gcc_unused unsigned y, unsigned width,
366                       bool selected, const void *data)
367 {
368         const auto &list = *(const std::vector<std::string> *)data;
369         char *p = utf8_to_locale(list[i].c_str());
370
371         screen_browser_paint_directory(w, width, selected, p);
372         g_free(p);
373 }
374
375 /**
376  * Paint one item in the album list.  There are two virtual items
377  * inserted: at the beginning, there's the special item ".." to go to
378  * the parent directory, and at the end, there's the item "All tracks"
379  * to view the tracks of all albums.
380  */
381 static void
382 paint_album_callback(WINDOW *w, unsigned i,
383                      gcc_unused unsigned y, unsigned width,
384                      bool selected, const void *data)
385 {
386         const auto &list = *(const std::vector<std::string> *)data;
387         const char *p;
388         char *q = nullptr;
389
390         if (i == 0)
391                 p = "..";
392         else if (i == list.size() + 1)
393                 p = _("All tracks");
394         else
395                 p = q = utf8_to_locale(list[i - 1].c_str());
396
397         screen_browser_paint_directory(w, width, selected, p);
398         g_free(q);
399 }
400
401 void
402 ArtistBrowserPage::Paint() const
403 {
404         switch (mode) {
405         case Mode::ARTISTS:
406                 lw.Paint(paint_artist_callback, &artist_list);
407                 break;
408
409         case Mode::ALBUMS:
410                 lw.Paint(paint_album_callback, &album_list);
411                 break;
412
413         case Mode::SONGS:
414                 FileListPage::Paint();
415                 break;
416         }
417 }
418
419 const char *
420 ArtistBrowserPage::GetTitle(char *str, size_t size) const
421 {
422         switch(mode) {
423                 char *s1, *s2;
424
425         case Mode::ARTISTS:
426                 g_snprintf(str, size, _("All artists"));
427                 break;
428
429         case Mode::ALBUMS:
430                 s1 = utf8_to_locale(artist.c_str());
431                 g_snprintf(str, size, _("Albums of artist: %s"), s1);
432                 g_free(s1);
433                 break;
434
435         case Mode::SONGS:
436                 s1 = utf8_to_locale(artist.c_str());
437
438                 if (IsNulled(album))
439                         g_snprintf(str, size,
440                                    _("All tracks of artist: %s"), s1);
441                 else if (!album.empty()) {
442                         s2 = utf8_to_locale(album.c_str());
443                         g_snprintf(str, size, _("Album: %s - %s"), s1, s2);
444                         g_free(s2);
445                 } else
446                         g_snprintf(str, size,
447                                    _("Tracks of no album of artist: %s"), s1);
448                 g_free(s1);
449                 break;
450         }
451
452         return str;
453 }
454
455 void
456 ArtistBrowserPage::Update(struct mpdclient &c, unsigned events)
457 {
458         if (filelist == nullptr)
459                 return;
460
461         if (events & MPD_IDLE_DATABASE)
462                 /* the db has changed -> update the list */
463                 Reload(c);
464
465         if (events & (MPD_IDLE_DATABASE | MPD_IDLE_QUEUE))
466                 screen_browser_sync_highlights(filelist, &c.playlist);
467
468         if (events & (MPD_IDLE_DATABASE
469 #ifndef NCMPC_MINI
470                       | MPD_IDLE_QUEUE
471 #endif
472                       ))
473                 SetDirty();
474 }
475
476 /* add_query - Add all songs satisfying specified criteria.
477    _artist is actually only used in the ALBUM case to distinguish albums with
478    the same name from different artists. */
479 static void
480 add_query(struct mpdclient *c, enum mpd_tag_type table, const char *_filter,
481           const char *_artist)
482 {
483         struct mpd_connection *connection = mpdclient_get_connection(c);
484
485         assert(_filter != nullptr);
486
487         if (connection == nullptr)
488                 return;
489
490         char *str = utf8_to_locale(_filter);
491         if (table == MPD_TAG_ALBUM)
492                 screen_status_printf(_("Adding album %s..."), str);
493         else
494                 screen_status_printf(_("Adding %s..."), str);
495         g_free(str);
496
497         mpd_search_db_songs(connection, true);
498         mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
499                                       table, _filter);
500         if (table == MPD_TAG_ALBUM)
501                 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
502                                               MPD_TAG_ARTIST, _artist);
503         mpd_search_commit(connection);
504
505         auto *addlist = filelist_new_recv(connection);
506
507         if (mpdclient_finish_command(c))
508                 mpdclient_filelist_add_all(c, addlist);
509
510         delete addlist;
511 }
512
513 inline bool
514 ArtistBrowserPage::OnListCommand(struct mpdclient &c, command_t cmd)
515 {
516         switch (mode) {
517         case Mode::ARTISTS:
518         case Mode::ALBUMS:
519                 if (lw.HandleCommand(cmd)) {
520                         SetDirty();
521                         return true;
522                 }
523
524                 return false;
525
526         case Mode::SONGS:
527                 return FileListPage::OnCommand(c, cmd);
528         }
529
530         assert(0);
531         return 0;
532 }
533
534 bool
535 ArtistBrowserPage::OnCommand(struct mpdclient &c, command_t cmd)
536 {
537         switch(cmd) {
538                 const char *selected;
539
540         case CMD_PLAY:
541                 switch (mode) {
542                 case Mode::ARTISTS:
543                         if (lw.selected >= artist_list.size())
544                                 return true;
545
546                         selected = artist_list[lw.selected].c_str();
547                         OpenAlbumList(c, selected);
548                         lw.Reset();
549
550                         SetDirty();
551                         return true;
552
553                 case Mode::ALBUMS:
554                         if (lw.selected == 0) {
555                                 /* handle ".." */
556                                 OpenArtistList(c, artist.c_str());
557                         } else if (lw.selected == album_list.size() + 1) {
558                                 /* handle "show all" */
559                                 OpenSongList(c, std::move(artist),
560                                              MakeNulledString());
561                                 lw.Reset();
562                         } else {
563                                 /* select album */
564                                 selected = album_list[lw.selected - 1].c_str();
565                                 OpenSongList(c, std::move(artist), selected);
566                                 lw.Reset();
567                         }
568
569                         SetDirty();
570                         return true;
571
572                 case Mode::SONGS:
573                         if (lw.selected == 0) {
574                                 /* handle ".." */
575                                 OpenAlbumList(c, std::move(artist),
576                                               IsNulled(album) ? nullptr : album.c_str());
577                                 SetDirty();
578                                 return true;
579                         }
580                         break;
581                 }
582                 break;
583
584                 /* FIXME? CMD_GO_* handling duplicates code from CMD_PLAY */
585
586         case CMD_GO_PARENT_DIRECTORY:
587                 switch (mode) {
588                 case Mode::ARTISTS:
589                         break;
590
591                 case Mode::ALBUMS:
592                         OpenArtistList(c, artist.c_str());
593                         break;
594
595                 case Mode::SONGS:
596                         OpenAlbumList(c, std::move(artist),
597                                       IsNulled(album) ? nullptr : album.c_str());
598                         break;
599                 }
600
601                 SetDirty();
602                 break;
603
604         case CMD_GO_ROOT_DIRECTORY:
605                 switch (mode) {
606                 case Mode::ARTISTS:
607                         break;
608
609                 case Mode::ALBUMS:
610                 case Mode::SONGS:
611                         OpenArtistList(c);
612                         lw.Reset();
613                         /* restore first list window state (pop while returning true) */
614                         /* XXX */
615                         break;
616                 }
617
618                 SetDirty();
619                 break;
620
621         case CMD_SELECT:
622         case CMD_ADD:
623                 switch(mode) {
624                 case Mode::ARTISTS:
625                         if (lw.selected >= artist_list.size())
626                                 return true;
627
628                         for (const unsigned i : lw.GetRange()) {
629                                 selected = artist_list[i].c_str();
630                                 add_query(&c, MPD_TAG_ARTIST, selected, nullptr);
631                                 cmd = CMD_LIST_NEXT; /* continue and select next item... */
632                         }
633                         break;
634
635                 case Mode::ALBUMS:
636                         for (const unsigned i : lw.GetRange()) {
637                                 if(i == album_list.size() + 1)
638                                         add_query(&c, MPD_TAG_ARTIST,
639                                                   artist.c_str(), nullptr);
640                                 else if (i > 0)
641                                 {
642                                         selected = album_list[lw.selected - 1].c_str();
643                                         add_query(&c, MPD_TAG_ALBUM, selected,
644                                                   artist.c_str());
645                                         cmd = CMD_LIST_NEXT; /* continue and select next item... */
646                                 }
647                         }
648                         break;
649
650                 case Mode::SONGS:
651                         /* handled by browser_cmd() */
652                         break;
653                 }
654                 break;
655
656                 /* continue and update... */
657         case CMD_SCREEN_UPDATE:
658                 Reload(c);
659                 return false;
660
661         case CMD_LIST_FIND:
662         case CMD_LIST_RFIND:
663         case CMD_LIST_FIND_NEXT:
664         case CMD_LIST_RFIND_NEXT:
665                 switch (mode) {
666                 case Mode::ARTISTS:
667                         screen_find(screen, &lw, cmd,
668                                     screen_artist_lw_callback, &artist_list);
669                         SetDirty();
670                         return true;
671
672                 case Mode::ALBUMS:
673                         screen_find(screen, &lw, cmd,
674                                     AlbumListCallback, &album_list);
675                         SetDirty();
676                         return true;
677
678                 case Mode::SONGS:
679                         /* handled by browser_cmd() */
680                         break;
681                 }
682
683                 break;
684
685         case CMD_LIST_JUMP:
686                 switch (mode) {
687                 case Mode::ARTISTS:
688                         screen_jump(screen, &lw,
689                                     screen_artist_lw_callback, &artist_list,
690                                     paint_artist_callback, &artist_list);
691                         SetDirty();
692                         return true;
693
694                 case Mode::ALBUMS:
695                         screen_jump(screen, &lw,
696                                     AlbumListCallback, &album_list,
697                                     paint_album_callback, &album_list);
698                         SetDirty();
699                         return true;
700
701                 case Mode::SONGS:
702                         /* handled by browser_cmd() */
703                         break;
704                 }
705
706                 break;
707
708         default:
709                 break;
710         }
711
712         if (OnListCommand(c, cmd))
713                 return true;
714
715         return false;
716 }
717
718 const struct screen_functions screen_artist = {
719         "artist",
720         screen_artist_init,
721 };