1 /* ncmpc (Ncurses MPD Client)
2 * (c) 2004-2020 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 "SearchPage.hxx"
21 #include "PageMeta.hxx"
22 #include "screen_status.hxx"
23 #include "TextListRenderer.hxx"
24 #include "i18n.h"
25 #include "Options.hxx"
26 #include "Bindings.hxx"
27 #include "GlobalBindings.hxx"
28 #include "charset.hxx"
29 #include "mpdclient.hxx"
30 #include "screen_utils.hxx"
31 #include "FileListPage.hxx"
32 #include "filelist.hxx"
33
34 #include <iterator>
35
36 #include <string.h>
37
38 enum {
39 SEARCH_URI = MPD_TAG_COUNT + 100,
40 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
41 SEARCH_MODIFIED,
42 #endif
43 SEARCH_ARTIST_TITLE,
44 };
45
46 static constexpr struct {
47 enum mpd_tag_type tag_type;
48 const char *name;
49 const char *localname;
50 } search_tag[MPD_TAG_COUNT] = {
51 { MPD_TAG_ARTIST, "artist", N_("artist") },
52 { MPD_TAG_ALBUM, "album", N_("album") },
53 { MPD_TAG_TITLE, "title", N_("title") },
54 { MPD_TAG_TRACK, "track", N_("track") },
55 { MPD_TAG_NAME, "name", N_("name") },
56 { MPD_TAG_GENRE, "genre", N_("genre") },
57 { MPD_TAG_DATE, "date", N_("date") },
58 { MPD_TAG_COMPOSER, "composer", N_("composer") },
59 { MPD_TAG_PERFORMER, "performer", N_("performer") },
60 { MPD_TAG_COMMENT, "comment", N_("comment") },
61 { MPD_TAG_COUNT, nullptr, nullptr }
62 };
63
64 static int
search_get_tag_id(const char * name)65 search_get_tag_id(const char *name)
66 {
67 if (strcasecmp(name, "file") == 0 ||
68 strcasecmp(name, _("file")) == 0)
69 return SEARCH_URI;
70
71 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
72 if (strcasecmp(name, "modified") == 0)
73 return SEARCH_MODIFIED;
74 #endif
75
76 for (unsigned i = 0; search_tag[i].name != nullptr; ++i)
77 if (strcasecmp(search_tag[i].name, name) == 0 ||
78 strcasecmp(search_tag[i].localname, name) == 0)
79 return search_tag[i].tag_type;
80
81 return -1;
82 }
83
84 struct SearchMode {
85 enum mpd_tag_type table;
86 const char *label;
87 };
88
89 static constexpr SearchMode mode[] = {
90 { MPD_TAG_TITLE, N_("Title") },
91 { MPD_TAG_ARTIST, N_("Artist") },
92 { MPD_TAG_ALBUM, N_("Album") },
93 { (enum mpd_tag_type)SEARCH_URI, N_("Filename") },
94 { (enum mpd_tag_type)SEARCH_ARTIST_TITLE, N_("Artist + Title") },
95 { MPD_TAG_COUNT, nullptr }
96 };
97
98 static const char *const help_text[] = {
99 "",
100 "",
101 "",
102 "Quick - Enter a string and ncmpc will search according",
103 " to the current search mode (displayed above).",
104 "",
105 "Advanced - <tag>:<search term> [<tag>:<search term>...]",
106 " Example: artist:radiohead album:pablo honey",
107 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
108 " Example: modified:14d (units: s, M, h, d, m, y)",
109 #else
110 " (\"modified:\" requires libmpdclient 2.10)",
111 #endif
112 "",
113 " Available tags: artist, album, title, track,",
114 " name, genre, date composer, performer, comment, file",
115 "",
116 };
117
118 static bool advanced_search_mode = false;
119
120 class SearchPage final : public FileListPage {
121 History search_history;
122 std::string pattern;
123
124 public:
SearchPage(ScreenManager & _screen,WINDOW * _w,Size size)125 SearchPage(ScreenManager &_screen, WINDOW *_w, Size size) noexcept
126 :FileListPage(_screen, _w, size,
127 !options.search_format.empty()
128 ? options.search_format.c_str()
129 : options.list_format.c_str()) {
130 lw.DisableCursor();
131 lw.SetLength(std::size(help_text));
132 }
133
134 private:
135 void Clear(bool clear_pattern) noexcept;
136 void Reload(struct mpdclient &c);
137 void Start(struct mpdclient &c);
138
139 public:
140 /* virtual methods from class Page */
141 void Paint() const noexcept override;
142 void Update(struct mpdclient &c, unsigned events) noexcept override;
143 bool OnCommand(struct mpdclient &c, Command cmd) override;
144 const char *GetTitle(char *s, size_t size) const noexcept override;
145 };
146
147 /* search info */
148 class SearchHelpText final : public ListText {
149 public:
150 /* virtual methods from class ListText */
GetListItemText(char * buffer,size_t size,unsigned idx) const151 const char *GetListItemText(char *buffer, size_t size,
152 unsigned idx) const noexcept override {
153 assert(idx < std::size(help_text));
154
155 if (idx == 0) {
156 snprintf(buffer, size,
157 " %s : %s",
158 GetGlobalKeyBindings().GetKeyNames(Command::SCREEN_SEARCH).c_str(),
159 "New search");
160 return buffer;
161 }
162
163 if (idx == 1) {
164 snprintf(buffer, size,
165 " %s : %s [%s]",
166 GetGlobalKeyBindings().GetKeyNames(Command::SEARCH_MODE).c_str(),
167 get_key_description(Command::SEARCH_MODE),
168 gettext(mode[options.search_mode].label));
169 return buffer;
170 }
171
172 return help_text[idx];
173 }
174 };
175
176 void
Clear(bool clear_pattern)177 SearchPage::Clear(bool clear_pattern) noexcept
178 {
179 if (filelist) {
180 filelist = std::make_unique<FileList>();
181 lw.SetLength(0);
182 }
183 if (clear_pattern)
184 pattern.clear();
185
186 SetDirty();
187 }
188
189 static std::unique_ptr<FileList>
search_simple_query(struct mpd_connection * connection,bool exact_match,int table,const char * local_pattern)190 search_simple_query(struct mpd_connection *connection, bool exact_match,
191 int table, const char *local_pattern)
192 {
193 const LocaleToUtf8 filter_utf8(local_pattern);
194
195 if (table == SEARCH_ARTIST_TITLE) {
196 mpd_command_list_begin(connection, false);
197
198 mpd_search_db_songs(connection, exact_match);
199 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
200 MPD_TAG_ARTIST,
201 filter_utf8.c_str());
202 mpd_search_commit(connection);
203
204 mpd_search_db_songs(connection, exact_match);
205 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
206 MPD_TAG_TITLE,
207 filter_utf8.c_str());
208 mpd_search_commit(connection);
209
210 mpd_command_list_end(connection);
211
212 auto list = filelist_new_recv(connection);
213 list->RemoveDuplicateSongs();
214 return list;
215 } else if (table == SEARCH_URI) {
216 mpd_search_db_songs(connection, exact_match);
217 mpd_search_add_uri_constraint(connection, MPD_OPERATOR_DEFAULT,
218 filter_utf8.c_str());
219 mpd_search_commit(connection);
220
221 return filelist_new_recv(connection);
222 } else {
223 mpd_search_db_songs(connection, exact_match);
224 mpd_search_add_tag_constraint(connection, MPD_OPERATOR_DEFAULT,
225 (enum mpd_tag_type)table,
226 filter_utf8.c_str());
227 mpd_search_commit(connection);
228
229 return filelist_new_recv(connection);
230 }
231 }
232
233 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
234
235 /**
236 * Throws on error.
237 */
238 static time_t
ParseModifiedSince(const char * s)239 ParseModifiedSince(const char *s)
240 {
241 char *endptr;
242 time_t value = strtoul(s, &endptr, 10);
243 if (endptr == s)
244 throw _("Invalid number");
245
246 constexpr time_t MINUTE = 60;
247 constexpr time_t HOUR = 60 * MINUTE;
248 constexpr time_t DAY = 24 * HOUR;
249 constexpr time_t MONTH = 30 * DAY; // TODO: inaccurate
250 constexpr time_t YEAR = 365 * DAY; // TODO: inaccurate
251
252 s = endptr;
253 switch (*s) {
254 case 's':
255 ++s;
256 break;
257
258 case 'M':
259 ++s;
260 value *= MINUTE;
261 break;
262
263 case 'h':
264 ++s;
265 value *= HOUR;
266 break;
267
268 case 'd':
269 ++s;
270 value *= DAY;
271 break;
272
273 case 'm':
274 ++s;
275 value *= MONTH;
276 break;
277
278 case 'y':
279 case 'Y':
280 ++s;
281 value *= YEAR;
282 break;
283
284 default:
285 throw _("Unrecognized suffix");
286 }
287
288 if (*s != '\0')
289 throw _("Unrecognized suffix");
290
291 return time(nullptr) - value;
292 }
293
294 #endif
295
296 /*-----------------------------------------------------------------------
297 * NOTE: This code exists to test a new search ui,
298 * Its ugly and MUST be redesigned before the next release!
299 *-----------------------------------------------------------------------
300 */
301 static std::unique_ptr<FileList>
search_advanced_query(struct mpd_connection * connection,const char * query)302 search_advanced_query(struct mpd_connection *connection, const char *query)
303 try {
304 advanced_search_mode = false;
305 if (strchr(query, ':') == nullptr)
306 return nullptr;
307
308 std::string str(query);
309
310 static constexpr size_t N = 10;
311
312 char *tabv[N];
313 char *matchv[N];
314 int table[N];
315
316 /*
317 * Replace every : with a '\0' and every space character
318 * before it unless spi = -1, link the resulting strings
319 * to their proper vector.
320 */
321 int spi = -1;
322 size_t n = 0;
323 for (size_t i = 0; str[i] != '\0' && n < N; i++) {
324 switch(str[i]) {
325 case ' ':
326 spi = i;
327 continue;
328 case ':':
329 str[i] = '\0';
330 if (spi != -1)
331 str[spi] = '\0';
332
333 matchv[n] = &str[i + 1];
334 tabv[n] = &str[spi + 1];
335 table[n] = search_get_tag_id(tabv[n]);
336 if (table[n] < 0) {
337 screen_status_printf(_("Bad search tag %s"),
338 tabv[n]);
339 return nullptr;
340 }
341
342 ++n;
343 /* FALLTHROUGH */
344 default:
345 continue;
346 }
347 }
348
349 /* Get rid of obvious failure case */
350 if (matchv[n - 1][0] == '\0') {
351 screen_status_printf(_("No argument for search tag %s"), tabv[n - 1]);
352 return nullptr;
353 }
354
355 advanced_search_mode = true;
356
357 /*-----------------------------------------------------------------------
358 * NOTE (again): This code exists to test a new search ui,
359 * Its ugly and MUST be redesigned before the next release!
360 * + the code below should live in mpdclient.c
361 *-----------------------------------------------------------------------
362 */
363 /** stupid - but this is just a test...... (fulhack) */
364 mpd_search_db_songs(connection, false);
365
366 for (size_t i = 0; i < n; i++) {
367 const LocaleToUtf8 value(matchv[i]);
368
369 if (table[i] == SEARCH_URI)
370 mpd_search_add_uri_constraint(connection,
371 MPD_OPERATOR_DEFAULT,
372 value.c_str());
373 #if LIBMPDCLIENT_CHECK_VERSION(2,10,0)
374 else if (table[i] == SEARCH_MODIFIED)
375 mpd_search_add_modified_since_constraint(connection,
376 MPD_OPERATOR_DEFAULT,
377 ParseModifiedSince(value.c_str()));
378 #endif
379 else
380 mpd_search_add_tag_constraint(connection,
381 MPD_OPERATOR_DEFAULT,
382 (enum mpd_tag_type)table[i],
383 value.c_str());
384 }
385
386 mpd_search_commit(connection);
387 auto fl = filelist_new_recv(connection);
388 if (!mpd_response_finish(connection))
389 fl.reset();
390
391 return fl;
392 } catch (...) {
393 mpd_search_cancel(connection);
394 throw;
395 }
396
397 static std::unique_ptr<FileList>
do_search(struct mpdclient * c,const char * query)398 do_search(struct mpdclient *c, const char *query)
399 {
400 auto *connection = c->GetConnection();
401 if (connection == nullptr)
402 return nullptr;
403
404 auto fl = search_advanced_query(connection, query);
405 if (fl != nullptr)
406 return fl;
407
408 if (mpd_connection_get_error(connection) != MPD_ERROR_SUCCESS) {
409 c->HandleError();
410 return nullptr;
411 }
412
413 fl = search_simple_query(connection, false,
414 mode[options.search_mode].table,
415 query);
416 if (fl == nullptr)
417 c->HandleError();
418 return fl;
419 }
420
421 void
Reload(struct mpdclient & c)422 SearchPage::Reload(struct mpdclient &c)
423 {
424 if (pattern.empty())
425 return;
426
427 lw.EnableCursor();
428 filelist = do_search(&c, pattern.c_str());
429 if (filelist == nullptr)
430 filelist = std::make_unique<FileList>();
431 lw.SetLength(filelist->size());
432
433 screen_browser_sync_highlights(*filelist, c.playlist);
434
435 SetDirty();
436 }
437
438 void
Start(struct mpdclient & c)439 SearchPage::Start(struct mpdclient &c)
440 {
441 if (!c.IsConnected())
442 return;
443
444 Clear(true);
445
446 pattern = screen_readln(_("Search"),
447 nullptr,
448 &search_history,
449 nullptr);
450
451 if (pattern.empty()) {
452 lw.Reset();
453 return;
454 }
455
456 Reload(c);
457 }
458
459 static std::unique_ptr<Page>
screen_search_init(ScreenManager & _screen,WINDOW * w,Size size)460 screen_search_init(ScreenManager &_screen, WINDOW *w, Size size)
461 {
462 return std::make_unique<SearchPage>(_screen, w, size);
463 }
464
465 void
Paint() const466 SearchPage::Paint() const noexcept
467 {
468 if (filelist) {
469 FileListPage::Paint();
470 } else {
471 lw.Paint(TextListRenderer(SearchHelpText()));
472 }
473 }
474
475 const char *
GetTitle(char * str,size_t size) const476 SearchPage::GetTitle(char *str, size_t size) const noexcept
477 {
478 if (advanced_search_mode && !pattern.empty())
479 snprintf(str, size, "%s '%s'", _("Search"), pattern.c_str());
480 else if (!pattern.empty())
481 snprintf(str, size,
482 "%s '%s' [%s]",
483 _("Search"),
484 pattern.c_str(),
485 gettext(mode[options.search_mode].label));
486 else
487 return _("Search");
488
489 return str;
490 }
491
492 void
Update(struct mpdclient & c,unsigned events)493 SearchPage::Update(struct mpdclient &c, unsigned events) noexcept
494 {
495 if (filelist != nullptr && events & MPD_IDLE_QUEUE) {
496 screen_browser_sync_highlights(*filelist, c.playlist);
497 SetDirty();
498 }
499 }
500
501 bool
OnCommand(struct mpdclient & c,Command cmd)502 SearchPage::OnCommand(struct mpdclient &c, Command cmd)
503 {
504 switch (cmd) {
505 case Command::SEARCH_MODE:
506 options.search_mode++;
507 if (mode[options.search_mode].label == nullptr)
508 options.search_mode = 0;
509 screen_status_printf(_("Search mode: %s"),
510 gettext(mode[options.search_mode].label));
511
512 if (pattern.empty())
513 /* show the new mode in the help text */
514 SetDirty();
515 else if (!advanced_search_mode)
516 /* reload only if the new search mode is going
517 to be considered */
518 Reload(c);
519 return true;
520
521 case Command::SCREEN_UPDATE:
522 Reload(c);
523 return true;
524
525 case Command::SCREEN_SEARCH:
526 Start(c);
527 return true;
528
529 case Command::CLEAR:
530 Clear(true);
531 lw.Reset();
532 return true;
533
534 default:
535 break;
536 }
537
538 if (FileListPage::OnCommand(c, cmd))
539 return true;
540
541 return false;
542 }
543
544 const PageMeta screen_search = {
545 "search",
546 N_("Search"),
547 Command::SCREEN_SEARCH,
548 screen_search_init,
549 };
550