Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
17 changes: 16 additions & 1 deletion src/backend/model/gaming/Game.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,10 @@ void Game::onEntryPlayStatsChanged()
const auto prev_play_time = m_data.playstats.play_time;
const auto prev_last_played = m_data.playstats.last_played;

Q_ASSERT(filesModel());
// this is null if called inside PlaytimeStats::run
if (filesModel() == nullptr)
return;

const std::vector<model::GameFile*>& filelist = filesModel()->entries();

m_data.playstats.play_count = std::accumulate(filelist.cbegin(), filelist.cend(), 0,
Expand All @@ -106,6 +109,10 @@ void Game::onEntryPlayStatsChanged()
return std::max(current_max, gamefile->lastPlayed());
});

m_data.playstats.play_count += m_data.playstats_self.play_count;
m_data.playstats.play_time += m_data.playstats_self.play_time;
m_data.playstats.last_played = std::max(m_data.playstats.last_played, m_data.playstats_self.last_played);

const bool changed = prev_play_count != m_data.playstats.play_count
|| prev_play_time != m_data.playstats.play_time
|| prev_last_played != m_data.playstats.last_played;
Expand All @@ -123,6 +130,14 @@ void Game::launch()
emit launchFileSelectorRequested();
}

void Game::update_playstats(int playcount, qint64 playtime, QDateTime last_played)
{
m_data.playstats_self.last_played = std::max(m_data.playstats_self.last_played, std::move(last_played));
m_data.playstats_self.play_time += playtime;
m_data.playstats_self.play_count += playcount;
onEntryPlayStatsChanged();
}

