diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..9aa03b3 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..8989659 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# trackmania-records + +This project allows you to compare your Trackmania 2020 records with your friends. + +In the browser, it looks like this: + +![Screenshot 1](https://github.com/rsnitsch/trackmania-records/raw/develop/screenshot1.png "Screenshot 1") + +It also features a total time comparison: + +![Screenshot 2](https://github.com/rsnitsch/trackmania-records/raw/develop/screenshot2.png "Screenshot 2") + +## Setup + +### 1. Install web application + +Please refer to: https://github.com/rsnitsch/trackmania-records/tree/develop/server + +### 2. Install uploader + +The uploader is what extracts your trackmania records from the autosaved replay files on +your computer and sends them to the web application. + +The installation and usage instructions for the upload tool can be found at the bottom of +the web application index page. Alternatively, you can look them up here: +https://github.com/rsnitsch/trackmania-records/tree/develop/client diff --git a/client/README.md b/client/README.md index 085b339..e43921d 100644 --- a/client/README.md +++ b/client/README.md @@ -1,9 +1,10 @@ -Trackmania records uploader -=========================== +trackmania-records - Uploader +============================= -This tool extracts your records for the training maps of the latest -Trackmania version (from 2020) and uploads them to a server so you can -easily share your records with friends. +This is the uploader for trackmania-records, a Trackmania 2020 records sharing/comparison tool. + +The uploader extracts your records for the training maps of the latest Trackmania version (from 2020) +and uploads them to the web application server so you can easily share your records with friends. Install (requires Python 3 on your system): diff --git a/client/setup.py b/client/setup.py index 3ad7022..9e9685c 100644 --- a/client/setup.py +++ b/client/setup.py @@ -1,102 +1,102 @@ -import codecs -import os.path -import pathlib -from setuptools import setup, find_packages - -def read(rel_path): - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, rel_path), 'r') as fp: - return fp.read() - -def get_version(rel_path): - for line in read(rel_path).splitlines(): - if line.startswith('__version__'): - delim = '"' if '"' in line else "'" - return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") - -here = pathlib.Path(__file__).parent.resolve() - -long_description = (here / 'README.md').read_text(encoding='utf-8') - -setup( - name='upload-tm-records', - # https://www.python.org/dev/peps/pep-0440/ - version=get_version('src/upload_tm_records.py'), - description='Upload your trackmania records to a trackmania-records server!', - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/rsnitsch/trackmania-records', - author='Robert Nitsch', - author_email='mail@robertnitsch.de', - # For a list of valid classifiers, see https://pypi.org/classifiers/ - classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: End Users/Desktop', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Topic :: Games/Entertainment', - - # Specify the Python versions you support here. In particular, ensure - # that you indicate you support Python 3. These classifiers are *not* - # checked by 'pip install'. See instead 'python_requires' below. - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3 :: Only', - ], - - # This field adds keywords for your project which will appear on the - # project page. What does your project relate to? - # - # Note that this is a list of additional keywords, separated - # by commas, to be used to assist searching for the distribution in a - # larger catalog. - keywords='trackmania, gaming', # Optional - - package_dir={'': 'src'}, - - # You can just specify package directories manually here if your project is - # simple. Or you can use find_packages(). - # - # Alternatively, if you just want to distribute a single Python file, use - # the `py_modules` argument instead as follows, which will expect a file - # called `my_module.py` to exist: - # - py_modules=["upload_tm_records"], - # - #packages=find_packages(where='src'), # Required - - # Specify which Python versions you support. In contrast to the - # 'Programming Language' classifiers above, 'pip install' will check this - # and refuse to install the project if the version does not match. See - # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires - python_requires='>=3.7, <4', - - # This field lists other packages that your project depends on to run. - # Any package you put here will be installed by pip when your project is - # installed, so they must be valid existing projects. - # - # For an analysis of "install_requires" vs pip's requirements files see: - # https://packaging.python.org/en/latest/requirements.html - install_requires=['requests'], # Optional - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # `pip` to create the appropriate form of executable for the target - # platform. - # - # For example, the following would provide a command called `sample` which - # executes the function `main` from this package when invoked: - entry_points={ # Optional - 'console_scripts': [ - 'upload-tm-records=upload_tm_records:main', - ], - }, - - project_urls={ # Optional - 'Bug Reports': 'https://github.com/rsnitsch/trackmania-records/issues', - 'Source': 'https://github.com/rsnitsch/trackmania-records', - }, -) +import codecs +import os.path +import pathlib +from setuptools import setup, find_packages + +def read(rel_path): + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, rel_path), 'r') as fp: + return fp.read() + +def get_version(rel_path): + for line in read(rel_path).splitlines(): + if line.startswith('__version__'): + delim = '"' if '"' in line else "'" + return line.split(delim)[1] + else: + raise RuntimeError("Unable to find version string.") + +here = pathlib.Path(__file__).parent.resolve() + +long_description = (here / 'README.md').read_text(encoding='utf-8') + +setup( + name='upload-tm-records', + # https://www.python.org/dev/peps/pep-0440/ + version=get_version('src/upload_tm_records.py'), + description='Upload your trackmania records to a trackmania-records server!', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/rsnitsch/trackmania-records', + author='Robert Nitsch', + author_email='mail@robertnitsch.de', + # For a list of valid classifiers, see https://pypi.org/classifiers/ + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Console', + 'Intended Audience :: End Users/Desktop', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Topic :: Games/Entertainment', + + # Specify the Python versions you support here. In particular, ensure + # that you indicate you support Python 3. These classifiers are *not* + # checked by 'pip install'. See instead 'python_requires' below. + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3 :: Only', + ], + + # This field adds keywords for your project which will appear on the + # project page. What does your project relate to? + # + # Note that this is a list of additional keywords, separated + # by commas, to be used to assist searching for the distribution in a + # larger catalog. + keywords='trackmania, gaming', # Optional + + package_dir={'': 'src'}, + + # You can just specify package directories manually here if your project is + # simple. Or you can use find_packages(). + # + # Alternatively, if you just want to distribute a single Python file, use + # the `py_modules` argument instead as follows, which will expect a file + # called `my_module.py` to exist: + # + py_modules=["upload_tm_records"], + # + #packages=find_packages(where='src'), # Required + + # Specify which Python versions you support. In contrast to the + # 'Programming Language' classifiers above, 'pip install' will check this + # and refuse to install the project if the version does not match. See + # https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires + python_requires='>=3.7, <4', + + # This field lists other packages that your project depends on to run. + # Any package you put here will be installed by pip when your project is + # installed, so they must be valid existing projects. + # + # For an analysis of "install_requires" vs pip's requirements files see: + # https://packaging.python.org/en/latest/requirements.html + install_requires=['requests'], # Optional + + # To provide executable scripts, use entry points in preference to the + # "scripts" keyword. Entry points provide cross-platform support and allow + # `pip` to create the appropriate form of executable for the target + # platform. + # + # For example, the following would provide a command called `sample` which + # executes the function `main` from this package when invoked: + entry_points={ # Optional + 'console_scripts': [ + 'upload-tm-records=upload_tm_records:main', + ], + }, + + project_urls={ # Optional + 'Bug Reports': 'https://github.com/rsnitsch/trackmania-records/issues', + 'Source': 'https://github.com/rsnitsch/trackmania-records', + }, +) diff --git a/client/src/upload_tm_records.py b/client/src/upload_tm_records.py index 377fc2f..30fc234 100644 --- a/client/src/upload_tm_records.py +++ b/client/src/upload_tm_records.py @@ -1,138 +1,139 @@ -import argparse -import json -import logging -import os -import platform -import re -import sys - -import requests - -__version__ = '1.0.0b1' - -logger = logging.getLogger(__name__) - - -def get_replay_directory(): - """ - Return the Trackmania 2020 Autosaves replay directory. - - Note: - In some cases, Trackmania 2020 uses the Documents\Trackmania2020 subfolder. Sometimes, - it uses the Documents\Trackmania subfolder (without any 2020 indication). - - I assume that it uses Trackmania2020 if another (older) Trackmania version is - installed on the system. Therefore, it seems prudent to try the Trackmania2020 subfolder - first and use the Trackmania subfolder as a fallback. - """ - replay_directory = os.path.expanduser(r'~\Documents\Trackmania2020\Replays\Autosaves') - if os.path.isdir(replay_directory): - return replay_directory - else: - logging.warning("Replay directory was not found at expected location: %s.", replay_directory) - - replay_directory = os.path.expanduser(r'~\Documents\Trackmania\Replays\Autosaves') - if os.path.isdir(replay_directory): - return replay_directory - else: - logging.warning("Replay directory was not found at expected location: %s.", replay_directory) - logging.warning("Will now try to use the Windows API workaround for finding the directory...") - - # Windows allows to change the location of the Documents folder, e.g. to move - # it to a separate harddrive or a network drive. In this case, the above code must fail. Instead, - # we have to ask the Windows API for the location of the Documents folder. - import ctypes.wintypes - CSIDL_PERSONAL = 5 # My Documents - SHGFP_TYPE_CURRENT = 0 # Get current value (instead of default value) - - buf = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH) - ctypes.windll.shell32.SHGetFolderPathW(None, CSIDL_PERSONAL, None, SHGFP_TYPE_CURRENT, buf) - - replay_directory = buf.value + r"\Trackmania2020\Replays\Autosaves" - if os.path.isdir(replay_directory): - return replay_directory - - replay_directory = buf.value + r"\Trackmania\Replays\Autosaves" - if os.path.isdir(replay_directory): - return replay_directory - - return None - - -def extract_record_from_gbx_file(path): - best_regexp = re.compile(b' $bestTime['best']) { - $rank++; - } - } - - return null; - } - - function table_for_track_set($track_set, $selectedUser) { - if ($track_set == "Training") - $count = 25; - else if ($track_set == "Summer 2020") { - $count = 25; - } else { - throw Exception("Unknown track set"); - } - - echo "

