]> git.sesse.net Git - pkanalytics/blob - stats.cpp
Move some meaty member functions out-of-line from EventsModel.
[pkanalytics] / stats.cpp
1 #include <QMediaPlayer>
2 #include <QMainWindow>
3 #include <QApplication>
4 #include <QGridLayout>
5 #include <QVideoWidget>
6 #include <QShortcut>
7 #include <algorithm>
8 #include <string>
9 #include <map>
10 #include <vector>
11 #include <optional>
12 #include <sqlite3.h>
13 #include "mainwindow.h"
14 #include "ui_mainwindow.h"
15
16 using namespace std;
17
18 string format_timestamp(uint64_t pos)
19 {
20         int ms = pos % 1000;
21         pos /= 1000;
22         int sec = pos % 60;
23         pos /= 60;
24         int min = pos % 60;
25         int hour = pos / 60;
26
27         char buf[256];
28         snprintf(buf, sizeof(buf), "%d:%02d:%02d.%03d", hour, min, sec, ms);
29         return buf;
30 }
31
32 class EventsModel : public QAbstractTableModel
33 {
34 public:
35         EventsModel(sqlite3 *db) : db(db) {}
36
37         int rowCount(const QModelIndex &parent) const override
38         {
39                 refresh_if_needed();
40                 return events.size();
41         }
42         int columnCount(const QModelIndex &column) const override
43         {
44                 return 3;
45         }
46         QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
47         QVariant data(const QModelIndex &index, int role) const override;
48
49 private:
50         struct Player {
51                 int player_id;
52                 string number;
53                 string name;
54         };
55         mutable map<int, Player> players;
56
57         struct Event {
58                 uint64_t t;
59                 optional<int> player_id;
60                 string type;
61         };
62         mutable vector<Event> events;
63         mutable bool stale = true;
64
65         sqlite3 *db;
66
67         void refresh_if_needed() const;
68 };
69
70 QVariant EventsModel::headerData(int section, Qt::Orientation orientation, int role) const
71 {
72         if (role != Qt::DisplayRole) {
73                 return QVariant();
74         }
75         if (orientation == Qt::Horizontal) {
76                 if (section == 0) {
77                         return "Time";
78                 } else if (section == 1) {
79                         return "Player";
80                 } else {
81                         return "Type";
82                 }
83         } else {
84                 return "";
85         }
86 }
87
88 QVariant EventsModel::data(const QModelIndex &index, int role) const
89 {
90         if (role != Qt::DisplayRole) {
91                 return QVariant();
92         }
93         refresh_if_needed();
94         if (index.column() == 0) {
95                 return QString::fromUtf8(format_timestamp(events[index.row()].t));
96         } else if (index.column() == 1) {
97                 optional<int> player_id = events[index.row()].player_id;
98                 if (player_id) {
99                         const Player &p = players[*player_id];
100                         return QString::fromUtf8(p.name + " (" + p.number + ")");
101                 } else {
102                         return QVariant();
103                 }
104         } else if (index.column() == 2) {
105                 return QString::fromUtf8(events[index.row()].type);
106         }
107         return QVariant();
108 }
109
110 void EventsModel::refresh_if_needed() const
111 {
112         if (!stale) {
113                 return;
114         }
115
116         players.clear();
117         events.clear();
118         stale = false;
119
120         // Read the players.
121         sqlite3_stmt *stmt;
122         int ret = sqlite3_prepare_v2(db, "SELECT player, number, name FROM player", -1, &stmt, 0);
123         if (ret != SQLITE_OK) {
124                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
125                 abort();
126         }
127         for ( ;; ) {
128                 ret = sqlite3_step(stmt);
129                 if (ret == SQLITE_ROW) {
130                         Player p;
131                         p.player_id = sqlite3_column_int(stmt, 0);
132                         p.number = (const char *)sqlite3_column_text(stmt, 1);
133                         p.name = (const char *) sqlite3_column_text(stmt, 2);
134                         players[p.player_id] = move(p);
135                 } else if (ret == SQLITE_DONE) {
136                         break;
137                 } else {
138                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
139                         abort();
140                 }
141         }
142         ret = sqlite3_finalize(stmt);
143         if (ret != SQLITE_OK) {
144                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
145                 abort();
146         }
147
148         // Read the events.
149         ret = sqlite3_prepare_v2(db, "SELECT t, player, type FROM event", -1, &stmt, 0);
150         if (ret != SQLITE_OK) {
151                 fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
152                 abort();
153         }
154         for ( ;; ) {
155                 ret = sqlite3_step(stmt);
156                 if (ret == SQLITE_ROW) {
157                         Event e;
158                         e.t = sqlite3_column_int(stmt, 0);
159                         e.player_id = sqlite3_column_int(stmt, 1);
160                         e.type = (const char *)sqlite3_column_text(stmt, 2);
161                         events.push_back(move(e));
162                 } else if (ret == SQLITE_DONE) {
163                         break;
164                 } else {
165                         fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
166                         abort();
167                 }
168         }
169         ret = sqlite3_finalize(stmt);
170         if (ret != SQLITE_OK) {
171                 fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
172                 abort();
173         }
174
175         // TODO what if data changes externally?
176         //emit dataChanged(QModelIndex(
177 }
178
179 MainWindow::MainWindow()
180 {
181         player = new QMediaPlayer;
182         //player->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate.mkv"));
183         player->setSource(QUrl::fromLocalFile("/home/sesse/dev/stats/ultimate-prores.mkv"));
184         player->play();
185
186         ui = new Ui::MainWindow;
187         ui->setupUi(this);
188
189         connect(player, &QMediaPlayer::positionChanged, [this](uint64_t pos) {
190                 ui->timestamp->setText(QString::fromUtf8(format_timestamp(pos)));
191                 if (buffered_seek) {
192                         player->setPosition(*buffered_seek);
193                         buffered_seek.reset();
194                 }
195                 if (!playing) {
196                         player->pause();  // We only played to get a picture.
197                 }
198         });
199
200         player->setVideoOutput(ui->video);
201
202         connect(ui->minus10s, &QPushButton::clicked, [this]() { seek(-10000); });
203         connect(ui->plus10s, &QPushButton::clicked, [this]() { seek(10000); });
204
205         connect(ui->minus2s, &QPushButton::clicked, [this]() { seek(-2000); });
206         connect(ui->plus2s, &QPushButton::clicked, [this]() { seek(2000); });
207
208         // TODO: Would be nice to actually have a frame...
209         connect(ui->minus1f, &QPushButton::clicked, [this]() { seek(-20); });
210         connect(ui->plus1f, &QPushButton::clicked, [this]() { seek(20); });
211
212         connect(ui->play_pause, &QPushButton::clicked, [this]() {
213                 if (playing) {
214                         player->pause();
215                         ui->play_pause->setText("Play (space)");
216                 } else {
217                         player->setPlaybackRate(1.0);
218                         player->play();
219                         ui->play_pause->setText("Pause (space)");
220                 }
221                 playing = !playing;
222
223                 // Needs to be set anew when we modify setText(), evidently.
224                 ui->play_pause->setShortcut(QCoreApplication::translate("MainWindow", "Space", nullptr));
225         });
226 }
227
228 void MainWindow::setModel(QAbstractItemModel *model)
229 {
230         ui->event_view->setModel(model);
231 }
232
233 void MainWindow::seek(int64_t delta_ms)
234 {
235         int64_t current_pos = buffered_seek ? *buffered_seek : player->position();
236         uint64_t pos = max<int64_t>(current_pos + delta_ms, 0);
237         buffered_seek = pos;
238         if (!playing) {
239                 player->setPlaybackRate(0.01);
240                 player->play();  // Or Qt won't show the seek.
241         }
242 }
243
244 sqlite3 *open_db(const char *filename)
245 {
246         sqlite3 *db;
247         int ret = sqlite3_open(filename, &db);
248         if (ret != SQLITE_OK) {
249                 fprintf(stderr, "%s: %s\n", filename, sqlite3_errmsg(db));
250                 exit(1);
251         }
252
253         sqlite3_exec(db, R"(
254                 CREATE TABLE IF NOT EXISTS player (player INTEGER PRIMARY KEY, number VARCHAR, name VARCHAR);
255         )", nullptr, nullptr, nullptr);  // Ignore errors.
256
257         sqlite3_exec(db, R"(
258                 CREATE TABLE IF NOT EXISTS event (t INTEGER, player INTEGER, type VARCHAR, FOREIGN KEY (player) REFERENCES player(player));
259         )", nullptr, nullptr, nullptr);  // Ignore errors.
260
261         sqlite3_exec(db, "PRAGMA journal_mode=WAL", nullptr, nullptr, nullptr);  // Ignore errors.
262         sqlite3_exec(db, "PRAGMA synchronous=NORMAL", nullptr, nullptr, nullptr);  // Ignore errors.
263         return db;
264 }
265
266 int main(int argc, char *argv[])
267 {
268         QApplication app(argc, argv);
269         sqlite3 *db = open_db("ultimate.db");
270
271         MainWindow mainWindow;
272         mainWindow.setModel(new EventsModel(db));
273         mainWindow.resize(QSize(1280, 720));
274         mainWindow.show();
275
276         return app.exec();
277
278 }