#include "library/playlistfeature.h" #include #include #include #include "controllers/keyboard/keyboardeventfilter.h" #include "library/library.h" #include "library/parser.h" #include "library/playlisttablemodel.h" #include "library/queryutil.h" #include "library/trackcollection.h" #include "library/trackcollectionmanager.h" #include "library/treeitem.h" #include "moc_playlistfeature.cpp" #include "sources/soundsourceproxy.h" #include "util/db/dbconnection.h" #include "util/dnd.h" #include "util/duration.h" #include "widget/wlibrary.h" #include "widget/wlibrarysidebar.h" #include "widget/wlibrarytextbrowser.h" namespace { QString createPlaylistLabel( const QString& name, int count, int duration) { return QString("%1 (%2) %3").arg(name, QString::number(count), mixxx::Duration::formatTime(duration, mixxx::Duration::Precision::SECONDS)); } } // anonymous namespace PlaylistFeature::PlaylistFeature( Library* pLibrary, UserSettingsPointer pConfig) : BasePlaylistFeature( pLibrary, pConfig, new PlaylistTableModel( nullptr, pLibrary->trackCollections(), "mixxx.db.model.playlist"), QStringLiteral("PLAYLISTHOME")), m_icon(QStringLiteral(":/images/library/ic_library_playlist.svg")) { // construct child model std::unique_ptr pRootItem = TreeItem::newRoot(this); m_childModel.setRootItem(std::move(pRootItem)); constructChildModel(-1); } PlaylistFeature::~PlaylistFeature() { } QVariant PlaylistFeature::title() { return tr("Playlists"); } QIcon PlaylistFeature::getIcon() { return m_icon; } void PlaylistFeature::bindSidebarWidget(WLibrarySidebar* pSidebarWidget) { // store the sidebar widget pointer for later use in onRightClickChild m_pSidebarWidget = pSidebarWidget; } void PlaylistFeature::onRightClick(const QPoint& globalPos) { m_lastRightClickedIndex = QModelIndex(); QMenu menu(m_pSidebarWidget); menu.addAction(m_pCreatePlaylistAction); menu.addSeparator(); menu.addAction(m_pCreateImportPlaylistAction); menu.exec(globalPos); } void PlaylistFeature::onRightClickChild(const QPoint& globalPos, const QModelIndex& index) { //Save the model index so we can get it in the action slots... m_lastRightClickedIndex = index; int playlistId = playlistIdFromIndex(index); bool locked = m_playlistDao.isPlaylistLocked(playlistId); m_pDeletePlaylistAction->setEnabled(!locked); m_pRenamePlaylistAction->setEnabled(!locked); m_pLockPlaylistAction->setText(locked ? tr("Unlock") : tr("Lock")); QMenu menu(m_pSidebarWidget); menu.addAction(m_pCreatePlaylistAction); menu.addSeparator(); menu.addAction(m_pRenamePlaylistAction); menu.addAction(m_pDuplicatePlaylistAction); menu.addAction(m_pDeletePlaylistAction); menu.addAction(m_pLockPlaylistAction); menu.addSeparator(); menu.addAction(m_pAddToAutoDJAction); menu.addAction(m_pAddToAutoDJTopAction); menu.addAction(m_pAddToAutoDJReplaceAction); menu.addSeparator(); menu.addAction(m_pAnalyzePlaylistAction); menu.addSeparator(); menu.addAction(m_pImportPlaylistAction); menu.addAction(m_pExportPlaylistAction); menu.addAction(m_pExportTrackFilesAction); menu.exec(globalPos); } bool PlaylistFeature::dropAcceptChild( const QModelIndex& index, const QList& urls, QObject* pSource) { int playlistId = playlistIdFromIndex(index); VERIFY_OR_DEBUG_ASSERT(playlistId >= 0) { return false; } // If a track is dropped onto a playlist's name, but the track isn't in the // library, then add the track to the library before adding it to the // playlist. // pSource != nullptr it is a drop from inside Mixxx and indicates all // tracks already in the DB QList trackIds = m_pLibrary->trackCollections()->internalCollection()->resolveTrackIdsFromUrls(urls, !pSource); if (!trackIds.size()) { return false; } // Return whether appendTracksToPlaylist succeeded. return m_playlistDao.appendTracksToPlaylist(trackIds, playlistId); } bool PlaylistFeature::dragMoveAcceptChild(const QModelIndex& index, const QUrl& url) { int playlistId = playlistIdFromIndex(index); bool locked = m_playlistDao.isPlaylistLocked(playlistId); bool formatSupported = SoundSourceProxy::isUrlSupported(url) || Parser::isPlaylistFilenameSupported(url.toLocalFile()); return !locked && formatSupported; } QList PlaylistFeature::createPlaylistLabels() { QSqlDatabase database = m_pLibrary->trackCollections()->internalCollection()->database(); QList playlistLabels; QString queryString = QString( "CREATE TEMPORARY VIEW IF NOT EXISTS PlaylistsCountsDurations " "AS SELECT " " Playlists.id AS id, " " Playlists.name AS name, " " LOWER(Playlists.name) AS sort_name, " " COUNT(case library.mixxx_deleted when 0 then 1 else null end) AS count, " " SUM(case library.mixxx_deleted when 0 then library.duration else 0 end) AS durationSeconds " "FROM Playlists " "LEFT JOIN PlaylistTracks ON PlaylistTracks.playlist_id = Playlists.id " "LEFT JOIN library ON PlaylistTracks.track_id = library.id " "WHERE Playlists.hidden = 0 " "GROUP BY Playlists.id"); queryString.append(mixxx::DbConnection::collateLexicographically( " ORDER BY sort_name")); QSqlQuery query(database); if (!query.exec(queryString)) { LOG_FAILED_QUERY(query); } // Setup the sidebar playlist model QSqlTableModel playlistTableModel(this, database); playlistTableModel.setTable("PlaylistsCountsDurations"); playlistTableModel.select(); while (playlistTableModel.canFetchMore()) { playlistTableModel.fetchMore(); } QSqlRecord record = playlistTableModel.record(); int nameColumn = record.indexOf("name"); int idColumn = record.indexOf("id"); int countColumn = record.indexOf("count"); int durationColumn = record.indexOf("durationSeconds"); for (int row = 0; row < playlistTableModel.rowCount(); ++row) { int id = playlistTableModel.data( playlistTableModel.index(row, idColumn)) .toInt(); QString name = playlistTableModel.data( playlistTableModel.index(row, nameColumn)) .toString(); int count = playlistTableModel.data( playlistTableModel.index(row, countColumn)) .toInt(); int duration = playlistTableModel.data( playlistTableModel.index(row, durationColumn)) .toInt(); BasePlaylistFeature::IdAndLabel idAndLabel; idAndLabel.id = id; idAndLabel.label = createPlaylistLabel(name, count, duration); playlistLabels.append(idAndLabel); } return playlistLabels; } QString PlaylistFeature::fetchPlaylistLabel(int playlistId) { // Setup the sidebar playlist model QSqlDatabase database = m_pLibrary->trackCollections()->internalCollection()->database(); QSqlTableModel playlistTableModel(this, database); playlistTableModel.setTable("PlaylistsCountsDurations"); QString filter = "id=" + QString::number(playlistId); playlistTableModel.setFilter(filter); playlistTableModel.select(); while (playlistTableModel.canFetchMore()) { playlistTableModel.fetchMore(); } QSqlRecord record = playlistTableModel.record(); int nameColumn = record.indexOf("name"); int countColumn = record.indexOf("count"); int durationColumn = record.indexOf("durationSeconds"); DEBUG_ASSERT(playlistTableModel.rowCount() <= 1); if (playlistTableModel.rowCount() > 0) { QString name = playlistTableModel.data( playlistTableModel.index(0, nameColumn)) .toString(); int count = playlistTableModel.data( playlistTableModel.index(0, countColumn)) .toInt(); int duration = playlistTableModel.data( playlistTableModel.index(0, durationColumn)) .toInt(); return createPlaylistLabel(name, count, duration); } return QString(); } void PlaylistFeature::decorateChild(TreeItem* item, int playlistId) { if (m_playlistDao.isPlaylistLocked(playlistId)) { item->setIcon(QIcon(":/images/library/ic_library_locked_tracklist.svg")); } else { item->setIcon(QIcon()); } } void PlaylistFeature::slotPlaylistTableChanged(int playlistId) { //qDebug() << "slotPlaylistTableChanged() playlistId:" << playlistId; enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_NOT_HIDDEN || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist clearChildModel(); m_lastRightClickedIndex = constructChildModel(playlistId); } } void PlaylistFeature::slotPlaylistContentChanged(QSet playlistIds) { for (const auto playlistId : qAsConst(playlistIds)) { enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_NOT_HIDDEN || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist updateChildModel(playlistId); } } } void PlaylistFeature::slotPlaylistTableRenamed( int playlistId, const QString& newName) { Q_UNUSED(newName); //qDebug() << "slotPlaylistTableChanged() playlistId:" << playlistId; enum PlaylistDAO::HiddenType type = m_playlistDao.getHiddenType(playlistId); if (type == PlaylistDAO::PLHT_NOT_HIDDEN || type == PlaylistDAO::PLHT_UNKNOWN) { // In case of a deleted Playlist clearChildModel(); m_lastRightClickedIndex = constructChildModel(playlistId); if (type != PlaylistDAO::PLHT_UNKNOWN) { activatePlaylist(playlistId); } } } QString PlaylistFeature::getRootViewHtml() const { QString playlistsTitle = tr("Playlists"); QString playlistsSummary = tr("Playlists are ordered lists of songs that allow you to plan your DJ sets."); QString playlistsSummary2 = tr("Some DJs construct playlists before they perform live, but others prefer to build them on-the-fly."); QString playlistsSummary3 = tr("When using a playlist during a live DJ set, remember to always pay close attention to how your audience reacts to the music you've chosen to play."); QString playlistsSummary4 = tr("It may be necessary to skip some songs in your prepared playlist or add some different songs in order to maintain the energy of your audience."); QString createPlaylistLink = tr("Create New Playlist"); QString html; html.append(QString("

%1

").arg(playlistsTitle)); html.append(QString("

%1

").arg(playlistsSummary)); html.append(QString("

%1

").arg(playlistsSummary2)); html.append(QString("

%1
%2

").arg(playlistsSummary3, playlistsSummary4)); html.append(QString("%1") .arg(createPlaylistLink)); return html; }