1 /***************************************************************************
2 * Copyright (C) 2008 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 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19 ***************************************************************************/
21 #include "tag_editor.h"
28 #include "textidentificationframe.h"
32 #include "status_checker.h"
36 extern ncmpcpp_keys Key
;
38 extern Connection
*Mpd
;
39 extern Menu
<Song
> *mPlaylist
;
41 extern Menu
<Buffer
> *mTagEditor
;
42 extern Window
*wFooter
;
47 const string patterns_list_file
= config_dir
+ "patterns.list";
48 vector
<string
> patterns_list
;
52 if (patterns_list
.empty())
54 std::ifstream
input(patterns_list_file
.c_str());
58 while (getline(input
, line
))
61 patterns_list
.push_back(line
);
68 void SavePatternList()
70 std::ofstream
output(patterns_list_file
.c_str());
73 for (vector
<string
>::const_iterator it
= patterns_list
.begin(); it
!= patterns_list
.end() && it
!= patterns_list
.begin()+30; it
++)
74 output
<< *it
<< std::endl
;
79 SongSetFunction
IntoSetFunction(char c
)
84 return &Song::SetArtist
;
86 return &Song::SetTitle
;
88 return &Song::SetAlbum
;
90 return &Song::SetYear
;
92 return &Song::SetTrack
;
94 return &Song::SetGenre
;
96 return &Song::SetComposer
;
98 return &Song::SetPerformer
;
100 return &Song::SetDisc
;
102 return &Song::SetComment
;
108 string
GenerateFilename(const Song
&s
, string
&pattern
)
110 string result
= s
.toString(pattern
);
111 EscapeUnallowedChars(result
);
115 string
ParseFilename(Song
&s
, string mask
, bool preview
)
117 std::stringstream result
;
118 vector
<string
> separators
;
119 vector
< std::pair
<char, string
> > tags
;
120 string file
= s
.GetName().substr(0, s
.GetName().find_last_of("."));
124 for (size_t i
= mask
.find("%"); i
!= string::npos
; i
= mask
.find("%"))
126 tags
.push_back(make_pair(mask
.at(i
+1), ""));
127 mask
= mask
.substr(i
+2);
130 separators
.push_back(mask
.substr(0, i
));
133 for (vector
<string
>::const_iterator it
= separators
.begin(); it
!= separators
.end(); it
++, i
++)
135 int j
= file
.find(*it
);
136 tags
.at(i
).second
= file
.substr(0, j
);
137 file
= file
.substr(j
+it
->length());
140 tags
.at(i
).second
= file
;
142 catch (std::out_of_range
)
144 return "Error while parsing filename!";
147 for (vector
< std::pair
<char, string
> >::iterator it
= tags
.begin(); it
!= tags
.end(); it
++)
149 for (string::iterator j
= it
->second
.begin(); j
!= it
->second
.end(); j
++)
155 SongSetFunction set
= IntoSetFunction(it
->first
);
157 (s
.*set
)(it
->second
);
160 result
<< "%" << it
->first
<< ": " << it
->second
<< "\n";
166 SongSetFunction
IntoSetFunction(mpd_TagItems tag
)
170 case MPD_TAG_ITEM_ARTIST
:
171 return &Song::SetArtist
;
172 case MPD_TAG_ITEM_ALBUM
:
173 return &Song::SetAlbum
;
174 case MPD_TAG_ITEM_TITLE
:
175 return &Song::SetTitle
;
176 case MPD_TAG_ITEM_TRACK
:
177 return &Song::SetTrack
;
178 case MPD_TAG_ITEM_GENRE
:
179 return &Song::SetGenre
;
180 case MPD_TAG_ITEM_DATE
:
181 return &Song::SetYear
;
182 case MPD_TAG_ITEM_COMPOSER
:
183 return &Song::SetComposer
;
184 case MPD_TAG_ITEM_PERFORMER
:
185 return &Song::SetPerformer
;
186 case MPD_TAG_ITEM_COMMENT
:
187 return &Song::SetComment
;
188 case MPD_TAG_ITEM_DISC
:
189 return &Song::SetDisc
;
190 case MPD_TAG_ITEM_FILENAME
:
191 return &Song::SetNewName
;
197 string
FindSharedDir(Menu
<Song
> *menu
)
200 for (size_t i
= 0; i
< menu
->Size(); i
++)
201 list
.push_back(&menu
->at(i
));
202 return FindSharedDir(list
);
205 string
FindSharedDir(const SongList
&v
)
210 result
= v
.front()->GetFile();
211 for (SongList::const_iterator it
= v
.begin()+1; it
!= v
.end(); it
++)
214 while (result
.substr(0, i
) == (*it
)->GetFile().substr(0, i
))
216 result
= result
.substr(0, i
);
218 size_t slash
= result
.find_last_of("/");
219 result
= slash
!= string::npos
? result
.substr(0, slash
) : "/";
224 void DisplayTag(const Song
&s
, void *data
, Menu
<Song
> *menu
)
226 switch (static_cast<Menu
<string
> *>(data
)->Choice())
229 *menu
<< ShowTag(s
.GetTitle());
232 *menu
<< ShowTag(s
.GetArtist());
235 *menu
<< ShowTag(s
.GetAlbum());
238 *menu
<< ShowTag(s
.GetYear());
241 *menu
<< ShowTag(s
.GetTrack());
244 *menu
<< ShowTag(s
.GetGenre());
247 *menu
<< ShowTag(s
.GetComposer());
250 *menu
<< ShowTag(s
.GetPerformer());
253 *menu
<< ShowTag(s
.GetDisc());
256 *menu
<< ShowTag(s
.GetComment());
259 if (s
.GetNewName().empty())
260 *menu
<< s
.GetName();
262 *menu
<< s
.GetName() << Config
.color2
<< " -> " << clEnd
<< s
.GetNewName();
269 void ReadTagsFromFile(mpd_Song
*s
)
271 TagLib::FileRef
f(s
->file
);
275 TagLib::MPEG::File
*mpegf
= dynamic_cast<TagLib::MPEG::File
*>(f
.file());
277 s
->artist
= !f
.tag()->artist().isEmpty() ? str_pool_get(f
.tag()->artist().toCString(UNICODE
)) : 0;
278 s
->title
= !f
.tag()->title().isEmpty() ? str_pool_get(f
.tag()->title().toCString(UNICODE
)) : 0;
279 s
->album
= !f
.tag()->album().isEmpty() ? str_pool_get(f
.tag()->album().toCString(UNICODE
)) : 0;
280 s
->track
= f
.tag()->track() ? str_pool_get(IntoStr(f
.tag()->track()).c_str()) : 0;
281 s
->date
= f
.tag()->year() ? str_pool_get(IntoStr(f
.tag()->year()).c_str()) : 0;
282 s
->genre
= !f
.tag()->genre().isEmpty() ? str_pool_get(f
.tag()->genre().toCString(UNICODE
)) : 0;
285 s
->composer
= !mpegf
->ID3v2Tag()->frameListMap()["TCOM"].isEmpty()
286 ? (!mpegf
->ID3v2Tag()->frameListMap()["TCOM"].front()->toString().isEmpty()
287 ? str_pool_get(mpegf
->ID3v2Tag()->frameListMap()["TCOM"].front()->toString().toCString(UNICODE
))
290 s
->performer
= !mpegf
->ID3v2Tag()->frameListMap()["TOPE"].isEmpty()
291 ? (!mpegf
->ID3v2Tag()->frameListMap()["TOPE"].front()->toString().isEmpty()
292 ? str_pool_get(mpegf
->ID3v2Tag()->frameListMap()["TOPE"].front()->toString().toCString(UNICODE
))
295 s
->disc
= !mpegf
->ID3v2Tag()->frameListMap()["TPOS"].isEmpty()
296 ? (!mpegf
->ID3v2Tag()->frameListMap()["TPOS"].front()->toString().isEmpty()
297 ? str_pool_get(mpegf
->ID3v2Tag()->frameListMap()["TPOS"].front()->toString().toCString(UNICODE
))
301 s
->comment
= !f
.tag()->comment().isEmpty() ? str_pool_get(f
.tag()->comment().toCString(UNICODE
)) : 0;
302 s
->time
= f
.audioProperties()->length();
305 bool GetSongTags(Song
&s
)
309 path_to_file
+= Config
.mpd_music_dir
;
310 path_to_file
+= s
.GetFile();
312 TagLib::FileRef
f(path_to_file
.c_str());
315 s
.SetComment(f
.tag()->comment().to8Bit(UNICODE
));
317 string ext
= s
.GetFile();
318 ext
= ext
.substr(ext
.find_last_of(".")+1);
324 mTagEditor
->ResizeBuffer(23);
326 for (size_t i
= 0; i
< 7; i
++)
327 mTagEditor
->Static(i
, 1);
329 mTagEditor
->IntoSeparator(7);
330 mTagEditor
->IntoSeparator(18);
331 mTagEditor
->IntoSeparator(20);
334 for (size_t i
= 14; i
<= 16; i
++)
335 mTagEditor
->Static(i
, 1);
337 mTagEditor
->Highlight(8);
339 mTagEditor
->at(0) << fmtBold
<< Config
.color1
<< "Song name: " << fmtBoldEnd
<< Config
.color2
<< s
.GetName() << clEnd
;
340 mTagEditor
->at(1) << fmtBold
<< Config
.color1
<< "Location in DB: " << fmtBoldEnd
<< Config
.color2
<< ShowTag(s
.GetDirectory()) << clEnd
;
341 mTagEditor
->at(3) << fmtBold
<< Config
.color1
<< "Length: " << fmtBoldEnd
<< Config
.color2
<< s
.GetLength() << clEnd
;
342 mTagEditor
->at(4) << fmtBold
<< Config
.color1
<< "Bitrate: " << fmtBoldEnd
<< Config
.color2
<< f
.audioProperties()->bitrate() << " kbps" << clEnd
;
343 mTagEditor
->at(5) << fmtBold
<< Config
.color1
<< "Sample rate: " << fmtBoldEnd
<< Config
.color2
<< f
.audioProperties()->sampleRate() << " Hz" << clEnd
;
344 mTagEditor
->at(6) << fmtBold
<< Config
.color1
<< "Channels: " << fmtBoldEnd
<< Config
.color2
<< (f
.audioProperties()->channels() == 1 ? "Mono" : "Stereo") << clDefault
;
346 mTagEditor
->at(8) << fmtBold
<< "Title:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetTitle());
347 mTagEditor
->at(9) << fmtBold
<< "Artist:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetArtist());
348 mTagEditor
->at(10) << fmtBold
<< "Album:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetAlbum());
349 mTagEditor
->at(11) << fmtBold
<< "Year:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetYear());
350 mTagEditor
->at(12) << fmtBold
<< "Track:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetTrack());
351 mTagEditor
->at(13) << fmtBold
<< "Genre:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetGenre());
352 mTagEditor
->at(14) << fmtBold
<< "Composer:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetComposer());
353 mTagEditor
->at(15) << fmtBold
<< "Performer:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetPerformer());
354 mTagEditor
->at(16) << fmtBold
<< "Disc:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetDisc());
355 mTagEditor
->at(17) << fmtBold
<< "Comment:" << fmtBoldEnd
<< ' ' << ShowTag(s
.GetComment());
357 mTagEditor
->at(19) << fmtBold
<< "Filename:" << fmtBoldEnd
<< ' ' << s
.GetName();
359 mTagEditor
->at(21) << "Save";
360 mTagEditor
->at(22) << "Cancel";
364 bool WriteTags(Song
&s
)
366 using namespace TagLib
;
368 bool file_is_from_db
= s
.IsFromDB();
370 path_to_file
+= Config
.mpd_music_dir
;
371 path_to_file
+= s
.GetFile();
372 FileRef
f(path_to_file
.c_str());
375 f
.tag()->setTitle(TO_WSTRING(s
.GetTitle()));
376 f
.tag()->setArtist(TO_WSTRING(s
.GetArtist()));
377 f
.tag()->setAlbum(TO_WSTRING(s
.GetAlbum()));
378 f
.tag()->setYear(StrToInt(s
.GetYear()));
379 f
.tag()->setTrack(StrToInt(s
.GetTrack()));
380 f
.tag()->setGenre(TO_WSTRING(s
.GetGenre()));
381 f
.tag()->setComment(TO_WSTRING(s
.GetComment()));
384 string ext
= s
.GetFile();
385 ext
= ext
.substr(ext
.find_last_of(".")+1);
389 MPEG::File
file(path_to_file
.c_str());
390 ID3v2::Tag
*tag
= file
.ID3v2Tag();
391 String::Type encoding
= UNICODE
? String::UTF8
: String::Latin1
;
392 ByteVector
Composer("TCOM");
393 ByteVector
Performer("TOPE");
394 ByteVector
Disc("TPOS");
395 ID3v2::Frame
*ComposerFrame
= new ID3v2::TextIdentificationFrame(Composer
, encoding
);
396 ID3v2::Frame
*PerformerFrame
= new ID3v2::TextIdentificationFrame(Performer
, encoding
);
397 ID3v2::Frame
*DiscFrame
= new ID3v2::TextIdentificationFrame(Disc
, encoding
);
398 ComposerFrame
->setText(TO_WSTRING(s
.GetComposer()));
399 PerformerFrame
->setText(TO_WSTRING(s
.GetPerformer()));
400 DiscFrame
->setText(TO_WSTRING(s
.GetDisc()));
401 tag
->removeFrames(Composer
);
402 tag
->addFrame(ComposerFrame
);
403 tag
->removeFrames(Performer
);
404 tag
->addFrame(PerformerFrame
);
405 tag
->removeFrames(Disc
);
406 tag
->addFrame(DiscFrame
);
409 if (!s
.GetNewName().empty())
413 old_name
+= Config
.mpd_music_dir
;
414 old_name
+= s
.GetFile();
417 new_name
+= Config
.mpd_music_dir
;
418 new_name
+= s
.GetDirectory() + "/" + s
.GetNewName();
419 if (rename(old_name
.c_str(), new_name
.c_str()) == 0 && !file_is_from_db
)
421 if (wPrev
== mPlaylist
)
423 // if we rename local file, it won't get updated
424 // so just remove it from playlist and add again
425 size_t pos
= mPlaylist
->Choice();
426 Mpd
->QueueDeleteSong(pos
);
428 int id
= Mpd
->AddSong("file://" + new_name
);
431 s
= mPlaylist
->Back();
432 Mpd
->Move(s
.GetPosition(), pos
);
435 else // only mBrowser
445 void __deal_with_filenames(SongList
&v
)
452 Menu
<string
> *Main
= new Menu
<string
>((COLS
-width
)/2, (LINES
-height
)/2, width
, height
, "", Config
.main_color
, Config
.window_border
);
453 Main
->SetTimeout(ncmpcpp_window_timeout
);
454 Main
->SetItemDisplayer(GenericDisplayer
);
455 Main
->AddOption("Get tags from filename");
456 Main
->AddOption("Rename files");
457 Main
->AddSeparator();
458 Main
->AddOption("Cancel");
462 while (!Keypressed(input
, Key
.Enter
))
466 Main
->ReadKey(input
);
467 if (Keypressed(input
, Key
.Down
))
469 else if (Keypressed(input
, Key
.Up
))
477 size_t choice
= Main
->Choice();
478 size_t one_width
= width
/2;
479 size_t two_width
= width
-one_width
;
484 Scrollpad
*Helper
= 0;
485 Scrollpad
*Legend
= 0;
486 Scrollpad
*Preview
= 0;
491 Legend
= new Scrollpad((COLS
-width
)/2+one_width
, (LINES
-height
)/2, two_width
, height
, "Legend", Config
.main_color
, Config
.window_border
);
492 Legend
->SetTimeout(ncmpcpp_window_timeout
);
493 *Legend
<< "%a - artist\n";
494 *Legend
<< "%t - title\n";
495 *Legend
<< "%b - album\n";
496 *Legend
<< "%y - year\n";
497 *Legend
<< "%n - track number\n";
498 *Legend
<< "%g - genre\n";
499 *Legend
<< "%c - composer\n";
500 *Legend
<< "%p - performer\n";
501 *Legend
<< "%d - disc\n";
502 *Legend
<< "%C - comment\n\n";
503 *Legend
<< fmtBold
<< "Files:\n" << fmtBoldEnd
;
504 for (SongList::const_iterator it
= v
.begin(); it
!= v
.end(); it
++)
505 *Legend
<< Config
.color2
<< " * " << clEnd
<< (*it
)->GetName() << "\n";
508 Preview
= Legend
->EmptyClone();
509 Preview
->SetTitle("Preview");
510 Preview
->SetTimeout(ncmpcpp_window_timeout
);
512 Main
= new Menu
<string
>((COLS
-width
)/2, (LINES
-height
)/2, one_width
, height
, "", Config
.main_color
, Config
.active_window_border
);
513 Main
->SetTimeout(ncmpcpp_window_timeout
);
514 Main
->SetItemDisplayer(GenericDisplayer
);
516 if (!patterns_list
.empty())
517 Config
.pattern
= patterns_list
.front();
518 Main
->AddOption("Pattern: " + Config
.pattern
);
519 Main
->AddOption("Preview");
520 Main
->AddOption("Legend");
521 Main
->AddSeparator();
522 Main
->AddOption("Proceed");
523 Main
->AddOption("Cancel");
524 if (!patterns_list
.empty())
526 Main
->AddSeparator();
527 Main
->AddOption("Recent patterns", 1, 1, 0);
528 Main
->AddSeparator();
529 for (vector
<string
>::const_iterator it
= patterns_list
.begin(); it
!= patterns_list
.end(); it
++)
530 Main
->AddOption(*it
);
536 Main
->SetTitle(!choice
? "Get tags from filename" : "Rename files");
544 Active
->ReadKey(input
);
546 if (Keypressed(input
, Key
.Up
))
548 else if (Keypressed(input
, Key
.Down
))
549 Active
->Scroll(wDown
);
550 else if (Keypressed(input
, Key
.PageUp
))
551 Active
->Scroll(wPageUp
);
552 else if (Keypressed(input
, Key
.PageDown
))
553 Active
->Scroll(wPageDown
);
554 else if (Keypressed(input
, Key
.Home
))
555 Active
->Scroll(wHome
);
556 else if (Keypressed(input
, Key
.End
))
557 Active
->Scroll(wEnd
);
558 else if (Keypressed(input
, Key
.Enter
) && Active
== Main
)
560 switch (Main
->RealChoice())
565 Statusbar() << "Pattern: ";
566 string new_pattern
= wFooter
->GetString(Config
.pattern
);
568 if (!new_pattern
.empty())
570 Config
.pattern
= new_pattern
;
571 Main
->at(0) = "Pattern: ";
572 Main
->at(0) += Config
.pattern
;
581 ShowMessage("Parsing...");
583 for (SongList::iterator it
= v
.begin(); it
!= v
.end(); it
++)
590 *Preview
<< fmtBold
<< s
.GetName() << ":\n" << fmtBoldEnd
;
591 *Preview
<< ParseFilename(s
, Config
.pattern
, preview
) << "\n";
594 ParseFilename(s
, Config
.pattern
, preview
);
598 const string
&file
= s
.GetName();
599 int last_dot
= file
.find_last_of(".");
600 string extension
= file
.substr(last_dot
);
601 basic_buffer
<my_char_t
> new_file
;
602 new_file
<< TO_WSTRING(GenerateFilename(s
, Config
.pattern
));
603 if (new_file
.Str().empty())
606 new_file
<< clRed
<< "!EMPTY!" << clEnd
;
609 ShowMessage("File '%s' would have an empty name!", s
.GetName().c_str());
615 s
.SetNewName(TO_STRING(new_file
.Str()) + extension
);
616 *Preview
<< file
<< Config
.color2
<< " -> " << clEnd
<< new_file
<< extension
<< "\n\n";
626 for (size_t i
= 0; i
< patterns_list
.size(); i
++)
628 if (patterns_list
[i
] == Config
.pattern
)
630 patterns_list
.erase(patterns_list
.begin()+i
);
634 patterns_list
.insert(patterns_list
.begin(), Config
.pattern
);
636 ShowMessage("Operation finished!");
658 Config
.pattern
= Main
->Current();
659 Main
->at(0) = "Pattern: " + Config
.pattern
;
663 else if (Keypressed(input
, Key
.VolumeUp
) && Active
== Main
)
665 Active
->SetBorder(Config
.window_border
);
668 Active
->SetBorder(Config
.active_window_border
);
671 else if (Keypressed(input
, Key
.VolumeDown
) && Active
== Helper
)
673 Active
->SetBorder(Config
.window_border
);
676 Active
->SetBorder(Config
.active_window_border
);