".htmlspecialchars($track_set)." - Records

"; - ?> - - - - - - -".htmlspecialchars($selectedUser)."'s time\n"; - echo " \n"; - echo " \n"; - echo " \n"; - } -?> - -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - - for ($i = 1; $i <= 25; $i++) { - $track = sprintf("$track_set - %02d", $i); - - // Determine best time for track. - $st = $pdo->prepare("SELECT user, best FROM records WHERE track = :track ORDER BY best ASC"); - $st->bindParam(':track', $track, PDO::PARAM_STR); - $st->execute(); - $bestTimes = $st->fetchAll(); - //print_r($bestTimes); - $bestTime = $bestTimes[0]['best']; - - // Get users that have driven this time. - $st = $pdo->prepare("SELECT user FROM records WHERE track = :track AND best = :best ORDER BY LOWER(user)"); - $st->bindParam(':track', $track, PDO::PARAM_STR); - $st->bindParam(':best', $bestTime, PDO::PARAM_INT); - $st->execute(); - $users = $st->fetchAll(PDO::FETCH_COLUMN, 0); - //print_r($users); -?> - - - -".($bestTimeByUser[0] / 1000.0)."s\n"; - echo " \n"; - echo " \n"; - echo " \n"; - } else { - echo " \n"; - echo " \n"; - echo " \n"; - echo " \n"; - } - } - ?> - -getMessage()); - } - ?> -
TrackBest timeDriven byAbsolute deltaRelative delta (%)".htmlspecialchars($selectedUser)."'s rank
s".sprintf("%.3f", $bestTimeByUser[0] / 1000.0 - $bestTime / 1000.0)."s".sprintf("%.1f", $bestTimeByUser[0] / $bestTime * 100)."%".$bestTimeByUser[1]."----
- -".htmlspecialchars($track_set)." - Total time per user"; ?> - - - - - - -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - $st = $pdo->prepare("SELECT user, SUM(best) AS total_time, COUNT(track) AS count FROM records WHERE track LIKE :track_set GROUP BY user HAVING count = 25 ORDER BY count DESC, total_time ASC"); - $st->bindValue('track_set', addcslashes("$track_set", "?%")."%", PDO::PARAM_STR); - $st->execute(); - while ($row = $st->fetch()) { - //print_r($row); -?> - - - -query("SELECT user, COUNT(track) AS count FROM records GROUP BY user HAVING count < 25 ORDER BY LOWER(user)"); - while ($row = $results->fetch()) { - //print_r($row); -?> - - - -getMessage()); - } - ?> -
UserTotal time
s
∞s
- - - - - Trackmania Records - - - - - -
-

Trackmania Records

- - - - -

Upload instructions

- -
    -
  • Download Python 3 from python.org and install it: https://www.python.org/downloads/
  • -
  • Open a shell/terminal window (cmd.exe or PowerShell)
  • -
  • Install the upload script by executing this command:
    - pip3 install --upgrade upload-tm-records
  • -
  • Now you can always run the following command to upload your latest records to this server:
    - upload-tm-records.exe
  • -
-
- + $bestTime['best']) { + $rank++; + } + } + + return null; + } + + function tableForTrackSet($trackSet, $selectedUser) { + if ($trackSet == "Training") + $count = 25; + else if ($trackSet == "Summer 2020" or $trackSet == "Fall 2020") { + $count = 25; + } else { + throw Exception("Unknown track set"); + } + + echo "

".htmlspecialchars($trackSet)." - Records

"; +?> + + + + + + +".htmlspecialchars($selectedUser)."'s time\n"; + echo " \n"; + echo " \n"; + echo " \n"; + } +?> + +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + + for ($i = 1; $i <= 25; $i++) { + $track = sprintf("$trackSet - %02d", $i); + + // Determine best time for track. + $st = $pdo->prepare("SELECT user, best FROM records WHERE track = :track ORDER BY best ASC"); + $st->bindParam(':track', $track, PDO::PARAM_STR); + $st->execute(); + $bestTimes = $st->fetchAll(); + //print_r($bestTimes); + $bestTime = $bestTimes[0]['best']; + + // Get users that have driven this time. + $st = $pdo->prepare("SELECT user FROM records WHERE track = :track AND best = :best ORDER BY LOWER(user)"); + $st->bindParam(':track', $track, PDO::PARAM_STR); + $st->bindParam(':best', $bestTime, PDO::PARAM_INT); + $st->execute(); + $users = $st->fetchAll(PDO::FETCH_COLUMN, 0); + //print_r($users); + + if ($trackSet == "Summer 2020" or $trackSet == "Fall 2020") { + if ($i <= 5) { + $tableColorClass = " class='whiteTracks'"; + } else if ($i <= 10) { + $tableColorClass = " class='greenTracks'"; + } else if ($i <= 15) { + $tableColorClass = " class='blueTracks'"; + } else if ($i <= 20) { + $tableColorClass = " class='redTracks'"; + } else if ($i <= 25) { + $tableColorClass = " class='blackTracks'"; + } + } else { + $tableColorClass = ""; + } +?> > + + + +".($bestTimeByUser[0] / 1000.0)."s\n"; + echo " \n"; + echo " \n"; + echo " \n"; + } else { + echo " \n"; + echo " \n"; + echo " \n"; + echo " \n"; + } + } +?> + +getMessage()); + } +?> +
TrackBest timeDriven byAbsolute deltaRelative delta (%)".htmlspecialchars($selectedUser)."'s rank
s".sprintf("%.3f", $bestTimeByUser[0] / 1000.0 - $bestTime / 1000.0)."s".sprintf("%.1f", $bestTimeByUser[0] / $bestTime * 100)."%".$bestTimeByUser[1]."----
+ +".htmlspecialchars($trackSet)." - Total time per user"; ?> + + + + + + +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $st = $pdo->prepare("SELECT user, SUM(best) AS total_time, COUNT(track) AS count FROM records WHERE track LIKE :track_set GROUP BY user HAVING count = 25 ORDER BY count DESC, total_time ASC"); + $st->bindValue('track_set', addcslashes("$trackSet", "?%")."%", PDO::PARAM_STR); + $st->execute(); + while ($row = $st->fetch()) { + //print_r($row); +?> + + + +prepare("SELECT user, SUM(best) AS total_time, COUNT(track) AS count FROM records WHERE track LIKE :track_set GROUP BY user HAVING count < 25 ORDER BY count DESC, total_time ASC"); + $st->bindValue('track_set', addcslashes("$trackSet", "?%")."%", PDO::PARAM_STR); + $st->execute(); + while ($row = $st->fetch()) { + //print_r($row); +?> + + + +getMessage()); + } + ?> +
UserTotal time
s
∞s
+ + + + + trackmania-records + + + + + + +
+

trackmania-records

+ +No records have been uploaded yet...

"; + } +?> + +

Upload instructions

+ +

To upload your Trackmania 2020 records to this page, follow these instructions:

+ +
    +
  • Download Python 3 from python.org and install it: https://www.python.org/downloads/
  • +
  • Open a shell/terminal window (cmd.exe or PowerShell)
  • +
  • Install the upload tool by executing this command:
    + pip3 install --upgrade upload-tm-records
  • +
  • Now you can always run the following command to upload your latest records to this server:
    + upload-tm-records.exe
  • +
+ +
+ +

trackmania-records is opensource: github.com/rsnitsch/trackmania-records

+
+ \ No newline at end of file diff --git a/server/html/style.css b/server/html/style.css new file mode 100644 index 0000000..26a177a --- /dev/null +++ b/server/html/style.css @@ -0,0 +1,39 @@ +tr.whiteTracks td:first-child { + background: linear-gradient(90deg, white 3px, transparent 3px); +} + +tr.whiteTracks td:last-child { + background: linear-gradient(270deg, white 3px, transparent 3px); +} + +tr.greenTracks td:first-child { + background: linear-gradient(90deg, green 3px, transparent 3px); +} + +tr.greenTracks td:last-child { + background: linear-gradient(270deg, green 3px, transparent 3px); +} + +tr.blueTracks td:first-child { + background: linear-gradient(90deg, blue 3px, transparent 3px); +} + +tr.blueTracks td:last-child { + background: linear-gradient(270deg, blue 3px, transparent 3px); +} + +tr.redTracks td:first-child { + background: linear-gradient(90deg, red 3px, transparent 3px); +} + +tr.redTracks td:last-child { + background: linear-gradient(270deg, red 3px, transparent 3px); +} + +tr.blackTracks td:first-child { + background: linear-gradient(90deg, black 3px, transparent 3px); +} + +tr.blackTracks td:last-child { + background: linear-gradient(270deg, black 3px, transparent 3px); +} \ No newline at end of file diff --git a/server/html/upload.php b/server/html/upload.php index e9aae6d..811b6f1 100644 --- a/server/html/upload.php +++ b/server/html/upload.php @@ -1,68 +1,69 @@ -setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - - $commands = ['CREATE TABLE IF NOT EXISTS records ( - game TEXT NOT NULL, - user TEXT NOT NULL, - track TEXT NOT NULL, - best INTEGER NOT NULL, - PRIMARY KEY (game, user, track) - )']; - - foreach ($commands as $command) { - $pdo->exec($command); - } - - foreach ($records as $record) { - //print_r($record); - - $user = $record['user']; - $track = $record['track']; - $best = intval($record['best']); - - // Delete previous record. - $st = $pdo->prepare('DELETE FROM records WHERE user = :user AND track = :track'); - $st->bindParam(':user', $user, PDO::PARAM_STR); - $st->bindParam(':track', $track, PDO::PARAM_STR); - $st->execute(); - - // Add new record. - $st = $pdo->prepare("INSERT INTO records (game, user, track, best) VALUES ('Trackmania 2020', :user, :track, :best)"); - $st->bindParam(':user', $user, PDO::PARAM_STR); - $st->bindParam(':track', $track, PDO::PARAM_STR); - $st->bindParam(':best', $best, PDO::PARAM_INT); - $st->execute(); - } - - echo "Success!"; - } catch (PDOException $e) { - echo 'Database error: '.$e->getMessage(); - } +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + + $commands = ['CREATE TABLE IF NOT EXISTS records ( + game TEXT NOT NULL, + user TEXT NOT NULL, + track TEXT NOT NULL, + best INTEGER NOT NULL, + PRIMARY KEY (game, user, track) + )']; + + foreach ($commands as $command) { + $pdo->exec($command); + } + + foreach ($records as $record) { + //print_r($record); + + $user = $record['user']; + $track = $record['track']; + $best = intval($record['best']); + + // Delete previous record. + $st = $pdo->prepare('DELETE FROM records WHERE user = :user AND track = :track'); + $st->bindParam(':user', $user, PDO::PARAM_STR); + $st->bindParam(':track', $track, PDO::PARAM_STR); + $st->execute(); + + // Add new record. + $st = $pdo->prepare("INSERT INTO records (game, user, track, best) VALUES ('Trackmania 2020', :user, :track, :best)"); + $st->bindParam(':user', $user, PDO::PARAM_STR); + $st->bindParam(':track', $track, PDO::PARAM_STR); + $st->bindParam(':best', $best, PDO::PARAM_INT); + $st->execute(); + } + + echo "Success!"; + } catch (PDOException $e) { + http_response_code(500); + die('Database error: '.$e->getMessage()); + }