fcd13e495cb3e7a15f7c10e30e55f14db42333bf
[ncmpc-debian.git] / src / LyricsPage.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 "LyricsPage.hxx"
21 #include "PageMeta.hxx"
22 #include "screen_status.hxx"
23 #include "FileBrowserPage.hxx"
24 #include "SongPage.hxx"
25 #include "i18n.h"
26 #include "Options.hxx"
27 #include "mpdclient.hxx"
28 #include "screen.hxx"
29 #include "lyrics.hxx"
30 #include "TextPage.hxx"
31 #include "screen_utils.hxx"
32 #include "ncu.hxx"
33
34 #include <boost/asio/steady_timer.hpp>
35
36 #include <string>
37
38 #include <assert.h>
39 #include <errno.h>
40 #include <sys/stat.h>
41 #include <sys/wait.h>
42 #include <stdlib.h>
43 #include <string.h>
44 #include <unistd.h>
45 #include <stdio.h>
46
47 static struct mpd_song *next_song;
48 static bool follow = false;
49
50 class LyricsPage final : public TextPage {
51         /** Set if the cursor position shall be kept during the next lyrics update. */
52         bool reloading = false;
53
54         struct mpd_song *song = nullptr;
55
56         /**
57          * These are pointers into the #mpd_song above, and will
58          * become invalid as soon as the mpd_song_free() is called.
59          */
60         const char *artist = nullptr, *title = nullptr;
61
62         std::string plugin_name;
63
64         PluginCycle *loader = nullptr;
65
66         boost::asio::steady_timer loader_timeout;
67
68 public:
69         LyricsPage(ScreenManager &_screen, WINDOW *w, Size size)
70                 :TextPage(_screen, w, size),
71                  loader_timeout(_screen.get_io_service()) {}
72
73         ~LyricsPage() override {
74                 Cancel();
75         }
76
77         auto &get_io_service() noexcept {
78                 return loader_timeout.get_io_service();
79         }
80
81 private:
82         void Cancel();
83
84         /**
85          * Repaint and update the screen, if it is currently active.
86          */
87         void RepaintIfActive() {
88                 if (screen.IsVisible(*this))
89                         Repaint();
90
91                 /* XXX repaint the screen title */
92         }
93
94         void Set(const char *s);
95
96         void Load(const struct mpd_song &song) noexcept;
97         void MaybeLoad(const struct mpd_song &new_song) noexcept;
98
99         void MaybeLoad(const struct mpd_song *new_song) noexcept {
100                 if (new_song != nullptr)
101                         MaybeLoad(*new_song);
102         }
103
104         void Reload();
105
106         bool Save();
107         bool Delete();
108
109         /** save current lyrics to a file and run editor on it */
110         void Edit();
111
112         static void PluginCallback(std::string &&result, bool success,
113                                    const char *plugin_name, void *data) {
114                 auto &p = *(LyricsPage *)data;
115                 p.PluginCallback(std::move(result), success, plugin_name);
116         }
117
118         void PluginCallback(std::string &&result, bool success,
119                             const char *plugin_name);
120
121         void OnTimeout(const boost::system::error_code &error) noexcept;
122
123 public:
124         /* virtual methods from class Page */
125         void OnOpen(struct mpdclient &c) noexcept override;
126         void Update(struct mpdclient &c, unsigned events) noexcept override;
127         bool OnCommand(struct mpdclient &c, Command cmd) override;
128         const char *GetTitle(char *, size_t) const noexcept override;
129 };
130
131 void
132 LyricsPage::Cancel()
133 {
134         if (loader != nullptr) {
135                 plugin_stop(loader);
136                 loader = nullptr;
137         }
138
139         loader_timeout.cancel();
140
141         plugin_name.clear();
142
143         if (song != nullptr) {
144                 mpd_song_free(song);
145                 song = nullptr;
146                 artist = nullptr;
147                 title = nullptr;
148         }
149 }
150
151 static void
152 path_lyr_file(char *path, size_t size,
153                 const char *artist, const char *title)
154 {
155         snprintf(path, size, "%s/.lyrics/%s - %s.txt",
156                         getenv("HOME"), artist, title);
157 }
158
159 static bool
160 exists_lyr_file(const char *artist, const char *title)
161 {
162         char path[1024];
163         path_lyr_file(path, 1024, artist, title);
164
165         struct stat result;
166         return (stat(path, &result) == 0);
167 }
168
169 static FILE *
170 create_lyr_file(const char *artist, const char *title)
171 {
172         char path[1024];
173         snprintf(path, 1024, "%s/.lyrics",
174                  getenv("HOME"));
175         mkdir(path, S_IRWXU);
176
177         path_lyr_file(path, 1024, artist, title);
178
179         return fopen(path, "w");
180 }
181
182 bool
183 LyricsPage::Save()
184 {
185         FILE *lyr_file = create_lyr_file(artist, title);
186         if (lyr_file == nullptr)
187                 return false;
188
189         for (const auto &i : lines)
190                 fprintf(lyr_file, "%s\n", i.c_str());
191
192         fclose(lyr_file);
193         return true;
194 }
195
196 bool
197 LyricsPage::Delete()
198 {
199         if (!exists_lyr_file(artist, title))
200                 return false;
201
202         char path[1024];
203         path_lyr_file(path, 1024, artist, title);
204         return unlink(path) == 0;
205 }
206
207 void
208 LyricsPage::Set(const char *s)
209 {
210         if (reloading) {
211                 unsigned saved_start = lw.start;
212
213                 TextPage::Set(s);
214
215                 /* restore the cursor and ensure that it's still valid */
216                 lw.start = saved_start;
217                 lw.FetchCursor();
218         } else {
219                 TextPage::Set(s);
220         }
221
222         reloading = false;
223
224         /* paint new data */
225
226         RepaintIfActive();
227 }
228
229 inline void
230 LyricsPage::PluginCallback(std::string &&result, const bool success,
231                            const char *_plugin_name)
232 {
233         assert(loader != nullptr);
234
235         if (_plugin_name != nullptr)
236                 plugin_name = _plugin_name;
237         else
238                 plugin_name.clear();
239
240         /* Display result, which may be lyrics or error messages */
241         Set(result.c_str());
242
243         if (success == true) {
244                 if (options.lyrics_autosave &&
245                     !exists_lyr_file(artist, title))
246                         Save();
247         } else {
248                 /* translators: no lyrics were found for the song */
249                 screen_status_message (_("No lyrics"));
250         }
251
252         loader_timeout.cancel();
253
254         plugin_stop(loader);
255         loader = nullptr;
256 }
257
258 void
259 LyricsPage::OnTimeout(const boost::system::error_code &error) noexcept
260 {
261         if (error)
262                 return;
263
264         plugin_stop(loader);
265         loader = nullptr;
266
267         screen_status_printf(_("Lyrics timeout occurred after %d seconds"),
268                              (int)std::chrono::duration_cast<std::chrono::seconds>(options.lyrics_timeout).count());
269 }
270
271 void
272 LyricsPage::Load(const struct mpd_song &_song) noexcept
273 {
274         Cancel();
275         Clear();
276
277         song = mpd_song_dup(&_song);
278         artist = mpd_song_get_tag(song, MPD_TAG_ARTIST, 0);
279         title = mpd_song_get_tag(song, MPD_TAG_TITLE, 0);
280
281         loader = lyrics_load(get_io_service(),
282                              artist, title, PluginCallback, this);
283
284         if (options.lyrics_timeout > std::chrono::steady_clock::duration::zero()) {
285                 boost::system::error_code error;
286                 loader_timeout.expires_from_now(options.lyrics_timeout,
287                                                 error);
288                 loader_timeout.async_wait(std::bind(&LyricsPage::OnTimeout, this,
289                                                      std::placeholders::_1));
290         }
291 }
292
293 void
294 LyricsPage::MaybeLoad(const struct mpd_song &new_song) noexcept
295 {
296         if (song == nullptr ||
297             strcmp(mpd_song_get_uri(&new_song),
298                    mpd_song_get_uri(song)) != 0)
299                 Load(new_song);
300 }
301
302 void
303 LyricsPage::Reload()
304 {
305         if (loader == nullptr && artist != nullptr && title != nullptr) {
306                 reloading = true;
307                 loader = lyrics_load(get_io_service(),
308                                      artist, title, PluginCallback, nullptr);
309                 Repaint();
310         }
311 }
312
313 static std::unique_ptr<Page>
314 lyrics_screen_init(ScreenManager &_screen, WINDOW *w, Size size)
315 {
316         return std::make_unique<LyricsPage>(_screen, w, size);
317 }
318
319 void
320 LyricsPage::OnOpen(struct mpdclient &c) noexcept
321 {
322         const struct mpd_song *next_song_c =
323                 next_song != nullptr ? next_song : c.GetPlayingSong();
324
325         MaybeLoad(next_song_c);
326
327         if (next_song != nullptr) {
328                 mpd_song_free(next_song);
329                 next_song = nullptr;
330         }
331 }
332
333 void
334 LyricsPage::Update(struct mpdclient &c, unsigned) noexcept
335 {
336         if (follow)
337                 MaybeLoad(c.GetPlayingSong());
338 }
339
340 const char *
341 LyricsPage::GetTitle(char *str, size_t size) const noexcept
342 {
343         if (loader != nullptr) {
344                 snprintf(str, size, "%s (%s)",
345                          _("Lyrics"),
346                          /* translators: this message is displayed
347                             while data is retrieved */
348                          _("loading..."));
349                 return str;
350         } else if (artist != nullptr && title != nullptr && !IsEmpty()) {
351                 int n;
352                 n = snprintf(str, size, "%s: %s - %s",
353                              _("Lyrics"),
354                              artist, title);
355
356                 if (options.lyrics_show_plugin && !plugin_name.empty() &&
357                     (unsigned int) n < size - 1)
358                         snprintf(str + n, size - n, " (%s)",
359                                  plugin_name.c_str());
360
361                 return str;
362         } else
363                 return _("Lyrics");
364 }
365
366 void
367 LyricsPage::Edit()
368 {
369         if (options.text_editor.empty()) {
370                 screen_status_message(_("Editor not configured"));
371                 return;
372         }
373
374         const char *editor = options.text_editor.c_str();
375         if (options.text_editor_ask) {
376                 const char *prompt =
377                         _("Do you really want to start an editor and edit these lyrics?");
378                 bool really = screen_get_yesno(prompt, false);
379                 if (!really) {
380                         screen_status_message(_("Aborted"));
381                         return;
382                 }
383         }
384
385         if (!Save())
386                 return;
387
388         ncu_deinit();
389
390         /* TODO: fork/exec/wait won't work on Windows, but building a command
391            string for system() is too tricky */
392         int status;
393         pid_t pid = fork();
394         if (pid == -1) {
395                 screen_status_printf(("%s (%s)"), _("Can't start editor"), strerror(errno));
396                 ncu_init();
397                 return;
398         } else if (pid == 0) {
399                 char path[1024];
400                 path_lyr_file(path, sizeof(path), artist, title);
401                 execlp(editor, editor, path, nullptr);
402                 /* exec failed, do what system does */
403                 _exit(127);
404         } else {
405                 int ret;
406                 do {
407                         ret = waitpid(pid, &status, 0);
408                 } while (ret == -1 && errno == EINTR);
409         }
410
411         ncu_init();
412
413         /* TODO: hardly portable */
414         if (WIFEXITED(status)) {
415                 if (WEXITSTATUS(status) == 0)
416                         /* update to get the changes */
417                         Reload();
418                 else if (WEXITSTATUS(status) == 127)
419                         screen_status_message(_("Can't start editor"));
420                 else
421                         screen_status_printf("%s (%d)",
422                                              _("Editor exited unexpectedly"),
423                                              WEXITSTATUS(status));
424         } else if (WIFSIGNALED(status)) {
425                 screen_status_printf("%s (signal %d)",
426                                      _("Editor exited unexpectedly"),
427                                      WTERMSIG(status));
428         }
429 }
430
431 bool
432 LyricsPage::OnCommand(struct mpdclient &c, Command cmd)
433 {
434         if (TextPage::OnCommand(c, cmd))
435                 return true;
436
437         switch(cmd) {
438         case Command::INTERRUPT:
439                 if (loader != nullptr) {
440                         Cancel();
441                         Clear();
442                 }
443                 return true;
444         case Command::SAVE_PLAYLIST:
445                 if (loader == nullptr && artist != nullptr &&
446                     title != nullptr && Save())
447                         /* lyrics for the song were saved on hard disk */
448                         screen_status_message (_("Lyrics saved"));
449                 return true;
450         case Command::DELETE:
451                 if (loader == nullptr && artist != nullptr &&
452                     title != nullptr) {
453                         screen_status_message(Delete()
454                                               ? _("Lyrics deleted")
455                                               : _("No saved lyrics"));
456                 }
457                 return true;
458         case Command::LYRICS_UPDATE:
459                 if (c.GetPlayingSong() != nullptr)
460                         Load(*c.GetPlayingSong());
461                 return true;
462         case Command::EDIT:
463                 Edit();
464                 return true;
465         case Command::SELECT:
466                 Reload();
467                 return true;
468
469 #ifdef ENABLE_SONG_SCREEN
470         case Command::SCREEN_SONG:
471                 if (song != nullptr) {
472                         screen_song_switch(screen, c, *song);
473                         return true;
474                 }
475
476                 break;
477 #endif
478         case Command::SCREEN_SWAP:
479                 screen.Swap(c, song);
480                 return true;
481
482         case Command::LOCATE:
483                 if (song != nullptr) {
484                         screen_file_goto_song(screen, c, *song);
485                         return true;
486                 }
487
488                 return false;
489
490         default:
491                 break;
492         }
493
494         return false;
495 }
496
497 const PageMeta screen_lyrics = {
498         "lyrics",
499         N_("Lyrics"),
500         Command::SCREEN_LYRICS,
501         lyrics_screen_init,
502 };
503
504 void
505 screen_lyrics_switch(ScreenManager &_screen, struct mpdclient &c,
506                      const struct mpd_song &song, bool f)
507 {
508         follow = f;
509         next_song = mpd_song_dup(&song);
510         _screen.Switch(screen_lyrics, c);
511 }