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