Inicio Apuntes FPApuntes DAMAcceso a Datos Organizador de listas M3U. Gestión y reproducción con facilidad

Organizador de listas M3U. Gestión y reproducción con facilidad

Un pequeño proyecto Python disponible como .DEB y .EXE

Publicado por entreunosyceros

Una vez más aquí. Hoy vengo a dejar un pequeño programa que puede ser útil para algunas personas hoy en día. En la era del streaming, las listas M3U se han convertido en una herramienta para poder disfrutar de mucho contenido (siempre de manera lega, ¡claro está!. Sin embargo, la gestión de estas listas puede ser tediosa y confusa para muchos, especialmente cuando las listas contienen cientos de enlaces. Para poner mi granito de arena en este problema, he desarrollado una aplicación que puede facilitar la gestión y reproducción de listas M3U. Se trata de un organizador de listas M3U, muy fácil de utilizar.

En este artículo vamos a echar un pequeño vistazo al proceso de desarrollo de la aplicación, incluyendo las funcionalidades principales, la integración de VLC como reproductor multimedia y el repositorio en GitHub donde se pueden encontrar el ejecutable para Windows y el archivo .DEB para Ubuntu …y demás.

El organizador de listas M3U (0rgan1zat0r)

Organizador de listas M3U y listas M3U8

Este pequeño proyecto ha sido desarrollado con Python 3, por lo que es necesario disponer de Python instalado para que funcione. Todo el proyecto lo he dividido en 6 archivos con el código necesario y un archivo .JSON en el que se van a guardar las URL que quiera conservar el usuario.

El código, como digo, se divide en varios archivos. A continuación vamos a ir viéndolos uno a uno para que quede claro que es lo que hace el código y el programa:

M3UOrgan1zat0r.py. El punto de entrada de este organizador de listas m3u

El archivo M3UOrgan1zat0r.py es el punto de entrada principal de la aplicación M3U Organ1zat0r. Este archivo se encarga de inicializar la aplicación, configurar la ventana principal, y gestionar la interfaz de este organizador de listas m3u.

import sys
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QIcon
from pathlib import Path
from organizadorm3u import M3UOrganizer

# Directorio del script actual
current_directory = Path(__file__).parent

if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWin = M3UOrganizer()
    mainWin.resize(800, 600)
    # Establecer un icono personalizado
    icon_path = current_directory / './resources/ordenar-m3u.png'
    if icon_path.exists():
        app.setWindowIcon(QIcon(str(icon_path)))

    mainWin.show()
    sys.exit(app.exec_())

Para empezar, vamos a importar los módulos necesarios para que todo funcione:

  • sys: Este módulo proporciona acceso a variables y funciones que interactúan fuertemente con el intérprete de Python.
  • PyQt5.QtWidgets.QApplication: Gestiona la aplicación y su ciclo de eventos. Es esencial para cualquier aplicación que utilice una GUI.
  • PyQt5.QtGui.QIcon: Permite establecer iconos personalizados para las ventanas de la aplicación.
  • pathlib.Path: Facilita la manipulación de rutas de archivos de manera independiente al sistema operativo.
  • organizador_m3u.M3UOrganizer: Esta es la clase principal que define la funcionalidad y la interfaz del organizador de listas M3U. Se importa desde el archivo organizador_m3u.py.

current_directory: Este fragmento de código determina el directorio en el que se encuentra el script actual (M3UOrgan1zat0r.py). Esto es útil para construir rutas relativas.

if __name__ == '__main__':: Esta línea asegura que el código dentro de este bloque solo se ejecutará si el script se ejecuta directamente (no si es importado como un módulo en otro script).

app = QApplication(sys.argv): Se crea una instancia de QApplication, que es necesaria para ejecutar cualquier aplicación basada en PyQt5. sys.argv permite que la aplicación acepte argumentos de la línea de comandos.

mainWin = M3UOrganizer(): Aquí se crea la instancia de la ventana principal de la aplicación, utilizando la clase M3UOrganizer, que define toda la lógica y la interfaz de usuario.

mainWin.resize(800, 600): Se ajusta el tamaño de la ventana principal a 800×600 píxeles.

icon_path = current_directory: nos servirá para establecer el icono de la aplicación.

mainWin.show(): Esta línea muestra la ventana principal de la aplicación en pantalla.

sys.exit(app.exec_()): app.exec_() inicia el ciclo de eventos de la aplicación, que es lo que mantiene la ventana abierta y responde a las interacciones del usuario. sys.exit() asegura que la aplicación se cierre completamente cuando se cierra la ventana.

Organizadorm3u.py

Este módulo define la clase M3UOrganizer, que sirve para crear este organizador de listas m3u como una aplicación de escritorio PyQt5, la cual he diseñado para cargar, filtrar, ordenar y gestionar archivos de listas de reproducción M3U. El programa permite al usuario cargar archivos M3U desde el sistema de archivos local o desde una URL, editar y organizar los canales listados, y guardarlos en un nuevo archivo M3U. También incluye integración con VLC para previsualizar streams de vídeo directamente desde la aplicación. Todo ello teniendo el siguiente código como base:

import os
from PyQt5.QtWidgets import QMainWindow,  QTextEdit, QHBoxLayout, QWidget, QAction, QVBoxLayout, QFileDialog, QMessageBox, QInputDialog,  QProgressDialog, QSystemTrayIcon, QMenu, QPushButton, QComboBox, QLabel, QLineEdit
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QTextCharFormat, QBrush, QColor, QIcon
from pathlib import Path
from optionsmenu import show_about_dialog, show_how_to_use_dialog, open_github_url, abrir_vpn, restore_window, show_about_dialog
from actions import copy_selection, paste_selection, show_context_menu, open_with_vlc, handle_double_click, guardar_url, ver_urls_guardadas
from threads import LoadFileThread, SearchThread
import requests  # Importa la librería requests para realizar la descarga
import logging # Para el manejo de advertencias y errores
import vlc
import re
from actions import VideoDialog 

# Directorio del script actual
current_directory = Path(__file__).parent


class M3UOrganizer(QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        
        try:
            # Inicializar la instancia de VLC y el reproductor de medios
            self.instance = vlc.Instance()
            self.media_player = self.instance.media_player_new()

            if not self.media_player:
                raise Exception("Error al inicializar el reproductor de medios VLC.")

        except Exception as e:
            QMessageBox.critical(self, "Error", f"Error al inicializar VLC: {str(e)}")
            self.media_player = None

        self.initUI()
        self.threads = []  # Inicializa el atributo threads
        self.temp_file_path = None  # Añade un atributo para la ruta del archivo temporal
        self.original_content = []  # Almacena el contenido original sin filtrar ni ordenar

        
    def initUI(self):
        # Establecer un icono personalizado
        icon_path = current_directory / './resources/ordenar-m3u.png'
        if icon_path.exists():
            self.setWindowIcon(QIcon(str(icon_path)))
            self.tray_icon = QSystemTrayIcon(QIcon(str(icon_path)), self)
            self.tray_icon.setToolTip("Organizador m3u")

            # Crear un menú para el icono de la bandeja del sistema
            tray_menu = QMenu(self)
            
            # Acción para abrir la suscripción de VPN
            vpn_action = QAction("30 días gratis de VPN", self)
            vpn_action.triggered.connect(lambda: abrir_vpn(self))
            tray_menu.addAction(vpn_action)
            
            # Acción para restaurar la ventana principal
            restore_action = QAction("Restaurar", self)
            restore_action.triggered.connect(lambda: restore_window(self))
            tray_menu.addAction(restore_action)
            
            # Acción para abrir la ventana "Acerca de"
            about_action = QAction("Acerca de", self)
            about_action.triggered.connect(lambda: show_about_dialog(self))
            tray_menu.addAction(about_action)
            
            
            # Acción para salir de la aplicación
            exit_action = QAction("Salir", self)
            exit_action.triggered.connect(self.close)
            tray_menu.addAction(exit_action)

            # Configurar el menú de la bandeja
            self.tray_icon.setContextMenu(tray_menu)
            self.tray_icon.show()

        else:
            logging.warning(f"Icono no encontrado en {icon_path}")
            
        self.loaded_lines = []  # Inicializar lista para acumular líneas cargadas
        self.original_lines = []  # Para almacenar la lista original sin filtrar/ordenar
        self.setWindowTitle('M3U 0rgan1zat0r')

        # Crear los widgets
        self.text_left = QTextEdit()
        self.text_right = QTextEdit()
        

        # Hacer que ambos cuadros de texto acepten arrastrar y soltar
        self.text_left.setAcceptDrops(True)
        self.text_right.setAcceptDrops(True)

        # Crear los botones de filtrado, ordenación y reseteo
        filter_label = QLabel("Filtrar:")
        self.filter_input = QLineEdit()
        filter_button = QPushButton("Aplicar")
        filter_button.clicked.connect(self.filter_list)

        sort_label = QLabel("Ordenar:")
        self.sort_selector = QComboBox()
        self.sort_selector.addItems([
            'Nombre del Canal (A-Z)', 
            'Nombre del Canal (Z-A)', 
            'Group-title (A-Z)', 
            'Group-title (Z-A)'
        ])
        sort_button = QPushButton("Aplicar")
        sort_button.clicked.connect(self.sort_list)

        reset_button = QPushButton("Restablecer")
        reset_button.clicked.connect(self.reset_list)

        # Layout superior con los botones de filtro, ordenación y reseteo
        top_layout = QHBoxLayout()
        top_layout.addWidget(filter_label)
        top_layout.addWidget(self.filter_input)
        top_layout.addWidget(filter_button)
        top_layout.addWidget(sort_label)
        top_layout.addWidget(self.sort_selector)
        top_layout.addWidget(sort_button)
        top_layout.addWidget(reset_button)

        # Layout principal con los textos
        h_layout = QHBoxLayout()
        h_layout.addWidget(self.text_left)
        h_layout.addWidget(self.text_right)
        #h_layout.addWidget(self.video_widget)  # Añadir el widget de video al diseño

        # Layout final que combina todo
        main_layout = QVBoxLayout()
        main_layout.addLayout(top_layout)
        main_layout.addLayout(h_layout)

        container = QWidget()
        container.setLayout(main_layout)
        self.setCentralWidget(container)
        
        # Inicializar el reproductor de VLC
        self.media_player = vlc.MediaPlayer()
       # self.media_player.set_hwnd(self.video_widget.winId())

        # Crear el menú (mantiene el código existente para el menú)
        menubar = self.menuBar()

        # Menú Archivo
        file_menu = menubar.addMenu('Archivo')
        open_action = QAction('Abrir M3U local', self)
        open_from_url_action = QAction('Abrir M3U desde URL', self)  
        save_action = QAction('Guardar M3U', self)
        exit_action = QAction('Salir', self)
        open_action.triggered.connect(self.load_m3u)
        open_from_url_action.triggered.connect(self.load_m3u_from_url) 
        save_action.triggered.connect(self.save_m3u)
        exit_action.triggered.connect(self.close)
        file_menu.addAction(open_action)
        file_menu.addAction(open_from_url_action)  
        file_menu.addAction(save_action)
        file_menu.addAction(exit_action)

        # Menú Editar
        edit_menu = menubar.addMenu('Editar')
        search_action = QAction('Buscar y seleccionar', self)
        copy_action = QAction('Copiar selección', self)
        paste_action = QAction('Pegar', self)
        search_action.triggered.connect(self.search_group_title)
        copy_action.triggered.connect(lambda: copy_selection(self))
        paste_action.triggered.connect(lambda: paste_selection(self))
        edit_menu.addAction(search_action)
        edit_menu.addAction(copy_action)
        edit_menu.addAction(paste_action)
        
        # Acción de Filtrado
        filter_action = QAction('Filtrar', self)
        filter_action.triggered.connect(self.filter_list)
        edit_menu.addAction(filter_action)

        # Acción de Ordenación
        sort_action = QAction('Ordenar', self)
        sort_action.triggered.connect(self.sort_list)
        edit_menu.addAction(sort_action)
        
        # Menú Listas
        list_menu = menubar.addMenu('Listas')
        
        # Subopción guardar URL
        save_list_action = QAction('Guardar URL', self)
        save_list_action.triggered.connect(lambda: guardar_url(self) )
        list_menu.addAction(save_list_action)
        # Ver URL
        view_list_action = QAction('Ver URLS Guardadas', self)
        view_list_action.triggered.connect(lambda: ver_urls_guardadas(self) )
        list_menu.addAction(view_list_action)

        # Menú Opciones
        options_menu = menubar.addMenu('Opciones')

        # Subopción Acerca de
        about_action = QAction('Acerca de', self)
        about_action.triggered.connect(lambda: show_about_dialog(self))
        options_menu.addAction(about_action)

        # Subopción Cómo usar
        how_to_use_action = QAction('Cómo usar', self)
        how_to_use_action.triggered.connect(lambda: show_how_to_use_dialog(self))
        options_menu.addAction(how_to_use_action)

        # Subopción Abrir URL del repositorio en GitHub
        open_github_action = QAction('Abrir URL del repositorio en GitHub', self)
        open_github_action.triggered.connect(lambda: open_github_url(self))
        options_menu.addAction(open_github_action)

        # Conectar el menú contextual del texto izquierdo
        self.text_left.setContextMenuPolicy(Qt.CustomContextMenu)
        self.text_left.customContextMenuRequested.connect(lambda position: show_context_menu(self, position))

        # Conectar el menú contextual del texto derecho
        self.text_right.setContextMenuPolicy(Qt.CustomContextMenu)
        self.text_right.customContextMenuRequested.connect(lambda position: show_context_menu(self, position))

        # Conectar la señal de doble clic a una función
        self.text_left.mouseDoubleClickEvent = lambda event: handle_double_click(self, event)
        self.text_right.mouseDoubleClickEvent = lambda event: handle_double_click(self, event)

    def preview_stream_from_menu(self, url):

        if not self.instance:
            QMessageBox.critical(self, "Error", "El reproductor VLC no está inicializado.")
            return

        video_dialog = VideoDialog(self, instance=self.instance)  # Crear una instancia de VideoDialog
        video_dialog.play_video(url)
        video_dialog.exec_()

    def load_m3u(self):
        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getOpenFileName(self, "Abrir M3U local", "", "M3U Files (*.m3u);;All Files (*)", options=options)
        if file_path:
            self.start_loading_m3u(file_path)

    def load_m3u_from_url(self):
        url, ok = QInputDialog.getText(self, 'Abrir M3U desde URL', 'Escribe la URL del archivo M3U:')
        
        if ok and url:
            try:
                response = requests.get(url, stream=True)
                response.raise_for_status()

                # Guardar temporalmente el archivo M3U descargado
                self.temp_file_path = str(current_directory / "temp_downloaded.m3u")
                with open(self.temp_file_path, 'wb') as temp_file:
                    for chunk in response.iter_content(chunk_size=1024):
                        if chunk:
                            temp_file.write(chunk)

                self.start_loading_m3u(self.temp_file_path)

            except requests.exceptions.RequestException as e:
                QMessageBox.critical(self, "Error", f"No se pudo descargar el archivo: {str(e)}")
                self.temp_file_path = None

    def start_loading_m3u(self, file_path):
        """
        Maneja el proceso de carga de un archivo M3U, ya sea desde un archivo local o una URL descargada.
        """
        self.text_left.clear()  # Borra el texto actual antes de cargar el nuevo archivo
        self.original_lines.clear()  # Limpiar la lista original

        self.progress_dialog = QProgressDialog("Cargando archivo...", "Cancelar", 0, 100, self)
        self.progress_dialog.setWindowTitle("Cargando")
        self.progress_dialog.setWindowModality(Qt.WindowModal)
        self.progress_dialog.setMinimumDuration(0)
        self.progress_dialog.setAutoClose(False)
        self.progress_dialog.setValue(0)

        self.thread = LoadFileThread(file_path)
        self.threads.append(self.thread)

        # Conexiones
        self.thread.progress.connect(self.update_progress)
        self.thread.lines_loaded.connect(self.append_line_to_original)  # Almacenar línea por línea
        self.thread.finished.connect(self.on_file_loaded)
        self.progress_dialog.canceled.connect(self.cancel_loading)

        self.thread.start()

    def append_line_to_original(self, line):
        """
        Almacena cada línea en la lista original y la añade al texto de la izquierda.
        """
        self.original_lines.append(line)
        self.append_text_to_left(line)
        
    def update_progress(self, value):
        self.progress_dialog.setValue(value)

    def append_text_to_left(self, text):
        # Ignora la línea #EXTM3U
        if not text.startswith("#EXTM3U"):
            self.original_content.append(text)  # Almacena el contenido original sin la línea #EXTM3U
            # Aquí pasamos una lista con una sola línea a la función que maneja el coloreado
            self.append_lines_to_text_edit(self.text_left, [text])

    def on_file_loaded(self):
        self.progress_dialog.close()  # Cerrar el QProgressDialog cuando todo haya terminado
        self.threads.remove(self.sender())
        
    def cancel_loading(self):
        if self.thread.isRunning():
            self.thread.terminate()
            self.progress_dialog.close()
            QMessageBox.information(self, "Cancelado", "La carga del archivo ha sido cancelada.")


    def append_lines_to_text_edit(self, text_edit, lines):
        cursor = text_edit.textCursor()
        for line in lines:
            if line.startswith("#EXTINF:") or line.startswith("http"):
                self.append_colored_text_with_cursor(cursor, line, QColor('black'))
            else:
                self.append_colored_text_with_cursor(cursor, line, QColor('red'))
        text_edit.setTextCursor(cursor)  # Asegura que el cursor esté al final

    def append_colored_text_with_cursor(self, cursor, text, color):
        fmt = QTextCharFormat()
        fmt.setForeground(QBrush(color))
        cursor.movePosition(QTextCursor.End)
        cursor.insertBlock()
        cursor.insertText(text, fmt)
        cursor.setCharFormat(QTextCharFormat())  # Resetear el formato
        
    def on_lines_loaded(self, line):
        # Acumula las líneas cargadas
        self.loaded_lines.append(line)
        # Muestra las líneas en grupos (por ejemplo, de 10,000) para evitar recargar la UI
        if len(self.loaded_lines) >= 10000:
            self.append_lines_to_text_edit(self.text_left, self.loaded_lines)
            self.loaded_lines.clear()

    def save_m3u(self):
        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getSaveFileName(self, "Guardar M3U", "", "M3U Files (*.m3u);;All Files (*)", options=options)
        if file_path:
            content = self.text_right.toPlainText()
            with open(file_path, 'w') as file:
                # Añadir #EXTM3U al principio del archivo
                file.write("#EXTM3U\n")
                file.write(content)

    def search_group_title(self):
        search_term, ok = QInputDialog.getText(self, 'Buscar', 'Escribe el contenido de group-title a buscar:')
        if ok and search_term:
            self.thread = SearchThread(self.text_left.toPlainText(), search_term)
            self.thread.result.connect(self.on_search_finished)
            self.thread.start()

    def on_search_finished(self, positions):
        if not positions:
            QMessageBox.warning(self, "Advertencia", "No hay resultados para el concepto buscado.")
        else:
            fmt = QTextCharFormat()
            fmt.setBackground(QBrush(QColor('yellow')))
            cursor = self.text_left.textCursor()

            # Resaltar todas las posiciones encontradas
            for pos in positions:
                cursor.setPosition(pos)
                cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, len(positions[pos]))
                cursor.mergeCharFormat(fmt)

    def closeEvent(self, event):
        # Preguntar al usuario si está seguro de cerrar
        reply = QMessageBox.question(self, 'Confirmar salida', '¿Está seguro de que desea salir?',
                                     QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
        if reply == QMessageBox.Yes:
            # Eliminar el archivo temporal si existe
            if self.temp_file_path and os.path.exists(self.temp_file_path):
                os.remove(self.temp_file_path)
                self.temp_file_path = None
            if self.media_player is not None:
                self.media_player.stop()
            # Cerrar todos los hilos y procesos en ejecución
            self.close_all_threads_and_processes()
            event.accept()
        else:
            event.ignore()

    def close_all_threads_and_processes(self):
        # Aquí deberías cerrar todos los hilos y procesos que estén en ejecución
        for thread in self.threads:
            if thread.isRunning():
                thread.quit()
                thread.wait()
        self.threads.clear()

    def filter_list(self):
        """
        Filtra la lista M3U en función del texto ingresado en la entrada de filtro.
        Cada canal está compuesto por un par de líneas (EXTINF y URL).
        """
        filter_term = self.filter_input.text().strip()
        if not filter_term:
            QMessageBox.warning(self, "Entrada Vacía", "Por favor, ingrese un término para filtrar.")
            return

        filtered_lines = []
        # Iterar en pasos de 2 para considerar cada canal (EXTINF + URL) como un bloque
        for i in range(0, len(self.original_lines), 2):
            extinf_line = self.original_lines[i]
            url_line = self.original_lines[i + 1] if i + 1 < len(self.original_lines) else ""
            
            # Verificar si el término de filtro aparece en alguna de las dos líneas
            if filter_term.lower() in extinf_line.lower() or filter_term.lower() in url_line.lower():
                filtered_lines.append(extinf_line)
                filtered_lines.append(url_line)

        if filtered_lines:
            self.text_left.clear()
            self.append_lines_to_text_edit(self.text_left, filtered_lines)
        else:
            QMessageBox.information(self, "Sin Resultados", "No se encontraron coincidencias con el criterio de filtrado.")


    def extract_group_title(self, line):
        match = re.search(r'group-title="([^"]*)"', line)
        if match:
            return match.group(1).strip().lower()
        return ''  # Devuelve una cadena vacía si no se encuentra el group-title
    
    def sort_list(self):
        """
        Ordena la lista M3U basada en la opción seleccionada en el combo box.
        Cada canal está compuesto por un par de líneas (EXTINF y URL).
        """
        sort_criteria = self.sort_selector.currentText()

        # Crear pares de líneas para cada canal
        channel_pairs = [(self.original_lines[i], self.original_lines[i + 1])
                        for i in range(0, len(self.original_lines), 2)]

        if sort_criteria == 'Nombre del Canal (A-Z)':
            sorted_pairs = sorted(channel_pairs, key=lambda pair: pair[0])
        elif sort_criteria == 'Nombre del Canal (Z-A)':
            sorted_pairs = sorted(channel_pairs, key=lambda pair: pair[0], reverse=True)
        elif sort_criteria == 'Group-title (A-Z)':
            sorted_pairs = sorted(channel_pairs, key=lambda pair: self.extract_group_title(pair[0]))
        elif sort_criteria == 'Group-title (Z-A)':
            sorted_pairs = sorted(channel_pairs, key=lambda pair: self.extract_group_title(pair[0]), reverse=True)

        # Descomprimir los pares en una lista simple de líneas ordenadas
        sorted_lines = [line for pair in sorted_pairs for line in pair]

        self.text_left.clear()
        self.append_lines_to_text_edit(self.text_left, sorted_lines)

    def reset_list(self):
        """
        Restaura la lista original cargada antes de aplicar filtros u ordenaciones.
        """
        self.text_left.clear()
        # Filtrar la línea que contiene #EXTM3U antes de mostrar el contenido
        filtered_lines = [line for line in self.original_content if not line.startswith("#EXTM3U")]

        # Mostrar el contenido filtrado en el cuadro de texto izquierdo
        self.append_lines_to_text_edit(self.text_left, filtered_lines)

        self.filter_input.clear()
        self.sort_selector.setCurrentIndex(0)

1. Configuración Inicial

  • Importaciones: El código importa varios módulos de PyQt5 para crear la interfaz gráfica, así como otras bibliotecas para manejar archivos, realizar solicitudes HTTP (con requests), manejar errores (con logging), y reproducir videos utilizando vlc y el módulo python-vlc.
  • Directorios y recursos: Se define current_directory para obtener la ruta del directorio donde se encuentra el script actual. Además, se carga un ícono personalizado para la aplicación.

2. Clase M3UOrganizer

  • Inicialización:
    • La clase hereda de QMainWindow, lo que permite crear una ventana principal para la aplicación.
    • Se inicializa una instancia del reproductor VLC (vlc.Instance) para reproducir medios.
    • Se preparan algunos atributos para manejar hilos de procesamiento, archivos temporales y el contenido original de las listas M3U.
  • Método initUI:
    • Icono y bandeja del sistema: Configura un icono para la ventana y la bandeja del sistema, añadiendo un menú contextual con opciones como abrir una VPN, restaurar la ventana, mostrar información «Acerca de», y salir de la aplicación.
    • Widgets principales: Se crean dos cajas de texto (QTextEdit) para mostrar la lista M3U original (izquierda de la ventana) y la modificada (derecha de la ventana) que será la que el usuario puede guardar como archivo .m3u.
    • Botones de filtrado y ordenación: Se añaden botones y campos de entrada para filtrar y ordenar las listas de canales.
    • Menús: Se configuran varios menús en la barra de menú (Archivo, Editar, Listas, Opciones) con acciones para abrir, guardar, filtrar, ordenar, buscar, y gestionar URLs.

3. Manejo de listas M3U

  • Cargar archivos M3U:
    • Se pueden cargar listas desde un archivo local o desde una URL. Para este último, se descarga el archivo temporalmente. Este archivo debería eliminarse cuando se cierre el programa.
    • El proceso de carga utiliza hilos (LoadFileThread) para manejar la tarea de forma asíncrona, evitando que la interfaz gráfica se congele.
    • El contenido cargado se muestra en el cuadro de texto izquierdo y se almacena en original_lines.
  • Filtrado y ordenación:
    • Filtrado: Permite buscar y mostrar solo los canales que coincidan con un término de búsqueda.
    • Ordenación: Ofrece opciones para ordenar la lista alfabéticamente por nombre de canal o por group-title (una etiqueta dentro de los archivos M3U).
  • Guardado de archivos M3U:
    • Permite guardar la lista modificada en un archivo M3U.
  • Otras funcionalidades:
    • Buscar group-title: Busca y resalta en la lista los canales que coincidan con un término de búsqueda.
    • Previsualización de Streaming: Permite previsualizar la URL de un canal mediante VLC en una ventana de diálogo (VideoDialog).
    • Menú Contextual y Doble Clic: Los cuadros de texto izquierdo y derecho tienen menús contextuales personalizados y manejan eventos de doble clic.

4. Manejo de eventos

  • Cierre de la aplicación: Antes de cerrar, la aplicación pregunta al usuario si desea salir, limpia los archivos temporales, detiene el reproductor VLC y finaliza los hilos en ejecución.

5. Hilos y proceso asíncrono

  • La carga de archivos y la búsqueda se manejan en hilos separados para mantener la interfaz de usuario en funcionamiento, evitando que se congele.

6. Funciones auxiliares

  • Filtrado y ordenación: Métodos para extraer etiquetas de group-title, aplicar filtros, ordenar canales y restablecer la lista original.
  • Manejo de coloreado: Añade colores específicos al texto mostrado en los cuadros de texto para diferenciar tipos de líneas (EXTINF y URLs).

Threads.py

Este módulo contiene dos clases que heredan de QThread y están diseñadas para realizar las operaciones en segundo plano dentro de la aplicación PyQt5. Las clases LoadFileThread y SearchThread proporcionan hilos para cargar archivos y buscar términos específicos en texto, respectivamente.

from PyQt5.QtCore import QThread, pyqtSignal
import chardet

class LoadFileThread(QThread):
    progress = pyqtSignal(int)
    lines_loaded = pyqtSignal(str)
    finished = pyqtSignal()

    def __init__(self, file_path, chunk_size=10000):  # chunk_size aumentado
        super().__init__()
        self.file_path = file_path
        self.chunk_size = chunk_size

    def run(self):
        # Detectar la codificación
        with open(self.file_path, 'rb') as f:
            raw_data = f.read(10000)  # Leer una pequeña parte del archivo
            result = chardet.detect(raw_data)
            encoding = result['encoding']

        total_lines = 0
        with open(self.file_path, 'r', encoding=encoding, errors='ignore') as file:
            while file.readline():
                total_lines += 1

        with open(self.file_path, 'r', encoding=encoding, errors='ignore') as file:
            for i, line in enumerate(file):
                if i % self.chunk_size == 0:
                    self.progress.emit(int((i / total_lines) * 100))
                self.lines_loaded.emit(line.strip())

        self.finished.emit()
        
    def process_lines(self, lines):
        processed_lines = []
        for line in lines:
            line = line.strip()
            color = 'black' if line.startswith("#EXTINF:") or line.startswith("http") else 'red'
            processed_lines.append(f"<span style='color:{color}'>{line}</span>")
        return "\n".join(processed_lines)

class SearchThread(QThread):
    result = pyqtSignal(dict)

    def __init__(self, text, search_term):
        super().__init__()
        self.text = text
        self.search_term = search_term

    def run(self):
        positions = {}
        current_pos = 0
        while current_pos >= 0:
            current_pos = self.text.find(self.search_term, current_pos)
            if (current_pos >= 0):
                positions[current_pos] = self.search_term
                current_pos += len(self.search_term)
        self.result.emit(positions)
        

Las clases:

  1. LoadFileThread:
    • Propósito: Leer un archivo M3U línea por línea en un hilo separado, emitir señales de progreso y enviar las líneas leídas a la interfaz.
    • Señales:
      • progress: Emite el progreso de la carga como un porcentaje.
      • lines_loaded: Emite cada línea del archivo después de leerla.
      • finished: Señala que el proceso de carga ha terminado.
    • Métodos:
      • __init__: Inicializa el hilo con la ruta del archivo y el tamaño de los bloques de lectura (chunk_size).
      • run: Detecta la codificación del archivo usando la librería chardet, cuenta el número total de líneas y luego las lee en bloques, emitiendo el progreso y las líneas cargadas.
      • process_lines: (Método adicional que podría no estar en uso) Procesa las líneas para aplicar colores según el tipo de contenido (EXTINF o URL).
  2. SearchThread:
    • Propósito: Buscar un término en un texto dado en un hilo separado y devolver las posiciones donde se encuentra el término.
    • Señales:
      • result: Emite un diccionario con las posiciones donde se encuentra el término de búsqueda.
    • Métodos:
      • __init__: Inicializa el hilo con el texto completo y el término de búsqueda.
      • run: Busca todas las ocurrencias del término en el texto y almacena las posiciones encontradas, que luego emite.

Funcionamiento general:

  • LoadFileThread: Se encarga de cargar un archivo grande en segundo plano. Primero detecta la codificación del archivo, luego cuenta las líneas y finalmente las lee y emite línea por línea mientras actualiza el progreso.
  • SearchThread: Se utiliza para realizar búsquedas de términos en un texto largo, encontrando y devolviendo todas las posiciones del término en un diccionario.

Actions.py

El archivo actions.py contiene las funciones y clases que definen la mayoría de las acciones que se pueden realizar dentro de la aplicación M3U Organ1zat0r. Este código es un ejemplo de una aplicación GUI (interfaz gráfica de usuario) que utiliza PyQt5 y VLC para la manipulación de texto, reproducción de videos y gestión de URLs.

from PyQt5.QtWidgets import (QAction, QDialog, QLineEdit, 
                             QApplication, QDialog, QVBoxLayout, QPushButton, QListWidget,
                            QInputDialog, QMenu, QMessageBox, QScrollArea, QWidget, QFileDialog)
from PyQt5.QtGui import QTextCursor
from PyQt5.QtCore import QProcess, QTimer
from PyQt5.QtMultimediaWidgets import QVideoWidget
import sys
import vlc
import json
import os

def copy_selection(main_window):
    text_edit = main_window.text_left if main_window.text_left.hasFocus() else main_window.text_right
    text_edit.copy()

def paste_selection(main_window):
    text_edit = main_window.text_left if main_window.text_left.hasFocus() else main_window.text_right
    text_edit.paste()

def show_context_menu(main_window, position):
    cursor = main_window.text_left.textCursor() if main_window.text_left.hasFocus() else main_window.text_right.textCursor()
    selected_text = cursor.selectedText().strip()

    context_menu = QMenu()

    # Opción para copiar el texto seleccionado
    copy_action = QAction("Copiar", main_window)
    copy_action.triggered.connect(lambda: copy_selection(main_window))
    context_menu.addAction(copy_action)

    # Opción para pegar el texto copiado
    paste_action = QAction("Pegar", main_window)
    paste_action.triggered.connect(lambda: paste_selection(main_window))
    context_menu.addAction(paste_action)

    # Opción para abrir con VLC si el texto seleccionado es una URL válida
    if selected_text.startswith("http://") or selected_text.startswith("https://"):
        open_with_vlc_action = QAction("Abrir con VLC", main_window)
        open_with_vlc_action.triggered.connect(lambda: open_with_vlc(main_window, selected_text))
        context_menu.addAction(open_with_vlc_action)

        # Opción para previsualizar en VLC
        preview_action = QAction("Previsualizar Streaming", main_window)
        preview_action.triggered.connect(lambda: main_window.preview_stream_from_menu(selected_text))
        context_menu.addAction(preview_action)

    # Opción para seleccionar todo el texto
    select_all_action = QAction("Seleccionar Todo", main_window)
    select_all_action.triggered.connect(lambda: (main_window.text_left.selectAll() if main_window.text_left.hasFocus() else main_window.text_right.selectAll()))
    context_menu.addAction(select_all_action)

    # Mostrar el menú contextual
    context_menu.exec_(main_window.text_left.mapToGlobal(position) if main_window.text_left.hasFocus() else main_window.text_right.mapToGlobal(position))

# Funciones para abrir con VLC

def is_vlc_installed(command):
    """Verifica si VLC está instalado intentando ejecutar el comando."""
    try:
        result = QProcess().startDetached(command)
        return result
    except Exception:
        return False

def open_with_vlc(main_window, url):
    if sys.platform.startswith('linux') or sys.platform == 'darwin':  # Linux o macOS
        command = f"vlc {url}"
    elif sys.platform.startswith('win'):  # Windows
        vlc_path = find_vlc_executable(main_window)

        if vlc_path is None:
            QMessageBox.critical(main_window, "Error", "No se encontró el ejecutable de VLC.")
            return

        command = f'"{vlc_path}" "{url}"'
    else:
        QMessageBox.warning(main_window, "Error", "Sistema operativo no soportado.")
        return

    # Verificar si VLC está instalado y disponible
    if not is_vlc_installed(command):
        QMessageBox.critical(main_window, "Error", "No se pudo encontrar VLC en el sistema. Por favor, asegúrate de que VLC está instalado.")
        return

    try:
        QProcess.startDetached(command)
    except Exception as e:
        QMessageBox.critical(main_window, "Error", f"No se pudo abrir VLC: {str(e)}")

def find_vlc_executable(main_window):
    vlc_executable = "vlc.exe"

    # Verificar si vlc.exe está en el PATH
    if is_executable_in_path(vlc_executable):
        return vlc_executable

    # Preguntar al usuario la ubicación del ejecutable de VLC
    vlc_path, _ = QFileDialog.getOpenFileName(
        main_window,
        "Como VLC no está en el PATH del sistema. Selecciona el ejecutable de VLC",
        os.getenv("ProgramFiles", "C:\\"),
        "VLC executable (vlc.exe)"
    )

    if vlc_path:
        # Aquí puedes guardar la ruta seleccionada para futuros usos, si es necesario
        return vlc_path
    else:
        return None

def is_executable_in_path(executable):
    # Verifica si el ejecutable está en el PATH del sistema
    for path in os.environ["PATH"].split(os.pathsep):
        exe_file = os.path.join(path, executable)
        if os.path.isfile(exe_file) and os.access(exe_file, os.X_OK):
            return True
    return False

def handle_double_click(main_window, event):
    # Identificar cuál de los QTextEdit ha recibido el evento
    text_edit = main_window.text_left if main_window.text_left.viewport().underMouse() else main_window.text_right

    cursor = text_edit.cursorForPosition(event.pos())
    cursor.select(QTextCursor.LineUnderCursor)
    selected_text = cursor.selectedText()

    # Personalizar el tamaño de la ventana de edición
    dialog = QInputDialog(main_window)
    dialog.setWindowTitle("Editar línea")
    dialog.setLabelText("Modifica la línea:")
    dialog.setTextValue(selected_text)
    dialog.setFixedSize(400, 150)  # Ajusta el tamaño de la ventana de edición

    ok = dialog.exec_()
    new_text = dialog.textValue()

    if ok:
        cursor.insertText(new_text)
        
class VideoDialog(QDialog):
    def __init__(self, parent=None, instance=None):
        super(VideoDialog, self).__init__(parent)
        self.setWindowTitle("Video Preview")
        self.video_widget = QVideoWidget()
        self.video_widget.setMinimumSize(640, 480)
        layout = QVBoxLayout()
        layout.addWidget(self.video_widget)
        self.setLayout(layout)
        self.media_player = None
        self.instance = instance

    def closeEvent(self, event):
        if self.media_player:
            self.media_player.stop()
        event.accept()

    def play_video(self, url):
        try:
            # Crear un nuevo objeto de medios desde la URL
            media = self.instance.media_new(url)
            if not media:
                raise Exception("No se pudo crear el objeto de medios VLC.")
            
            # Establecer la opción vout en opengl
            media.add_option('vout=opengl')
            
            # Asociar el medio con el reproductor
            self.media_player = vlc.MediaPlayer()
            self.media_player.set_media(media)
            
            # Establecer el widget de salida de video según el sistema operativo
            if sys.platform.startswith('linux'):
                self.media_player.set_xwindow(int(self.video_widget.winId()))
            elif sys.platform.startswith('win'):
                self.media_player.set_hwnd(int(self.video_widget.winId()))
            elif sys.platform.startswith('darwin'):  # macOS
                self.media_player.set_nsobject(int(self.video_widget.winId()))

            # Comenzar la reproducción (sin mostrar la ventana aún)
            self.media_player.play()
            print("Reproduciendo URL:", url)  # Mensaje de depuración

            # Esperar un momento para verificar si el stream comienza
            QTimer.singleShot(3000, self.check_stream_status)  # Esperar 3 segundos antes de verificar

        except Exception as e:
            QMessageBox.critical(self, "Error", f"Error al intentar reproducir el stream: {str(e)}")

    def check_stream_status(self):
        # Comprobar si el estado es Error, Stopped o Ended
        if self.media_player.get_state() in [vlc.State.Error, vlc.State.Stopped, vlc.State.Ended]:
            # Detener el reproductor y mostrar un mensaje de error
            self.media_player.stop()
            QMessageBox.warning(self, "Error de reproducción", "No se pudo reproducir el stream. La URL puede estar inactiva o ser incorrecta.")
            print("El stream no se pudo reproducir.")
            self.close()  # Cerrar la ventana si la reproducción falla
        else:
            self.show()  # Mostrar la ventana solo si la reproducción es exitosa

            
# Acciones sobre las URL a Guardar
def load_urls():
    if not os.path.exists("urls_guardadas.json"):
        return {}
    
    with open("urls_guardadas.json", "r") as file:
        return json.load(file)

def save_urls(urls):
    with open("urls_guardadas.json", "w") as file:
        json.dump(urls, file, indent=4)


def guardar_url(self):
    # Mostrar un cuadro de diálogo para obtener el nombre y la URL
    nombre, ok_pressed = QInputDialog.getText(self, "Guardar URL", "Ingrese un nombre para la URL:")
    if not ok_pressed or not nombre:
        return  # Si el usuario cancela o no ingresa un nombre, salir
    
    url, ok_pressed = QInputDialog.getText(self, "Guardar URL", "Ingrese la URL:")
    if not ok_pressed or not url:
        return  # Si el usuario cancela o no ingresa una URL, salir
    
    urls = load_urls()
    urls[nombre] = url
    save_urls(urls)

    QMessageBox.information(self, "Acción completada", f"URL guardada bajo el nombre '{nombre}'")


def ver_urls_guardadas(self):
    urls = load_urls()

    if not urls:
        QMessageBox.information(self, "Información", "No hay URLs guardadas.")
        return

    # Crear un diálogo para mostrar y gestionar URLs
    dialog = QDialog(self)
    dialog.setWindowTitle("URLs Guardadas")
    layout = QVBoxLayout(dialog)

    # Crear un área de desplazamiento
    scroll_area = QScrollArea(dialog)
    scroll_area.setWidgetResizable(True)

    # Crear un widget contenedor para el área de desplazamiento
    container_widget = QWidget()
    container_layout = QVBoxLayout(container_widget)

    list_widget = QListWidget(container_widget)
    for nombre in urls:
        list_widget.addItem(nombre)

    container_layout.addWidget(list_widget)
    container_widget.setLayout(container_layout)
    scroll_area.setWidget(container_widget)

    layout.addWidget(scroll_area)

    # Añadir botones para editar, eliminar y copiar URL
    edit_button = QPushButton("Editar URL", dialog)
    delete_button = QPushButton("Eliminar URL", dialog)
    copy_button = QPushButton("Copiar URL", dialog)

    def edit_url():
        current_item = list_widget.currentItem()
        if current_item:
            nombre_original = current_item.text()

            # Editar el nombre
            nuevo_nombre, ok_pressed = QInputDialog.getText(dialog, "Editar Nombre", "Modifica el nombre:", text=nombre_original)
            if not ok_pressed or not nuevo_nombre:
                QMessageBox.warning(dialog, "Advertencia", "El nombre no puede estar vacío.")
                return

            # Editar la URL
            nueva_url, ok_pressed = QInputDialog.getText(dialog, "Editar URL", "Modifica la URL:", text=urls[nombre_original])
            if not ok_pressed or not nueva_url:
                QMessageBox.warning(dialog, "Advertencia", "La URL no puede estar vacía.")
                return

            # Actualizar el registro y guardar
            del urls[nombre_original]  # Eliminar el antiguo nombre
            urls[nuevo_nombre] = nueva_url
            save_urls(urls)

            # Actualizar la lista en la UI
            current_item.setText(nuevo_nombre)

            QMessageBox.information(dialog, "Éxito", f"'{nombre_original}' actualizado correctamente a '{nuevo_nombre}'.")

    def delete_url():
        current_item = list_widget.currentItem()
        if current_item:
            nombre = current_item.text()
            del urls[nombre]
            save_urls(urls)
            list_widget.takeItem(list_widget.row(current_item))
            QMessageBox.information(dialog, "Éxito", f"URL '{nombre}' eliminada correctamente.")

    def copy_url():
        current_item = list_widget.currentItem()
        if current_item:
            nombre = current_item.text()
            url = urls[nombre]
            clipboard = QApplication.clipboard()
            clipboard.setText(url)
            QMessageBox.information(dialog, "Éxito", f"URL '{url}' copiada al portapapeles.")

    edit_button.clicked.connect(edit_url)
    delete_button.clicked.connect(delete_url)
    copy_button.clicked.connect(copy_url)

    layout.addWidget(edit_button)
    layout.addWidget(delete_button)
    layout.addWidget(copy_button)

    dialog.setLayout(layout)
    dialog.exec_()

Funcionalidades:

  1. Funciones de Copiar y Pegar:
    • copy_selection(main_window) y paste_selection(main_window): Copian y pegan el texto seleccionado del área de texto que está actualmente enfocada en la ventana principal (text_left o text_right).
  2. Menú contextual:
    • show_context_menu(main_window, position): Muestra un menú contextual con opciones como copiar, pegar, abrir en VLC, previsualizar en VLC, y seleccionar todo el texto. También añade la posibilidad de que si el texto seleccionado es una URL, agrega opciones adicionales para abrirla o previsualizarla con VLC.
  3. Integración con VLC:
    • is_vlc_installed(command) y open_with_vlc(main_window, url): Verifican si VLC está instalado y disponible en el sistema, y luego intentan abrir la URL con VLC.
    • find_vlc_executable(main_window): Busca el ejecutable de VLC (vlc.exe) en el sistema. Si no se encuentra, pide al usuario que seleccione manualmente la ubicación del ejecutable.
    • is_executable_in_path(executable): Verifica si un ejecutable está disponible en el PATH del sistema.
  4. Edición de texto:
    • handle_double_click(main_window, event): Maneja el doble clic en un área de texto, permitiendo al usuario editar la línea seleccionada en un cuadro de diálogo.
  5. Reproducción de vídeo en una ventana de diálogo:
    • VideoDialog: Es una clase que crea un diálogo con un reproductor de video VLC integrado. Se utiliza para previsualizar streams de video. La clase maneja la reproducción del video, así como la verificación de si el stream es reproducible.
  6. Gestión de URLs guardadas:
    • load_urls() y save_urls(urls): Cargan y guardan un diccionario de URLs en un archivo JSON (urls_guardadas.json).
    • guardar_url(self): Muestra un diálogo para que el usuario guarde una nueva URL con un nombre asociado.
    • ver_urls_guardadas(self): Muestra un diálogo con una lista de URLs guardadas, permitiendo al usuario editar, eliminar o copiar URLs.

Funciones de gestión de URLs en este organizador de listas m3u:

  • ver_urls_guardadas(self): Crea una interfaz para visualizar las URLs guardadas, donde se puede:
    • Editar: Modificar el nombre y la URL asociada.
    • Eliminar: Quitar una URL de la lista guardada.
    • Copiar: Copiar la URL al portapapeles del sistema.

El código en su conjunto proporciona una interfaz completa para funcionar con este organizador de listas m3u (listas de reproducción M3U o M3U8), gestionar URLs de streaming, y reproducir estas URLs usando VLC, todo ello integrado en una aplicación de escritorio en Python.

Optionsmenu.py

Este código proporciona funcionalidades para una aplicación GUI, llamada «M3U Organizer», que está diseñada para gestionar listas de reproducción en formato M3U.

from PyQt5.QtWidgets import QDialog, QHBoxLayout, QLabel, QVBoxLayout
from PyQt5.QtGui import QPixmap
import webbrowser
from pathlib import Path



current_directory = Path(__file__).parent

def show_about_dialog(main_window):
    dialog = QDialog(main_window)
    dialog.setWindowTitle("Acerca de M3U Organizer")

    # Usamos un QHBoxLayout para organizar la imagen a la izquierda y el texto a la derecha
    layout = QHBoxLayout()

    # Cargar y mostrar la imagen
    image_path = current_directory / './resources/logo.png'
    if image_path.exists():
        pixmap = QPixmap(str(image_path))
        image_label = QLabel()
        image_label.setPixmap(pixmap)
        layout.addWidget(image_label)

    # Texto de la descripción
    description_label = QLabel("M3U Organizer (V.0.5) es una herramienta para gestionar listas de reproducción en formato M3U.\n"
                               "Este es un programa escrito por entreunosyceros como un ejercicio práctico de Python.")
    description_label.setWordWrap(True)
    layout.addWidget(description_label)

    dialog.setLayout(layout)
    # Fijar el tamaño del diálogo para que no se pueda redimensionar
    dialog.setFixedSize(dialog.sizeHint())
    dialog.exec_()

def show_how_to_use_dialog(main_window):
    dialog = QDialog(main_window)
    dialog.setWindowTitle("Cómo usar M3U Organizer")

    layout = QVBoxLayout()

    # Texto de la explicación
    instruction_text = ("0. Formato de lista .m3u válido:\n"
                        "#EXTM3U\n"
                        "#EXTINF:-1 ETIQUETAS\n"
                        "http://URL-DEL-STREAMING\n"
                        "#EXTINF:-1 ETIQUETAS\n"
                        "http://URL-DEL-STREAMING\n"
                        "---------------------------\n"
                        "1. Usa 'Abrir M3U' para cargar una lista de reproducción en el lado izquierdo de la pantalla.\n"
                        "2. Utiliza la opción 'Abrir con VLC' en el menú contextual del ratón para reproducir la URL seleccionada en el lado izquierdo de la pantalla.\n"
                        "3. En el menú contextual del ratón también podrás seleccionar todo el contenido del lado izquierdo de la pantalla.\n"
                        "4. Usa 'Buscar y seleccionar' para buscar un group-title.\n"
                        "5. El usuario podrá previsualizar el streaming de la URL seleccionada en el lado izquierdo de pantalla.\n También podrá directamente abrir la URL con VLC para ver el streaming."
                        "6. Arrastra el texto seleccionado de un lado a otro de la pantalla.\n Ordena el texto seleccionado del lado derecho de la pantalla arrastrando o utilizando las opciones del menú del ratón.\n"
                        "7. Puedes copiar la selección al panel derecho y guardar la lista modificada como un archivo m3u.\n")
    instruction_label = QLabel(instruction_text)
    instruction_label.setWordWrap(True)
    layout.addWidget(instruction_label)

    dialog.setLayout(layout)
    # Fijar el tamaño del diálogo para que no se pueda redimensionar
    dialog.setFixedSize(dialog.sizeHint())
    dialog.exec_()

def open_github_url(main_window):
    # URL del repositorio en GitHub
    github_url = "https://github.com/sapoclay/organizador-m3u"
    webbrowser.open(github_url)

def abrir_vpn(self):
    """
    Abre la URL para obtener una VPN gratuita durante 30 días en el navegador web predeterminado.
    """
    webbrowser.open("https://www.expressvpn.com/refer-a-friend/30-days-free?locale=es&referrer_id=40141467&utm_campaign=referrals&utm_medium=copy_link&utm_source=referral_dashboard")

def restore_window(self):
    """
    Restaura la ventana principal si está minimizada o escondida.
    """
    if self.isMinimized() or not self.isVisible():
        self.showNormal()
        self.activateWindow()
        

El código proporciona varias funciones que sirven para mostrar información sobre la aplicación, así como guiar al usuario en su uso, y acceder a recursos externos como el repositorio en GitHub y una oferta de VPN. Además, incluye una función para manejar la restauración de la ventana principal de la aplicación. Las interfaces gráficas están construidas utilizando PyQt5, con una organización simple y lógica para intentar mejorar la experiencia del usuario.

Cómo descargar este organizador de listas m3u

Estos son los códigos necesarios para este pequeño proyecto. Puedes descargar el código fuente y el ejecutable desde la página de lanzamientos que se puede encontrar en el repositorio en GitHub en el que lo he colgado.

M3U Organ1zat0r es un organizador de listar M3U sencillo de utilizar, que está diseñado para usuarios que necesitan una solución eficiente y rápida. Con su capacidad para manejar grandes volúmenes de enlaces, realizar búsquedas rápidas y reproducir contenido directamente desde VLC, esta aplicación se puede convertir un una herramienta interesante para los amantes del streaming.

Quizás le vaya añadiendo nuevas funcionalidades a medida que me vayan resultando necesarias en mi día a día.

También te puede interesar ...

Deja un comentario

* Al utilizar este formulario, aceptas que este sitio web almacene y maneje tus datos.

Adblock Detectado!!

Ayúdanos deshabilitando la extensión AdBlocker de tu navegador para visitar esta web.
Si no sabes hacerlo en Chrome, consulta el siguiente enlace. Si utilizas Firefox, puedes consultar este otro enlace.
Esto mejorará tu experiencia en este sitio web.