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