1 /*
2 * Cantata
3 *
4 * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5 *
6 * ----
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
22 */
23
24 #include "tageditor.h"
25 #include "tags.h"
26 #include "widgets/tagspinbox.h"
27 #include "mpd-interface/mpdconnection.h"
28 #include "gui/settings.h"
29 #include "support/messagebox.h"
30 #include "support/inputdialog.h"
31 #include "trackorganiser.h"
32 #include "mpd-interface/cuefile.h"
33 #include "support/utils.h"
34 #include "support/monoicon.h"
35 #ifdef ENABLE_DEVICES_SUPPORT
36 #include "models/devicesmodel.h"
37 #include "devices/device.h"
38 #endif
39 #include <QMenu>
40 #include <QCloseEvent>
41 #include <QCoreApplication>
42 #include <QEventLoop>
43 #include <QDir>
44 #include <QTimer>
45 #include <algorithm>
46
47 #define REMOVE(w) \
48 w->setVisible(false); \
49 w->deleteLater(); \
50 w=0;
51
52 //
53 // NOTE: Cantata does NOT read, and store, comments from MPD. For comment support, Cantata will read these from the
54 // files when the tag editor is opened.
55 //
equalTags(const Song & a,const Song & b,bool compareCommon,bool composerSupport,bool commentSupport)56 static bool equalTags(const Song &a, const Song &b, bool compareCommon, bool composerSupport, bool commentSupport)
57 {
58 return (compareCommon || a.track==b.track) && a.year==b.year && a.disc==b.disc &&
59 a.artist==b.artist && a.genres[0]==b.genres[0] && a.album==b.album &&
60 a.albumartist==b.albumartist && (!composerSupport || a.composer()==b.composer()) &&
61 (!commentSupport || a.comment()==b.comment()) && (compareCommon || a.title==b.title);
62 }
63
setString(QString & str,const QString & v,bool skipEmpty)64 static bool setString(QString &str, const QString &v, bool skipEmpty) {
65 if (!skipEmpty || !v.isEmpty()) {
66 str=v;
67 return true;
68 }
69 return false;
70 }
71
trim(QString str)72 static QString trim(QString str) {
73 str=str.trimmed();
74 str=str.simplified();
75 str=str.replace(QLatin1String(" ;"), QLatin1String(";"));
76 str=str.replace(QLatin1String("; "), QLatin1String(";"));
77 return str;
78 }
79
80 // Split genres back out
splitGenres(Song & song)81 static void splitGenres(Song &song) {
82 QStringList genres=song.genres[0].split(",", QString::SkipEmptyParts);
83 for (int i=0; i<Song::constNumGenres; ++i) {
84 song.genres[i]=i<genres.count() ? genres.at(i).trimmed() : QString();
85 }
86 }
87
88 static int iCount=0;
89
instanceCount()90 int TagEditor::instanceCount()
91 {
92 return iCount;
93 }
94
nullRating(const quint8 & rating)95 static inline bool nullRating(const quint8 &rating)
96 {
97 return rating>Song::Rating_Max;
98 }
99
nullRating(const Song & s)100 static inline bool nullRating(const Song &s)
101 {
102 return nullRating(s.rating);
103 }
104
TagEditor(QWidget * parent,const QList<Song> & songs,const QSet<QString> & existingArtists,const QSet<QString> & existingAlbumArtists,const QSet<QString> & existingComposers,const QSet<QString> & existingAlbums,const QSet<QString> & existingGenres,const QString & udi)105 TagEditor::TagEditor(QWidget *parent, const QList<Song> &songs,
106 const QSet<QString> &existingArtists, const QSet<QString> &existingAlbumArtists, const QSet<QString> &existingComposers,
107 const QSet<QString> &existingAlbums, const QSet<QString> &existingGenres, const QString &udi)
108 : SongDialog(parent)
109 #ifdef ENABLE_DEVICES_SUPPORT
110 , deviceUdi(udi)
111 #endif
112 , currentSongIndex(-1)
113 , updating(false)
114 , haveArtists(false)
115 , haveAlbumArtists(false)
116 , haveComposers(false)
117 , haveComments(false)
118 , haveAlbums(false)
119 , haveGenres(false)
120 , haveDiscs(false)
121 , haveYears(false)
122 , haveRatings(false)
123 , saving(false)
124 , composerSupport(false)
125 , commentSupport(false)
126 , readRatingsAct(nullptr)
127 , writeRatingsAct(nullptr)
128 {
129 iCount++;
130 bool ratingsSupport=false;
131 #ifdef ENABLE_DEVICES_SUPPORT
132 if (deviceUdi.isEmpty()) {
133 baseDir=MPDConnection::self()->getDetails().dir;
134 composerSupport=MPDConnection::self()->composerTagSupported();
135 commentSupport=MPDConnection::self()->commentTagSupported();
136 ratingsSupport=MPDConnection::self()->stickersSupported();
137 } else {
138 Device *dev=getDevice(udi, parentWidget());
139
140 if (!dev) {
141 deleteLater();
142 return;
143 }
144
145 baseDir=dev->path();
146 }
147 #else
148 baseDir=MPDConnection::self()->getDetails().dir;
149 composerSupport=MPDConnection::self()->composerTagSupported();
150 commentSupport=MPDConnection::self()->commentTagSupported();
151 ratingsSupport=MPDConnection::self()->stickersSupported();
152 #endif
153
154 for (const Song &s: songs) {
155 if (CueFile::isCue(s.file)) {
156 continue;
157 }
158 Song song(s);
159 song.rating=Song::Rating_Null;
160 if (s.guessed) {
161 song.revertGuessedTags();
162 }
163 // Store all Genres's in 1st genre
164 song.genres[0]=song.displayGenre();
165 song.genres[1]=QString();
166 original.append(song);
167 if (s.isLocalFile()) {
168 ratingsSupport = false;
169 composerSupport = true;
170 commentSupport = false;
171 baseDir = QString();
172 }
173 }
174
175 if (original.isEmpty()) {
176 deleteLater();
177 return;
178 }
179 std::sort(original.begin(), original.end());
180
181 if (!songsOk(original, baseDir, udi.isEmpty())) {
182 return;
183 }
184
185 QWidget *mainWidet = new QWidget(this);
186 setupUi(mainWidet);
187 if (!ratingsSupport) {
188 REMOVE(ratingWidget);
189 REMOVE(ratingLabel);
190 REMOVE(ratingVarious);
191 REMOVE(ratingNoteLabel);
192 } else {
193 connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString)));
194 connect(this, SIGNAL(setRating(QString,quint8)), MPDConnection::self(), SLOT(setRating(QString,quint8)));
195 connect(MPDConnection::self(), SIGNAL(rating(QString,quint8)), this, SLOT(rating(QString,quint8)));
196 ratingWidget->setShowZeroForNull(true);
197 QColor col=palette().color(QPalette::WindowText);
198 ratingVarious->setStyleSheet(QString("QLabel{color:rgba(%1,%2,%3,128);}").arg(col.red()).arg(col.green()).arg(col.blue()));
199 }
200 setMainWidget(mainWidet);
201 ButtonCodes buttons=Ok|Cancel|Reset|User3;
202 if (songs.count()>1) {
203 buttons|=User2|User1;
204 }
205 setButtons(buttons);
206 setCaption(tr("Tags"));
207 if (songs.count()>1) {
208 setButtonGuiItem(User2, StdGuiItem::back(true));
209 setButtonGuiItem(User1,StdGuiItem::forward(true));
210 enableButton(User1, false);
211 enableButton(User2, false);
212 }
213 setButtonGuiItem(Ok, StdGuiItem::save());
214 setButtonGuiItem(User3, GuiItem(tr("Tools"), FontAwesome::magic));
215 QMenu *toolsMenu=new QMenu(this);
216 toolsMenu->addAction(tr("Apply \"Various Artists\" Workaround"), this, SLOT(applyVa()));
217 toolsMenu->addAction(tr("Revert \"Various Artists\" Workaround"), this, SLOT(revertVa()));
218 toolsMenu->addAction(tr("Set 'Album Artist' from 'Artist'"), this, SLOT(setAlbumArtistFromArtist()));
219 toolsMenu->addAction(tr("Capitalize"), this, SLOT(capitalise()));
220 toolsMenu->addAction(tr("Adjust Track Numbers"), this, SLOT(adjustTrackNumbers()));
221 if (ratingsSupport) {
222 readRatingsAct=toolsMenu->addAction(tr("Read Ratings from File"), this, SLOT(readRatings()));
223 writeRatingsAct=toolsMenu->addAction(tr("Write Ratings to File"), this, SLOT(writeRatings()));
224 readRatingsAct->setEnabled(false);
225 writeRatingsAct->setEnabled(false);
226 }
227 setButtonMenu(User3, toolsMenu, InstantPopup);
228 enableButton(Ok, false);
229 enableButton(Reset, false);
230
231 setAttribute(Qt::WA_DeleteOnClose);
232
233 QStringList strings=existingArtists.toList();
234 strings.sort();
235 artist->clear();
236 artist->insertItems(0, strings);
237
238 strings=existingAlbumArtists.toList();
239 strings.sort();
240 albumArtist->clear();
241 albumArtist->insertItems(0, strings);
242
243 if (composerSupport) {
244 strings=existingComposers.toList();
245 strings.sort();
246 composer->clear();
247 composer->insertItems(0, strings);
248 } else {
249 REMOVE(composer)
250 REMOVE(composerLabel)
251 }
252
253 if (commentSupport) {
254 comment->clear();
255 composer->insertItems(0, strings);
256 } else {
257 REMOVE(comment)
258 REMOVE(commentLabel)
259 }
260
261 strings=existingAlbums.toList();
262 strings.sort();
263 album->clear();
264 album->insertItems(0, strings);
265
266 strings=existingGenres.toList();
267 strings.sort();
268 genre->clear();
269 genre->insertItems(0, strings);
270
271 trackName->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
272 trackName->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength);
273 trackName->view()->setTextElideMode(Qt::ElideLeft);
274
275 if (original.count()>1) {
276 QSet<QString> songArtists;
277 QSet<QString> songAlbumArtists;
278 QSet<QString> songAlbums;
279 QSet<QString> songGenres;
280 QSet<QString> songComposers;
281 QSet<QString> songComments;
282 QSet<int> songYears;
283 QSet<int> songDiscs;
284
285 for (const Song &s: original) {
286 songArtists.insert(s.artist);
287 songAlbumArtists.insert(s.albumartist);
288 songAlbums.insert(s.album);
289 for (int i=0; i<Song::constNumGenres && !s.genres[i].isEmpty(); ++i) {
290 songGenres.insert(s.genres[i]);
291 }
292 if (composerSupport) {
293 songComposers.insert(s.composer());
294 }
295 if (commentSupport) {
296 songComments.insert(s.comment());
297 }
298 songYears.insert(s.year);
299 songDiscs.insert(s.disc);
300 if (songArtists.count()>1 && songAlbumArtists.count()>1 && songAlbums.count()>1 &&
301 songGenres.count()>1 && songYears.count()>1 && songDiscs.count()>1 &&
302 (!composerSupport || songComposers.count()>1) && (!commentSupport || songComments.count()>1)) {
303 break;
304 }
305 }
306 Song all;
307 all.file.clear();
308 all.title.clear();
309 all.track=0;
310 all.artist=1==songArtists.count() ? *(songArtists.begin()) : QString();
311 if (1==songComposers.count()) {
312 all.setComposer(*(songComposers.begin()));
313 }
314 all.setComment(1==songComments.count() ? *(songComments.begin()) : QString());
315 all.albumartist=1==songAlbumArtists.count() ? *(songAlbumArtists.begin()) : QString();
316 all.album=1==songAlbums.count() ? *(songAlbums.begin()) : QString();
317 all.genres[0]=1==songGenres.count() ? *(songGenres.begin()) : QString();
318 all.year=1==songYears.count() ? *(songYears.begin()) : 0;
319 all.disc=1==songDiscs.count() ? *(songDiscs.begin()) : 0;
320 all.rating=Song::Rating_Null;
321 original.prepend(all);
322 artist->setFocus();
323 haveArtists=songArtists.count()>1;
324 haveAlbumArtists=songAlbumArtists.count()>1;
325 haveAlbums=songAlbums.count()>1;
326 haveGenres=songGenres.count()>1;
327 haveComposers=songComposers.count()>1;
328 haveDiscs=songDiscs.count()>1;
329 haveYears=songYears.count()>1;
330 } else {
331 title->setFocus();
332 }
333 edited=original;
334 setIndex(0);
335
336 if (ratingsSupport || commentSupport) {
337 progress->setVisible(true);
338 progress->setRange(0, (original.count()>1 ? (original.count()-1) : 1)*(commentSupport && ratingsSupport ? 2 : 1));
339 progress->setValue(0);
340 } else {
341 progress->setVisible(false);
342 }
343
344 bool first=original.count()>1;
345 for (const Song &s: original) {
346 if (first) {
347 trackName->insertItem(trackName->count(), tr("All tracks"));
348 first=false;
349 } else {
350 trackName->insertItem(trackName->count(), s.filePath());
351 if (ratingsSupport) {
352 emit getRating(s.file);
353 }
354 }
355 }
356 connect(title, SIGNAL(textChanged(const QString &)), SLOT(checkChanged()));
357 connect(artist, SIGNAL(activated(int)), SLOT(checkChanged()));
358 connect(artist, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
359 connect(albumArtist, SIGNAL(activated(int)), SLOT(checkChanged()));
360 connect(albumArtist, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
361 if (composerSupport) {
362 connect(composer, SIGNAL(activated(int)), SLOT(checkChanged()));
363 connect(composer, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
364 }
365 if (commentSupport) {
366 connect(comment, SIGNAL(textChanged(const QString &)), SLOT(checkChanged()));
367 }
368 connect(album, SIGNAL(activated(int)), SLOT(checkChanged()));
369 connect(album, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
370 connect(genre, SIGNAL(activated(int)), SLOT(checkChanged()));
371 connect(track, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
372 connect(disc, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
373 connect(genre, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
374 connect(year, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
375 connect(trackName, SIGNAL(activated(int)), SLOT(setIndex(int)));
376 connect(this, SIGNAL(update()), MPDConnection::self(), SLOT(updateMaybe()));
377 if (ratingWidget) {
378 connect(ratingWidget, SIGNAL(valueChanged(int)), SLOT(checkRating()));
379 }
380 adjustSize();
381 int w=Utils::scaleForDpi(600);
382 if (width()<w) {
383 resize(w, height());
384 }
385 if (commentSupport) {
386 QTimer::singleShot(0, this, SLOT(readComments()));
387 }
388 trackName->lineEdit()->setReadOnly(true);
389 }
390
~TagEditor()391 TagEditor::~TagEditor()
392 {
393 iCount--;
394 }
395
fillSong(Song & s,bool isAll,bool skipEmpty) const396 void TagEditor::fillSong(Song &s, bool isAll, bool skipEmpty) const
397 {
398 Song all=original.at(0);
399 bool haveAll=original.count()>1;
400
401 if (!isAll) {
402 setString(s.title, title->text().trimmed(), skipEmpty);
403 }
404 setString(s.artist, artist->text().trimmed(), skipEmpty && (!haveAll || all.artist.isEmpty()));
405 setString(s.album, album->text().trimmed(), skipEmpty && (!haveAll || all.album.isEmpty()));
406 setString(s.albumartist, albumArtist->text().trimmed(), skipEmpty && (!haveAll || all.albumartist.isEmpty()));
407 if (composerSupport) {
408 QString str;
409 if (setString(str, composer->text().trimmed(), skipEmpty && (!haveAll || all.composer().isEmpty()))) {
410 s.setComposer(str);
411 }
412 }
413 if (commentSupport) {
414 QString str;
415 if (setString(str, comment->text().trimmed(), skipEmpty && (!haveAll || all.comment().isEmpty()))) {
416 s.setComment(str);
417 }
418 }
419 if (!isAll) {
420 s.track=track->value();
421 }
422 if (!isAll || 0!=disc->value()) {
423 s.disc=disc->value();
424 }
425 setString(s.genres[0], trim(genre->text()), skipEmpty && (!haveAll || all.genres[0].isEmpty()));
426 if (!isAll || 0!=year->value()) {
427 s.year=year->value();
428 }
429 if (ratingWidget) {
430 s.rating=ratingWidget->value();
431 }
432 }
433
setVariousHint()434 void TagEditor::setVariousHint()
435 {
436 if (0==currentSongIndex && original.count()>1) {
437 Song all=original.at(0);
438 artist->setPlaceholderText(all.artist.isEmpty() && haveArtists ? TagSpinBox::variousStr() : QString());
439 album->setPlaceholderText(all.album.isEmpty() && haveAlbums ? TagSpinBox::variousStr() : QString());
440 albumArtist->setPlaceholderText(all.albumartist.isEmpty() && haveAlbumArtists ? TagSpinBox::variousStr() : QString());
441 if (composerSupport) {
442 composer->setPlaceholderText(all.composer().isEmpty() && haveComposers ? TagSpinBox::variousStr() : QString());
443 }
444 if (commentSupport) {
445 comment->setPlaceholderText(all.comment().isEmpty() && haveComments ? TagSpinBox::variousStr() : QString());
446 }
447 genre->setPlaceholderText(all.genres[0].isEmpty() && haveGenres ? TagSpinBox::variousStr() : QString());
448 disc->setVarious(0==all.disc && haveDiscs);
449 year->setVarious(0==all.year && haveYears);
450 if (ratingVarious) {
451 ratingVarious->setVisible(nullRating(all) && haveRatings);
452 }
453 } else {
454 artist->setPlaceholderText(QString());
455 album->setPlaceholderText(QString());
456 albumArtist->setPlaceholderText(QString());
457 if (composerSupport) {
458 composer->setPlaceholderText(QString());
459 }
460 if (commentSupport) {
461 comment->setPlaceholderText(QString());
462 }
463 genre->setPlaceholderText(QString());
464 disc->setVarious(false);
465 year->setVarious(false);
466 if (ratingVarious) {
467 ratingVarious->setVisible(false);
468 }
469 }
470 }
471
enableOkButton()472 void TagEditor::enableOkButton()
473 {
474 enableButton(Ok, (editedIndexes.count()>1) ||
475 (1==original.count() && 1==editedIndexes.count()) ||
476 (1==editedIndexes.count() && !editedIndexes.contains(0)) );
477 enableButton(Reset, isButtonEnabled(Ok));
478 }
479
setLabelStates()480 void TagEditor::setLabelStates()
481 {
482 Song o=original.at(currentSongIndex);
483 Song e=edited.at(currentSongIndex);
484 bool isAll=0==currentSongIndex && original.count()>1;
485
486 titleLabel->setOn(!isAll && o.title!=e.title);
487 artistLabel->setOn(o.artist!=e.artist);
488 if (composerSupport) {
489 composerLabel->setOn(o.composer()!=e.composer());
490 }
491 if (commentSupport) {
492 commentLabel->setOn(o.comment()!=e.comment());
493 }
494 albumArtistLabel->setOn(o.albumartist!=e.albumartist);
495 albumLabel->setOn(o.album!=e.album);
496 trackLabel->setOn(!isAll && o.track!=e.track);
497 discLabel->setOn(o.disc!=e.disc);
498 genreLabel->setOn(o.genres[0]!=e.genres[0]);
499 yearLabel->setOn(o.year!=e.year);
500 if (ratingLabel) {
501 ratingLabel->setOn(o.rating<=Song::Rating_Max &&
502 e.rating<=Song::Rating_Max &&
503 o.rating!=e.rating);
504 }
505 }
506
readComments()507 void TagEditor::readComments()
508 {
509 bool haveMultiple=original.count()>1;
510 bool updated=false;
511 bool multipleComments=false;
512 QString allComment;
513
514 for (int i=0; i<original.count(); ++i) {
515 if (0!=i || !haveMultiple) {
516 progress->setValue(progress->value()+1);
517 }
518 if (i && 0==i%10) {
519 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
520 }
521
522 if (0==i && haveMultiple) {
523 continue;
524 }
525 Song song=original.at(i);
526 QString comment=Tags::readComment(song.filePath(baseDir));
527 if (!comment.isEmpty()) {
528 song.setComment(comment);
529 original.replace(i, song);
530 haveComments=true;
531 updated=true;
532 if (haveMultiple) {
533 if (allComment.isEmpty()) {
534 allComment=comment;
535 } else if (comment!=allComment) {
536 multipleComments=true;
537 }
538 }
539 }
540 }
541 if (updated) {
542 edited=original;
543 if (haveMultiple && !allComment.isEmpty() && !multipleComments) {
544 edited[0].setComment(allComment);
545 original[0].setComment(allComment);
546 if (0==currentSongIndex) {
547 comment->setText(allComment);
548 }
549 }
550 }
551 controlInitialActionsState();
552 if (haveMultiple) {
553 setVariousHint();
554 } else {
555 setSong(original.at(0));
556 }
557 }
558
applyVa()559 void TagEditor::applyVa()
560 {
561 bool isAll=0==currentSongIndex && original.count()>1;
562
563 if (MessageBox::No==MessageBox::questionYesNo(this, (isAll ? tr("Apply \"Various Artists\" workaround to <b>all</b> tracks?")
564 : tr("Apply \"Various Artists\" workaround?"))+
565 QLatin1String("<br/><br/>")+
566 tr("<i>This will set 'Album artist' and 'Artist' to "
567 "\"Various Artists\", and set 'Title' to "
568 "\"TrackArtist - TrackTitle\"</i>"), tr("Apply \"Various Artists\" Workaround"),
569 StdGuiItem::apply(), StdGuiItem::cancel())) {
570 return;
571 }
572
573 if (isAll) {
574 updating=true;
575 for (int i=0; i<edited.count(); ++i) {
576 Song s=edited.at(i);
577 if (s.fixVariousArtists()) {
578 if (0==i && QLatin1String(" - ")==s.title) {
579 s.title=QString();
580 }
581 edited.replace(i, s);
582 updateEditedStatus(i);
583 if (i==currentSongIndex) {
584 setSong(s);
585 }
586 }
587 }
588 updating=false;
589 setLabelStates();
590 enableOkButton();
591 } else {
592 Song s=edited.at(currentSongIndex);
593 if (s.fixVariousArtists()) {
594 edited.replace(currentSongIndex, s);
595 updateEditedStatus(currentSongIndex);
596 setSong(s);
597 }
598 }
599 }
600
revertVa()601 void TagEditor::revertVa()
602 {
603 bool isAll=0==currentSongIndex && original.count()>1;
604
605 if (MessageBox::No==MessageBox::questionYesNo(this, (isAll ? tr("Revert \"Various Artists\" workaround on <b>all</b> tracks?")
606 : tr("Revert \"Various Artists\" workaround"))+
607 QLatin1String("<br/><br/>")+
608 tr("<i>Where the 'Album artist' is the same as 'Artist' "
609 "and the 'Title' is of the format \"TrackArtist - TrackTitle\", "
610 "'Artist' will be taken from 'Title' and 'Title' itself will be "
611 "set to just the title. e.g. <br/><br/>"
612 "If 'Title' is \"Wibble - Wobble\", then 'Artist' will be set to "
613 "\"Wibble\" and 'Title' will be set to \"Wobble\"</i>"), tr("Revert \"Various Artists\" Workaround"),
614 GuiItem(tr("Revert")), StdGuiItem::cancel())) {
615 return;
616 }
617
618 if (isAll) {
619 updating=true;
620 QSet<QString> artists;
621 for (int i=1; i<edited.count(); ++i) {
622 Song s=edited.at(i);
623 if (s.revertVariousArtists()) {
624 artists.insert(s.artist);
625 edited.replace(i, s);
626 updateEditedStatus(i);
627 if (i==currentSongIndex) {
628 setSong(s);
629 }
630 }
631 }
632 Song s=edited.at(0);
633 s.artist=artists.count()!=1 ? QString() : *artists.constBegin();
634 edited.replace(0, s);
635 updateEditedStatus(0);
636 setSong(s);
637 updating=false;
638 setLabelStates();
639 enableOkButton();
640 } else {
641 Song s=edited.at(currentSongIndex);
642 if (s.revertVariousArtists()) {
643 edited.replace(currentSongIndex, s);
644 updateEditedStatus(currentSongIndex);
645 setSong(s);
646 enableOkButton();
647 }
648 }
649 }
650
setAlbumArtistFromArtist()651 void TagEditor::setAlbumArtistFromArtist()
652 {
653 bool isAll=0==currentSongIndex && original.count()>1;
654
655 if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Set 'Album Artist' from 'Artist' (if 'Album Artist' is empty) for <b>all</b> tracks?")
656 : tr("Set 'Album Artist' from 'Artist' (if 'Album Artist' is empty)?"),
657 tr("Album Artist from Artist"), StdGuiItem::apply(), StdGuiItem::cancel())) {
658 return;
659 }
660
661 if (isAll) {
662 updating=true;
663 for (int i=0; i<edited.count(); ++i) {
664 Song s=edited.at(i);
665 if (s.setAlbumArtist()) {
666 edited.replace(i, s);
667 updateEditedStatus(i);
668 if (i==currentSongIndex) {
669 setSong(s);
670 }
671 }
672 }
673 updating=false;
674 setLabelStates();
675 enableOkButton();
676 } else {
677 Song s=edited.at(currentSongIndex);
678 if (s.setAlbumArtist()) {
679 edited.replace(currentSongIndex, s);
680 updateEditedStatus(currentSongIndex);
681 setSong(s);
682 }
683 }
684 }
685
capitalise()686 void TagEditor::capitalise()
687 {
688 bool isAll=0==currentSongIndex && original.count()>1;
689
690 if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Capitalize the first letter of text fields (e.g. 'Title', 'Artist', etc) "
691 "of <b>all</b> tracks?")
692 : tr("Capitalize the first letter of text fields (e.g. 'Title', 'Artist', etc)?"),
693 tr("Capitalize"), GuiItem(tr("Capitalize")), StdGuiItem::cancel())) {
694 return;
695 }
696
697 if (isAll) {
698 for (int i=0; i<edited.count(); ++i) {
699 Song s=edited.at(i);
700 if (s.capitalise()) {
701 edited.replace(i, s);
702 updateEditedStatus(i);
703 if (i==currentSongIndex) {
704 setSong(s);
705 }
706 }
707 }
708 } else {
709 Song s=edited.at(currentSongIndex);
710 if (s.capitalise()) {
711 edited.replace(currentSongIndex, s);
712 updateEditedStatus(currentSongIndex);
713 setSong(s);
714 }
715 }
716 }
717
adjustTrackNumbers()718 void TagEditor::adjustTrackNumbers()
719 {
720 bool isAll=0==currentSongIndex && original.count()>1;
721 bool ok=false;
722 int adj=InputDialog::getInteger(tr("Adjust Track Numbers"), isAll ? tr("Adjust the value of each track number by:")
723 : tr("Adjust track number by:"),
724 0, -500, 500, 1, 10, &ok, this);
725
726 if (!ok || 0==adj) {
727 return;
728 }
729
730 if (isAll) {
731 for (int i=1; i<edited.count(); ++i) {
732 Song s=edited.at(i);
733 s.track+=adj;
734 edited.replace(i, s);
735 updateEditedStatus(i);
736 if (i==currentSongIndex) {
737 setSong(s);
738 }
739 }
740 } else {
741 Song s=edited.at(currentSongIndex);
742 s.track+=adj;
743 edited.replace(currentSongIndex, s);
744 updateEditedStatus(currentSongIndex);
745 setSong(s);
746 }
747 }
748
readRatings()749 void TagEditor::readRatings()
750 {
751 bool isAll=0==currentSongIndex && original.count()>1;
752
753 if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Read ratings for all tracks from the music files?")
754 : tr("Read rating from music file?"),
755 tr("Ratings"),
756 isAll ? GuiItem(tr("Read Ratings")) : GuiItem(tr("Read Rating")),
757 StdGuiItem::cancel())) {
758 return;
759 }
760
761 if (isAll) {
762 progress->setVisible(true);
763 progress->setRange(0, original.count());
764 QStringList updated;
765 for (int i=1; i<original.count(); ++i) {
766 progress->setValue(i+1);
767 if (i && 0==i%10) {
768 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
769 }
770 Song s=edited.at(i);
771 int r=Tags::readRating(s.filePath(baseDir));
772 if (r>=0 && r<=Song::Rating_Max && s.rating!=r) {
773 s.rating=r;
774 edited.replace(i, s);
775 updated.append(s.file);
776 editedIndexes.insert(i);
777 updateTrackName(i, true);
778 if (i==currentSongIndex) {
779 setSong(s);
780 }
781 }
782 }
783 progress->setVisible(false);
784 if (!updated.isEmpty()) {
785 MessageBox::informationList(this, tr("Read, and updated, ratings from the following tracks:"), updated);
786 }
787 } else {
788 Song s=edited.at(currentSongIndex);
789 int r=Tags::readRating(s.filePath(baseDir));
790 if (r>=0 && r<=Song::Rating_Max && s.rating!=r) {
791 s.rating=r;
792 edited.replace(currentSongIndex, s);
793 setSong(s);
794 }
795 }
796 enableOkButton();
797 }
798
writeRatings()799 void TagEditor::writeRatings()
800 {
801 bool isAll=0==currentSongIndex && original.count()>1;
802
803 if (isAll) {
804 for (int i=1; i<original.count(); ++i) {
805 if (nullRating(original.at(i))) {
806 MessageBox::error(this, tr("Not all Song ratings have been read from MPD!")+QLatin1String("\n\n")+
807 tr("Song ratings are not stored in the song files, but within MPD's 'sticker' database. "
808 "In order to save these into the actual file, Cantata must first read them from MPD."));
809 return;
810 }
811 }
812 } else {
813 if (nullRating(original.at(currentSongIndex))) {
814 MessageBox::error(this, tr("Song rating has not been read from MPD!")+QLatin1String("\n\n")+
815 tr("Song ratings are not stored in the song files, but within MPD's 'sticker' database. "
816 "In order to save these into the actual file, Cantata must first read them from MPD."));
817 return;
818 }
819 }
820
821 if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Write ratings for all tracks to the music files?")
822 : tr("Write rating to music file?"),
823 tr("Ratings"),
824 isAll ? GuiItem(tr("Write Ratings")) : GuiItem(tr("Write Rating")),
825 StdGuiItem::cancel())) {
826 return;
827 }
828
829 if (isAll) {
830 progress->setVisible(true);
831 progress->setRange(0, edited.count());
832 QStringList failed;
833 for (int i=1; i<edited.count(); ++i) {
834 progress->setValue(i+1);
835 if (i && 0==i%10) {
836 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
837 }
838 Song s=edited.at(i);
839 if (s.rating<=Song::Rating_Max) {
840 Tags::Update status=Tags::updateRating(s.filePath(baseDir), s.rating);
841 if (Tags::Update_Failed==status || Tags::Update_BadFile==status){
842 failed.append(s.file);
843 }
844 }
845 }
846 progress->setVisible(false);
847 if (!failed.isEmpty()) {
848 MessageBox::errorList(this, tr("Failed to write ratings of the following tracks:"), failed);
849 }
850 } else {
851 Song s=edited.at(currentSongIndex);
852 if (s.rating<=Song::Rating_Max) {
853 Tags::Update status=Tags::updateRating(s.filePath(baseDir), s.rating);
854 if (Tags::Update_Failed==status || Tags::Update_BadFile==status){
855 MessageBox::error(this, tr("Failed to write rating to music file!"));
856 }
857 }
858 }
859 }
860
checkChanged()861 void TagEditor::checkChanged()
862 {
863 if (updating) {
864 return;
865 }
866
867 bool allWasEdited=editedIndexes.contains(0);
868
869 updateEdited();
870
871 bool allEdited=editedIndexes.contains(0);
872 bool isAll=0==currentSongIndex && original.count()>1;
873
874 if (isAll && (allEdited || allWasEdited)) {
875 int save=currentSongIndex;
876 for (int i=1; i<edited.count(); ++i) {
877 currentSongIndex=i;
878 updateEdited(true);
879 }
880 currentSongIndex=save;
881 }
882 enableOkButton();
883 setLabelStates();
884 }
885
updateTrackName(int index,bool edited)886 void TagEditor::updateTrackName(int index, bool edited)
887 {
888 bool isAll=0==index && original.count()>1;
889
890 if (edited) {
891 if (isAll) {
892 trackName->setItemText(index, tr("All tracks [modified]"));
893 } else {
894 trackName->setItemText(index, tr("%1 [modified]").arg(original.at(index).filePath()));
895 }
896 } else {
897 if (isAll) {
898 trackName->setItemText(index, tr("All tracks"));
899 } else {
900 trackName->setItemText(index, original.at(index).filePath());
901 }
902 }
903 }
904
updateEditedStatus(int index)905 void TagEditor::updateEditedStatus(int index)
906 {
907 bool isAll=0==index && original.count()>1;
908 Song s=edited.at(index);
909 if (equalTags(s, original.at(index), isAll, composerSupport, commentSupport) && s.rating==original.at(index).rating) {
910 if (editedIndexes.contains(index)) {
911 editedIndexes.remove(index);
912 updateTrackName(index, false);
913 edited.replace(index, s);
914 }
915 } else {
916 if (!editedIndexes.contains(index)) {
917 editedIndexes.insert(index);
918 updateTrackName(index, true);
919 }
920 edited.replace(index, s);
921 }
922 }
923
updateEdited(bool isFromAll)924 void TagEditor::updateEdited(bool isFromAll)
925 {
926 Song s=edited.at(currentSongIndex);
927 bool isAll=0==currentSongIndex && original.count()>1;
928 fillSong(s, isFromAll || isAll, /*isFromAll*/false);
929
930 if (!isAll && isFromAll && original.count()>1) {
931 Song all=original.at(0);
932 Song o=original.at(currentSongIndex);
933 if (all.artist.isEmpty() && s.artist.isEmpty() && !o.artist.isEmpty()) {
934 s.artist=o.artist;
935 }
936 if (all.albumartist.isEmpty() && s.albumartist.isEmpty() && !o.albumartist.isEmpty()) {
937 s.albumartist=o.albumartist;
938 }
939 if (composerSupport && all.composer().isEmpty() && s.composer().isEmpty() && !o.composer().isEmpty()) {
940 s.setComposer(o.composer());
941 }
942 if (commentSupport && all.comment().isEmpty() && s.comment().isEmpty() && !o.comment().isEmpty()) {
943 s.setComment(o.comment());
944 }
945 if (all.album.isEmpty() && s.album.isEmpty() && !o.album.isEmpty()) {
946 s.album=o.album;
947 }
948 if (all.genres[0].isEmpty() && s.genres[0].isEmpty() && !o.genres[0].isEmpty()) {
949 s.genres[0]=o.genres[0];
950 }
951 }
952
953 if (equalTags(s, original.at(currentSongIndex), isFromAll || isAll, composerSupport, commentSupport) && s.rating==original.at(currentSongIndex).rating) {
954 if (editedIndexes.contains(currentSongIndex)) {
955 editedIndexes.remove(currentSongIndex);
956 updateTrackName(currentSongIndex, false);
957 edited.replace(currentSongIndex, s);
958 }
959 } else {
960 if (!editedIndexes.contains(currentSongIndex)) {
961 editedIndexes.insert(currentSongIndex);
962 updateTrackName(currentSongIndex, true);
963 }
964 edited.replace(currentSongIndex, s);
965 }
966 }
967
setSong(const Song & s)968 void TagEditor::setSong(const Song &s)
969 {
970 blockSignals(true);
971 title->setText(s.title);
972 artist->setText(s.artist);
973 albumArtist->setText(s.albumartist);
974 if (composerSupport) {
975 composer->setText(s.composer());
976 }
977 if (commentSupport) {
978 comment->setText(s.comment());
979 }
980 album->setText(s.album);
981 track->setValue(s.track);
982 disc->setValue(s.disc);
983 genre->setText(s.genres[0]);
984 year->setValue(s.year);
985 if (ratingWidget) {
986 ratingWidget->setValue(s.rating);
987 }
988 blockSignals(false);
989 checkChanged();
990 }
991
setIndex(int idx)992 void TagEditor::setIndex(int idx)
993 {
994 if (currentSongIndex==idx || idx>(original.count()-1)) {
995 return;
996 }
997 updating=true;
998
999 bool haveMultiple=original.count()>1;
1000
1001 if (haveMultiple && currentSongIndex>=0) {
1002 updateEdited();
1003 }
1004 Song s=edited.at(!haveMultiple || idx==0 ? 0 : idx);
1005 setSong(s);
1006 currentSongIndex=idx;
1007
1008 bool isMultiple=haveMultiple && 0==idx;
1009 title->setEnabled(!isMultiple);
1010 titleLabel->setEnabled(!isMultiple);
1011 track->setEnabled(!isMultiple);
1012 trackLabel->setEnabled(!isMultiple);
1013
1014 if (isMultiple) {
1015 title->setText(QString());
1016 track->setValue(0);
1017 }
1018
1019 if (original.count()>1) {
1020 enableButton(User1, !isMultiple && idx<(original.count()-1)); // Next
1021 enableButton(User2, !isMultiple && idx>1); // Prev
1022 }
1023 setVariousHint();
1024 enableOkButton();
1025 trackName->setCurrentIndex(idx);
1026 setLabelStates();
1027 updating=false;
1028 }
1029
rating(const QString & f,quint8 r)1030 void TagEditor::rating(const QString &f, quint8 r)
1031 {
1032 if (!ratingWidget) {
1033 return;
1034 }
1035 for (int i=original.count()>1 ? 1 : 0; i<original.count(); ++i) {
1036 Song s=original.at(i);
1037 if (nullRating(s) && s.rating!=r && s.file==f) {
1038 progress->setValue(progress->value()+1);
1039 controlInitialActionsState();
1040 s.rating=r;
1041 original.replace(i, s);
1042 s=edited.at(i);
1043 if (nullRating(s)) {
1044 s.rating=r;
1045 edited.replace(i, s);
1046 }
1047 if (i==currentSongIndex && ratingWidget->value()>Song::Rating_Max) {
1048 ratingWidget->setValue(r);
1049 }
1050 }
1051 }
1052
1053 if (original.count()>1 && !haveRatings) {
1054 quint8 rating=Song::Rating_Null;
1055 bool first=true;
1056 for (int i=1; i<original.count() && !haveRatings; ++i) {
1057 quint8 r=original.at(i).rating;
1058 if (nullRating(r)) {
1059 continue;
1060 }
1061 if (first) {
1062 rating=r;
1063 first=false;
1064 } else if (r!=rating) {
1065 haveRatings=true;
1066 rating=Song::Rating_Null;
1067 }
1068 }
1069 Song s=original.at(0);
1070 if (s.rating!=rating) {
1071 s.rating=rating;
1072 original.replace(0, s);
1073 }
1074 s=edited.at(0);
1075 if (s.rating!=rating) {
1076 s.rating=rating;
1077 edited.replace(0, s);
1078 }
1079 if (0==currentSongIndex) {
1080 ratingWidget->setValue(rating);
1081 }
1082 setVariousHint();
1083 }
1084 }
1085
checkRating()1086 void TagEditor::checkRating()
1087 {
1088 if (!ratingWidget) {
1089 return;
1090 }
1091 checkChanged();
1092 if (original.count()>1 && 0==currentSongIndex) {
1093 quint8 rating=Song::Rating_Null;
1094 bool first=true;
1095 haveRatings=false;
1096 for (int i=1; i<edited.count() && !haveRatings; ++i) {
1097 quint8 r=edited.at(i).rating;
1098 if (nullRating(r)) {
1099 continue;
1100 }
1101 if (first) {
1102 rating=r;
1103 first=false;
1104 } else if (r!=rating) {
1105 haveRatings=true;
1106 rating=Song::Rating_Null;
1107 }
1108 }
1109 Song s=edited.at(0);
1110 if (s.rating!=rating) {
1111 s.rating=rating;
1112 edited.replace(0, s);
1113 if (0==currentSongIndex) {
1114 ratingWidget->setValue(rating);
1115 }
1116 }
1117 setVariousHint();
1118 }
1119 }
1120
applyUpdates()1121 bool TagEditor::applyUpdates()
1122 {
1123 bool skipFirst=original.count()>1;
1124 QStringList failed;
1125 DeviceOptions opts;
1126 QString udi;
1127 #ifdef ENABLE_DEVICES_SUPPORT
1128 Device * dev=nullptr;
1129 if (!deviceUdi.isEmpty()) {
1130 dev=getDevice(deviceUdi, this);
1131 if (!dev) {
1132 return true;
1133 }
1134 opts=dev->options();
1135 udi=dev->id();
1136 } else
1137 #endif
1138 opts.load(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true);
1139
1140 int toSave=editedIndexes.count();
1141 bool renameFiles=false;
1142 QList<Song> updatedSongs;
1143
1144 saving=true;
1145 enableButton(Ok, false);
1146 enableButton(Cancel, false);
1147 enableButton(Reset, false);
1148 enableButton(User1, false);
1149 enableButton(User2, false);
1150 enableButton(User3, false);
1151 progress->setVisible(true);
1152 progress->setRange(1, toSave);
1153
1154 int count=0;
1155 bool someTimedout=false;
1156 bool isLocal=false;
1157 for (int idx: editedIndexes) {
1158 progress->setValue(progress->value()+1);
1159
1160 if (0==count++%10) {
1161 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1162 }
1163
1164 if (skipFirst && 0==idx) {
1165 continue;
1166 }
1167 Song orig=original.at(idx);
1168 Song edit=edited.at(idx);
1169
1170 if (orig.isLocalFile()) {
1171 isLocal = true;
1172 }
1173 if (ratingWidget && orig.rating!=edit.rating && edit.rating<=Song::Rating_Max) {
1174 emit setRating(orig.file, edit.rating);
1175 }
1176
1177 if (equalTags(orig, edit, false, composerSupport, commentSupport)) {
1178 continue;
1179 }
1180
1181 QString file=orig.filePath();
1182 QString afile=orig.filePath(baseDir);
1183 splitGenres(orig);
1184 splitGenres(edit);
1185 switch(Tags::update(afile, orig, edit, -1, commentSupport)) {
1186 case Tags::Update_Modified:
1187 edit.setComment(QString());
1188 #ifdef ENABLE_DEVICES_SUPPORT
1189 if (!deviceUdi.isEmpty()) {
1190 if (!dev->updateSong(orig, edit)) {
1191 dev->removeSongFromList(orig);
1192 dev->addSongToList(edit);
1193 }
1194 } else
1195 #endif
1196 {
1197 // if (!MusicLibraryModel::self()->updateSong(orig, edit)) {
1198 // MusicLibraryModel::self()->removeSongFromList(orig);
1199 // MusicLibraryModel::self()->addSongToList(edit);
1200 // }
1201 }
1202 updatedSongs.append(edit);
1203 if (!renameFiles && !isLocal && file!=opts.createFilename(edit)) {
1204 renameFiles=true;
1205 }
1206 break;
1207 case Tags::Update_Failed:
1208 failed.append(file);
1209 break;
1210 case Tags::Update_BadFile:
1211 failed.append(tr("%1 (Corrupt tags?)", "filename (Corrupt tags?)").arg(file));
1212 break;
1213 default:
1214 break;
1215 }
1216 }
1217 saving=false;
1218
1219 if (failed.count()) {
1220 MessageBox::errorListEx(this, tr("Failed to update the tags of the following tracks:"), failed);
1221 }
1222
1223 if (updatedSongs.count()) {
1224 // If we call tag-editor, no need to do MPD update - as this will be done from that dialog...
1225 if (renameFiles &&
1226 MessageBox::Yes==MessageBox::questionYesNo(this, tr("Would you also like to rename your song files, so as to match your tags?"),
1227 tr("Rename Files"), GuiItem(tr("Rename")), StdGuiItem::cancel())) {
1228 TrackOrganiser *dlg=new TrackOrganiser(parentWidget());
1229 dlg->show(updatedSongs, udi, true);
1230 } else {
1231 #ifdef ENABLE_DEVICES_SUPPORT
1232 if (!deviceUdi.isEmpty()) {
1233 dev->saveCache();
1234 } else
1235 #endif
1236 if (!isLocal) {
1237 // MusicLibraryModel::self()->removeCache();
1238 emit update();
1239 }
1240 }
1241 }
1242
1243 return !someTimedout;
1244 }
1245
slotButtonClicked(int button)1246 void TagEditor::slotButtonClicked(int button)
1247 {
1248 switch (button) {
1249 case Ok: {
1250 if (applyUpdates()) {
1251 accept();
1252 }
1253 break;
1254 }
1255 case Reset: // Reset
1256 if (0==currentSongIndex && original.count()>1) {
1257 for (int i=0; i<original.count(); ++i) {
1258 edited.replace(i, original.at(i));
1259 updateTrackName(i, false);
1260 }
1261 editedIndexes.clear();
1262 setSong(original.at(currentSongIndex));
1263 checkRating();
1264 } else {
1265 setSong(original.at(currentSongIndex));
1266 }
1267 setLabelStates();
1268 enableOkButton();
1269 break;
1270 case User1: // Next
1271 setIndex(currentSongIndex+1);
1272 break;
1273 case User2: // Prev
1274 setIndex(currentSongIndex-1);
1275 break;
1276 case Cancel:
1277 reject();
1278 // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
1279 Dialog::slotButtonClicked(button);
1280 break;
1281 default:
1282 break;
1283 }
1284 }
1285
1286 #ifdef ENABLE_DEVICES_SUPPORT
getDevice(const QString & udi,QWidget * p)1287 Device * TagEditor::getDevice(const QString &udi, QWidget *p)
1288 {
1289 Device *dev=DevicesModel::self()->device(udi);
1290 if (!dev) {
1291 MessageBox::error(p ? p : this, tr("Device has been removed!"));
1292 reject();
1293 return nullptr;
1294 }
1295 if (!dev->isConnected()) {
1296 MessageBox::error(p ? p : this, tr("Device is not connected."));
1297 reject();
1298 return nullptr;
1299 }
1300 if (!dev->isIdle()) {
1301 MessageBox::error(p ? p : this, tr("Device is busy?"));
1302 reject();
1303 return nullptr;
1304 }
1305 return dev;
1306 }
1307 #endif
1308
closeEvent(QCloseEvent * event)1309 void TagEditor::closeEvent(QCloseEvent *event)
1310 {
1311 if (saving) {
1312 event->ignore();
1313 } else {
1314 Dialog::closeEvent(event);
1315 }
1316 }
1317
controlInitialActionsState()1318 void TagEditor::controlInitialActionsState()
1319 {
1320 if (progress->value()>=progress->maximum()) {
1321 progress->setVisible(false);
1322 if (readRatingsAct) {
1323 readRatingsAct->setEnabled(true);
1324 }
1325 if (writeRatingsAct) {
1326 writeRatingsAct->setEnabled(true);
1327 }
1328 }
1329 }
1330
1331 #include "moc_tageditor.cpp"
1332