¡Bienvenido a la guía de desarrollo de plugins para ML-Player! Este documento te proporcionará todo lo que necesitas saber para extender la funcionalidad de ML-Player.
Un plugin es un archivo de Python que contiene una clase especial que ML-Player puede descubrir y cargar. A través de esta clase, puedes interactuar con el reproductor, controlar la reproducción, modificar la lista de reproducción, añadir elementos a la interfaz de usuario y mucho más.
Cada plugin debe contener una clase que herede de MLPlayerPlugin e implementar los siguientes métodos abstractos:
load(self, app_context): Se llama cuando el plugin es cargado por primera vez. Aquí se realiza la inicialización.unload(self): Se llama cuando el plugin va a ser descargado. Aquí se debe limpiar cualquier recurso.Acceso a Metadatos:
Los metadatos del plugin (nombre, versión, autor, descripción, permisos) se cargan automáticamente desde plugin.json y se pasan al constructor de tu clase MLPlayerPlugin. Puedes acceder a ellos a través del atributo self.metadata.
Este es el esqueleto mínimo de un plugin:
# lib/plugins/mi_primer_plugin.py
from utils.plugin_interface import MLPlayerPlugin
class MiPlugin(MLPlayerPlugin):
def __init__(self, metadata: dict):
super().__init__(metadata) # Llama al constructor de la clase base
# Ahora puedes acceder a los metadatos así:
# self.metadata['name']
# self.metadata['version']
# self.metadata['author']
# self.metadata['description']
# --- Métodos del Ciclo de Vida ---
def load(self, app_context):
"""Se llama cuando el plugin es cargado por primera vez."""
self.app_context = app_context
print(f"¡Plugin '{self.metadata['name']}' ha sido cargado!")
def unload(self):
"""Se llama cuando el plugin va a ser descargado."""
print(f"Plugin '{self.metadata['name']}' ha sido descargado.")
AppContext: Tu API PrincipalCuando tu plugin se carga, recibe una instancia de AppContext en su método load. Este objeto es tu único punto de acceso para interactuar con ML-Player. A continuación se detallan todos los métodos disponibles en app_context, agrupados por funcionalidad.
Por seguridad, los plugins deben declarar los permisos que necesitan para operar. El usuario debe conceder estos permisos para que el plugin pueda acceder a ciertas funcionalidades.
has_permission(permission_name)Verifica si el usuario ha concedido un permiso específico a tu plugin.
Permisos Disponibles:
network_access: Para realizar peticiones de red (ej. reproducir URLs, buscar letras, descargar contenido).file_system_read: Para leer archivos del sistema (ej. escanear una librería de música).file_system_write: Para escribir o modificar archivos en el sistema.execute_system_commands: (PELIGROSO) Permite al plugin ejecutar comandos directamente en el sistema operativo. Conceder este permiso puede comprometer la seguridad de tu sistema. Úsalo solo con plugins de absoluta confianza.# Dentro de la clase de tu plugin
def buscar_letras_online(self):
if self.app_context.has_permission("network_access"):
# Lógica para realizar la petición de red
print("Permiso de red concedido. Buscando letras...")
else:
# Informar al usuario que el permiso es necesario
print("Permiso de red denegado. No se pueden buscar letras.")
wx.MessageBox(
"Este plugin necesita acceso a la red para buscar letras. "
"Por favor, habilita el permiso 'network_access' en el Gestor de Plugins.",
"Permiso Requerido",
wx.ICON_WARNING
)
Puedes añadir menús, diálogos y dar feedback de audio.
register_menu_item(menu_name, item_text, callback, help_text="")Añade una opción a un menú en la barra principal. Si el menú no existe, se crea. El help_text opcional se mostrará en la barra de estado de la aplicación.
# Dentro de la clase de tu plugin
def mi_accion_de_menu(self, event):
wx.MessageBox("¡Has hecho clic en el menú del plugin!", "Info")
def load(self, app_context):
self.app_context = app_context
self.app_context.register_menu_item(
menu_name="Mis Plugins",
item_text="Mi Acción Especial",
callback=self.mi_accion_de_menu,
help_text="Esta es una descripción para la barra de estado."
)
unregister_menu_item(menu_name, item_text)Elimina una opción de un menú previamente registrada por el plugin. Es crucial llamar a este método en el unload de tu plugin para limpiar la interfaz.
Si el menú queda vacío después de eliminar el elemento, también se eliminará el menú completo de la barra de menús.
# Dentro de la clase de tu plugin
def unload(self):
# Asegúrate de desregistrar todos los elementos de menú que registraste en load()
self.app_context.unregister_menu_item(
menu_name="Mis Plugins",
item_text="Mi Acción Especial"
)
speak(message, interrupt=False, context='ui')Verbaliza un mensaje utilizando el motor de voz configurado en la aplicación (NVDA o SAPI). Es la forma preferida de dar feedback de audio al usuario.
message (str): El texto a verbalizar.interrupt (bool): Si es True, interrumpe cualquier verbalización que esté en curso. Útil para notificaciones importantes.context (str): Define qué motor de voz usar en modo híbrido. Puede ser 'ui' (generalmente NVDA) o 'subtitle' (generalmente SAPI).# Dentro de la clase de tu plugin
def mi_accion(self):
self.app_context.speak("Realizando acción importante.", interrupt=True)
# ...código de la acción...
self.app_context.speak("Acción completada.")
show_text_input_dialog(title, message)Muestra un diálogo para pedirle texto al usuario.
# Dentro de la clase de tu plugin
def preguntar_nombre(self):
nombre = self.app_context.show_text_input_dialog(
title="Pregunta",
message="¿Cómo te llamas?"
)
if nombre:
print(f"El usuario se llama {nombre}")
show_read_only_text_dialog(title, text)Muestra un diálogo con texto que no se puede editar, útil para mostrar información.
# Dentro de la clase de tu plugin
def mostrar_info(self):
info = "Esta es una información importante que el usuario debe leer."
self.app_context.show_read_only_text_dialog("Información del Plugin", info)
register_button(parent_sizer, button_text, callback, tooltip="")Añade un nuevo botón a un sizer específico en la interfaz principal. El parent_sizer debe ser obtenido de app_context.get_plugin_buttons_sizer().
# Dentro de la clase de tu plugin
def on_mi_boton_click(self, event):
wx.MessageBox("¡Botón del plugin clickeado!", "Info")
def load(self, app_context):
self.app_context = app_context
plugin_sizer = self.app_context.get_plugin_buttons_sizer()
if plugin_sizer:
self.app_context.register_button(
parent_sizer=plugin_sizer,
button_text="Mi Botón",
callback=self.on_mi_boton_click,
tooltip="Este es un botón añadido por mi plugin."
)
unregister_button(parent_sizer, button_text)Elimina un botón previamente registrado por el plugin. Es crucial llamar a este método en el unload de tu plugin para limpiar la interfaz.
# Dentro de la clase de tu plugin
def unload(self):
plugin_sizer = self.app_context.get_plugin_buttons_sizer()
if plugin_sizer:
self.app_context.unregister_button(
parent_sizer=plugin_sizer,
button_text="Mi Botón" # Debe coincidir con el texto usado en register_button
)
get_plugin_buttons_sizer()Devuelve el sizer principal donde los plugins pueden añadir sus propios botones. Utiliza este sizer como parent_sizer para register_button.
# Dentro de la clase de tu plugin
def load(self, app_context):
self.app_context = app_context
my_sizer = self.app_context.get_plugin_buttons_sizer()
if my_sizer:
# Ahora puedes usar my_sizer con register_button
pass
create_dialog(title)Para interfaces más complejas, puedes construir tus propios diálogos personalizados. Este método devuelve un objeto "constructor" que te permite añadir componentes.
register_context_menu_item(target_ui_element_id, item_text, callback)Añade una opción a un menú contextual de un elemento de la interfaz de usuario.
target_ui_element_id: Un identificador para el elemento de la UI al que se adjuntará el menú contextual. Actualmente, el único valor soportado es "main_panel_context_menu" para el menú contextual del panel principal.
# Dentro de la clase de tu plugin
def on_context_menu_action(self, event):
wx.MessageBox("¡Has hecho clic en una opción del menú contextual del plugin!", "Info")
def load(self, app_context):
self.app_context = app_context
self.app_context.register_context_menu_item(
target_ui_element_id="main_panel_context_menu",
item_text="Acción de Plugin (Contextual)",
callback=self.on_context_menu_action
)
unregister_context_menu_item(target_ui_element_id, item_text)Elimina una opción de un menú contextual previamente registrada por el plugin. Es crucial llamar a este método en el unload de tu plugin para limpiar la interfaz.
# Dentro de la clase de tu plugin
def unload(self):
self.app_context.unregister_context_menu_item(
target_ui_element_id="main_panel_context_menu",
item_text="Acción de Plugin (Contextual)"
)
Métodos del objeto diálogo:
add_label(text, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un texto estático.add_text_input(name, initial_value="", parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un campo de texto. name es un ID único para leer su valor después.add_list_box(name, items=[], on_select=None, parent_sizer=None, proportion=1, flag=wx.ALL | wx.EXPAND, border=5): Añade una lista de opciones.add_button(label, callback, parent_sizer=None, proportion=0, flag=wx.ALL | wx.ALIGN_CENTER, border=5): Añade un botón.add_sizer(name, orientation=wx.VERTICAL, proportion=0, flag=wx.EXPAND, border=0, parent_sizer=None): Añade un nuevo sizer (contenedor de diseño) al diálogo. Permite organizar los controles de forma horizontal, vertical o en cuadrícula.add_checkbox(name, label, initial_value=False, on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade una casilla de verificación.add_radio_button(name, label, initial_value=False, on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un botón de radio individual.add_radio_box(name, label, choices, initial_selection=0, on_change=None, parent_sizer=None, major_dimension=0, style=wx.RA_SPECIFY_ROWS, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un grupo de botones de radio.add_combobox(name, choices, initial_value="", on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un combobox (lista desplegable).add_spin_ctrl(name, initial_value=0, min_value=0, max_value=100, on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un control numérico (SpinCtrl).add_slider(name, initial_value=0, min_value=0, max_value=100, on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un control deslizante (Slider).add_file_picker(name, message="Selecciona un archivo", default_path="", wildcard="*.*", on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un selector de archivos (FilePickerCtrl).add_dir_picker(name, message="Selecciona un directorio", default_path="", on_change=None, parent_sizer=None, proportion=0, flag=wx.ALL | wx.EXPAND, border=5): Añade un selector de directorios (DirPickerCtrl).add_notebook(name, parent_sizer=None, proportion=1, flag=wx.EXPAND | wx.ALL, border=5): Añade un control de pestañas (Notebook).add_notebook_page(notebook, page_name, parent_sizer=None): Añade una nueva página a un Notebook.get_value(name): Obtiene el valor actual de un control (text_input, list_box, checkbox, radio_button, radio_box, combobox, spin_ctrl, slider, file_picker, dir_picker) usando el name que le diste.show(): Muestra el diálogo al usuario.close(): Cierra el diálogo.Parámetros comunes para los métodos add_:
name (str): Un identificador único para el control, usado para recuperar su valor con get_value().parent_sizer (wx.Sizer, optional): El sizer al que se añadirá el control. Si no se especifica, se añade al sizer principal del diálogo.proportion (int, optional): Factor de crecimiento del control dentro del sizer.flag (int, optional): Banderas de alineación y expansión (ej. wx.EXPAND | wx.ALL).border (int, optional): Espacio en píxeles alrededor del control.Ejemplo de uso:
# Dentro de la clase de tu plugin
def on_show_dialog(self, event):
dialog = self.app_context.create_dialog("Ejemplo de Controles UI Avanzados")
# Sizer vertical para nombre y texto
vertical_sizer = dialog.add_sizer("vertical_group", wx.VERTICAL, flag=wx.EXPAND | wx.ALL, border=10)
dialog.add_label("Introduce tu nombre:", parent_sizer=vertical_sizer)
dialog.add_text_input("user_name", initial_value="Anónimo", parent_sizer=vertical_sizer)
# Sizer horizontal para checkbox y combobox
horizontal_sizer = dialog.add_sizer("horizontal_group", wx.HORIZONTAL, flag=wx.EXPAND | wx.ALL, border=10)
dialog.add_checkbox("enable_feature", "Habilitar Característica", initial_value=True, parent_sizer=horizontal_sizer)
dialog.add_combobox("options_combo", ["Opción 1", "Opción 2", "Opción 3"], initial_value="Opción 2", parent_sizer=horizontal_sizer)
# Radio Box
dialog.add_radio_box("choice_radio", "Elige una opción", ["Primero", "Segundo", "Tercero"], initial_selection=1)
# Notebook (pestañas)
notebook = dialog.add_notebook("my_notebook")
# Página 1 del Notebook
page1_panel, page1_sizer = dialog.add_notebook_page(notebook, "Pestaña Uno")
dialog.add_label("Contenido de la Pestaña Uno", parent_sizer=page1_sizer)
# Página 2 del Notebook
page2_panel, page2_sizer = dialog.add_notebook_page(notebook, "Pestaña Dos")
dialog.add_label("Contenido de la Pestaña Dos", parent_sizer=page2_sizer)
# Botón para obtener valores
dialog.add_button("Obtener Valores", callback=lambda e: self.on_get_values(dialog))
dialog.add_button("Cerrar", callback=dialog.close)
dialog.show()
def on_get_values(self, dialog):
name = dialog.get_value("user_name")
feature_enabled = dialog.get_value("enable_feature")
selected_option = dialog.get_value("options_combo")
radio_choice = dialog.get_value("choice_radio")
message = (
f"Nombre: {name}\n"
f"Característica Habilitada: {feature_enabled}\n"
f"Opción Seleccionada: {selected_option}\n"
f"Elección de Radio: {radio_choice}"
)
wx.MessageBox(message, "Valores Obtenidos", wx.OK | wx.ICON_INFORMATION)
# No cerramos el diálogo aquí para que el usuario pueda ver los valores y seguir interactuando
Se han introducido mejoras significativas para ofrecer mayor flexibilidad y una mejor experiencia de usuario al crear diálogos personalizados para tus plugins.
add_custom_panel(custom_panel: wx.Panel, parent_sizer=None, proportion=1, flag=wx.EXPAND | wx.ALL, border=5)Este método permite integrar un wx.Panel completamente personalizado, creado por tu plugin, directamente en el PluginDialog. Esto te otorga acceso total a la API de wxPython para diseñar interfaces de usuario complejas y únicas.
custom_panel (wx.Panel): La instancia de wx.Panel que has creado y configurado con tus propios widgets y sizers.parent_sizer, proportion, flag, border) funcionan de manera similar a otros métodos add_, controlando cómo se posiciona tu panel personalizado dentro del diálogo.Ventajas:
wxPython, sizers complejos, dibujo personalizado, y manejo de eventos avanzado dentro de tu panel.add_ predefinidos.Consideraciones:
custom_panel debe ser una instancia de wx.Panel (o una subclase) y debe ser hijo del panel interno del PluginDialog (ej. MyCustomPanel(dialog.panel)).AppContext explícitamente (ej. en su constructor).# Dentro de la clase de tu plugin
import wx
from utils.plugin_ui import PluginDialog # Asegúrate de importar PluginDialog
from utils.app_context import AppContext # Asegúrate de importar AppContext
class MyComplexCustomPanel(wx.Panel):
def __init__(self, parent, app_context: AppContext):
super().__init__(parent)
self.app_context = app_context
# ... aquí construyes tu UI compleja con cualquier widget de wxPython ...
# Por ejemplo, un wx.ListCtrl, wx.GridBagSizer, etc.
# Puedes acceder a self.app_context para interactuar con la aplicación.
class MiPlugin(MLPlayerPlugin):
# ...
def show_mi_dialogo_avanzado(self, event):
dialog = PluginDialog(self.app_context.get_main_window(), "Mi Diálogo Avanzado")
# Crear una instancia de tu panel personalizado, pasándole el app_context
custom_panel = MyComplexCustomPanel(dialog.panel, self.app_context)
# Añadir el panel personalizado al PluginDialog
dialog.add_custom_panel(custom_panel, proportion=1, flag=wx.EXPAND | wx.ALL, border=10)
# Puedes seguir añadiendo botones estándar del PluginDialog si lo deseas
dialog.add_button("Cerrar", lambda e: dialog.close(), parent_sizer=dialog.main_sizer)
dialog.show()
Ahora, los diálogos creados con PluginDialog pueden persistir en memoria mientras el plugin esté cargado, en lugar de ser destruidos y recreados cada vez que se abren. Esto permite que el estado de los controles y los datos dentro del diálogo se mantengan entre aperturas.
Para aprovechar esta característica, tu plugin debe almacenar una referencia a la instancia de PluginDialog y reutilizarla:
# Dentro de la clase de tu plugin
class MiPlugin(MLPlayerPlugin):
def __init__(self, metadata: dict):
super().__init__(metadata)
self.my_dialog_instance = None # Almacenar la instancia del diálogo
def load(self, app_context: AppContext):
self.app_context = app_context
# ... registrar menú para abrir el diálogo ...
def unload(self):
# Es crucial destruir el diálogo cuando el plugin se descarga
if self.my_dialog_instance and self.my_dialog_instance.dialog:
self.my_dialog_instance.dialog.Destroy()
self.my_dialog_instance = None
def show_mi_dialogo(self, event):
if not self.my_dialog_instance or not self.my_dialog_instance.dialog or not self.my_dialog_instance.dialog.IsBeingDeleted():
# Crear una nueva instancia si no existe o ha sido destruida
dialog = PluginDialog(self.app_context.get_main_window(), "Mi Diálogo Persistente")
# ... añadir controles al diálogo ...
self.my_dialog_instance = dialog
# Mostrar el diálogo (ya sea nuevo o existente)
self.my_dialog_instance.show()
Todos los diálogos creados con PluginDialog ahora incluyen un manejador por defecto para la tecla Escape. Al presionar Escape, el diálogo se cerrará automáticamente, mejorando la usabilidad.
No necesitas implementar esto manualmente en tus diálogos; el comportamiento ya está integrado.
Puedes controlar todos los aspectos de la reproducción.
get_playback_status()Devuelve un diccionario con el estado actual del reproductor.
# Dentro de la clase de tu plugin
def chequear_estado(self):
status = self.app_context.get_playback_status()
# status -> {'state': 'playing', 'position_ms': 34500, 'duration_ms': 240000}
if status and status['state'] == 'playing':
print(f"La canción va por el segundo {status['position_ms'] // 1000}")
toggle_playback()Pausa o reanuda la reproducción.
self.app_context.toggle_playback()
play_media(path)Reproduce un nuevo archivo o URL, reemplazando la lista actual. Requiere permiso file_system_read para archivos locales.
# Reproducir un archivo local
self.app_context.play_media("C:\\Music\\song.mp3")
# Reproducir una URL
self.app_context.play_media("https://example.com/stream.ogg")
play_url_from_plugin(url)Permite a un plugin reproducir una URL directamente. Requiere permiso network_access.
# Reproducir una URL desde el plugin
self.app_context.play_url_from_plugin("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
stop()Detiene la reproducción por completo.
self.app_context.stop()
seek(milliseconds)Salta a un punto específico de la canción.
# Saltar al segundo 30
self.app_context.seek(30000)
set_volume(level)Ajusta el volumen de 0 a 100.
# Poner el volumen al 75%
self.app_context.set_volume(75)
Puedes leer y manipular la lista de reproducción actual.
get_playlist_items()Devuelve la lista actual como una lista de tuplas (ruta, es_url, titulo).
# Dentro de la clase de tu plugin
def imprimir_lista(self):
items = self.app_context.get_playlist_items()
for ruta, es_url, titulo in items:
print(f"- {titulo}")
add_to_playlist(path)Añade una nueva pista al final de la lista actual. Requiere permiso file_system_read para archivos locales.
self.app_context.add_to_playlist("D:\\podcast.mp3")
clear_playlist()Vacía por completo la lista de reproducción y detiene la reproducción.
self.app_context.clear_playlist()
play_playlist_item(index)Reproduce una canción de la lista usando su índice (empezando en 0).
# Reproducir la primera canción de la lista
self.app_context.play_playlist_item(0)
Tu plugin puede reaccionar a eventos que ocurren en la aplicación.
subscribe_to_event(event_name, callback)Registra una función de tu plugin para que sea llamada cuando ocurra un evento.
Eventos Disponibles:
media_loaded(path, title): Cuando se carga una nueva pista.media_ended: Al finalizar una pista.playback_paused: Cuando la reproducción se pausa.playback_resumed: Cuando la reproducción se reanuda.volume_changed(volume): Al cambiar el volumen.seeked(position): Cuando el usuario salta a un punto de la canción.audio_track_changed(track_id): Al cambiar la pista de audio (en videos).playlist_updated: Cuando la lista de reproducción es modificada.next_track(path, title): Cuando se avanza a la siguiente pista en la playlist.previous_track(path, title): Cuando se retrocede a la pista anterior en la playlist.app_closing: Justo antes de que la aplicación se cierre.Ejemplo de uso:
# Dentro de la clase de tu plugin
def load(self, app_context):
self.app_context = app_context
# Nos suscribimos al evento de carga de un nuevo medio
self.app_context.subscribe_to_event('media_loaded', self.on_nueva_cancion)
self.app_context.subscribe_to_event('app_closing', self.on_cerrar_app)
def unload(self):
# Es MUY importante desuscribirse de los eventos al descargar el plugin
self.app_context.unsubscribe_from_event('media_loaded', self.on_nueva_cancion)
self.app_context.unsubscribe_from_event('app_closing', self.on_cerrar_app)
def on_nueva_cancion(self, path, title):
print(f"El plugin dice: Empezó a sonar '{title}'")
def on_cerrar_app(self):
print("El plugin dice: ¡La aplicación se está cerrando! Adiós.")
Puedes obtener información de la pista actual y guardar la configuración de tu plugin.
get_current_track_info()Devuelve un diccionario con metadatos de la pista actual.
# Dentro de la clase de tu plugin
def obtener_artista(self):
info = self.app_context.get_current_track_info()
if info and 'artist' in info:
print(f"El artista es {info['artist']}")
else:
print("No se encontró información del artista.")
get_app_config_value(key)Permite a los plugins leer valores de configuración global de la aplicación. Solo se pueden acceder a las claves de configuración permitidas por seguridad.
Claves de configuración permitidas (whitelist):
show_extension_in_title: (bool) Si se muestra la extensión del archivo en el título.history_limit: (int) Límite de elementos en el historial de reproducción.voice_output_enabled: (bool) Si la salida de voz está habilitada.verbalize_shuffle_mode: (bool) Si se verbaliza el modo aleatorio.verbalize_repeat_mode: (bool) Si se verbaliza el modo de repetición.speech_engine_mode: (str) Modo del motor de voz (ej. 'hybrid', 'nvda_only', 'sapi_only').update_channel: (str) Canal de actualización (ej. 'stable', 'beta').language: (str) Idioma de la aplicación (ej. 'es', 'en').allow_multiple_instances: (bool) Si se permiten múltiples instancias de la aplicación.show_adult_content: (bool) Si se muestra contenido para adultos.# Dentro de la clase de tu plugin
def verificar_configuracion(self):
idioma_app = self.app_context.get_app_config_value('language')
if idioma_app:
print(f"El idioma actual de la aplicación es: {idioma_app}")
else:
print("No se pudo obtener el idioma de la aplicación o la clave no está permitida.")
mostrar_adulto = self.app_context.get_app_config_value('show_adult_content')
print(f"Mostrar contenido para adultos: {mostrar_adulto}")
save_plugin_config(config_dict) y load_plugin_config()Permiten a tu plugin guardar y cargar un diccionario de configuración, que persistirá entre sesiones.
Ejemplo de uso:
# Dentro de la clase de tu plugin
def load(self, app_context):
self.app_context = app_context
# Cargar nuestra configuración
self.settings = self.app_context.load_plugin_config()
if not self.settings: # Si no hay configuración, usar valores por defecto
self.settings = {'api_key': None, 'user_name': 'default'}
self.app_context.register_menu_item("Mi Plugin", "Guardar API Key", self.guardar_api_key)
def guardar_api_key(self, event):
key = self.app_context.show_text_input_dialog("API Key", "Introduce tu API Key:")
if key:
self.settings['api_key'] = key
# Guardar el diccionario de configuración
self.app_context.save_plugin_config(self.settings)
print("API Key guardada.")
.ml-plugin. Se le presenta la información del plugin y los permisos que solicita. Si acepta, el plugin se añade al sistema.lib/plugins/.load): Si el plugin está habilitado, se crea una instancia de tu clase y se llama al método load(). Aquí es donde debes registrar menús, suscribirte a eventos, etc.unload): Cuando la aplicación se cierra o el plugin se deshabilita/desinstala, se llama al método unload(). Debes usar este método para limpiar cualquier recurso, y especialmente para desuscribirte de los eventos a los que te hayas suscrito.Para facilitar la distribución a usuarios finales, es recomendable empaquetar tu plugin en un archivo .ml-plugin. Esto agrupa todos los archivos necesarios en un solo paquete, es más seguro y se ve más profesional.
El sistema puede cargar tanto archivos .py sueltos (para desarrollo) como paquetes .ml-plugin.
Un paquete es simplemente un archivo .zip con la extensión cambiada a .ml-plugin. El contenido del paquete debe estar estructurado de la siguiente manera:
plugin.json (El archivo de manifiesto, obligatorio)mi_codigo.py (El archivo principal de tu plugin)plugin_deps/ (Carpeta que contiene las dependencias externas del plugin)un_icono.png (Cualquier otro recurso que necesites)otro_modulo.py (Otros módulos de Python que tu plugin use)plugin.jsonEste archivo es obligatorio y debe estar en la raíz del paquete.
Formato del plugin.json:
{
"name": "Nombre Descriptivo de tu Plugin",
"version": "1.0.0",
"author": "Tu Nombre o Nickname",
"description": "Una breve descripción de lo que hace tu plugin.",
"main_file": "mi_codigo.py",
"permissions": {
"network_access": "Necesita acceso a internet para buscar letras de canciones.",
"file_system_read": "Necesita leer archivos para añadir canciones a la lista de reproducción."
},
"dependencies": [
"requests",
"beautifulsoup4"
]
}
name, version, author, description, main_file: Campos obligatorios.permissions: Un diccionario donde cada clave es el nombre de un permiso y el valor es una cadena explicando por qué el plugin necesita ese permiso.dependencies: (Nuevo) Una lista de las librerías de Python que tu plugin necesita para funcionar.Para asegurar la compatibilidad y evitar conflictos de versiones, los plugins de ML-Player deben empaquetar sus propias dependencias externas. Esto significa que cada plugin es autocontenido y no depende de las librerías instaladas globalmente en el sistema del usuario o por otros plugins.
La clave "dependencies" en tu archivo plugin.json es ahora **informativa**. Puedes listar las librerías que tu plugin utiliza, pero ML-Player no las instalará automáticamente. Es responsabilidad del desarrollador incluir estas librerías dentro del paquete del plugin.
¿Cómo funciona?
.ml-plugin, debes crear una carpeta llamada plugin_deps/. Todas las librerías de Python que tu plugin necesite y que no estén incluidas en la instalación base de ML-Player deben copiarse en esta carpeta.plugin_deps/ de tu plugin al `sys.path` de Python. Esto permite que tu plugin importe sus propias versiones de las librerías sin interferir con otros plugins o con ML-Player. Una vez que tu plugin se descarga, esta ruta se elimina del `sys.path`.Recomendación para desarrolladores: Utiliza un entorno virtual (venv) para desarrollar tu plugin. Instala tus dependencias en ese entorno y luego copia las carpetas de las librerías (por ejemplo, colorama/, requests/) desde el directorio venv/Lib/site-packages/ de tu entorno virtual a la carpeta plugin_deps/ de tu plugin.
.ml-pluginplugin.json a la carpeta, incluyendo las secciones de permissions y dependencies si son necesarias.plugin_deps/) y comprímelos en un archivo .zip. No comprimas la carpeta contenedora, sino su contenido..zip a .ml-plugin.¡Y listo! Ese archivo .ml-plugin es el que puedes compartir con otros usuarios.