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:
4 files changed
+65
-7
No files matched your search
@@ -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,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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user