Merge branch 'development' of gitea.centx.de:lelo/bethaus-app into development
This commit is contained in:
commit
b992303d41
62
app.py
62
app.py
@ -237,7 +237,7 @@ def api_browse(subpath):
|
|||||||
@app.route("/media/<path:subpath>")
|
@app.route("/media/<path:subpath>")
|
||||||
@auth.require_secret
|
@auth.require_secret
|
||||||
def serve_file(subpath):
|
def serve_file(subpath):
|
||||||
# ─── 1) Locate the real file on disk ───
|
# 1) Locate the real file on disk
|
||||||
root, *relative_parts = subpath.split('/')
|
root, *relative_parts = subpath.split('/')
|
||||||
base_path = session['folders'].get(root)
|
base_path = session['folders'].get(root)
|
||||||
full_path = os.path.join(base_path or '', *relative_parts)
|
full_path = os.path.join(base_path or '', *relative_parts)
|
||||||
@ -246,7 +246,7 @@ def serve_file(subpath):
|
|||||||
app.logger.error(f"File not found: {full_path}")
|
app.logger.error(f"File not found: {full_path}")
|
||||||
return "File not found", 404
|
return "File not found", 404
|
||||||
|
|
||||||
# ─── 2) Prep request info ───
|
# 2) Prep request info
|
||||||
mime, _ = mimetypes.guess_type(full_path)
|
mime, _ = mimetypes.guess_type(full_path)
|
||||||
mime = mime or 'application/octet-stream'
|
mime = mime or 'application/octet-stream'
|
||||||
|
|
||||||
@ -259,29 +259,18 @@ def serve_file(subpath):
|
|||||||
if mime == 'audio/mpeg' and request.method != 'HEAD':
|
if mime == 'audio/mpeg' and request.method != 'HEAD':
|
||||||
do_log = False
|
do_log = False
|
||||||
|
|
||||||
# ─── 3) Pick the right cache ───
|
# 3) Pick cache
|
||||||
if mime.startswith('audio/'): cache = cache_audio
|
if mime.startswith('audio/'): cache = cache_audio
|
||||||
elif mime.startswith('image/'): cache = cache_image
|
elif mime.startswith('image/'): cache = cache_image
|
||||||
elif mime.startswith('video/'): cache = cache_video
|
elif mime.startswith('video/'): cache = cache_video
|
||||||
else: cache = cache_other
|
else: cache = cache_other
|
||||||
|
|
||||||
# ─── 4) Try to stream directly from diskcache ───
|
# 4) Ensure cached on‑disk file and get its path
|
||||||
try:
|
try:
|
||||||
# returns a file-like whose .name is the real path on disk
|
|
||||||
with cache.read(subpath) as reader:
|
with cache.read(subpath) as reader:
|
||||||
file_path = reader.name
|
file_path = reader.name
|
||||||
filesize = os.path.getsize(file_path)
|
|
||||||
response = send_file(
|
|
||||||
file_path,
|
|
||||||
mimetype= mime,
|
|
||||||
conditional= True # enable Range / If-Modified / etc
|
|
||||||
)
|
|
||||||
cached_hit = True
|
|
||||||
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# cache miss → generate & write back to cache
|
# cache miss
|
||||||
cached_hit = False
|
|
||||||
|
|
||||||
if mime.startswith('image/'):
|
if mime.startswith('image/'):
|
||||||
# ─── 4a) Image branch: thumbnail & cache the JPEG ───
|
# ─── 4a) Image branch: thumbnail & cache the JPEG ───
|
||||||
try:
|
try:
|
||||||
@ -289,51 +278,52 @@ def serve_file(subpath):
|
|||||||
img.thumbnail((1920, 1920))
|
img.thumbnail((1920, 1920))
|
||||||
if img.mode in ("RGBA", "P"):
|
if img.mode in ("RGBA", "P"):
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
|
|
||||||
thumb_io = io.BytesIO()
|
thumb_io = io.BytesIO()
|
||||||
img.save(thumb_io, format='JPEG', quality=85)
|
img.save(thumb_io, format='JPEG', quality=85)
|
||||||
thumb_io.seek(0)
|
thumb_io.seek(0)
|
||||||
|
|
||||||
# write thumbnail into diskcache as a real file
|
# write thumbnail into diskcache as a real file
|
||||||
cache.set(subpath, thumb_io, read=True)
|
cache.set(subpath, thumb_io, read=True)
|
||||||
|
|
||||||
# now re-open from cache to get the on-disk path
|
# now re-open from cache to get the on-disk path
|
||||||
with cache.read(subpath) as reader:
|
with cache.read(subpath) as reader:
|
||||||
file_path = reader.name
|
file_path = reader.name
|
||||||
filesize = os.path.getsize(file_path)
|
|
||||||
response = send_file(
|
|
||||||
file_path,
|
|
||||||
mimetype= 'image/jpeg',
|
|
||||||
conditional= True
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Image processing failed for {subpath}: {e}")
|
app.logger.error(f"Image processing failed for {subpath}: {e}")
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# ─── 4b) Non-image branch: cache original file ───
|
# ─── 4b) Non-image branch: cache original file ───
|
||||||
try:
|
try:
|
||||||
# store the real file on diskcache
|
# store the real file on diskcache
|
||||||
cache.set(subpath, open(full_path, 'rb'), read=True)
|
cache.set(subpath, open(full_path, 'rb'), read=True)
|
||||||
|
|
||||||
# read back to get its path
|
# read back to get its path
|
||||||
with cache.read(subpath) as reader:
|
with cache.read(subpath) as reader:
|
||||||
file_path = reader.name
|
file_path = reader.name
|
||||||
filesize = os.path.getsize(file_path)
|
|
||||||
response = send_file(
|
|
||||||
file_path,
|
|
||||||
mimetype= mime,
|
|
||||||
conditional= True
|
|
||||||
)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error(f"Failed to cache file {subpath}: {e}")
|
app.logger.error(f"Failed to cache file {subpath}: {e}")
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
# ─── 5) Common headers & logging ───
|
filesize = os.path.getsize(file_path)
|
||||||
|
cached_hit = True # or False if you want to pass that into your logger
|
||||||
|
|
||||||
|
# 5) Figure out download flag and filename
|
||||||
|
ask_download = request.args.get('download') == 'true'
|
||||||
|
filename = os.path.basename(full_path)
|
||||||
|
|
||||||
|
# 6) Single send_file call with proper attachment handling
|
||||||
|
response = send_file(
|
||||||
|
file_path,
|
||||||
|
mimetype = mime,
|
||||||
|
conditional = True,
|
||||||
|
as_attachment = ask_download,
|
||||||
|
download_name = filename if ask_download else None
|
||||||
|
)
|
||||||
|
|
||||||
|
# Explicitly force inline if not downloading
|
||||||
|
if not ask_download:
|
||||||
|
response.headers['Content-Disposition'] = 'inline'
|
||||||
|
|
||||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||||
|
|
||||||
|
# 7) Logging
|
||||||
if do_log:
|
if do_log:
|
||||||
a.log_file_access(
|
a.log_file_access(
|
||||||
subpath, filesize, mime,
|
subpath, filesize, mime,
|
||||||
|
|||||||
@ -472,15 +472,19 @@ document.getElementById('globalAudio').addEventListener('ended', () => {
|
|||||||
|
|
||||||
|
|
||||||
function syncThemeColor() {
|
function syncThemeColor() {
|
||||||
// read the CSS variable from :root (or any selector)
|
|
||||||
const cssVar = getComputedStyle(document.documentElement)
|
const cssVar = getComputedStyle(document.documentElement)
|
||||||
.getPropertyValue('--dark-background').trim();
|
.getPropertyValue('--dark-background')
|
||||||
if (cssVar) {
|
.trim();
|
||||||
document
|
if (!cssVar) return;
|
||||||
.querySelector('meta[name="theme-color"]')
|
|
||||||
|
// sync the theme‑color meta tag
|
||||||
|
document.querySelector('meta[name="theme-color"]')
|
||||||
.setAttribute('content', cssVar);
|
.setAttribute('content', cssVar);
|
||||||
}
|
|
||||||
|
// apply fill to every <svg> inside elements with .icon-color
|
||||||
|
document
|
||||||
|
.querySelectorAll('.icon-color svg')
|
||||||
|
.forEach(svg => svg.setAttribute('fill', cssVar));
|
||||||
}
|
}
|
||||||
|
|
||||||
// sync once on load
|
|
||||||
document.addEventListener('DOMContentLoaded', syncThemeColor);
|
document.addEventListener('DOMContentLoaded', syncThemeColor);
|
||||||
@ -1,19 +1,22 @@
|
|||||||
|
// read the CSS variable from :root (or any selector)
|
||||||
|
const cssVar = getComputedStyle(document.documentElement).getPropertyValue('--dark-background').trim();
|
||||||
|
|
||||||
const playerButton = document.querySelector('.player-button'),
|
const playerButton = document.querySelector('.player-button'),
|
||||||
audio = document.querySelector('audio'),
|
audio = document.querySelector('audio'),
|
||||||
timeline = document.querySelector('.timeline'),
|
timeline = document.querySelector('.timeline'),
|
||||||
timeInfo = document.getElementById('timeInfo'),
|
timeInfo = document.getElementById('timeInfo'),
|
||||||
playIcon = `
|
playIcon = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
`,
|
`,
|
||||||
pauseIcon = `
|
pauseIcon = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
`,
|
`,
|
||||||
soundIcon = `
|
soundIcon = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="${cssVar}">
|
||||||
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
`
|
`
|
||||||
@ -98,37 +101,21 @@ audio.onended = function() {
|
|||||||
playerButton.innerHTML = playIcon;
|
playerButton.innerHTML = playIcon;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function downloadAudio() {
|
async function downloadAudio() {
|
||||||
|
const audio = document.getElementById('globalAudio');
|
||||||
const src = audio.currentSrc || audio.src;
|
const src = audio.currentSrc || audio.src;
|
||||||
if (!src) return;
|
if (!src) return;
|
||||||
|
|
||||||
// Build a fresh URL every time
|
// Build the URL with your download flag + cache‑buster
|
||||||
const requestUrl = new URL(src, window.location.href);
|
const downloadUrl = new URL(src, window.location.href);
|
||||||
requestUrl.searchParams.set('_', Date.now());
|
downloadUrl.searchParams.set('download', 'true');
|
||||||
|
downloadUrl.searchParams.set('_', Date.now());
|
||||||
// Force network fetch and include same‑origin credentials
|
|
||||||
const response = await fetch(requestUrl.toString(), {
|
|
||||||
credentials: 'same-origin',
|
|
||||||
cache: 'no-store'
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Download failed: ${response.status}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Turn the response into a blob, then download it
|
|
||||||
const blob = await response.blob();
|
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
|
// Create a “real” link to that URL and click it
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
a.href = blobUrl;
|
a.href = downloadUrl.toString();
|
||||||
// Retain original filename if possible
|
// NOTE: do NOT set a.download here – we want the server's Content-Disposition to drive it
|
||||||
a.download = decodeURIComponent(src.split('/').pop() || 'audio');
|
|
||||||
document.body.appendChild(a);
|
document.body.appendChild(a);
|
||||||
a.click();
|
a.click();
|
||||||
document.body.removeChild(a);
|
document.body.removeChild(a);
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
URL.revokeObjectURL(blobUrl);
|
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
const cacheName = 'gottesdienste-v1.7';
|
const cacheName = 'gottesdienste-v1.8';
|
||||||
const assets = [
|
const assets = [
|
||||||
'/',
|
'/',
|
||||||
'/static/app.css',
|
'/static/app.css',
|
||||||
|
|||||||
@ -63,8 +63,8 @@
|
|||||||
Your browser does not support the audio element.
|
Your browser does not support the audio element.
|
||||||
</audio>
|
</audio>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="player-button">
|
<button class="player-button icon-color">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="#34495e">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@ -72,8 +72,8 @@
|
|||||||
<input type="range" class="timeline" max="100" value="0" step="0.1">
|
<input type="range" class="timeline" max="100" value="0" step="0.1">
|
||||||
<div id="timeInfo" class="now-playing-info"></div>
|
<div id="timeInfo" class="now-playing-info"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="sound-button" onclick="downloadAudio()">
|
<button class="sound-button icon-color" onclick="downloadAudio()">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 120.89" fill="#34495e" width="35" height="35">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 120.89" width="35" height="35">
|
||||||
<path fill-rule="evenodd" d="M84.58,47a7.71,7.71,0,1,1,10.8,11L66.09,86.88a7.72,7.72,0,0,1-10.82,0L26.4,58.37a7.71,7.71,0,1,1,10.81-11L53.1,63.12l.16-55.47a7.72,7.72,0,0,1,15.43.13l-.15,55L84.58,47ZM0,113.48.1,83.3a7.72,7.72,0,1,1,15.43.14l-.07,22q46,.09,91.91,0l.07-22.12a7.72,7.72,0,1,1,15.44.14l-.1,30h-.09a7.71,7.71,0,0,1-7.64,7.36q-53.73.1-107.38,0A7.7,7.7,0,0,1,0,113.48Z"/>
|
<path fill-rule="evenodd" d="M84.58,47a7.71,7.71,0,1,1,10.8,11L66.09,86.88a7.72,7.72,0,0,1-10.82,0L26.4,58.37a7.71,7.71,0,1,1,10.81-11L53.1,63.12l.16-55.47a7.72,7.72,0,0,1,15.43.13l-.15,55L84.58,47ZM0,113.48.1,83.3a7.72,7.72,0,1,1,15.43.14l-.07,22q46,.09,91.91,0l.07-22.12a7.72,7.72,0,1,1,15.44.14l-.1,30h-.09a7.71,7.71,0,0,1-7.64,7.36q-53.73.1-107.38,0A7.7,7.7,0,0,1,0,113.48Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -74,6 +74,7 @@
|
|||||||
<li class="nav-item"><a href="{{ url_for('songs_dashboard') }}" class="nav-link">Auswertung-Wiederholungen</a></li>
|
<li class="nav-item"><a href="{{ url_for('songs_dashboard') }}" class="nav-link">Auswertung-Wiederholungen</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<span>Anzahl Verbindungen: <strong id="totalConnections">0</strong></span>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
@ -108,6 +109,12 @@
|
|||||||
socket.emit("request_initial_data");
|
socket.emit("request_initial_data");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// compute & write stats into the header
|
||||||
|
function updateStats(data) {
|
||||||
|
const total = data.length;
|
||||||
|
document.getElementById("totalConnections").textContent = total;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: Create a table row.
|
// Helper: Create a table row.
|
||||||
// When applyAnimation is true, the row uses the slide-in animation.
|
// When applyAnimation is true, the row uses the slide-in animation.
|
||||||
function createRow(record, applyAnimation = true) {
|
function createRow(record, applyAnimation = true) {
|
||||||
@ -202,6 +209,7 @@
|
|||||||
|
|
||||||
// Listen for incoming connection data from the server.
|
// Listen for incoming connection data from the server.
|
||||||
socket.on("recent_connections", function(data) {
|
socket.on("recent_connections", function(data) {
|
||||||
|
updateStats(data);
|
||||||
animateTableWithNewRow(data);
|
animateTableWithNewRow(data);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -44,6 +44,9 @@
|
|||||||
.btn {
|
.btn {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
.search-container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -53,7 +56,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<h1>{{ title_long }}</h1>
|
<h1>{{ title_long }}</h1>
|
||||||
</header>
|
</header>
|
||||||
<div class="container search-container">
|
<div class="search-container">
|
||||||
<h1>Suche</h1>
|
<h1>Suche</h1>
|
||||||
<form id="searchForm" method="post" class="mb-4">
|
<form id="searchForm" method="post" class="mb-4">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user