1 /***************************************************************************
2 * Copyright (C) 2008-2012 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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. *
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. *
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 ***************************************************************************/
21 #include "tag_editor.h"
31 #include "textidentificationframe.h"
33 #include "vorbisfile.h"
35 #include "xiphcomment.h"
45 #include "song_info.h"
46 #include "statusbar.h"
47 #include "utility/comparators.h"
50 using namespace std::placeholders
;
52 using Global::myScreen
;
53 using Global::MainHeight
;
54 using Global::MainStartY
;
56 TagEditor
*myTagEditor
= new TagEditor
;
60 size_t LeftColumnWidth
;
61 size_t LeftColumnStartX
;
62 size_t MiddleColumnWidth
;
63 size_t MiddleColumnStartX
;
64 size_t RightColumnWidth
;
65 size_t RightColumnStartX
;
67 size_t FParserDialogWidth
;
68 size_t FParserDialogHeight
;
70 size_t FParserWidthOne
;
71 size_t FParserWidthTwo
;
74 std::list
<std::string
> Patterns
;
75 std::string PatternsFile
= "patterns.list";
77 bool isAnyModified(const NC::Menu
<MPD::MutableSong
> &m
);
79 std::string
CapitalizeFirstLetters(const std::string
&s
);
80 void CapitalizeFirstLetters(MPD::MutableSong
&s
);
81 void LowerAllLetters(MPD::MutableSong
&s
);
82 void GetTagList(TagLib::StringList
&list
, const MPD::MutableSong
&s
, MPD::Song::GetFunction f
);
85 void WriteID3v2(const TagLib::ByteVector
&type
, TagLib::ID3v2::Tag
*tag
, const T
&list
);
86 void WriteXiphComments(const MPD::MutableSong
&s
, TagLib::Ogg::XiphComment
*tag
);
88 void GetPatternList();
89 void SavePatternList();
91 MPD::MutableSong::SetFunction
IntoSetFunction(char c
);
92 std::string
GenerateFilename(const MPD::MutableSong
&s
, const std::string
&pattern
);
93 std::string
ParseFilename(MPD::MutableSong
&s
, std::string mask
, bool preview
);
95 std::string
SongToString(const MPD::MutableSong
&s
);
96 bool DirEntryMatcher(const Regex
&rx
, const std::pair
<std::string
, std::string
> &dir
, bool filter
);
97 bool SongEntryMatcher(const Regex
&rx
, const MPD::MutableSong
&s
);
101 void TagEditor::Init()
103 PatternsFile
= Config
.ncmpcpp_directory
+ "patterns.list";
104 SetDimensions(0, COLS
);
106 Dirs
= new NC::Menu
< std::pair
<std::string
, std::string
> >(0, MainStartY
, LeftColumnWidth
, MainHeight
, Config
.titles_visibility
? "Directories" : "", Config
.main_color
, NC::brNone
);
107 Dirs
->setHighlightColor(Config
.active_column_color
);
108 Dirs
->cyclicScrolling(Config
.use_cyclic_scrolling
);
109 Dirs
->centeredCursor(Config
.centered_cursor
);
110 Dirs
->setItemDisplayer(Display::Pair
<std::string
, std::string
>);
112 TagTypes
= new NC::Menu
<std::string
>(MiddleColumnStartX
, MainStartY
, MiddleColumnWidth
, MainHeight
, Config
.titles_visibility
? "Tag types" : "", Config
.main_color
, NC::brNone
);
113 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
114 TagTypes
->cyclicScrolling(Config
.use_cyclic_scrolling
);
115 TagTypes
->centeredCursor(Config
.centered_cursor
);
116 TagTypes
->setItemDisplayer(Display::Default
<std::string
>);
118 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
119 TagTypes
->addItem(m
->Name
);
120 TagTypes
->addSeparator();
121 TagTypes
->addItem("Filename");
122 TagTypes
->addSeparator();
123 if (Config
.titles_visibility
)
124 TagTypes
->addItem("Options", 1, 1);
125 TagTypes
->addSeparator();
126 TagTypes
->addItem("Capitalize First Letters");
127 TagTypes
->addItem("lower all letters");
128 TagTypes
->addSeparator();
129 TagTypes
->addItem("Reset");
130 TagTypes
->addItem("Save");
132 Tags
= new NC::Menu
<MPD::MutableSong
>(RightColumnStartX
, MainStartY
, RightColumnWidth
, MainHeight
, Config
.titles_visibility
? "Tags" : "", Config
.main_color
, NC::brNone
);
133 Tags
->setHighlightColor(Config
.main_highlight_color
);
134 Tags
->cyclicScrolling(Config
.use_cyclic_scrolling
);
135 Tags
->centeredCursor(Config
.centered_cursor
);
136 Tags
->setSelectedPrefix(Config
.selected_item_prefix
);
137 Tags
->setSelectedSuffix(Config
.selected_item_suffix
);
138 Tags
->setItemDisplayer(Display::Tags
);
140 FParserDialog
= new NC::Menu
<std::string
>((COLS
-FParserDialogWidth
)/2, (MainHeight
-FParserDialogHeight
)/2+MainStartY
, FParserDialogWidth
, FParserDialogHeight
, "", Config
.main_color
, Config
.window_border
);
141 FParserDialog
->cyclicScrolling(Config
.use_cyclic_scrolling
);
142 FParserDialog
->centeredCursor(Config
.centered_cursor
);
143 FParserDialog
->setItemDisplayer(Display::Default
<std::string
>);
144 FParserDialog
->addItem("Get tags from filename");
145 FParserDialog
->addItem("Rename files");
146 FParserDialog
->addSeparator();
147 FParserDialog
->addItem("Cancel");
149 FParser
= new NC::Menu
<std::string
>((COLS
-FParserWidth
)/2, (MainHeight
-FParserHeight
)/2+MainStartY
, FParserWidthOne
, FParserHeight
, "_", Config
.main_color
, Config
.active_window_border
);
150 FParser
->cyclicScrolling(Config
.use_cyclic_scrolling
);
151 FParser
->centeredCursor(Config
.centered_cursor
);
152 FParser
->setItemDisplayer(Display::Default
<std::string
>);
154 FParserLegend
= new NC::Scrollpad((COLS
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
, FParserWidthTwo
, FParserHeight
, "Legend", Config
.main_color
, Config
.window_border
);
156 FParserPreview
= new NC::Scrollpad((COLS
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
, FParserWidthTwo
, FParserHeight
, "Preview", Config
.main_color
, Config
.window_border
);
162 void TagEditor::SetDimensions(size_t x_offset
, size_t width
)
164 MiddleColumnWidth
= std::min(26, COLS
-2);
165 LeftColumnStartX
= x_offset
;
166 LeftColumnWidth
= (width
-MiddleColumnWidth
)/2;
167 MiddleColumnStartX
= LeftColumnStartX
+LeftColumnWidth
+1;
168 RightColumnWidth
= width
-LeftColumnWidth
-MiddleColumnWidth
-2;
169 RightColumnStartX
= MiddleColumnStartX
+MiddleColumnWidth
+1;
171 FParserDialogWidth
= std::min(30, COLS
);
172 FParserDialogHeight
= std::min(size_t(6), MainHeight
);
173 FParserWidth
= width
*0.9;
174 FParserHeight
= std::min(size_t(LINES
*0.8), MainHeight
);
175 FParserWidthOne
= FParserWidth
/2;
176 FParserWidthTwo
= FParserWidth
-FParserWidthOne
;
179 void TagEditor::Resize()
181 size_t x_offset
, width
;
182 GetWindowResizeParams(x_offset
, width
);
183 SetDimensions(x_offset
, width
);
185 Dirs
->resize(LeftColumnWidth
, MainHeight
);
186 TagTypes
->resize(MiddleColumnWidth
, MainHeight
);
187 Tags
->resize(RightColumnWidth
, MainHeight
);
188 FParserDialog
->resize(FParserDialogWidth
, FParserDialogHeight
);
189 FParser
->resize(FParserWidthOne
, FParserHeight
);
190 FParserLegend
->resize(FParserWidthTwo
, FParserHeight
);
191 FParserPreview
->resize(FParserWidthTwo
, FParserHeight
);
193 Dirs
->moveTo(LeftColumnStartX
, MainStartY
);
194 TagTypes
->moveTo(MiddleColumnStartX
, MainStartY
);
195 Tags
->moveTo(RightColumnStartX
, MainStartY
);
197 FParserDialog
->moveTo(x_offset
+(width
-FParserDialogWidth
)/2, (MainHeight
-FParserDialogHeight
)/2+MainStartY
);
198 FParser
->moveTo(x_offset
+(width
-FParserWidth
)/2, (MainHeight
-FParserHeight
)/2+MainStartY
);
199 FParserLegend
->moveTo(x_offset
+(width
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
);
200 FParserPreview
->moveTo(x_offset
+(width
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
);
202 if (MainHeight
< 5 && (w
== FParserDialog
|| w
== FParser
|| w
== FParserHelper
)) // screen too low
203 w
= TagTypes
; // fall back to main columns
208 std::wstring
TagEditor::Title()
210 return L
"Tag editor";
213 void TagEditor::SwitchTo()
215 using Global::myLockedScreen
;
217 if (myScreen
== this)
224 UpdateInactiveScreen(this);
226 if (hasToBeResized
|| myLockedScreen
)
229 if (myScreen
!= this && myScreen
->isTabbable())
230 Global::myPrevScreen
= myScreen
;
236 void TagEditor::Refresh()
239 mvvline(MainStartY
, MiddleColumnStartX
-1, 0, MainHeight
);
241 mvvline(MainStartY
, RightColumnStartX
-1, 0, MainHeight
);
244 if (w
== FParserDialog
)
246 FParserDialog
->display();
248 else if (w
== FParser
|| w
== FParserHelper
)
251 FParserHelper
->display();
255 void TagEditor::Update()
257 if (Dirs
->reallyEmpty())
259 Dirs
->Window::clear();
262 int highlightme
= -1;
263 auto dirs
= Mpd
.GetDirectories(itsBrowsedDir
);
264 std::sort(dirs
.begin(), dirs
.end(), LocaleBasedSorting(std::locale(), Config
.ignore_leading_the
));
265 if (itsBrowsedDir
!= "/")
267 size_t slash
= itsBrowsedDir
.rfind("/");
268 std::string parent
= slash
!= std::string::npos
? itsBrowsedDir
.substr(0, slash
) : "/";
269 Dirs
->addItem(make_pair("..", parent
));
272 Dirs
->addItem(std::make_pair(".", "/"));
273 for (auto dir
= dirs
.begin(); dir
!= dirs
.end(); ++dir
)
275 size_t slash
= dir
->rfind("/");
276 std::string to_display
= slash
!= std::string::npos
? dir
->substr(slash
+1) : *dir
;
277 Dirs
->addItem(make_pair(to_display
, *dir
));
278 if (*dir
== itsHighlightedDir
)
279 highlightme
= Dirs
->size()-1;
281 if (highlightme
!= -1)
282 Dirs
->highlight(highlightme
);
287 if (Tags
->reallyEmpty())
290 auto songs
= Mpd
.GetSongs(Dirs
->current().value().second
);
291 std::sort(songs
.begin(), songs
.end(),
292 LocaleBasedSorting(std::locale(), Config
.ignore_leading_the
));
293 for (auto s
= songs
.begin(); s
!= songs
.end(); ++s
)
298 if (w
== TagTypes
&& TagTypes
->choice() < 13)
302 else if (TagTypes
->choice() >= 13)
304 Tags
->Window::clear();
305 Tags
->Window::refresh();
309 void TagEditor::EnterPressed()
311 using Global::wFooter
;
315 auto dirs
= Mpd
.GetDirectories(Dirs
->current().value().second
);
318 itsHighlightedDir
= itsBrowsedDir
;
319 itsBrowsedDir
= Dirs
->current().value().second
;
324 Statusbar::msg("No subdirectories found");
326 else if (w
== FParserDialog
)
328 size_t choice
= FParserDialog
->choice();
329 if (choice
== 3) // cancel
337 // prepare additional windows
339 FParserLegend
->clear();
340 *FParserLegend
<< L
"%a - artist\n";
341 *FParserLegend
<< L
"%A - album artist\n";
342 *FParserLegend
<< L
"%t - title\n";
343 *FParserLegend
<< L
"%b - album\n";
344 *FParserLegend
<< L
"%y - date\n";
345 *FParserLegend
<< L
"%n - track number\n";
346 *FParserLegend
<< L
"%g - genre\n";
347 *FParserLegend
<< L
"%c - composer\n";
348 *FParserLegend
<< L
"%p - performer\n";
349 *FParserLegend
<< L
"%d - disc\n";
350 *FParserLegend
<< L
"%C - comment\n\n";
351 *FParserLegend
<< NC::fmtBold
<< L
"Files:\n" << NC::fmtBoldEnd
;
352 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
353 *FParserLegend
<< Config
.color2
<< L
" * " << NC::clEnd
<< (*it
)->getName() << '\n';
354 FParserLegend
->flush();
356 if (!Patterns
.empty())
357 Config
.pattern
= Patterns
.front();
360 FParser
->addItem("Pattern: " + Config
.pattern
);
361 FParser
->addItem("Preview");
362 FParser
->addItem("Legend");
363 FParser
->addSeparator();
364 FParser
->addItem("Proceed");
365 FParser
->addItem("Cancel");
366 if (!Patterns
.empty())
368 FParser
->addSeparator();
369 FParser
->addItem("Recent patterns", 1, 1);
370 FParser
->addSeparator();
371 for (std::list
<std::string
>::const_iterator it
= Patterns
.begin(); it
!= Patterns
.end(); ++it
)
372 FParser
->addItem(*it
);
375 FParser
->setTitle(choice
== 0 ? "Get tags from filename" : "Rename files");
377 FParserUsePreview
= 1;
378 FParserHelper
= FParserLegend
;
379 FParserHelper
->display();
381 else if (w
== FParser
)
384 size_t pos
= FParser
->choice();
386 if (pos
== 4) // save
387 FParserUsePreview
= 0;
389 if (pos
== 0) // change pattern
392 Statusbar::put() << "Pattern: ";
393 std::string new_pattern
= wFooter
->getString(Config
.pattern
);
395 if (!new_pattern
.empty())
397 Config
.pattern
= new_pattern
;
398 FParser
->at(0).value() = "Pattern: ";
399 FParser
->at(0).value() += Config
.pattern
;
402 else if (pos
== 1 || pos
== 4) // preview or proceed
405 Statusbar::msg("Parsing...");
406 FParserPreview
->clear();
407 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
409 MPD::MutableSong
&s
= **it
;
410 if (FParserDialog
->choice() == 0) // get tags from filename
412 if (FParserUsePreview
)
414 *FParserPreview
<< NC::fmtBold
<< s
.getName() << L
":\n" << NC::fmtBoldEnd
;
415 *FParserPreview
<< ParseFilename(s
, Config
.pattern
, FParserUsePreview
) << '\n';
418 ParseFilename(s
, Config
.pattern
, FParserUsePreview
);
422 std::string file
= s
.getName();
423 size_t last_dot
= file
.rfind(".");
424 std::string extension
= file
.substr(last_dot
);
425 std::string new_file
= GenerateFilename(s
, "{" + Config
.pattern
+ "}");
426 if (new_file
.empty() && !FParserUsePreview
)
428 Statusbar::msg("File \"%s\" would have an empty name", s
.getName().c_str());
429 FParserUsePreview
= 1;
432 if (!FParserUsePreview
)
433 s
.setNewURI(new_file
+ extension
);
434 *FParserPreview
<< file
<< Config
.color2
<< L
" -> " << NC::clEnd
;
435 if (new_file
.empty())
436 *FParserPreview
<< Config
.empty_tags_color
<< Config
.empty_tag
<< NC::clEnd
;
438 *FParserPreview
<< new_file
<< extension
;
439 *FParserPreview
<< '\n' << '\n';
444 if (FParserUsePreview
)
446 FParserHelper
= FParserPreview
;
447 FParserHelper
->flush();
448 FParserHelper
->display();
452 Patterns
.remove(Config
.pattern
);
453 Patterns
.insert(Patterns
.begin(), Config
.pattern
);
456 if (pos
!= 4 || success
)
457 Statusbar::msg("Operation finished");
459 else if (pos
== 2) // show legend
461 FParserHelper
= FParserLegend
;
462 FParserHelper
->display();
464 else if (pos
== 5) // cancel
468 else // list of patterns
470 Config
.pattern
= FParser
->current().value();
471 FParser
->at(0).value() = "Pattern: " + Config
.pattern
;
483 if ((w
!= TagTypes
&& w
!= Tags
) || Tags
->empty()) // after this point we start dealing with tags
487 if (Tags
->hasSelected()) // if there are selected songs, perform operations only on them
489 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
490 if (it
->isSelected())
491 EditedSongs
.push_back(&it
->value());
495 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
496 EditedSongs
.push_back(&it
->value());
499 size_t id
= TagTypes
->choice();
501 if (w
== TagTypes
&& id
== 5)
503 bool yes
= Action::AskYesNoQuestion("Number tracks?", Status::trace
);
506 auto it
= EditedSongs
.begin();
507 for (unsigned i
= 1; i
<= EditedSongs
.size(); ++i
, ++it
)
509 if (Config
.tag_editor_extended_numeration
)
510 (*it
)->setTrack(unsignedIntTo
<std::string
>::apply(i
) + "/" + unsignedIntTo
<std::string
>::apply(EditedSongs
.size()));
512 (*it
)->setTrack(unsignedIntTo
<std::string
>::apply(i
));
514 Statusbar::msg("Tracks numbered");
517 Statusbar::msg("Aborted");
523 MPD::Song::GetFunction get
= SongInfo::Tags
[id
].Get
;
524 MPD::MutableSong::SetFunction set
= SongInfo::Tags
[id
].Set
;
525 if (id
> 0 && w
== TagTypes
)
528 Statusbar::put() << NC::fmtBold
<< TagTypes
->current().value() << NC::fmtBoldEnd
<< ": ";
529 std::string new_tag
= wFooter
->getString(Tags
->current().value().getTags(get
));
531 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
532 (*it
)->setTags(set
, new_tag
);
537 Statusbar::put() << NC::fmtBold
<< TagTypes
->current().value() << NC::fmtBoldEnd
<< ": ";
538 std::string new_tag
= wFooter
->getString(Tags
->current().value().getTags(get
));
540 if (new_tag
!= Tags
->current().value().getTags(get
))
541 Tags
->current().value().setTags(set
, new_tag
);
542 Tags
->scroll(NC::wDown
);
547 if (id
== 12) // filename related options
551 if (size_t(COLS
) < FParserDialogWidth
|| MainHeight
< FParserDialogHeight
)
553 Statusbar::msg("Screen is too small to display additional windows");
556 FParserDialog
->reset();
561 MPD::MutableSong
&s
= Tags
->current().value();
562 std::string old_name
= s
.getNewURI().empty() ? s
.getName() : s
.getNewURI();
563 size_t last_dot
= old_name
.rfind(".");
564 std::string extension
= old_name
.substr(last_dot
);
565 old_name
= old_name
.substr(0, last_dot
);
567 Statusbar::put() << NC::fmtBold
<< "New filename: " << NC::fmtBoldEnd
;
568 std::string new_name
= wFooter
->getString(old_name
);
570 if (!new_name
.empty() && new_name
!= old_name
)
571 s
.setNewURI(new_name
+ extension
);
572 Tags
->scroll(NC::wDown
);
575 else if (id
== 16) // capitalize first letters
577 Statusbar::msg("Processing...");
578 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
579 CapitalizeFirstLetters(**it
);
580 Statusbar::msg("Done");
582 else if (id
== 17) // lower all letters
584 Statusbar::msg("Processing...");
585 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
586 LowerAllLetters(**it
);
587 Statusbar::msg("Done");
589 else if (id
== 19) // reset
591 for (auto it
= Tags
->beginV(); it
!= Tags
->endV(); ++it
)
592 it
->clearModifications();
593 Statusbar::msg("Changes reset");
595 else if (id
== 20) // save
598 Statusbar::msg("Writing changes...");
599 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
601 Statusbar::msg("Writing tags in \"%s\"...", (*it
)->getName().c_str());
602 if (!WriteTags(**it
))
604 const char msg
[] = "Error while writing tags in \"%ls\"";
605 Statusbar::msg(msg
, wideShorten(ToWString((*it
)->getURI()), COLS
-const_strlen(msg
)).c_str());
612 Statusbar::msg("Tags updated");
613 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
617 Dirs
->setHighlightColor(Config
.active_column_color
);
618 Mpd
.UpdateDirectory(getSharedDirectory(Tags
->beginV(), Tags
->endV()));
626 void TagEditor::SpacePressed()
628 if (w
== Tags
&& !Tags
->empty())
630 Tags
->current().setSelected(!Tags
->current().isSelected());
631 w
->scroll(NC::wDown
);
635 void TagEditor::MouseButtonPressed(MEVENT me
)
637 if (w
== FParserDialog
)
639 if (FParserDialog
->hasCoords(me
.x
, me
.y
))
641 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
643 FParserDialog
->Goto(me
.y
);
644 if (me
.bstate
& BUTTON3_PRESSED
)
648 Screen
<NC::Window
>::MouseButtonPressed(me
);
651 else if (w
== FParser
|| w
== FParserHelper
)
653 if (FParser
->hasCoords(me
.x
, me
.y
))
657 if (size_t(me
.y
) < FParser
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
660 if (me
.bstate
& BUTTON3_PRESSED
)
664 Screen
<NC::Window
>::MouseButtonPressed(me
);
666 else if (FParserHelper
->hasCoords(me
.x
, me
.y
))
668 if (w
!= FParserHelper
)
670 ScrollpadMouseButtonPressed(FParserHelper
, me
);
673 else if (!Dirs
->empty() && Dirs
->hasCoords(me
.x
, me
.y
))
680 if (size_t(me
.y
) < Dirs
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
683 if (me
.bstate
& BUTTON1_PRESSED
)
689 Screen
<NC::Window
>::MouseButtonPressed(me
);
692 else if (!TagTypes
->empty() && TagTypes
->hasCoords(me
.x
, me
.y
))
695 w
== Dirs
? NextColumn() : PrevColumn();
696 if (size_t(me
.y
) < TagTypes
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
698 if (!TagTypes
->Goto(me
.y
))
702 if (me
.bstate
& BUTTON3_PRESSED
)
706 Screen
<NC::Window
>::MouseButtonPressed(me
);
708 else if (!Tags
->empty() && Tags
->hasCoords(me
.x
, me
.y
))
715 if (size_t(me
.y
) < Tags
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
719 if (me
.bstate
& BUTTON3_PRESSED
)
723 Screen
<NC::Window
>::MouseButtonPressed(me
);
727 /***********************************************************************/
729 bool TagEditor::allowsFiltering()
731 return w
== Dirs
|| w
== Tags
;
734 std::string
TagEditor::currentFilter()
738 filter
= RegexFilter
< std::pair
<std::string
, std::string
> >::currentFilter(*Dirs
);
740 filter
= RegexFilter
<MPD::MutableSong
>::currentFilter(*Tags
);
744 void TagEditor::applyFilter(const std::string
&filter
)
749 auto fun
= std::bind(DirEntryMatcher
, _1
, _2
, true);
750 auto rx
= RegexFilter
< std::pair
<std::string
, std::string
> >(filter
, Config
.regex_type
, fun
);
751 Dirs
->filter(Dirs
->begin(), Dirs
->end(), rx
);
756 auto rx
= RegexFilter
<MPD::MutableSong
>(filter
, Config
.regex_type
, SongEntryMatcher
);
757 Tags
->filter(Tags
->begin(), Tags
->end(), rx
);
761 /***********************************************************************/
763 bool TagEditor::allowsSearching()
765 return w
== Dirs
|| w
== Tags
;
768 bool TagEditor::search(const std::string
&constraint
)
773 auto fun
= std::bind(DirEntryMatcher
, _1
, _2
, false);
774 auto rx
= RegexFilter
< std::pair
<std::string
, std::string
> >(constraint
, Config
.regex_type
, fun
);
775 result
= Dirs
->search(Dirs
->begin(), Dirs
->end(), rx
);
779 auto rx
= RegexFilter
<MPD::MutableSong
>(constraint
, Config
.regex_type
, SongEntryMatcher
);
780 result
= Tags
->search(Tags
->begin(), Tags
->end(), rx
);
785 void TagEditor::nextFound(bool wrap
)
788 Dirs
->nextFound(wrap
);
790 Tags
->nextFound(wrap
);
793 void TagEditor::prevFound(bool wrap
)
796 Dirs
->prevFound(wrap
);
798 Tags
->prevFound(wrap
);
801 /***********************************************************************/
803 std::shared_ptr
<ProxySongList
> TagEditor::getProxySongList()
805 auto ptr
= nullProxySongList();
807 ptr
= mkProxySongList(*Tags
, [](NC::Menu
<MPD::MutableSong
>::Item
&item
) {
808 return &item
.value();
813 bool TagEditor::allowsSelection()
818 void TagEditor::reverseSelection()
821 reverseSelectionHelper(Tags
->begin(), Tags
->end());
824 MPD::SongList
TagEditor::getSelectedSongs()
826 MPD::SongList result
;
829 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
830 if (it
->isSelected())
831 result
.push_back(it
->value());
832 // if no song was selected, add current one
833 if (result
.empty() && !Tags
->empty())
834 result
.push_back(Tags
->current().value());
839 /***********************************************************************/
841 bool TagEditor::ifAnyModifiedAskForDiscarding()
844 if (isAnyModified(*Tags
))
845 result
= Action::AskYesNoQuestion("There are pending changes, are you sure?", Status::trace
);
849 bool TagEditor::isNextColumnAvailable()
854 if (!TagTypes
->reallyEmpty() && !Tags
->reallyEmpty())
857 else if (w
== TagTypes
)
859 if (!Tags
->reallyEmpty())
862 else if (w
== FParser
)
867 bool TagEditor::NextColumn()
871 Dirs
->setHighlightColor(Config
.main_highlight_color
);
874 TagTypes
->setHighlightColor(Config
.active_column_color
);
877 else if (w
== TagTypes
&& TagTypes
->choice() < 13 && !Tags
->reallyEmpty())
879 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
882 Tags
->setHighlightColor(Config
.active_column_color
);
885 else if (w
== FParser
)
887 FParser
->setBorder(Config
.window_border
);
890 FParserHelper
->setBorder(Config
.active_window_border
);
891 FParserHelper
->display();
897 bool TagEditor::isPrevColumnAvailable()
902 if (!TagTypes
->reallyEmpty() && !Dirs
->reallyEmpty())
905 else if (w
== TagTypes
)
907 if (!Dirs
->reallyEmpty())
908 result
= ifAnyModifiedAskForDiscarding();
910 else if (w
== FParserHelper
)
915 bool TagEditor::PrevColumn()
919 Tags
->setHighlightColor(Config
.main_highlight_color
);
922 TagTypes
->setHighlightColor(Config
.active_column_color
);
925 else if (w
== TagTypes
)
927 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
930 Dirs
->setHighlightColor(Config
.active_column_color
);
933 else if (w
== FParserHelper
)
935 FParserHelper
->setBorder(Config
.window_border
);
936 FParserHelper
->display();
938 FParser
->setBorder(Config
.active_window_border
);
945 void TagEditor::LocateSong(const MPD::Song
&s
)
947 if (myScreen
== this)
950 if (s
.getDirectory().empty())
953 if (Global::myScreen
!= this)
956 // go to right directory
957 if (itsBrowsedDir
!= s
.getDirectory())
959 itsBrowsedDir
= s
.getDirectory();
960 size_t last_slash
= itsBrowsedDir
.rfind('/');
961 if (last_slash
!= std::string::npos
)
962 itsBrowsedDir
= itsBrowsedDir
.substr(0, last_slash
);
965 if (itsBrowsedDir
.empty())
970 if (itsBrowsedDir
== "/")
971 Dirs
->reset(); // go to the first pos, which is "." (music dir root)
973 // highlight directory we need and get files from it
974 std::string dir
= getBasename(s
.getDirectory());
975 for (size_t i
= 0; i
< Dirs
->size(); ++i
)
977 if ((*Dirs
)[i
].value().first
== dir
)
983 // refresh window so we can be highlighted item
989 // reset TagTypes since it can be under Filename
990 // and then songs in right column are not visible.
992 // go to the right column
996 // highlight our file
997 for (size_t i
= 0; i
< Tags
->size(); ++i
)
999 if ((*Tags
)[i
].value().getHash() == s
.getHash())
1007 void TagEditor::ReadTags(MPD::MutableSong
&s
)
1009 TagLib::FileRef
f(s
.getURI().c_str());
1013 TagLib::MPEG::File
*mpegf
= dynamic_cast<TagLib::MPEG::File
*>(f
.file());
1014 s
.setDuration(f
.audioProperties()->length());
1016 s
.setArtist(f
.tag()->artist().to8Bit(1));
1017 s
.setTitle(f
.tag()->title().to8Bit(1));
1018 s
.setAlbum(f
.tag()->album().to8Bit(1));
1019 s
.setTrack(intTo
<std::string
>::apply(f
.tag()->track()));
1020 s
.setDate(intTo
<std::string
>::apply(f
.tag()->year()));
1021 s
.setGenre(f
.tag()->genre().to8Bit(1));
1024 s
.setAlbumArtist(!mpegf
->ID3v2Tag()->frameListMap()["TPE2"].isEmpty() ? mpegf
->ID3v2Tag()->frameListMap()["TPE2"].front()->toString().to8Bit(1) : "");
1025 s
.setComposer(!mpegf
->ID3v2Tag()->frameListMap()["TCOM"].isEmpty() ? mpegf
->ID3v2Tag()->frameListMap()["TCOM"].front()->toString().to8Bit(1) : "");
1026 s
.setPerformer(!mpegf
->ID3v2Tag()->frameListMap()["TOPE"].isEmpty() ? mpegf
->ID3v2Tag()->frameListMap()["TOPE"].front()->toString().to8Bit(1) : "");
1027 s
.setDisc(!mpegf
->ID3v2Tag()->frameListMap()["TPOS"].isEmpty() ? mpegf
->ID3v2Tag()->frameListMap()["TPOS"].front()->toString().to8Bit(1) : "");
1029 s
.setComment(f
.tag()->comment().to8Bit(1));
1032 bool TagEditor::WriteTags(MPD::MutableSong
&s
)
1034 std::string path_to_file
;
1035 bool file_is_from_db
= s
.isFromDatabase();
1036 if (file_is_from_db
)
1037 path_to_file
+= Config
.mpd_music_dir
;
1038 path_to_file
+= s
.getURI();
1039 TagLib::FileRef
f(path_to_file
.c_str());
1042 f
.tag()->setTitle(ToWString(s
.getTitle()));
1043 f
.tag()->setArtist(ToWString(s
.getArtist()));
1044 f
.tag()->setAlbum(ToWString(s
.getAlbum()));
1045 f
.tag()->setYear(stringToInt(s
.getDate()));
1046 f
.tag()->setTrack(stringToInt(s
.getTrack()));
1047 f
.tag()->setGenre(ToWString(s
.getGenre()));
1048 f
.tag()->setComment(ToWString(s
.getComment()));
1049 if (TagLib::MPEG::File
*mp3_file
= dynamic_cast<TagLib::MPEG::File
*>(f
.file()))
1051 TagLib::ID3v2::Tag
*tag
= mp3_file
->ID3v2Tag(1);
1052 TagLib::StringList list
;
1054 WriteID3v2("TIT2", tag
, ToWString(s
.getTitle())); // title
1055 WriteID3v2("TPE1", tag
, ToWString(s
.getArtist())); // artist
1056 WriteID3v2("TALB", tag
, ToWString(s
.getAlbum())); // album
1057 WriteID3v2("TDRC", tag
, ToWString(s
.getDate())); // date
1058 WriteID3v2("TRCK", tag
, ToWString(s
.getTrack())); // track
1059 WriteID3v2("TCON", tag
, ToWString(s
.getGenre())); // genre
1060 WriteID3v2("TPOS", tag
, ToWString(s
.getDisc())); // disc
1062 GetTagList(list
, s
, &MPD::Song::getAlbumArtist
);
1063 WriteID3v2("TPE2", tag
, list
); // album artist
1065 GetTagList(list
, s
, &MPD::Song::getComposer
);
1066 WriteID3v2("TCOM", tag
, list
); // composer
1068 GetTagList(list
, s
, &MPD::Song::getPerformer
);
1069 WriteID3v2("TPE3", tag
, list
); // performer
1071 else if (TagLib::Ogg::Vorbis::File
*ogg_file
= dynamic_cast<TagLib::Ogg::Vorbis::File
*>(f
.file()))
1073 WriteXiphComments(s
, ogg_file
->tag());
1075 else if (TagLib::FLAC::File
*flac_file
= dynamic_cast<TagLib::FLAC::File
*>(f
.file()))
1077 WriteXiphComments(s
, flac_file
->xiphComment(1));
1082 if (!s
.getNewURI().empty())
1084 std::string new_name
;
1085 if (file_is_from_db
)
1086 new_name
+= Config
.mpd_music_dir
;
1087 new_name
+= s
.getDirectory() + "/" + s
.getNewURI();
1088 if (rename(path_to_file
.c_str(), new_name
.c_str()) == 0 && !file_is_from_db
)
1090 if (Global::myOldScreen
== myPlaylist
)
1092 // if we rename local file, it won't get updated
1093 // so just remove it from playlist and add again
1094 size_t pos
= myPlaylist
->Items
->choice();
1095 Mpd
.StartCommandsList();
1097 int id
= Mpd
.AddSong("file://" + new_name
);
1100 s
= myPlaylist
->Items
->back().value();
1101 Mpd
.Move(s
.getPosition(), pos
);
1103 Mpd
.CommitCommandsList();
1105 else // only myBrowser->Main()
1106 myBrowser
->GetDirectory(myBrowser
->CurrentDir());
1117 bool isAnyModified(const NC::Menu
<MPD::MutableSong
> &m
)
1119 for (auto it
= m
.beginV(); it
!= m
.endV(); ++it
)
1120 if (it
->isModified())
1125 std::string
CapitalizeFirstLetters(const std::string
&s
)
1127 std::wstring ws
= ToWString(s
);
1129 for (auto it
= ws
.begin(); it
!= ws
.end(); ++it
)
1131 if (!iswalpha(prev
) && prev
!= L
'\'')
1132 *it
= towupper(*it
);
1135 return ToString(ws
);
1138 void CapitalizeFirstLetters(MPD::MutableSong
&s
)
1140 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
1143 for (std::string tag
; !(tag
= (s
.*m
->Get
)(i
)).empty(); ++i
)
1144 (s
.*m
->Set
)(CapitalizeFirstLetters(tag
), i
);
1148 void LowerAllLetters(MPD::MutableSong
&s
)
1150 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
1153 for (std::string tag
; !(tag
= (s
.*m
->Get
)(i
)).empty(); ++i
)
1154 (s
.*m
->Set
)(ToString(lowercase(ToWString(tag
))), i
);
1158 void GetTagList(TagLib::StringList
&list
, const MPD::MutableSong
&s
, MPD::Song::GetFunction f
)
1162 for (std::string value
; !(value
= (s
.*f
)(pos
)).empty(); ++pos
)
1163 list
.append(ToWString(value
));
1166 template <typename T
> void WriteID3v2(const TagLib::ByteVector
&type
, TagLib::ID3v2::Tag
*tag
, const T
&list
)
1168 using TagLib::ID3v2::TextIdentificationFrame
;
1169 tag
->removeFrames(type
);
1170 TextIdentificationFrame
*frame
= new TextIdentificationFrame(type
, TagLib::String::UTF8
);
1171 frame
->setText(list
);
1172 tag
->addFrame(frame
);
1175 void WriteXiphComments(const MPD::MutableSong
&s
, TagLib::Ogg::XiphComment
*tag
)
1177 TagLib::StringList list
;
1179 tag
->addField("DISCNUMBER", ToWString(s
.getDisc())); // disc
1181 tag
->removeField("ALBUM ARTIST"); // album artist
1182 GetTagList(list
, s
, &MPD::Song::getAlbumArtist
);
1183 for (TagLib::StringList::ConstIterator it
= list
.begin(); it
!= list
.end(); ++it
)
1184 tag
->addField("ALBUM ARTIST", *it
, 0);
1186 tag
->removeField("COMPOSER"); // composer
1187 GetTagList(list
, s
, &MPD::Song::getComposer
);
1188 for (TagLib::StringList::ConstIterator it
= list
.begin(); it
!= list
.end(); ++it
)
1189 tag
->addField("COMPOSER", *it
, 0);
1191 tag
->removeField("PERFORMER"); // performer
1192 GetTagList(list
, s
, &MPD::Song::getPerformer
);
1193 for (TagLib::StringList::ConstIterator it
= list
.begin(); it
!= list
.end(); ++it
)
1194 tag
->addField("PERFORMER", *it
, 0);
1197 void GetPatternList()
1199 if (Patterns
.empty())
1201 std::ifstream
input(PatternsFile
.c_str());
1202 if (input
.is_open())
1205 while (getline(input
, line
))
1207 Patterns
.push_back(line
);
1213 void SavePatternList()
1215 std::ofstream
output(PatternsFile
.c_str());
1216 if (output
.is_open())
1218 std::list
<std::string
>::const_iterator it
= Patterns
.begin();
1219 for (unsigned i
= 30; it
!= Patterns
.end() && i
; ++it
, --i
)
1220 output
<< *it
<< std::endl
;
1224 MPD::MutableSong::SetFunction
IntoSetFunction(char c
)
1229 return &MPD::MutableSong::setArtist
;
1231 return &MPD::MutableSong::setAlbumArtist
;
1233 return &MPD::MutableSong::setTitle
;
1235 return &MPD::MutableSong::setAlbum
;
1237 return &MPD::MutableSong::setDate
;
1239 return &MPD::MutableSong::setTrack
;
1241 return &MPD::MutableSong::setGenre
;
1243 return &MPD::MutableSong::setComposer
;
1245 return &MPD::MutableSong::setPerformer
;
1247 return &MPD::MutableSong::setDisc
;
1249 return &MPD::MutableSong::setComment
;
1255 std::string
GenerateFilename(const MPD::MutableSong
&s
, const std::string
&pattern
)
1257 std::string result
= s
.toString(pattern
);
1258 removeInvalidCharsFromFilename(result
);
1262 std::string
ParseFilename(MPD::MutableSong
&s
, std::string mask
, bool preview
)
1264 std::ostringstream result
;
1265 std::vector
<std::string
> separators
;
1266 std::vector
< std::pair
<char, std::string
> > tags
;
1267 std::string file
= s
.getName().substr(0, s
.getName().rfind("."));
1269 for (size_t i
= mask
.find("%"); i
!= std::string::npos
; i
= mask
.find("%"))
1271 tags
.push_back(std::make_pair(mask
.at(i
+1), ""));
1272 mask
= mask
.substr(i
+2);
1275 separators
.push_back(mask
.substr(0, i
));
1278 for (auto it
= separators
.begin(); it
!= separators
.end(); ++it
, ++i
)
1280 size_t j
= file
.find(*it
);
1281 tags
.at(i
).second
= file
.substr(0, j
);
1282 if (j
+it
->length() > file
.length())
1284 file
= file
.substr(j
+it
->length());
1288 if (i
>= tags
.size())
1290 tags
.at(i
).second
= file
;
1296 return "Error while parsing filename!\n";
1299 for (auto it
= tags
.begin(); it
!= tags
.end(); ++it
)
1301 for (std::string::iterator j
= it
->second
.begin(); j
!= it
->second
.end(); ++j
)
1307 MPD::MutableSong::SetFunction set
= IntoSetFunction(it
->first
);
1309 s
.setTags(set
, it
->second
);
1312 result
<< "%" << it
->first
<< ": " << it
->second
<< "\n";
1314 return result
.str();
1317 std::string
SongToString(const MPD::MutableSong
&s
)
1320 size_t i
= myTagEditor
->TagTypes
->choice();
1322 result
= (s
.*SongInfo::Tags
[i
].Get
)(0);
1324 result
= s
.getNewURI().empty() ? s
.getName() : s
.getName() + " -> " + s
.getNewURI();
1325 return result
.empty() ? Config
.empty_tag
: result
;
1328 bool DirEntryMatcher(const Regex
&rx
, const std::pair
<std::string
, std::string
> &dir
, bool filter
)
1330 if (dir
.first
== "." || dir
.first
== "..")
1332 return rx
.match(dir
.first
);
1335 bool SongEntryMatcher(const Regex
&rx
, const MPD::MutableSong
&s
)
1337 return rx
.match(SongToString(s
));