bicycle-statistics: Restructure
Signed-off-by: Thomas Klaehn <thomas.klaehn@u-blox.com>
This commit is contained in:
parent
538be61afd
commit
492c67bec3
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,3 @@
|
|||||||
data/
|
data/
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
gpx_parser/__init__.pyc
|
*.pyc
|
||||||
|
3
bicycle-stat
Executable file
3
bicycle-stat
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
python -m bicycle_statistics $@
|
10
bicycle-stat.service
Normal file
10
bicycle-stat.service
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Bicucle statistics
|
||||||
|
After=multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=idle
|
||||||
|
ExecStart=/usr/local/bin/bicycle-stat
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
0
bicycle_statistics/__init__.py
Normal file
0
bicycle_statistics/__init__.py
Normal file
18
bicycle_statistics/__main__.py
Normal file
18
bicycle_statistics/__main__.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
from gpx2html import Gpx2Html
|
||||||
|
|
||||||
|
def parse_args():
|
||||||
|
'''Shell argument parser.'''
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument('infolder', help='Specify the in folder.')
|
||||||
|
parser.add_argument('outfolder', help='Specify the out folder.')
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args = parse_args()
|
||||||
|
gpx2html = Gpx2Html(args.infolder, args.outfolder)
|
||||||
|
gpx2html.process()
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
191
gpx2html
191
gpx2html
@ -1,191 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import datetime
|
|
||||||
import sys
|
|
||||||
import gpx_parser
|
|
||||||
import matplotlib
|
|
||||||
matplotlib.use('Agg')
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy
|
|
||||||
import os
|
|
||||||
import pandas as pd
|
|
||||||
import collections
|
|
||||||
|
|
||||||
MONTH_LABELS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
|
||||||
|
|
||||||
def parse_args():
|
|
||||||
'''Shell argument parser.'''
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('infolder', help='Specify the in folder.')
|
|
||||||
parser.add_argument('outfolder', help='Specify the out folder.')
|
|
||||||
return parser.parse_args()
|
|
||||||
|
|
||||||
|
|
||||||
def plot_bar_chart(labels, ticklabels, values, title, xlabel, ylabel, filename, xtick_rotation=0):
|
|
||||||
fig = plt.figure()
|
|
||||||
ax1 = fig.add_subplot(111)
|
|
||||||
ax1.grid(zorder=0)
|
|
||||||
ax1.spines["top"].set_visible(False)
|
|
||||||
ax1.spines["bottom"].set_visible(False)
|
|
||||||
ax1.spines["left"].set_visible(False)
|
|
||||||
ax1.spines["right"].set_visible(False)
|
|
||||||
|
|
||||||
plt.title(title)
|
|
||||||
plt.xlabel(xlabel)
|
|
||||||
plt.ylabel(ylabel)
|
|
||||||
|
|
||||||
width = 1.0 / len(values) - 0.03
|
|
||||||
x_base = numpy.arange(len(ticklabels))
|
|
||||||
x_pos = list()
|
|
||||||
|
|
||||||
for i in range(len(values)):
|
|
||||||
x_pos.append([x + (width / 2) + i * width for x in range(len(x_base))])
|
|
||||||
plt.bar(x_pos[i], values[i], width=width, label=labels[i], zorder=2)
|
|
||||||
|
|
||||||
plt.xticks(x_base, ticklabels, rotation=xtick_rotation)
|
|
||||||
|
|
||||||
# Tweak spacing to prevent clipping of tick-labels
|
|
||||||
plt.subplots_adjust(bottom=0.2)
|
|
||||||
|
|
||||||
plt.legend()
|
|
||||||
plt.savefig(filename)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
args = parse_args()
|
|
||||||
tracks = gpx_parser.Tracks(args.infolder)
|
|
||||||
years_distance = list()
|
|
||||||
years_avg_spd = list()
|
|
||||||
years = list()
|
|
||||||
for year in range(2017, datetime.datetime.now().year + 1):
|
|
||||||
month_avg_spd = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
|
|
||||||
month_distance = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
|
|
||||||
month_duration = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
|
|
||||||
for month in range(1, 13):
|
|
||||||
if month == 12:
|
|
||||||
max_day = (datetime.date(year + 1, 1, 1) - datetime.timedelta(days=1)).day
|
|
||||||
else:
|
|
||||||
max_day = (datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)).day
|
|
||||||
for day in range(1, max_day + 1):
|
|
||||||
date_tracks = tracks.get(year, month, day)
|
|
||||||
for track in date_tracks:
|
|
||||||
month_distance[month] += (track.distance / 1000) # km
|
|
||||||
month_duration[month] += track.duration.total_seconds() / 3600 # h
|
|
||||||
for i in range(1, 13):
|
|
||||||
if month_duration[i] > 0:
|
|
||||||
month_avg_spd[i] = month_distance[i] / month_duration[i]
|
|
||||||
years_distance.append(month_distance.values())
|
|
||||||
years_avg_spd.append(month_avg_spd.values())
|
|
||||||
years.append(str(year))
|
|
||||||
|
|
||||||
out_folder = os.path.abspath(args.outfolder)
|
|
||||||
|
|
||||||
dst_file_name = 'distance.png'
|
|
||||||
plot_bar_chart(years, MONTH_LABELS, years_distance, 'Distance', 'Month', 'km', os.path.join(out_folder, dst_file_name))
|
|
||||||
|
|
||||||
avg_file_name = 'avg_spd.png'
|
|
||||||
plot_bar_chart(years, MONTH_LABELS, years_avg_spd, 'Average Speed', 'Month', 'km/h', os.path.join(out_folder, avg_file_name))
|
|
||||||
|
|
||||||
# last n days
|
|
||||||
n = 14
|
|
||||||
end_date = datetime.date.today()
|
|
||||||
start_date = end_date - datetime.timedelta(days=n)
|
|
||||||
dates = pd.date_range(start_date, end_date)
|
|
||||||
|
|
||||||
date_distance = dict()
|
|
||||||
date_duration = dict()
|
|
||||||
date_avg_spd = dict()
|
|
||||||
|
|
||||||
for date in dates:
|
|
||||||
date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day)
|
|
||||||
date_tracks = tracks.get(date.year, date.month, date.day)
|
|
||||||
for track in date_tracks:
|
|
||||||
try:
|
|
||||||
current_dist = date_distance[date_str]
|
|
||||||
current_duration = date_duration[date_str]
|
|
||||||
except KeyError:
|
|
||||||
current_dist = 0
|
|
||||||
current_duration = 0
|
|
||||||
current_dist += track.distance / 1000
|
|
||||||
date_distance.update({date_str:current_dist})
|
|
||||||
current_duration += track.duration.total_seconds() / 3600
|
|
||||||
date_duration.update({date_str:current_duration})
|
|
||||||
# check for empty dates
|
|
||||||
try:
|
|
||||||
current_dist = date_distance[date_str]
|
|
||||||
current_duration = date_duration[date_str]
|
|
||||||
except KeyError:
|
|
||||||
date_distance.update({date_str:0})
|
|
||||||
date_duration.update({date_str:0})
|
|
||||||
|
|
||||||
date_duration = collections.OrderedDict(sorted(date_duration.items()))
|
|
||||||
|
|
||||||
for key, value in date_duration.items():
|
|
||||||
if value == 0:
|
|
||||||
date_avg_spd.update({key:0})
|
|
||||||
else:
|
|
||||||
avg_spd = date_distance[key] / value
|
|
||||||
date_avg_spd.update({key:avg_spd})
|
|
||||||
|
|
||||||
date_avg_spd = collections.OrderedDict(sorted(date_avg_spd.items()))
|
|
||||||
date_distance = collections.OrderedDict(sorted(date_distance.items()))
|
|
||||||
|
|
||||||
dst_n_file_name = "distance_last_{}_days.png".format(n)
|
|
||||||
plot_bar_chart(["Distance", "Average speed"],
|
|
||||||
date_distance.keys(),
|
|
||||||
[date_distance.values(), date_avg_spd.values()],
|
|
||||||
'Last {} days'.format(n),
|
|
||||||
'Date',
|
|
||||||
'km, km/h',
|
|
||||||
os.path.join(out_folder, dst_n_file_name),
|
|
||||||
90)
|
|
||||||
|
|
||||||
html_file = os.path.join(out_folder, 'index.html')
|
|
||||||
with open(html_file, 'w') as handle:
|
|
||||||
handle.write('<!DOCTYPE html>\n')
|
|
||||||
handle.write('<html>\n')
|
|
||||||
handle.write('<head>\n')
|
|
||||||
handle.write('<style>\n')
|
|
||||||
handle.write('table {\n')
|
|
||||||
handle.write(' border-collapse: separate;\n')
|
|
||||||
handle.write(' border-spacing: 20px 0;\n')
|
|
||||||
handle.write('}\n')
|
|
||||||
handle.write('th {\n')
|
|
||||||
handle.write(' text-align: left;\n')
|
|
||||||
handle.write('}\n')
|
|
||||||
handle.write('</style>\n')
|
|
||||||
handle.write('<title> Bicycle </title>\n')
|
|
||||||
handle.write('</head>\n')
|
|
||||||
handle.write('<body>\n')
|
|
||||||
handle.write('<center>\n')
|
|
||||||
handle.write('<h1> Bicycle </h1>\n')
|
|
||||||
handle.write('<p>\n')
|
|
||||||
|
|
||||||
handle.write('<table>\n')
|
|
||||||
handle.write('<tr>\n')
|
|
||||||
for year in years:
|
|
||||||
handle.write('<th>{}</th>\n'.format(year))
|
|
||||||
handle.write('</tr>\n')
|
|
||||||
|
|
||||||
handle.write('<tr>\n')
|
|
||||||
for i in range(len(years_distance)):
|
|
||||||
handle.write('<td>{} km</td>\n'.format(round(sum(years_distance[i]), 1)))
|
|
||||||
handle.write('</tr>\n')
|
|
||||||
handle.write('</table>\n')
|
|
||||||
|
|
||||||
handle.write('</p>\n')
|
|
||||||
|
|
||||||
handle.write('<p>\n')
|
|
||||||
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format(dst_file_name))
|
|
||||||
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format(avg_file_name))
|
|
||||||
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format(dst_n_file_name))
|
|
||||||
handle.write('</p>\n')
|
|
||||||
|
|
||||||
handle.write('</body>\n')
|
|
||||||
handle.write('<center>\n')
|
|
||||||
handle.write('</html>\n')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.exit(main())
|
|
184
gpx2html/__init__.py
Executable file
184
gpx2html/__init__.py
Executable file
@ -0,0 +1,184 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import datetime
|
||||||
|
import sys
|
||||||
|
import gpx_parser
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy
|
||||||
|
import os
|
||||||
|
import pandas as pd
|
||||||
|
import collections
|
||||||
|
|
||||||
|
def plot_bar_chart(labels, ticklabels, values, title, xlabel, ylabel, filename, xtick_rotation=0):
|
||||||
|
fig = plt.figure()
|
||||||
|
ax1 = fig.add_subplot(111)
|
||||||
|
ax1.grid(zorder=0)
|
||||||
|
ax1.spines["top"].set_visible(False)
|
||||||
|
ax1.spines["bottom"].set_visible(False)
|
||||||
|
ax1.spines["left"].set_visible(False)
|
||||||
|
ax1.spines["right"].set_visible(False)
|
||||||
|
|
||||||
|
plt.title(title)
|
||||||
|
plt.xlabel(xlabel)
|
||||||
|
plt.ylabel(ylabel)
|
||||||
|
|
||||||
|
width = 1.0 / len(values) - 0.03
|
||||||
|
x_base = numpy.arange(len(ticklabels))
|
||||||
|
x_pos = list()
|
||||||
|
|
||||||
|
for i in range(len(values)):
|
||||||
|
x_pos.append([x + (width / 2) + i * width for x in range(len(x_base))])
|
||||||
|
plt.bar(x_pos[i], values[i], width=width, label=labels[i], zorder=2)
|
||||||
|
|
||||||
|
plt.xticks(x_base, ticklabels, rotation=xtick_rotation)
|
||||||
|
|
||||||
|
# Tweak spacing to prevent clipping of tick-labels
|
||||||
|
plt.subplots_adjust(bottom=0.2)
|
||||||
|
|
||||||
|
plt.legend()
|
||||||
|
plt.savefig(filename)
|
||||||
|
|
||||||
|
|
||||||
|
class Gpx2Html(object):
|
||||||
|
MONTH_LABELS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
||||||
|
|
||||||
|
def __init__(self, infolder, outfolder):
|
||||||
|
self.infolder = infolder
|
||||||
|
self.outfolder = outfolder
|
||||||
|
|
||||||
|
|
||||||
|
def process(self):
|
||||||
|
self.tracks = gpx_parser.Tracks(self.infolder)
|
||||||
|
years_distance = list()
|
||||||
|
years_avg_spd = list()
|
||||||
|
years = list()
|
||||||
|
for year in range(2017, datetime.datetime.now().year + 1):
|
||||||
|
month_avg_spd = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
|
||||||
|
month_distance = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
|
||||||
|
month_duration = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0, 9:0, 10:0, 11:0, 12:0}
|
||||||
|
for month in range(1, 13):
|
||||||
|
if month == 12:
|
||||||
|
max_day = (datetime.date(year + 1, 1, 1) - datetime.timedelta(days=1)).day
|
||||||
|
else:
|
||||||
|
max_day = (datetime.date(year, month + 1, 1) - datetime.timedelta(days=1)).day
|
||||||
|
for day in range(1, max_day + 1):
|
||||||
|
date_tracks = self.tracks.get(year, month, day)
|
||||||
|
for track in date_tracks:
|
||||||
|
month_distance[month] += (track.distance / 1000) # km
|
||||||
|
month_duration[month] += track.duration.total_seconds() / 3600 # h
|
||||||
|
for i in range(1, 13):
|
||||||
|
if month_duration[i] > 0:
|
||||||
|
month_avg_spd[i] = month_distance[i] / month_duration[i]
|
||||||
|
years_distance.append(month_distance.values())
|
||||||
|
years_avg_spd.append(month_avg_spd.values())
|
||||||
|
years.append(str(year))
|
||||||
|
|
||||||
|
out_folder = os.path.abspath(self.outfolder)
|
||||||
|
|
||||||
|
dst_file_name = 'distance.png'
|
||||||
|
plot_bar_chart(years, self.MONTH_LABELS, years_distance, 'Distance', 'Month', 'km', os.path.join(out_folder, dst_file_name))
|
||||||
|
|
||||||
|
avg_file_name = 'avg_spd.png'
|
||||||
|
plot_bar_chart(years, self.MONTH_LABELS, years_avg_spd, 'Average Speed', 'Month', 'km/h', os.path.join(out_folder, avg_file_name))
|
||||||
|
|
||||||
|
# last n days
|
||||||
|
n = 14
|
||||||
|
end_date = datetime.date.today()
|
||||||
|
start_date = end_date - datetime.timedelta(days=n)
|
||||||
|
dates = pd.date_range(start_date, end_date)
|
||||||
|
|
||||||
|
date_distance = dict()
|
||||||
|
date_duration = dict()
|
||||||
|
date_avg_spd = dict()
|
||||||
|
|
||||||
|
for date in dates:
|
||||||
|
date_str = "{0:04d}-{1:02d}-{2:02d}".format(date.year, date.month, date.day)
|
||||||
|
date_tracks = self.tracks.get(date.year, date.month, date.day)
|
||||||
|
for track in date_tracks:
|
||||||
|
try:
|
||||||
|
current_dist = date_distance[date_str]
|
||||||
|
current_duration = date_duration[date_str]
|
||||||
|
except KeyError:
|
||||||
|
current_dist = 0
|
||||||
|
current_duration = 0
|
||||||
|
current_dist += track.distance / 1000
|
||||||
|
date_distance.update({date_str:current_dist})
|
||||||
|
current_duration += track.duration.total_seconds() / 3600
|
||||||
|
date_duration.update({date_str:current_duration})
|
||||||
|
# check for empty dates
|
||||||
|
try:
|
||||||
|
current_dist = date_distance[date_str]
|
||||||
|
current_duration = date_duration[date_str]
|
||||||
|
except KeyError:
|
||||||
|
date_distance.update({date_str:0})
|
||||||
|
date_duration.update({date_str:0})
|
||||||
|
|
||||||
|
date_duration = collections.OrderedDict(sorted(date_duration.items()))
|
||||||
|
|
||||||
|
for key, value in date_duration.items():
|
||||||
|
if value == 0:
|
||||||
|
date_avg_spd.update({key:0})
|
||||||
|
else:
|
||||||
|
avg_spd = date_distance[key] / value
|
||||||
|
date_avg_spd.update({key:avg_spd})
|
||||||
|
|
||||||
|
date_avg_spd = collections.OrderedDict(sorted(date_avg_spd.items()))
|
||||||
|
date_distance = collections.OrderedDict(sorted(date_distance.items()))
|
||||||
|
|
||||||
|
dst_n_file_name = "distance_last_{}_days.png".format(n)
|
||||||
|
plot_bar_chart(["Distance", "Average speed"],
|
||||||
|
date_distance.keys(),
|
||||||
|
[date_distance.values(), date_avg_spd.values()],
|
||||||
|
'Last {} days'.format(n),
|
||||||
|
'Date',
|
||||||
|
'km, km/h',
|
||||||
|
os.path.join(out_folder, dst_n_file_name),
|
||||||
|
90)
|
||||||
|
|
||||||
|
html_file = os.path.join(out_folder, 'index.html')
|
||||||
|
with open(html_file, 'w') as handle:
|
||||||
|
handle.write('<!DOCTYPE html>\n')
|
||||||
|
handle.write('<html>\n')
|
||||||
|
handle.write('<head>\n')
|
||||||
|
handle.write('<style>\n')
|
||||||
|
handle.write('table {\n')
|
||||||
|
handle.write(' border-collapse: separate;\n')
|
||||||
|
handle.write(' border-spacing: 20px 0;\n')
|
||||||
|
handle.write('}\n')
|
||||||
|
handle.write('th {\n')
|
||||||
|
handle.write(' text-align: left;\n')
|
||||||
|
handle.write('}\n')
|
||||||
|
handle.write('</style>\n')
|
||||||
|
handle.write('<title> Bicycle </title>\n')
|
||||||
|
handle.write('</head>\n')
|
||||||
|
handle.write('<body>\n')
|
||||||
|
handle.write('<center>\n')
|
||||||
|
handle.write('<h1> Bicycle </h1>\n')
|
||||||
|
handle.write('<p>\n')
|
||||||
|
|
||||||
|
handle.write('<table>\n')
|
||||||
|
handle.write('<tr>\n')
|
||||||
|
for year in years:
|
||||||
|
handle.write('<th>{}</th>\n'.format(year))
|
||||||
|
handle.write('</tr>\n')
|
||||||
|
|
||||||
|
handle.write('<tr>\n')
|
||||||
|
for i in range(len(years_distance)):
|
||||||
|
handle.write('<td>{} km</td>\n'.format(round(sum(years_distance[i]), 1)))
|
||||||
|
handle.write('</tr>\n')
|
||||||
|
handle.write('</table>\n')
|
||||||
|
|
||||||
|
handle.write('</p>\n')
|
||||||
|
|
||||||
|
handle.write('<p>\n')
|
||||||
|
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format(dst_file_name))
|
||||||
|
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format(avg_file_name))
|
||||||
|
handle.write('<IMG SRC="{}" ALT="Distance">\n'.format(dst_n_file_name))
|
||||||
|
handle.write('</p>\n')
|
||||||
|
|
||||||
|
handle.write('</body>\n')
|
||||||
|
handle.write('<center>\n')
|
||||||
|
handle.write('</html>\n')
|
18
setup.py
18
setup.py
@ -1,12 +1,22 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
|
||||||
NAME = 'gpx_parser'
|
NAME = 'bicycle-statistics'
|
||||||
VERSION = '0.2.1'
|
VERSION = '0.1.0'
|
||||||
AUTHOR = 'Thomas Klaehn'
|
AUTHOR = 'Thomas Klaehn'
|
||||||
EMAIL = 'tkl@blackfinn.de'
|
EMAIL = 'tkl@blackfinn.de'
|
||||||
PACKAGES = [NAME]
|
PACKAGES = [NAME, 'gpx_parser', 'gpx2html']
|
||||||
SCRIPTS = ['example-gpx-parser', 'gpx2html']
|
SCRIPTS = ['example-gpx-parser', 'bicycle-stat']
|
||||||
|
|
||||||
|
DAEMON_START_SCRIPT = os.path.join("/lib/systemd/system", "bicycle-stat.service")
|
||||||
|
|
||||||
|
if sys.argv[1] == 'install':
|
||||||
|
shutil.copyfile("bicycle-stat.service", DAEMON_START_SCRIPT)
|
||||||
|
os.chmod(DAEMON_START_SCRIPT, stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
|
||||||
|
|
||||||
setup(name=NAME, version=VERSION, author=AUTHOR, author_email=EMAIL, packages=PACKAGES, scripts=SCRIPTS)
|
setup(name=NAME, version=VERSION, author=AUTHOR, author_email=EMAIL, packages=PACKAGES, scripts=SCRIPTS)
|
||||||
|
Loading…
Reference in New Issue
Block a user