From d6efabe24cc346416a46d4036f655cf0ea34f1ef Mon Sep 17 00:00:00 2001 From: Fred Boniface Date: Mon, 14 Aug 2023 20:26:26 +0100 Subject: [PATCH] Init repo --- .gitignore | 160 ++++++++++++++++++++++++++++++++++++++++++ LICENSE | 14 ++++ README.md | 43 ++++++++++++ example.conf.toml | 27 +++++++ main.py | 175 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 419 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 example.conf.toml create mode 100644 main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6769e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1a28641 --- /dev/null +++ b/LICENSE @@ -0,0 +1,14 @@ +Copyright 2023 Frederick Boniface (git.fjla.uk/fredboniface.co.uk) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..9b153cb --- /dev/null +++ b/README.md @@ -0,0 +1,43 @@ +# map-dots-fetch + +A one-shot Python (3.11) script that fetches one or more images from a map-dots server. + +Remember that map-dots can be run as a CLI application so if you are not looking to access map-dots images on more than one computer, you are probably best skipping this script and running map-dots locally. + + +## Running + +map-dots-fetch uses imports from the standard-library with the exception of `requests`. You can choose to run it in a venv however it has been written to run in the global environment and should not interfere with any other Python applications that you are running. + +map-dots-fetch requires a configuration file, an example of which is provided in this repository. The configuration file must be placed in one of these locations: + +Linux systems: + - `~/.config/map-dots-fetch/conf.toml` + - `/etc/map-dots-fetch/conf.toml` + - `./conf.toml` In the same folder as map-dots-fetch.py + +Windows systems: + - `C:\Users\{user}\map-dots-fetch.toml` Alongside your user files + - `./conf.toml` + +If the config file exists in more than one place, the first file found will be loaded. Files are checked in the order they appear in the lists. + +If no config file is present, the script will exit with an exit code of `1`. + + +## License + +Copyright 2023 Frederick Boniface (git.fjla.uk/fredboniface.co.uk) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/example.conf.toml b/example.conf.toml new file mode 100644 index 0000000..fe72a6c --- /dev/null +++ b/example.conf.toml @@ -0,0 +1,27 @@ + [server] + # Include the scheme in the url, eg. "https://" or "http://" + url = "" + port = 443 + + [images] + # Image settings are arrays, this enables you to generate multiple images with + # different settings at the same time. + + # Traccar Device ID + deviceId = [1] + + # Output Image Size + size = [[1920,1080]] + + # Valid values from from & to are: "now", "-hour", "-day", "-week", "-month", "-quarter" + from = ["-month"] + to = ["now"] + + # Output Image Style + style = ["circles"] + + format = "png" + + [files] + destDir = "/var/wallpapers/map-dots" + diff --git a/main.py b/main.py new file mode 100644 index 0000000..71991fe --- /dev/null +++ b/main.py @@ -0,0 +1,175 @@ +### map-dots-fetch +### Connects to a map-dots instance and fetches images, saving them to the filesystem. Cross platform support is +### provided and only the Python standard library is used - it should run on any system with Python 3.x installed + +### The script is designed to be run on a schedule using systemd-timers of cron. The choice is yours - of course +### you can also run it manually if you wish. + +### A configuration file should be created at /etc/map-dots-fetch/conf.toml or ~/.config/map-dots-fetch/conf.toml for Linux +### or at \Users\{username}\map-dots-fetch.toml for Windows. +### If no configuration file can be found at these paths, the program directory will be checked. +### If both paths exist on Linux, the file in your home directory will take precedence +### Configurable Options (Example configuration): +""" +conf.toml +--------- + [server] + # Include the scheme in the url, eg. "https://" or "http://" + url = "" + port = 443 + + [images] + # Image settings are arrays, this enables you to generate multiple images with + # different settings at the same time. + + # Traccar Device ID + deviceId = [1] + + # Output Image Size + size = [[1920,1080]] + + # Valid values from from & to are: "now", "-hour", "-day", "-week", "-month", "-quarter" + from = ["-month"] + to = ["now"] + + # Output Image Style + style = ["circles"] + + format = "png" + + [files] + destDir = "/var/wallpapers/map-dots" + +""" + + +##### LICENSE ##### +""" +Copyright 2023 Frederick Boniface (git.fjla.uk/fredboniface.co.uk) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +documentation files (the “Software”), to deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit +persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the +Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +""" + +import os, sys, requests, datetime, tomllib +from pathlib import Path + +pre_start = datetime.datetime.now().timestamp() + +print("Finding configuration file") +user = os.environ.get('USERNAME', os.environ.get('USER')) +os_type = os.name + +global_conf_path = "/etc/map-dots-fetch/conf.toml" +home_path = Path.home() +local_conf_path = f"{home_path}/.config/map-dots-fetch/conf.toml" +win_conf_path = f"{home_path}\\map-dots-fetch.toml" + +if os.path.exists(local_conf_path) and os_type == "posix": + conf_path = local_conf_path +elif os.path.exists(global_conf_path) and os_type == "posix": + conf_path = global_conf_path +elif os.path.exists(win_conf_path) and os_type == "nt": + conf_path = win_conf_path +elif os.path.exists("conf.toml"): + conf_path = "conf.toml" +else: + if os_type == "posix": + print(f"Unable to find configuration file after searching '{global_conf_path}', '{local_conf_path}', and './conf.toml'") + elif os_type == "nt": + print(f"Unable to find configuration file after searching '{win_conf_path}' and this directory") + else: + print(f"System type: {os_type}, not supported.") + sys.exit(1) + +print(f"Loading configuration file: {conf_path}") +with open(conf_path, 'rb') as conf: + conf_values = tomllib.load(conf) + +request_url = f"{conf_values['server']['url']}:{conf_values['server']['port']}/traccar/" + +# Define usable datetime objects +dateMap = { + 'now': datetime.datetime.utcnow(), + '-hour': datetime.datetime.utcnow() - datetime.timedelta(hours=1), + '-day': datetime.datetime.utcnow() - datetime.timedelta(days=1), + '-week': datetime.datetime.utcnow() - datetime.timedelta(weeks=1), + '-month': datetime.datetime.utcnow() - datetime.timedelta(days=30), # Approximate 30 days + '-quarter': datetime.datetime.utcnow() - datetime.timedelta(days=90) # Approximate 90 days +} + +print("Ensuring destination folders exist") +os.makedirs(conf_values['files']['destDir'], exist_ok=True) + +for i in range(len(conf_values['images']['deviceId'])): + img_dev_id = conf_values['images']['deviceId'][i] + img_width, img_height = conf_values['images']['size'][i] + img_from = dateMap[conf_values['images']['from'][i]].strftime("%Y-%m-%dT%H:%M:%SZ") + img_to = dateMap[conf_values['images']['to'][i]].strftime("%Y-%m-%dT%H:%M:%SZ") + img_style = conf_values['images']['style'][i] + img_format = conf_values['images']['format'] + + print("Requesting image:") + print(f"ID: {img_dev_id}, Size: {img_width}x{img_height}, From/To: {img_from}, {img_to}, Style: {img_style}, Format: {img_format}") + + params = { + "id": img_dev_id, + "width": img_width, + "height": img_height, + "from": img_from, + "to": img_to, + "format": img_format + } + + pre_req_time = datetime.datetime.now().timestamp() + try: + res = requests.get(request_url, params=params, timeout=120) + except requests.Timeout: + print("HTTP request timed out after 2 minutes") + sys.exit(1) + except requests.RequestException as e: + print("Error with HTTP request: ", e) + sys.exit(1) + + post_res_time = datetime.datetime.now().timestamp() + print(f"HTTP request completed in {post_res_time - pre_req_time} seconds") + + if res.status_code != 200: + print(f"HTTP Response error: {res.status_code}") + sys.exit(1) + + print("Ensuring image directory exists") + image_dir = os.path.join(conf_values['files']['destDir'], f"{i}") + os.makedirs(image_dir, exist_ok=True) + + print("Deleting old image files") + for filename in os.listdir(image_dir): + file_path = os.path.join(image_dir, filename) + try: + if os.path.isfile(file_path): + os.unlink(file_path) + except Exception as e: + print("Error deleting old file: ", e) + + print("Saving image") + image_filename = f"mapdot-{i}-{img_to}.png" + image_path = os.path.join(image_dir, image_filename) + try: + with open(image_path, "wb") as image_file: + image_file.write(res.content) + print(f"Image saved: {image_path}") + except Exception as e: + print(f"Unable to save image file: {e}") + +pre_finish = datetime.datetime.now().timestamp() +print(f"map-dots-fetch completed in: {pre_finish - pre_start} seconds")