util/LocaleString: replacement for locale_width() using wcswidth()
[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 #include "util/StringUTF8.hxx"
38
39 #include <mpd/client.h>
40
41 #include <glib/gprintf.h>
42
43 #include <vector>
44 #include <string>
45
46 #include <assert.h>
47 #include <string.h>
48 #include <time.h>
49
50 enum {
51         LABEL_LENGTH = MPD_TAG_COUNT,
52         LABEL_PATH,
53         LABEL_BITRATE,
54         LABEL_FORMAT,
55         LABEL_POSITION,
56 };
57
58 struct tag_label {
59         unsigned tag_type;
60         const char *label;
61 };
62
63 static const struct tag_label tag_labels[] = {
64         { MPD_TAG_ARTIST, N_("Artist") },
65         { MPD_TAG_TITLE, N_("Title") },
66         { MPD_TAG_ALBUM, N_("Album") },
67         { LABEL_LENGTH, N_("Length") },
68         { LABEL_POSITION, N_("Position") },
69         { MPD_TAG_COMPOSER, N_("Composer") },
70         { MPD_TAG_NAME, N_("Name") },
71         { MPD_TAG_DISC, N_("Disc") },
72         { MPD_TAG_TRACK, N_("Track") },
73         { MPD_TAG_DATE, N_("Date") },
74         { MPD_TAG_GENRE, N_("Genre") },
75         { MPD_TAG_COMMENT, N_("Comment") },
76         { LABEL_PATH, N_("Path") },
77         { LABEL_BITRATE, N_("Bitrate") },
78         { LABEL_FORMAT, N_("Format") },
79         { 0, nullptr }
80 };
81
82 static unsigned max_tag_label_width;
83
84 enum stats_label {
85         STATS_ARTISTS,
86         STATS_ALBUMS,
87         STATS_SONGS,
88         STATS_UPTIME,
89         STATS_DBUPTIME,
90         STATS_PLAYTIME,
91         STATS_DBPLAYTIME,
92 };
93
94 static const char *const stats_labels[] = {
95         N_("Number of artists"),
96         N_("Number of albums"),
97         N_("Number of songs"),
98         N_("Uptime"),
99         N_("Most recent db update"),
100         N_("Playtime"),
101         N_("DB playtime"),
102 };
103
104 static unsigned max_stats_label_width;
105
106 static struct mpd_song *next_song;
107
108 class SongPage final : public ListPage, ListText {
109         ScreenManager &screen;
110
111         mpd_song *selected_song = nullptr;
112         mpd_song *played_song = nullptr;
113
114         std::vector<std::string> lines;
115
116 public:
117         SongPage(ScreenManager &_screen, WINDOW *w, Size size)
118                 :ListPage(w, size),
119                  screen(_screen) {
120                 lw.hide_cursor = true;
121         }
122
123         ~SongPage() override {
124                 Clear();
125         }
126
127 private:
128         void Clear();
129
130         /**
131          * Appends a line with a fixed width for the label column.
132          * Handles nullptr strings gracefully.
133          */
134         void AppendLine(const char *label, const char *value,
135                         unsigned label_col);
136
137         void AppendTag(const struct mpd_song *song, enum mpd_tag_type tag);
138         void AddSong(const struct mpd_song *song);
139         void AppendStatsLine(enum stats_label label, const char *value);
140         bool AddStats(struct mpd_connection *connection);
141
142 public:
143         /* virtual methods from class Page */
144         void OnClose() override {
145                 Clear();
146         }
147
148         void Paint() const override;
149         void Update(struct mpdclient &c, unsigned events) override;
150         bool OnCommand(struct mpdclient &c, Command cmd) override;
151         const char *GetTitle(char *s, size_t size) const override;
152
153 private:
154         /* virtual methods from class ListText */
155         const char *GetListItemText(char *buffer, size_t size,
156                                     unsigned i) const override;
157 };
158
159 void
160 SongPage::Clear()
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
176 {
177         return lines[idx].c_str();
178 }
179
180 static Page *
181 screen_song_init(ScreenManager &_screen, WINDOW *w, Size size)
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 new SongPage(_screen, w, size);
199 }
200
201 const char *
202 SongPage::GetTitle(gcc_unused char *str, gcc_unused size_t size) const
203 {
204         return _("Song viewer");
205 }
206
207 void
208 SongPage::Paint() const
209 {
210         lw.Paint(TextListRenderer(*this));
211 }
212
213 void
214 SongPage::AppendLine(const char *label, const char *value, unsigned label_col)
215 {
216         const unsigned label_width = StringWidthMB(label) + 2;
217
218         assert(label != nullptr);
219         assert(value != nullptr);
220         assert(g_utf8_validate(value, -1, nullptr));
221
222         /* +2 for ': ' */
223         label_col += 2;
224         const int value_col = lw.size.width - label_col;
225         /* calculate the number of required linebreaks */
226         const gchar *value_iter = value;
227         const size_t label_length = strlen(label);
228         const size_t label_size = label_length + label_col;
229
230         while (*value_iter != 0) {
231                 char *entry = (char *)g_malloc(label_size), *entry_iter;
232                 if (value_iter == value) {
233                         memcpy(entry, label, label_length);
234                         entry_iter = entry + label_length;
235                         *entry_iter++ = ':';
236                         /* fill the label column with whitespaces */
237                         size_t n_space = label_col - label_width + 1;
238                         memset(entry_iter, ' ', n_space);
239                         entry_iter += n_space;
240                 }
241                 else {
242                         /* fill the label column with whitespaces */
243                         memset(entry, ' ', label_col);
244                         entry_iter = entry + label_col;
245                 }
246                 /* skip whitespaces */
247                 value_iter = StripLeft(value_iter);
248
249                 char *p = g_strdup(value_iter);
250                 unsigned width = utf8_cut_width(p, value_col);
251                 if (width == 0) {
252                         /* not enough room for anything - bail out */
253                         g_free(entry);
254                         g_free(p);
255                         break;
256                 }
257
258                 *entry_iter = 0;
259
260                 value_iter += strlen(p);
261                 p = replace_utf8_to_locale(p);
262                 char *q = g_strconcat(entry, p, nullptr);
263                 g_free(entry);
264                 g_free(p);
265
266                 lines.emplace_back(q);
267                 g_free(q);
268         }
269 }
270
271 gcc_pure
272 static const char *
273 get_tag_label(unsigned tag)
274 {
275         for (unsigned i = 0; tag_labels[i].label != nullptr; ++i)
276                 if (tag_labels[i].tag_type == tag)
277                         return gettext(tag_labels[i].label);
278
279         assert(tag < MPD_TAG_COUNT);
280         return mpd_tag_name((enum mpd_tag_type)tag);
281 }
282
283 void
284 SongPage::AppendTag(const struct mpd_song *song, enum mpd_tag_type tag)
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)
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)
359 {
360         AppendLine(gettext(stats_labels[label]), value, max_stats_label_width);
361 }
362
363 bool
364 SongPage::AddStats(struct mpd_connection *connection)
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)
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)
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         if (selected_song != nullptr &&
451             (c.song == nullptr ||
452              strcmp(mpd_song_get_uri(selected_song),
453                     mpd_song_get_uri(c.song)) != 0 ||
454              !c.playing_or_paused)) {
455                 lines.emplace_back(_("Selected song"));
456                 AddSong(selected_song);
457                 lines.emplace_back(std::string());
458         }
459
460         if (c.song != nullptr && c.playing_or_paused) {
461                 if (played_song != nullptr)
462                         mpd_song_free(played_song);
463
464                 played_song = mpd_song_dup(c.song);
465                 lines.emplace_back(_("Currently playing song"));
466                 AddSong(played_song);
467
468                 if (mpd_status_get_kbit_rate(c.status) > 0) {
469                         char buf[16];
470                         snprintf(buf, sizeof(buf), _("%d kbps"),
471                                  mpd_status_get_kbit_rate(c.status));
472                         AppendLine(get_tag_label(LABEL_BITRATE), buf,
473                                    max_tag_label_width);
474                 }
475
476                 const struct mpd_audio_format *format =
477                         mpd_status_get_audio_format(c.status);
478                 if (format) {
479                         char buf[32];
480                         audio_format_to_string(buf, sizeof(buf), format);
481                         AppendLine(get_tag_label(LABEL_FORMAT), buf,
482                                    max_tag_label_width);
483                 }
484
485                 lines.emplace_back(std::string());
486         }
487
488         /* Add some statistics about mpd */
489         auto *connection = c.GetConnection();
490         if (connection != nullptr && !AddStats(connection))
491                 c.HandleError();
492
493         lw.SetLength(lines.size());
494         SetDirty();
495 }
496
497 bool
498 SongPage::OnCommand(struct mpdclient &c, Command cmd)
499 {
500         if (ListPage::OnCommand(c, cmd))
501                 return true;
502
503         switch(cmd) {
504         case Command::LOCATE:
505                 if (selected_song != nullptr) {
506                         screen_file_goto_song(screen, c, *selected_song);
507                         return true;
508                 }
509                 if (played_song != nullptr) {
510                         screen_file_goto_song(screen, c, *played_song);
511                         return true;
512                 }
513
514                 return false;
515
516 #ifdef ENABLE_LYRICS_SCREEN
517         case Command::SCREEN_LYRICS:
518                 if (selected_song != nullptr) {
519                         screen_lyrics_switch(screen, c, *selected_song, false);
520                         return true;
521                 }
522                 if (played_song != nullptr) {
523                         screen_lyrics_switch(screen, c, *played_song, true);
524                         return true;
525                 }
526                 return false;
527
528 #endif
529
530         case Command::SCREEN_SWAP:
531                 if (selected_song != nullptr)
532                         screen.Swap(c, selected_song);
533                 else
534                 // No need to check if this is null - we'd pass null anyway
535                         screen.Swap(c, played_song);
536                 return true;
537
538         default:
539                 break;
540         }
541
542         if (screen_find(screen, &lw, cmd, *this)) {
543                 /* center the row */
544                 lw.Center(lw.selected);
545                 SetDirty();
546                 return true;
547         }
548
549         return false;
550 }
551
552 const PageMeta screen_song = {
553         "song",
554         N_("Song"),
555         Command::SCREEN_SONG,
556         screen_song_init,
557 };
558
559 void
560 screen_song_switch(ScreenManager &_screen, struct mpdclient &c,
561                    const struct mpd_song &song)
562 {
563         next_song = mpd_song_dup(&song);
564         _screen.Switch(screen_song, c);
565 }