diff --git a/channel_updater.py b/channel_updater.py new file mode 100644 index 0000000..6fbf963 --- /dev/null +++ b/channel_updater.py @@ -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 diff --git a/locales/en.json b/locales/en.json index cfa4c03..be2eb70 100644 --- a/locales/en.json +++ b/locales/en.json @@ -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." } diff --git a/locales/es.json b/locales/es.json index f96a71b..159e59a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -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." } diff --git a/main.py b/main.py index 2fa1268..d09c85b 100755 --- a/main.py +++ b/main.py @@ -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: diff --git a/server.py b/server.py index 8d99d48..4c6dfba 100755 --- a/server.py +++ b/server.py @@ -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.app.run(host=host, port=port) + 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) diff --git a/templates/index.html b/templates/index.html index b8e9091..cf71b35 100644 --- a/templates/index.html +++ b/templates/index.html @@ -471,6 +471,9 @@ +