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: