File indexing completed on 2023-05-30 11:30:44
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 "collectionlist.h" 0018 0019 #include <kmessagebox.h> 0020 #include <KConfigGroup> 0021 #include <KSharedConfig> 0022 #include <kactioncollection.h> 0023 #include <ktoolbarpopupaction.h> 0024 #include <kdirwatch.h> 0025 #include <KLocalizedString> 0026 0027 #include <QDragMoveEvent> 0028 #include <QDropEvent> 0029 #include <QElapsedTimer> 0030 #include <QFileInfo> 0031 #include <QHeaderView> 0032 #include <QList> 0033 #include <QMenu> 0034 #include <QReadLocker> 0035 #include <QSaveFile> 0036 #include <QTime> 0037 #include <QTimer> 0038 #include <QWriteLocker> 0039 0040 #include "playlistcollection.h" 0041 #include "stringshare.h" 0042 #include "cache.h" 0043 #include "actioncollection.h" 0044 #include "juktag.h" 0045 #include "viewmode.h" 0046 #include "juk_debug.h" 0047 0048 using ActionCollection::action; 0049 0050 //////////////////////////////////////////////////////////////////////////////// 0051 // static methods 0052 //////////////////////////////////////////////////////////////////////////////// 0053 0054 CollectionList *CollectionList::m_list = 0; 0055 0056 CollectionList *CollectionList::instance() 0057 { 0058 return m_list; 0059 } 0060 0061 static QElapsedTimer stopwatch; 0062 0063 void CollectionList::startLoadingCachedItems() 0064 { 0065 if(!m_list) 0066 return; 0067 0068 qCDebug(JUK_LOG) << "Starting to load cached items"; 0069 stopwatch.start(); 0070 0071 if(!Cache::instance()->prepareToLoadCachedItems()) { 0072 qCCritical(JUK_LOG) << "Unable to setup to load cache... perhaps it doesn't exist?"; 0073 0074 completedLoadingCachedItems(); 0075 return; 0076 } 0077 0078 QTimer::singleShot(0, this, &CollectionList::loadNextBatchCachedItems); 0079 } 0080 0081 void CollectionList::loadNextBatchCachedItems() 0082 { 0083 Cache *cache = Cache::instance(); 0084 bool done = false; 0085 0086 QReadLocker lock(&m_itemsDictLock); 0087 0088 for(int i = 0; i < 127; ++i) { 0089 FileHandle cachedItem(cache->loadNextCachedItem()); 0090 0091 if(cachedItem.isNull()) { 0092 done = true; 0093 break; 0094 } 0095 0096 // This may have already been created via a loaded playlist. 0097 if(!m_itemsDict.contains(cachedItem.absFilePath())) { 0098 lock.unlock(); 0099 CollectionListItem *newItem = new CollectionListItem(this, cachedItem); 0100 lock.relock(); 0101 0102 setupItem(newItem); 0103 } 0104 } 0105 0106 if(!done) { 0107 QTimer::singleShot(0, this, &CollectionList::loadNextBatchCachedItems); 0108 } 0109 else { 0110 completedLoadingCachedItems(); 0111 } 0112 } 0113 0114 void CollectionList::completedLoadingCachedItems() 0115 { 0116 // The CollectionList is created with sorting disabled for speed. Re-enable 0117 // it here, and perform the sort. 0118 KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); 0119 0120 Qt::SortOrder order = Qt::DescendingOrder; 0121 if(config.readEntry("CollectionListSortAscending", true)) 0122 order = Qt::AscendingOrder; 0123 0124 m_list->sortByColumn(config.readEntry("CollectionListSortColumn", 1), order); 0125 0126 qCDebug(JUK_LOG) << "Finished loading cached items, took" << stopwatch.elapsed() << "ms"; 0127 qCDebug(JUK_LOG) << m_itemsDict.size() << "items are in the CollectionList"; 0128 qCDebug(JUK_LOG) << StringShare::numHits() << "string intern hits out of" << StringShare::numAttempts() << "attempts"; 0129 0130 emit cachedItemsLoaded(); 0131 } 0132 0133 void CollectionList::initialize(PlaylistCollection *collection) 0134 { 0135 if(m_list) 0136 return; 0137 0138 // We have to delay initialization here because dynamic_cast or comparing to 0139 // the collection instance won't work in the PlaylistBox::Item initialization 0140 // won't work until the CollectionList is fully constructed. 0141 0142 m_list = new CollectionList(collection); 0143 m_list->setName(i18n("Collection List")); 0144 0145 collection->setupPlaylist(m_list, "folder-sound"); 0146 } 0147 0148 //////////////////////////////////////////////////////////////////////////////// 0149 // public methods 0150 //////////////////////////////////////////////////////////////////////////////// 0151 0152 CollectionListItem *CollectionList::createItem(const FileHandle &file, QTreeWidgetItem *) 0153 { 0154 // It's probably possible to optimize the line below away, but, well, right 0155 // now it's more important to not load duplicate items. 0156 0157 if(hasItem(file.absFilePath())) 0158 return nullptr; 0159 0160 CollectionListItem *item = new CollectionListItem(this, file); 0161 0162 if(!item->isValid()) { 0163 qCCritical(JUK_LOG) << "CollectionList::createItem() -- A valid tag was not created for \"" 0164 << file.absFilePath() << "\""; 0165 delete item; 0166 return nullptr; 0167 } 0168 0169 setupItem(item); 0170 0171 return item; 0172 } 0173 0174 void CollectionList::clearItems(const PlaylistItemList &items) 0175 { 0176 foreach(PlaylistItem *item, items) { 0177 delete item; 0178 } 0179 0180 playlistItemsChanged(); 0181 } 0182 0183 void CollectionList::setupTreeViewEntries(ViewMode *viewMode) const 0184 { 0185 TreeViewMode *treeViewMode = dynamic_cast<TreeViewMode *>(viewMode); 0186 if(!treeViewMode) { 0187 qCWarning(JUK_LOG) << "Can't setup entries on a non-tree-view mode!\n"; 0188 return; 0189 } 0190 0191 QList<int> columnList; 0192 columnList << PlaylistItem::ArtistColumn; 0193 columnList << PlaylistItem::GenreColumn; 0194 columnList << PlaylistItem::AlbumColumn; 0195 0196 foreach(int column, columnList) 0197 treeViewMode->addItems(m_columnTags[column]->keys(), column); 0198 } 0199 0200 void CollectionList::slotNewItems(const KFileItemList &items) 0201 { 0202 QStringList files; 0203 0204 for(KFileItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) 0205 files.append((*it).url().path()); 0206 0207 addFiles(files); 0208 update(); 0209 } 0210 0211 void CollectionList::slotRefreshItems(const QList<QPair<KFileItem, KFileItem> > &items) 0212 { 0213 for(int i = 0; i < items.count(); ++i) { 0214 const KFileItem fileItem = items[i].second; 0215 CollectionListItem *item = lookup(fileItem.url().path()); 0216 0217 if(item) { 0218 item->refreshFromDisk(); 0219 0220 // If the item is no longer on disk, remove it from the collection. 0221 0222 if(item->file().fileInfo().exists()) 0223 item->repaint(); 0224 else 0225 delete item; 0226 } 0227 } 0228 0229 update(); 0230 } 0231 0232 void CollectionList::slotDeleteItems(const KFileItemList &items) 0233 { 0234 for(const auto &item : items) { 0235 delete lookup(item.url().path()); 0236 } 0237 } 0238 0239 void CollectionList::saveItemsToCache() const 0240 { 0241 qCDebug(JUK_LOG) << "Saving collection list to cache"; 0242 0243 QSaveFile f(Cache::fileHandleCacheFileName()); 0244 0245 if(!f.open(QIODevice::WriteOnly)) { 0246 qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString(); 0247 return; 0248 } 0249 0250 QByteArray data; 0251 QDataStream s(&data, QIODevice::WriteOnly); 0252 s.setVersion(QDataStream::Qt_4_3); 0253 0254 { // locked scope 0255 QHash<QString, CollectionListItem *>::const_iterator it; 0256 QWriteLocker lock(&m_itemsDictLock); 0257 0258 for(it = m_itemsDict.begin(); it != m_itemsDict.end(); ++it) { 0259 s << it.key(); 0260 s << (*it)->file(); 0261 } 0262 } 0263 0264 QDataStream fs(&f); 0265 0266 qint32 checksum = qChecksum(data.data(), data.size()); 0267 0268 fs << qint32(Cache::playlistItemsCacheVersion) 0269 << checksum 0270 << data; 0271 0272 if(!f.commit()) 0273 qCCritical(JUK_LOG) << "Error saving cache:" << f.errorString(); 0274 } 0275 0276 //////////////////////////////////////////////////////////////////////////////// 0277 // public slots 0278 //////////////////////////////////////////////////////////////////////////////// 0279 0280 void CollectionList::clear() 0281 { 0282 int result = KMessageBox::warningContinueCancel(this, 0283 i18n("Removing an item from the collection will also remove it from " 0284 "all of your playlists. Are you sure you want to continue?\n\n" 0285 "Note, however, that if the directory that these files are in is in " 0286 "your \"scan on startup\" list, they will be readded on startup.")); 0287 0288 if(result == KMessageBox::Continue) { 0289 Playlist::clear(); 0290 emit signalCollectionChanged(); 0291 } 0292 } 0293 0294 void CollectionList::slotCheckCache() 0295 { 0296 PlaylistItemList invalidItems; 0297 qCDebug(JUK_LOG) << "Starting to check cached items for consistency"; 0298 stopwatch.start(); 0299 0300 { // locked scope 0301 QWriteLocker lock(&m_itemsDictLock); 0302 0303 for(auto item : qAsConst(m_itemsDict)) { 0304 if(!item->checkCurrent()) 0305 invalidItems.append(item); 0306 } 0307 } 0308 0309 clearItems(invalidItems); 0310 0311 qCDebug(JUK_LOG) << "Finished consistency check, took" << stopwatch.elapsed() << "ms"; 0312 } 0313 0314 void CollectionList::slotRemoveItem(const QString &file) 0315 { 0316 QWriteLocker lock(&m_itemsDictLock); 0317 delete m_itemsDict[file]; 0318 } 0319 0320 void CollectionList::slotRefreshItem(const QString &file) 0321 { 0322 auto item = lookup(file); 0323 if(item) 0324 item->refresh(); 0325 } 0326 0327 //////////////////////////////////////////////////////////////////////////////// 0328 // protected methods 0329 //////////////////////////////////////////////////////////////////////////////// 0330 0331 CollectionList::CollectionList(PlaylistCollection *collection) : 0332 Playlist(collection, true), 0333 m_columnTags(15, 0) 0334 { 0335 QAction *spaction = ActionCollection::actions()->addAction("showPlaying"); 0336 spaction->setText(i18n("Show Playing")); 0337 connect(spaction, SIGNAL(triggered(bool)), SLOT(slotShowPlaying())); 0338 0339 connect(action<KToolBarPopupAction>("back")->menu(), SIGNAL(aboutToShow()), 0340 this, SLOT(slotPopulateBackMenu())); 0341 connect(action<KToolBarPopupAction>("back")->menu(), SIGNAL(triggered(QAction*)), 0342 this, SLOT(slotPlayFromBackMenu(QAction*))); 0343 setSortingEnabled(false); // Temporarily disable sorting to add items faster. 0344 0345 m_columnTags[PlaylistItem::ArtistColumn] = new TagCountDict; 0346 m_columnTags[PlaylistItem::AlbumColumn] = new TagCountDict; 0347 m_columnTags[PlaylistItem::GenreColumn] = new TagCountDict; 0348 0349 // Even set to true it wouldn't work with this class due to other checks 0350 setAllowDuplicates(false); 0351 } 0352 0353 CollectionList::~CollectionList() 0354 { 0355 KConfigGroup config(KSharedConfig::openConfig(), "Playlists"); 0356 config.writeEntry("CollectionListSortColumn", header()->sortIndicatorSection()); 0357 config.writeEntry("CollectionListSortAscending", header()->sortIndicatorOrder() == Qt::AscendingOrder); 0358 0359 // The CollectionListItems will try to remove themselves from the 0360 // m_columnTags member, so we must make sure they're gone before we 0361 // are. 0362 0363 clearItems(items()); 0364 0365 qDeleteAll(m_columnTags); 0366 m_columnTags.clear(); 0367 } 0368 0369 void CollectionList::dropEvent(QDropEvent *e) 0370 { 0371 if(e->source() == this) 0372 return; // Don't rearrange in the CollectionList. 0373 else 0374 Playlist::dropEvent(e); 0375 } 0376 0377 void CollectionList::dragMoveEvent(QDragMoveEvent *e) 0378 { 0379 if(e->source() != this) 0380 Playlist::dragMoveEvent(e); 0381 else 0382 e->setAccepted(false); 0383 } 0384 0385 void CollectionList::addToDict(const QString &file, CollectionListItem *item) 0386 { 0387 QWriteLocker lock(&m_itemsDictLock); 0388 m_itemsDict.insert(file, item); 0389 } 0390 0391 void CollectionList::removeFromDict(const QString &file) 0392 { 0393 QWriteLocker lock(&m_itemsDictLock); 0394 m_itemsDict.remove(file); 0395 } 0396 0397 bool CollectionList::hasItem(const QString &file) const 0398 { 0399 QReadLocker lock(&m_itemsDictLock); 0400 return m_itemsDict.contains(file); 0401 } 0402 0403 CollectionListItem *CollectionList::lookup(const QString &file) const 0404 { 0405 QReadLocker lock(&m_itemsDictLock); 0406 return m_itemsDict.value(file, nullptr); 0407 } 0408 0409 QString CollectionList::addStringToDict(const QString &value, int column) 0410 { 0411 if(column > m_columnTags.count() || value.trimmed().isEmpty()) 0412 return QString(); 0413 0414 if(m_columnTags[column]->contains(value)) 0415 ++((*m_columnTags[column])[value]); 0416 else { 0417 m_columnTags[column]->insert(value, 1); 0418 emit signalNewTag(value, column); 0419 } 0420 0421 return value; 0422 } 0423 0424 QStringList CollectionList::uniqueSet(UniqueSetType t) const 0425 { 0426 int column; 0427 0428 switch(t) 0429 { 0430 case Artists: 0431 column = PlaylistItem::ArtistColumn; 0432 break; 0433 0434 case Albums: 0435 column = PlaylistItem::AlbumColumn; 0436 break; 0437 0438 case Genres: 0439 column = PlaylistItem::GenreColumn; 0440 break; 0441 0442 default: 0443 return QStringList(); 0444 } 0445 0446 return m_columnTags[column]->keys(); 0447 } 0448 0449 void CollectionList::removeStringFromDict(const QString &value, int column) 0450 { 0451 if(column > m_columnTags.count() || value.trimmed().isEmpty()) 0452 return; 0453 0454 if(m_columnTags[column]->contains(value) && 0455 --((*m_columnTags[column])[value])) // If the decrement goes to 0... 0456 { 0457 emit signalRemovedTag(value, column); 0458 m_columnTags[column]->remove(value); 0459 } 0460 } 0461 0462 void CollectionList::addWatched(const QString &file) 0463 { 0464 m_dirWatch->addFile(file); 0465 } 0466 0467 void CollectionList::removeWatched(const QString &file) 0468 { 0469 m_dirWatch->removeFile(file); 0470 } 0471 0472 //////////////////////////////////////////////////////////////////////////////// 0473 // CollectionListItem public methods 0474 //////////////////////////////////////////////////////////////////////////////// 0475 0476 void CollectionListItem::refresh() 0477 { 0478 int offset = CollectionList::instance()->columnOffset(); 0479 int columns = lastColumn() + offset + 1; 0480 0481 sharedData()->metadata.resize(columns); 0482 sharedData()->cachedWidths.resize(columns); 0483 0484 for(int i = offset; i < columns; i++) { 0485 setText(i, text(i)); 0486 int id = i - offset; 0487 if(id != TrackNumberColumn && id != LengthColumn) { 0488 // All columns other than track num and length need local-encoded data for sorting 0489 0490 QString toLower = text(i).toLower(); 0491 0492 // For some columns, we may be able to share some strings 0493 0494 if((id == ArtistColumn) || (id == AlbumColumn) || 0495 (id == GenreColumn) || (id == YearColumn) || 0496 (id == CommentColumn)) 0497 { 0498 toLower = StringShare::tryShare(toLower); 0499 0500 if(id != YearColumn && id != CommentColumn && sharedData()->metadata[id] != toLower) { 0501 CollectionList::instance()->removeStringFromDict(sharedData()->metadata[id], id); 0502 CollectionList::instance()->addStringToDict(text(i), id); 0503 } 0504 } 0505 0506 sharedData()->metadata[id] = toLower; 0507 } 0508 0509 int newWidth = treeWidget()->fontMetrics().horizontalAdvance(text(i)); 0510 if(newWidth != sharedData()->cachedWidths[i]) 0511 playlist()->slotWeightDirty(i); 0512 0513 sharedData()->cachedWidths[i] = newWidth; 0514 } 0515 0516 for(PlaylistItemList::Iterator it = m_children.begin(); it != m_children.end(); ++it) { 0517 (*it)->playlist()->update(); 0518 (*it)->playlist()->playlistItemsChanged(); 0519 } 0520 if(treeWidget()->isVisible()) 0521 treeWidget()->viewport()->update(); 0522 0523 CollectionList::instance()->playlistItemsChanged(); 0524 emit CollectionList::instance()->signalCollectionChanged(); 0525 } 0526 0527 PlaylistItem *CollectionListItem::itemForPlaylist(const Playlist *playlist) 0528 { 0529 if(playlist == CollectionList::instance()) 0530 return this; 0531 0532 PlaylistItemList::ConstIterator it; 0533 for(it = m_children.constBegin(); it != m_children.constEnd(); ++it) 0534 if((*it)->playlist() == playlist) 0535 return *it; 0536 return 0; 0537 } 0538 0539 void CollectionListItem::updateCollectionDict(const QString &oldPath, const QString &newPath) 0540 { 0541 CollectionList *collection = CollectionList::instance(); 0542 0543 if(!collection) 0544 return; 0545 0546 collection->removeFromDict(oldPath); 0547 collection->addToDict(newPath, this); 0548 } 0549 0550 void CollectionListItem::repaint() const 0551 { 0552 // FIXME repaint 0553 /*QItemDelegate::repaint(); 0554 for(PlaylistItemList::ConstIterator it = m_children.constBegin(); it != m_children.constEnd(); ++it) 0555 (*it)->repaint();*/ 0556 } 0557 0558 //////////////////////////////////////////////////////////////////////////////// 0559 // CollectionListItem protected methods 0560 //////////////////////////////////////////////////////////////////////////////// 0561 0562 CollectionListItem::CollectionListItem(CollectionList *parent, const FileHandle &file) 0563 : PlaylistItem(parent) 0564 , m_shuttingDown(false) 0565 { 0566 PlaylistItem::m_collectionItem = this; 0567 parent->addToDict(file.absFilePath(), this); 0568 0569 sharedData()->fileHandle = file; 0570 0571 if(file.tag()) { 0572 refresh(); 0573 parent->playlistItemsChanged(); 0574 } 0575 else { 0576 qCCritical(JUK_LOG) << "CollectionListItem::CollectionListItem() -- Tag() could not be created."; 0577 } 0578 } 0579 0580 CollectionListItem::~CollectionListItem() 0581 { 0582 m_shuttingDown = true; 0583 0584 foreach(PlaylistItem *item, m_children) 0585 delete item; 0586 0587 CollectionList *l = CollectionList::instance(); 0588 if(l) { 0589 l->removeFromDict(file().absFilePath()); 0590 l->removeStringFromDict(file().tag()->album(), AlbumColumn); 0591 l->removeStringFromDict(file().tag()->artist(), ArtistColumn); 0592 l->removeStringFromDict(file().tag()->genre(), GenreColumn); 0593 } 0594 0595 m_collectionItem = nullptr; 0596 } 0597 0598 void CollectionListItem::addChildItem(PlaylistItem *child) 0599 { 0600 m_children.append(child); 0601 } 0602 0603 void CollectionListItem::removeChildItem(PlaylistItem *child) 0604 { 0605 if(!m_shuttingDown) 0606 m_children.removeAll(child); 0607 } 0608 0609 bool CollectionListItem::checkCurrent() 0610 { 0611 if(!file().fileInfo().exists() || !file().fileInfo().isFile()) 0612 return false; 0613 0614 if(!file().current()) { 0615 file().refresh(); 0616 refresh(); 0617 } 0618 0619 return true; 0620 } 0621 0622 // vim: set et sw=4 tw=0 sta: