Made it so the font actually gets loaded, fixed run.sh

This commit is contained in:
Ignacio Rivero 2025-02-24 23:30:28 -03:00
parent b6e7445d5f
commit b63565646a
2 changed files with 70 additions and 69 deletions

133
main.py
View File

@ -7,23 +7,17 @@ import re
import subprocess import subprocess
import os import os
import time import time
import threading
import io
import multiprocessing import multiprocessing
from PIL import Image
from flask import request from flask import request
from flask import jsonify from flask import jsonify
from PyQt5.QtWidgets import QApplication, QWidget, QLabel, QVBoxLayout, QHBoxLayout from PyQt5.QtWidgets import *
from PyQt5.QtCore import Qt, QObject, QTimer, QRect, pyqtSignal, pyqtSlot from PyQt5.QtCore import *
from PyQt5.QtGui import QPainter, QColor, QFont, QPen, QBrush, QPixmap, QPainterPath from PyQt5.QtGui import *
from multiprocessing import Queue from multiprocessing import Queue
import traceback
os.environ["LC_ALL"] = "C" os.environ["LC_ALL"] = "C"
os.environ["LANG"] = "C" os.environ["LANG"] = "C"
@ -37,9 +31,13 @@ M3U_URL = os.environ.get('IPMPV_M3U_URL')
class OsdWidget(QWidget): class OsdWidget(QWidget):
def __init__(self, channel_info, width=600, height=165, close_time=5, corner_radius=int(osd_corner_radius) if osd_corner_radius is not None else 15): def __init__(self, channel_info, width=600, height=165, close_time=5, corner_radius=int(osd_corner_radius) if osd_corner_radius is not None else 15):
QFontDatabase.addApplicationFont('FiraSans-Regular.ttf')
QFontDatabase.addApplicationFont('FiraSans-Bold.ttf')
global is_wayland global is_wayland
super().__init__() super().__init__()
self.channel_info = channel_info self.channel_info = channel_info
self.orig_width = width self.orig_width = width
self.orig_height = height self.orig_height = height
@ -53,31 +51,31 @@ class OsdWidget(QWidget):
# Setup window # Setup window
self.setWindowTitle("OSD") self.setWindowTitle("OSD")
self.setFixedSize(width, height) self.setFixedSize(width, height)
# Check if we're running on Wayland # Check if we're running on Wayland
self.is_wayland = is_wayland self.is_wayland = is_wayland
# Set appropriate window flags and size # Set appropriate window flags and size
if self.is_wayland: if self.is_wayland:
# For Wayland, use fullscreen transparent approach # For Wayland, use fullscreen transparent approach
self.setWindowFlags( self.setWindowFlags(
Qt.FramelessWindowHint | Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint | Qt.WindowStaysOnTopHint |
Qt.WindowDoesNotAcceptFocus Qt.WindowDoesNotAcceptFocus
) )
# Set fullscreen size # Set fullscreen size
self.screen_geometry = QApplication.desktop().screenGeometry() self.screen_geometry = QApplication.desktop().screenGeometry()
self.setFixedSize(self.screen_geometry.width(), self.screen_geometry.height()) self.setFixedSize(self.screen_geometry.width(), self.screen_geometry.height())
# Calculate content positioning # Calculate content positioning
self.content_x = (self.screen_geometry.width() - self.orig_width) // 2 self.content_x = (self.screen_geometry.width() - self.orig_width) // 2
self.content_y = 20 # 20px from top self.content_y = 20 # 20px from top
else: else:
# For X11, use the original approach # For X11, use the original approach
self.setWindowFlags( self.setWindowFlags(
Qt.FramelessWindowHint | Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint | Qt.WindowStaysOnTopHint |
Qt.X11BypassWindowManagerHint | Qt.X11BypassWindowManagerHint |
Qt.Tool | Qt.Tool |
Qt.ToolTip Qt.ToolTip
@ -85,13 +83,13 @@ class OsdWidget(QWidget):
self.setFixedSize(width, height) self.setFixedSize(width, height)
self.content_x = 0 self.content_x = 0
self.content_y = 0 self.content_y = 0
# Enable transparency # Enable transparency
self.setAttribute(Qt.WA_TranslucentBackground) self.setAttribute(Qt.WA_TranslucentBackground)
# Position window at the top center of the screen # Position window at the top center of the screen
self.position_window() self.position_window()
# Load logo if available # Load logo if available
self.logo_pixmap = None self.logo_pixmap = None
if channel_info["logo"]: if channel_info["logo"]:
@ -99,12 +97,12 @@ class OsdWidget(QWidget):
if self.is_wayland: if self.is_wayland:
self.setAttribute(Qt.WA_TransparentForMouseEvents) self.setAttribute(Qt.WA_TransparentForMouseEvents)
def position_window(self): def position_window(self):
if self.is_wayland: if self.is_wayland:
# For Wayland, we just position at 0,0 (fullscreen) # For Wayland, we just position at 0,0 (fullscreen)
self.move(0, 0) self.move(0, 0)
# Ensure window stays on top # Ensure window stays on top
self.stay_on_top_timer = QTimer(self) self.stay_on_top_timer = QTimer(self)
self.stay_on_top_timer.timeout.connect(lambda: self.raise_()) self.stay_on_top_timer.timeout.connect(lambda: self.raise_())
@ -115,12 +113,12 @@ class OsdWidget(QWidget):
x = (screen_geometry.width() - self.orig_width) // 2 x = (screen_geometry.width() - self.orig_width) // 2
y = 20 # 20px from top y = 20 # 20px from top
self.setGeometry(x, y, self.orig_width, self.orig_height) self.setGeometry(x, y, self.orig_width, self.orig_height)
# X11 specific window hints # X11 specific window hints
self.setAttribute(Qt.WA_X11NetWmWindowTypeNotification) self.setAttribute(Qt.WA_X11NetWmWindowTypeNotification)
QTimer.singleShot(100, lambda: self.move(x, y)) QTimer.singleShot(100, lambda: self.move(x, y))
QTimer.singleShot(500, lambda: self.move(x, y)) QTimer.singleShot(500, lambda: self.move(x, y))
# Periodically ensure window stays on top # Periodically ensure window stays on top
self.stay_on_top_timer = QTimer(self) self.stay_on_top_timer = QTimer(self)
self.stay_on_top_timer.timeout.connect(lambda: self.raise_()) self.stay_on_top_timer.timeout.connect(lambda: self.raise_())
@ -137,11 +135,11 @@ class OsdWidget(QWidget):
self.update() # Trigger repaint self.update() # Trigger repaint
except Exception as e: except Exception as e:
print(f"Failed to load logo: {e}") print(f"Failed to load logo: {e}")
def paintEvent(self, event): def paintEvent(self, a0):
painter = QPainter(self) painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) painter.setRenderHint(QPainter.Antialiasing)
if self.is_wayland: if self.is_wayland:
# For Wayland, we're drawing the content in the right position on a fullscreen widget # For Wayland, we're drawing the content in the right position on a fullscreen widget
self.draw_osd_content(painter, self.content_x, self.content_y) self.draw_osd_content(painter, self.content_x, self.content_y)
@ -152,20 +150,19 @@ class OsdWidget(QWidget):
# Create a path for rounded rectangle background # Create a path for rounded rectangle background
path = QPainterPath() path = QPainterPath()
path.addRoundedRect( path.addRoundedRect(
x_offset, y_offset, x_offset, y_offset,
self.orig_width, self.orig_height, self.orig_width, self.orig_height,
self.corner_radius, self.corner_radius self.corner_radius, self.corner_radius
) )
# Fill the rounded rectangle with semi-transparent background # Fill the rounded rectangle with semi-transparent background
painter.setPen(Qt.NoPen) painter.setPen(Qt.NoPen)
painter.setBrush(QColor(0, 50, 100, 200)) # RGBA painter.setBrush(QColor(0, 50, 100, 200)) # RGBA
painter.drawPath(path) painter.drawPath(path)
# Setup text drawing # Setup text drawing
painter.setPen(QColor(255, 255, 255)) painter.setPen(QColor(255, 255, 255))
# Use Fira Sans as originally intended
try: try:
font = QFont("Fira Sans", 18) font = QFont("Fira Sans", 18)
font.setBold(True) font.setBold(True)
@ -177,13 +174,13 @@ class OsdWidget(QWidget):
font.setPointSize(14) font.setPointSize(14)
font.setBold(False) font.setBold(False)
painter.setFont(font) painter.setFont(font)
# Draw deinterlace status # Draw deinterlace status
painter.drawText(x_offset + 20, y_offset + 70, f"Deinterlacing {'on' if self.channel_info['deinterlace'] else 'off'}") painter.drawText(x_offset + 20, y_offset + 70, f"Deinterlacing {'on' if self.channel_info['deinterlace'] else 'off'}")
# Draw latency mode # Draw latency mode
painter.drawText(x_offset + 20, y_offset + 100, f"{'Low' if self.channel_info['low_latency'] else 'High'} latency") painter.drawText(x_offset + 20, y_offset + 100, f"{'Low' if self.channel_info['low_latency'] else 'High'} latency")
# Draw codec badges if available # Draw codec badges if available
if self.video_codec: if self.video_codec:
self.draw_badge(painter, self.video_codec, x_offset + 80, y_offset + self.orig_height - 40) self.draw_badge(painter, self.video_codec, x_offset + 80, y_offset + self.orig_height - 40)
@ -194,7 +191,7 @@ class OsdWidget(QWidget):
# Draw logo if available # Draw logo if available
if self.logo_pixmap: if self.logo_pixmap:
painter.drawPixmap(x_offset + self.orig_width - 100, y_offset + 20, self.logo_pixmap) painter.drawPixmap(x_offset + self.orig_width - 100, y_offset + 20, self.logo_pixmap)
except Exception as e: except Exception as e:
print(f"Error in painting: {e}") print(f"Error in painting: {e}")
import traceback import traceback
@ -203,38 +200,38 @@ class OsdWidget(QWidget):
def draw_badge(self, painter, text, x, y): def draw_badge(self, painter, text, x, y):
# Save current painter state # Save current painter state
painter.save() painter.save()
# Draw rounded badge # Draw rounded badge
painter.setPen(QPen(QColor(255, 255, 255, 255), 2)) painter.setPen(QPen(QColor(255, 255, 255, 255), 2))
painter.setBrush(Qt.NoBrush) painter.setBrush(Qt.NoBrush)
# Use QPainterPath for consistent rounded corners # Use QPainterPath for consistent rounded corners
badge_path = QPainterPath() badge_path = QPainterPath()
badge_path.addRoundedRect(x, y, 48, 20, 7, 7) badge_path.addRoundedRect(x, y, 48, 20, 7, 7)
painter.drawPath(badge_path) painter.drawPath(badge_path)
# Draw text # Draw text
painter.setPen(QColor(255, 255, 255)) painter.setPen(QColor(255, 255, 255))
font = painter.font() font = painter.font()
font.setBold(True) font.setBold(True)
font.setPointSize(8) font.setPointSize(8)
painter.setFont(font) painter.setFont(font)
# Center text in badge # Center text in badge
font_metrics = painter.fontMetrics() font_metrics = painter.fontMetrics()
text_width = font_metrics.width(text) text_width = font_metrics.width(text)
text_height = font_metrics.height() text_height = font_metrics.height()
# We need to use integer coordinates for drawText, not floats # We need to use integer coordinates for drawText, not floats
text_x = int(x + (48 - text_width) / 2) text_x = int(x + (48 - text_width) / 2)
text_y = int(y + text_height) text_y = int(y + text_height)
# Use the int, int, string version of drawText # Use the int, int, string version of drawText
painter.drawText(text_x, text_y, text) painter.drawText(text_x, text_y, text)
# Restore painter state # Restore painter state
painter.restore() painter.restore()
def update_codecs(self, video_codec, audio_codec, video_res, interlaced): def update_codecs(self, video_codec, audio_codec, video_res, interlaced):
if video_codec: if video_codec:
self.video_codec = video_codec self.video_codec = video_codec
@ -256,14 +253,14 @@ class OsdWidget(QWidget):
def start_close_timer(self, seconds=5): def start_close_timer(self, seconds=5):
""" """
Starts a timer to close the widget after the specified number of seconds. Starts a timer to close the widget after the specified number of seconds.
Parameters: Parameters:
seconds (int): Number of seconds before closing the widget (default: 3) seconds (int): Number of seconds before closing the widget (default: 3)
""" """
# Cancel any existing close timer # Cancel any existing close timer
if hasattr(self, 'close_timer') and self.close_timer.isActive(): if hasattr(self, 'close_timer') and self.close_timer.isActive():
self.close_timer.stop() self.close_timer.stop()
# Create and start a new timer # Create and start a new timer
self.close_timer = QTimer(self) self.close_timer = QTimer(self)
self.close_timer.setSingleShot(True) self.close_timer.setSingleShot(True)
@ -276,7 +273,7 @@ def qt_process():
"""Run Qt application in a separate process""" """Run Qt application in a separate process"""
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
app = QApplication(sys.argv) app = QApplication(sys.argv)
osd = None osd = None
@ -304,10 +301,10 @@ def qt_process():
osd.update_codecs(command['vcodec'], command['acodec'], command['video_res'], command['interlaced']) osd.update_codecs(command['vcodec'], command['acodec'], command['video_res'], command['interlaced'])
# Schedule next check # Schedule next check
QTimer.singleShot(100, check_queue) QTimer.singleShot(100, check_queue)
# Start the queue check # Start the queue check
check_queue() check_queue()
# Run Qt event loop # Run Qt event loop
app.exec_() app.exec_()
@ -318,7 +315,7 @@ def get_channels():
print("Error: IPMPV_M3U_URL not set. Please set this environment variable to the URL of your IPTV list, in M3U format.") print("Error: IPMPV_M3U_URL not set. Please set this environment variable to the URL of your IPTV list, in M3U format.")
exit(1) exit(1)
lines = response.text.splitlines() lines = response.text.splitlines()
channels = [] channels = []
regex = re.compile(r'tvg-logo="(.*?)".*?group-title="(.*?)"', re.IGNORECASE) regex = re.compile(r'tvg-logo="(.*?)".*?group-title="(.*?)"', re.IGNORECASE)
@ -423,33 +420,33 @@ def audio_codec_observer(name, value):
def play_channel(index): def play_channel(index):
global current_index, vcodec, acodec, video_res, interlaced global current_index, vcodec, acodec, video_res, interlaced
print(f"\n=== Starting channel change to index {index} ===") print(f"\n=== Starting channel change to index {index} ===")
to_qt_queue.put({ to_qt_queue.put({
'action': 'close_osd' 'action': 'close_osd'
}) })
print("Closed OSD") print("Closed OSD")
vcodec = None vcodec = None
acodec = None acodec = None
current_index = index % len(channels) current_index = index % len(channels)
print(f"Playing channel: {channels[current_index]['name']} ({channels[current_index]['url']})") print(f"Playing channel: {channels[current_index]['name']} ({channels[current_index]['url']})")
try: try:
player.loadfile("./novideo.png") player.loadfile("./novideo.png")
player.wait_until_playing() player.wait_until_playing()
channel_info = { channel_info = {
"name": channels[current_index]["name"], "name": channels[current_index]["name"],
"deinterlace": deinterlace, "deinterlace": deinterlace,
"low_latency": low_latency, "low_latency": low_latency,
"logo": channels[current_index]["logo"] "logo": channels[current_index]["logo"]
} }
to_qt_queue.put({ to_qt_queue.put({
'action': 'show_osd', 'action': 'show_osd',
'channel_info': channel_info 'channel_info': channel_info
}) })
player.loadfile(channels[current_index]['url']) player.loadfile(channels[current_index]['url'])
time.sleep(0.5) time.sleep(0.5)
player.wait_until_playing() player.wait_until_playing()
@ -470,7 +467,7 @@ def play_channel(index):
to_qt_queue.put({ to_qt_queue.put({
'action': 'start_close', 'action': 'start_close',
}) })
except Exception as e: except Exception as e:
print(f"\033[91mError in play_channel: {str(e)}\033[0m") print(f"\033[91mError in play_channel: {str(e)}\033[0m")
traceback.print_exc() traceback.print_exc()
@ -483,7 +480,7 @@ def index():
grouped_channels = {} grouped_channels = {}
for channel in channels: for channel in channels:
grouped_channels.setdefault(channel["group"], []).append(channel) grouped_channels.setdefault(channel["group"], []).append(channel)
flat_channel_list = [channel for channel in channels] flat_channel_list = [channel for channel in channels]
html = f""" html = f"""
@ -600,7 +597,7 @@ def index():
<h2>All Channels</h2> <h2>All Channels</h2>
<div class="group-container"> <div class="group-container">
""" """
for group, ch_list in grouped_channels.items(): for group, ch_list in grouped_channels.items():
html += f'<div class="group">{group}' html += f'<div class="group">{group}'
for channel in ch_list: for channel in ch_list:
@ -688,7 +685,7 @@ def play_custom():
global current_index global current_index
current_index = None current_index = None
url = request.args.get("url") url = request.args.get("url")
if not url or not is_valid_url(url): if not url or not is_valid_url(url):
return jsonify(success=False, error="Invalid or unsupported URL") return jsonify(success=False, error="Invalid or unsupported URL")
@ -727,7 +724,7 @@ def show_osd():
@app.route("/channel") @app.route("/channel")
def switch_channel(): def switch_channel():
index = int(request.args.get("index", current_index)) index = int(request.args.get("index", current_index))
play_channel(index) play_channel(index)
return "", 204 return "", 204
@app.route("/toggle_deinterlace") @app.route("/toggle_deinterlace")
@ -823,10 +820,10 @@ def toggle_resolution():
return jsonify(res=get_current_resolution()) return jsonify(res=get_current_resolution())
if __name__ == "__main__": if __name__ == "__main__":
# Start Qt process # Start Qt process
qt_proc = multiprocessing.Process(target=qt_process) qt_proc = multiprocessing.Process(target=qt_process)
qt_proc.daemon = True qt_proc.daemon = True
qt_proc.start() qt_proc.start()
# Start Flask in main thread # Start Flask in main thread
app.run(host="0.0.0.0", port=5000) app.run(host="0.0.0.0", port=5000)

6
run.sh
View File

@ -1,5 +1,7 @@
#!/bin/bash #!/bin/bash
cd "$(dirname "$0")"
# Check if virtual environment already exists # Check if virtual environment already exists
if [ -d "./.venv" ]; then if [ -d "./.venv" ]; then
echo "Virtual environment already exists." echo "Virtual environment already exists."
@ -8,6 +10,8 @@ else
python -m venv --system-site-packages ./.venv python -m venv --system-site-packages ./.venv
fi fi
export PYTHONUNBUFFERED=1
# Activate the virtual environment # Activate the virtual environment
source ./.venv/bin/activate source ./.venv/bin/activate
@ -16,4 +20,4 @@ pip install -r requirements.txt
# Run the application # Run the application
echo "Starting..." echo "Starting..."
python main.py python main.py &> ipmpv.log