Added channel update functionality

This commit is contained in:
Ignacio Rivero 2025-03-28 13:21:22 -03:00
parent e8fae1a14b
commit 2300fd8685
7 changed files with 153 additions and 6 deletions

83
channel_updater.py Normal file
View File

@ -0,0 +1,83 @@
#!/usr/bin/python
"""Channel update functionality for IPMPV.
This module provides functionality to periodically update the channel list
without requiring application restart."""
import threading
import time
class ChannelUpdater:
"""Class to handle periodic channel list updates."""
def __init__(self, server, update_interval=3600):
"""
Initialize the channel updater.
Args:
server: The IPMPVServer instance
update_interval: Update interval in seconds (default: 1 hour)
"""
self.server = server
self.update_interval = update_interval
self.running = False
self.update_thread = None
def start(self):
"""Start the periodic update thread."""
if self.update_thread is not None and self.update_thread.is_alive():
return # Already running
self.running = True
self.update_thread = threading.Thread(
target=self._update_loop,
daemon=True
)
self.update_thread.start()
print(f"Channel updater started with {self.update_interval}s interval")
def stop(self):
"""Stop the periodic update thread."""
self.running = False
if self.update_thread:
self.update_thread.join(timeout=1)
self.update_thread = None
def _update_loop(self):
"""Background loop to update channels periodically."""
while self.running:
# Sleep first to avoid immediate update after startup
for _ in range(self.update_interval):
if not self.running:
return
time.sleep(1)
# Perform the update
try:
self._update_channels()
except Exception as e:
print(f"Error updating channels: {e}")
def _update_channels(self):
"""Update the channel list."""
from channels import get_channels
print("Updating channel list...")
new_channels = get_channels()
# Only update if we got valid channels
if new_channels:
# Update the server's channel list
self.server.channels = new_channels
print(f"Channel list updated: {len(new_channels)} channels")
else:
print("Channel update failed or returned empty list")
def force_update(self):
"""Force an immediate channel list update."""
try:
self._update_channels()
return True
except Exception as e:
print(f"Error during forced channel update: {e}")
return False

View File

@ -6,6 +6,7 @@
"stop_retroarch": "Stop RetroArch",
"deinterlacing": "Deinterlacing",
"resolution": "Resolution",
"refresh_channels": "Refresh",
"volume": "Volume",
"mute": "mute",
"toggle_osd": "Toggle OSD",
@ -27,5 +28,8 @@
"latency_high": "Hi",
"latency_low": "Lo",
"other": "Other",
"invalid_url": "Invalid or unsupported URL"
"invalid_url": "Invalid or unsupported URL",
"channels_updated_p1": "Channels updated! Found",
"channels_updated_p2": "channels.",
"channel_update_failed": "Channel update failed."
}

View File

@ -6,6 +6,7 @@
"stop_retroarch": "Detener RetroArch",
"deinterlacing": "Desentrelazado",
"resolution": "Resolución",
"refresh_channels": "Refrescar",
"volume": "Volumen",
"mute": "mute",
"toggle_osd": "Alternar OSD",
@ -27,5 +28,8 @@
"latency_high": "Hi",
"latency_low": "Lo",
"other": "Otro",
"invalid_url": "URL inválida o no soportada"
"invalid_url": "URL inválida o no soportada",
"channels_updated_p1": "Canales actualizados.",
"channels_updated_p2": "canales encontrados.",
"channel_update_failed": "Error al actualizar canales."
}

View File

@ -6,7 +6,7 @@ from multiprocessing import Queue
import sys
# Set up utils first
from utils import setup_environment, get_current_resolution, ipmpv_retroarch_cmd
from utils import setup_environment, get_current_resolution, ipmpv_retroarch_cmd, update_interval
# Initialize environment
setup_environment()
@ -52,7 +52,9 @@ def main():
from_qt_queue=from_qt_queue,
resolution=resolution,
ipmpv_retroarch_cmd=ipmpv_retroarch_cmd,
volume_control=volume_control
volume_control=volume_control,
# Mmm, hacky! TODO: fix this.
update_interval=update_interval if update_interval is not None else 3600
)
try:

View File

