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: