Compare commits
No commits in common. "c1ee80ce1d35f991b5947ab7b2e696ee4431331a" and "384775dd3651ce33815ac654e8c20b3d2d5deb6c" have entirely different histories.
c1ee80ce1d
...
384775dd36
1
.gitignore
vendored
1
.gitignore
vendored
@ -16,4 +16,3 @@
|
|||||||
/app_config.json
|
/app_config.json
|
||||||
/custom_logo
|
/custom_logo
|
||||||
/static/theme.css
|
/static/theme.css
|
||||||
/transcription_folder.yml
|
|
||||||
142
app.py
142
app.py
@ -1,6 +1,6 @@
|
|||||||
from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory, make_response, abort
|
from flask import Flask, render_template, send_file, url_for, jsonify, request, session, send_from_directory, abort
|
||||||
import os
|
import os
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image
|
||||||
import io
|
import io
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@ -100,8 +100,6 @@ def list_directory_contents(directory, subpath):
|
|||||||
music_exts = ('.mp3',)
|
music_exts = ('.mp3',)
|
||||||
image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp')
|
image_exts = ('.jpg', '.jpeg', '.png', '.gif', '.bmp')
|
||||||
|
|
||||||
blocked_filenames = ['Thumbs.db']
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with os.scandir(directory) as it:
|
with os.scandir(directory) as it:
|
||||||
# Sorting by name if required.
|
# Sorting by name if required.
|
||||||
@ -110,10 +108,6 @@ def list_directory_contents(directory, subpath):
|
|||||||
if entry.name.startswith('.'):
|
if entry.name.startswith('.'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip blocked_filenames
|
|
||||||
if entry.name in blocked_filenames:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if entry.is_dir(follow_symlinks=False):
|
if entry.is_dir(follow_symlinks=False):
|
||||||
if entry.name in ["Transkription", "@eaDir"]:
|
if entry.name in ["Transkription", "@eaDir"]:
|
||||||
continue
|
continue
|
||||||
@ -255,6 +249,7 @@ def serve_file(subpath):
|
|||||||
# 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'
|
||||||
|
|
||||||
is_cache_request = request.headers.get('X-Cache-Request') == 'true'
|
is_cache_request = request.headers.get('X-Cache-Request') == 'true'
|
||||||
ip_address = request.remote_addr
|
ip_address = request.remote_addr
|
||||||
user_agent = request.headers.get('User-Agent')
|
user_agent = request.headers.get('User-Agent')
|
||||||
@ -265,124 +260,77 @@ def serve_file(subpath):
|
|||||||
do_log = False
|
do_log = False
|
||||||
|
|
||||||
# 3) Pick cache
|
# 3) Pick cache
|
||||||
if mime.startswith('audio/'):
|
if mime.startswith('audio/'): cache = cache_audio
|
||||||
cache = cache_audio
|
elif mime.startswith('image/'): cache = cache_image
|
||||||
elif mime.startswith('image/'):
|
elif mime.startswith('video/'): cache = cache_video
|
||||||
cache = cache_image
|
else: cache = cache_other
|
||||||
elif mime.startswith('video/'):
|
|
||||||
cache = cache_video
|
|
||||||
else:
|
|
||||||
cache = cache_other
|
|
||||||
|
|
||||||
# 4) Image and thumbnail handling first
|
# 4) Ensure cached on‑disk file and get its path
|
||||||
if mime.startswith('image/'):
|
|
||||||
small = request.args.get('thumbnail') == 'true'
|
|
||||||
name, ext = os.path.splitext(subpath)
|
|
||||||
orig_key = subpath
|
|
||||||
small_key = f"{name}_small{ext}"
|
|
||||||
cache_key = small_key if small else orig_key
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Try to read the requested variant
|
|
||||||
with cache.read(cache_key) as reader:
|
|
||||||
file_path = reader.name
|
|
||||||
cached_hit = True
|
|
||||||
except KeyError:
|
|
||||||
if small: # do not create when thumbnail requested
|
|
||||||
response = make_response('', 204)
|
|
||||||
return response
|
|
||||||
|
|
||||||
cached_hit = False
|
|
||||||
# On miss: generate both full-size and small thumb, then cache
|
|
||||||
with Image.open(full_path) as orig:
|
|
||||||
|
|
||||||
img = ImageOps.exif_transpose(orig)
|
|
||||||
variants = {
|
|
||||||
orig_key: (1920, 1920),
|
|
||||||
small_key: ( 480, 480),
|
|
||||||
}
|
|
||||||
for key, size in variants.items():
|
|
||||||
thumb = img.copy()
|
|
||||||
thumb.thumbnail(size, Image.LANCZOS)
|
|
||||||
if thumb.mode in ("RGBA", "P"):
|
|
||||||
thumb = thumb.convert("RGB")
|
|
||||||
bio = io.BytesIO()
|
|
||||||
|
|
||||||
thumb.save(bio, format='JPEG', quality=85)
|
|
||||||
bio.seek(0)
|
|
||||||
cache.set(key, bio, read=True)
|
|
||||||
# Read back the variant we need
|
|
||||||
with cache.read(cache_key) as reader:
|
|
||||||
file_path = reader.name
|
|
||||||
|
|
||||||
# Serve the image variant
|
|
||||||
response = send_file(
|
|
||||||
file_path,
|
|
||||||
mimetype=mime,
|
|
||||||
conditional=True,
|
|
||||||
as_attachment=(request.args.get('download') == 'true'),
|
|
||||||
download_name=os.path.basename(orig_key)
|
|
||||||
)
|
|
||||||
response.headers['Content-Disposition'] = 'inline'
|
|
||||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
|
||||||
|
|
||||||
if do_log and not small:
|
|
||||||
a.log_file_access(
|
|
||||||
cache_key,
|
|
||||||
os.path.getsize(file_path),
|
|
||||||
mime,
|
|
||||||
ip_address,
|
|
||||||
user_agent,
|
|
||||||
session['device_id'],
|
|
||||||
cached_hit
|
|
||||||
)
|
|
||||||
return response
|
|
||||||
|
|
||||||
# 5) Non-image branch: ensure original is cached
|
|
||||||
try:
|
try:
|
||||||
with cache.read(subpath) as reader:
|
with cache.read(subpath) as reader:
|
||||||
file_path = reader.name
|
file_path = reader.name
|
||||||
cached_hit = True
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
cached_hit = False
|
# cache miss
|
||||||
|
if mime.startswith('image/'):
|
||||||
|
# ─── 4a) Image branch: thumbnail & cache the JPEG ───
|
||||||
try:
|
try:
|
||||||
|
with Image.open(full_path) as img:
|
||||||
|
img.thumbnail((1920, 1920))
|
||||||
|
if img.mode in ("RGBA", "P"):
|
||||||
|
img = img.convert("RGB")
|
||||||
|
thumb_io = io.BytesIO()
|
||||||
|
img.save(thumb_io, format='JPEG', quality=85)
|
||||||
|
thumb_io.seek(0)
|
||||||
|
# write thumbnail into diskcache as a real file
|
||||||
|
cache.set(subpath, thumb_io, read=True)
|
||||||
|
# now re-open from cache to get the on-disk path
|
||||||
|
with cache.read(subpath) as reader:
|
||||||
|
file_path = reader.name
|
||||||
|
except Exception as e:
|
||||||
|
app.logger.error(f"Image processing failed for {subpath}: {e}")
|
||||||
|
abort(500)
|
||||||
|
else:
|
||||||
|
# ─── 4b) Non-image branch: cache original file ───
|
||||||
|
try:
|
||||||
|
# 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
|
||||||
with cache.read(subpath) as reader:
|
with cache.read(subpath) as reader:
|
||||||
file_path = reader.name
|
file_path = reader.name
|
||||||
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)
|
||||||
|
|
||||||
# 6) Build response for non-image
|
|
||||||
filesize = os.path.getsize(file_path)
|
filesize = os.path.getsize(file_path)
|
||||||
|
cached_hit = True # or False if you want to pass that into your logger
|
||||||
|
|
||||||
# Figure out download flag and filename
|
# 5) Figure out download flag and filename
|
||||||
ask_download = request.args.get('download') == 'true'
|
ask_download = request.args.get('download') == 'true'
|
||||||
filename = os.path.basename(full_path)
|
filename = os.path.basename(full_path)
|
||||||
|
|
||||||
# Single send_file call with proper attachment handling
|
# 6) Single send_file call with proper attachment handling
|
||||||
response = send_file(
|
response = send_file(
|
||||||
file_path,
|
file_path,
|
||||||
mimetype=mime,
|
mimetype = mime,
|
||||||
conditional=True,
|
conditional = True,
|
||||||
as_attachment=ask_download,
|
as_attachment = ask_download,
|
||||||
download_name=filename if ask_download else None
|
download_name = filename if ask_download else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Explicitly force inline if not downloading
|
||||||
if not ask_download:
|
if not ask_download:
|
||||||
response.headers['Content-Disposition'] = 'inline'
|
response.headers['Content-Disposition'] = 'inline'
|
||||||
|
|
||||||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||||||
|
|
||||||
# 7) Logging
|
# 7) Logging
|
||||||
if do_log:
|
if do_log:
|
||||||
a.log_file_access(
|
a.log_file_access(
|
||||||
subpath,
|
subpath, filesize, mime,
|
||||||
filesize,
|
ip_address, user_agent,
|
||||||
mime,
|
session['device_id'], cached_hit
|
||||||
ip_address,
|
|
||||||
user_agent,
|
|
||||||
session['device_id'],
|
|
||||||
cached_hit
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@ services:
|
|||||||
# Production-ready Gunicorn command with eventlet
|
# Production-ready Gunicorn command with eventlet
|
||||||
command: >
|
command: >
|
||||||
sh -c "pip install -r requirements.txt &&
|
sh -c "pip install -r requirements.txt &&
|
||||||
gunicorn --worker-class eventlet -w 4 -b 0.0.0.0:5000 app:app"
|
gunicorn --worker-class eventlet -w 1 -b 0.0.0.0:5000 app:app"
|
||||||
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@ -237,55 +237,3 @@ footer {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* IMAGE GRID */
|
|
||||||
|
|
||||||
.images-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fill, 150px);
|
|
||||||
grid-auto-rows: 150px;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-item {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
border: 2px solid #ccc;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* the filename overlay, centered at bottom */
|
|
||||||
.image-item .file-name {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
padding: 4px 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
color: #fff;
|
|
||||||
box-sizing: border-box;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* once loaded, hide the filename overlay */
|
|
||||||
.image-item.loaded .file-name {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* thumbnail styling unchanged */
|
|
||||||
.thumbnail {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* pop when loaded */
|
|
||||||
.image-item.loaded .thumbnail {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|||||||
100
static/app.js
100
static/app.js
@ -34,7 +34,8 @@ function paintFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderContent(data) {
|
function renderContent(data) {
|
||||||
// Render breadcrumbs
|
|
||||||
|
// Render breadcrumbs, directories (grid view when appropriate), and files.
|
||||||
let breadcrumbHTML = '';
|
let breadcrumbHTML = '';
|
||||||
data.breadcrumbs.forEach((crumb, index) => {
|
data.breadcrumbs.forEach((crumb, index) => {
|
||||||
breadcrumbHTML += `<a href="#" class="breadcrumb-link" data-path="${crumb.path}">${crumb.name}</a>`;
|
breadcrumbHTML += `<a href="#" class="breadcrumb-link" data-path="${crumb.path}">${crumb.name}</a>`;
|
||||||
@ -44,40 +45,14 @@ function renderContent(data) {
|
|||||||
});
|
});
|
||||||
document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML;
|
document.getElementById('breadcrumbs').innerHTML = breadcrumbHTML;
|
||||||
|
|
||||||
// Check for image-only directory (no subdirectories, at least one file, all images)
|
|
||||||
const isImageOnly = data.directories.length === 0
|
|
||||||
&& data.files.length > 0
|
|
||||||
&& data.files.every(file => file.file_type === 'image');
|
|
||||||
|
|
||||||
|
// Render directories.
|
||||||
let contentHTML = '';
|
let contentHTML = '';
|
||||||
|
|
||||||
if (isImageOnly) {
|
|
||||||
// Display thumbnails grid
|
|
||||||
contentHTML += '<div class="images-grid">';
|
|
||||||
data.files.forEach(file => {
|
|
||||||
const thumbUrl = `${file.path}?thumbnail=true`;
|
|
||||||
contentHTML += `
|
|
||||||
<div class="image-item">
|
|
||||||
|
|
||||||
<a href="#" class="play-file image-link"
|
|
||||||
data-url="${file.path}"
|
|
||||||
data-file-type="${file.file_type}">
|
|
||||||
<img
|
|
||||||
src="/media/${thumbUrl}"
|
|
||||||
class="thumbnail"
|
|
||||||
onload="this.closest('.image-item').classList.add('loaded')"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<span class="file-name">${file.name}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
contentHTML += '</div>';
|
|
||||||
} else {
|
|
||||||
// Render directories normally
|
|
||||||
if (data.directories.length > 0) {
|
if (data.directories.length > 0) {
|
||||||
|
contentHTML += '<ul>';
|
||||||
|
// Check if every directory name is short (≤15 characters) and no files are present
|
||||||
const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0;
|
const areAllShort = data.directories.every(dir => dir.name.length <= 15) && data.files.length === 0;
|
||||||
if (areAllShort && data.breadcrumbs.length !== 1) {
|
if (areAllShort & data.breadcrumbs.length != 1) {
|
||||||
contentHTML += '<div class="directories-grid">';
|
contentHTML += '<div class="directories-grid">';
|
||||||
data.directories.forEach(dir => {
|
data.directories.forEach(dir => {
|
||||||
contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`;
|
contentHTML += `<div class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></div>`;
|
||||||
@ -88,15 +63,15 @@ function renderContent(data) {
|
|||||||
data.directories.forEach(dir => {
|
data.directories.forEach(dir => {
|
||||||
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`;
|
contentHTML += `<li class="directory-item">📁 <a href="#" class="directory-link" data-path="${dir.path}">${dir.name}</a></li>`;
|
||||||
});
|
});
|
||||||
if (data.breadcrumbs.length === 1) {
|
if (data.breadcrumbs.length == 1) {
|
||||||
contentHTML += `<li class="link-item" onclick="window.location.href='/search'">🔎 <a href="/search" class="link-link">Suche</a></li>`;
|
contentHTML += `<li class="link-item" onclick="window.location.href='/search'">🔎 <a href="/search" class="link-link">Suche</a></li>`;
|
||||||
}
|
}
|
||||||
contentHTML += '</ul>';
|
contentHTML += '</ul>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render files (including music and non-image files)
|
// Render files.
|
||||||
currentMusicFiles = [];
|
currentMusicFiles = []; // Reset the music files array.
|
||||||
if (data.files.length > 0) {
|
if (data.files.length > 0) {
|
||||||
contentHTML += '<ul>';
|
contentHTML += '<ul>';
|
||||||
data.files.forEach((file, idx) => {
|
data.files.forEach((file, idx) => {
|
||||||
@ -117,48 +92,55 @@ function renderContent(data) {
|
|||||||
});
|
});
|
||||||
contentHTML += '</ul>';
|
contentHTML += '</ul>';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Insert generated content
|
// Insert the generated HTML into a container in the DOM.
|
||||||
document.getElementById('content').innerHTML = contentHTML;
|
document.getElementById('content').innerHTML = contentHTML;
|
||||||
|
|
||||||
// Attach event listeners for directories
|
// Now attach the event listeners for directories.
|
||||||
document.querySelectorAll('.directory-item').forEach(item => {
|
document.querySelectorAll('div.directory-item').forEach(li => {
|
||||||
item.addEventListener('click', function(e) {
|
li.addEventListener('click', function(e) {
|
||||||
|
// Only trigger if the click did not start on an <a>.
|
||||||
if (!e.target.closest('a')) {
|
if (!e.target.closest('a')) {
|
||||||
const anchor = item.querySelector('a.directory-link');
|
const anchor = li.querySelector('a.directory-link');
|
||||||
if (anchor) anchor.click();
|
if (anchor) {
|
||||||
|
anchor.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach event listeners for file items (including images)
|
// Now attach the event listeners for directories.
|
||||||
if (isImageOnly) {
|
document.querySelectorAll('li.directory-item').forEach(li => {
|
||||||
document.querySelectorAll('.image-item').forEach(item => {
|
li.addEventListener('click', function(e) {
|
||||||
item.addEventListener('click', function(e) {
|
// Only trigger if the click did not start on an <a>.
|
||||||
if (!e.target.closest('a')) {
|
if (!e.target.closest('a')) {
|
||||||
const anchor = item.querySelector('a.image-link');
|
const anchor = li.querySelector('a.directory-link');
|
||||||
if (anchor) anchor.click();
|
if (anchor) {
|
||||||
|
anchor.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
document.querySelectorAll('li.file-item').forEach(item => {
|
|
||||||
item.addEventListener('click', function(e) {
|
|
||||||
if (!e.target.closest('a')) {
|
|
||||||
const anchor = item.querySelector('a.play-file');
|
|
||||||
if (anchor) anchor.click();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update gallery images for lightbox or similar
|
// Now attach the event listeners for files.
|
||||||
|
document.querySelectorAll('li.file-item').forEach(li => {
|
||||||
|
li.addEventListener('click', function(e) {
|
||||||
|
// Only trigger if the click did not start on an <a>.
|
||||||
|
if (!e.target.closest('a')) {
|
||||||
|
const anchor = li.querySelector('a.play-file');
|
||||||
|
if (anchor) {
|
||||||
|
anchor.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update global variable for gallery images (only image files).
|
||||||
currentGalleryImages = data.files
|
currentGalleryImages = data.files
|
||||||
.filter(f => f.file_type === 'image')
|
.filter(f => f.file_type === 'image')
|
||||||
.map(f => f.path);
|
.map(f => f.path);
|
||||||
|
|
||||||
attachEventListeners();
|
attachEventListeners(); // Reattach event listeners for newly rendered elements.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch directory data from the API.
|
// Fetch directory data from the API.
|
||||||
|
|||||||
@ -35,7 +35,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<a href="/">
|
<a href="#">
|
||||||
<img src="/custom_logo/logoW.png" alt="Logo" class="logo">
|
<img src="/custom_logo/logoW.png" alt="Logo" class="logo">
|
||||||
</a>
|
</a>
|
||||||
<h1>{{ title_long }}</h1>
|
<h1>{{ title_long }}</h1>
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import whisper
|
|||||||
import concurrent.futures
|
import concurrent.futures
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import yaml
|
|
||||||
|
|
||||||
# model_name = "large-v3"
|
# model_name = "large-v3"
|
||||||
model_name = "medium"
|
model_name = "medium"
|
||||||
@ -14,9 +13,17 @@ model_name = "medium"
|
|||||||
start_time = 0
|
start_time = 0
|
||||||
total_audio_length = 0
|
total_audio_length = 0
|
||||||
|
|
||||||
with open("transcription_folder.yml", "r", encoding="utf-8") as file:
|
folder_list = [
|
||||||
settings = yaml.safe_load(file)
|
# Speyer
|
||||||
folder_list = settings.get("folder_list", [])
|
# "\\\\10.1.0.11\\Aufnahme-stereo\\010 Gottesdienste ARCHIV\\2025",
|
||||||
|
# "\\\\10.1.0.11\\Aufnahme-stereo\\010 Gottesdienste ARCHIV\\2016",
|
||||||
|
# "\\\\10.1.0.11\\Aufnahme-stereo\\010 Gottesdienste ARCHIV\\2015",
|
||||||
|
# "\\\\10.1.0.11\\Aufnahme-stereo\\010 Gottesdienste ARCHIV\\2014",
|
||||||
|
|
||||||
|
# Schwegenheim
|
||||||
|
"\\\\10.1.1.11\\Aufnahme-stereo\\010 Gottesdienste ARCHIV\\2025",
|
||||||
|
"\\\\10.1.1.11\\Aufnahme-stereo\\010 Gottesdienste ARCHIV\\2024"
|
||||||
|
]
|
||||||
|
|
||||||
def format_timestamp(seconds):
|
def format_timestamp(seconds):
|
||||||
"""Format seconds into HH:MM:SS."""
|
"""Format seconds into HH:MM:SS."""
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user