generated from unai/python_boilerplate
main #10
@ -27,12 +27,6 @@
|
||||
"-v",
|
||||
"/dev:/dev"
|
||||
],
|
||||
"mounts": [
|
||||
"source=${localEnv:XDG_RUNTIME_DIR}/ssh-agent.socket,target=/tmp/ssh-agent.socket,type=bind"
|
||||
],
|
||||
"remoteEnv": {
|
||||
"SSH_AUTH_SOCK": "/tmp/ssh-agent.socket"
|
||||
},
|
||||
"remoteUser": "root",
|
||||
"postCreateCommand": "poetry install"
|
||||
}
|
||||
@ -8,6 +8,8 @@ packages = [{include = "my_project", from = "src"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.14"
|
||||
pydantic-settings = "^2.12.0"
|
||||
requests = "^2.32.5"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^9.0.2"
|
||||
|
||||
29
src/m3u_list_builder/config.py
Normal file
29
src/m3u_list_builder/config.py
Normal file
@ -0,0 +1,29 @@
|
||||
"""Configuración de la aplicación para generar listas M3U desde un servicio IPTV."""
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Configuración de la aplicación para generar listas M3U desde un servicio IPTV."""
|
||||
|
||||
# Variables obligatorias (sin default)
|
||||
host: str = Field(..., description="URL del host remoto (ej: http://iptv.com)")
|
||||
username: str = Field(..., description="Usuario del servicio IPTV")
|
||||
password: str = Field(..., description="Contraseña del servicio IPTV")
|
||||
|
||||
# Variables con valores por defecto
|
||||
port: int = Field(8080, description="Puerto del servidor local")
|
||||
update_interval: int = Field(
|
||||
3600, description="Intervalo de actualización en segundos"
|
||||
)
|
||||
output_file: str = Field("playlist.m3u", description="Nombre del archivo de salida")
|
||||
|
||||
# Configuración para leer de variables de entorno y archivo .env si existe
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env", env_file_encoding="utf-8", extra="ignore"
|
||||
)
|
||||
|
||||
|
||||
# Instancia única de configuración
|
||||
settings = Settings()
|
||||
37
src/m3u_list_builder/main.py
Normal file
37
src/m3u_list_builder/main.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
Módulo principal para iniciar el servicio de construcción de listas M3U."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from my_project.config import settings
|
||||
from my_project.playlist import PlaylistManager
|
||||
from my_project.server import run_server
|
||||
|
||||
# Configuración básica de logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
handlers=[logging.StreamHandler(sys.stdout)],
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
"""Función principal para iniciar el servicio."""
|
||||
logger = logging.getLogger("main")
|
||||
logger.info(f"Iniciando servicio para host: {settings.host}")
|
||||
|
||||
# 1. Hilo de actualización (Daemon para que muera si el main muere)
|
||||
manager = PlaylistManager()
|
||||
updater_thread = threading.Thread(
|
||||
target=manager.loop, daemon=True, name="UpdaterThread"
|
||||
)
|
||||
updater_thread.start()
|
||||
|
||||
# 2. Servidor Web (Hilo principal)
|
||||
run_server()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
75
src/m3u_list_builder/playlist.py
Normal file
75
src/m3u_list_builder/playlist.py
Normal file
@ -0,0 +1,75 @@
|
||||
"""Módulo para gestionar la generación y actualización de listas M3U."""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from my_project.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlaylistManager:
|
||||
"""Clase para gestionar la generación y actualización de listas M3U."""
|
||||
|
||||
def __init__(self):
|
||||
self.running = False
|
||||
|
||||
def fetch_and_generate(self):
|
||||
"""Descarga datos y regenera el archivo M3U."""
|
||||
logger.info("Iniciando actualización de playlist...")
|
||||
|
||||
url = f"{settings.host}/player_api.php"
|
||||
params = {
|
||||
"username": settings.username,
|
||||
"password": settings.password,
|
||||
"action": "get_live_streams",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, params=params, timeout=30)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
self._write_m3u(data)
|
||||
logger.info(
|
||||
f"Playlist actualizada exitosamente. Total canales: {len(data)}"
|
||||
)
|
||||
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Error de red al obtener playlist: {e}")
|
||||
except Exception as e:
|
||||
logger.exception(f"Error inesperado actualizando playlist: {e}")
|
||||
|
||||
def _write_m3u(self, channels: list):
|
||||
"""Escribe el archivo M3U en disco de forma atómica."""
|
||||
# Escribimos en un temporal y renombramos para evitar lecturas de archivo corrupto
|
||||
temp_file = Path(f"{settings.output_file}.tmp")
|
||||
final_file = Path(settings.output_file)
|
||||
|
||||
with open(temp_file, "w", encoding="utf-8") as f:
|
||||
f.write("#EXTM3U\n")
|
||||
for channel in channels:
|
||||
name = channel.get("name", "Unknown")
|
||||
stream_id = channel.get("stream_id")
|
||||
icon = channel.get("stream_icon", "")
|
||||
cat_id = channel.get("category_id", "")
|
||||
|
||||
# Construir URL directa
|
||||
stream_url = f"{settings.host}/live/{settings.username}/{settings.password}/{stream_id}.ts"
|
||||
|
||||
f.write(
|
||||
f'#EXTINF:-1 tvg-id="{name}" tvg-logo="{icon}" group-title="Cat_{cat_id}",{name}\n'
|
||||
)
|
||||
f.write(f"{stream_url}\n")
|
||||
|
||||
# Reemplazo atómico
|
||||
temp_file.replace(final_file)
|
||||
|
||||
def loop(self):
|
||||
"""Bucle infinito de actualización."""
|
||||
self.running = True
|
||||
while self.running:
|
||||
self.fetch_and_generate()
|
||||
time.sleep(settings.update_interval)
|
||||
29
src/m3u_list_builder/server.py
Normal file
29
src/m3u_list_builder/server.py
Normal file
@ -0,0 +1,29 @@
|
||||
import logging
|
||||
from functools import partial
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
from my_project.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run_server():
|
||||
"""Inicia el servidor HTTP bloqueante."""
|
||||
handler = partial(SimpleHTTPRequestHandler, directory=".")
|
||||
|
||||
# Truco: SimpleHTTPRequestHandler sirve el directorio actual,
|
||||
# asegurarse de que el CWD es correcto o mover el archivo a una carpeta 'public'
|
||||
# Para este ejemplo simple, asumimos que se ejecuta donde se genera el archivo.
|
||||
|
||||
server_address = ("", settings.port)
|
||||
httpd = ThreadingHTTPServer(server_address, SimpleHTTPRequestHandler)
|
||||
|
||||
logger.info(
|
||||
f"Servidor M3U activo en http://localhost:{settings.port}/{settings.output_file}"
|
||||
)
|
||||
try:
|
||||
httpd.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
httpd.server_close()
|
||||
Loading…
x
Reference in New Issue
Block a user