1 /***************************************************************************
2  *   Copyright (C) 2008-2021 by Andrzej Rybczak                            *
3  *   andrzej@rybczak.net                                                   *
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     *
16  *   along with this program; if not, write to the                         *
17  *   Free Software Foundation, Inc.,                                       *
18  *   51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.              *
19  ***************************************************************************/
20 
21 #include "screens/tag_editor.h"
22 
23 #ifdef HAVE_TAGLIB_H
24 
25 #include <boost/locale/conversion.hpp>
26 #include <algorithm>
27 #include <fstream>
28 
29 #include "actions.h"
30 #include "screens/browser.h"
31 #include "charset.h"
32 #include "display.h"
33 #include "global.h"
34 #include "helpers.h"
35 #include "format_impl.h"
36 #include "curses/menu_impl.h"
37 #include "screens/playlist.h"
38 #include "screens/song_info.h"
39 #include "statusbar.h"
40 #include "helpers/song_iterator_maker.h"
41 #include "utility/functional.h"
42 #include "utility/comparators.h"
43 #include "title.h"
44 #include "tags.h"
45 #include "screens/screen_switcher.h"
46 
47 using Global::myScreen;
48 using Global::MainHeight;
49 using Global::MainStartY;
50 
51 namespace ph = std::placeholders;
52 
53 TagEditor *myTagEditor;
54 
55 namespace {
56 
57 size_t LeftColumnWidth;
58 size_t LeftColumnStartX;
59 size_t MiddleColumnWidth;
60 size_t MiddleColumnStartX;
61 size_t RightColumnWidth;
62 size_t RightColumnStartX;
63 
64 size_t FParserDialogWidth;
65 size_t FParserDialogHeight;
66 size_t FParserWidth;
67 size_t FParserWidthOne;
68 size_t FParserWidthTwo;
69 size_t FParserHeight;
70 
71 std::list<std::string> Patterns;
72 std::string PatternsFile = "patterns.list";
73 
74 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m);
75 
76 std::string CapitalizeFirstLetters(const std::string &s);
77 void CapitalizeFirstLetters(MPD::MutableSong &s);
78 void LowerAllLetters(MPD::MutableSong &s);
79 
80 void GetPatternList();
81 void SavePatternList();
82 
83 MPD::MutableSong::SetFunction IntoSetFunction(char c);
84 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern);
85 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview);
86 
87 std::string SongToString(const MPD::MutableSong &s);
88 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter);
89 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s);
90 
91 }
92 
currentS()93 SongIterator TagsWindow::currentS()
94 {
95 	return makeSongIterator(current());
96 }
97 
currentS() const98 ConstSongIterator TagsWindow::currentS() const
99 {
100 	return makeConstSongIterator(current());
101 }
102 
beginS()103 SongIterator TagsWindow::beginS()
104 {
105 	return makeSongIterator(begin());
106 }
107 
beginS() const108 ConstSongIterator TagsWindow::beginS() const
109 {
110 	return makeConstSongIterator(begin());
111 }
112 
endS()113 SongIterator TagsWindow::endS()
114 {
115 	return makeSongIterator(end());
116 }
117 
endS() const118 ConstSongIterator TagsWindow::endS() const
119 {
120 	return makeConstSongIterator(end());
121 }
122 
getSelectedSongs()123 std::vector<MPD::Song> TagsWindow::getSelectedSongs()
124 {
125 	return {}; // TODO
126 }
127 
128 /**********************************************************************/
129 
TagEditor()130 TagEditor::TagEditor() : FParser(0), FParserHelper(0), FParserLegend(0), FParserPreview(0), itsBrowsedDir("/")
131 {
132 	PatternsFile = Config.ncmpcpp_directory + "patterns.list";
133 	SetDimensions(0, COLS);
134 
135 	Dirs = new NC::Menu< std::pair<std::string, std::string> >(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Directories" : "", Config.main_color, NC::Border());
136 	setHighlightFixes(*Dirs);
137 	Dirs->cyclicScrolling(Config.use_cyclic_scrolling);
138 	Dirs->centeredCursor(Config.centered_cursor);
139 	Dirs->setItemDisplayer([](NC::Menu<std::pair<std::string, std::string>> &menu) {
140 		menu << Charset::utf8ToLocale(menu.drawn()->value().first);
141 	});
142 
143 	TagTypes = new NC::Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, Config.titles_visibility ? "Tag types" : "", Config.main_color, NC::Border());
144 	setHighlightInactiveColumnFixes(*TagTypes);
145 	TagTypes->cyclicScrolling(Config.use_cyclic_scrolling);
146 	TagTypes->centeredCursor(Config.centered_cursor);
147 	TagTypes->setItemDisplayer([](NC::Menu<std::string> &menu) {
148 		menu << Charset::utf8ToLocale(menu.drawn()->value());
149 	});
150 
151 	for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
152 		TagTypes->addItem(m->Name);
153 	TagTypes->addSeparator();
154 	TagTypes->addItem("Filename");
155 	TagTypes->addSeparator();
156 	if (Config.titles_visibility)
157 	{
158 		TagTypes->addItem("Options", NC::List::Properties::Inactive);
159 		TagTypes->addSeparator();
160 	}
161 	TagTypes->addItem("Capitalize First Letters");
162 	TagTypes->addItem("lower all letters");
163 	TagTypes->addSeparator();
164 	TagTypes->addItem("Reset");
165 	TagTypes->addItem("Save");
166 
167 	Tags = new TagsWindow(NC::Menu<MPD::MutableSong>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Tags" : "", Config.main_color, NC::Border()));
168 	setHighlightInactiveColumnFixes(*Tags);
169 	Tags->cyclicScrolling(Config.use_cyclic_scrolling);
170 	Tags->centeredCursor(Config.centered_cursor);
171 	Tags->setSelectedPrefix(Config.selected_item_prefix);
172 	Tags->setSelectedSuffix(Config.selected_item_suffix);
173 	Tags->setItemDisplayer(Display::Tags);
174 
175 	auto parser_display = [](NC::Menu<std::string> &menu) {
176 		menu << Charset::utf8ToLocale(menu.drawn()->value());
177 	};
178 
179 	FParserDialog = new NC::Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
180 	FParserDialog->cyclicScrolling(Config.use_cyclic_scrolling);
181 	FParserDialog->centeredCursor(Config.centered_cursor);
182 	FParserDialog->setItemDisplayer(parser_display);
183 	FParserDialog->addItem("Get tags from filename");
184 	FParserDialog->addItem("Rename files");
185 	FParserDialog->addItem("Cancel");
186 
187 	FParser = new NC::Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
188 	FParser->cyclicScrolling(Config.use_cyclic_scrolling);
189 	FParser->centeredCursor(Config.centered_cursor);
190 	FParser->setItemDisplayer(parser_display);
191 
192 	FParserLegend = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
193 
194 	FParserPreview = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
195 
196 	w = Dirs;
197 }
198 
SetDimensions(size_t x_offset,size_t width)199 void TagEditor::SetDimensions(size_t x_offset, size_t width)
200 {
201 	MiddleColumnWidth = std::min(26, COLS-2);
202 	LeftColumnStartX = x_offset;
203 	LeftColumnWidth = (width-MiddleColumnWidth)/2;
204 	MiddleColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
205 	RightColumnWidth = width-LeftColumnWidth-MiddleColumnWidth-2;
206 	RightColumnStartX = MiddleColumnStartX+MiddleColumnWidth+1;
207 
208 	FParserDialogWidth = std::min(30, COLS);
209 	FParserDialogHeight = std::min(size_t(5), MainHeight);
210 	FParserWidth = width*0.9;
211 	FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
212 	FParserWidthOne = FParserWidth/2;
213 	FParserWidthTwo = FParserWidth-FParserWidthOne;
214 }
215 
resize()216 void TagEditor::resize()
217 {
218 	size_t x_offset, width;
219 	getWindowResizeParams(x_offset, width);
220 	SetDimensions(x_offset, width);
221 
222 	Dirs->resize(LeftColumnWidth, MainHeight);
223 	TagTypes->resize(MiddleColumnWidth, MainHeight);
224 	Tags->resize(RightColumnWidth, MainHeight);
225 	FParserDialog->resize(FParserDialogWidth, FParserDialogHeight);
226 	FParser->resize(FParserWidthOne, FParserHeight);
227 	FParserLegend->resize(FParserWidthTwo, FParserHeight);
228 	FParserPreview->resize(FParserWidthTwo, FParserHeight);
229 
230 	Dirs->moveTo(LeftColumnStartX, MainStartY);
231 	TagTypes->moveTo(MiddleColumnStartX, MainStartY);
232 	Tags->moveTo(RightColumnStartX, MainStartY);
233 
234 	FParserDialog->moveTo(x_offset+(width-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
235 	FParser->moveTo(x_offset+(width-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
236 	FParserLegend->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
237 	FParserPreview->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
238 
239 	hasToBeResized = 0;
240 }
241 
title()242 std::wstring TagEditor::title()
243 {
244 	return L"Tag editor";
245 }
246 
switchTo()247 void TagEditor::switchTo()
248 {
249 	SwitchTo::execute(this);
250 	drawHeader();
251 	refresh();
252 }
253 
refresh()254 void TagEditor::refresh()
255 {
256 	Dirs->display();
257 	drawSeparator(MiddleColumnStartX-1);
258 	TagTypes->display();
259 	drawSeparator(RightColumnStartX-1);
260 	Tags->display();
261 
262 	if (w == FParserDialog)
263 	{
264 		FParserDialog->display();
265 	}
266 	else if (w == FParser || w == FParserHelper)
267 	{
268 		FParser->display();
269 		FParserHelper->display();
270 	}
271 }
272 
update()273 void TagEditor::update()
274 {
275 	if (Dirs->empty())
276 	{
277 		Dirs->Window::clear();
278 		Tags->clear();
279 
280 		if (itsBrowsedDir != "/")
281 			Dirs->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir)));
282 		else
283 			Dirs->addItem(std::make_pair(".", "/"));
284 		MPD::DirectoryIterator directory = Mpd.GetDirectories(itsBrowsedDir), end;
285 		for (; directory != end; ++directory)
286 		{
287 			Dirs->addItem(std::make_pair(getBasename(directory->path()), directory->path()));
288 			if (directory->path() == itsHighlightedDir)
289 				Dirs->highlight(Dirs->size()-1);
290 		};
291 		std::sort(Dirs->beginV()+1, Dirs->endV(),
292 			LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
293 		Dirs->display();
294 	}
295 
296 	if (Tags->empty())
297 	{
298 		Tags->reset();
299 		MPD::SongIterator s = Mpd.GetSongs(Dirs->current()->value().second), end;
300 		for (; s != end; ++s)
301 			Tags->addItem(std::move(*s));
302 		std::sort(Tags->beginV(), Tags->endV(),
303 			LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
304 		Tags->refresh();
305 	}
306 
307 	if (w == TagTypes && TagTypes->choice() < 13)
308 	{
309 		Tags->refresh();
310 	}
311 	else if (TagTypes->choice() >= 13)
312 	{
313 		Tags->Window::clear();
314 		Tags->Window::refresh();
315 	}
316 }
317 
enterDirectory()318 bool TagEditor::enterDirectory()
319 {
320 	bool result = false;
321 	if (w == Dirs && !Dirs->empty())
322 	{
323 		MPD::DirectoryIterator directory = Mpd.GetDirectories(Dirs->current()->value().second), end;
324 		bool has_subdirs = directory != end;
325 		if (has_subdirs)
326 		{
327 			directory.finish();
328 			itsHighlightedDir = itsBrowsedDir;
329 			itsBrowsedDir = Dirs->current()->value().second;
330 			Dirs->clear();
331 			Dirs->reset();
332 			result = true;
333 		}
334 	}
335 	return result;
336 }
337 
mouseButtonPressed(MEVENT me)338 void TagEditor::mouseButtonPressed(MEVENT me)
339 {
340 	auto tryPreviousColumn = [this]() -> bool {
341 		bool result = true;
342 		if (w != Dirs)
343 		{
344 			if (previousColumnAvailable())
345 				previousColumn();
346 			else
347 				result = false;
348 		}
349 		return result;
350 	};
351 	auto tryNextColumn = [this]() -> bool {
352 		bool result = true;
353 		if (w != Tags)
354 		{
355 			if (nextColumnAvailable())
356 				nextColumn();
357 			else
358 				result = false;
359 		}
360 		return result;
361 	};
362 	if (w == FParserDialog)
363 	{
364 		if (FParserDialog->hasCoords(me.x, me.y))
365 		{
366 			if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
367 			{
368 				FParserDialog->Goto(me.y);
369 				if (me.bstate & BUTTON3_PRESSED)
370 					runAction();
371 			}
372 			else
373 				Screen<WindowType>::mouseButtonPressed(me);
374 		}
375 	}
376 	else if (w == FParser || w == FParserHelper)
377 	{
378 		if (FParser->hasCoords(me.x, me.y))
379 		{
380 			if (w != FParser)
381 			{
382 				if (previousColumnAvailable())
383 					previousColumn();
384 				else
385 					return;
386 			}
387 			if (size_t(me.y) < FParser->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
388 			{
389 				FParser->Goto(me.y);
390 				if (me.bstate & BUTTON3_PRESSED)
391 					runAction();
392 			}
393 			else
394 				Screen<WindowType>::mouseButtonPressed(me);
395 		}
396 		else if (FParserHelper->hasCoords(me.x, me.y))
397 		{
398 			if (w != FParserHelper)
399 			{
400 				if (nextColumnAvailable())
401 					nextColumn();
402 				else
403 					return;
404 			}
405 			scrollpadMouseButtonPressed(*FParserHelper, me);
406 		}
407 	}
408 	else if (!Dirs->empty() && Dirs->hasCoords(me.x, me.y))
409 	{
410 		if (!tryPreviousColumn() || !tryPreviousColumn())
411 			return;
412 		if (size_t(me.y) < Dirs->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
413 		{
414 			Dirs->Goto(me.y);
415 			if (me.bstate & BUTTON1_PRESSED)
416 				enterDirectory();
417 		}
418 		else
419 			Screen<WindowType>::mouseButtonPressed(me);
420 		Tags->clear();
421 	}
422 	else if (!TagTypes->empty() && TagTypes->hasCoords(me.x, me.y))
423 	{
424 		if (w != TagTypes)
425 		{
426 			bool success;
427 			if (w == Dirs)
428 				success = tryNextColumn();
429 			else
430 				success = tryPreviousColumn();
431 			if (!success)
432 				return;
433 		}
434 		if (size_t(me.y) < TagTypes->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
435 		{
436 			if (!TagTypes->Goto(me.y))
437 				return;
438 			TagTypes->refresh();
439 			Tags->refresh();
440 			if (me.bstate & BUTTON3_PRESSED)
441 				runAction();
442 		}
443 		else
444 			Screen<WindowType>::mouseButtonPressed(me);
445 	}
446 	else if (!Tags->empty() && Tags->hasCoords(me.x, me.y))
447 	{
448 		if (!tryNextColumn() || !tryNextColumn())
449 			return;
450 		if (size_t(me.y) < Tags->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
451 		{
452 			Tags->Goto(me.y);
453 			Tags->refresh();
454 			if (me.bstate & BUTTON3_PRESSED)
455 				runAction();
456 		}
457 		else
458 			Screen<WindowType>::mouseButtonPressed(me);
459 	}
460 }
461 
462 /***********************************************************************/
463 
allowsSearching()464 bool TagEditor::allowsSearching()
465 {
466 	return w == Dirs || w == Tags;
467 }
468 
searchConstraint()469 const std::string &TagEditor::searchConstraint()
470 {
471 	if (w == Dirs)
472 		return m_directories_search_predicate.constraint();
473 	else if (w == Tags)
474 		return m_songs_search_predicate.constraint();
475 	throw std::runtime_error("shouldn't happen due to condition in allowsSearching");
476 }
477 
setSearchConstraint(const std::string & constraint)478 void TagEditor::setSearchConstraint(const std::string &constraint)
479 {
480 	if (w == Dirs)
481 	{
482 		m_directories_search_predicate = Regex::Filter<std::pair<std::string, std::string>>(
483 			constraint,
484 			Config.regex_type,
485 			std::bind(DirEntryMatcher, ph::_1, ph::_2, false));
486 	}
487 	else if (w == Tags)
488 	{
489 		m_songs_search_predicate = Regex::Filter<MPD::MutableSong>(
490 			constraint,
491 			Config.regex_type,
492 			SongEntryMatcher);
493 	}
494 }
495 
clearSearchConstraint()496 void TagEditor::clearSearchConstraint()
497 {
498 	if (w == Dirs)
499 		m_directories_search_predicate.clear();
500 	else if (w == Tags)
501 		m_songs_search_predicate.clear();
502 }
503 
search(SearchDirection direction,bool wrap,bool skip_current)504 bool TagEditor::search(SearchDirection direction, bool wrap, bool skip_current)
505 {
506 	bool result = false;
507 	if (w == Dirs)
508 		result = ::search(*Dirs, m_directories_search_predicate, direction, wrap, skip_current);
509 	else if (w == Tags)
510 		result = ::search(*Tags, m_songs_search_predicate, direction, wrap, skip_current);
511 	return result;
512 }
513 
514 /***********************************************************************/
515 
actionRunnable()516 bool TagEditor::actionRunnable()
517 {
518 	// TODO: put something more refined here. It requires reworking
519 	// runAction though, i.e. splitting it into smaller parts.
520 	return (w == Tags && !Tags->empty())
521 		|| w != Tags;
522 }
523 
runAction()524 void TagEditor::runAction()
525 {
526 	using Global::wFooter;
527 
528 	if (w == FParserDialog)
529 	{
530 		size_t choice = FParserDialog->choice();
531 		if (choice == 2) // cancel
532 		{
533 			w = TagTypes;
534 			refresh();
535 			return;
536 		}
537 		GetPatternList();
538 
539 		// prepare additional windows
540 
541 		FParserLegend->clear();
542 		*FParserLegend << "%a - artist\n";
543 		*FParserLegend << "%A - album artist\n";
544 		*FParserLegend << "%t - title\n";
545 		*FParserLegend << "%b - album\n";
546 		*FParserLegend << "%y - date\n";
547 		*FParserLegend << "%n - track number\n";
548 		*FParserLegend << "%g - genre\n";
549 		*FParserLegend << "%c - composer\n";
550 		*FParserLegend << "%p - performer\n";
551 		*FParserLegend << "%d - disc\n";
552 		*FParserLegend << "%C - comment\n\n";
553 		*FParserLegend << NC::Format::Bold << "Files:\n" << NC::Format::NoBold;
554 		for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
555 			*FParserLegend << Config.color2
556 			               << " * "
557 			               << NC::FormattedColor::End<>(Config.color2)
558 			               << (*it)->getName()
559 			               << "\n";
560 		FParserLegend->flush();
561 
562 		if (!Patterns.empty())
563 			Config.pattern = Patterns.front();
564 		FParser->clear();
565 		FParser->reset();
566 		FParser->addItem("Pattern: " + Config.pattern);
567 		FParser->addItem("Preview");
568 		FParser->addItem("Legend");
569 		FParser->addSeparator();
570 		FParser->addItem("Proceed");
571 		FParser->addItem("Cancel");
572 		if (!Patterns.empty())
573 		{
574 			FParser->addSeparator();
575 			FParser->addItem("Recent patterns", NC::List::Properties::Inactive);
576 			FParser->addSeparator();
577 			for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
578 				FParser->addItem(*it);
579 		}
580 
581 		FParser->setTitle(choice == 0 ? "Get tags from filename" : "Rename files");
582 		w = FParser;
583 		FParserUsePreview = 1;
584 		FParserHelper = FParserLegend;
585 		FParserHelper->display();
586 	}
587 	else if (w == FParser)
588 	{
589 		bool quit = 0;
590 		size_t pos = FParser->choice();
591 
592 		if (pos == 4) // save
593 			FParserUsePreview = 0;
594 
595 		if (pos == 0) // change pattern
596 		{
597 			std::string new_pattern;
598 			{
599 				Statusbar::ScopedLock slock;
600 				Statusbar::put() << "Pattern: ";
601 				new_pattern = wFooter->prompt(Config.pattern);
602 			}
603 			Config.pattern = new_pattern;
604 			FParser->at(0).value() = "Pattern: ";
605 			FParser->at(0).value() += Config.pattern;
606 		}
607 		else if (pos == 1 || pos == 4) // preview or proceed
608 		{
609 			bool success = 1;
610 			Statusbar::print("Parsing...");
611 			FParserPreview->clear();
612 			for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
613 			{
614 				MPD::MutableSong &s = **it;
615 				if (FParserDialog->choice() == 0) // get tags from filename
616 				{
617 					if (FParserUsePreview)
618 					{
619 						*FParserPreview << NC::Format::Bold << s.getName() << ":\n" << NC::Format::NoBold;
620 						*FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << '\n';
621 					}
622 					else
623 						ParseFilename(s, Config.pattern, FParserUsePreview);
624 				}
625 				else // rename files
626 				{
627 					std::string file = s.getName();
628 					size_t last_dot = file.rfind(".");
629 					std::string extension = file.substr(last_dot);
630 					std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
631 					if (new_file.empty() && !FParserUsePreview)
632 					{
633 						Statusbar::printf("File \"%1%\" would have an empty name", s.getName());
634 						FParserUsePreview = 1;
635 						success = 0;
636 					}
637 					if (!FParserUsePreview)
638 						s.setNewName(new_file + extension);
639 					*FParserPreview << file
640 					                << Config.color2
641 					                << " -> "
642 					                << NC::FormattedColor::End<>(Config.color2);
643 					if (new_file.empty())
644 						*FParserPreview << Config.empty_tags_color
645 						                << Config.empty_tag
646 						                << NC::FormattedColor::End<>(Config.empty_tags_color);
647 					else
648 						*FParserPreview << new_file << extension;
649 					*FParserPreview << "\n\n";
650 					if (!success)
651 						break;
652 				}
653 			}
654 			if (FParserUsePreview)
655 			{
656 				FParserHelper = FParserPreview;
657 				FParserHelper->flush();
658 				FParserHelper->display();
659 			}
660 			else if (success)
661 			{
662 				Patterns.remove(Config.pattern);
663 				Patterns.insert(Patterns.begin(), Config.pattern);
664 				quit = 1;
665 			}
666 			if (pos != 4 || success)
667 				Statusbar::print("Operation finished");
668 		}
669 		else if (pos == 2) // show legend
670 		{
671 			FParserHelper = FParserLegend;
672 			FParserHelper->display();
673 		}
674 		else if (pos == 5) // cancel
675 		{
676 			quit = 1;
677 		}
678 		else // list of patterns
679 		{
680 			Config.pattern = FParser->current()->value();
681 			FParser->at(0).value() = "Pattern: " + Config.pattern;
682 		}
683 
684 		if (quit)
685 		{
686 			SavePatternList();
687 			w = TagTypes;
688 			refresh();
689 			return;
690 		}
691 	}
692 
693 	if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
694 		return;
695 
696 	EditedSongs.clear();
697 	// if there are selected songs, perform operations only on them
698 	if (hasSelected(Tags->begin(), Tags->end()))
699 	{
700 		for (auto it = Tags->begin(); it != Tags->end(); ++it)
701 			if (it->isSelected())
702 				EditedSongs.push_back(&it->value());
703 	}
704 	else
705 	{
706 		for (auto it = Tags->begin(); it != Tags->end(); ++it)
707 			EditedSongs.push_back(&it->value());
708 	}
709 
710 	size_t id = TagTypes->choice();
711 
712 	if (w == TagTypes && id == 5)
713 	{
714 		Actions::confirmAction("Number tracks?");
715 		auto it = EditedSongs.begin();
716 		for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
717 		{
718 			if (Config.tag_editor_extended_numeration)
719 				(*it)->setTrack(boost::lexical_cast<std::string>(i) + "/" + boost::lexical_cast<std::string>(EditedSongs.size()));
720 			else
721 				(*it)->setTrack(boost::lexical_cast<std::string>(i));
722 			// discard other track number tags
723 			(*it)->setTrack("", 1);
724 		}
725 		Statusbar::print("Tracks numbered");
726 		return;
727 	}
728 
729 	if (id < 11)
730 	{
731 		MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
732 		MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
733 		if (w == TagTypes)
734 		{
735 			Statusbar::ScopedLock slock;
736 			Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
737 			std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
738 			for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
739 				(*it)->setTags(set, new_tag);
740 		}
741 		else if (w == Tags)
742 		{
743 			Statusbar::ScopedLock slock;
744 			Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
745 			std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
746 			if (new_tag != Tags->current()->value().getTags(get))
747 				Tags->current()->value().setTags(set, new_tag);
748 			Tags->scroll(NC::Scroll::Down);
749 		}
750 	}
751 	else
752 	{
753 		if (id == 12) // filename related options
754 		{
755 			if (w == TagTypes)
756 			{
757 				FParserDialog->reset();
758 				w = FParserDialog;
759 			}
760 			else if (w == Tags)
761 			{
762 				Statusbar::ScopedLock slock;
763 				MPD::MutableSong &s = Tags->current()->value();
764 				std::string old_name = s.getNewName().empty() ? s.getName() : s.getNewName();
765 				size_t last_dot = old_name.rfind(".");
766 				std::string extension = old_name.substr(last_dot);
767 				old_name = old_name.substr(0, last_dot);
768 				Statusbar::put() << NC::Format::Bold << "New filename: " << NC::Format::NoBold;
769 				std::string new_name = wFooter->prompt(old_name);
770 				if (!new_name.empty())
771 					s.setNewName(new_name + extension);
772 				Tags->scroll(NC::Scroll::Down);
773 			}
774 		}
775 		else if (id == TagTypes->size()-5) // capitalize first letters
776 		{
777 			Statusbar::print("Processing...");
778 			for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
779 				CapitalizeFirstLetters(**it);
780 			Statusbar::print("Done");
781 		}
782 		else if (id == TagTypes->size()-4) // lower all letters
783 		{
784 			Statusbar::print("Processing...");
785 			for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
786 				LowerAllLetters(**it);
787 			Statusbar::print("Done");
788 		}
789 		else if (id == TagTypes->size()-2) // reset
790 		{
791 			for (auto it = Tags->beginV(); it != Tags->endV(); ++it)
792 				it->clearModifications();
793 			Statusbar::print("Changes reset");
794 		}
795 		else if (id == TagTypes->size()-1) // save
796 		{
797 			bool success = 1;
798 			Statusbar::print("Writing changes...");
799 			for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
800 			{
801 				Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
802 				if (!Tags::write(**it))
803 				{
804 					Statusbar::printf("Error while writing tags to \"%1%\": %2%",
805 					                  (*it)->getName(), strerror(errno));
806 					success = 0;
807 					break;
808 				}
809 			}
810 			if (success)
811 			{
812 				Statusbar::print("Tags updated");
813 				setHighlightInactiveColumnFixes(*TagTypes);
814 				TagTypes->reset();
815 				w->refresh();
816 				w = Dirs;
817 				setHighlightFixes(*Dirs);
818 				Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
819 			}
820 			else
821 				Tags->clear();
822 		}
823 	}
824 }
825 
826 
827 /***********************************************************************/
828 
itemAvailable()829 bool TagEditor::itemAvailable()
830 {
831 	if (w == Tags)
832 		return !Tags->empty();
833 	return false;
834 }
835 
addItemToPlaylist(bool play)836 bool TagEditor::addItemToPlaylist(bool play)
837 {
838 	return addSongToPlaylist(*Tags->currentV(), play);
839 }
840 
getSelectedSongs()841 std::vector<MPD::Song> TagEditor::getSelectedSongs()
842 {
843 	std::vector<MPD::Song> result;
844 	if (w == Tags)
845 	{
846 		for (auto it = Tags->begin(); it != Tags->end(); ++it)
847 			if (it->isSelected())
848 				result.push_back(it->value());
849 		// if no song was selected, add current one
850 		if (result.empty() && !Tags->empty())
851 			result.push_back(Tags->current()->value());
852 	}
853 	return result;
854 }
855 
856 /***********************************************************************/
857 
previousColumnAvailable()858 bool TagEditor::previousColumnAvailable()
859 {
860 	bool result = false;
861 	if (w == Tags)
862 	{
863 		if (!TagTypes->empty() && !Dirs->empty())
864 			result = true;
865 	}
866 	else if (w == TagTypes)
867 	{
868 		if (!Dirs->empty() && isAnyModified(*Tags))
869 			Actions::confirmAction("There are pending changes, are you sure?");
870 		result = true;
871 	}
872 	else if (w == FParserHelper)
873 		result = true;
874 	return result;
875 }
876 
previousColumn()877 void TagEditor::previousColumn()
878 {
879 	if (w == Tags)
880 	{
881 		setHighlightInactiveColumnFixes(*Tags);
882 		w->refresh();
883 		w = TagTypes;
884 		setHighlightFixes(*TagTypes);
885 	}
886 	else if (w == TagTypes)
887 	{
888 		setHighlightInactiveColumnFixes(*TagTypes);
889 		w->refresh();
890 		w = Dirs;
891 		setHighlightFixes(*Dirs);
892 	}
893 	else if (w == FParserHelper)
894 	{
895 		FParserHelper->setBorder(Config.window_border);
896 		FParserHelper->display();
897 		w = FParser;
898 		FParser->setBorder(Config.active_window_border);
899 		FParser->display();
900 	}
901 }
902 
nextColumnAvailable()903 bool TagEditor::nextColumnAvailable()
904 {
905 	bool result = false;
906 	if (w == Dirs)
907 	{
908 		if (!TagTypes->empty() && !Tags->empty())
909 			result = true;
910 	}
911 	else if (w == TagTypes)
912 	{
913 		if (!Tags->empty())
914 			result = true;
915 	}
916 	else if (w == FParser)
917 		result = true;
918 	return result;
919 }
920 
nextColumn()921 void TagEditor::nextColumn()
922 {
923 	if (w == Dirs)
924 	{
925 		setHighlightInactiveColumnFixes(*Dirs);
926 		w->refresh();
927 		w = TagTypes;
928 		setHighlightFixes(*TagTypes);
929 	}
930 	else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->empty())
931 	{
932 		setHighlightInactiveColumnFixes(*TagTypes);
933 		w->refresh();
934 		w = Tags;
935 		setHighlightFixes(*Tags);
936 	}
937 	else if (w == FParser)
938 	{
939 		FParser->setBorder(Config.window_border);
940 		FParser->display();
941 		w = FParserHelper;
942 		FParserHelper->setBorder(Config.active_window_border);
943 		FParserHelper->display();
944 	}
945 }
946 
947 /***********************************************************************/
948 
LocateSong(const MPD::Song & s)949 void TagEditor::LocateSong(const MPD::Song &s)
950 {
951 	if (myScreen == this)
952 		return;
953 
954 	if (s.getDirectory().empty())
955 		return;
956 
957 	if (Global::myScreen != this)
958 		switchTo();
959 
960 	// go to right directory
961 	if (itsBrowsedDir != s.getDirectory())
962 	{
963 		itsBrowsedDir = s.getDirectory();
964 		size_t last_slash = itsBrowsedDir.rfind('/');
965 		if (last_slash != std::string::npos)
966 			itsBrowsedDir = itsBrowsedDir.substr(0, last_slash);
967 		else
968 			itsBrowsedDir = "/";
969 		if (itsBrowsedDir.empty())
970 			itsBrowsedDir = "/";
971 		Dirs->clear();
972 		update();
973 	}
974 	if (itsBrowsedDir == "/")
975 		Dirs->reset(); // go to the first pos, which is "." (music dir root)
976 
977 	// highlight directory we need and get files from it
978 	std::string dir = getBasename(s.getDirectory());
979 	for (size_t i = 0; i < Dirs->size(); ++i)
980 	{
981 		if ((*Dirs)[i].value().first == dir)
982 		{
983 			Dirs->highlight(i);
984 			break;
985 		}
986 	}
987 	// refresh window so we can be highlighted item
988 	Dirs->refresh();
989 
990 	Tags->clear();
991 	update();
992 
993 	// reset TagTypes since it can be under Filename
994 	// and then songs in right column are not visible.
995 	TagTypes->reset();
996 	// go to the right column
997 	nextColumn();
998 	nextColumn();
999 
1000 	// highlight our file
1001 	for (size_t i = 0; i < Tags->size(); ++i)
1002 	{
1003 		if ((*Tags)[i].value() == s)
1004 		{
1005 			Tags->highlight(i);
1006 			break;
1007 		}
1008 	}
1009 }
1010 
1011 namespace {
1012 
isAnyModified(const NC::Menu<MPD::MutableSong> & m)1013 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1014 {
1015 	for (auto it = m.beginV(); it != m.endV(); ++it)
1016 		if (it->isModified())
1017 			return true;
1018 	return false;
1019 }
1020 
CapitalizeFirstLetters(const std::string & s)1021 std::string CapitalizeFirstLetters(const std::string &s)
1022 {
1023 	std::wstring ws = ToWString(s);
1024 	wchar_t prev = 0;
1025 	for (auto it = ws.begin(); it != ws.end(); ++it)
1026 	{
1027 		if (!iswalpha(prev) && prev != L'\'')
1028 			*it = towupper(*it);
1029 		prev = *it;
1030 	}
1031 	return ToString(ws);
1032 }
1033 
CapitalizeFirstLetters(MPD::MutableSong & s)1034 void CapitalizeFirstLetters(MPD::MutableSong &s)
1035 {
1036 	for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1037 	{
1038 		unsigned i = 0;
1039 		for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1040 			(s.*m->Set)(CapitalizeFirstLetters(tag), i);
1041 	}
1042 }
1043 
LowerAllLetters(MPD::MutableSong & s)1044 void LowerAllLetters(MPD::MutableSong &s)
1045 {
1046 	for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1047 	{
1048 		unsigned i = 0;
1049 		for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1050 			(s.*m->Set)(boost::locale::to_lower(tag), i);
1051 	}
1052 }
1053 
GetPatternList()1054 void GetPatternList()
1055 {
1056 	if (Patterns.empty())
1057 	{
1058 		std::ifstream input(PatternsFile.c_str());
1059 		if (input.is_open())
1060 		{
1061 			std::string line;
1062 			while (std::getline(input, line))
1063 				if (!line.empty())
1064 					Patterns.push_back(line);
1065 			input.close();
1066 		}
1067 	}
1068 }
1069 
SavePatternList()1070 void SavePatternList()
1071 {
1072 	std::ofstream output(PatternsFile.c_str());
1073 	if (output.is_open())
1074 	{
1075 		std::list<std::string>::const_iterator it = Patterns.begin();
1076 		for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1077 			output << *it << std::endl;
1078 		output.close();
1079 	}
1080 }
IntoSetFunction(char c)1081 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1082 {
1083 	switch (c)
1084 	{
1085 		case 'a':
1086 			return &MPD::MutableSong::setArtist;
1087 		case 'A':
1088 			return &MPD::MutableSong::setAlbumArtist;
1089 		case 't':
1090 			return &MPD::MutableSong::setTitle;
1091 		case 'b':
1092 			return &MPD::MutableSong::setAlbum;
1093 		case 'y':
1094 			return &MPD::MutableSong::setDate;
1095 		case 'n':
1096 			return &MPD::MutableSong::setTrack;
1097 		case 'g':
1098 			return &MPD::MutableSong::setGenre;
1099 		case 'c':
1100 			return &MPD::MutableSong::setComposer;
1101 		case 'p':
1102 			return &MPD::MutableSong::setPerformer;
1103 		case 'd':
1104 			return &MPD::MutableSong::setDisc;
1105 		case 'C':
1106 			return &MPD::MutableSong::setComment;
1107 		default:
1108 			return 0;
1109 	}
1110 }
1111 
GenerateFilename(const MPD::MutableSong & s,const std::string & pattern)1112 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern)
1113 {
1114 	std::string result = Format::stringify<char>(Format::parse(pattern), &s);
1115 	removeInvalidCharsFromFilename(result, Config.generate_win32_compatible_filenames);
1116 	return result;
1117 }
1118 
ParseFilename(MPD::MutableSong & s,std::string mask,bool preview)1119 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview)
1120 {
1121 	std::ostringstream result;
1122 	std::vector<std::string> separators;
1123 	std::vector< std::pair<char, std::string> > tags;
1124 	std::string file = s.getName().substr(0, s.getName().rfind("."));
1125 
1126 	size_t i = mask.find("%");
1127 
1128 	if (!mask.substr(0, i).empty())
1129 		file = file.substr(i);
1130 
1131 	for (; i != std::string::npos; i = mask.find("%"))
1132 	{
1133 		tags.push_back(std::make_pair(mask.at(i+1), ""));
1134 		mask = mask.substr(i+2);
1135 		i = mask.find("%");
1136 		if (!mask.empty())
1137 			separators.push_back(mask.substr(0, i));
1138 	}
1139 	i = 0;
1140 	for (auto it = separators.begin(); it != separators.end(); ++it, ++i)
1141 	{
1142 		size_t j = file.find(*it);
1143 		tags.at(i).second = file.substr(0, j);
1144 		if (j+it->length() > file.length())
1145 			goto PARSE_FAILED;
1146 		file = file.substr(j+it->length());
1147 	}
1148 	if (!file.empty())
1149 	{
1150 		if (i >= tags.size())
1151 			goto PARSE_FAILED;
1152 		tags.at(i).second = file;
1153 	}
1154 
1155 	if (0) // tss...
1156 	{
1157 		PARSE_FAILED:
1158 		return "Error while parsing filename!\n";
1159 	}
1160 
1161 	for (auto it = tags.begin(); it != tags.end(); ++it)
1162 	{
1163 		for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1164 			if (*j == '_')
1165 				*j = ' ';
1166 
1167 		if (!preview)
1168 		{
1169 			MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1170 			if (set)
1171 				s.setTags(set, it->second);
1172 		}
1173 		else
1174 			result << "%" << it->first << ": " << it->second << "\n";
1175 	}
1176 	return result.str();
1177 }
1178 
SongToString(const MPD::MutableSong & s)1179 std::string SongToString(const MPD::MutableSong &s)
1180 {
1181 	std::string result;
1182 	size_t i = myTagEditor->TagTypes->choice();
1183 	if (i < 11)
1184 		result = (s.*SongInfo::Tags[i].Get)(0);
1185 	else if (i == 12)
1186 		result = s.getNewName().empty() ? s.getName() : s.getName() + " -> " + s.getNewName();
1187 	return result.empty() ? Config.empty_tag : result;
1188 }
1189 
DirEntryMatcher(const Regex::Regex & rx,const std::pair<std::string,std::string> & dir,bool filter)1190 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter)
1191 {
1192 	if (dir.first == "." || dir.first == "..")
1193 		return filter;
1194 	return Regex::search(dir.first, rx, Config.ignore_diacritics);
1195 }
1196 
SongEntryMatcher(const Regex::Regex & rx,const MPD::MutableSong & s)1197 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s)
1198 {
1199 	return Regex::search(SongToString(s), rx, Config.ignore_diacritics);
1200 }
1201 
1202 }
1203 
1204 #endif
1205 
1206