good progress

This commit is contained in:
BotoX 2021-05-23 16:55:57 +02:00
parent 90ea2174db
commit fb7ade79cb
12 changed files with 671 additions and 189 deletions

11
README.md Normal file
View File

@ -0,0 +1,11 @@
Populate player name (from most used one) after database is fully populated:
```SQL
UPDATE `player` SET `name`=(
SELECT `name`
FROM `player_names`
WHERE `player_names`.guid = `player`.guid
GROUP BY guid, name
ORDER BY MAX(`time`) DESC LIMIT 1
);
```

View File

@ -1,5 +1,5 @@
from contextlib import nullcontext
from datetime import datetime
from datetime import datetime, timedelta
from flask import Flask, g, has_app_context, request
from .extensions import default_config
@ -17,6 +17,21 @@ def register_jinja2_filters(app):
def to_json(value):
return orjson.dumps(value).decode('utf-8')
@app.template_filter('to_duration')
def to_duration(seconds):
out = ''
(days, remainder) = divmod(seconds, 86400)
(hours, remainder) = divmod(remainder, 3600)
(minutes, seconds) = divmod(remainder, 60)
if days:
out += f'{days:.0f}d '
if hours:
out += f'{hours:.0f}h '
if minutes:
out += f'{minutes:.0f}m '
out += f'{seconds:.0f}s'
return out
def register_blueprints(app):
with app.app_context():

View File

@ -44,15 +44,19 @@ class Player(db.Model):
class PlayerNames(db.Model):
guid = db.Column(db.String(32), primary_key=True)
guid = db.Column(db.String(32), db.ForeignKey('player.guid'), primary_key=True)
name = db.Column(db.String(32), primary_key=True)
time = db.Column(db.Float)
player = db.relationship('Player', backref='names', foreign_keys=[guid])
class PlayerSprays(db.Model):
guid = db.Column(db.String(32), primary_key=True)
guid = db.Column(db.String(32), db.ForeignKey('player.guid'), primary_key=True)
spray = db.Column(db.String(32), primary_key=True)
player = db.relationship('Player', backref='sprays', foreign_keys=[guid])
class PlayerSession(db.Model):
player_guid = db.Column(db.String(32), db.ForeignKey('player.guid'), primary_key=True)
@ -63,6 +67,9 @@ class PlayerSession(db.Model):
kills = db.Column(db.Integer)
voicetime = db.Column(db.Float)
session = db.relationship('Session', foreign_keys=[session_id])
player = db.relationship('Player', backref='sessions', foreign_keys=[player_guid])
class Chat(db.Model):
id = db.Column(db.Integer, primary_key=True)

View File

