773 lines
30 KiB
Python
Executable File
773 lines
30 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
#
|
|
# Projekcija - Lyrics Projector
|
|
#
|
|
# 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/>.
|
|
|
|
import sqlite3
|
|
import tkinter as tk
|
|
from tkinter import font
|
|
import json
|
|
import os
|
|
import math
|
|
import subprocess
|
|
import sys
|
|
import ctypes
|
|
import tkinter.messagebox as messagebox
|
|
from web.server import start_server_thread, notify_clients
|
|
import urllib.request
|
|
import tempfile
|
|
from db_schema import create_tables
|
|
|
|
DB_PATH = 'songs.db'
|
|
SETTINGS_PATH = 'settings.json'
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
|
|
class SongProjector:
|
|
def __init__(self, root):
|
|
self.root = root
|
|
# ...
|
|
try:
|
|
# Odpri read-only; ne bo ustvaril prazne baze, če datoteka manjka
|
|
# check_same_thread=False omogoča uporabo v večih nitih
|
|
self.conn = sqlite3.connect(DB_PATH, check_same_thread=False)
|
|
create_tables(self.conn)
|
|
self.cursor = self.conn.cursor()
|
|
except sqlite3.OperationalError as e:
|
|
# Jasno sporočilo in varen izhod
|
|
messagebox.showerror("Napaka baze", f"Ne morem odpreti baze '{DB_PATH}':\n{e}")
|
|
root.destroy()
|
|
sys.exit(1)
|
|
self.current_song = None
|
|
self.pages = []
|
|
self.current_page_index = 0
|
|
self.song_number = ""
|
|
self.song_number_last = ""
|
|
self.all_caps_mode = False
|
|
|
|
# --------------------------------------------------
|
|
# Nastavitve
|
|
# --------------------------------------------------
|
|
DEFAULT_SETTINGS = {
|
|
"font_name": "Noto Sans",
|
|
"bg_color": "#000000",
|
|
"fg_color": "#FFFFFF",
|
|
"font_size": 32,
|
|
"screen_width_percent": 60,
|
|
"font_bold": False,
|
|
"show_song_info": True,
|
|
"split_by_stanza": False,
|
|
"web_port": 5000,
|
|
"db_update_url": "",
|
|
"ntfy_topic": "",
|
|
"installation_label": "Projekcija"
|
|
}
|
|
|
|
if not os.path.exists(SETTINGS_PATH):
|
|
self.settings = DEFAULT_SETTINGS.copy()
|
|
self.save_settings()
|
|
else:
|
|
try:
|
|
with open(SETTINGS_PATH, "r", encoding="utf-8") as f:
|
|
self.settings = json.load(f)
|
|
except (json.JSONDecodeError, OSError):
|
|
# Povratek na varne privzete
|
|
self.settings = DEFAULT_SETTINGS.copy()
|
|
|
|
# --------------------------------------------------
|
|
# Pisava (Font)
|
|
# --------------------------------------------------
|
|
font_weight = "bold" if self.settings.get("font_bold", False) else "normal"
|
|
self.custom_font = font.Font(
|
|
family=self.settings["font_name"],
|
|
size=int(self.settings["font_size"]),
|
|
weight=font_weight
|
|
)
|
|
|
|
# Izračunamo dejansko višino vrstice glede na pisavo
|
|
# metrics("linespace") vrne priporočen razmik med vrsticami v pikslih
|
|
self.line_height = self.custom_font.metrics("linespace")
|
|
|
|
# --------------------------------------------------
|
|
# Okno
|
|
# --------------------------------------------------
|
|
root.attributes('-fullscreen', True)
|
|
root.configure(bg=self.settings["bg_color"])
|
|
root.bind("<Return>", self.enter_pressed)
|
|
root.bind("<KP_Enter>", self.enter_pressed)
|
|
root.bind("<plus>", self.clear_screen)
|
|
root.bind("<KP_Add>", self.clear_screen)
|
|
root.bind("<minus>", self.prev_page)
|
|
root.bind("<KP_Subtract>", self.prev_page)
|
|
root.bind("<slash>", self.search_song)
|
|
root.bind("<KP_Divide>", self.search_song)
|
|
root.bind("<Key>", self.key_pressed)
|
|
|
|
screen_width = root.winfo_screenwidth()
|
|
screen_height = root.winfo_screenheight()
|
|
screen_width_percent = self.settings["screen_width_percent"]
|
|
color_width = int(screen_width * screen_width_percent / 100)
|
|
|
|
self.screen_height = screen_height
|
|
self.color_width = color_width
|
|
|
|
black_side_width = (screen_width - color_width) // 2
|
|
|
|
self.left_frame = tk.Frame(root, bg="black", width=black_side_width, height=screen_height)
|
|
self.left_frame.pack(side="left", fill="y")
|
|
|
|
self.color_frame = tk.Frame(root, bg=self.settings["bg_color"], width=color_width, height=screen_height)
|
|
self.color_frame.place(relx=0.5, rely=0.5, anchor="center")
|
|
|
|
self.right_frame = tk.Frame(root, bg="black", width=black_side_width, height=screen_height)
|
|
self.right_frame.pack(side="right", fill="y")
|
|
|
|
self.display_text = tk.Label(
|
|
self.color_frame,
|
|
text="",
|
|
bg=self.settings["bg_color"],
|
|
fg=self.settings["fg_color"],
|
|
font=self.custom_font,
|
|
wraplength=color_width,
|
|
justify="center"
|
|
)
|
|
self.display_text.pack(expand=True)
|
|
|
|
right_edge_x = int((screen_width - color_width) / 2 + color_width)
|
|
|
|
self.song_info_label = tk.Label(
|
|
root,
|
|
text="",
|
|
bg=self.settings["bg_color"],
|
|
fg=self.settings["fg_color"],
|
|
font=(self.settings["font_name"], int(self.settings["font_size"]) - 5),
|
|
anchor="se",
|
|
justify="right"
|
|
)
|
|
self.song_info_label.place(x=right_edge_x - 10, y=screen_height - 10, anchor="se")
|
|
|
|
self.search_label = None
|
|
self.search_entry = None
|
|
self.waiting_for_song = True
|
|
|
|
# --------------------------------------------------
|
|
# Samodejno skrivanje kazalca
|
|
# --------------------------------------------------
|
|
def hide_cursor_after_delay():
|
|
self.root.config(cursor="none")
|
|
|
|
def reset_cursor_timer(event=None):
|
|
self.root.config(cursor="")
|
|
if hasattr(self, "cursor_job"):
|
|
self.root.after_cancel(self.cursor_job)
|
|
self.cursor_job = self.root.after(0, hide_cursor_after_delay)
|
|
|
|
self.root.bind_all('<Any-Motion>', reset_cursor_timer)
|
|
self.root.bind_all('<Key>', reset_cursor_timer)
|
|
self.cursor_job = self.root.after(0, hide_cursor_after_delay)
|
|
|
|
self.clear_screen()
|
|
|
|
# --------------------------------------------------
|
|
# Zagon Flask web serverja (če je port nastavljen)
|
|
# --------------------------------------------------
|
|
web_port = self.settings.get("web_port", 0)
|
|
if web_port and web_port > 0:
|
|
try:
|
|
start_server_thread(self, host='0.0.0.0', port=web_port)
|
|
print(f"Web server zagnan na http://0.0.0.0:{web_port}")
|
|
except Exception as e:
|
|
print(f"Napaka pri zagonu web serverja: {e}")
|
|
|
|
# --------------------------------------------------
|
|
# Preprečevanje ohranjevalnika zaslona
|
|
# --------------------------------------------------
|
|
self.keep_awake()
|
|
|
|
def keep_awake(self):
|
|
"""
|
|
Prepreči vklop ohranjevalnika zaslona ali spanja.
|
|
"""
|
|
self.inhibitor_cookie = None
|
|
try:
|
|
if sys.platform.startswith("win"):
|
|
# Windows: ES_CONTINUOUS | ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED
|
|
# 0x80000000 | 0x00000002 | 0x00000001
|
|
# Uporabimo c_uint za zagotovitev pravilnega tipa
|
|
ctypes.windll.kernel32.SetThreadExecutionState(ctypes.c_uint(0x80000003))
|
|
elif sys.platform == "linux":
|
|
# GNOME DBus inhibitor (org.gnome.SessionManager)
|
|
# Flags: 1=logout, 2=switch user, 4=suspend, 8=idle
|
|
# Uporabimo 12 (4+8) za suspend in idle.
|
|
# toplevel_xid=0 (uint32:0)
|
|
cmd = [
|
|
"dbus-send", "--print-reply", "--dest=org.gnome.SessionManager",
|
|
"/org/gnome/SessionManager", "org.gnome.SessionManager.Inhibit",
|
|
"string:projector", "uint32:0", "string:Projecting lyrics", "uint32:12"
|
|
]
|
|
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode()
|
|
# Output format: uint32 12345678
|
|
for line in output.splitlines():
|
|
if "uint32" in line:
|
|
self.inhibitor_cookie = line.split()[-1]
|
|
break
|
|
|
|
except Exception as e:
|
|
print(f"Napaka pri preprečevanju spanja: {e}")
|
|
|
|
# ------------------------------------------------------
|
|
# NOVA METODA: enakomeren prelom predolgih vrstic
|
|
# ------------------------------------------------------
|
|
def split_long_line(self, line):
|
|
"""
|
|
Vrne seznam pod-vrstic, ki skupaj tvorijo `line`,
|
|
pri čemer so dolge največ `self.color_width` pikslov.
|
|
"""
|
|
words = line.split()
|
|
if not words:
|
|
return [""]
|
|
|
|
sub_lines = []
|
|
current_words = []
|
|
|
|
for word in words:
|
|
test_line = " ".join(current_words + [word])
|
|
# Izmerimo širino v pikslih
|
|
if self.custom_font.measure(test_line) <= self.color_width:
|
|
current_words.append(word)
|
|
else:
|
|
if current_words:
|
|
sub_lines.append(" ".join(current_words))
|
|
current_words = [word]
|
|
else:
|
|
# Beseda je sama po sebi predolga (zelo redko)
|
|
sub_lines.append(word)
|
|
current_words = []
|
|
|
|
if current_words:
|
|
sub_lines.append(" ".join(current_words))
|
|
|
|
return sub_lines
|
|
|
|
# ------------------------------------------------------
|
|
# Upravljanje tipkovnice
|
|
# ------------------------------------------------------
|
|
def key_pressed(self, event):
|
|
if event.char and event.char.isdigit():
|
|
self.song_number += event.char
|
|
elif event.keysym == "BackSpace":
|
|
self.song_number = self.song_number[:-1]
|
|
elif event.char == "*":
|
|
self.toggle_split_mode()
|
|
|
|
def enter_pressed(self, event=None):
|
|
if self.song_number:
|
|
self.load_song()
|
|
elif not self.waiting_for_song:
|
|
self.next_page()
|
|
|
|
def save_settings(self):
|
|
"""Shrani trenutne nastavitve v settings.json."""
|
|
try:
|
|
with open(SETTINGS_PATH, "w", encoding="utf-8") as f:
|
|
json.dump(self.settings, f, indent=4, ensure_ascii=False)
|
|
except Exception as e:
|
|
print(f"Napaka pri shranjevanju nastavitev: {e}")
|
|
|
|
def toggle_split_mode(self):
|
|
"""Preklopi med načinom preloma po kiticah in prostim prelomom."""
|
|
self.settings["split_by_stanza"] = not self.settings.get("split_by_stanza", False)
|
|
# Shranimo v settings.json, da se ohrani ob ponovnem zagonu
|
|
# TODO: preveri z Urošem, kaj bi bilo bolje (shraniti ali ne)
|
|
# self.save_settings()
|
|
|
|
# Ponovno naložimo trenutno pesem, da se osveži prelom
|
|
if self.song_number_last:
|
|
self.song_number = self.song_number_last
|
|
self.load_song()
|
|
|
|
# ------------------------------------------------------
|
|
# Nalaganje in obdelava besedila
|
|
# ------------------------------------------------------
|
|
def load_song(self):
|
|
if not self.song_number:
|
|
return
|
|
|
|
if self.song_number == "0":
|
|
self.clear_screen()
|
|
self.song_number = ""
|
|
return
|
|
|
|
# Ponastavitev izgleda barvnega območja
|
|
self.color_frame.config(bg=self.settings["bg_color"], width=self.color_width, height=self.screen_height)
|
|
self.color_frame.place(relx=0.5, rely=0.5, anchor="center")
|
|
self.display_text.config(bg=self.settings["bg_color"], fg=self.settings["fg_color"])
|
|
self.display_text.pack(expand=True)
|
|
|
|
# Posebni ukazi
|
|
if self.song_number == "9999":
|
|
if sys.platform.startswith("win"):
|
|
subprocess.Popen(["shutdown", "/s", "/t", "0"]) # brez /f - manj agresivno
|
|
else:
|
|
subprocess.Popen(["shutdown", "-h", "now"])
|
|
return
|
|
elif self.song_number == "9998":
|
|
if sys.platform.startswith("win"):
|
|
subprocess.Popen(["shutdown", "/r", "/t", "0"])
|
|
else:
|
|
subprocess.Popen(["shutdown", "-r", "now"])
|
|
return
|
|
elif self.song_number == "9997":
|
|
self.restart_program()
|
|
return
|
|
elif self.song_number == "9988":
|
|
self.exit_program()
|
|
return
|
|
elif self.song_number == "9901":
|
|
self.update_songs_database()
|
|
return
|
|
elif self.song_number == "9900":
|
|
self.show_app_info_tkinter()
|
|
return
|
|
try:
|
|
song_id = int(self.song_number)
|
|
self.cursor.execute("SELECT lyrics FROM songs WHERE id=?", (song_id,))
|
|
result = self.cursor.fetchone()
|
|
if result:
|
|
lyrics = result[0]
|
|
|
|
# Pustimo 10% varnostnega roba zgoraj in spodaj + prostor za song_info
|
|
max_height = self.screen_height * 0.90
|
|
|
|
# 1. Razdelimo na kitice (stanzas)
|
|
# Kitice so ločene z dvojno prazno vrstico (\n\n\n ali \n\s*\n\s*\n)
|
|
import re
|
|
# Razdelimo po vsaj dveh praznih vrsticah (trije ali več \n)
|
|
stanzas_raw = re.split(r'\n\s*\n\s*\n+', lyrics.strip())
|
|
|
|
processed_stanzas = []
|
|
for stanza in stanzas_raw:
|
|
# Znotraj kitice ohranimo enojne prazne vrstice kot razmike
|
|
lines = stanza.splitlines()
|
|
stanza_lines = []
|
|
for line in lines:
|
|
if line.strip():
|
|
stanza_lines.extend(self.split_long_line(line))
|
|
else:
|
|
stanza_lines.append("") # Enojna prazna vrstica znotraj kitice
|
|
if stanza_lines:
|
|
processed_stanzas.append(stanza_lines)
|
|
|
|
# 2. Razdeljevanje kitic na strani
|
|
pages = []
|
|
current_page_lines = []
|
|
current_height = 0
|
|
split_by_stanza = self.settings.get("split_by_stanza", False)
|
|
|
|
for stanza in processed_stanzas:
|
|
stanza_height = len(stanza) * self.line_height
|
|
|
|
# Če je vklopljen split_by_stanza, vsaka kitica dobi svojo stran
|
|
if split_by_stanza:
|
|
if current_page_lines:
|
|
pages.append("\n".join(current_page_lines))
|
|
current_page_lines = []
|
|
current_height = 0
|
|
|
|
# Če je kitica predolga za eno stran, jo še vedno moramo razdeliti
|
|
if stanza_height > max_height:
|
|
temp_stanza_lines = []
|
|
temp_height = 0
|
|
for line in stanza:
|
|
if temp_height + self.line_height > max_height:
|
|
pages.append("\n".join(temp_stanza_lines))
|
|
temp_stanza_lines = [line]
|
|
temp_height = self.line_height
|
|
else:
|
|
temp_stanza_lines.append(line)
|
|
temp_height += self.line_height
|
|
if temp_stanza_lines:
|
|
pages.append("\n".join(temp_stanza_lines))
|
|
else:
|
|
pages.append("\n".join(stanza))
|
|
continue
|
|
|
|
# Standardna logika (več kitic na stran, če grejo)
|
|
if stanza_height > max_height:
|
|
if current_page_lines:
|
|
pages.append("\n".join(current_page_lines))
|
|
current_page_lines = []
|
|
current_height = 0
|
|
|
|
temp_stanza_lines = []
|
|
temp_height = 0
|
|
for line in stanza:
|
|
if temp_height + self.line_height > max_height:
|
|
pages.append("\n".join(temp_stanza_lines))
|
|
temp_stanza_lines = [line]
|
|
temp_height = self.line_height
|
|
else:
|
|
temp_stanza_lines.append(line)
|
|
temp_height += self.line_height
|
|
if temp_stanza_lines:
|
|
current_page_lines = temp_stanza_lines
|
|
current_height = temp_height
|
|
|
|
elif current_height + stanza_height + (self.line_height if current_page_lines else 0) > max_height:
|
|
pages.append("\n".join(current_page_lines))
|
|
current_page_lines = stanza
|
|
current_height = stanza_height
|
|
|
|
else:
|
|
if current_page_lines:
|
|
current_page_lines.append("") # Razmik med kiticami
|
|
current_height += self.line_height
|
|
current_page_lines.extend(stanza)
|
|
current_height += stanza_height
|
|
|
|
if current_page_lines:
|
|
pages.append("\n".join(current_page_lines))
|
|
|
|
self.pages = pages
|
|
self.current_page_index = 0
|
|
self.waiting_for_song = False
|
|
self.song_number_last = self.song_number
|
|
self.show_page()
|
|
else:
|
|
self.display_text.config(text="")
|
|
self.pages = []
|
|
self.current_page_index = 0
|
|
self.waiting_for_song = True
|
|
self.song_number_last = self.song_number
|
|
self.song_info_label.config(text=self.song_number_last)
|
|
self.song_info_label.lift()
|
|
except Exception as e:
|
|
self.display_text.config(text=f"Napaka: {e}")
|
|
finally:
|
|
self.song_number = ""
|
|
|
|
# ------------------------------------------------------
|
|
# Prikaz trenutne strani
|
|
# ------------------------------------------------------
|
|
def show_page(self):
|
|
if self.pages:
|
|
text = self.pages[self.current_page_index]
|
|
if self.all_caps_mode:
|
|
text = text.upper()
|
|
self.display_text.config(text=text)
|
|
if self.settings.get("show_song_info", False):
|
|
current_page = self.current_page_index + 1
|
|
total_pages = len(self.pages)
|
|
self.song_info_label.config(text=f"{self.song_number_last} {current_page}/{total_pages}")
|
|
self.song_info_label.lift()
|
|
else:
|
|
self.song_info_label.config(text="")
|
|
notify_clients()
|
|
|
|
# ------------------------------------------------------
|
|
# Navigacija po straneh
|
|
# ------------------------------------------------------
|
|
def next_page(self, event=None):
|
|
if self.pages and self.current_page_index + 1 < len(self.pages):
|
|
self.current_page_index += 1
|
|
self.show_page()
|
|
|
|
def prev_page(self, event=None):
|
|
if self.pages and self.current_page_index > 0:
|
|
self.current_page_index -= 1
|
|
self.show_page()
|
|
|
|
# ------------------------------------------------------
|
|
# Očisti zaslon
|
|
# ------------------------------------------------------
|
|
def clear_screen(self, event=None):
|
|
# odstranimo vse strani, da se tudi API posodobi
|
|
self.pages = []
|
|
self.current_page_index = 0
|
|
self.waiting_for_song = True
|
|
|
|
self.display_text.config(text="")
|
|
self.display_text.pack_forget()
|
|
self.color_frame.config(bg="black", width=self.color_width, height=self.screen_height)
|
|
self.color_frame.place(relx=0.5, rely=0.5, anchor="center")
|
|
self.song_info_label.config(text="")
|
|
notify_clients()
|
|
|
|
# ------------------------------------------------------
|
|
# Iskanje po naslovu
|
|
# ------------------------------------------------------
|
|
def search_song(self, event=None):
|
|
self.clear_screen()
|
|
|
|
if self.search_label:
|
|
self.search_label.destroy()
|
|
if self.search_entry:
|
|
self.search_entry.destroy()
|
|
|
|
self.search_label = tk.Label(
|
|
self.color_frame,
|
|
text="Vpiši del naslova:",
|
|
bg=self.settings["bg_color"],
|
|
fg=self.settings["fg_color"],
|
|
font=(self.settings["font_name"], int(self.settings["font_size"])),
|
|
wraplength=self.color_width,
|
|
justify="center"
|
|
)
|
|
self.search_label.pack(pady=(40, 10))
|
|
|
|
self.search_entry = tk.Entry(
|
|
self.color_frame,
|
|
font=(self.settings["font_name"], int(self.settings["font_size"])),
|
|
justify="center",
|
|
bg=self.settings["bg_color"],
|
|
fg=self.settings["fg_color"],
|
|
insertbackground=self.settings["fg_color"],
|
|
borderwidth=0,
|
|
highlightthickness=0
|
|
)
|
|
self.search_entry.pack()
|
|
self.search_entry.focus()
|
|
self.search_entry.bind("<Return>", self.perform_search)
|
|
|
|
def perform_search(self, event=None):
|
|
query = self.search_entry.get().strip()
|
|
self.search_label.destroy()
|
|
self.search_entry.destroy()
|
|
self.display_text.pack(expand=True)
|
|
|
|
if not query:
|
|
self.display_text.config(text="(Prazno iskanje)")
|
|
return
|
|
|
|
try:
|
|
self.cursor.execute(
|
|
"SELECT id, title FROM songs WHERE title LIKE ? COLLATE NOCASE",
|
|
(f"%{query}%",)
|
|
)
|
|
matched = self.cursor.fetchall()
|
|
except Exception as e:
|
|
self.display_text.config(text=f"Napaka pri iskanju: {e}")
|
|
return
|
|
|
|
if matched:
|
|
found = "\n".join(f"{sid}: {title}" for sid, title in matched)
|
|
self.display_text.config(text=found)
|
|
else:
|
|
self.display_text.config(text="Ni zadetkov.")
|
|
|
|
# ------------------------------------------------------
|
|
# Posodobitev baze pesmi
|
|
# ------------------------------------------------------
|
|
def update_songs_database(self):
|
|
url = self.settings.get("db_update_url", "").strip()
|
|
if not url:
|
|
msg = "URL za posodobitev ni nastavljen v settings.json."
|
|
print(msg)
|
|
self.display_text.config(text=msg)
|
|
self.song_number = ""
|
|
return
|
|
|
|
msg = f"Prenašam posodobitev iz: {url}..."
|
|
print(msg)
|
|
self.display_text.config(text=msg)
|
|
self.root.update()
|
|
|
|
temp_db_path = None
|
|
try:
|
|
# Prenos datoteke
|
|
with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
|
|
temp_db_path = tmp_file.name
|
|
with urllib.request.urlopen(url) as response:
|
|
tmp_file.write(response.read())
|
|
|
|
# Preverjanje integritete
|
|
print("Preverjam integriteto prenesene baze...")
|
|
check_conn = sqlite3.connect(temp_db_path)
|
|
check_cursor = check_conn.cursor()
|
|
|
|
# PRAGMA integrity_check
|
|
check_cursor.execute("PRAGMA integrity_check")
|
|
integrity_result = check_cursor.fetchone()[0]
|
|
if integrity_result != "ok":
|
|
raise Exception(f"Integriteta baze ni OK: {integrity_result}")
|
|
|
|
# Preverjanje obstoja tabele songs
|
|
check_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='songs'")
|
|
if not check_cursor.fetchone():
|
|
raise Exception("Tabela 'songs' ne obstaja v preneseni bazi.")
|
|
|
|
# Upsert v trenutno bazo
|
|
print("Izvajam upsert v lokalno bazo...")
|
|
check_cursor.execute("SELECT id, title, lyrics FROM songs")
|
|
new_songs = check_cursor.fetchall()
|
|
|
|
upsert_count = 0
|
|
for song_id, title, lyrics in new_songs:
|
|
self.cursor.execute("""
|
|
INSERT INTO songs (id, title, lyrics) VALUES (?, ?, ?)
|
|
ON CONFLICT(id) DO UPDATE SET title=excluded.title, lyrics=excluded.lyrics
|
|
""", (song_id, title, lyrics))
|
|
upsert_count += 1
|
|
|
|
self.conn.commit()
|
|
check_conn.close()
|
|
|
|
final_msg = f"Posodobitev uspešna! Posodobljenih/dodanih {upsert_count} pesmi."
|
|
print(final_msg)
|
|
self.display_text.config(text=final_msg)
|
|
|
|
except Exception as e:
|
|
error_msg = f"Napaka pri posodobitvi: {e}"
|
|
print(error_msg)
|
|
self.display_text.config(text=error_msg)
|
|
finally:
|
|
if temp_db_path and os.path.exists(temp_db_path):
|
|
try:
|
|
os.remove(temp_db_path)
|
|
except:
|
|
pass
|
|
self.song_number = ""
|
|
|
|
# ------------------------------------------------------
|
|
# Izhod
|
|
# ------------------------------------------------------
|
|
def exit_program(self, event=None):
|
|
# Ponastavi stanje preprečevanja spanja na Windows
|
|
if sys.platform.startswith("win"):
|
|
try:
|
|
# ES_CONTINUOUS (0x80000000)
|
|
ctypes.windll.kernel32.SetThreadExecutionState(ctypes.c_uint(0x80000000))
|
|
except:
|
|
pass
|
|
elif sys.platform == "linux":
|
|
try:
|
|
# Sprostimo DBus inhibitor, če ga imamo
|
|
if hasattr(self, "inhibitor_cookie") and self.inhibitor_cookie:
|
|
subprocess.Popen([
|
|
"dbus-send", "--dest=org.gnome.SessionManager",
|
|
"/org/gnome/SessionManager", "org.gnome.SessionManager.UnInhibit",
|
|
f"uint32:{self.inhibitor_cookie}"
|
|
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
except:
|
|
pass
|
|
|
|
self.conn.close()
|
|
self.root.destroy()
|
|
# ------------------------------------------------------
|
|
# Restart programa
|
|
# ------------------------------------------------------
|
|
def restart_program(self):
|
|
try:
|
|
if self.conn:
|
|
self.conn.close()
|
|
except Exception:
|
|
pass
|
|
|
|
python = sys.executable
|
|
args = [python] + sys.argv
|
|
subprocess.Popen(args)
|
|
|
|
self.root.after(200, self.exit_program)
|
|
|
|
# ------------------------------------------------------
|
|
# Pošiljanje ntfy obvestil
|
|
# ------------------------------------------------------
|
|
def send_ntfy_notification(self, song_id, title, lyrics):
|
|
"""
|
|
Pošlje obvestilo o spremembi besedila preko ntfy.sh.
|
|
"""
|
|
topic = self.settings.get("ntfy_topic", "").strip()
|
|
if not topic:
|
|
return
|
|
|
|
label = self.settings.get("installation_label", "Projekcija")
|
|
# ntfy.sh headers must be ASCII. We'll use base64 encoding for the title to support non-ASCII characters.
|
|
import base64
|
|
encoded_label = "=?UTF-8?B?" + base64.b64encode(label.encode('utf-8')).decode('utf-8') + "?="
|
|
|
|
message = f"{song_id}\n{title}\n{lyrics}"
|
|
|
|
try:
|
|
url = f"https://ntfy.sh/{topic}"
|
|
req = urllib.request.Request(
|
|
url,
|
|
data=message.encode('utf-8'),
|
|
method='POST',
|
|
headers={ "Title": encoded_label,
|
|
"Priority": "high",
|
|
"Tags": "loudspeaker" }
|
|
)
|
|
# Uporabimo kratek timeout, da ne blokiramo aplikacije
|
|
with urllib.request.urlopen(req, timeout=5) as response:
|
|
pass
|
|
except Exception as e:
|
|
print(f"Napaka pri pošiljanju ntfy obvestila: {e}")
|
|
|
|
# ------------------------------------------------------
|
|
# Prikaži informacije o aplikaciji v Tkinter oknu
|
|
# ------------------------------------------------------
|
|
def show_app_info_tkinter(self):
|
|
"""Prikaže informacije o aplikaciji v glavnem oknu (ukaz 9900)."""
|
|
try:
|
|
with open('appinfo.json', 'r', encoding='utf-8') as f:
|
|
info = json.load(f)
|
|
|
|
self.cursor.execute("SELECT COUNT(*) FROM songs")
|
|
count = self.cursor.fetchone()[0]
|
|
|
|
authors = ", ".join(info.get("authors", []))
|
|
|
|
display_text = (
|
|
f"{info.get('name', 'Projekcija')}\n"
|
|
f"Verzija: {info.get('version', 'neznana')}\n\n"
|
|
f"{info.get('description', '')}\n\n"
|
|
f"Avtorji: {authors}\n\n"
|
|
f"Število pesmi v bazi: {count}"
|
|
)
|
|
|
|
# Ponastavitev izgleda barvnega območja za prikaz informacij
|
|
self.color_frame.config(bg=self.settings["bg_color"], width=self.color_width, height=self.screen_height)
|
|
self.color_frame.place(relx=0.5, rely=0.5, anchor="center")
|
|
self.display_text.config(bg=self.settings["bg_color"], fg=self.settings["fg_color"], text=display_text)
|
|
self.display_text.pack(expand=True)
|
|
|
|
# self.song_number_last = "9900"
|
|
self.song_info_label.config(text="Informacije o programu")
|
|
self.song_info_label.lift()
|
|
|
|
self.pages = []
|
|
self.current_page_index = 0
|
|
self.waiting_for_song = True
|
|
|
|
notify_clients()
|
|
|
|
except Exception as e:
|
|
self.display_text.config(text=f"Napaka pri branju informacij: {e}")
|
|
finally:
|
|
self.song_number = ""
|
|
|
|
# ----------------------------------------------------------
|
|
# Zagon aplikacije
|
|
# ----------------------------------------------------------
|
|
if __name__ == "__main__":
|
|
root = tk.Tk()
|
|
app = SongProjector(root)
|
|
root.mainloop()
|