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