first commit

This commit is contained in:
jr-k 2024-02-02 01:36:47 +01:00
commit d746f5b541
8 changed files with 320 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.idea
*.iws
*.iml
*.ipr
out/
data/uploads/*
data/slideshow.json

45
README.md Normal file
View File

@ -0,0 +1,45 @@
# Reclame
## About
Use a RaspberryPi to show a full-screen Slideshow (Kiosk-mode)
## Installation TL;DR
```bash
sudo apt-get update && sudo apt-get dist-upgrade
sudo apt-get install git chromium-browser -y
git clone https://github.com/jr-k/reclame.git
cd reclame && pip3 install -r requirements.txt && cp data/slideshow.json.dist data/slideshow.json
./reclame.py
```
## Installation - step by step
### Basic Setup
For basic RaspberryPi setup you can use most of the available guides, for example this one:
https://gist.github.com/blackjid/dfde6bedef148253f987
### HDMI Mode
You may need to set the HDMI Mode on the raspi to ensure the hdmi resolution matches your screen exactly. Here is the official documentation:
https://www.raspberrypi.org/documentation/configuration/config-txt/video.md
However, I used this one: `(2,82) = 1920x1080 60Hz 1080p`
### Installation of base software
```bash
sudo apt-get update
sudo apt-get upgrade
sudo apt-get dist-upgrade
sudo apt-get install git chromium-browser -y
git clone https://github.com/jr-k/reclame.git
cd reclame && pip3 install -r requirements.txt && cp data/slideshow.json.dist data/slideshow.json
./reclame.py
```
## Prepare your Slideshow
Everything slideshow-related happens in the ./data/uploads folder.
- Put some images into the /data/uploads folder. Ideally with the same resultion of the screen (eg. 1920x1080px).
- Edit the slideshow.json
## You are done now :)
If everything is set up correctly, the RaspberryPi shall start chromium in fullscreen directly after bootup and after some seconds of showing the date & time (default.html) your slideshow shall start and loop endlessly.

1
data/404.html Executable file
View File

@ -0,0 +1 @@
404 Not found

4
data/slideshow.json.dist Executable file
View File

@ -0,0 +1,4 @@
[
{"location":"https://ffmpeg.org","delay":10,"type":"url"},
{"location":"https://unix.org","delay":20,"type":"url"},
]

67
reclame.py Executable file
View File

@ -0,0 +1,67 @@
#!/usr/bin/python3
from flask import Flask, render_template, redirect, request, url_for, send_from_directory
import json
import os
import re
import shutil
import subprocess
# <server>
app = Flask(__name__, template_folder='views', static_folder='data')
port = 5000
# </server>
# <xenv>
destination_path = '/home/pi/.config/lxsession/LXDE-pi/autostart'
os.makedirs(os.path.dirname(destination_path), exist_ok=True)
xenv_presets = f"""
@lxpanel --profile LXDE-pi
@pcmanfm --desktop --profile LXDE-pi
@xscreensaver -no-splash
#@point-rpi
@xset s off
@xset -dpms
@xset s noblank
@unclutter -display :0 -noevents -grab
@sed -i 's/"exited_cleanly": false/"exited_cleanly": true/' ~/.config/chromium/Default/Preferences
@chromium-browser --autoplay-policy=no-user-gesture-required --start-maximized --allow-running-insecure-content --remember-cert-error-decisions --disable-restore-session-state --noerrdialogs --kiosk --incognito --window-position=0,0 --display=:0 http://localhost:{port}
"""
with open(destination_path, 'w') as file:
file.write(xenv_presets)
# </xenv>
# <utils>
def get_ip_address():
try:
result = subprocess.run(
["ip", "-4", "route", "get", "8.8.8.8"],
capture_output=True,
text=True
)
ip_address = result.stdout.split()[6]
return ip_address
except Exception as e:
print(f"Error obtaining IP address: {e}")
return 'Unknown'
# </utils>
# <web>
@app.route('/')
def index():
with open('./data/slideshow.json', 'r') as file:
items = json.load(file)
return render_template('player.jinja.html', port=port, items=json.dumps(items))
@app.route('/slide/default')
def slide_default():
return render_template('default.jinja.html', ipaddr=get_ip_address())
@app.errorhandler(404)
def not_found(e):
return send_from_directory('data', '404.html'), 404
# </web>
if __name__ == '__main__':
app.run(host='0.0.0.0', port=port)

1
requirements.txt Executable file
View File

@ -0,0 +1 @@
flask

64
views/default.jinja.html Executable file
View File

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<head>
<script type="text/javascript">
function updateTime() {
var date = new Date();
var hours = (date.getHours() < 10 ? '0' : '') + date.getHours();
var minutes = (date.getMinutes() < 10 ? '0' : '') + date.getMinutes();
var seconds = (date.getSeconds() < 10 ? '0' : '') + date.getSeconds();
var dayInMonth = date.getDate();
var month = date.getMonth();
var year = date.getFullYear();
var day = date.getDay();
var dayLabels = ["Dimanche", "Lundi", "Mardis", "Mercredi", "Jeudi", "Vendredi", "Samedi"];
var monthLabels = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"];
var timeLabel = hours + ":" + minutes;
var dateLabel = dayLabels[day] + " " + dayInMonth + " " + monthLabels[month] + " " + year;
document.getElementById('time').innerHTML = timeLabel;
document.getElementById('date').innerHTML = dateLabel;
setTimeout(updateTime, 1000);
}
window.addEventListener("load", updateTime);
const urlParams = new URLSearchParams(window.location.search);
</script>
<style>
body {
text-align: center;
font-family: 'Arial', 'sans-serif';
color: white;
background-color: black
}
#bottom {
background: #111;
position: fixed;
left: 0;
right: 0;
bottom: 0;
padding: 20px 0;
}
#time {
font-size: 10em;
}
#date {
font-size: 3em;
}
#ipaddr {
font-size: 1em;
}
</style>
</head>
<body>
<div id="time"></div>
<div id="date"></div>
<div id="bottom">
<div id="ipaddr"></div>
</div>
<script>
document.getElementById('ipaddr').innerText = '{{ ipaddr|safe }}';
</script>
</body>
</html>

131
views/player.jinja.html Executable file
View File

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<title>
Reclame
</title>
<meta name="robots" content="noindex, nofollow">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
background-color: white;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.slide {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
background: black;
}
.slide, iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
padding-top: 0;
box-sizing: border-box;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
}
.slide iframe {
background: white;
}
.slide img {
height: 100%;
}
</style>
</head>
<body>
<div id="FirstSlide" class="slide" style="visibility: hidden;">
<iframe src="/slide/default"></iframe>
</div>
<div id="SecondSlide" style="visibility: visible;">
<iframe src="/slide/default"></iframe>
</div>
<script type="text/javascript">
var items = {{ items|safe }};
var delay = 3000 / 1;
var curUrl = 0;
preloadIntoFirstSlide();
function loadUrl(element, callbackReady) {
element.innerHTML = `<iframe src="${items[curUrl].location}"></iframe>`;
callbackReady();
}
function loadPicture(element, callbackReady) {
element.innerHTML = `<img src="${items[curUrl].location}" alt="" />`;
callbackReady();
}
function loadVideo(element, callbackReady) {
element.innerHTML = `<video autoplay controls><source src=${items[curUrl].location} type="video/mp4" /></video>`;
var video = element.querySelector('video');
video.addEventListener('loadedmetadata', function() {
items[curUrl].delay = Math.ceil(video.duration);
console.log('change delay to ', video.duration)
console.log(items[curUrl]);
callbackReady();
});
}
function preloadIntoFirstSlide() {
var element = document.getElementById('FirstSlide');
var callbackReady = function() {
setTimeout("moveToFirstSlide()", delay);
};
switch (items[curUrl].type) {
case 'url': loadUrl(element, callbackReady); break;
case 'picture': loadPicture(element, callbackReady); break;
case 'video': loadVideo(element, callbackReady); break;
default: loadUrl(element, callbackReady); break;
}
}
function moveToFirstSlide() {
console.log(items[curUrl]);
delay = items[curUrl].delay * 1000;
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
document.querySelector('#FirstSlide').style.visibility = 'visible';
document.querySelector('#SecondSlide').style.visibility = 'hidden';
preloadIntoSecondSlide();
}
function preloadIntoSecondSlide() {
var element = document.getElementById('SecondSlide');
var callbackReady = function() {
setTimeout("moveToSecondSlide()", delay);
};
switch (items[curUrl].type) {
case 'url': loadUrl(element, callbackReady); break;
case 'picture': loadPicture(element, callbackReady); break;
case 'video': loadVideo(element, callbackReady); break;
default: loadUrl(element, callbackReady); break;
}
}
function moveToSecondSlide() {
delay = items[curUrl].delay * 1000;
curUrl = (curUrl + 1) === items.length ? 0 : curUrl + 1;
document.querySelector('#FirstSlide').style.visibility = 'hidden';
document.querySelector('#SecondSlide').style.visibility = 'visible';
preloadIntoFirstSlide();
}
</script>
</body>
</html>