1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2018 The Music Player Daemon Project
3 * Project homepage: http://musicpd.org
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.
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.
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.
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"
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"
38 #include <mpd/client.h>
40 #include <glib/gprintf.h>
50 LABEL_LENGTH = MPD_TAG_COUNT,
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") },
81 static unsigned max_tag_label_width;
93 static const char *const stats_labels[] = {
94 N_("Number of artists"),
95 N_("Number of albums"),
96 N_("Number of songs"),
98 N_("Most recent db update"),
103 static unsigned max_stats_label_width;
105 static struct mpd_song *next_song;
107 class SongPage final : public ListPage, ListText {
108 ScreenManager &screen;
110 mpd_song *selected_song = nullptr;
111 mpd_song *played_song = nullptr;
113 std::vector<std::string> lines;
116 SongPage(ScreenManager &_screen, WINDOW *w, Size size)
119 lw.hide_cursor = true;
122 ~SongPage() override {
130 * Appends a line with a fixed width for the label column.
131 * Handles nullptr strings gracefully.
133 void AppendLine(const char *label, const char *value,
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);
142 /* virtual methods from class Page */
143 void OnClose() override {
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;
153 /* virtual methods from class ListText */
154 const char *GetListItemText(char *buffer, size_t size,
155 unsigned i) const override;
163 if (selected_song != nullptr) {
164 mpd_song_free(selected_song);
165 selected_song = nullptr;
167 if (played_song != nullptr) {
168 mpd_song_free(played_song);
169 played_song = nullptr;
174 SongPage::GetListItemText(char *, size_t, unsigned idx) const
176 return lines[idx].c_str();
180 screen_song_init(ScreenManager &_screen, WINDOW *w, Size size)
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;
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]));
192 if (width > max_stats_label_width)
193 max_stats_label_width = width;
197 return new SongPage(_screen, w, size);
201 SongPage::GetTitle(gcc_unused char *str, gcc_unused size_t size) const
203 return _("Song viewer");
207 SongPage::Paint() const
209 lw.Paint(TextListRenderer(*this));
213 SongPage::AppendLine(const char *label, const char *value, unsigned label_col)
215 const unsigned label_width = locale_width(label) + 2;
217 assert(label != nullptr);
218 assert(value != nullptr);
219 assert(g_utf8_validate(value, -1, nullptr));
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;
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;
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;
241 /* fill the label column with whitespaces */
242 memset(entry, ' ', label_col);
243 entry_iter = entry + label_col;
245 /* skip whitespaces */
246 value_iter = StripLeft(value_iter);
248 char *p = g_strdup(value_iter);
249 unsigned width = utf8_cut_width(p, value_col);
251 /* not enough room for anything - bail out */
259 value_iter += strlen(p);
260 p = replace_utf8_to_locale(p);
261 char *q = g_strconcat(entry, p, nullptr);
265 lines.emplace_back(q);
272 get_tag_label(unsigned tag)
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);
278 assert(tag < MPD_TAG_COUNT);
279 return mpd_tag_name((enum mpd_tag_type)tag);
283 SongPage::AppendTag(const struct mpd_song *song, enum mpd_tag_type tag)
285 const char *label = get_tag_label(tag);
289 assert((unsigned)tag < ARRAY_SIZE(tag_labels));
290 assert(label != nullptr);
292 while ((value = mpd_song_get_tag(song, tag, i++)) != nullptr)
293 AppendLine(label, value, max_tag_label_width);
297 SongPage::AddSong(const struct mpd_song *song)
299 assert(song != nullptr);
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);
306 AppendTag(song, MPD_TAG_ARTIST);
307 AppendTag(song, MPD_TAG_TITLE);
308 AppendTag(song, MPD_TAG_ALBUM);
310 /* create time string and add it */
311 if (mpd_song_get_duration(song) > 0) {
313 format_duration_short(length, sizeof(length),
314 mpd_song_get_duration(song));
316 const char *value = length;
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));
327 snprintf(buffer, sizeof(buffer), "%s [%s-%s]\n",
330 } else if (mpd_song_get_start(song) > 0) {
332 format_duration_short(start, sizeof(start),
333 mpd_song_get_start(song));
335 snprintf(buffer, sizeof(buffer), "%s [%s-]\n",
340 AppendLine(get_tag_label(LABEL_LENGTH), value,
341 max_tag_label_width);
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);
352 AppendLine(get_tag_label(LABEL_PATH), mpd_song_get_uri(song),
353 max_tag_label_width);
357 SongPage::AppendStatsLine(enum stats_label label, const char *value)
359 AppendLine(gettext(stats_labels[label]), value, max_stats_label_width);
363 SongPage::AddStats(struct mpd_connection *connection)
365 struct mpd_stats *mpd_stats = mpd_run_stats(connection);
366 if (mpd_stats == nullptr)
369 lines.emplace_back(_("MPD statistics"));
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);
382 format_duration_long(buf, sizeof(buf),
383 mpd_stats_get_db_play_time(mpd_stats));
384 AppendStatsLine(STATS_DBPLAYTIME, buf);
386 format_duration_long(buf, sizeof(buf),
387 mpd_stats_get_play_time(mpd_stats));
388 AppendStatsLine(STATS_PLAYTIME, buf);
390 format_duration_long(buf, sizeof(buf),
391 mpd_stats_get_uptime(mpd_stats));
392 AppendStatsLine(STATS_UPTIME, buf);
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);
398 mpd_stats_free(mpd_stats);
403 audio_format_to_string(char *buffer, size_t size,
404 const struct mpd_audio_format *format)
406 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
407 if (format->bits == MPD_SAMPLE_FORMAT_FLOAT) {
408 snprintf(buffer, size, "%u:f:%u",
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
419 snprintf(buffer, size, "dsd%u:%u",
420 format->sample_rate * 8 / 44100,
425 snprintf(buffer, size, "%u:dsd:%u",
432 snprintf(buffer, size, "%u:%u:%u",
433 format->sample_rate, format->bits,
438 SongPage::Update(struct mpdclient &c, unsigned)
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;
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());
459 if (c.song != nullptr && c.playing_or_paused) {
460 if (played_song != nullptr)
461 mpd_song_free(played_song);
463 played_song = mpd_song_dup(c.song);
464 lines.emplace_back(_("Currently playing song"));
465 AddSong(played_song);
467 if (mpd_status_get_kbit_rate(c.status) > 0) {
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);
475 const struct mpd_audio_format *format =
476 mpd_status_get_audio_format(c.status);
479 audio_format_to_string(buf, sizeof(buf), format);
480 AppendLine(get_tag_label(LABEL_FORMAT), buf,
481 max_tag_label_width);
484 lines.emplace_back(std::string());
487 /* Add some statistics about mpd */
488 auto *connection = c.GetConnection();
489 if (connection != nullptr && !AddStats(connection))
492 lw.SetLength(lines.size());
497 SongPage::OnCommand(struct mpdclient &c, Command cmd)
499 if (ListPage::OnCommand(c, cmd))
503 case Command::LOCATE:
504 if (selected_song != nullptr) {
505 screen_file_goto_song(screen, c, *selected_song);
508 if (played_song != nullptr) {
509 screen_file_goto_song(screen, c, *played_song);
515 #ifdef ENABLE_LYRICS_SCREEN
516 case Command::SCREEN_LYRICS:
517 if (selected_song != nullptr) {
518 screen_lyrics_switch(screen, c, *selected_song, false);
521 if (played_song != nullptr) {
522 screen_lyrics_switch(screen, c, *played_song, true);
529 case Command::SCREEN_SWAP:
530 if (selected_song != nullptr)
531 screen.Swap(c, selected_song);
533 // No need to check if this is null - we'd pass null anyway
534 screen.Swap(c, played_song);
541 if (screen_find(screen, &lw, cmd, *this)) {
543 lw.Center(lw.selected);
551 const PageMeta screen_song = {
554 Command::SCREEN_SONG,
559 screen_song_switch(ScreenManager &_screen, struct mpdclient &c,
560 const struct mpd_song &song)
562 next_song = mpd_song_dup(&song);
563 _screen.Switch(screen_song, c);