285 lines
9.0 KiB
Python
285 lines
9.0 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Projekcija - Lyrics Projector (Web Server)
|
|
#
|
|
# Izvorna zasnova in implementacija: Uroš Urbanija
|
|
# Nadgradnje in vzdrževanje: Valentin Korenjak
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
"""Flask web server for the song projector application.
|
|
|
|
HTML, CSS and JavaScript are now stored in separate template/static files rather
|
|
than being generated inside Python code.
|
|
"""
|
|
|
|
import threading
|
|
import os
|
|
|
|
from flask import Flask, render_template, request, jsonify
|
|
|
|
# create Flask app with proper folders relative to this file
|
|
app = Flask(__name__, static_folder="static", template_folder="templates")
|
|
|
|
# Globalna referenca na SongProjector aplikaciju
|
|
_projector_app = None
|
|
|
|
|
|
def set_projector_app(projector_app):
|
|
"""Postavi referenco na SongProjector aplikacijo"""
|
|
global _projector_app
|
|
_projector_app = projector_app
|
|
|
|
|
|
# ----------------------------------------------------------
|
|
# Flask rute
|
|
# ----------------------------------------------------------
|
|
|
|
@app.route("/", methods=["GET"])
|
|
def index():
|
|
"""Prikaže glavno HTML stranko"""
|
|
# predaja nadzora Jinja2, ki poišče web/templates/index.html
|
|
return render_template("index.html")
|
|
|
|
|
|
@app.route('/api/state', methods=['GET'])
|
|
def get_state():
|
|
"""Vrati trenutno stanje aplikacije"""
|
|
if _projector_app is None:
|
|
return jsonify({
|
|
'current_text': 'Napaka: Aplikacija ni inicijalizirana',
|
|
'page_info': '',
|
|
'caps_mode': False,
|
|
'can_prev': False,
|
|
'can_next': False
|
|
})
|
|
|
|
current_text = ""
|
|
page_info = ""
|
|
can_prev = False
|
|
can_next = False
|
|
|
|
if _projector_app.pages:
|
|
current_text = _projector_app.pages[_projector_app.current_page_index]
|
|
if _projector_app.all_caps_mode:
|
|
current_text = current_text.upper()
|
|
|
|
current_page = _projector_app.current_page_index + 1
|
|
total_pages = len(_projector_app.pages)
|
|
page_info = f"{_projector_app.song_number_last} {current_page}/{total_pages}"
|
|
|
|
can_prev = _projector_app.current_page_index > 0
|
|
can_next = _projector_app.current_page_index + 1 < len(_projector_app.pages)
|
|
else:
|
|
current_text = "Pripravljeno. Vpiši številko pesmi ali drugega besedila."
|
|
|
|
return jsonify({
|
|
'current_text': current_text,
|
|
'page_info': page_info,
|
|
'caps_mode': _projector_app.all_caps_mode,
|
|
'can_prev': can_prev,
|
|
'can_next': can_next
|
|
})
|
|
|
|
|
|
@app.route('/api/load_song', methods=['POST'])
|
|
def load_song():
|
|
"""Naloži pesem po številki"""
|
|
if _projector_app is None:
|
|
return jsonify({'status': 'error', 'message': 'Aplikacija ni inicijalizirana'})
|
|
|
|
data = request.get_json()
|
|
song_number = data.get('song_number', '').strip()
|
|
|
|
if song_number:
|
|
_projector_app.song_number = song_number
|
|
_projector_app.load_song()
|
|
if hasattr(_projector_app, 'show_page'):
|
|
_projector_app.show_page()
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
@app.route('/api/next_page', methods=['POST'])
|
|
def next_page():
|
|
"""Naslednja stran"""
|
|
if _projector_app is not None:
|
|
_projector_app.next_page()
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
@app.route('/api/prev_page', methods=['POST'])
|
|
def prev_page():
|
|
"""Prejšnja stran"""
|
|
if _projector_app is not None:
|
|
_projector_app.prev_page()
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
@app.route('/api/clear_screen', methods=['POST'])
|
|
def clear_screen():
|
|
"""Zatemniti ekran"""
|
|
if _projector_app is not None:
|
|
_projector_app.clear_screen()
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
@app.route('/api/toggle_caps', methods=['POST'])
|
|
def toggle_caps():
|
|
"""Preklop med velikimi in malimi črkami"""
|
|
if _projector_app is not None:
|
|
_projector_app.all_caps_mode = not _projector_app.all_caps_mode
|
|
_projector_app.show_page()
|
|
|
|
return jsonify({'status': 'ok'})
|
|
|
|
|
|
@app.route('/api/search_songs', methods=['POST'])
|
|
def search_songs():
|
|
"""Iskanje besedil po naslovu"""
|
|
if _projector_app is None:
|
|
return jsonify({'results': []})
|
|
|
|
data = request.get_json()
|
|
query = data.get('query', '').strip()
|
|
|
|
if not query:
|
|
return jsonify({'results': []})
|
|
|
|
try:
|
|
_projector_app.cursor.execute(
|
|
"SELECT id, title FROM songs WHERE title LIKE ? COLLATE NOCASE",
|
|
(f"%{query}%",)
|
|
)
|
|
results = _projector_app.cursor.fetchall()
|
|
return jsonify({'results': results})
|
|
except Exception as e:
|
|
return jsonify({'error': str(e)})
|
|
|
|
|
|
@app.route('/api/get_song_details', methods=['GET'])
|
|
def get_song_details():
|
|
"""Vrne podrobnosti trenutno naložene pesmi ali pesmi po ID-ju"""
|
|
if _projector_app is None:
|
|
return jsonify({'status': 'error', 'message': 'Aplikacija ni inicijalizirana'})
|
|
|
|
song_id_param = request.args.get('id')
|
|
if song_id_param:
|
|
try:
|
|
song_id = int(song_id_param)
|
|
except ValueError:
|
|
return jsonify({'status': 'error', 'message': 'Neveljaven ID pesmi'})
|
|
elif _projector_app.song_number_last:
|
|
try:
|
|
song_id = int(_projector_app.song_number_last)
|
|
except ValueError:
|
|
return jsonify({'status': 'error', 'message': 'Neveljavna številka naložene pesmi'})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': 'Nobena pesem ni naložena'})
|
|
|
|
try:
|
|
_projector_app.cursor.execute("SELECT id, title, lyrics FROM songs WHERE id=?", (song_id,))
|
|
result = _projector_app.cursor.fetchone()
|
|
if result:
|
|
return jsonify({
|
|
'status': 'ok',
|
|
'song': {
|
|
'id': result[0],
|
|
'title': result[1],
|
|
'lyrics': result[2]
|
|
}
|
|
})
|
|
else:
|
|
return jsonify({'status': 'error', 'message': f'Pesem s številko {song_id} ni najdena v bazi'})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)})
|
|
|
|
|
|
@app.route('/api/update_song', methods=['POST'])
|
|
def update_song():
|
|
"""Posodobi naslov in besedilo pesmi ali ustvari novo"""
|
|
if _projector_app is None:
|
|
return jsonify({'status': 'error', 'message': 'Aplikacija ni inicijalizirana'})
|
|
|
|
data = request.get_json()
|
|
song_id = data.get('id')
|
|
title = data.get('title', '').strip()
|
|
lyrics = data.get('lyrics', '').strip()
|
|
|
|
if not song_id or not title or not lyrics:
|
|
return jsonify({'status': 'error', 'message': 'Manjkajoči podatki'})
|
|
|
|
try:
|
|
if song_id == 'new':
|
|
# Pridobi prvo naslednjo prosto številko
|
|
_projector_app.cursor.execute("SELECT MAX(id) FROM songs")
|
|
max_id = _projector_app.cursor.fetchone()[0]
|
|
new_id = (max_id or 0) + 1
|
|
|
|
_projector_app.cursor.execute(
|
|
"INSERT INTO songs (id, title, lyrics) VALUES (?, ?, ?)",
|
|
(new_id, title, lyrics)
|
|
)
|
|
_projector_app.conn.commit()
|
|
|
|
# Naloži novo pesem
|
|
_projector_app.song_number = str(new_id)
|
|
_projector_app.load_song()
|
|
|
|
# Pošlji ntfy obvestilo
|
|
if hasattr(_projector_app, 'send_ntfy_notification'):
|
|
_projector_app.send_ntfy_notification(new_id, title, lyrics)
|
|
|
|
return jsonify({'status': 'ok', 'new_id': new_id})
|
|
|
|
# Obstoječa pesem
|
|
_projector_app.cursor.execute("UPDATE songs SET title=?, lyrics=? WHERE id=?", (title, lyrics, song_id))
|
|
_projector_app.conn.commit()
|
|
|
|
# Pošlji ntfy obvestilo
|
|
if hasattr(_projector_app, 'send_ntfy_notification'):
|
|
_projector_app.send_ntfy_notification(song_id, title, lyrics)
|
|
|
|
# Osvežimo trenutno pesem, če je to ta, ki smo jo pravkar uredili
|
|
if str(_projector_app.song_number_last) == str(song_id):
|
|
_projector_app.song_number = str(song_id)
|
|
_projector_app.load_song()
|
|
|
|
return jsonify({'status': 'ok'})
|
|
except Exception as e:
|
|
return jsonify({'status': 'error', 'message': str(e)})
|
|
|
|
|
|
def run_server(host='127.0.0.1', port=5000):
|
|
"""Zaženi Flask server"""
|
|
app.run(host=host, port=port, debug=False, use_reloader=False, threaded=True)
|
|
|
|
|
|
def start_server_thread(projector_app, host='127.0.0.1', port=5000):
|
|
"""Zaženi server v zvjenoj niti"""
|
|
set_projector_app(projector_app)
|
|
|
|
server_thread = threading.Thread(
|
|
target=run_server,
|
|
args=(host, port),
|
|
daemon=True
|
|
)
|
|
server_thread.start()
|
|
|
|
return server_thread
|