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