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