gui improvements

This commit is contained in:
lelo 2026-01-26 19:02:43 +00:00
parent 6cc1fb8d56
commit 5a3ad7ef6e
7 changed files with 98 additions and 32 deletions

View File

@ -4,6 +4,7 @@ import sqlite3
from datetime import datetime from datetime import datetime
from time import monotonic from time import monotonic
import re import re
from typing import Optional
import helperfunctions as hf import helperfunctions as hf
SEARCH_DB_NAME = 'search.db' SEARCH_DB_NAME = 'search.db'
@ -139,6 +140,20 @@ def build_transcript_index(transcript_dir: str, stats: dict):
return index return index
except FileNotFoundError: except FileNotFoundError:
return None return None
def read_text_file(path: str) -> Optional[str]:
try:
with open(path, 'r', encoding='utf-8') as handle:
return handle.read()
except UnicodeDecodeError:
try:
with open(path, 'r', encoding='cp1252') as handle:
return handle.read()
except Exception:
return None
except Exception:
return None
except PermissionError: except PermissionError:
log_permission_error(transcript_dir, stats) log_permission_error(transcript_dir, stats)
return None return None
@ -264,6 +279,14 @@ def updatefileindex():
except Exception: except Exception:
transcript_errors += 1 transcript_errors += 1
if transcript is None and filetype == '.sng':
sng_text = read_text_file(entry_path)
if sng_text is not None:
transcript = sng_text
transcripts_read += 1
else:
transcript_errors += 1
category, titel, name = extract_structure(entry.name) category, titel, name = extract_structure(entry.name)
performance_date = extract_date(relative_path) performance_date = extract_date(relative_path)

View File

@ -3,6 +3,7 @@ from flask import Flask, render_template, request, request, jsonify, session
import random import random
import json import json
from datetime import datetime from datetime import datetime
from urllib.parse import quote
app = Flask(__name__) app = Flask(__name__)
@ -122,6 +123,8 @@ def searchcommand():
transcript.lower().count(w.lower()) for w in words transcript.lower().count(w.lower()) for w in words
) )
record.pop('transcript', None) record.pop('transcript', None)
record['fulltext_url'] = f"/media/{quote(record.get('relative_path', ''), safe='/')}"
record['fulltext_type'] = 'sng' if (record.get('filetype') or '').lower() == '.sng' else 'transcript'
# convert date to TT.MM.YYYY format # convert date to TT.MM.YYYY format
if record.get('performance_date'): if record.get('performance_date'):
try: try:

View File

