ListCursor: store only height, move width to class ListWindow
[ncmpc-debian.git] / src / SongPage.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 "SongPage.hxx"
21 #include "PageMeta.hxx"
22 #include "ListPage.hxx"
23 #include "ListText.hxx"
24 #include "TextListRenderer.hxx"
25 #include "FileBrowserPage.hxx"
26 #include "LyricsPage.hxx"
27 #include "screen_find.hxx"
28 #include "Command.hxx"
29 #include "i18n.h"
30 #include "screen.hxx"
31 #include "charset.hxx"
32 #include "time_format.hxx"
33 #include "mpdclient.hxx"
34 #include "util/LocaleString.hxx"
35 #include "util/Macros.hxx"
36 #include "util/StringStrip.hxx"
37
38 #include <mpd/client.h>
39
40 #include <algorithm>
41 #include <vector>
42 #include <string>
43
44 #include <assert.h>
45 #include <string.h>
46 #include <time.h>
47
48 enum {
49         LABEL_LENGTH = MPD_TAG_COUNT,
50         LABEL_PATH,
51         LABEL_BITRATE,
52         LABEL_FORMAT,
53         LABEL_POSITION,
54 };
55
56 struct tag_label {
57         unsigned tag_type;
58         const char *label;
59 };
60
61 static constexpr struct tag_label tag_labels[] = {
62         { MPD_TAG_ARTIST, N_("Artist") },
63         { MPD_TAG_TITLE, N_("Title") },
64         { MPD_TAG_ALBUM, N_("Album") },
65         { LABEL_LENGTH, N_("Length") },
66         { LABEL_POSITION, N_("Position") },
67         { MPD_TAG_COMPOSER, N_("Composer") },
68         { MPD_TAG_NAME, N_("Name") },
69         { MPD_TAG_DISC, N_("Disc") },
70         { MPD_TAG_TRACK, N_("Track") },
71         { MPD_TAG_DATE, N_("Date") },
72         { MPD_TAG_GENRE, N_("Genre") },
73         { MPD_TAG_COMMENT, N_("Comment") },
74         { LABEL_PATH, N_("Path") },
75         { LABEL_BITRATE, N_("Bitrate") },
76         { LABEL_FORMAT, N_("Format") },
77         { 0, nullptr }
78 };
79
80 static unsigned max_tag_label_width;
81
82 enum stats_label {
83         STATS_ARTISTS,
84         STATS_ALBUMS,
85         STATS_SONGS,
86         STATS_UPTIME,
87         STATS_DBUPTIME,
88         STATS_PLAYTIME,
89         STATS_DBPLAYTIME,
90 };
91
92 static constexpr const char *stats_labels[] = {
93         N_("Number of artists"),
94         N_("Number of albums"),
95         N_("Number of songs"),
96         N_("Uptime"),
97         N_("Most recent db update"),
98         N_("Playtime"),
99         N_("DB playtime"),
100 };
101
102 static unsigned max_stats_label_width;
103
104 static struct mpd_song *next_song;
105
106 class SongPage final : public ListPage, ListText {
107         ScreenManager &screen;
108
109         mpd_song *selected_song = nullptr;
110         mpd_song *played_song = nullptr;
111
112         std::vector<std::string> lines;
113
114 public:
115         SongPage(ScreenManager &_screen, WINDOW *w, Size size) noexcept
116                 :ListPage(w, size),
117                  screen(_screen) {
118                 lw.DisableCursor();
119         }
120
121         ~SongPage() noexcept override {
122                 Clear();
123         }
124
125 private:
126         void Clear() noexcept;
127
128         /**
129          * Appends a line with a fixed width for the label column.
130          * Handles nullptr strings gracefully.
131          */
132         void AppendLine(const char *label, const char *value,
133                         unsigned label_col) noexcept;
134
135         void AppendTag(const struct mpd_song *song,
136                        enum mpd_tag_type tag) noexcept;
137         void AddSong(const struct mpd_song *song) noexcept;
138         void AppendStatsLine(enum stats_label label,
139                              const char *value) noexcept;
140         bool AddStats(struct mpd_connection *connection) noexcept;
141
142 public:
143         /* virtual methods from class Page */
144         void OnClose() noexcept override {
145                 Clear();
146         }
147
148         void Paint() const noexcept override;
149         void Update(struct mpdclient &c, unsigned events) noexcept override;
150         bool OnCommand(struct mpdclient &c, Command cmd) override;
151         const char *GetTitle(char *s, size_t size) const noexcept override;
152
153 private:
154         /* virtual methods from class ListText */
155         const char *GetListItemText(char *buffer, size_t size,
156                                     unsigned i) const noexcept override;
157 };
158
159 void
160 SongPage::Clear() noexcept
161 {
162         lines.clear();
163
164         if (selected_song != nullptr) {
165                 mpd_song_free(selected_song);
166                 selected_song = nullptr;
167         }
168         if (played_song != nullptr) {
169                 mpd_song_free(played_song);
170                 played_song = nullptr;
171         }
172 }
173
174 const char *
175 SongPage::GetListItemText(char *, size_t, unsigned idx) const noexcept
176 {
177         return lines[idx].c_str();
178 }
179
180 static std::unique_ptr<Page>
181 screen_song_init(ScreenManager &_screen, WINDOW *w, Size size) noexcept
182 {
183         for (unsigned i = 0; tag_labels[i].label != nullptr; ++i) {
184                 unsigned width = StringWidthMB(gettext(tag_labels[i].label));
185                 if (width > max_tag_label_width)
186                         max_tag_label_width = width;
187         }
188
189         for (unsigned i = 0; i < ARRAY_SIZE(stats_labels); ++i) {
190                 if (stats_labels[i] != nullptr) {
191                         unsigned width = StringWidthMB(gettext(stats_labels[i]));
192
193                         if (width > max_stats_label_width)
194                                 max_stats_label_width = width;
195                 }
196         }
197
198         return std::make_unique<SongPage>(_screen, w, size);
199 }
200
201 const char *
202 SongPage::GetTitle(gcc_unused char *str, gcc_unused size_t size) const noexcept
203 {
204         return _("Song viewer");
205 }
206
207 void
208 SongPage::Paint() const noexcept
209 {
210         lw.Paint(TextListRenderer(*this));
211 }
212
213 void
214 SongPage::AppendLine(const char *label, const char *value_utf8,
215                      unsigned label_col) noexcept
216 {
217         assert(label != nullptr);
218         assert(value_utf8 != nullptr);
219
220         static constexpr size_t BUFFER_SIZE = 1024;
221         if (label_col >= BUFFER_SIZE - 16)
222                 return;
223
224         /* +2 for ': ' */
225         label_col += 2;
226         const int value_col = lw.GetWidth() - label_col;
227         /* calculate the number of required linebreaks */
228         const Utf8ToLocale value_locale(value_utf8);
229         const char *value = value_locale.c_str();
230
231         const char *const value_end = value + strlen(value);
232         const char *value_iter = value;
233
234         while (*value_iter != 0) {
235                 char buffer[BUFFER_SIZE];
236                 const char *const buffer_end = buffer + BUFFER_SIZE;
237
238                 char *p = buffer;
239                 size_t n_space = label_col;
240                 if (value_iter == value) {
241                         const size_t label_length = std::min(strlen(label),
242                                                              BUFFER_SIZE - 16);
243                         p = std::copy_n(label, label_length, p);
244                         *p++ = ':';
245                         /* fill the label column with whitespaces */
246                         const unsigned label_width = StringWidthMB(label);
247                         n_space -= label_width + 1;
248                 }
249
250                 p = std::fill_n(p, n_space, ' ');
251
252                 /* skip whitespaces */
253                 value_iter = StripLeft(value_iter);
254
255                 const char *value_iter_end = AtWidthMB(value_iter,
256                                                        value_end - value_iter,
257                                                        value_col);
258                 if (value_iter_end == value_iter)
259                         /* not enough room for anything - bail out */
260                         break;
261
262                 p += snprintf(p, buffer_end - p, "%.*s",
263                               int(value_iter_end - value_iter), value_iter);
264                 value_iter = value_iter_end;
265
266                 lines.emplace_back(buffer, p);
267         }
268 }
269
270 gcc_pure
271 static const char *
272 get_tag_label(unsigned tag) noexcept
273 {
274         for (unsigned i = 0; tag_labels[i].label != nullptr; ++i)
275                 if (tag_labels[i].tag_type == tag)
276                         return gettext(tag_labels[i].label);
277
278         assert(tag < MPD_TAG_COUNT);
279         return mpd_tag_name((enum mpd_tag_type)tag);
280 }
281
282 void
283 SongPage::AppendTag(const struct mpd_song *song,
284                     enum mpd_tag_type tag) noexcept
285 {
286         const char *label = get_tag_label(tag);
287         unsigned i = 0;
288         const char *value;
289
290         assert((unsigned)tag < ARRAY_SIZE(tag_labels));
291         assert(label != nullptr);
292
293         while ((value = mpd_song_get_tag(song, tag, i++)) != nullptr)
294                 AppendLine(label, value, max_tag_label_width);
295 }
296
297 void
298 SongPage::AddSong(const struct mpd_song *song) noexcept
299 {
300         assert(song != nullptr);
301
302         char songpos[16];
303         snprintf(songpos, sizeof(songpos), "%d", mpd_song_get_pos(song) + 1);
304         AppendLine(get_tag_label(LABEL_POSITION), songpos,
305                    max_tag_label_width);
306
307         AppendTag(song, MPD_TAG_ARTIST);
308         AppendTag(song, MPD_TAG_TITLE);
309         AppendTag(song, MPD_TAG_ALBUM);
310
311         /* create time string and add it */
312         if (mpd_song_get_duration(song) > 0) {
313                 char length[16];
314                 format_duration_short(length, sizeof(length),
315                                       mpd_song_get_duration(song));
316
317                 const char *value = length;
318
319                 char buffer[64];
320
321                 if (mpd_song_get_end(song) > 0) {
322                         char start[16], end[16];
323                         format_duration_short(start, sizeof(start),
324                                               mpd_song_get_start(song));
325                         format_duration_short(end, sizeof(end),
326                                               mpd_song_get_end(song));
327
328                         snprintf(buffer, sizeof(buffer), "%s [%s-%s]\n",
329                                  length, start, end);
330                         value = buffer;
331                 } else if (mpd_song_get_start(song) > 0) {
332                         char start[16];
333                         format_duration_short(start, sizeof(start),
334                                               mpd_song_get_start(song));
335
336                         snprintf(buffer, sizeof(buffer), "%s [%s-]\n",
337                                  length, start);
338                         value = buffer;
339                 }
340
341                 AppendLine(get_tag_label(LABEL_LENGTH), value,
342                                    max_tag_label_width);
343         }
344
345         AppendTag(song, MPD_TAG_COMPOSER);
346         AppendTag(song, MPD_TAG_NAME);
347         AppendTag(song, MPD_TAG_DISC);
348         AppendTag(song, MPD_TAG_TRACK);
349         AppendTag(song, MPD_TAG_DATE);
350         AppendTag(song, MPD_TAG_GENRE);
351         AppendTag(song, MPD_TAG_COMMENT);
352
353         AppendLine(get_tag_label(LABEL_PATH), mpd_song_get_uri(song),
354                    max_tag_label_width);
355 }
356
357 void
358 SongPage::AppendStatsLine(enum stats_label label, const char *value) noexcept
359 {
360         AppendLine(gettext(stats_labels[label]), value, max_stats_label_width);
361 }
362
363 bool
364 SongPage::AddStats(struct mpd_connection *connection) noexcept
365 {
366         struct mpd_stats *mpd_stats = mpd_run_stats(connection);
367         if (mpd_stats == nullptr)
368                 return false;
369
370         lines.emplace_back(_("MPD statistics"));
371
372         char buf[64];
373         snprintf(buf, sizeof(buf), "%d",
374                  mpd_stats_get_number_of_artists(mpd_stats));
375         AppendStatsLine(STATS_ARTISTS, buf);
376         snprintf(buf, sizeof(buf), "%d",
377                  mpd_stats_get_number_of_albums(mpd_stats));
378         AppendStatsLine(STATS_ALBUMS, buf);
379         snprintf(buf, sizeof(buf), "%d",
380                  mpd_stats_get_number_of_songs(mpd_stats));
381         AppendStatsLine(STATS_SONGS, buf);
382
383         format_duration_long(buf, sizeof(buf),
384                              mpd_stats_get_db_play_time(mpd_stats));
385         AppendStatsLine(STATS_DBPLAYTIME, buf);
386
387         format_duration_long(buf, sizeof(buf),
388                              mpd_stats_get_play_time(mpd_stats));
389         AppendStatsLine(STATS_PLAYTIME, buf);
390
391         format_duration_long(buf, sizeof(buf),
392                              mpd_stats_get_uptime(mpd_stats));
393         AppendStatsLine(STATS_UPTIME, buf);
394
395         const time_t t = mpd_stats_get_db_update_time(mpd_stats);
396         strftime(buf, sizeof(buf), "%x", localtime(&t));
397         AppendStatsLine(STATS_DBUPTIME, buf);
398
399         mpd_stats_free(mpd_stats);
400         return true;
401 }
402
403 static void
404 audio_format_to_string(char *buffer, size_t size,
405                        const struct mpd_audio_format *format) noexcept
406 {
407 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
408         if (format->bits == MPD_SAMPLE_FORMAT_FLOAT) {
409                 snprintf(buffer, size, "%u:f:%u",
410                          format->sample_rate,
411                          format->channels);
412                 return;
413         }
414
415         if (format->bits == MPD_SAMPLE_FORMAT_DSD) {
416                 if (format->sample_rate > 0 &&
417                     format->sample_rate % 44100 == 0) {
418                         /* use shortcuts such as "dsd64" which implies the
419                            sample rate */
420                         snprintf(buffer, size, "dsd%u:%u",
421                                  format->sample_rate * 8 / 44100,
422                                  format->channels);
423                         return;
424                 }
425
426                 snprintf(buffer, size, "%u:dsd:%u",
427                          format->sample_rate,
428                          format->channels);
429                 return;
430         }
431 #endif
432
433         snprintf(buffer, size, "%u:%u:%u",
434                  format->sample_rate, format->bits,
435                  format->channels);
436 }
437
438 void
439 SongPage::Update(struct mpdclient &c, unsigned) noexcept
440 {
441         lines.clear();
442
443         /* If a song was selected before the song screen was opened */
444         if (next_song != nullptr) {
445                 assert(selected_song == nullptr);
446                 selected_song = next_song;
447                 next_song = nullptr;
448         }
449
450         const auto *const playing_song = c.GetPlayingSong();
451
452         if (selected_song != nullptr &&
453             (playing_song == nullptr ||
454              strcmp(mpd_song_get_uri(selected_song),
455                     mpd_song_get_uri(playing_song)) != 0)) {
456                 lines.emplace_back(_("Selected song"));
457                 AddSong(selected_song);
458                 lines.emplace_back(std::string());
459         }
460
461         if (playing_song) {
462                 if (played_song != nullptr)
463                         mpd_song_free(played_song);
464
465                 played_song = mpd_song_dup(playing_song);
466                 lines.emplace_back(_("Currently playing song"));
467                 AddSong(played_song);
468
469                 if (mpd_status_get_kbit_rate(c.status) > 0) {
470                         char buf[16];
471                         snprintf(buf, sizeof(buf), _("%d kbps"),
472                                  mpd_status_get_kbit_rate(c.status));
473                         AppendLine(get_tag_label(LABEL_BITRATE), buf,
474                                    max_tag_label_width);
475                 }
476
477                 const struct mpd_audio_format *format =
478                         mpd_status_get_audio_format(c.status);
479                 if (format) {
480                         char buf[32];
481                         audio_format_to_string(buf, sizeof(buf), format);
482                         AppendLine(get_tag_label(LABEL_FORMAT), buf,
483                                    max_tag_label_width);
484                 }
485
486                 lines.emplace_back(std::string());
487         }
488
489         /* Add some statistics about mpd */
490         auto *connection = c.GetConnection();
491         if (connection != nullptr && !AddStats(connection))
492                 c.HandleError();
493
494         lw.SetLength(lines.size());
495         SetDirty();
496 }
497
498 bool
499 SongPage::OnCommand(struct mpdclient &c, Command cmd)
500 {
501         if (ListPage::OnCommand(c, cmd))
502                 return true;
503
504         switch(cmd) {
505         case Command::LOCATE:
506                 if (selected_song != nullptr) {
507                         screen_file_goto_song(screen, c, *selected_song);
508                         return true;
509                 }
510                 if (played_song != nullptr) {
511                         screen_file_goto_song(screen, c, *played_song);
512                         return true;
513                 }
514
515                 return false;
516
517 #ifdef ENABLE_LYRICS_SCREEN
518         case Command::SCREEN_LYRICS:
519                 if (selected_song != nullptr) {
520                         screen_lyrics_switch(screen, c, *selected_song, false);
521                         return true;
522                 }
523                 if (played_song != nullptr) {
524                         screen_lyrics_switch(screen, c, *played_song, true);
525                         return true;
526                 }
527                 return false;
528
529 #endif
530
531         case Command::SCREEN_SWAP:
532                 if (selected_song != nullptr)
533                         screen.Swap(c, selected_song);
534                 else
535                 // No need to check if this is null - we'd pass null anyway
536                         screen.Swap(c, played_song);
537                 return true;
538
539         default:
540                 break;
541         }
542
543         if (screen_find(screen, lw, cmd, *this)) {
544                 /* center the row */
545                 lw.Center(lw.GetCursorIndex());
546                 SetDirty();
547                 return true;
548         }
549
550         return false;
551 }
552
553 const PageMeta screen_song = {
554         "song",
555         N_("Song"),
556         Command::SCREEN_SONG,
557         screen_song_init,
558 };
559
560 void
561 screen_song_switch(ScreenManager &_screen, struct mpdclient &c,
562                    const struct mpd_song &song) noexcept
563 {
564         next_song = mpd_song_dup(&song);
565         _screen.Switch(screen_song, c);
566 }