Mopidy is an extensible music server written in Python
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Mopidy's extension system provides a plugin architecture that allows third-party developers to add new music sources, frontends, and other functionality. Extensions integrate seamlessly with Mopidy's configuration system, dependency management, and component registry.
The foundational class for creating Mopidy extensions with configuration, lifecycle management, and component registration.
class Extension:
"""
Base class for Mopidy extensions providing plugin functionality.
Attributes:
- dist_name (str): Distribution name as registered on PyPI
- ext_name (str): Extension short name for configuration
- version (str): Extension version string
"""
dist_name: str # e.g., "Mopidy-Spotify"
ext_name: str # e.g., "spotify"
version: str # e.g., "4.1.1"
def get_default_config(self):
"""
Get default configuration for this extension.
Returns:
- str: Default configuration as INI format string
"""
...
def get_config_schema(self):
"""
Get configuration schema for validation.
Returns:
- ConfigSchema: Schema for extension configuration
"""
...
def validate_environment(self):
"""
Validate that extension can run in current environment.
Raises ExtensionError if environment is invalid.
"""
...
def setup(self, registry):
"""
Register extension components with Mopidy.
Parameters:
- registry: Component registry for registration
"""
...
def get_cache_dir(self, config):
"""
Get cache directory for this extension.
Parameters:
- config: Mopidy configuration
Returns:
- pathlib.Path: Cache directory path
"""
...
def get_config_dir(self, config):
"""
Get configuration directory for this extension.
Parameters:
- config: Mopidy configuration
Returns:
- pathlib.Path: Configuration directory path
"""
...
def get_data_dir(self, config):
"""
Get data directory for this extension.
Parameters:
- config: Mopidy configuration
Returns:
- pathlib.Path: Data directory path
"""
...Usage example:
from mopidy import config
from mopidy.ext import Extension
class SpotifyExtension(Extension):
dist_name = "Mopidy-Spotify"
ext_name = "spotify"
version = "4.1.1"
def get_default_config(self):
return """\
[spotify]
enabled = true
username =
password =
client_id =
client_secret =
"""
def get_config_schema(self):
schema = super().get_config_schema()
schema["username"] = config.String()
schema["password"] = config.Secret()
schema["client_id"] = config.String()
schema["client_secret"] = config.Secret()
return schema
def validate_environment(self):
try:
import spotipy
except ImportError as e:
raise ExtensionError("Spotify extension requires spotipy library") from e
def setup(self, registry):
from .backend import SpotifyBackend
from .frontend import SpotifyWebApp
registry.add("backend", SpotifyBackend)
registry.add("frontend", SpotifyWebApp)Data structure containing extension information and configuration for the system.
class ExtensionData(NamedTuple):
"""
Container for extension metadata and configuration.
Attributes:
- extension (Extension): Extension instance
- entry_point: setuptools entry point object
- config_schema (ConfigSchema): Extension configuration schema
- config_defaults: Default configuration values
- command (Command, optional): Extension CLI command
"""
extension: Extension
entry_point: Any
config_schema: ConfigSchema
config_defaults: Any
command: Optional[Command]Functions for discovering, loading, and validating extensions at runtime.
def load_extensions():
"""
Load all available Mopidy extensions.
Returns:
- list[ExtensionData]: Loaded extension data
"""
...
def get_extensions_by_name():
"""
Get extensions mapped by name.
Returns:
- dict[str, ExtensionData]: Extensions by name
"""
...
def validate_extension(extension_data):
"""
Validate that extension can be used.
Parameters:
- extension_data (ExtensionData): Extension to validate
Returns:
- bool: True if extension is valid
"""
...Registry system for managing extension-provided components like backends, frontends, and mixers.
class Registry:
"""Registry for extension-provided components."""
def add(self, component_type, component_class):
"""
Register a component class.
Parameters:
- component_type (str): Type of component ('backend', 'frontend', 'mixer')
- component_class: Component class to register
"""
...
def get(self, component_type):
"""
Get all registered components of a type.
Parameters:
- component_type (str): Component type to retrieve
Returns:
- list: Registered component classes
"""
...
def get_by_name(self, component_type, name):
"""
Get specific component by name.
Parameters:
- component_type (str): Component type
- name (str): Component name
Returns:
- Component class or None
"""
...Usage example:
def setup(self, registry):
from .backend import MyBackend
from .frontend import MyFrontend
from .mixer import MyMixer
registry.add("backend", MyBackend)
registry.add("frontend", MyFrontend)
registry.add("mixer", MyMixer)Extensions integrate with Mopidy's configuration system through schemas and defaults.
def get_config_schemas(extensions_data):
"""
Get configuration schemas from extensions.
Parameters:
- extensions_data (list[ExtensionData]): Extension data
Returns:
- list[ConfigSchema]: Configuration schemas
"""
...
def get_config_defaults(extensions_data):
"""
Get default configuration from extensions.
Parameters:
- extensions_data (list[ExtensionData]): Extension data
Returns:
- list[str]: Default configuration strings
"""
...Extensions can provide command-line interface commands that integrate with Mopidy's CLI system.
class Command:
"""Base class for extension CLI commands."""
help: str # Help text for the command
def __init__(self):
...
def add_to_parser(self, parser):
"""
Add command arguments to argument parser.
Parameters:
- parser: Argument parser instance
"""
...
def run(self, args, config, extensions_data):
"""
Execute the command.
Parameters:
- args: Parsed command arguments
- config: Mopidy configuration
- extensions_data (list[ExtensionData]): Available extensions
Returns:
- int: Exit code
"""
...Usage example:
from mopidy.commands import Command
class ScanCommand(Command):
help = "Scan local music library"
def add_to_parser(self, parser):
parser.add_argument(
"--force",
action="store_true",
help="Force full rescan"
)
def run(self, args, config, extensions_data):
print("Scanning music library...")
# Implement scan logic
return 0
# In extension setup
def get_command(self):
return ScanCommand()Guidelines for packaging and distributing extensions.
# setup.py example for extensions
from setuptools import setup
setup(
name="Mopidy-MyExtension",
version="1.0.0",
packages=["mopidy_myextension"],
entry_points={
"mopidy.ext": [
"myextension = mopidy_myextension:Extension"
]
},
install_requires=[
"Mopidy >= 3.0.0",
"Pykka >= 2.0.1",
# Extension-specific dependencies
]
)Best practices for validating extension environments and dependencies.
def validate_environment(self):
"""Validate extension environment and dependencies."""
# Check required dependencies
try:
import required_library
except ImportError as e:
raise ExtensionError(
f"Extension requires 'required_library' package"
) from e
# Check version requirements
if required_library.__version__ < "2.0.0":
raise ExtensionError(
f"Extension requires required_library >= 2.0.0, "
f"found {required_library.__version__}"
)
# Check system requirements
if not os.path.exists("/dev/audio"):
raise ExtensionError("Extension requires audio device")
# Check configuration
config = self.get_config()
if not config["api_key"]:
raise ExtensionError("Extension requires API key configuration")Extensions can respond to system lifecycle events for initialization and cleanup.
class Extension:
def on_start(self):
"""Called when Mopidy starts up."""
...
def on_stop(self):
"""Called when Mopidy shuts down."""
...Mopidy includes several built-in extensions that demonstrate the extension system:
class HttpExtension(Extension):
"""HTTP frontend providing web interface and API."""
dist_name = "Mopidy"
ext_name = "http"class FileExtension(Extension):
"""Local file backend for playing music files."""
dist_name = "Mopidy"
ext_name = "file"class M3uExtension(Extension):
"""M3U playlist backend for .m3u playlist files."""
dist_name = "Mopidy"
ext_name = "m3u"class StreamExtension(Extension):
"""Stream backend for internet radio and streaming URLs."""
dist_name = "Mopidy"
ext_name = "stream"class SoftwareMixerExtension(Extension):
"""Software-based audio mixer implementation."""
dist_name = "Mopidy"
ext_name = "softwaremixer"def get_config_schema(self):
schema = super().get_config_schema()
# Use appropriate config types
schema["api_endpoint"] = config.String()
schema["api_key"] = config.Secret() # For sensitive data
schema["timeout"] = config.Integer(minimum=1, maximum=300)
schema["enabled_features"] = config.List()
schema["cache_size"] = config.Integer(minimum=0)
return schemafrom mopidy.exceptions import ExtensionError
class MyExtension(Extension):
def validate_environment(self):
try:
self._check_dependencies()
self._check_credentials()
except Exception as e:
raise ExtensionError(f"MyExtension setup failed: {e}") from eimport logging
logger = logging.getLogger(__name__)
class MyExtension(Extension):
def setup(self, registry):
logger.info("Setting up MyExtension")
try:
# Setup logic
registry.add("backend", MyBackend)
logger.info("MyExtension setup complete")
except Exception as e:
logger.error(f"MyExtension setup failed: {e}")
raise