@ -268,24 +268,40 @@ class SimpleAudioPlayer {
} }
async fileDownload() { async fileDownload() {
let relUrl = this.currentRelUrl || '';
const src = this.audio.currentSrc || this.audio.src; const src = this.audio.currentSrc || this.audio.src;
if (!src) return;
// Extract the subpath from the src if (!relUrl && src) {
const urlObj = new URL(src, window.location.href); try {
let subpath = urlObj.pathname; const urlObj = new URL(src, window.location.href);
subpath = subpath.slice('/media'.length); if (urlObj.pathname.startsWith('/media/')) {
relUrl = urlObj.pathname.slice('/media/'.length);
}
} catch {
// ignore parsing errors
}
}
if (!relUrl) return;
const encodedSubpath = relUrl
.replace(/^\/+/, '')
.split('/')
.map(segment => encodeURIComponent(decodeURIComponent(segment)))
.join('/');
// Fetch the tokenized URL from your backend // Fetch the tokenized URL from your backend
let tokenizedUrl; let tokenizedUrl;
try { try {
const resp = await fetch(`/create_dltoken${subpath}`); const resp = await fetch(`/create_dltoken/${encodedSubpath}`);
if (!resp.ok) return; if (!resp.ok) return;
tokenizedUrl = await resp.text(); tokenizedUrl = (await resp.text()).trim();
} catch { } catch {
return; return;
} }
if (!tokenizedUrl) return;
// Build the URL with cache-buster // Build the URL with cache-buster
const downloadUrl = new URL(tokenizedUrl, window.location.href); const downloadUrl = new URL(tokenizedUrl, window.location.href);

View File

@ -5,6 +5,7 @@
let animateInterval = null; let animateInterval = null;
let statsInterval = null; let statsInterval = null;
let refreshMapSize = null; let refreshMapSize = null;
const TABLE_LIMIT = 20;
function initConnectionsPage() { function initConnectionsPage() {
const mapEl = document.getElementById('map'); const mapEl = document.getElementById('map');
@ -190,6 +191,8 @@
}); });
function updateStats(data) { function updateStats(data) {
const headerCount = document.getElementById('connectionsTotalHeader');
if (headerCount) headerCount.textContent = `${data.length} Verbindungen`;
const totalConnections = document.getElementById('totalConnections'); const totalConnections = document.getElementById('totalConnections');
if (totalConnections) totalConnections.textContent = data.length; if (totalConnections) totalConnections.textContent = data.length;
const totalBytes = data.reduce((sum, rec) => { const totalBytes = data.reduce((sum, rec) => {
@ -276,7 +279,8 @@
socket.on('recent_connections', data => { socket.on('recent_connections', data => {
updateStats(data); updateStats(data);
animateTableWithNewRow(data); const tableData = data.slice(0, TABLE_LIMIT);
animateTableWithNewRow(tableData);
}); });
} }

View File

@ -23,6 +23,12 @@ function initSearch() {
const isAudio = audioExts.includes((file.filetype || '').toLowerCase()); const isAudio = audioExts.includes((file.filetype || '').toLowerCase());
const isSng = (file.filetype || '').toLowerCase() === '.sng' || (file.filename || '').toLowerCase().endsWith('.sng'); const isSng = (file.filetype || '').toLowerCase() === '.sng' || (file.filename || '').toLowerCase().endsWith('.sng');
const encodedRelPath = encodeURI(file.relative_path); const encodedRelPath = encodeURI(file.relative_path);
const fulltextType = file.fulltext_type || 'transcript';
const fulltextUrl = file.fulltext_url || transcriptURL;
const fulltextLabel = 'Treffer im Volltext';
const fulltextAction = fulltextType === 'sng'
? `<a href="#" class="open-sng-link" data-path="${file.relative_path}" title="Liedtext öffnen"><i class="bi bi-journal-text"></i></a>`
: `<a href="#" class="show-transcript" data-url="${fulltextUrl}" data-audio-url="${file.relative_path}" highlight="${file.query}"><i class="bi bi-journal-text"></i></a>`;
card.className = 'card'; card.className = 'card';
let fileAction = ''; let fileAction = '';
@ -39,8 +45,8 @@ function initSearch() {
<p>${fileAction}</p> <p>${fileAction}</p>
<p><button class="btn btn-light btn-sm folder-open-btn" data-folder="${parentFolder}" data-file="${file.relative_path}" style="width:100%;"><i class="bi bi-folder"></i> ${parentFolder || 'Ordner'}</button></p> <p><button class="btn btn-light btn-sm folder-open-btn" data-folder="${parentFolder}" data-file="${file.relative_path}" style="width:100%;"><i class="bi bi-folder"></i> ${parentFolder || 'Ordner'}</button></p>
<p class="card-text">Anzahl Downloads: ${file.hitcount}</p> <p class="card-text">Anzahl Downloads: ${file.hitcount}</p>
${ file.performance_date !== undefined ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``} ${ (file.performance_date !== undefined && file.performance_date !== null && String(file.performance_date).trim() !== '') ? `<p class="card-text">Datum: ${file.performance_date}</p>` : ``}
${ file.transcript_hits !== undefined ? `<p class="card-text">Treffer im Transkript: ${file.transcript_hits} <a href="#" class="show-transcript" data-url="${transcriptURL}" data-audio-url="${file.relative_path}" highlight="${file.query}"><i class="bi bi-journal-text"></i></a></p>` : ``} ${ file.transcript_hits !== undefined ? `<p class="card-text">${fulltextLabel}: ${file.transcript_hits} ${fulltextAction}</p>` : ``}
</div> </div>
`; `;
resultsDiv.appendChild(card); resultsDiv.appendChild(card);
@ -54,6 +60,7 @@ function initSearch() {
attachEventListeners(); attachEventListeners();
attachSearchFolderButtons(); attachSearchFolderButtons();
attachSearchSngButtons(); attachSearchSngButtons();
attachSearchFulltextButtons();
} else { } else {
resultsDiv.innerHTML = `<h5>${total} Treffer</h5><p>Keine Treffer gefunden.</p>`; resultsDiv.innerHTML = `<h5>${total} Treffer</h5><p>Keine Treffer gefunden.</p>`;
} }
@ -248,6 +255,18 @@ function attachSearchSngButtons() {
}); });
} }
function attachSearchFulltextButtons() {
document.querySelectorAll('.open-sng-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const path = link.dataset.path;
if (typeof window.openSngModal === 'function') {
window.openSngModal(path);
}
});
});
}
function openFolderAndHighlight(folderPath, filePath) { function openFolderAndHighlight(folderPath, filePath) {
const targetFolder = folderPath || ''; const targetFolder = folderPath || '';
// Switch back to main view before loading folder // Switch back to main view before loading folder

View File

@ -63,6 +63,11 @@
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<!-- Transkript durchsuchen -->
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript">
<label class="form-check-label" for="includeTranscript">Im Volltext suchen</label>
</div>
</div> </div>
<!-- Toggle für Suchoptionen --> <!-- Toggle für Suchoptionen -->
@ -81,6 +86,20 @@
<!-- Suchoptionen einklappbar --> <!-- Suchoptionen einklappbar -->
<div id="searchOptions" class="collapse border rounded p-3 mb-3"> <div id="searchOptions" class="collapse border rounded p-3 mb-3">
<!-- In Ordner suchen -->
<div class="mb-3">
<label for="folder" class="form-label">In Ordner suchen:</label>
<select id="folder" name="folder" class="form-select">
<option value="">Alle</option>
{% for folder in search_folders %}
<option value="{{ folder }}">{{ folder }}</option>
{% endfor %}
</select>
</div>
<hr>
<!-- Kategorie --> <!-- Kategorie -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Kategorie:</label> <label class="form-label">Kategorie:</label>
@ -114,19 +133,6 @@
<hr> <hr>
<!-- In Ordner suchen -->
<div class="mb-3">
<label for="folder" class="form-label">In Ordner suchen:</label>
<select id="folder" name="folder" class="form-select">
<option value="">Alle</option>
{% for folder in search_folders %}
<option value="{{ folder }}">{{ folder }}</option>
{% endfor %}
</select>
</div>
<hr>
<!-- Dateityp --> <!-- Dateityp -->
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Dateityp:</label> <label class="form-label">Dateityp:</label>
@ -157,14 +163,6 @@
<hr> <hr>
<!-- Transkript durchsuchen -->
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript">
<label class="form-check-label" for="includeTranscript">Im Transkript suchen</label>
</div>
<hr>
<!-- Zeitraum --> <!-- Zeitraum -->
<div class="row g-2 mb-3"> <div class="row g-2 mb-3">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">

View File

@ -103,7 +103,10 @@
{% block content %} {% block content %}
<div class="page-content"> <div class="page-content">
<div class="section-header"> <div class="section-header">
<h2 style="margin: 0;">Verbindungen der letzten 10 Minuten (nur Audio)</h2> <h2 style="margin: 0;">
Verbindungen der letzten 10 Minuten (nur Audio)
<span class="text-muted" id="connectionsTotalHeader" style="font-size: 14px; margin-left: 8px;">0 Verbindungen</span>
</h2>
<p class="text-muted" style="margin: 4px 0 0 0;">Diese Ansicht listet ausschließlich Zugriffe auf Audio-Dateien.</p> <p class="text-muted" style="margin: 4px 0 0 0;">Diese Ansicht listet ausschließlich Zugriffe auf Audio-Dateien.</p>
<div class="stats"> <div class="stats">
<div class="stat-item"> <div class="stat-item">