Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add "Last Played" column to library #2670

Closed
wants to merge 27 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
6e5acb1
last-played-datetime: initial work to create a sortable column for la…
ywwg Apr 16, 2020
a920568
last played time: Respond to setlog changes
ywwg Apr 16, 2020
36a0e54
last played: cleanup before review
ywwg Apr 16, 2020
95446b1
last played: tiny optimization
ywwg Apr 16, 2020
06f4ed6
last played: triple slashes for docs
ywwg Apr 16, 2020
ff8a303
last played: this change breaks the PR. Why doesn't bindValue work??
ywwg Apr 16, 2020
a7960e2
last played: fix bindvalue uses
ywwg Apr 17, 2020
7cd7eaa
last played: oops can't return nothing
ywwg Apr 17, 2020
b7a8236
last played: oops use the right conditional
ywwg Apr 17, 2020
8b42a7c
last played: address notes
ywwg Apr 17, 2020
a013e9e
last played: make const extern
ywwg Apr 17, 2020
cbbff3f
last played: fix naming
ywwg Apr 17, 2020
9fe8ba0
last played: persist and reuse QSqlQuery
ywwg Apr 18, 2020
9d50d62
last played: check for not-found
ywwg Apr 18, 2020
f7ec07d
last played: make sure query is finished
ywwg Apr 18, 2020
2ebf657
Merge branch 'master' into last-played
ywwg Apr 20, 2020
46ba736
Merge branch 'master' into last-played
ywwg Apr 27, 2020
ddb88eb
Merge branch 'master' into last-played
ywwg May 9, 2020
eb3c64f
Merge branch 'master' into last-played
ywwg Jun 17, 2020
55d5350
Merge branch 'master' into last-played
ywwg Jun 17, 2020
8734cf8
Last Played: fix not reprocessing tracks from deleted playlists
ywwg Jun 17, 2020
b6b4ed4
Last Played: minor cleanup
ywwg Jun 17, 2020
a8576f8
Last Played: line wrap
ywwg Jun 17, 2020
975cb46
Merge branch 'master' into last-played
ywwg Jul 17, 2020
a22a3cd
Merge branch 'master' into last-played
ywwg Sep 11, 2020
c718c15
Merge branch 'master' into last-played
ywwg Sep 13, 2020
bd27ffb
Fix bad merge
ywwg Sep 13, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,7 @@ add_library(mixxx-lib STATIC EXCLUDE_FROM_ALL
src/library/externaltrackcollection.cpp
src/library/hiddentablemodel.cpp
src/library/itunes/itunesfeature.cpp
src/library/lastplayedcache.cpp
src/library/library.cpp
src/library/librarycontrol.cpp
src/library/libraryfeature.cpp
Expand Down
3 changes: 3 additions & 0 deletions src/library/basesqltablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ void BaseSqlTableModel::initSortColumnMapping() {
m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_BPM] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM);
m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_REPLAYGAIN] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_REPLAYGAIN);
m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_DATETIMEADDED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED);
m_columnIndexBySortColumnId
[TrackModel::SortColumnId::SORTCOLUMN_LASTPLAYEDDATETIME] =
fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEPLAYED);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commits in the history that do not even build are bad for git bisect. Redo and force push.

