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__
|
||||
ipmpv.log
|
||||
.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)
|
||||
60
server.py
60
server.py
@ -6,8 +6,9 @@ import re
|
||||
import subprocess
|
||||
import threading
|
||||
import flask
|
||||
from flask import request, jsonify, send_from_directory
|
||||
from utils import is_valid_url, change_resolution, get_current_resolution, is_wayland
|
||||
from flask import request, jsonify, send_from_directory, session, redirect, url_for
|
||||
from localization import localization, _
|
||||
from utils import is_valid_url, change_resolution, get_current_resolution, is_wayland, get_or_create_secret_key
|
||||
|
||||
class IPMPVServer:
|
||||
"""Flask server for IPMPV web interface."""
|
||||
@ -25,6 +26,7 @@ class IPMPVServer:
|
||||
self.ipmpv_retroarch_cmd = ipmpv_retroarch_cmd
|
||||
self.retroarch_p = None
|
||||
self.volume_control = volume_control
|
||||
self.app.secret_key = get_or_create_secret_key()
|
||||
|
||||
# Register routes
|
||||
self._register_routes()
|
||||
@ -36,6 +38,11 @@ class IPMPVServer:
|
||||
def _register_routes(self):
|
||||
"""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("/")
|
||||
def index():
|
||||
return self._handle_index()
|
||||
@ -120,7 +127,6 @@ 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
|
||||
@ -131,7 +137,9 @@ class IPMPVServer:
|
||||
# Create the channel groups HTML
|
||||
channel_groups_html = ""
|
||||
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:
|
||||
index = flat_channel_list.index(channel) # Get correct global index
|
||||
channel_groups_html += f'''
|
||||
@ -142,20 +150,48 @@ class IPMPVServer:
|
||||
'''
|
||||
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 = html.replace("%WELCOME_TEXT%", _("welcome_to_ipmpv"))
|
||||
html = html.replace("%CURRENT_CHANNEL_LABEL%", _("current_channel"))
|
||||
html = html.replace("%CURRENT_CHANNEL%",
|
||||
self.channels[self.player.current_index]['name']
|
||||
if self.player.current_index is not None else "None")
|
||||
self.channels[self.player.current_index]['name']
|
||||
if self.player.current_index is not None else "None")
|
||||
html = html.replace("%RETROARCH_STATE%",
|
||||
"ON" if self.retroarch_p and self.retroarch_p.poll() is None else "OFF")
|
||||
"ON" if self.retroarch_p and self.retroarch_p.poll() is None 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_STATE%", "ON" if self.player.deinterlace else "OFF")
|
||||
_("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("%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("%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
|
||||
|
||||
@ -164,7 +200,7 @@ class IPMPVServer:
|
||||
url = request.args.get("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.current_index = None
|
||||
|
||||
@ -304,7 +304,7 @@
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 28px;
|
||||
font-size: 48px;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
@ -422,13 +422,34 @@
|
||||
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>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="language-selector">
|
||||
<select onchange="changeLanguage(this.value)">
|
||||
%LANGUAGE_SELECTOR%
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h1>Welcome to IPMPV</h1>
|
||||
<p>Current Channel: <span id="current-channel">%CURRENT_CHANNEL%</span></p>
|
||||
<h1><i>%WELCOME_TEXT%</i></h1>
|
||||
<p>%CURRENT_CHANNEL_LABEL%: <span id="current-channel">%CURRENT_CHANNEL%</span></p>
|
||||
|
||||
<div class="section">
|
||||
<div class="dpad-container">
|
||||
@ -440,49 +461,49 @@
|
||||
</div>
|
||||
|
||||
<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()">
|
||||
<span id="retroarch-state">%RETROARCH_LABEL%</span>
|
||||
</button>
|
||||
<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 id="resolution-btn" class="control-button" onclick="toggleResolution()">
|
||||
Resolution: <span id="resolution-state">%RESOLUTION%</span>
|
||||
%RESOLUTION_LABEL%: <span id="resolution-state">%RESOLUTION%</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Volume</h2>
|
||||
<h2>%VOLUME_LABEL%</h2>
|
||||
<div class="osd-toggle">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Toggle OSD</h2>
|
||||
<h2>%TOGGLE_OSD_LABEL%</h2>
|
||||
<div class="osd-toggle">
|
||||
<button class="leftbtn" id="osd-on-btn" onclick="showOSD()">on</button>
|
||||
<button class="rightbtn" id="osd-off-btn" onclick="hideOSD()">off</button>
|
||||
<button class="leftbtn" id="osd-on-btn" onclick="showOSD()">%ON_LABEL%</button>
|
||||
<button class="rightbtn" id="osd-off-btn" onclick="hideOSD()">%OFF_LABEL%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Play Custom URL</h2>
|
||||
<h2>%PLAY_CUSTOM_URL_LABEL%</h2>
|
||||
<div class="url-input-container">
|
||||
<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()">
|
||||
<span id="latency-state">%LATENCY_LABEL%</span>
|
||||
</button>
|
||||
<button class="input-btn" onclick="playCustomURL()">Play</button>
|
||||
<button class="input-btn" onclick="playCustomURL()">%PLAY_LABEL%</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>All Channels</h2>
|
||||
<h2>%ALL_CHANNELS_LABEL%</h2>
|
||||
<div class="group-container">
|
||||
<!-- Channel groups will be inserted here -->
|
||||
%CHANNEL_GROUPS%
|
||||
@ -490,6 +511,11 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Function to change language
|
||||
function changeLanguage(language) {
|
||||
window.location.href = '/switch_language/' + language;
|
||||
}
|
||||
|
||||
// Improve mobile touch experience
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Add active/touch state for all buttons
|
||||
@ -514,7 +540,7 @@
|
||||
// Show loading indicator
|
||||
const playButton = document.querySelector('.input-btn');
|
||||
const originalText = playButton.textContent;
|
||||
playButton.textContent = "Loading...";
|
||||
playButton.textContent = "%LOADING%";
|
||||
playButton.disabled = true;
|
||||
|
||||
fetch(`/play_custom?url=${encodeURIComponent(url)}`)
|
||||
@ -525,15 +551,15 @@
|
||||
|
||||
if (data.success) {
|
||||
// Show toast instead of alert on mobile
|
||||
showToast("Now playing: " + url);
|
||||
showToast("%NOW_PLAYING%: " + url);
|
||||
} else {
|
||||
showToast("Error: " + data.error);
|
||||
showToast("%ERROR%: " + data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
playButton.textContent = originalText;
|
||||
playButton.disabled = false;
|
||||
showToast("Connection error. Please try again.");
|
||||
showToast("%CONNECTION_ERROR%");
|
||||
});
|
||||
}
|
||||
|
||||
@ -541,7 +567,7 @@
|
||||
fetch(`/toggle_latency`)
|
||||
.then(response => response.json())
|
||||
.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";
|
||||
});
|
||||
}
|
||||
@ -550,7 +576,7 @@
|
||||
fetch(`/toggle_retroarch`)
|
||||
.then(response => response.json())
|
||||
.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";
|
||||
});
|
||||
}
|
||||
@ -574,7 +600,7 @@
|
||||
btn.disabled = true;
|
||||
});
|
||||
|
||||
showToast("Loading channel...");
|
||||
showToast("%LOADING_CHANNEL%");
|
||||
|
||||
fetch(`/channel?index=${index}`)
|
||||
.then(() => window.location.reload())
|
||||
@ -582,7 +608,7 @@
|
||||
channelButtons.forEach(btn => {
|
||||
btn.disabled = false;
|
||||
});
|
||||
showToast("Error loading channel. Try again.");
|
||||
showToast("%ERROR_LOADING_CHANNEL%");
|
||||
});
|
||||
}
|
||||
|
||||
@ -590,7 +616,7 @@
|
||||
fetch(`/toggle_deinterlace`)
|
||||
.then(response => response.json())
|
||||
.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";
|
||||
});
|
||||
}
|
||||
@ -607,7 +633,7 @@
|
||||
fetch(`/volume_up`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast("Volume: " + data.volume + "%");
|
||||
showToast("%VOLUME_LEVEL%".replace("{0}", data.volume));
|
||||
});
|
||||
}
|
||||
|
||||
@ -615,7 +641,7 @@
|
||||
fetch(`/volume_down`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
showToast("Volume: " + data.volume + "%");
|
||||
showToast("%VOLUME_LEVEL%".replace("{0}", data.volume));
|
||||
});
|
||||
}
|
||||
|
||||
@ -623,19 +649,19 @@
|
||||
fetch(`/toggle_mute`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
muted = data.muted ? "yes" : "no";
|
||||
showToast("Muted: " + muted);
|
||||
muted = data.muted ? "%MUTED_YES%" : "%MUTED_NO%";
|
||||
showToast(muted);
|
||||
});
|
||||
}
|
||||
|
||||
function channelUp() {
|
||||
showToast("Loading channel...");
|
||||
showToast("%LOADING_CHANNEL%");
|
||||
fetch(`/channel_up`)
|
||||
.then(() => window.location.reload())
|
||||
}
|
||||
|
||||
function channelDown() {
|
||||
showToast("Loading channel...");
|
||||
showToast("%LOADING_CHANNEL%");
|
||||
fetch(`/channel_down`)
|
||||
.then(() => window.location.reload())
|
||||
}
|
||||
|
||||
31
utils.py
31
utils.py
@ -4,6 +4,7 @@
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import secrets
|
||||
|
||||
# Environment variables
|
||||
is_wayland = "WAYLAND_DISPLAY" in os.environ
|
||||
@ -98,3 +99,33 @@ def change_resolution(current_resolution):
|
||||
return get_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