File indexing completed on 2021-12-21 13:27:51

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