some improvements for handling mpd connection
[ncmpcpp/cirrus.git] / src / tag_editor.cpp
blobe0754434125f1dd36c1600e6fb19cb8fd8c05d5d
1 /***************************************************************************
2 * Copyright (C) 2008 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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 * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *
19 ***************************************************************************/
21 #include "tag_editor.h"
23 #ifdef HAVE_TAGLIB_H
25 #include <fstream>
27 #include "id3v2tag.h"
28 #include "textidentificationframe.h"
29 #include "mpegfile.h"
31 #include "helpers.h"
32 #include "status_checker.h"
34 using namespace MPD;
36 extern ncmpcpp_keys Key;
38 extern Connection *Mpd;
39 extern Menu<Song> *mPlaylist;
41 extern Menu<Buffer> *mTagEditor;
42 extern Window *wFooter;
43 extern Window *wPrev;
45 namespace
47 const string patterns_list_file = config_dir + "patterns.list";
48 vector<string> patterns_list;
50 void GetPatternList()
52 if (patterns_list.empty())
54 std::ifstream input(patterns_list_file.c_str());
55 if (input.is_open())
57 string line;
58 while (getline(input, line))
60 if (!line.empty())
61 patterns_list.push_back(line);
63 input.close();
68 void SavePatternList()
70 std::ofstream output(patterns_list_file.c_str());
71 if (output.is_open())
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;
75 output.close();
79 SongSetFunction IntoSetFunction(char c)
81 switch (c)
83 case 'a':
84 return &Song::SetArtist;
85 case 't':
86 return &Song::SetTitle;
87 case 'b':
88 return &Song::SetAlbum;
89 case 'y':
90 return &Song::SetYear;
91 case 'n':
92 return &Song::SetTrack;
93 case 'g':
94 return &Song::SetGenre;
95 case 'c':
96 return &Song::SetComposer;
97 case 'p':
98 return &Song::SetPerformer;
99 case 'd':
100 return &Song::SetDisc;
101 case 'C':
102 return &Song::SetComment;
103 default:
104 return NULL;
108 string GenerateFilename(const Song &s, string &pattern)
110 string result = s.toString(pattern);
111 EscapeUnallowedChars(result);
112 return 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);
128 i = mask.find("%");
129 if (!mask.empty())
130 separators.push_back(mask.substr(0, i));
132 int i = 0;
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());
139 if (!file.empty())
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++)
150 if (*j == '_')
151 *j = ' ';
153 if (!preview)
155 SongSetFunction set = IntoSetFunction(it->first);
156 if (set)
157 (s.*set)(it->second);
159 else
160 result << "%" << it->first << ": " << it->second << "\n";
162 return result.str();
166 SongSetFunction IntoSetFunction(mpd_TagItems tag)
168 switch (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;
192 default:
193 return NULL;
197 string FindSharedDir(Menu<Song> *menu)
199 SongList list;
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)
207 string result;
208 if (!v.empty())
210 result = v.front()->GetFile();
211 for (SongList::const_iterator it = v.begin()+1; it != v.end(); it++)
213 int i = 1;
214 while (result.substr(0, i) == (*it)->GetFile().substr(0, i))
215 i++;
216 result = result.substr(0, i);
218 size_t slash = result.find_last_of("/");
219 result = slash != string::npos ? result.substr(0, slash) : "/";
221 return result;
224 void DisplayTag(const Song &s, void *data, Menu<Song> *menu)
226 switch (static_cast<Menu<string> *>(data)->Choice())
228 case 0:
229 *menu << ShowTag(s.GetTitle());
230 return;
231 case 1:
232 *menu << ShowTag(s.GetArtist());
233 return;
234 case 2:
235 *menu << ShowTag(s.GetAlbum());
236 return;
237 case 3:
238 *menu << ShowTag(s.GetYear());
239 return;
240 case 4:
241 *menu << ShowTag(s.GetTrack());
242 return;
243 case 5:
244 *menu << ShowTag(s.GetGenre());
245 return;
246 case 6:
247 *menu << ShowTag(s.GetComposer());
248 return;
249 case 7:
250 *menu << ShowTag(s.GetPerformer());
251 return;
252 case 8:
253 *menu << ShowTag(s.GetDisc());
254 return;
255 case 9:
256 *menu << ShowTag(s.GetComment());
257 return;
258 case 11:
259 if (s.GetNewName().empty())
260 *menu << s.GetName();
261 else
262 *menu << s.GetName() << Config.color2 << " -> " << clEnd << s.GetNewName();
263 return;
264 default:
265 return;
269 void ReadTagsFromFile(mpd_Song *s)
271 TagLib::FileRef f(s->file);
272 if (f.isNull())
273 return;
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;
283 if (mpegf)
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))
288 : 0)
289 : 0;
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))
293 : 0)
294 : 0;
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))
298 : 0)
299 : 0;
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)
307 string path_to_file;
308 if (s.IsFromDB())
309 path_to_file += Config.mpd_music_dir;
310 path_to_file += s.GetFile();
312 TagLib::FileRef f(path_to_file.c_str());
313 if (f.isNull())
314 return false;
315 s.SetComment(f.tag()->comment().to8Bit(UNICODE));
317 string ext = s.GetFile();
318 ext = ext.substr(ext.find_last_of(".")+1);
319 ToLower(ext);
321 mTagEditor->Clear();
322 mTagEditor->Reset();
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);
333 if (ext != "mp3")
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";
361 return true;
364 bool WriteTags(Song &s)
366 using namespace TagLib;
367 string path_to_file;
368 bool file_is_from_db = s.IsFromDB();
369 if (file_is_from_db)
370 path_to_file += Config.mpd_music_dir;
371 path_to_file += s.GetFile();
372 FileRef f(path_to_file.c_str());
373 if (!f.isNull())
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()));
382 f.save();
384 string ext = s.GetFile();
385 ext = ext.substr(ext.find_last_of(".")+1);
386 ToLower(ext);
387 if (ext == "mp3")
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);
407 file.save();
409 if (!s.GetNewName().empty())
411 string old_name;
412 if (file_is_from_db)
413 old_name += Config.mpd_music_dir;
414 old_name += s.GetFile();
415 string new_name;
416 if (file_is_from_db)
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);
427 Mpd->CommitQueue();
428 int id = Mpd->AddSong("file://" + new_name);
429 if (id >= 0)
431 s = mPlaylist->Back();
432 Mpd->Move(s.GetPosition(), pos);
435 else // only mBrowser
436 s.SetFile(new_name);
439 return true;
441 else
442 return false;
445 void __deal_with_filenames(SongList &v)
447 int width = 30;
448 int height = 6;
450 GetPatternList();
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");
459 Main->Display();
461 int input = 0;
462 while (!Keypressed(input, Key.Enter))
464 TraceMpdStatus();
465 Main->Refresh();
466 Main->ReadKey(input);
467 if (Keypressed(input, Key.Down))
468 Main->Scroll(wDown);
469 else if (Keypressed(input, Key.Up))
470 Main->Scroll(wUp);
473 width = COLS*0.9;
474 height = LINES*0.8;
475 bool exit = 0;
476 bool preview = 1;
477 size_t choice = Main->Choice();
478 size_t one_width = width/2;
479 size_t two_width = width-one_width;
481 delete Main;
483 Main = 0;
484 Scrollpad *Helper = 0;
485 Scrollpad *Legend = 0;
486 Scrollpad *Preview = 0;
487 Window *Active = 0;
489 if (choice != 3)
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";
506 Legend->Flush();
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);
533 Active = Main;
534 Helper = Legend;
536 Main->SetTitle(!choice ? "Get tags from filename" : "Rename files");
537 Main->Display();
538 Helper->Display();
540 while (!exit)
542 TraceMpdStatus();
543 Active->Refresh();
544 Active->ReadKey(input);
546 if (Keypressed(input, Key.Up))
547 Active->Scroll(wUp);
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())
562 case 0:
564 LockStatusbar();
565 Statusbar() << "Pattern: ";
566 string new_pattern = wFooter->GetString(Config.pattern);
567 UnlockStatusbar();
568 if (!new_pattern.empty())
570 Config.pattern = new_pattern;
571 Main->at(0) = "Pattern: ";
572 Main->at(0) += Config.pattern;
574 break;
576 case 3: // save
577 preview = 0;
578 case 1:
580 bool success = 1;
581 ShowMessage("Parsing...");
582 Preview->Clear();
583 for (SongList::iterator it = v.begin(); it != v.end(); it++)
585 Song &s = **it;
586 if (!choice)
588 if (preview)
590 *Preview << fmtBold << s.GetName() << ":\n" << fmtBoldEnd;
591 *Preview << ParseFilename(s, Config.pattern, preview) << "\n";
593 else
594 ParseFilename(s, Config.pattern, preview);
596 else
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())
605 if (preview)
606 new_file << clRed << "!EMPTY!" << clEnd;
607 else
609 ShowMessage("File '%s' would have an empty name!", s.GetName().c_str());
610 success = 0;
611 break;
614 if (!preview)
615 s.SetNewName(TO_STRING(new_file.Str()) + extension);
616 *Preview << file << Config.color2 << " -> " << clEnd << new_file << extension << "\n\n";
619 if (!success)
621 preview = 1;
622 continue;
624 else
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);
631 i--;
634 patterns_list.insert(patterns_list.begin(), Config.pattern);
636 ShowMessage("Operation finished!");
637 if (preview)
639 Helper = Preview;
640 Helper->Flush();
641 Helper->Display();
642 break;
645 case 4:
647 exit = 1;
648 break;
650 case 2:
652 Helper = Legend;
653 Helper->Display();
654 break;
656 default:
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);
666 Active->Display();
667 Active = Helper;
668 Active->SetBorder(Config.active_window_border);
669 Active->Display();
671 else if (Keypressed(input, Key.VolumeDown) && Active == Helper)
673 Active->SetBorder(Config.window_border);
674 Active->Display();
675 Active = Main;
676 Active->SetBorder(Config.active_window_border);
677 Active->Display();
681 SavePatternList();
682 delete Main;
683 delete Legend;
684 delete Preview;
687 #endif