File indexing completed on 2023-05-30 11:30:50
0001 /** 0002 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org> 0003 * 0004 * This program is free software; you can redistribute it and/or modify it under 0005 * the terms of the GNU General Public License as published by the Free Software 0006 * Foundation; either version 2 of the License, or (at your option) any later 0007 * version. 0008 * 0009 * This program is distributed in the hope that it will be useful, but WITHOUT ANY 0010 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 0011 * PARTICULAR PURPOSE. See the GNU General Public License for more details. 0012 * 0013 * You should have received a copy of the GNU General Public License along with 0014 * this program. If not, see <http://www.gnu.org/licenses/>. 0015 */ 0016 0017 #include "playlistitem.h" 0018 0019 #include <algorithm> 0020 0021 #include <kiconloader.h> 0022 0023 #include <QCollator> 0024 #include <QFileInfo> 0025 #include <QHeaderView> 0026 0027 #include "collectionlist.h" 0028 #include "juktag.h" 0029 #include "coverinfo.h" 0030 #include "covermanager.h" 0031 #include "tagtransactionmanager.h" 0032 0033 #include "juk_debug.h" 0034 0035 PlaylistItemList PlaylistItem::m_playingItems; // static 0036 0037 static int naturalCompare(const QString &first, const QString &second) 0038 { 0039 static QCollator collator; 0040 collator.setNumericMode(true); 0041 collator.setCaseSensitivity(Qt::CaseInsensitive); 0042 return collator.compare(first, second); 0043 } 0044 0045 //////////////////////////////////////////////////////////////////////////////// 0046 // PlaylistItem public methods 0047 //////////////////////////////////////////////////////////////////////////////// 0048 0049 PlaylistItem::~PlaylistItem() 0050 { 0051 // Although this isn't the most efficient way to accomplish the task of 0052 // stopping playback when deleting the item being played, it has the 0053 // stark advantage of working reliably. I'll tell anyone who tries to 0054 // optimize this, the timing issues can be *hard*. -- mpyne 0055 0056 if(m_collectionItem) { 0057 m_collectionItem->removeChildItem(this); 0058 } 0059 0060 if(m_playingItems.contains(this)) { 0061 m_playingItems.removeAll(this); 0062 if(m_playingItems.isEmpty()) 0063 playlist()->setPlaying(0); 0064 } 0065 0066 playlist()->updateDeletedItem(this); 0067 emit playlist()->signalAboutToRemove(this); 0068 0069 if(m_watched) 0070 Pointer::clear(this); 0071 } 0072 0073 void PlaylistItem::setFile(const FileHandle &file) 0074 { 0075 m_collectionItem->updateCollectionDict(d->fileHandle.absFilePath(), file.absFilePath()); 0076 d->fileHandle = file; 0077 refresh(); 0078 } 0079 0080 void PlaylistItem::setFile(const QString &file) 0081 { 0082 QString oldPath = d->fileHandle.absFilePath(); 0083 d->fileHandle.setFile(file); 0084 m_collectionItem->updateCollectionDict(oldPath, d->fileHandle.absFilePath()); 0085 refresh(); 0086 } 0087 0088 FileHandle PlaylistItem::file() const 0089 { 0090 return d->fileHandle; 0091 } 0092 0093 QString PlaylistItem::text(int column) const 0094 { 0095 if(!d->fileHandle.tag()) 0096 return QString(); 0097 0098 int offset = playlist()->columnOffset(); 0099 0100 switch(column - offset) { 0101 case TrackColumn: 0102 return d->fileHandle.tag()->title(); 0103 case ArtistColumn: 0104 return d->fileHandle.tag()->artist(); 0105 case AlbumColumn: 0106 return d->fileHandle.tag()->album(); 0107 case CoverColumn: 0108 return QString(); 0109 case TrackNumberColumn: 0110 return d->fileHandle.tag()->track() > 0 0111 ? QString::number(d->fileHandle.tag()->track()) 0112 : QString(); 0113 case GenreColumn: 0114 return d->fileHandle.tag()->genre(); 0115 case YearColumn: 0116 return d->fileHandle.tag()->year() > 0 0117 ? QString::number(d->fileHandle.tag()->year()) 0118 : QString(); 0119 case LengthColumn: 0120 return d->fileHandle.tag()->lengthString(); 0121 case BitrateColumn: 0122 return QString::number(d->fileHandle.tag()->bitrate()); 0123 case CommentColumn: 0124 return d->fileHandle.tag()->comment(); 0125 case FileNameColumn: 0126 return d->fileHandle.fileInfo().fileName(); 0127 case FullPathColumn: 0128 return d->fileHandle.fileInfo().absoluteFilePath(); 0129 default: 0130 return QTreeWidgetItem::text(column); 0131 } 0132 } 0133 0134 void PlaylistItem::setText(int column, const QString &text) 0135 { 0136 QTreeWidgetItem::setText(column, text); 0137 playlist()->slotWeightDirty(column); 0138 } 0139 0140 bool PlaylistItem::isPlaying() const 0141 { 0142 return std::any_of(m_playingItems.begin(), m_playingItems.end(), 0143 [this](const PlaylistItem *playingItem) { 0144 return this == playingItem; 0145 }); 0146 } 0147 0148 void PlaylistItem::setPlaying(bool playing, bool master) 0149 { 0150 m_playingItems.removeAll(this); 0151 0152 if(playing) { 0153 if(master) 0154 m_playingItems.prepend(this); 0155 else 0156 m_playingItems.append(this); 0157 } 0158 else { 0159 0160 // This is a tricky little recursion, but it 0161 // in fact does clear the list. 0162 0163 if(!m_playingItems.isEmpty()) 0164 m_playingItems.front()->setPlaying(false); 0165 } 0166 0167 treeWidget()->viewport()->update(); 0168 } 0169 0170 void PlaylistItem::guessTagInfo(TagGuesser::Type type) 0171 { 0172 switch(type) { 0173 case TagGuesser::FileName: 0174 { 0175 TagGuesser guesser(d->fileHandle.absFilePath()); 0176 Tag *tag = TagTransactionManager::duplicateTag(d->fileHandle.tag()); 0177 0178 if(!guesser.title().isNull()) 0179 tag->setTitle(guesser.title()); 0180 if(!guesser.artist().isNull()) 0181 tag->setArtist(guesser.artist()); 0182 if(!guesser.album().isNull()) 0183 tag->setAlbum(guesser.album()); 0184 if(!guesser.track().isNull()) 0185 tag->setTrack(guesser.track().toInt()); 0186 if(!guesser.comment().isNull()) 0187 tag->setComment(guesser.comment()); 0188 0189 TagTransactionManager::instance()->changeTagOnItem(this, tag); 0190 break; 0191 } 0192 case TagGuesser::MusicBrainz: 0193 qCDebug(JUK_LOG) << "Ignoring MusicBrainz query request until support is reimplemented."; 0194 break; 0195 } 0196 } 0197 0198 Playlist *PlaylistItem::playlist() const 0199 { 0200 return static_cast<Playlist *>(treeWidget()); 0201 } 0202 0203 QVector<int> PlaylistItem::cachedWidths() const 0204 { 0205 return d->cachedWidths; 0206 } 0207 0208 void PlaylistItem::refresh() 0209 { 0210 m_collectionItem->refresh(); 0211 } 0212 0213 void PlaylistItem::refreshFromDisk() 0214 { 0215 d->fileHandle.refresh(); 0216 refresh(); 0217 } 0218 0219 void PlaylistItem::clear() 0220 { 0221 playlist()->clearItem(this); 0222 } 0223 0224 //////////////////////////////////////////////////////////////////////////////// 0225 // PlaylistItem protected methods 0226 //////////////////////////////////////////////////////////////////////////////// 0227 0228 PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent) : 0229 QTreeWidgetItem(parent), 0230 d(0), 0231 m_watched(0) 0232 { 0233 setup(item); 0234 } 0235 0236 PlaylistItem::PlaylistItem(CollectionListItem *item, Playlist *parent, QTreeWidgetItem *after) : 0237 QTreeWidgetItem(parent, after), 0238 d(0), 0239 m_watched(0) 0240 { 0241 setup(item); 0242 } 0243 0244 0245 // This constructor should only be used by the CollectionList subclass. 0246 0247 PlaylistItem::PlaylistItem(CollectionList *parent) 0248 : QTreeWidgetItem(parent) 0249 // We *will* be this but we aren't yet; subclass will fix 0250 , m_collectionItem(nullptr) 0251 , m_watched(false) 0252 { 0253 d = new Data; 0254 setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled); 0255 } 0256 0257 int PlaylistItem::compare(const QTreeWidgetItem *item, int column, bool ascending) const 0258 { 0259 // reimplemented from QListViewItem 0260 0261 int offset = playlist()->columnOffset(); 0262 0263 if(!item) 0264 return 0; 0265 0266 const PlaylistItem *playlistItem = static_cast<const PlaylistItem *>(item); 0267 0268 // The following statments first check to see if you can sort based on the 0269 // specified column. If the values for the two PlaylistItems are the same 0270 // in that column it then tries to sort based on columns 1, 2, 3 and 0, 0271 // (artist, album, track number, track name) in that order. 0272 0273 int c = compare(this, playlistItem, column, ascending); 0274 0275 if(c != 0) 0276 return c; 0277 else { 0278 // Loop through the columns doing comparisons until something is differnt. 0279 // If all else is the same, compare the track name. 0280 0281 int last = !playlist()->isColumnHidden(AlbumColumn + offset) ? TrackNumberColumn : ArtistColumn; 0282 0283 for(int i = ArtistColumn; i <= last; i++) { 0284 if(!playlist()->isColumnHidden(i + offset)) { 0285 c = compare(this, playlistItem, i, ascending); 0286 if(c != 0) 0287 return c; 0288 } 0289 } 0290 return compare(this, playlistItem, TrackColumn + offset, ascending); 0291 } 0292 } 0293 0294 int PlaylistItem::compare(const PlaylistItem *firstItem, const PlaylistItem *secondItem, int column, bool) const 0295 { 0296 int offset = playlist()->columnOffset(); 0297 0298 if(column < 0 || column > lastColumn() + offset || !firstItem->d || !secondItem->d) 0299 return 0; 0300 0301 if(column < offset) { 0302 QString first = firstItem->text(column); 0303 QString second = secondItem->text(column); 0304 return naturalCompare(first, second); 0305 } 0306 0307 switch(column - offset) { 0308 case TrackNumberColumn: 0309 if(firstItem->d->fileHandle.tag()->track() > secondItem->d->fileHandle.tag()->track()) 0310 return 1; 0311 else if(firstItem->d->fileHandle.tag()->track() < secondItem->d->fileHandle.tag()->track()) 0312 return -1; 0313 else 0314 return 0; 0315 break; 0316 case LengthColumn: 0317 if(firstItem->d->fileHandle.tag()->seconds() > secondItem->d->fileHandle.tag()->seconds()) 0318 return 1; 0319 else if(firstItem->d->fileHandle.tag()->seconds() < secondItem->d->fileHandle.tag()->seconds()) 0320 return -1; 0321 else 0322 return 0; 0323 break; 0324 case BitrateColumn: 0325 if(firstItem->d->fileHandle.tag()->bitrate() > secondItem->d->fileHandle.tag()->bitrate()) 0326 return 1; 0327 else if(firstItem->d->fileHandle.tag()->bitrate() < secondItem->d->fileHandle.tag()->bitrate()) 0328 return -1; 0329 else 0330 return 0; 0331 break; 0332 case CoverColumn: 0333 if(firstItem->d->fileHandle.coverInfo()->coverId() == secondItem->d->fileHandle.coverInfo()->coverId()) 0334 return 0; 0335 else if (firstItem->d->fileHandle.coverInfo()->coverId() != CoverManager::NoMatch) 0336 return -1; 0337 else 0338 return 1; 0339 break; 0340 default: 0341 return naturalCompare(firstItem->d->metadata[column - offset], 0342 secondItem->d->metadata[column - offset]); 0343 } 0344 } 0345 0346 bool PlaylistItem::operator<(const QTreeWidgetItem &other) const 0347 { 0348 bool ascending = playlist()->header()->sortIndicatorOrder() == Qt::AscendingOrder; 0349 return compare(&other, playlist()->sortColumn(), ascending) == -1; 0350 } 0351 0352 bool PlaylistItem::isValid() const 0353 { 0354 return bool(d->fileHandle.tag()); 0355 } 0356 0357 void PlaylistItem::setTrackId(quint32 id) 0358 { 0359 m_trackId = id; 0360 } 0361 0362 //////////////////////////////////////////////////////////////////////////////// 0363 // PlaylistItem private methods 0364 //////////////////////////////////////////////////////////////////////////////// 0365 0366 void PlaylistItem::setup(CollectionListItem *item) 0367 { 0368 m_collectionItem = item; 0369 0370 d = item->d; 0371 item->addChildItem(this); 0372 setFlags(flags() | Qt::ItemIsEditable | Qt::ItemIsDragEnabled); 0373 0374 int offset = playlist()->columnOffset(); 0375 int columns = lastColumn() + offset + 1; 0376 0377 for(int i = offset; i < columns; i++) { 0378 setText(i, text(i)); 0379 } 0380 } 0381 0382 //////////////////////////////////////////////////////////////////////////////// 0383 // PlaylistItem::Pointer implementation 0384 //////////////////////////////////////////////////////////////////////////////// 0385 0386 QHash<PlaylistItem *, QVector<PlaylistItem::Pointer *> > PlaylistItem::Pointer::m_itemPointers; // static 0387 0388 PlaylistItem::Pointer::Pointer(PlaylistItem *item) : 0389 m_item(item) 0390 { 0391 if(!m_item) 0392 return; 0393 0394 m_item->m_watched = true; 0395 m_itemPointers[m_item].append(this); 0396 } 0397 0398 PlaylistItem::Pointer::Pointer(const Pointer &p) : 0399 m_item(p.m_item) 0400 { 0401 m_itemPointers[m_item].append(this); 0402 } 0403 0404 PlaylistItem::Pointer::~Pointer() 0405 { 0406 if(!m_item) 0407 return; 0408 0409 m_itemPointers[m_item].removeAll(this); 0410 if(m_itemPointers[m_item].isEmpty()) { 0411 m_itemPointers.remove(m_item); 0412 m_item->m_watched = false; 0413 } 0414 } 0415 0416 PlaylistItem::Pointer &PlaylistItem::Pointer::operator=(PlaylistItem *item) 0417 { 0418 if(item == m_item) 0419 return *this; 0420 0421 if(m_item) { 0422 m_itemPointers[m_item].removeAll(this); 0423 if(m_itemPointers[m_item].isEmpty()) { 0424 m_itemPointers.remove(m_item); 0425 m_item->m_watched = false; 0426 } 0427 } 0428 0429 if(item) { 0430 m_itemPointers[item].append(this); 0431 item->m_watched = true; 0432 } 0433 0434 m_item = item; 0435 0436 return *this; 0437 } 0438 0439 void PlaylistItem::Pointer::clear(PlaylistItem *item) // static 0440 { 0441 if(!item) 0442 return; 0443 0444 const auto itemPointers = m_itemPointers[item]; 0445 for(const auto &pointerGuard : itemPointers) 0446 pointerGuard->m_item = nullptr; 0447 m_itemPointers.remove(item); 0448 item->m_watched = false; 0449 } 0450 0451 // vim: set et sw=4 tw=0 sta: