Added quality of life functions

* Ability to use full path for image path
*  Ability to configure the wait time between images
*  Added protection from escaping the image directory
This commit is contained in:
brammp committed 2026-09-21 23:13:19 +02:00
1 parent d0564914e8
commit f6c2b9dd5b
4 files changed
+65 -7

No files matched your search

+1
View File
@@ -16,6 +16,7 @@ A chromecast ambient mode like Diashow
1. Install `pyinstaller` using `pip install -U pyinstaller` 1. Install `pyinstaller` using `pip install -U pyinstaller`
2. Create binary using `pyinstaller server-pyinstaller.spec` 2. Create binary using `pyinstaller server-pyinstaller.spec`
3. The binary is created in the dist folder, to use it create a config an the configured image folder here (or wherever you move the binary to) 3. The binary is created in the dist folder, to use it create a config an the configured image folder here (or wherever you move the binary to)
4. Update the path to the images to a full path
* if you wish to cleanup you can remove the build (and if you copied the binary somware else the dist folder) * if you wish to cleanup you can remove the build (and if you copied the binary somware else the dist folder)
* Removing `pyinstaller` is safe you can use `pip uninstall -y altgraph setuptools packaging pyinstaller-hooks-contrib pyinstaller pywin32-ctypes pefile` * Removing `pyinstaller` is safe you can use `pip uninstall -y altgraph setuptools packaging pyinstaller-hooks-contrib pyinstaller pywin32-ctypes pefile`
+1
View File
@@ -1,6 +1,7 @@
{ {
"PORT":8000, "PORT":8000,
"IMAGE_DIR":"images", "IMAGE_DIR":"images",
"IMAGE_TIMEOUT":Minutes the image is shown,
"WeatherProvider":"HomeAssistant or MetNO", "WeatherProvider":"HomeAssistant or MetNO",
"METNO_LAT":"MetNO Weather GPS Latitude", "METNO_LAT":"MetNO Weather GPS Latitude",
"METNO_LON":"MetNO Weather GPS longitude", "METNO_LON":"MetNO Weather GPS longitude",
+58 -3
View File
@@ -7,6 +7,8 @@ from PIL import Image
from PIL.ExifTags import TAGS from PIL.ExifTags import TAGS
import urllib.parse import urllib.parse
from datetime import datetime from datetime import datetime
import mimetypes
from pathlib import Path
# -- # --
# Configuration # Configuration
@@ -21,6 +23,8 @@ class SlideshowHandler(http.server.SimpleHTTPRequestHandler):
def do_GET(self): def do_GET(self):
if self.path == "/": if self.path == "/":
self.send_slideshow_page() self.send_slideshow_page()
elif self.path == "/config.js":
self.send_clientconf()
elif self.path == "/images.json": elif self.path == "/images.json":
self.send_image_list() self.send_image_list()
elif self.path == "/weather": elif self.path == "/weather":
@@ -30,8 +34,10 @@ class SlideshowHandler(http.server.SimpleHTTPRequestHandler):
self.get_METNOweather() self.get_METNOweather()
elif self.path.startswith("/exif"): elif self.path.startswith("/exif"):
self.send_exif_data() self.send_exif_data()
elif self.path.startswith("/img"):
self.send_image()
else: else:
super().do_GET() self.send_error(404, "Not found")
#-- #--
# Main Page # Main Page
@@ -49,13 +55,51 @@ class SlideshowHandler(http.server.SimpleHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(content) self.wfile.write(content)
#--
# Image depot
#--
def send_image(self):
parsed = urllib.parse.urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
filename = params.get("file", [None])[0]
if not filename:
self.send_error(400, "Missing ?file= parameter")
return
base_dir = Path(cfg["IMAGE_DIR"]).resolve()
print(base_dir)
file_path = (base_dir / filename).resolve()
try:
file_path.relative_to(base_dir)
except ValueError:
self.send_error(403, "Forbidden")
return
content_type, _ = mimetypes.guess_type(filename)
self.send_response(200)
self.send_header("Content-Type", content_type or "application/octet-stream")
self.end_headers()
with open(cfg['IMAGE_DIR'] + "/" + filename, 'rb') as file:
self.wfile.write(file.read())
#--
# Client config
#--
def send_clientconf(self):
self.send_response(200)
self.send_header("Content-type", "text/javascript")
self.end_headers()
response = f"""const ImgDuration = "{cfg["IMAGE_TIMEOUT"]}";"""
self.wfile.write(response.encode("utf-8"))
#-- #--
# Image list # Image list
#-- #--
def send_image_list(self): def send_image_list(self):
files = os.listdir(cfg['IMAGE_DIR']) files = os.listdir(cfg['IMAGE_DIR'])
images = [ images = [
f"/{cfg['IMAGE_DIR']}/{f}" f"{f}"
for f in files for f in files
if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp")) if f.lower().endswith((".jpg", ".jpeg", ".png", ".gif", ".webp"))
] ]
@@ -145,6 +189,7 @@ class SlideshowHandler(http.server.SimpleHTTPRequestHandler):
self.send_header("Content-Length", str(len(payload))) self.send_header("Content-Length", str(len(payload)))
self.end_headers() self.end_headers()
self.wfile.write(payload) self.wfile.write(payload)
#-- #--
# Image info # Image info
#-- #--
@@ -157,7 +202,17 @@ class SlideshowHandler(http.server.SimpleHTTPRequestHandler):
self.send_error(400, "Missing ?file= parameter") self.send_error(400, "Missing ?file= parameter")
return return
filepath = filename.lstrip("/") base_dir = Path(cfg["IMAGE_DIR"]).resolve()
print(base_dir)
file_path = (base_dir / filename).resolve()
try:
file_path.relative_to(base_dir)
except ValueError:
self.send_error(403, "Forbidden")
return
filepath = cfg['IMAGE_DIR'] +"/"+ filename
if not os.path.exists(filepath): if not os.path.exists(filepath):
self.send_error(404, "File not found") self.send_error(404, "File not found")
+5 -4
View File
@@ -134,6 +134,7 @@
</div> </div>
</div> </div>
<script src="/config.js"></script>
<script> <script>
// --- Slideshow --- // --- Slideshow ---
@@ -149,7 +150,7 @@
const photoB = document.getElementById('photoB'); const photoB = document.getElementById('photoB');
// Start with A visible // Start with A visible
photoA.style.backgroundImage = 'url(' + images[index] + ')'; photoA.style.backgroundImage = 'url(' + "/img?file=" + images[index] + ')';
photoA.classList.add('visible'); photoA.classList.add('visible');
photoInfo(images[index]) photoInfo(images[index])
document.getElementById('photo-Date').textContent = "" document.getElementById('photo-Date').textContent = ""
@@ -157,17 +158,17 @@
setInterval(() => { setInterval(() => {
index = (index + 1) % images.length; index = (index + 1) % images.length;
if (showingA) { if (showingA) {
photoB.style.backgroundImage = 'url(' + images[index] + ')'; photoB.style.backgroundImage = 'url(' + "/img?file=" + images[index] + ')';
photoB.classList.add('visible'); photoB.classList.add('visible');
photoA.classList.remove('visible'); photoA.classList.remove('visible');
} else { } else {
photoA.style.backgroundImage = 'url(' + images[index] + ')'; photoA.style.backgroundImage = 'url(' + "/img?file=" + images[index] + ')';
photoA.classList.add('visible'); photoA.classList.add('visible');
photoB.classList.remove('visible'); photoB.classList.remove('visible');
} }
photoInfo(images[index]) photoInfo(images[index])
showingA = !showingA; showingA = !showingA;
}, 5 * 60 * 1000); }, ImgDuration * 60 * 1000);
} }
start(); start();