Game& Game::setFiles(std::vector<model::GameFile*>&& files)
{
for (model::GameFile* const gamefile : files) {
Expand Down
9 changes: 9 additions & 0 deletions src/backend/model/gaming/Game.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct GameData {
QString sort_by;
QString summary;
QString description;
QString slug;

QStringList developers;
QStringList publishers;
Expand All @@ -59,6 +60,9 @@ struct GameData {
QDateTime last_played;
} playstats;

// playstats tied to the game itself and not a specific file
PlayStats playstats_self;

bool is_favorite = false;
bool missing = false;

Expand All @@ -81,6 +85,8 @@ class Game : public QObject {
GETTER(const QString&, sortBy, sort_by)
GETTER(const QString&, summary, summary)
GETTER(const QString&, description, description)
GETTER(const QString&, slug, slug)
bool hasSlug() const { return !m_data.slug.isEmpty(); }
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated
GETTER(const QDate&, releaseDate, release_date)
GETTER(int, playerCount, player_count)
GETTER(float, rating, rating)
Expand Down Expand Up @@ -113,6 +119,7 @@ class Game : public QObject {
SETTER(QString, SortBy, sort_by)
SETTER(QString, Summary, summary)
SETTER(QString, Description, description)
SETTER(QString, Slug, slug)
SETTER(QDate, ReleaseDate, release_date)

SETTER(QString, LaunchCmd, launch_params.launch_cmd)
Expand Down Expand Up @@ -144,6 +151,7 @@ class Game : public QObject {
Q_PROPERTY(QString sortBy READ sortBy CONSTANT)
Q_PROPERTY(QString summary READ summary CONSTANT)
Q_PROPERTY(QString description READ description CONSTANT)
Q_PROPERTY(QString slug READ slug CONSTANT)
Q_PROPERTY(QDate release READ releaseDate CONSTANT)
Q_PROPERTY(int players READ playerCount CONSTANT)
Q_PROPERTY(float rating READ rating CONSTANT)
Expand Down Expand Up @@ -201,6 +209,7 @@ private slots:
explicit Game(QString name, QObject* parent = nullptr);

Q_INVOKABLE void launch();
void update_playstats(int playcount, qint64 playtime, QDateTime last_played);

void finalize();
};
Expand Down
11 changes: 10 additions & 1 deletion src/backend/providers/SearchContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,15 @@ model::Game* SearchContext::create_game()
return game_ptr;
}

std::vector<model::Game*> SearchContext::games_by_slug(const QString& slug) const
{
std::vector<model::Game*> found_games;
for (const auto& pair : m_game_entries) {
if (pair.first->slug() == slug) found_games.emplace_back(pair.first);
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated
}
return found_games;
}

model::Game* SearchContext::game_by_filepath(const QString& can_path) const
{
model::GameFile* const entry_ptr = gamefile_by_filepath(can_path);
Expand Down Expand Up @@ -249,7 +258,7 @@ void SearchContext::finalize_apply_lists()
for (auto& pair : m_collection_games) {
VEC_REMOVE_DUPLICATES(pair.second);
pair.first->setGames(std::move(pair.second));
}
}
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated
}

std::pair<std::vector<model::Collection*>, std::vector<model::Game*>> SearchContext::finalize(QObject* const parent)
Expand Down
1 change: 1 addition & 0 deletions src/backend/providers/SearchContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class SearchContext : public QObject {
model::Game* create_game();
SearchContext& game_add_to(model::Game&, model::Collection&);

std::vector<model::Game*> games_by_slug(const QString&) const;
model::Game* game_by_filepath(const QString&) const;
model::Game* game_by_uri(const QString&) const;
model::GameFile* gamefile_by_filepath(const QString&) const;
Expand Down
8 changes: 8 additions & 0 deletions src/backend/providers/pegasus_metadata/PegasusMetadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ enum class GameAttrib : unsigned char {
LAUNCH_CMD,
LAUNCH_WORKDIR,
SORT_BY,
SLUG,
};


Expand Down Expand Up @@ -134,6 +135,7 @@ Metadata::Metadata(QString log_tag)
{ QStringLiteral("description"), GameAttrib::LONG_DESC },
{ QStringLiteral("release"), GameAttrib::RELEASE },
{ QStringLiteral("rating"), GameAttrib::RATING },
{ QStringLiteral("slug"), GameAttrib::SLUG },
// sort title variations
{ QStringLiteral("sorttitle"), GameAttrib::SORT_BY },
{ QStringLiteral("sortname"), GameAttrib::SORT_BY },
Expand Down Expand Up @@ -426,6 +428,12 @@ void Metadata::apply_game_entry(ParserState& ps, const metafile::Entry& entry, S
case GameAttrib::SORT_BY:
ps.cur_game->setSortBy(first_line_of(ps, entry));
break;
case GameAttrib::SLUG:
// normalize slug to lowercase without spaces
ps.cur_game->setSlug(first_line_of(ps, entry)
.toLower()
.remove(QLatin1String(R"( )")));
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated
break;
}
}

Expand Down
67 changes: 48 additions & 19 deletions src/backend/providers/pegasus_playtime/PlaytimeStats.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@


namespace {
const QString SLUG_PREFIX = QStringLiteral("pg-slug:");
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated
QString default_db_path()
{
return paths::writableConfigDir() + QStringLiteral("/stats.db");
Expand Down Expand Up @@ -162,8 +163,12 @@ void migrate_play_entry(const QString& log_tag, int old_path_id, int new_path_id

void update_modelgame(model::GameFile* const gamefile, const QDateTime& start_time, const qint64 duration)
{
Q_ASSERT(gamefile);
gamefile->update_playstats(1, duration, start_time.addSecs(duration));
Q_ASSERT(gamefile && gamefile->parentGame());
model::Game* game = gamefile->parentGame();
if (game->hasSlug())
game->update_playstats(1, duration, start_time.addSecs(duration));
else
gamefile->update_playstats(1, duration, start_time.addSecs(duration));
Comment thread
itzTheMeow marked this conversation as resolved.
}

} // namespace
Expand Down Expand Up @@ -214,34 +219,52 @@ Provider& PlaytimeStats::run(SearchContext& sctx)
qint64 playtime { 0 };
QDateTime last_played;
};
HashMap<model::GameFile*, Stats> stat_map;
HashMap<model::Game*, Stats> stat_map_game;
HashMap<model::GameFile*, Stats> stat_map_gamefile;

QMutexLocker lock(&m_queue_guard);

while (query.next()) {
const QString path = query.value(0).toString();
model::GameFile* game_ptr = sctx.gamefile_by_uri(path);
if (!game_ptr) {
game_ptr = sctx.gamefile_by_filepath(path);
// schedule a data migration if path is being used for a URI game
if (game_ptr && game_ptr->hasUri())
m_pending_migrations.emplace_back(game_ptr);
}
if (!game_ptr)
continue;

const qint64 start_epoch = query.value(1).toLongLong();
const qint64 duration = query.value(2).toLongLong();

Stats& stats = stat_map[game_ptr];
stats.last_played = QDateTime::fromSecsSinceEpoch(start_epoch + duration);
stats.playtime += std::max(static_cast<qint64>(0), duration);
stats.playcount++;
const auto update_stats = [&](Stats& stats) {
stats.last_played = QDateTime::fromSecsSinceEpoch(start_epoch + duration);
stats.playtime += std::max(static_cast<qint64>(0), duration);
stats.playcount++;
};

if (path.startsWith(SLUG_PREFIX)) {
std::vector<model::Game*> games = sctx.games_by_slug(path.mid(SLUG_PREFIX.length()));
if (games.empty())
continue;
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated

for (model::Game* game_ptr : games)
update_stats(stat_map_game[game_ptr]);
} else {
model::GameFile* game_ptr = sctx.gamefile_by_uri(path);
if (!game_ptr) {
game_ptr = sctx.gamefile_by_filepath(path);
// schedule a data migration if path is being used for a URI game
if (game_ptr && game_ptr->hasUri())
m_pending_migrations.emplace_back(game_ptr);
}
if (!game_ptr)
continue;
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated

update_stats(stat_map_gamefile[game_ptr]);
}
}

// trigger update only once
// TODO: C++17
for (const auto& pair : stat_map) {
for (const auto& pair : stat_map_game) {
model::Game* const game = pair.first;
const Stats& stats = pair.second;
game->update_playstats(stats.playcount, stats.playtime, stats.last_played);
}
for (const auto& pair : stat_map_gamefile) {
model::GameFile* const gamefile = pair.first;
const Stats& stats = pair.second;
gamefile->update_playstats(stats.playcount, stats.playtime, stats.last_played);
Expand Down Expand Up @@ -309,8 +332,14 @@ void PlaytimeStats::start_processing()
}

for (const QueueEntry& entry : m_active_tasks) {
const QString path = entry.gamefile->hasUri()
const model::Game* game = entry.gamefile->parentGame();
const QString path = !game->hasSlug() ?
// game slug overrides everything
SLUG_PREFIX + game->slug()
// use URI if provided
: entry.gamefile->hasUri()
? entry.gamefile->uri()
// otherwise use the file path
Comment thread
itzTheMeow marked this conversation as resolved.
Outdated
: ::clean_abs_path(entry.gamefile->fileinfo());
const int path_id = get_path_id(display_name(), path);
if (path_id >= 0)
Expand Down
66 changes: 66 additions & 0 deletions tests/backend/providers/playtime/test_Playtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
#include "providers/pegasus_playtime/PlaytimeStats.h"

#include <QSqlDatabase>
#include <QSqlQuery>


namespace {
Expand Down Expand Up @@ -50,6 +51,8 @@ private slots:
void read();
void write();
void write_queue();
void write_slug();
void read_slug();
};

void test_Playtime::read()
Expand Down Expand Up @@ -135,5 +138,68 @@ void test_Playtime::write_queue()
}


void test_Playtime::write_slug()
{
QTemporaryFile db_file;
QVERIFY(db_file.open());

providers::SearchContext sctx;
model::Collection& collection = *sctx.get_or_create_collection(QStringLiteral("coll_slug"));
model::Game& game = *sctx.create_game_for(collection);
sctx.game_add_filepath(game, QStringLiteral("dummy_slug"));
game.setSlug(QStringLiteral("my-slug"));

providers::playtime::PlaytimeStats playtime(db_file.fileName());
const auto [collections, games] = sctx.finalize(this);

QSignalSpy spy_start(&playtime, &providers::playtime::PlaytimeStats::startedWriting);
QSignalSpy spy_end(&playtime, &providers::playtime::PlaytimeStats::finishedWriting);
QVERIFY(spy_start.isValid() && spy_end.isValid());

playtime.onGameLaunched(games.at(0)->filesModel()->entries().front());
playtime.onGameFinished(games.at(0)->filesModel()->entries().front());

QVERIFY(spy_start.count() || spy_start.wait());
QVERIFY(spy_end.count() || spy_end.wait());
QCOMPARE(spy_start.count(), 1);
QCOMPARE(spy_end.count(), 1);

QCOMPARE(games.at(0)->property("playCount").toInt(), 1);
}

void test_Playtime::read_slug()
{
QTemporaryFile db_file;
QVERIFY(db_file.open());

{
QSqlDatabase db = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("slug_read"));
db.setDatabaseName(db_file.fileName());
QVERIFY(db.open());

QSqlQuery query(db);
query.exec(QStringLiteral("CREATE TABLE paths (id INTEGER PRIMARY KEY, path TEXT UNIQUE NOT NULL);"));
query.exec(QStringLiteral("CREATE TABLE plays (id INTEGER PRIMARY KEY, path_id INTEGER NOT NULL REFERENCES plays(id), start_time INTEGER NOT NULL, duration INTEGER NOT NULL);"));
query.exec(QStringLiteral("INSERT INTO paths VALUES (1, 'pg-slug:my-slug');"));
query.exec(QStringLiteral("INSERT INTO plays VALUES (1, 1, 1531755000, 35);"));
db.close();
}
QSqlDatabase::removeDatabase(QStringLiteral("slug_read"));

providers::SearchContext sctx;
model::Collection& collection = *sctx.get_or_create_collection(QStringLiteral("coll_slug"));
model::Game& game = *sctx.create_game_for(collection);
sctx.game_add_filepath(game, QStringLiteral("/my/game"));
game.setSlug(QStringLiteral("my-slug"));

providers::playtime::PlaytimeStats(db_file.fileName()).run(sctx);
const auto [collections, games] = sctx.finalize(this);

QCOMPARE(games.at(0)->property("playCount").toInt(), 1);
QCOMPARE(games.at(0)->property("playTime").toInt(), 35);
QCOMPARE(games.at(0)->property("lastPlayed").toDateTime(), QDateTime::fromSecsSinceEpoch(1531755035));
}


QTEST_MAIN(test_Playtime)
#include "test_Playtime.moc"
Loading