Compare commits

...

7 Commits

Author SHA1 Message Date
659c6350d5 Merge pull request 'Dev' (#1) from Dev into main
Some checks failed
CI/CD Pipeline / test-and-lint (push) Failing after 26s
CI/CD Pipeline / publish-container (push) Has been skipped
Reviewed-on: #1
2026-02-01 18:18:50 +00:00
9cfada5b92 Add CI/CD pipeline configuration for testing and container publishing
Some checks failed
CI/CD Pipeline / test-and-lint (pull_request) Failing after 5m13s
CI/CD Pipeline / publish-container (pull_request) Has been skipped
2026-02-01 18:16:35 +00:00
4b9481d162 Renamed ci yml to yaml 2026-02-01 18:15:41 +00:00
5b1903087a Add configuration, main module, playlist manager, and server for M3U list generation 2026-02-01 18:12:25 +00:00
9446cd7589 Renamed project name 2026-02-01 18:12:08 +00:00
ce62dc7005 Cleaned devcontainer to avoid ssh agent conflicts 2026-02-01 18:11:30 +00:00
8d2d482c05 Add pydantic-settings and requests dependencies 2026-02-01 18:10:45 +00:00
9 changed files with 172 additions and 6 deletions

View File

@ -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"
}

View File

@ -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"

View 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()

View 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()

View 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)

View 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()

View File