Added l10n, and cookie for that
This commit is contained in:
parent
24338557ee
commit
7bd77a4641
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
ipmpv.log
|
ipmpv.log
|
||||||
.venv
|
.venv
|
||||||
|
.secret_key
|
||||||
|
|||||||
31
locales/en.json
Normal file
31
locales/en.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"welcome_to_ipmpv": "welcome",
|
||||||
|
"current_channel": "Current channel",
|
||||||
|
"stop": "Stop",
|
||||||
|
"start_retroarch": "Start RetroArch",
|
||||||
|
"stop_retroarch": "Stop RetroArch",
|
||||||
|
"deinterlacing": "Deinterlacing",
|
||||||
|
"resolution": "Resolution",
|
||||||
|
"volume": "Volume",
|
||||||
|
"mute": "mute",
|
||||||
|
"toggle_osd": "Toggle OSD",
|
||||||
|
"on": "on",
|
||||||
|
"off": "off",
|
||||||
|
"play_custom_url": "Play Custom URL",
|
||||||
|
"enter_stream_url": "Enter stream URL",
|
||||||
|
"play": "Play",
|
||||||
|
"all_channels": "All Channels",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"now_playing": "Now playing",
|
||||||
|
"error": "Error",
|
||||||
|
"connection_error": "Connection error. Please try again.",
|
||||||
|
"loading_channel": "Loading channel...",
|
||||||
|
"error_loading_channel": "Error loading channel. Try again.",
|
||||||
|
"volume_level": "Volume: {0}%",
|
||||||
|
"muted_yes": "Muted: yes",
|
||||||
|
"muted_no": "Muted: no",
|
||||||
|
"latency_high": "Hi",
|
||||||
|
"latency_low": "Lo",
|
||||||
|
"other": "Other",
|
||||||
|
"invalid_url": "Invalid or unsupported URL"
|
||||||
|
}
|
||||||
31
locales/es.json
Normal file
31
locales/es.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"welcome_to_ipmpv": "bienvenido/a",
|
||||||
|
"current_channel": "Canal actual",
|
||||||
|
"stop": "Detener",
|
||||||
|
"start_retroarch": "Iniciar RetroArch",
|
||||||
|
"stop_retroarch": "Detener RetroArch",
|
||||||
|
"deinterlacing": "Desentrelazado",
|
||||||
|
"resolution": "Resolución",
|
||||||
|
"volume": "Volumen",
|
||||||
|
"mute": "mute",
|
||||||
|
"toggle_osd": "Alternar OSD",
|
||||||
|
"on": "sí",
|
||||||
|
"off": "no",
|
||||||
|
"play_custom_url": "Reproducir URL Personalizada",
|
||||||
|
"enter_stream_url": "Introduzca URL de la transmisión",
|
||||||
|
"play": "Reproducir",
|
||||||
|
"all_channels": "Todos los Canales",
|
||||||
|
"loading": "Cargando...",
|
||||||
|
"now_playing": "Reproduciendo ahora",
|
||||||
|
"error": "Error",
|
||||||
|
"connection_error": "Error de conexión. Por favor intente de nuevo.",
|
||||||
|
"loading_channel": "Cargando canal...",
|
||||||
|
"error_loading_channel": "Error al cargar el canal. Intente de nuevo.",
|
||||||
|
"volume_level": "Volumen: {0}%",
|
||||||
|
"muted_yes": "Silencio: sí",
|
||||||
|
"muted_no": "Silencio: no",
|
||||||
|
"latency_high": "Hi",
|
||||||
|
"latency_low": "Lo",
|
||||||
|
"other": "Otro",
|
||||||
|
"invalid_url": "URL inválida o no soportada"
|
||||||
|
}
|
||||||
88
localization.py
Normal file
88
localization.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
from flask import request, session
|
||||||
|
|
||||||
|
class Localization:
|
||||||
|
"""Handles localization for IPMPV"""
|
||||||
|
|
||||||
|
def __init__(self, default_language='en'):
|
||||||
|
"""Initialize the localization system"""
|
||||||
|
self.default_language = default_language
|
||||||
|
self.translations = {}
|
||||||
|
self.available_languages = []
|
||||||
|
self._load_translations()
|
||||||
|
|
||||||
|
def _load_translations(self):
|
||||||
|
"""Load all translation files from the locales directory"""
|
||||||
|
locales_dir = os.path.join(os.path.dirname(__file__), 'locales')
|
||||||
|
|
||||||
|
# Create locales directory if it doesn't exist
|
||||||
|
if not os.path.exists(locales_dir):
|
||||||
|
os.makedirs(locales_dir)
|
||||||
|
|
||||||
|
# Load each translation file
|
||||||
|
for filename in os.listdir(locales_dir):
|
||||||
|
if filename.endswith('.json'):
|
||||||
|
language_code = filename.split('.')[0]
|
||||||
|
self.available_languages.append(language_code)
|
||||||
|
|
||||||
|
with open(os.path.join(locales_dir, filename), 'r', encoding='utf-8') as f:
|
||||||
|
self.translations[language_code] = json.load(f)
|
||||||
|
|
||||||
|
# If no translations are available, create an empty one for the default language
|
||||||
|
if not self.translations:
|
||||||
|
self.available_languages.append(self.default_language)
|
||||||
|
self.translations[self.default_language] = {}
|
||||||
|
|
||||||
|
def get_language(self):
|
||||||
|
"""Get the current language based on session or browser settings"""
|
||||||
|
# Check session first
|
||||||
|
if 'language' in session:
|
||||||
|
lang = session['language']
|
||||||
|
if lang in self.available_languages:
|
||||||
|
return lang
|
||||||
|
|
||||||
|
# Check browser Accept-Language header
|
||||||
|
if request.accept_languages:
|
||||||
|
for lang in request.accept_languages.values():
|
||||||
|
if lang[:2] in self.available_languages:
|
||||||
|
session['language'] = lang[:2]
|
||||||
|
return lang[:2]
|
||||||
|
|
||||||
|
# Fallback to default language
|
||||||
|
return self.default_language
|
||||||
|
|
||||||
|
def set_language(self, language_code):
|
||||||
|
"""Set the current language"""
|
||||||
|
if language_code in self.available_languages:
|
||||||
|
session['language'] = language_code
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def translate(self, key, language=None):
|
||||||
|
"""Translate a key to the specified or current language"""
|
||||||
|
if language is None:
|
||||||
|
language = self.get_language()
|
||||||
|
|
||||||
|
# If the language doesn't exist, use default
|
||||||
|
if language not in self.translations:
|
||||||
|
language = self.default_language
|
||||||
|
|
||||||
|
# If the key exists in the language, return the translation
|
||||||
|
if key in self.translations[language]:
|
||||||
|
return self.translations[language][key]
|
||||||
|
|
||||||
|
# If not found in the current language, try the default language
|
||||||
|
if language != self.default_language and key in self.translations[self.default_language]:
|
||||||
|
return self.translations[self.default_language][key]
|
||||||
|
|
||||||
|
# If still not found, return the key itself
|
||||||
|
return key
|
||||||
|
|
||||||
|
# Create a global localization instance
|
||||||
|
localization = Localization()
|
||||||
|
|
||||||
|
# Helper function to use in templates
|
||||||
|
def _(key):
|
||||||
|
"""Shorthand for translating a key"""
|
||||||
|
return localization.translate(key)
|
||||||
74
server.py
74
server.py
@ -6,8 +6,9 @@ import re
|
|||||||
import subprocess
|
import subprocess
|
||||||
import threading
|
import threading
|
||||||
import flask
|
import flask
|
||||||
from flask import request, jsonify, send_from_directory
|
from flask import request, jsonify, send_from_directory, session, redirect, url_for
|
||||||
from utils import is_valid_url, change_resolution, get_current_resolution, is_wayland
|
from localization import localization, _
|
||||||
|
from utils import is_valid_url, change_resolution, get_current_resolution, is_wayland, get_or_create_secret_key
|
||||||
|
|
||||||
class IPMPVServer:
|
class IPMPVServer:
|
||||||
"""Flask server for IPMPV web interface."""
|
"""Flask server for IPMPV web interface."""
|
||||||
@ -25,6 +26,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.app.secret_key = get_or_create_secret_key()
|
||||||
|
|
||||||
# Register routes
|
# Register routes
|
||||||
self._register_routes()
|
self._register_routes()
|
||||||
@ -36,6 +38,11 @@ class IPMPVServer:
|
|||||||
def _register_routes(self):
|
def _register_routes(self):
|
||||||
"""Register Flask routes."""
|
"""Register Flask routes."""
|
||||||
|
|
||||||
|
@self.app.route("/switch_language/<language>")
|
||||||
|
def switch_language(language):
|
||||||
|
localization.set_language(language)
|
||||||
|
return redirect(request.referrer or url_for('index'))
|
||||||
|
|
||||||
@self.app.route("/")
|
@self.app.route("/")
|
||||||
def index():
|
def index():
|
||||||
return self._handle_index()
|
return self._handle_index()
|
||||||
@ -120,18 +127,19 @@ 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
|
||||||
|
|
||||||
grouped_channels = group_channels(self.channels)
|
grouped_channels = group_channels(self.channels)
|
||||||
flat_channel_list = [channel for channel in self.channels]
|
flat_channel_list = [channel for channel in self.channels]
|
||||||
|
|
||||||
# Create the channel groups HTML
|
# Create the channel groups HTML
|
||||||
channel_groups_html = ""
|
channel_groups_html = ""
|
||||||
for group, ch_list in grouped_channels.items():
|
for group, ch_list in grouped_channels.items():
|
||||||
channel_groups_html += f'<div class="group">{group}'
|
# Translate group name if it's a common group
|
||||||
|
translated_group = _(group.lower()) if group.lower() in ["other"] else group
|
||||||
|
channel_groups_html += f'<div class="group">{translated_group}'
|
||||||
for channel in ch_list:
|
for channel in ch_list:
|
||||||
index = flat_channel_list.index(channel) # Get correct global index
|
index = flat_channel_list.index(channel) # Get correct global index
|
||||||
channel_groups_html += f'''
|
channel_groups_html += f'''
|
||||||
@ -141,22 +149,50 @@ class IPMPVServer:
|
|||||||
</div>
|
</div>
|
||||||
'''
|
'''
|
||||||
channel_groups_html += '</div>'
|
channel_groups_html += '</div>'
|
||||||
|
|
||||||
# Replace placeholders with actual values
|
# Get current language and available languages for language selector
|
||||||
|
current_language = localization.get_language()
|
||||||
|
languages = {
|
||||||
|
'en': 'English',
|
||||||
|
'es': 'Español'
|
||||||
|
# Add more languages here as you support them
|
||||||
|
}
|
||||||
|
|
||||||
|
language_selector_html = ""
|
||||||
|
for code, name in languages.items():
|
||||||
|
selected = ' selected' if code == current_language else ''
|
||||||
|
language_selector_html += f'<option value="{code}"{selected}>{name}</option>'
|
||||||
|
|
||||||
|
# Replace placeholders with actual values, using translation
|
||||||
html = open("templates/index.html").read()
|
html = open("templates/index.html").read()
|
||||||
html = html.replace("%CURRENT_CHANNEL%",
|
html = html.replace("%WELCOME_TEXT%", _("welcome_to_ipmpv"))
|
||||||
self.channels[self.player.current_index]['name']
|
html = html.replace("%CURRENT_CHANNEL_LABEL%", _("current_channel"))
|
||||||
if self.player.current_index is not None else "None")
|
html = html.replace("%CURRENT_CHANNEL%",
|
||||||
html = html.replace("%RETROARCH_STATE%",
|
self.channels[self.player.current_index]['name']
|
||||||
"ON" if self.retroarch_p and self.retroarch_p.poll() is None else "OFF")
|
if self.player.current_index is not None else "None")
|
||||||
html = html.replace("%RETROARCH_LABEL%",
|
html = html.replace("%RETROARCH_STATE%",
|
||||||
"Stop RetroArch" if self.retroarch_p and self.retroarch_p.poll() is None else "Start RetroArch")
|
"ON" if self.retroarch_p and self.retroarch_p.poll() is None else "OFF")
|
||||||
html = html.replace("%DEINTERLACE_STATE%", "ON" if self.player.deinterlace else "OFF")
|
html = html.replace("%RETROARCH_LABEL%",
|
||||||
|
_("stop_retroarch") if self.retroarch_p and self.retroarch_p.poll() is None else _("start_retroarch"))
|
||||||
|
html = html.replace("%DEINTERLACE_LABEL%", _("deinterlacing"))
|
||||||
|
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("%RESOLUTION%", self.resolution)
|
||||||
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%", "Lo" if self.player.low_latency else "Hi")
|
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)
|
||||||
|
html = html.replace("%VOLUME_LABEL%", _("volume"))
|
||||||
|
html = html.replace("%MUTE_LABEL%", _("mute"))
|
||||||
|
html = html.replace("%TOGGLE_OSD_LABEL%", _("toggle_osd"))
|
||||||
|
html = html.replace("%ON_LABEL%", _("on"))
|
||||||
|
html = html.replace("%OFF_LABEL%", _("off"))
|
||||||
|
html = html.replace("%PLAY_CUSTOM_URL_LABEL%", _("play_custom_url"))
|
||||||
|
html = html.replace("%ENTER_URL_PLACEHOLDER%", _("enter_stream_url"))
|
||||||
|
html = html.replace("%PLAY_LABEL%", _("play"))
|
||||||
|
html = html.replace("%ALL_CHANNELS_LABEL%", _("all_channels"))
|
||||||
|
html = html.replace("%STOP_LABEL%", _("stop"))
|
||||||
|
html = html.replace("%LANGUAGE_SELECTOR%", language_selector_html)
|
||||||
|
|
||||||
return html
|
return html
|
||||||
|
|
||||||
def _handle_play_custom(self):
|
def _handle_play_custom(self):
|
||||||
@ -164,7 +200,7 @@ class IPMPVServer:
|
|||||||
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_url"))
|
||||||
|
|
||||||
self.player.player.loadfile(url)
|
self.player.player.loadfile(url)
|
||||||
self.player.current_index = None
|
self.player.current_index = None
|
||||||
|
|||||||
@ -304,7 +304,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 28px;
|
font-size: 48px;
|
||||||
margin: 15px 0;
|
margin: 15px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -422,13 +422,34 @@
|
|||||||
padding: 14px;
|
padding: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.language-selector {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector select {
|
||||||
|
background-color: var(--secondary-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
<div class="language-selector">
|
||||||
|
<select onchange="changeLanguage(this.value)">
|
||||||
|
%LANGUAGE_SELECTOR%
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Welcome to IPMPV</h1>
|
<h1><i>%WELCOME_TEXT%</i></h1>
|
||||||
<p>Current Channel: <span id="current-channel">%CURRENT_CHANNEL%</span></p>
|
<p>%CURRENT_CHANNEL_LABEL%: <span id="current-channel">%CURRENT_CHANNEL%</span></p>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="dpad-container">
|
<div class="dpad-container">
|
||||||
@ -440,49 +461,49 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="control-button" onclick="stopPlayer()">Stop</button>
|
<button class="control-button" onclick="stopPlayer()">%STOP_LABEL%</button>
|
||||||
<button id="retroarch-btn" class="%RETROARCH_STATE%" onclick="toggleRetroArch()">
|
<button id="retroarch-btn" class="%RETROARCH_STATE%" onclick="toggleRetroArch()">
|
||||||
<span id="retroarch-state">%RETROARCH_LABEL%</span>
|
<span id="retroarch-state">%RETROARCH_LABEL%</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="deinterlace-btn" class="%DEINTERLACE_STATE%" onclick="toggleDeinterlace()">
|
<button id="deinterlace-btn" class="%DEINTERLACE_STATE%" onclick="toggleDeinterlace()">
|
||||||
Deinterlacing: <span id="deinterlace-state">%DEINTERLACE_STATE%</span>
|
%DEINTERLACE_LABEL%: <span id="deinterlace-state">%DEINTERLACE_STATE%</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="resolution-btn" class="control-button" onclick="toggleResolution()">
|
<button id="resolution-btn" class="control-button" onclick="toggleResolution()">
|
||||||
Resolution: <span id="resolution-state">%RESOLUTION%</span>
|
%RESOLUTION_LABEL%: <span id="resolution-state">%RESOLUTION%</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Volume</h2>
|
<h2>%VOLUME_LABEL%</h2>
|
||||||
<div class="osd-toggle">
|
<div class="osd-toggle">
|
||||||
<button class="leftbtn" id="vol-up-btn" onclick="volumeDown()">-</button>
|
<button class="leftbtn" id="vol-up-btn" onclick="volumeDown()">-</button>
|
||||||
<button class="midbtn %MUTE_STATE%" id="vol-mute-btn" onclick="toggleMute()">mute</button>
|
<button class="midbtn %MUTE_STATE%" id="vol-mute-btn" onclick="toggleMute()">%MUTE_LABEL%</button>
|
||||||
<button class="rightbtn" id="vol-dn-btn" onclick="volumeUp()">+</button>
|
<button class="rightbtn" id="vol-dn-btn" onclick="volumeUp()">+</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Toggle OSD</h2>
|
<h2>%TOGGLE_OSD_LABEL%</h2>
|
||||||
<div class="osd-toggle">
|
<div class="osd-toggle">
|
||||||
<button class="leftbtn" id="osd-on-btn" onclick="showOSD()">on</button>
|
<button class="leftbtn" id="osd-on-btn" onclick="showOSD()">%ON_LABEL%</button>
|
||||||
<button class="rightbtn" id="osd-off-btn" onclick="hideOSD()">off</button>
|
<button class="rightbtn" id="osd-off-btn" onclick="hideOSD()">%OFF_LABEL%</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>Play Custom URL</h2>
|
<h2>%PLAY_CUSTOM_URL_LABEL%</h2>
|
||||||
<div class="url-input-container">
|
<div class="url-input-container">
|
||||||
<div class="url-input-group">
|
<div class="url-input-group">
|
||||||
<input type="text" id="custom-url" class="custom-url-input" placeholder="Enter stream URL">
|
<input type="text" id="custom-url" class="custom-url-input" placeholder="%ENTER_URL_PLACEHOLDER%">
|
||||||
<button id="latency-btn" class="%LATENCY_STATE%" onclick="toggleLatency()">
|
<button id="latency-btn" class="%LATENCY_STATE%" onclick="toggleLatency()">
|
||||||
<span id="latency-state">%LATENCY_LABEL%</span>
|
<span id="latency-state">%LATENCY_LABEL%</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="input-btn" onclick="playCustomURL()">Play</button>
|
<button class="input-btn" onclick="playCustomURL()">%PLAY_LABEL%</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2>All Channels</h2>
|
<h2>%ALL_CHANNELS_LABEL%</h2>
|
||||||
<div class="group-container">
|
<div class="group-container">
|
||||||
<!-- Channel groups will be inserted here -->
|
<!-- Channel groups will be inserted here -->
|
||||||
%CHANNEL_GROUPS%
|
%CHANNEL_GROUPS%
|
||||||
@ -490,6 +511,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Function to change language
|
||||||
|
function changeLanguage(language) {
|
||||||
|
window.location.href = '/switch_language/' + language;
|
||||||
|
}
|
||||||
|
|
||||||
// Improve mobile touch experience
|
// Improve mobile touch experience
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Add active/touch state for all buttons
|
// Add active/touch state for all buttons
|
||||||
@ -514,7 +540,7 @@
|
|||||||
// Show loading indicator
|
// Show loading indicator
|
||||||
const playButton = document.querySelector('.input-btn');
|
const playButton = document.querySelector('.input-btn');
|
||||||
const originalText = playButton.textContent;
|
const originalText = playButton.textContent;
|
||||||
playButton.textContent = "Loading...";
|
playButton.textContent = "%LOADING%";
|
||||||
playButton.disabled = true;
|
playButton.disabled = true;
|
||||||
|
|
||||||
fetch(`/play_custom?url=${encodeURIComponent(url)}`)
|
fetch(`/play_custom?url=${encodeURIComponent(url)}`)
|
||||||
@ -525,15 +551,15 @@
|
|||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
// Show toast instead of alert on mobile
|
// Show toast instead of alert on mobile
|
||||||
showToast("Now playing: " + url);
|
showToast("%NOW_PLAYING%: " + url);
|
||||||
} else {
|
} else {
|
||||||
showToast("Error: " + data.error);
|
showToast("%ERROR%: " + data.error);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
playButton.textContent = originalText;
|
playButton.textContent = originalText;
|
||||||
playButton.disabled = false;
|
playButton.disabled = false;
|
||||||
showToast("Connection error. Please try again.");
|
showToast("%CONNECTION_ERROR%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -541,7 +567,7 @@
|
|||||||
fetch(`/toggle_latency`)
|
fetch(`/toggle_latency`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById("latency-state").textContent = data.state ? "Lo" : "Hi";
|
document.getElementById("latency-state").textContent = data.state ? "%LATENCY_LOW%" : "%LATENCY_HIGH%";
|
||||||
document.getElementById("latency-btn").className = data.state ? "ON" : "OFF";
|
document.getElementById("latency-btn").className = data.state ? "ON" : "OFF";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -550,7 +576,7 @@
|
|||||||
fetch(`/toggle_retroarch`)
|
fetch(`/toggle_retroarch`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById("retroarch-state").textContent = data.state ? "Stop RetroArch" : "Start RetroArch";
|
document.getElementById("retroarch-state").textContent = data.state ? "%STOP_RETROARCH%" : "%START_RETROARCH%";
|
||||||
document.getElementById("retroarch-btn").className = data.state ? "ON" : "OFF";
|
document.getElementById("retroarch-btn").className = data.state ? "ON" : "OFF";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -574,7 +600,7 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
showToast("Loading channel...");
|
showToast("%LOADING_CHANNEL%");
|
||||||
|
|
||||||
fetch(`/channel?index=${index}`)
|
fetch(`/channel?index=${index}`)
|
||||||
.then(() => window.location.reload())
|
.then(() => window.location.reload())
|
||||||
@ -582,7 +608,7 @@
|
|||||||
channelButtons.forEach(btn => {
|
channelButtons.forEach(btn => {
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
});
|
});
|
||||||
showToast("Error loading channel. Try again.");
|
showToast("%ERROR_LOADING_CHANNEL%");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -590,7 +616,7 @@
|
|||||||
fetch(`/toggle_deinterlace`)
|
fetch(`/toggle_deinterlace`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
document.getElementById("deinterlace-state").textContent = data.state ? "ON" : "OFF";
|
document.getElementById("deinterlace-state").textContent = data.state ? "%ON_LABEL%" : "%OFF_LABEL%";
|
||||||
document.getElementById("deinterlace-btn").className = data.state ? "ON" : "OFF";
|
document.getElementById("deinterlace-btn").className = data.state ? "ON" : "OFF";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -607,7 +633,7 @@
|
|||||||
fetch(`/volume_up`)
|
fetch(`/volume_up`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
showToast("Volume: " + data.volume + "%");
|
showToast("%VOLUME_LEVEL%".replace("{0}", data.volume));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -615,7 +641,7 @@
|
|||||||
fetch(`/volume_down`)
|
fetch(`/volume_down`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
showToast("Volume: " + data.volume + "%");
|
showToast("%VOLUME_LEVEL%".replace("{0}", data.volume));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -623,19 +649,19 @@
|
|||||||
fetch(`/toggle_mute`)
|
fetch(`/toggle_mute`)
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
muted = data.muted ? "yes" : "no";
|
muted = data.muted ? "%MUTED_YES%" : "%MUTED_NO%";
|
||||||
showToast("Muted: " + muted);
|
showToast(muted);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelUp() {
|
function channelUp() {
|
||||||
showToast("Loading channel...");
|
showToast("%LOADING_CHANNEL%");
|
||||||
fetch(`/channel_up`)
|
fetch(`/channel_up`)
|
||||||
.then(() => window.location.reload())
|
.then(() => window.location.reload())
|
||||||
}
|
}
|
||||||
|
|
||||||
function channelDown() {
|
function channelDown() {
|
||||||
showToast("Loading channel...");
|
showToast("%LOADING_CHANNEL%");
|
||||||
fetch(`/channel_down`)
|
fetch(`/channel_down`)
|
||||||
.then(() => window.location.reload())
|
.then(() => window.location.reload())
|
||||||
}
|
}
|
||||||
|
|||||||
33
utils.py
33
utils.py
@ -4,6 +4,7 @@
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import secrets
|
||||||
|
|
||||||
# Environment variables
|
# Environment variables
|
||||||
is_wayland = "WAYLAND_DISPLAY" in os.environ
|
is_wayland = "WAYLAND_DISPLAY" in os.environ
|
||||||
@ -96,5 +97,35 @@ def change_resolution(current_resolution):
|
|||||||
xrandr_env["DISPLAY"] = ":0"
|
xrandr_env["DISPLAY"] = ":0"
|
||||||
subprocess.run(["xrandr", "--output", "Composite-1", "--mode", new_res], check=False, env=xrandr_env)
|
subprocess.run(["xrandr", "--output", "Composite-1", "--mode", new_res], check=False, env=xrandr_env)
|
||||||
return get_current_resolution()
|
return get_current_resolution()
|
||||||
|
|
||||||
return current_resolution
|
return current_resolution
|
||||||
|
|
||||||
|
def get_or_create_secret_key():
|
||||||
|
"""
|
||||||
|
Get the secret key from a file or create a new one if it doesn't exist.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The secret key
|
||||||
|
"""
|
||||||
|
key_file = os.path.join(os.path.dirname(__file__), '.secret_key')
|
||||||
|
|
||||||
|
# If the file exists, read the key
|
||||||
|
if os.path.exists(key_file):
|
||||||
|
with open(key_file, 'r') as f:
|
||||||
|
return f.read().strip()
|
||||||
|
|
||||||
|
# Otherwise, generate a new key and save it
|
||||||
|
new_key = secrets.token_hex(16) # 32-character hex string
|
||||||
|
|
||||||
|
# Create the file with restricted permissions (readable only by the owner)
|
||||||
|
try:
|
||||||
|
# This works on Unix-like systems (Linux, macOS)
|
||||||
|
with open(key_file, 'w') as f:
|
||||||
|
f.write(new_key)
|
||||||
|
os.chmod(key_file, 0o600) # Owner can read/write, no one else can access
|
||||||
|
except:
|
||||||
|
# Simplified approach for Windows or if chmod fails
|
||||||
|
with open(key_file, 'w') as f:
|
||||||
|
f.write(new_key)
|
||||||
|
|
||||||
|
return new_key
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user