m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_TIMESPLAYED] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_TIMESPLAYED);
m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_RATING] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_RATING);
m_columnIndexBySortColumnId[TrackModel::SortColumnId::SORTCOLUMN_KEY] = fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_KEY);
Expand Down
2 changes: 2 additions & 0 deletions src/library/basetrackcache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ void BaseTrackCache::getTrackValueForColumn(TrackPointer pTrack,
trackValue.setValue(pTrack->getYear());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED) == column) {
trackValue.setValue(pTrack->getDateAdded());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEPLAYED) == column) {
trackValue.setValue(pTrack->getLastPlayedDate());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_GENRE) == column) {
trackValue.setValue(pTrack->getGenre());
} else if (fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COMPOSER) == column) {
Expand Down
9 changes: 9 additions & 0 deletions src/library/basetracktablemodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,10 @@ void BaseTrackTableModel::initHeaderProperties() {
ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED,
tr("Date Added"),
defaultColumnWidth() * 3);
setHeaderProperties(
ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEPLAYED,
tr("Last Time Played"),
defaultColumnWidth() * 3);
setHeaderProperties(
ColumnCache::COLUMN_LIBRARYTABLE_DURATION,
tr("Duration"),
Expand Down Expand Up @@ -502,6 +506,10 @@ QVariant BaseTrackTableModel::roleValue(
return mixxx::localDateTimeFromUtc(mixxx::convertVariantToDateTime(rawValue));
} else if (column == fieldIndex(ColumnCache::COLUMN_PLAYLISTTRACKSTABLE_DATETIMEADDED)) {
return mixxx::localDateTimeFromUtc(mixxx::convertVariantToDateTime(rawValue));
} else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEPLAYED)) {
QDateTime gmtDate = rawValue.toDateTime();
gmtDate.setTimeSpec(Qt::UTC);
return gmtDate.toLocalTime();
} else if (column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_BPM)) {
bool ok;
const auto bpmValue = rawValue.toDouble(&ok);
Expand Down Expand Up @@ -636,6 +644,7 @@ Qt::ItemFlags BaseTrackTableModel::readWriteFlags(
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COLOR) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_COVERART) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEADDED) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DATETIMEPLAYED) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_DURATION) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_FILETYPE) ||
column == fieldIndex(ColumnCache::COLUMN_LIBRARYTABLE_NATIVELOCATION) ||
Expand Down
2 changes: 2 additions & 0 deletions src/library/columncache.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ void ColumnCache::setColumns(const QStringList& columns) {
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_CHANNELS] = fieldIndex(LIBRARYTABLE_CHANNELS);
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_MIXXXDELETED] = fieldIndex(LIBRARYTABLE_MIXXXDELETED);
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_DATETIMEADDED] = fieldIndex(LIBRARYTABLE_DATETIMEADDED);
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_DATETIMEPLAYED] =
fieldIndex(LIBRARYTABLE_DATETIMEPLAYED);
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_HEADERPARSED] = fieldIndex(LIBRARYTABLE_HEADERPARSED);
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_TIMESPLAYED] = fieldIndex(LIBRARYTABLE_TIMESPLAYED);
m_columnIndexByEnum[COLUMN_LIBRARYTABLE_PLAYED] = fieldIndex(LIBRARYTABLE_PLAYED);
Expand Down
1 change: 1 addition & 0 deletions src/library/columncache.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class ColumnCache : public QObject {

COLUMN_REKORDBOX_ANALYZE_PATH,

COLUMN_LIBRARYTABLE_DATETIMEPLAYED,
// NUM_COLUMNS should always be the last item.
NUM_COLUMNS
};
Expand Down
3 changes: 1 addition & 2 deletions src/library/dao/autodjcratesdao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1047,8 +1047,7 @@ void AutoDJCratesDAO::slotPlaylistTrackAdded(int playlistId, TrackId trackId,

// Signaled by the playlist DAO when a track is removed from a playlist.
void AutoDJCratesDAO::slotPlaylistTrackRemoved(int playlistId,
TrackId trackId,
int /* a_iPosition */) {
TrackId trackId) {
// Deal with changes to the auto-DJ playlist.
if (playlistId == m_iAutoDjPlaylistId) {
QSqlQuery oQuery(m_database);
Expand Down
3 changes: 1 addition & 2 deletions src/library/dao/autodjcratesdao.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,7 @@ class AutoDJCratesDAO : public QObject {
int position);

// Signaled by the playlist DAO when a track is removed from a playlist.
void slotPlaylistTrackRemoved(int playlistId, TrackId trackId,
int position);
void slotPlaylistTrackRemoved(int playlistId, TrackId trackId);

// Signaled by the PlayerInfo singleton when a track is loaded to, or
// unloaded from, a deck.
Expand Down
7 changes: 6 additions & 1 deletion src/library/dao/playlistdao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ int PlaylistDAO::getPlaylistIdFromName(const QString& name) const {

void PlaylistDAO::deletePlaylist(const int playlistId) {
//qDebug() << "PlaylistDAO::deletePlaylist" << QThread::currentThread() << m_database.connectionName();
const auto trackIds = getTrackIds(playlistId);

ScopedTransaction transaction(m_database);

// Get the playlist id for this
Expand Down Expand Up @@ -205,6 +207,9 @@ void PlaylistDAO::deletePlaylist(const int playlistId) {
}
}

for (const auto& trackId : trackIds) {
emit trackRemoved(playlistId, trackId);
}
emit deleted(playlistId);
}

Expand Down Expand Up @@ -516,7 +521,7 @@ void PlaylistDAO::removeTracksFromPlaylistInner(int playlistId, int position) {
}

m_playlistsTrackIsIn.remove(trackId, playlistId);
emit trackRemoved(playlistId, trackId, position);
emit trackRemoved(playlistId, trackId);
}

bool PlaylistDAO::insertTrackIntoPlaylist(TrackId trackId, const int playlistId, int position) {
Expand Down
2 changes: 1 addition & 1 deletion src/library/dao/playlistdao.h
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ class PlaylistDAO : public QObject, public virtual DAO {
void renamed(int playlistId, QString newName);
void lockChanged(int playlistId);
void trackAdded(int playlistId, TrackId trackId, int position);
void trackRemoved(int playlistId, TrackId trackId, int position);
void trackRemoved(int playlistId, TrackId trackId);
void tracksChanged(QSet<int> playlistIds); // added/removed/reordered

private:
Expand Down
12 changes: 12 additions & 0 deletions src/library/dao/trackdao.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ TrackDAO::~TrackDAO() {
addTracksFinish(true);
}

void TrackDAO::initialize(const QSqlDatabase& database) {
DAO::initialize(database);
m_pLastPlayedFetcher.reset(new LastPlayedFetcher(m_database));
}

void TrackDAO::finish() {
qDebug() << "TrackDAO::finish()";

Expand Down Expand Up @@ -1437,6 +1442,13 @@ TrackPointer TrackDAO::getTrackById(TrackId trackId) const {
// Validate and refresh cover image hash values if needed.
pTrack->refreshCoverImageDigest();

VERIFY_OR_DEBUG_ASSERT(m_pLastPlayedFetcher) {
qDebug() << "expected m_pLastPlayedFetcher to be constructed by now";
}
else {
pTrack->setLastPlayedDate(m_pLastPlayedFetcher->fetch(pTrack));
}

// Listen to signals from Track objects and forward them to
// receivers. TrackDAO works as a relay for selected track signals
// that allows receivers to use permanent connections with
Expand Down
12 changes: 8 additions & 4 deletions src/library/dao/trackdao.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@
#define TRACKDAO_H

#include <QFileInfo>
#include <QList>
#include <QObject>
#include <QSet>
#include <QList>
#include <QSqlDatabase>
#include <QString>

#include "preferences/usersettings.h"
#include "library/dao/dao.h"
#include "library/lastplayedcache.h"
#include "library/relocatedtrack.h"
#include "preferences/usersettings.h"
#include "track/globaltrackcache.h"
#include "util/class.h"
#include "util/memory.h"
Expand Down Expand Up @@ -42,10 +43,12 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC
UserSettingsPointer pConfig);
~TrackDAO() override;

void initialize(const QSqlDatabase& database) override;

void finish();

QList<TrackId> resolveTrackIds(
const QList<TrackFile> &trackFiles,
const QList<TrackFile>& trackFiles,
ResolveTrackIdFlags flags = ResolveTrackIdFlag::ResolveOnly);

TrackId getTrackIdByRef(
Expand Down Expand Up @@ -148,7 +151,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC
volatile const bool* pCancel);

void detectCoverArtForTracksWithoutCover(volatile const bool* pCancel,
QSet<TrackId>* pTracksChanged);
QSet<TrackId>* pTracksChanged);

// Callback for GlobalTrackCache
TrackFile relocateCachedTrack(
Expand All @@ -168,6 +171,7 @@ class TrackDAO : public QObject, public virtual DAO, public virtual GlobalTrackC
std::unique_ptr<QSqlQuery> m_pQueryLibraryUpdate;
std::unique_ptr<QSqlQuery> m_pQueryLibrarySelect;
std::unique_ptr<SqlTransaction> m_pTransaction;
std::unique_ptr<LastPlayedFetcher> m_pLastPlayedFetcher;
int m_trackLocationIdColumn;
int m_queryLibraryIdColumn;
int m_queryLibraryMixxxDeletedColumn;
Expand Down
1 change: 1 addition & 0 deletions src/library/dao/trackschema.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const QString LIBRARYTABLE_CHANNELS = QStringLiteral("channels");
const QString LIBRARYTABLE_MIXXXDELETED = QStringLiteral("mixxx_deleted");
const QString LIBRARYTABLE_DATETIMEADDED = QStringLiteral("datetime_added");
const QString LIBRARYTABLE_HEADERPARSED = QStringLiteral("header_parsed");
const QString LIBRARYTABLE_DATETIMEPLAYED = QStringLiteral("datetime_last_played");
const QString LIBRARYTABLE_TIMESPLAYED = QStringLiteral("timesplayed");
const QString LIBRARYTABLE_PLAYED = QStringLiteral("played");
const QString LIBRARYTABLE_RATING = QStringLiteral("rating");
Expand Down
81 changes: 81 additions & 0 deletions src/library/lastplayedcache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#include "library/lastplayedcache.h"

#include <QSqlDatabase>
#include <QtDebug>

#include "library/dao/playlistdao.h"
#include "library/trackcollection.h"

const QString LASTPLAYEDTABLE_NAME = "last_played_datetimes";

QDateTime LastPlayedFetcher::fetch(TrackPointer pTrack) {
m_fetchQuery.prepare(
"SELECT datetime_played FROM " +
LASTPLAYEDTABLE_NAME +
" WHERE track_id = :trackid");
m_fetchQuery.bindValue(":trackid", pTrack->getId().toVariant());
if (!m_fetchQuery.exec()) {
LOG_FAILED_QUERY(m_fetchQuery);
return QDateTime();
}
if (!m_fetchQuery.first()) {
return QDateTime();
}
QDateTime ret = m_fetchQuery.value(0).toDateTime();
m_fetchQuery.finish();
return ret;
}

LastPlayedCache::LastPlayedCache(TrackCollection* trackCollection)
: m_pTrackCollection(trackCollection),
m_helper(trackCollection->database()) {
initTableView();

connect(&m_pTrackCollection->getPlaylistDAO(),
&PlaylistDAO::trackAdded,
this,
[=](int playlistId, TrackId trackId) {
playlistTrackChanged(playlistId, trackId);
});
connect(&m_pTrackCollection->getPlaylistDAO(),
&PlaylistDAO::trackRemoved,
this,
[=](int playlistId, TrackId trackId) {
playlistTrackChanged(playlistId, trackId);
});
}

void LastPlayedCache::initTableView() {
QSqlQuery lastPlayedQuery(m_pTrackCollection->database());
// Views can't use params, so just use .args here. There is no injection
// risk because these are constants.
lastPlayedQuery.prepare(QStringLiteral(
"CREATE TEMPORARY VIEW IF NOT EXISTS %1 AS "
"SELECT "
" PlaylistTracks.track_id, "
" MAX(PlaylistTracks.pl_datetime_added) as datetime_played "
"FROM PlaylistTracks "
"JOIN Playlists ON PlaylistTracks.playlist_id == Playlists.id "
"WHERE Playlists.hidden = %2 "
"GROUP BY PlaylistTracks.track_id")
.arg(LASTPLAYEDTABLE_NAME,
QString::number(PlaylistDAO::
PLHT_SET_LOG)));
if (!lastPlayedQuery.exec()) {
LOG_FAILED_QUERY(lastPlayedQuery);
}
}

void LastPlayedCache::playlistTrackChanged(
int playlistId, TrackId trackId) {
const auto type = m_pTrackCollection->getPlaylistDAO().getHiddenType(playlistId);
// Deleted playlists show up as unknown type
if (type != PlaylistDAO::PLHT_SET_LOG && type != PlaylistDAO::PLHT_UNKNOWN) {
return;
}

TrackPointer pTrack = m_pTrackCollection->getTrackById(trackId);
if (pTrack) {
pTrack->setLastPlayedDate(m_helper.fetch(pTrack));
}
}
48 changes: 48 additions & 0 deletions src/library/lastplayedcache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#pragma once

#include <QObject>
#include <QSet>
#include <QSqlDatabase>
#include <QString>

#include "library/queryutil.h"
#include "track/track.h"
#include "track/trackid.h"

class TrackCollection;

extern const QString LASTPLAYEDTABLE_NAME;

/// LastPlayedFetcher is a small object for fetching last played times from the
/// database. It holds and reuses a QSqlQuery for efficiency.
class LastPlayedFetcher {
public:
explicit LastPlayedFetcher(QSqlDatabase db)
: m_fetchQuery(db) {
}

/// Fetches the last played time for the given track. Returns an empty (null)
/// QDateTime on error.
QDateTime fetch(TrackPointer pTrack);

private:
QSqlQuery m_fetchQuery;
};

/// LastPlayedCache is a helper object for creating and maintaining
/// the "last_played" temporary view on the database. It creates the
/// view and also watches for changes to the Setlog Playlists and updates
/// tracks in the cache as necessary.
class LastPlayedCache : public QObject {
Q_OBJECT
public:
explicit LastPlayedCache(TrackCollection* trackCollection);
~LastPlayedCache() override = default;

private:
void initTableView();
void playlistTrackChanged(int playlistId, TrackId trackId);

TrackCollection* const m_pTrackCollection;
LastPlayedFetcher m_helper;
};
Loading