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