@ -9,11 +9,12 @@ import flask
from flask import request, jsonify, send_from_directory, redirect, url_for, make_response
from localization import localization, _
from utils import is_valid_url, change_resolution, get_current_resolution, is_wayland, get_or_create_secret_key
from channel_updater import ChannelUpdater
class IPMPVServer:
"""Flask server for IPMPV web interface."""
def __init__(self, channels, player, to_qt_queue, from_qt_queue, resolution, ipmpv_retroarch_cmd, volume_control=None):
def __init__(self, channels, player, to_qt_queue, from_qt_queue, resolution, ipmpv_retroarch_cmd, volume_control=None, update_interval=3600):
"""Initialize the server."""
self.app = flask.Flask(__name__,
static_folder='static',
@ -26,6 +27,7 @@ class IPMPVServer:
self.ipmpv_retroarch_cmd = ipmpv_retroarch_cmd
self.retroarch_p = None
self.volume_control = volume_control
self.channel_updater = ChannelUpdater(self, update_interval=update_interval)
# Register routes
self._register_routes()
@ -33,7 +35,12 @@ class IPMPVServer:
def run(self, host="0.0.0.0", port=5000):
"""Run the Flask server."""
self.channel_updater.start()
try:
self.app.run(host=host, port=port)
finally:
self.channel_updater.stop()
def _register_routes(self):
"""Register Flask routes."""
@ -103,6 +110,10 @@ class IPMPVServer:
def channel_down():
return self._handle_channel_down()
@self.app.route('/update_channels')
def update_channels():
return self._handle_update_channels()
@self.app.route('/manifest.json')
def serve_manifest():
return send_from_directory("static", 'manifest.json',
@ -127,6 +138,7 @@ class IPMPVServer:
def serve_favicon():
return send_from_directory("static", 'favicon.ico',
mimetype='image/vnd.microsoft.icon')
def _handle_index(self):
"""Handle the index route."""
from channels import group_channels
@ -178,6 +190,7 @@ class IPMPVServer:
html = html.replace("%DEINTERLACE_STATE%", _("on") if self.player.deinterlace else _("off"))
html = html.replace("%RESOLUTION_LABEL%", _("resolution"))
html = html.replace("%RESOLUTION%", self.resolution)
html = html.replace("%REFRESH_CH_LABEL%", _("refresh_channels"))
html = html.replace("%LATENCY_STATE%", "ON" if self.player.low_latency else "OFF")
html = html.replace("%LATENCY_LABEL%", _("latency_low") if self.player.low_latency else _("latency_high"))
html = html.replace("%CHANNEL_GROUPS%", channel_groups_html)
@ -208,6 +221,9 @@ class IPMPVServer:
html = html.replace("%JS_START_RETROARCH%", _("start_retroarch"))
html = html.replace("%JS_ON_LABEL%", _("on"))
html = html.replace("%JS_OFF_LABEL%", _("off"))
html = html.replace("%JS_CH_UPDATE_P1%", _("channels_updated_p1"))
html = html.replace("%JS_CH_UPDATE_P2%", _("channels_updated_p2"))
html = html.replace("%JS_CH_UPDATE_FAIL%", _("channel_update_failed"))
return html
@ -359,3 +375,9 @@ class IPMPVServer:
is_muted = self.volume_control.is_muted()
return jsonify(volume=volume, muted=is_muted)
return jsonify(error="Volume control not available"), 404
def _handle_update_channels(self):
"""Handle the update_channels route."""
success = self.channel_updater.force_update()
return jsonify(success=success,
channel_count=len(self.channels) if success else None)

View File

@ -471,6 +471,9 @@
<button id="resolution-btn" class="control-button" onclick="toggleResolution()">
%RESOLUTION_LABEL%: <span id="resolution-state">%RESOLUTION%</span>
</button>
<button id="refresh-ch-btn" class="control-button" onclick="refreshChannels()">
%REFRESH_CH_LABEL%
</button>
</div>
<div class="section">
@ -672,6 +675,34 @@
.then(() => window.location.reload())
}
function refreshChannels() {
const refreshBtn = document.getElementById('refresh-ch-btn');
const originalText = refreshBtn.textContent;
refreshBtn.textContent = "Updating...";
refreshBtn.disabled = true;
fetch('/update_channels')
.then(response => response.json())
.then(data => {
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
if (data.success) {
showToast(`%JS_CH_UPDATE_P1% ${data.channel_count} %JS_CH_UPDATE_P2%`);
// Reload the page to show updated channel list
setTimeout(() => window.location.reload(), 2000);
} else {
showToast("%JS_CH_UPDATE_FAIL%");
}
})
.catch(error => {
refreshBtn.textContent = originalText;
refreshBtn.disabled = false;
showToast("Error updating channels. Please try again.");
});
}
// Mobile-friendly toast notification
function showToast(message) {
const toast = document.createElement('div');

View File

@ -13,6 +13,7 @@ ipmpv_retroarch_cmd = os.environ.get("IPMPV_RETROARCH_CMD")
m3u_url = os.environ.get('IPMPV_M3U_URL')
hwdec = os.environ.get('IPMPV_HWDEC')
ao = os.environ.get('IPMPV_AO')
update_interval = os.environ.get('IPMPV_UPDATE_INTERVAL')
def setup_environment():
"""Set up environment variables."""