diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..722f84d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+/web/logs/
+/bin/
+/lib/
+/lib64
+/include/
+/share/
+/__pycache__
diff --git a/convert-logs.py b/convert-logs.py
index 90c8b61..6bdac6f 100644
--- a/convert-logs.py
+++ b/convert-logs.py
@@ -1,62 +1,55 @@
from json import loads, dumps
+from makeline import make_line
+from sys import argv
-with open('all-time.txt', 'r') as fin:
- (last_t, last_ev, last_evt, last_ps) = (0, 'error', 0, [])
+log_path = '/dev/null'
- with open('events.json', 'w') as fout:
- cur_t = 0
- cur_ps = []
+def parse_logs():
+ with open('all-time.txt', 'r') as fin:
+ (last_t, last_ev, last_evt, last_ps) = (0, 'error', 0, [])
- for line in fin:
- current = loads(line)
- cur_t = current['time']
- cur_ps = current['players']
+ with open(log_path+'events.json', 'w') as fout:
+ cur_t = 0
+ cur_ps = []
- if last_t == 0:
- (last_t, last_ev, last_evt, last_ps) = (cur_t, 'online', cur_t, cur_ps)
- else:
- if cur_t - last_t > 70:
- if last_ev == 'online':
- fout.write(dumps({'period': [last_evt, last_t], 'status': last_ev, 'players': last_ps})+'\n')
- (last_ev, last_evt, last_ps) = ('unreachable', last_t, cur_ps)
- else:
- if last_ev != 'online':
- fout.write(dumps({'period': [last_evt, last_t], 'status': last_ev})+'\n')
- (last_ev, last_evt, last_ps) = ('online', last_t, last_ps)
+ for line in fin:
+ current = loads(line)
+ cur_t = current['time']
+ cur_ps = current['players']
- if last_ps != cur_ps:
- fout.write(dumps({'period': [last_evt, cur_t], 'status': last_ev, 'players': last_ps})+'\n')
- (last_ev, last_evt, last_ps) = ('online', cur_t, cur_ps)
+ if last_t == 0:
+ (last_t, last_ev, last_evt, last_ps) = (cur_t, 'online', cur_t, cur_ps)
+ else:
+ if cur_t - last_t > 90:
+ if last_ev == 'online':
+ fout.write(dumps({'period': [last_evt, last_t], 'status': last_ev, 'players': last_ps})+'\n')
+ (last_ev, last_evt, last_ps) = ('unreachable', last_t, cur_ps)
+ else:
+ if last_ev != 'online':
+ fout.write(dumps({'period': [last_evt, last_t], 'status': last_ev})+'\n')
+ (last_ev, last_evt, last_ps) = ('online', last_t, last_ps)
- last_t = cur_t
+ if last_ps != cur_ps:
+ fout.write(dumps({'period': [last_evt, cur_t], 'status': last_ev, 'players': last_ps})+'\n')
+ (last_ev, last_evt, last_ps) = ('online', cur_t, cur_ps)
- with open('current-state.json', 'w') as fout:
- fout.write(dumps({'period': [last_evt, last_t], 'status': 'online', 'players': last_ps}))
+ last_t = cur_t
+ with open(log_path+'current-state.json', 'w') as fout:
+ fout.write(dumps({'period': [last_evt, last_t], 'status': 'online', 'players': last_ps}))
-
-# scale down coordinates to dx=1 per day to prevent SVG coordinate overflows
-scale = 1/60/60/24
-
-def get_date(dtime):
- return datetime.fromtimestamp(dtime).strftime('%Y-%m-%d %H:%M')
-
-def make_line(event):
- evstart = event['period'][0]
- evstop = event['period'][1]
- return '{text} ({start_date} — {stop_date})'.format(
- type = '' if event['status'] == 'online' else ' class="error"',
- start = scale*evstart,
- stop = scale*evstop,
- start_date = get_date(evstart),
- stop_date = get_date(evstop),
- height = len(event['players']) if 'players' in event else 0,
- text = ('no one' if len(event['players']) == 0 else ', '.join(event['players'])) if event['status'] == 'online' else event['status']
- )
-
def events_to_lines():
- with open('events.json', 'r') as fin:
- with open('events.log', 'w') as fout:
+ with open(log_path+'events.json', 'r') as fin:
+ with open(log_path+'events.log', 'w') as fout:
for l in fin:
fout.write(make_line(loads(l))+'\n')
+
+
+if __name__ == "__main__":
+ if len(argv) > 1:
+ log_path = argv[1]+'/'
+ parse_logs()
+ events_to_lines()
+ else:
+ print('error: specify target path')
diff --git a/generate-html.py b/generate-html.py
index 177f36f..9d8beea 100644
--- a/generate-html.py
+++ b/generate-html.py
@@ -1,61 +1,59 @@
from json import loads
from datetime import datetime
+from pytz import utc
+from makeline import make_line, scale
from re import compile
+from sys import argv
-# scale down coordinates to dx=1 per day to prevent SVG coordinate overflows
-scale = 1/60/60/24
+log_path = '/dev/null'
-def get_date(dtime):
- return datetime.fromtimestamp(dtime).strftime('%Y-%m-%d %H:%M')
-
-def make_line(event):
- evstart = event['period'][0]
- evstop = event['period'][1]
- return '{text} ({start_date} — {stop_date})'.format(
- type = '' if event['status'] == 'online' else ' class="error"',
- start = scale*evstart,
- stop = scale*evstop,
- start_date = get_date(evstart),
- stop_date = get_date(evstop),
- height = len(event['players']) if 'players' in event else 0,
- text = ('no one' if len(event['players']) == 0 else ', '.join(event['players'])) if event['status'] == 'online' else event['status']
- )
-
-def get_lines(start,stop):
- lines = []
- find_start = compile(r'x1="([0-9.]+)"')
+def get_lines(start):
+ lines = ''
find_stop = compile(r'x2="([0-9.]+)"')
- with open('events.log', 'r') as fin:
+ with open(log_path+'events.log', 'r') as fin:
for l in fin:
- evstart = float(find_start.search(l).group(1))
evstop = float(find_stop.search(l).group(1))
- if evstop > start and evstart < stop:
- lines.append(l)
- with open('current-state.json', 'r') as fin:
- lines.append(make_line(loads(fin.read())))
+ if evstop > start:
+ break
+ lines += l
+ lines += fin.read()
+
+ with open(log_path+'current-state.json', 'r') as fin:
+ lines += make_line(loads(fin.read()))
return lines
-def make_html(name,period):
+
+def make_html(name, period, display_scale = 144): # display_scale = 1 pixel per 10 minutes
stop = scale*datetime.now().timestamp()
start = stop-period
- lines = get_lines(start, stop)
- display_scale = 24*60/10 # 1 pixel per 10 minutes
+ day_start = scale*datetime.fromtimestamp(start/scale).replace(hour=0,minute=0,second=0,microsecond=0,tzinfo=utc).timestamp()
+ lines = get_lines(start)
+
with open('template.html', 'r') as fin:
- with open(name+'.html', 'w') as fout:
+ with open(log_path+name+'.html', 'w') as fout:
fout.write(fin.read().format(
width = display_scale*period,
start = start,
period = period,
- day_start = scale*datetime.fromtimestamp(start/scale).replace(hour=0,minute=0,second=0,microsecond=0).timestamp(),
+ day_start = day_start,
day_stop = stop,
- events = ''.join(lines)
+ events = lines
))
+
def make_htmls():
- make_html('day', 1)
+ make_html('hour', 1/24, 24*60*4) # 4 pixels per minute
+ make_html('day', 1, 24*60) # 1 pixel per minute
make_html('month', 31)
make_html('4months', 120)
+ make_html('18months', 365.256*1.5, 24) # 1 pixel per hour
-make_htmls()
+
+if __name__ == "__main__":
+ if len(argv) > 1:
+ log_path = argv[1]+'/'
+ make_htmls()
+ else:
+ print('error: specify target path')
diff --git a/makeline.py b/makeline.py
new file mode 100644
index 0000000..7621ac3
--- /dev/null
+++ b/makeline.py
@@ -0,0 +1,22 @@
+from datetime import datetime
+from pytz import utc
+
+# scale down coordinates to dx=1 per day to prevent SVG coordinate overflows
+scale = 1/60/60/24
+
+def get_date(dtime):
+ return datetime.fromtimestamp(dtime).astimezone(utc).strftime('%Y-%m-%d %H:%M')
+
+
+def make_line(event):
+ evstart = event['period'][0]
+ evstop = event['period'][1]
+ return '{text} ({start_date} — {stop_date})'.format(
+ type = '' if event['status'] == 'online' else ' class="error"',
+ start = scale*evstart,
+ stop = scale*evstop,
+ start_date = get_date(evstart),
+ stop_date = get_date(evstop),
+ height = len(event['players']) if 'players' in event else 0,
+ text = ('no one' if len(event['players']) == 0 else ', '.join(event['players'])) if event['status'] == 'online' else event['status']
+ )
diff --git a/pyvenv.cfg b/pyvenv.cfg
new file mode 100644
index 0000000..12a52df
--- /dev/null
+++ b/pyvenv.cfg
@@ -0,0 +1,3 @@
+home = /usr/bin
+include-system-site-packages = false
+version = 3.6.7
diff --git a/template.html b/template.html
index de1b2d5..cf3c82c 100644
--- a/template.html
+++ b/template.html
@@ -2,20 +2,15 @@
-
-
+
+
+
+
-
-
-
+
diff --git a/update-status.py b/update-status.py
new file mode 100644
index 0000000..23cb4b8
--- /dev/null
+++ b/update-status.py
@@ -0,0 +1,61 @@
+from mcstatus import MinecraftServer
+from datetime import datetime
+from makeline import make_line
+from json import loads, dumps
+from sys import argv
+
+log_path = '/dev/null'
+
+def get_status():
+ time = datetime.now().timestamp()
+ try:
+ server = MinecraftServer("xkcd.cbouton.com", 25555)
+ query = server.query(retries=1)
+ players = query.players.names
+ return {'time': time, 'status': 'online', 'players': players}
+ except:
+ return {'time': time, 'status': 'unreachable', 'players': []}
+
+
+def log(event):
+ if event['period'][0] != event['period'][1]:
+ with open(log_path+'events.json', 'a') as fout:
+ fout.write(dumps(event)+'\n')
+ with open(log_path+'events.log', 'a') as fout:
+ fout.write(make_line(event)+'\n')
+
+
+def new_state_and_log():
+ prev = {'period': [0,0], 'status': 'undefined', 'players': []}
+ cur = get_status()
+
+ with open(log_path+'current-state.json', 'r') as fin:
+ prev = loads(fin.read())
+
+ if cur['time'] - prev['period'][1] > 90:
+ log(prev)
+ log({'period': [prev['period'][1], cur['time']], 'status': 'monitor-offline' })
+ return {'period': [cur['time'],cur['time']], 'status': cur['status'], 'players': cur['players']}
+ else:
+ prev['period'][1] = cur['time']
+ if prev['status'] == cur['status'] and prev['players'] == cur['players']:
+ return prev
+ else:
+ log(prev)
+ return {'period': [cur['time'],cur['time']], 'status': cur['status'], 'players': cur['players']}
+
+
+def update():
+ next = new_state_and_log()
+ if next['status'] != 'online':
+ del next['players']
+
+ with open(log_path+'current-state.json', 'w') as fout:
+ fout.write(dumps(next))
+
+if __name__ == "__main__":
+ if len(argv) > 1:
+ log_path = argv[1]+'/'
+ update()
+ else:
+ print('error: specify target path')
diff --git a/xkcraftia-monitor.sh b/xkcraftia-monitor.sh
new file mode 100755
index 0000000..05e50b2
--- /dev/null
+++ b/xkcraftia-monitor.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+source bin/activate
+
+python3 update-status.py /var/www/xkcd.flumble.nl/logs
+python3 generate-html.py /var/www/xkcd.flumble.nl/logs