Merge remote-tracking branch 'origin/development'

This commit is contained in:
lelo 2025-05-07 20:26:51 +00:00
commit 39e868daad
4 changed files with 235 additions and 160 deletions

View File

@ -39,41 +39,12 @@ def init_db():
''')
search_db.commit()
# If the table already existed, try to add the new columns.
try:
cursor.execute("ALTER TABLE files ADD COLUMN hitcount INTEGER DEFAULT 0")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
try:
cursor.execute("ALTER TABLE files ADD COLUMN basefolder TEXT")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
try:
cursor.execute("ALTER TABLE files ADD COLUMN category TEXT")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
try:
cursor.execute("ALTER TABLE files ADD COLUMN titel TEXT")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
try:
cursor.execute("ALTER TABLE files ADD COLUMN name TEXT")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
try:
cursor.execute("ALTER TABLE files ADD COLUMN performance_date TEXT")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
try:
cursor.execute("ALTER TABLE files ADD COLUMN site TEXT")
except sqlite3.OperationalError:
# Likely the column already exists, so we ignore this error.
pass
# try:
# cursor.execute("ALTER TABLE files ADD COLUMN category TEXT")
# except sqlite3.OperationalError:
# # Likely the column already exists, so we ignore this error.
# pass
search_db.commit()
def scan_dir(directory):
@ -200,16 +171,15 @@ def updatefileindex():
date_match = re.search(r'(\d{1,2}\.\d{1,2}\.\d{2,4})', relative_path)
if date_match:
date_str = date_match.group(1)
# Convert to YYYY-MM-DD format
try:
date_obj = datetime.strptime(date_str, '%d.%m.%Y')
performance_date = date_obj.strftime('%d.%m.%Y')
except ValueError:
performance_date = None
for fmt in ('%d.%m.%Y', '%d.%m.%y', '%Y-%m-%d'):
try:
date_obj = datetime.strptime(date_str, '%d.%m.%y')
performance_date = date_obj.strftime('%d.%m.%Y')
date_obj = datetime.strptime(date_str, fmt)
# Convert to ISO format YYYY-MM-DD
performance_date = date_obj.strftime('%Y-%m-%d')
break
except ValueError:
performance_date = None
continue
else:
performance_date = None
@ -236,7 +206,52 @@ def updatefileindex():
return "File index updated successfully"
def convert_dates(search_db,
date_formats=('%d.%m.%Y', '%d.%m.%y')):
"""
Connects to the SQLite database at search_db, and for every row in table 'files':
- Reads the date from performance_date (expects 'dd.mm.yyyy' or 'dd.mm.yy').
- Parses it and reformats to ISO 'YYYY-MM-DD'.
- Updates the row (using id as primary key).
Only counts rows where the conversion was successful.
"""
# Regex to quickly filter out non-matching strings
date_regex = re.compile(r'^\d{1,2}\.\d{1,2}\.\d{2,4}$')
cur = search_db.cursor()
# Fetch all rows with a non-null date
cur.execute("SELECT id, performance_date FROM files")
rows = cur.fetchall()
converted_count = 0
for pk, raw_date in rows:
if not raw_date or not date_regex.match(raw_date):
continue
for fmt in date_formats:
try:
dt = datetime.strptime(raw_date, fmt)
new_date = dt.strftime('%Y-%m-%d')
# Only update if the reformatted date is different
if new_date != raw_date:
cur.execute(
"UPDATE files SET performance_date = ? WHERE id = ?",
(new_date, pk)
)
converted_count += 1
break # stop trying other formats
except ValueError:
continue
search_db.commit()
search_db.close()
print(f"Converted {converted_count} rows to ISO format.")
if __name__ == "__main__":
convert_dates(search_db)
init_db() # Initialize the database schema if it doesn't exist
updatefileindex() # Update the file index
search_db.close() # Close the search database connection

136
search.py
View File

@ -17,87 +17,91 @@ def searchcommand():
query = request.form.get("query", "").strip()
category = request.form.get("category", "").strip()
searchfolder = request.form.get("folder", "").strip()
datefrom = request.form.get("datefrom", "").strip()
dateto = request.form.get("dateto", "").strip()
include_transcript = request.form.get("includeTranscript") in ["true", "on"]
words = [w for w in query.split() if w]
cursor = search_db.cursor()
allowed_basefolders = list(session['folders'].keys())
# if the search folder is allowed to be searched, select it
# if not just allowed_basefolders rules apply
if searchfolder != "" and searchfolder in allowed_basefolders:
# Determine allowed basefolders
allowed_basefolders = list(session['folders'].keys())
if searchfolder and searchfolder in allowed_basefolders:
allowed_basefolders = [searchfolder]
if not include_transcript:
conditions = []
params = []
# Apply query words to relative_path and filename
for word in words:
conditions.append("(relative_path LIKE ? OR filename LIKE ?)")
params.extend([f"%{word}%", f"%{word}%"])
# Search category in filename
if category:
conditions.append("(filename LIKE ?)")
params.extend([f"%{category}%"])
# Only include rows where basefolder is in allowed_basefolders
if allowed_basefolders:
placeholders = ",".join("?" for _ in allowed_basefolders)
conditions.append(f"basefolder IN ({placeholders})")
params.extend(allowed_basefolders)
sql = "SELECT * FROM files"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
cursor.execute(sql, params)
raw_results = cursor.fetchall()
results = [dict(row) for row in raw_results]
# Randomize the list before sorting to break ties randomly.
random.shuffle(results)
results.sort(key=lambda x: x["hitcount"], reverse=True)
# Build conditions and parameters
conditions = []
params = []
# Choose fields for word search
if include_transcript:
fields = ['filename', 'transcript']
else:
# Advanced search: include transcript. Count transcript hits.
conditions = []
params = []
# Apply query words for filename and transcript
for word in words:
conditions.append("(filename LIKE ? OR transcript LIKE ?)")
params.extend([f"%{word}%", f"%{word}%"])
# Search category in filename
if category:
conditions.append("(filename LIKE ?)")
params.extend([f"%{category}%"])
# Only include rows where basefolder is in allowed_basefolders
if allowed_basefolders:
placeholders = ",".join("?" for _ in allowed_basefolders)
conditions.append(f"basefolder IN ({placeholders})")
params.extend(allowed_basefolders)
fields = ['relative_path', 'filename']
sql = "SELECT * FROM files"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
cursor.execute(sql, params)
raw_results = cursor.fetchall()
for word in words:
field_clauses = [f"{f} LIKE ?" for f in fields]
conditions.append(f"({ ' OR '.join(field_clauses) })")
for _ in fields:
params.append(f"%{word}%")
results = []
for row in raw_results:
result = dict(row)
transcript = result.get("transcript") or ""
total_hits = sum(transcript.lower().count(word.lower()) for word in words)
result["transcript_hits"] = total_hits
result.pop("transcript", None)
results.append(result)
# Randomize the list before sorting to break ties randomly.
random.shuffle(results)
results.sort(key=lambda x: x["transcript_hits"], reverse=True)
# Category filter
if category:
conditions.append("filename LIKE ?")
params.append(f"%{category}%")
# Basefolder filter
if allowed_basefolders:
placeholders = ",".join("?" for _ in allowed_basefolders)
conditions.append(f"basefolder IN ({placeholders})")
params.extend(allowed_basefolders)
# Date range filters
if datefrom:
try:
conditions.append("performance_date >= ?")
params.append(datefrom)
except ValueError:
pass
if dateto:
try:
conditions.append("performance_date <= ?")
params.append(dateto)
except ValueError:
pass
# Ensure we only include entries with dates when filtering by date
if datefrom or dateto:
conditions.append("performance_date IS NOT NULL")
# Build and execute SQL
sql = "SELECT * FROM files"
if conditions:
sql += " WHERE " + " AND ".join(conditions)
cursor.execute(sql, params)
raw_results = cursor.fetchall()
# Process results
results = []
for row in raw_results:
record = dict(row)
if include_transcript:
transcript = record.get('transcript', '') or ''
record['transcript_hits'] = sum(
transcript.lower().count(w.lower()) for w in words
)
record.pop('transcript', None)
results.append(record)
# Randomize and sort
random.shuffle(results)
key = 'transcript_hits' if include_transcript else 'hitcount'
results.sort(key=lambda x: x.get(key, 0), reverse=True)
# Limit results
results = results[:100]
return jsonify(results=results)
def search():
allowed_basefolders = list(session['folders'].keys())
title_short = app_config.get('TITLE_SHORT', 'Default Title')

View File

@ -7,6 +7,7 @@
<title>{% block title %}Meine Links{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='app.css') }}">

View File

@ -28,6 +28,7 @@
<!-- Bootstrap CSS for modern styling -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css" rel="stylesheet">
<!-- Your CSS -->
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
@ -50,70 +51,116 @@
</a>
<h1>{{ title_long }}</h1>
</header>
<div class="search-container">
<h1>Suche</h1>
<form id="searchForm" method="post" class="mb-4">
<!-- Suchwörter -->
<div class="mb-3">
<label for="query" class="h5 form-label">Suchwörter:</label>
<input type="text" id="query" name="query" class="form-control" required>
</div>
<!-- Toggle für Suchoptionen -->
<div class="d-flex align-items-center mb-2">
<h2 class="h5 mb-0">Suchoptionen</h2>
<button class="btn btn-sm btn-link p-0"
type="button"
data-bs-toggle="collapse"
data-bs-target="#searchOptions"
aria-expanded="false"
aria-controls="searchOptions"
aria-label="Toggle Suchoptionen">
<i class="bi bi-plus-lg"></i>
</button>
</div>
<!-- Suchoptionen einklappbar -->
<div id="searchOptions" class="collapse border rounded p-3 mb-3">
<!-- Kategorie -->
<div class="mb-3">
<label for="query" class="form-label">Suchwörter:</label>
<input type="text" id="query" name="query" class="form-control" required>
</div>
<!-- Radio Button for Kategorie -->
<div class="mb-3">
<label class="form-label">Kategorie:</label>
<div>
<div class="form-check form-check-inline">
<!-- "Alles" is default if nothing is selected -->
<input class="form-check-input" type="radio" name="category" id="none" value="" checked>
<label class="form-check-label" for="none">Alles</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="lied" value="Lied">
<label class="form-check-label" for="lied">Lied</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="gedicht" value="Gedicht">
<label class="form-check-label" for="gedicht">Gedicht</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="predigt" value="Predigt">
<label class="form-check-label" for="predigt">Predigt</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="chor" value="chor">
<label class="form-check-label" for="chor">Chor</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="orchester" value="orchester">
<label class="form-check-label" for="orchester">Orchester</label>
</div>
<label class="form-label">Kategorie:</label>
<div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="none" value="" checked>
<label class="form-check-label" for="none">Alles</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="lied" value="Lied">
<label class="form-check-label" for="lied">Lied</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="gedicht" value="Gedicht">
<label class="form-check-label" for="gedicht">Gedicht</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="predigt" value="Predigt">
<label class="form-check-label" for="predigt">Predigt</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="chor" value="chor">
<label class="form-check-label" for="chor">Chor</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="category" id="orchester" value="orchester">
<label class="form-check-label" for="orchester">Orchester</label>
</div>
</div>
</div>
<!-- dropdown search folder-->
<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>
<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>
<!-- Checkbox for transcript search -->
<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>
<input type="checkbox" class="form-check-input" id="includeTranscript" name="includeTranscript">
<label class="form-check-label" for="includeTranscript">Im Transkript suchen</label>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Suchen</button>
<!-- Clear Button -->
<button type="button" id="clearBtn" class="btn btn-secondary ms-2">neue Suche</button>
<!-- Back Button -->
<button type="button" id="backBtn" class="btn btn-secondary ms-2">zurück</button>
<hr>
<!-- Zeitraum -->
<div class="row g-2 mb-3">
<div class="col-12 col-md-6">
<label for="datefrom" class="form-label">Datum von:</label>
<input type="date" id="datefrom" name="datefrom" class="form-control">
</div>
<div class="col-12 col-md-6">
<label for="dateto" class="form-label">Datum bis:</label>
<input type="date" id="dateto" name="dateto" class="form-control">
</div>
</div>
</div>
<!-- Ende Suchoptionen -->
<!-- Buttons -->
<div class="mb-3">
<button type="submit" class="btn btn-primary">Suchen</button>
<button type="button" id="clearBtn" class="btn btn-secondary ms-2">zurücksetzen</button>
<button type="button" id="backBtn" class="btn btn-secondary ms-2">beenden</button>
</div>
</form>
<!-- Container for AJAX-loaded results -->
<!-- AJAX-loaded results -->
<div id="results"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
@ -209,6 +256,8 @@ document.addEventListener('DOMContentLoaded', function() {
formData.append('query', query);
formData.append('category', category);
formData.append('folder', document.getElementById('folder').value);
formData.append('datefrom', document.getElementById('datefrom').value);
formData.append('dateto', document.getElementById('dateto').value);
formData.append('includeTranscript', includeTranscript);
fetch('/searchcommand', {
@ -241,12 +290,18 @@ document.addEventListener('DOMContentLoaded', function() {
localStorage.removeItem("searchResponse");
localStorage.removeItem("searchQuery");
localStorage.removeItem("searchCategory");
localStorage.removeItem("folder");
localStorage.removeItem("datefrom");
localStorage.removeItem("dateto");
localStorage.removeItem("searchIncludeTranscript");
// Reset form fields to defaults
document.getElementById('query').value = '';
document.querySelector('input[name="category"][value=""]').checked = true;
const otherRadios = document.querySelectorAll('input[name="category"]:not([value=""])');
otherRadios.forEach(radio => radio.checked = false);
document.getElementById('folder').value = ''; // Reset to "Alle"
document.getElementById('datefrom').value = ''; // Reset date from
document.getElementById('dateto').value = ''; // Reset date to
document.getElementById('includeTranscript').checked = false;
// Clear the results div
document.getElementById('results').innerHTML = '';