Added channel update functionality
This commit is contained in:
parent
e8fae1a14b
commit
2300fd8685
83
channel_updater.py
Normal file
83
channel_updater.py
Normal 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
|
||||||
@ -6,6 +6,7 @@
|
|||||||
"stop_retroarch": "Stop RetroArch",
|
"stop_retroarch": "Stop RetroArch",
|
||||||
"deinterlacing": "Deinterlacing",
|
"deinterlacing": "Deinterlacing",
|
||||||
"resolution": "Resolution",
|
"resolution": "Resolution",
|
||||||
|
"refresh_channels": "Refresh",
|
||||||
"volume": "Volume",
|
"volume": "Volume",
|
||||||
"mute": "mute",
|
"mute": "mute",
|
||||||
"toggle_osd": "Toggle OSD",
|
"toggle_osd": "Toggle OSD",
|
||||||
@ -27,5 +28,8 @@
|
|||||||
"latency_high": "Hi",
|
"latency_high": "Hi",
|
||||||
"latency_low": "Lo",
|
"latency_low": "Lo",
|
||||||
"other": "Other",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@
|
|||||||
"stop_retroarch": "Detener RetroArch",
|
"stop_retroarch": "Detener RetroArch",
|
||||||
"deinterlacing": "Desentrelazado",
|
"deinterlacing": "Desentrelazado",
|
||||||
"resolution": "Resolución",
|
"resolution": "Resolución",
|
||||||
|
"refresh_channels": "Refrescar",
|
||||||
"volume": "Volumen",
|
"volume": "Volumen",
|
||||||
"mute": "mute",
|
"mute": "mute",
|
||||||
"toggle_osd": "Alternar OSD",
|
"toggle_osd": "Alternar OSD",
|
||||||
@ -27,5 +28,8 @@
|
|||||||
"latency_high": "Hi",
|
"latency_high": "Hi",
|
||||||
"latency_low": "Lo",
|
"latency_low": "Lo",
|
||||||
"other": "Otro",
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
6
main.py
6
main.py
@ -6,7 +6,7 @@ from multiprocessing import Queue
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
# Set up utils first
|
# 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
|
# Initialize environment
|
||||||
setup_environment()
|
setup_environment()
|
||||||
@ -52,7 +52,9 @@ def main():
|
|||||||
from_qt_queue=from_qt_queue,
|
from_qt_queue=from_qt_queue,
|
||||||
resolution=resolution,
|
resolution=resolution,
|
||||||
ipmpv_retroarch_cmd=ipmpv_retroarch_cmd,
|
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:
|
try:
|
||||||
|
|||||||
26
server.py
26
server.py
@ -9,11 +9,12 @@ import flask
|
|||||||
from flask import request, jsonify, send_from_directory, redirect, url_for, make_response
|
from flask import request, jsonify, send_from_directory, redirect, url_for, make_response
|
||||||
from localization import localization, _
|
from localization import localization, _
|
||||||
from utils import is_valid_url, change_resolution, get_current_resolution, is_wayland, get_or_create_secret_key
|
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:
|
class IPMPVServer:
|
||||||
"""Flask server for IPMPV web interface."""
|
"""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."""
|
"""Initialize the server."""
|
||||||
self.app = flask.Flask(__name__,
|
self.app = flask.Flask(__name__,
|
||||||
static_folder='static',
|
static_folder='static',
|
||||||
@ -26,6 +27,7 @@ class IPMPVServer:
|
|||||||
self.ipmpv_retroarch_cmd = ipmpv_retroarch_cmd
|
self.ipmpv_retroarch_cmd = ipmpv_retroarch_cmd
|
||||||
self.retroarch_p = None
|
self.retroarch_p = None
|
||||||
self.volume_control = volume_control
|
self.volume_control = volume_control
|
||||||
|
self.channel_updater = ChannelUpdater(self, update_interval=update_interval)
|
||||||
|
|
||||||
# Register routes
|
# Register routes
|
||||||
self._register_routes()
|
self._register_routes()
|
||||||
@ -33,7 +35,12 @@ class IPMPVServer:
|
|||||||
|
|
||||||
def run(self, host="0.0.0.0", port=5000):
|
def run(self, host="0.0.0.0", port=5000):
|
||||||
"""Run the Flask server."""
|
"""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):
|
def _register_routes(self):
|
||||||
"""Register Flask routes."""
|
"""Register Flask routes."""
|
||||||
|
|
||||||
@ -103,6 +110,10 @@ class IPMPVServer:
|
|||||||
def channel_down():
|
def channel_down():
|
||||||
return self._handle_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')
|
@self.app.route('/manifest.json')
|
||||||
def serve_manifest():
|
def serve_manifest():
|
||||||
return send_from_directory("static", 'manifest.json',
|
return send_from_directory("static", 'manifest.json',
|
||||||
@ -127,6 +138,7 @@ class IPMPVServer:
|
|||||||
def serve_favicon():
|
def serve_favicon():
|
||||||
return send_from_directory("static", 'favicon.ico',
|
return send_from_directory("static", 'favicon.ico',
|
||||||
mimetype='image/vnd.microsoft.icon')
|
mimetype='image/vnd.microsoft.icon')
|
||||||
|
|
||||||
def _handle_index(self):
|
def _handle_index(self):
|
||||||
"""Handle the index route."""
|
"""Handle the index route."""
|
||||||
from channels import group_channels
|
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("%DEINTERLACE_STATE%", _("on") if self.player.deinterlace else _("off"))
|
||||||
html = html.replace("%RESOLUTION_LABEL%", _("resolution"))
|
html = html.replace("%RESOLUTION_LABEL%", _("resolution"))
|
||||||
html = html.replace("%RESOLUTION%", self.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_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("%LATENCY_LABEL%", _("latency_low") if self.player.low_latency else _("latency_high"))
|
||||||
html = html.replace("%CHANNEL_GROUPS%", channel_groups_html)
|
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_START_RETROARCH%", _("start_retroarch"))
|
||||||
html = html.replace("%JS_ON_LABEL%", _("on"))
|
html = html.replace("%JS_ON_LABEL%", _("on"))
|
||||||
html = html.replace("%JS_OFF_LABEL%", _("off"))
|
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
|
return html
|
||||||
|
|
||||||
@ -359,3 +375,9 @@ class IPMPVServer:
|
|||||||
is_muted = self.volume_control.is_muted()
|
is_muted = self.volume_control.is_muted()
|
||||||
return jsonify(volume=volume, muted=is_muted)
|
return jsonify(volume=volume, muted=is_muted)
|
||||||
return jsonify(error="Volume control not available"), 404
|
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)
|
||||||
|
|||||||
@ -471,6 +471,9 @@
|
|||||||
<button id="resolution-btn" class="control-button" onclick="toggleResolution()">
|
<button id="resolution-btn" class="control-button" onclick="toggleResolution()">
|
||||||
%RESOLUTION_LABEL%: <span id="resolution-state">%RESOLUTION%</span>
|
%RESOLUTION_LABEL%: <span id="resolution-state">%RESOLUTION%</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="refresh-ch-btn" class="control-button" onclick="refreshChannels()">
|
||||||
|
%REFRESH_CH_LABEL%
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
@ -672,6 +675,34 @@
|
|||||||
.then(() => window.location.reload())
|
.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
|
// Mobile-friendly toast notification
|
||||||
function showToast(message) {
|
function showToast(message) {
|
||||||
const toast = document.createElement('div');
|
const toast = document.createElement('div');
|
||||||
|
|||||||
1
utils.py
1
utils.py
@ -13,6 +13,7 @@ ipmpv_retroarch_cmd = os.environ.get("IPMPV_RETROARCH_CMD")
|
|||||||
m3u_url = os.environ.get('IPMPV_M3U_URL')
|
m3u_url = os.environ.get('IPMPV_M3U_URL')
|
||||||
hwdec = os.environ.get('IPMPV_HWDEC')
|
hwdec = os.environ.get('IPMPV_HWDEC')
|
||||||
ao = os.environ.get('IPMPV_AO')
|
ao = os.environ.get('IPMPV_AO')
|
||||||
|
update_interval = os.environ.get('IPMPV_UPDATE_INTERVAL')
|
||||||
|
|
||||||
def setup_environment():
|
def setup_environment():
|
||||||
"""Set up environment variables."""
|
"""Set up environment variables."""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user