@ -1,8 +1,7 @@
main { display: flex; flex-wrap: nowrap; height: 100vh; height: -webkit-fill-available; max-height: 100vh; overflow-x: auto; overflow-y: hidden; }
.btn-group-xs > .btn, .btn-xs { padding: .25rem .4rem; font-size: .875rem; line-height: .5; border-radius: .2rem; }
.playlist { margin: 2em 0; }
.playlist .playlist-time-scale { height: 30px; }
.playlist .playlist-tracks { background: #e0eff1; }
@ -31,10 +30,12 @@
.playlist .controls { background: white; text-align: center; border: 1px solid black; border-radius: 0.2rem; }
.playlist .controls .track-header { overflow: hidden; color: black; height: 26px; display: flex; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-bottom: -10px; }
.playlist .controls .track-header { overflow: hidden; color: black; height: 18px; display: flex; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-bottom: -10px; }
.playlist .controls .track-header button { width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; }
.playlist .controls .track-header span { margin-top: -8px; }
.playlist .controls input[type="range"] { display: inline-block; width: 90%; }
.playlist .controls .user-info { word-break: break-word; color: black; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-top: -5px; }
.playlist .controls .info { word-break: break-word; color: black; align-items: center; justify-content: space-between; padding: 0 0.2rem; font-size: 0.65rem; margin-top: -10px; }

View File

@ -1,7 +1,7 @@
var playlist = WaveformPlaylist.init({
container: document.getElementById("playlist"),
timescale: true,
state: 'cursor',
state: 'select',
samplesPerPixel: 16384,
zoomLevels: [2048, 4096, 8192, 16384],
@ -12,23 +12,58 @@ var playlist = WaveformPlaylist.init({
stereoPan: false,
}
},
waveHeight: 96,
waveHeight: 80,
});
function onFinishedLoading() {
//initialize the WAV exporter.
playlist.initExporter();
const tracks = playlist.tracks;
for (var i = 0; i < tracks.length; i++) {
playlist.collapseTrack(tracks[i], {collapsed: true});
}
const highlight = window.location.hash.split('#').filter(Boolean);
for(var i = 0; i < highlight.length; i++) {
const guid = highlight[i];
for (var j = 0; j < tracks.length; j++) {
if (tracks[j].name == guid) {
tracks[j].setWaveOutlineColor('#d1e7dd');
}
}
for (var j = 0; j < g_chats.length; j++) {
if (g_chats[j].player_guid == guid) {
chatRows[j].classList.add("table-active");
}
}
}
playlist.drawRequest();
}
function collapseTrack(guid, collapse) {
const tracks = playlist.tracks;
for (var i = 0; i < tracks.length; i++) {
if (guid === null || tracks[i].name == guid) {
playlist.collapseTrack(tracks[i], {collapsed: collapse});
if (guid != null) {
break;
}
}
}
}
function updateTrackInfo(guid, info) {
var tracks = $(".playlist-tracks").children();
const tracks = playlist.tracks;
for (var i = 0; i < tracks.length; i++) {
var track = tracks[i].firstChild;
var trackGuid = track.firstChild.getElementsByTagName('span')[0].innerText;
var userInfoElem = track.lastChild;
if (userInfoElem.className != 'user-info') {
userInfoElem = document.createElement('label');
userInfoElem.className = 'user-info';
track.appendChild(userInfoElem);
if (guid === null || tracks[i].name == guid) {
tracks[i].setInfo(info);
if (guid != null) {
break;
}
if (guid === null || trackGuid == guid) {
userInfoElem.innerText = info;
}
}
}
@ -56,25 +91,144 @@ function clockFormat(seconds, decimals) {
return result;
}
var lastChunkIdx = 0;
var lastUpdateTime = 0;
function updateTime(time) {
$time.html(clockFormat(time, 3));
audioPos = time;
if (time < lastUpdateTime) {
lastChunkIdx = 0;
}
var tick = time / g_session.tickinterval;
var silenceTicks = 0;
for (var i = 0; i < g_session.silence_chunks.length; i++) {
var chunk = g_session.silence_chunks[i];
for (; lastChunkIdx < g_session.silence_chunks.length; lastChunkIdx++) {
var chunk = g_session.silence_chunks[lastChunkIdx];
if (tick > chunk[0]) {
silenceTicks += chunk[1];
silenceTicks = chunk[1];
} else {
break;
}
}
var tickedTime = (tick + silenceTicks) * g_session.tickinterval;
tick += silenceTicks;
var tickedTime = tick * g_session.tickinterval;
$time_.html(clockFormat(tickedTime, 3));
lastUpdateTime = time;
if (lastChunkIdx > 0) {
lastChunkIdx -= 1;
}
onTick(tick, tickedTime);
}
updateTime(audioPos);
function onEvent(idx, event) {
var update = 0;
if (event.event == "player_connect") {
collapseTrack(event.player_guid, false);
updateTrackInfo(event.player_guid, event.data.name);
update += 1;
}
else if (event.event == "player_disconnect") {
collapseTrack(event.player_guid, true);
update += 1;
}
else if (event.event == "player_changename") {
updateTrackInfo(event.player_guid, event.data.newname);
update += 1;
}
return update;
}
updateTime(audioPos);
var chatBox = $("div#chat");
var chatRows = $("div#chat>table>tbody").children();
var lastPrimaryRow = undefined;
function onChat(idx, chat) {
if (idx == lastPrimaryRow) {
return 0;
}
if (lastPrimaryRow != undefined) {
chatRows[lastPrimaryRow].classList.remove("table-primary");
}
chatRows[idx].classList.add("table-primary");
if (autoScrollChat) {
chatRows[idx].scrollIntoViewIfNeeded();
}
lastPrimaryRow = idx;
return 1;
}
var lastTick = undefined;
var lastChatIdx = 0;
var lastEventIdx = 0;
function onTick(tick, time) {
var update = 0;
if (tick == lastTick) {
return;
}
if (tick < lastTick) {
lastChatIdx = 0;
lastEventIdx = 0;
}
for (; lastEventIdx < g_events.length; lastEventIdx++) {
const event = g_events[lastEventIdx];
if (tick > event.tick) {
update += onEvent(lastEventIdx, event);
} else {
break;
}
}
if (lastEventIdx > 0) {
lastEventIdx -= 1;
}
for (; lastChatIdx < g_chats.length; lastChatIdx++) {
const chat = g_chats[lastChatIdx];
if (tick < chat.tick) {
if (lastChatIdx > 0) {
lastChatIdx -= 1;
}
update += onChat(lastChatIdx, chat);
break;
}
}
lastTick = tick;
if (update) {
playlist.drawRequest();
}
}
function gameTimeToAudio(tick) {
tick += 1;
var silenceTicks = 0;
for (var i = 0; i < g_session.silence_chunks.length; i++) {
const chunk = g_session.silence_chunks[i];
if ((tick - chunk[1]) > chunk[0]) {
silenceTicks = chunk[1];
} else {
break;
}
}
return (tick - silenceTicks) * g_session.tickinterval;
}
function jumpToGameTick(tick) {
var audioTime = gameTimeToAudio(tick);
playlist.seek(audioTime);
playlist.drawRequest();
updateTime(audioTime);
}
$container.on("click", ".btn-play", function() {
ee.emit("play");
@ -100,7 +254,7 @@ $container.on("click", ".btn-fast-forward", function() {
ee.emit("fastforward");
});
//zoom buttons
// zoom buttons
$container.on("click", ".btn-zoom-in", function() {
ee.emit("zoomin");
});
@ -109,21 +263,102 @@ $container.on("click", ".btn-zoom-out", function() {
ee.emit("zoomout");
});
// download
var downloadUrl = undefined;
var downloadName = undefined;
$container.on("click", ".btn-download", function () {
if (downloadName) {
return;
}
downloadName = g_session.demoname;
if (playlist.isSegmentSelection()) {
const segment = playlist.getTimeSelection();
downloadName += "-" + clockFormat(segment.start).replaceAll(':', '-') + "_" + clockFormat(segment.end).replaceAll(':', '-');
}
downloadName += ".wav";
ee.emit('startaudiorendering', 'wav');
});
ee.on('audiorenderingfinished', function (type, data) {
if (type != 'wav') {
return;
}
if (downloadUrl) {
window.URL.revokeObjectURL(downloadUrl);
}
downloadUrl = window.URL.createObjectURL(data);
const tempLink = document.createElement('a');
tempLink.style.display = 'none';
tempLink.href = downloadUrl;
tempLink.setAttribute('download', downloadName);
document.body.appendChild(tempLink);
tempLink.click();
document.body.removeChild(tempLink);
downloadName = undefined;
});
$container.on("input change", ".master-gain", function(e){
ee.emit("mastervolumechange", e.target.value);
});
$container.find(".master-gain").change();
$container.on("change", ".automatic-scroll", function(e){
ee.emit("automaticscroll", $(e.target).is(':checked'));
var autoScrollVoice = false;
$container.on("change", "#autoscroll_voice", function(e){
autoScrollVoice = $(e.target).is(':checked');
ee.emit("automaticscroll", autoScrollVoice);
});
$container.find(".automatic-scroll").change();
$container.find("#autoscroll_voice").change();
var autoScrollChat = false;
$container.on("change", "#autoscroll_chat", function(e){
autoScrollChat = $(e.target).is(':checked');
});
$container.find("#autoscroll_chat").change();
ee.on("timeupdate", updateTime);
function onFinishedLoading() {
updateTrackInfo(null, "name");
function getParent(el) {
var parent = el.parentNode;
if (parent === document) {
return document;
} else if (parent.offsetHeight < parent.scrollHeight || parent.offsetWidth < parent.scrollWidth) {
return parent;
} else {
return getParent(parent);
}
}
if (!Element.prototype.scrollIntoViewIfNeeded) {
Element.prototype.scrollIntoViewIfNeeded = function (centerIfNeeded) {
centerIfNeeded = arguments.length === 0 ? true : !!centerIfNeeded;
var parent = getParent(this),
parentComputedStyle = window.getComputedStyle(parent, null),
parentBorderTopWidth = parseInt(parentComputedStyle.getPropertyValue('border-top-width')),
parentBorderLeftWidth = parseInt(parentComputedStyle.getPropertyValue('border-left-width')),
overTop = this.offsetTop - parent.offsetTop < parent.scrollTop,
overBottom = (this.offsetTop - parent.offsetTop + this.clientHeight - parentBorderTopWidth) > (parent.scrollTop + parent.clientHeight),
overLeft = this.offsetLeft - parent.offsetLeft < parent.scrollLeft,
overRight = (this.offsetLeft - parent.offsetLeft + this.clientWidth - parentBorderLeftWidth) > (parent.scrollLeft + parent.clientWidth),
alignWithTop = overTop && !overBottom;
if ((overTop || overBottom) && centerIfNeeded) {
parent.scrollTop = this.offsetTop - parent.offsetTop - parent.clientHeight / 2 - parentBorderTopWidth + this.clientHeight / 2;
}
if ((overLeft || overRight) && centerIfNeeded) {
parent.scrollLeft = this.offsetLeft - parent.offsetLeft - parent.clientWidth / 2 - parentBorderLeftWidth + this.clientWidth / 2;
}
if ((overTop || overBottom || overLeft || overRight) && !centerIfNeeded) {
this.scrollIntoView(alignWithTop);
}
};
}

View File

@ -106,7 +106,8 @@ var WaveformPlaylist =
volume: true,
stereoPan: true,
collapse: true,
remove: true
remove: true,
info: true
}
},
colors: {
@ -3465,6 +3466,7 @@ var WaveformPlaylist =
var tracks = audioBuffers.map(function (audioBuffer, index) {
var info = trackList[index];
var name = info.name || 'Untitled';
var infostr = info.info || undefined;
var start = info.start || 0;
var states = info.states || {};
var fadeIn = info.fadeIn;
@ -3487,6 +3489,7 @@ var WaveformPlaylist =
track.src = info.src;
track.setBuffer(audioBuffer);
track.setName(name);
track.setInfo(infostr);
track.setEventEmitter(_this3.ee);
track.setEnabledStates(states);
track.setCues(cueIn, cueOut);
@ -3586,15 +3589,24 @@ var WaveformPlaylist =
if (this.isRendering) {
return;
}
this.isRendering = true;
this.offlineAudioContext = new OfflineAudioContext(2, 44100 * this.duration, 44100);
var duration = this.duration;
var startTime = 0;
var endTime = 0;
if (this.isSegmentSelection()) {
var segment = this.getTimeSelection();
startTime = segment.start;
endTime = segment.end;
duration = endTime - startTime;
}
this.offlineAudioContext = new OfflineAudioContext(1, 44100 * duration, 44100);
var currentTime = this.offlineAudioContext.currentTime;
this.tracks.forEach(function (track) {
track.setOfflinePlayout(new _Playout2.default(_this4.offlineAudioContext, track.buffer));
track.schedulePlay(currentTime, 0, 0, {
track.schedulePlay(currentTime, startTime, endTime, {
shouldPlay: _this4.shouldTrackPlay(track),
masterGain: 1,
isOffline: true
@ -3615,7 +3627,8 @@ var WaveformPlaylist =
_this4.exportWorker.postMessage({
command: 'init',
config: {
sampleRate: 44100
sampleRate: 44100,
stereo: false
}
});
@ -3633,7 +3646,7 @@ var WaveformPlaylist =
// send the channel data from our buffer to the worker
_this4.exportWorker.postMessage({
command: 'record',
buffer: [audioBuffer.getChannelData(0), audioBuffer.getChannelData(1)]
buffer: [audioBuffer.getChannelData(0)]
});
// ask the worker for a WAV
@ -5905,7 +5918,7 @@ var WaveformPlaylist =
if (audioData.byteLength > 16) {
var view = new DataView(audioData);
var wanted = "DEMOPUSHEADER_V1";
var wanted = "DEMOPUSHEADER_V2";
var success = true;
for (var i = 0, n = 16; i < n; i++) {
var c = view.getUint8(i);
@ -5942,29 +5955,41 @@ var WaveformPlaylist =
var _this2 = this;
this.setStateChange(STATE_DECODING);
var parsed = [];
var sampleRate = 0;
var numSamples = 0;
var channels = 1;
var promises = [];
var view = new DataView(demopusData);
var ofs = 16; // skip header
while (ofs < demopusData.byteLength) {
var header = view.getUint8(ofs);
ofs += 1;
if (header == 0x02) {
// opus
var dataLen = Number(view.getBigUint64(ofs, true));
var channels = 1;
var sampleRate = view.getUint32(ofs, true);
ofs += 4;
var numSamples = Number(view.getBigUint64(ofs, true));
ofs += 8;
// output sample rate != input sample rate
numSamples *= this.ac.sampleRate / sampleRate;
var audioBuffer = this.ac.createBuffer(channels, numSamples, this.ac.sampleRate);
while (ofs < demopusData.byteLength) {
var samplesOfs = Number(view.getBigUint64(ofs, true));
ofs += 8;
samplesOfs *= this.ac.sampleRate / sampleRate;
if (ofs >= demopusData.byteLength) {
break;
}
var dataLen = view.getUint32(ofs, true);
ofs += 4;
var opusData = demopusData.slice(ofs, ofs + dataLen);
ofs += dataLen;
var promise = this.ac.decodeAudioData(opusData, function (audioBuffer) {
return audioBuffer;
}, function (err) {
var promise = this.ac.decodeAudioData(opusData, function (decoded) {
var buf = decoded.getChannelData(0);
audioBuffer.copyToChannel(buf, 0, this);
return decoded.length;
}.bind(samplesOfs), function (err) {
if (err === null) {
// Safari issues with null error
return Error('MediaDecodeAudioDataUnknownContentType');
@ -5973,43 +5998,11 @@ var WaveformPlaylist =
}
});
parsed.push(promise);
} else if (header == 0x03) {
// silence
var samples = Number(view.getBigUint64(ofs, true));
ofs += 8;
parsed.push(samples);
} else if (header == 0x01) {
// info
sampleRate = view.getUint32(ofs, true);
ofs += 4;
numSamples = Number(view.getBigUint64(ofs, true));
ofs += 8;
} else if (header == 0x04) {
// done
break;
}
promises.push(promise);
}
return new Promise(function (resolve, reject) {
// output sample rate != input sample rate
numSamples *= _this2.ac.sampleRate / sampleRate;
var audioBuffer = _this2.ac.createBuffer(channels, numSamples, _this2.ac.sampleRate);
return Promise.all(parsed).then(function (result) {
var curSamples = 0;
for (var i = 0; i < result.length; i++) {
var elem = result[i];
if (typeof elem == "number") {
curSamples += elem * (_this2.ac.sampleRate / sampleRate);
} else {
var buf = elem.getChannelData(0);
audioBuffer.copyToChannel(buf, 0, curSamples);
curSamples += elem.length;
}
}
Promise.all(promises).then(function (result) {
_this2.setStateChange(STATE_FINISHED);
resolve(audioBuffer);
});
@ -6505,6 +6498,7 @@ var WaveformPlaylist =
_classCallCheck(this, _class);
this.name = 'Untitled';
this.info = undefined;
this.customClass = undefined;
this.waveOutlineColor = undefined;
this.gain = 1;
@ -6532,6 +6526,11 @@ var WaveformPlaylist =
value: function setName(name) {
this.name = name;
}
}, {
key: 'setInfo',
value: function setInfo(info) {
this.info = info;
}
}, {
key: 'setCustomClass',
value: function setCustomClass(className) {
@ -6980,6 +6979,10 @@ var WaveformPlaylist =
}
})]));
}
if (widgets.info) {
controls.push((0, _h2.default)('label.info', [this.info]));
}
}
return (0, _h2.default)('div.controls', {
@ -7134,6 +7137,7 @@ var WaveformPlaylist =
start: this.startTime,
end: this.endTime,
name: this.name,
info: this.info,
customClass: this.customClass,
cuein: this.cueIn,
cueout: this.cueOut,
@ -10408,14 +10412,18 @@ var WaveformPlaylist =
var recBuffersL = [];
var recBuffersR = [];
var sampleRate = void 0;
var stereo = void 0;
function init(config) {
sampleRate = config.sampleRate;
stereo = config.stereo;
}
function record(inputBuffer) {
recBuffersL.push(inputBuffer[0]);
if (stereo) {
recBuffersR.push(inputBuffer[1]);
}
recLength += inputBuffer[0].length;
}
@ -10434,15 +10442,14 @@ var WaveformPlaylist =
}
function encodeWAV(samples) {
var mono = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var numChannels = stereo ? 2 : 1;
var buffer = new ArrayBuffer(44 + samples.length * 2);
var view = new DataView(buffer);
/* RIFF identifier */
writeString(view, 0, 'RIFF');
/* file length */
view.setUint32(4, 32 + samples.length * 2, true);
view.setUint32(4, 36 + samples.length * 2, true);
/* RIFF type */
writeString(view, 8, 'WAVE');
/* format chunk identifier */
@ -10452,13 +10459,13 @@ var WaveformPlaylist =
/* sample format (raw) */
view.setUint16(20, 1, true);
/* channel count */
view.setUint16(22, mono ? 1 : 2, true);
view.setUint16(22, numChannels, true);
/* sample rate */
view.setUint32(24, sampleRate, true);
/* byte rate (sample rate * block align) */
view.setUint32(28, sampleRate * 4, true);
/* byte rate (sample rate * channel count * bytes per sample) */
view.setUint32(28, sampleRate * numChannels * 2, true);
/* block align (channel count * bytes per sample) */
view.setUint16(32, 4, true);
view.setUint16(32, numChannels * 2, true);
/* bits per sample */
view.setUint16(34, 16, true);
/* data chunk identifier */
@ -10500,8 +10507,11 @@ var WaveformPlaylist =
function exportWAV(type) {
var bufferL = mergeBuffers(recBuffersL, recLength);
var interleaved = bufferL;
if (stereo) {
var bufferR = mergeBuffers(recBuffersR, recLength);
var interleaved = interleave(bufferL, bufferR);
interleaved = interleave(bufferL, bufferR);
}
var dataview = encodeWAV(interleaved);
var audioBlob = new Blob([dataview], { type: type });

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>
{{ player.guid }} - {{ player.name }}
</title>
<link rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css"
integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x"
crossorigin="anonymous"
/>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/css/theme.bootstrap_4.min.css"
integrity="sha512-2C6AmJKgt4B+bQc08/TwUeFKkq8CsBNlTaNcNgUmsDJSU1Fg+R6azDbho+ZzuxEkJnCjLZQMozSq3y97ZmgwjA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</head>
<body>
<main class="container">
<div class="row">
<div class="col">
<table class="table table-sm table-borderless">
<tbody>
<tr>
<th scope="row">guid</th>
<td>
<a href="https://steamcommunity.com/profiles/{{ player.guid }}" target="_blank">
{{ player.guid }}
</a>
</td>
<tr>
<th scope="row">name</th>
<td>{{ player.name }}</td>
<tr>
<th scope="row">first_seen</th>
<td>{{ player.first_seen }}</td>
<tr>
<th scope="row">last_seen</th>
<td>{{ player.last_seen }}</td>
<tr>
<th scope="row">playtime</th>
<td>{{ player.playtime | to_duration }}</td>
<tr>
<th scope="row">chats</th>
<td>{{ player.chats }}</td>
<tr>
<th scope="row">deaths</th>
<td>{{ player.deaths }}</td>
<tr>
<th scope="row">kills</th>
<td>{{ player.kills }}</td>
<tr>
<th scope="row">voicetime</th>
<td>{{ player.voicetime | to_duration }}</td>
</tbody>
</table>
</div>
<div class="col">
<table class="table table-striped tablesorter" id="playerNamesTable">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col" class="sorter-duration">Time</th>
</tr>
</thead>
<tbody>
{%- for name in player.names %}
<tr>
<th scope="row">{{ name.name }}</th>
<td>{{ (name.time * 0.015) | to_duration }}</td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
<div class="col">
<table class="table table-striped" id="playerSpraysTable">
<thead>
<tr>
<th scope="col">Spray</th>
</tr>
</thead>
<tbody>
{%- for spray in player.sprays %}
<tr>
<th scope="row">{{ spray.spray }}</th>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
</div>
<div class="row">
<h3>Sessions</h3>
<table class="table table-striped tablesorter" id="playerSessionsTable">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col" class="sorter-isoDate">Time</th>
<th scope="col" class="sorter-duration">Length</th>
<th scope="col">Map</th>
<th scope="col" class="sorter-duration">Playtime</th>
<th scope="col">Chats</th>
<th scope="col" class="sorter-duration">Voicetime</th>
</tr>
</thead>
<tbody>
{%- for session in player_sessions %}
<tr>
<th scope="row">
<a href="{{ url_for('views.session', session_id=session.session.id) }}#{{ player.guid }}">
{{ session.session.id }}
</a>
</th>
<td>{{ session.session.time.isoformat(' ') }}</td>
<td>{{ session.session.length | to_duration }}</td>
<td>{{ session.session.mapname }}</td>
<td>{{ session.playtime | to_duration }}</td>
<td>{{ session.chats }}</td>
<td>{{ session.voicetime | to_duration }}</td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
</main>
<script
src="https://code.jquery.com/jquery-3.6.0.min.js"
integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.min.js"
integrity="sha512-qzgd5cYSZcosqpzpn7zF2ZId8f/8CHmFKZ8j7mU4OUXTNRd5g+ZHBPsgKEwoqxCtdQvExE5LprwwPAgoicguNg=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/parsers/parser-duration.min.js"
integrity="sha512-X7QJLLEO6yg8gSlmgRAP7Ec2qDD+ndnFcd8yagZkkN5b/7bCMbhRQdyJ4SjENUEr+4eBzgwvaFH5yR/bLJZJQA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<script type="text/javascript">
$(function() {
$("#playerNamesTable").tablesorter({
theme: "bootstrap"
});
$("#playerSessionsTable").tablesorter({
theme: "bootstrap"
});
});
</script>
</body>
</html>

View File

@ -16,35 +16,10 @@
<body>
<nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="#">Top navbar</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarCollapse">
<ul class="navbar-nav me-auto mb-2 mb-md-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
</li>
</ul>
<form class="d-flex">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>
<main>
<main class="container-fluid">
<div class="btn-toolbar" role="toolbar">
<div class="d-flex flex-column flex-grow-1">
<div class="btn-toolbar" role="toolbar" style="min-width: max-content;">
<div class="btn-group me-2" role="group">
<button type="button" class="btn-pause btn btn-outline-warning" title="Pause">
<i class="fas fa-pause"></i>
@ -91,24 +66,63 @@
id="master-gain"
/>
</div>
</div>
<div class="btn-group me-2">
<div style="margin: 6px">
<span class="audio-pos" aria-label="Audio position">00:00:00.0</span>
<span class="audio-pos font-monospace" aria-label="Audio position">00:00:00.0</span>
</div>
<div style="margin: 6px">
<span class="audio-pos-2" aria-label="Audio position">00:00:00.0</span>
<span class="audio-pos-2 font-monospace" aria-label="Audio position">00:00:00.0</span>
</div>
<div class="form-check form-switch">
<input class="form-check-input automatic-scroll" type="checkbox" id="automatic_scroll" checked>
<label class="form-check-label" for="automatic_scroll">Autoscroll</label>
<input class="form-check-input" type="checkbox" id="autoscroll_voice" checked>
<label class="form-check-label" for="autoscroll_voice">Voice</label>
</div>
<div class="form-check form-switch">
<label class="form-check-label" for="autoscroll_chat">Chat</label>
<input class="form-check-input" type="checkbox" id="autoscroll_chat" checked>
</div>
</div>
<div class="btn-group me-2">
<button type="button" title="Download the selection as Wav file" class="btn btn-download btn-outline-primary">
<i class="fas fa-download" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="overflow-auto" id="playlist">
</div>
</div>
<div id="playlist">
<div class="d-flex flex-column overflow-auto" id="chat">
<table class="table table-sm text-nowrap">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">SteamID</th>
<th scope="col">Name</th>
<th scope="col">Message</th>
</tr>
</thead>
<tbody>
{%- for chat in chats %}
<tr>
<td onclick="jumpToGameTick({{ chat.tick }})">
{{ (chat.tick * session.tickinterval) | to_duration }}
</td>
<td>{{ chat.player_guid }}</td>
<td>{{ chat.name }}</td>
<td>{{ chat.chat }}</td>
</tr>
{%- endfor %}
</tbody>
</table>
</div>
</main>
@ -154,7 +168,7 @@ var g_events = [
playlist.load([
{%- for guid, psess in player_sessions.items() %}
{%- if psess.voicetime > 0 %}
{src: "/static/css-ze-parsed/{{ session.demoname }}/voice/{{ guid }}.demopus", name: "{{ guid }}"},
{src: "/static/css-ze-parsed/{{ session.demoname }}/voice/{{ guid }}.demopus", name: "{{ guid }}", info: "{{ psess.player.name }}"},
{%- endif -%}
{% endfor %}
]).then(function() {

View File

@ -40,3 +40,16 @@ def session(session_id):
chats=chats,
events=events
)
@bp.route('/player/<guid>')
def player(guid):
player = Player.query.filter_by(guid=guid).one()
if not player:
return '404 Not Found', 404
player_sessions = PlayerSession.query.filter_by(player_guid=guid).all()
return render_template('player.html',
player=player,
player_sessions=player_sessions
)

View File

@ -125,7 +125,7 @@ def parse_demo(path):
kills = models.Player.kills + obj['kills'],
voicetime = models.Player.voicetime + obj['voicetime'],
first_seen = func.least(models.Player.first_seen, starttime),
last_seen = func.greatest(models.Player.first_seen, starttime),
last_seen = func.greatest(models.Player.last_seen, starttime),
))
db.session.commit()
break
@ -225,9 +225,11 @@ def parse_demo(path):
eventdata = obj.copy()
del eventdata['tick']
del eventdata['event']
if 'steamid' in eventdata:
del eventdata['steamid']
event = models.Event(
player_guid=guid,
player_guid=obj['steamid'] if 'steamid' in obj else None,
session_id=session.id,
tick=obj['tick'],
time=starttime + timedelta(seconds=obj['tick'] * tickinterval),

View File

@ -12,6 +12,7 @@ orjson==3.5.2
python-dotenv==0.17.1
SQLAlchemy==1.4.15
tqdm==4.60.0
uWSGI==2.0.19.1
Werkzeug==2.0.0
zope.event==4.5.0
zope.interface==5.4.0