Plugin Development
Plugin Development Guide
Section titled “Plugin Development Guide”This guide explains how to develop plugins for Sublarr, including provider plugins, translation backends, and utility tools.
Table of Contents
Section titled “Table of Contents”- Plugin Architecture
- Creating a Provider Plugin
- Creating a Translation Backend
- Plugin Structure
- API Reference
- Best Practices
- Testing
- Publishing
Plugin Architecture
Section titled “Plugin Architecture”Sublarr uses a plugin system that allows developers to extend functionality without modifying the core codebase. Plugins are Python modules that implement specific interfaces.
Plugin Types
Section titled “Plugin Types”- Provider Plugins: Add new subtitle sources (e.g., custom websites, APIs)
- Translation Backends: Add new translation services (e.g., custom LLM APIs)
- Tool Plugins: Add utility functions (e.g., custom scoring, post-processing)
Plugin Discovery
Section titled “Plugin Discovery”Plugins are discovered automatically from:
- Built-in plugins:
backend/providers/plugins/ - User plugins:
/config/plugins/(configurable)
Creating a Provider Plugin
Section titled “Creating a Provider Plugin”Basic Structure
Section titled “Basic Structure”from providers.base import SubtitleProvider, SubtitleResult
class MyProvider(SubtitleProvider): """My custom subtitle provider."""
name = "MyProvider" priority = 100 enabled = True
def search(self, query): """Search for subtitles.
Args: query: SearchQuery object with file_path, series_title, etc.
Returns: List of SubtitleResult objects """ results = [] # Your search logic here return results
def download(self, subtitle_id, file_path): """Download subtitle file.
Args: subtitle_id: Provider-specific subtitle identifier file_path: Target file path
Returns: Path to downloaded file """ # Your download logic here return file_pathRequired Methods
Section titled “Required Methods”search(query): Search for subtitles matching the querydownload(subtitle_id, file_path): Download a specific subtitle
Optional Methods
Section titled “Optional Methods”test(): Test provider connectivity/credentialsget_config(): Return provider configuration schemavalidate_config(config): Validate configuration
Example: Simple Provider
Section titled “Example: Simple Provider”import requestsfrom providers.base import SubtitleProvider, SubtitleResult
class ExampleProvider(SubtitleProvider): name = "ExampleProvider" priority = 50
def __init__(self): super().__init__() self.api_key = self.get_setting("api_key", "")
def search(self, query): if not self.api_key: return []
# Make API request response = requests.get( "https://api.example.com/search", params={ "title": query.series_title, "season": query.season, "episode": query.episode, }, headers={"Authorization": f"Bearer {self.api_key}"}, )
results = [] for item in response.json().get("results", []): results.append(SubtitleResult( provider=self.name, subtitle_id=item["id"], language=item["language"], format=item["format"], score=self._calculate_score(item, query), download_url=item["download_url"], ))
return results
def download(self, subtitle_id, file_path): response = requests.get( f"https://api.example.com/download/{subtitle_id}", headers={"Authorization": f"Bearer {self.api_key}"}, )
with open(file_path, "wb") as f: f.write(response.content)
return file_path
def _calculate_score(self, item, query): score = 100 # Custom scoring logic if item.get("hash") == query.file_hash: score += 200 return scoreCreating a Translation Backend
Section titled “Creating a Translation Backend”Translation backends implement the TranslationBackend interface:
from translation.base import TranslationBackend
class MyTranslationBackend(TranslationBackend): """My custom translation backend."""
name = "MyTranslation" supports_streaming = False
def translate(self, text, source_lang, target_lang, context=None): """Translate text.
Args: text: Source text to translate source_lang: Source language code (e.g., "en") target_lang: Target language code (e.g., "de") context: Optional context (e.g., glossary entries)
Returns: Translated text """ # Your translation logic here return translated_text
def test(self): """Test backend connectivity.""" try: result = self.translate("test", "en", "de") return True, "Backend is working" except Exception as e: return False, str(e)Plugin Structure
Section titled “Plugin Structure”Directory Layout
Section titled “Directory Layout”my-plugin/├── __init__.py # Plugin entry point├── provider.py # Provider implementation (if provider plugin)├── translation.py # Translation backend (if translation plugin)├── README.md # Plugin documentation├── config.json # Plugin metadata└── requirements.txt # Python dependencies (optional)__init__.py Example
Section titled “__init__.py Example”"""My Plugin - A custom subtitle provider."""
from .provider import MyProvider
# Export the provider class__all__ = ["MyProvider"]config.json Example
Section titled “config.json Example”{ "name": "my-plugin", "version": "1.0.0", "description": "My custom subtitle provider", "author": "Your Name", "type": "provider", "category": "provider", "url": "https://github.com/username/my-plugin", "license": "MIT"}API Reference
Section titled “API Reference”SubtitleProvider Base Class
Section titled “SubtitleProvider Base Class”class SubtitleProvider: name: str # Provider name (required) priority: int = 100 # Search priority (lower = higher priority) enabled: bool = True # Whether provider is enabled
def search(self, query: SearchQuery) -> List[SubtitleResult]: """Search for subtitles.""" pass
def download(self, subtitle_id: str, file_path: str) -> str: """Download subtitle file.""" pass
def test(self) -> Tuple[bool, str]: """Test provider connectivity.""" return True, "OK"
def get_setting(self, key: str, default=None): """Get provider setting.""" passSearchQuery Object
Section titled “SearchQuery Object”class SearchQuery: file_path: str # Path to video file file_hash: str # Video file hash series_title: str # Series title season: int # Season number episode: int # Episode number year: int # Release year release_group: str # Release group name source_lang: str # Source language code target_lang: str # Target language codeSubtitleResult Object
Section titled “SubtitleResult Object”class SubtitleResult: provider: str # Provider name subtitle_id: str # Provider-specific ID language: str # Language code format: str # Format (ass, srt, etc.) score: int # Quality score (0-1000+) download_url: str # Direct download URL (optional) metadata: dict # Additional metadataBest Practices
Section titled “Best Practices”1. Error Handling
Section titled “1. Error Handling”Always handle errors gracefully:
def search(self, query): try: # Your search logic return results except requests.RequestException as e: logger.error(f"{self.name} search failed: {e}") return [] except Exception as e: logger.exception(f"{self.name} unexpected error") return []2. Rate Limiting
Section titled “2. Rate Limiting”Respect rate limits and use the built-in session:
def search(self, query): # Use self.session which has rate limiting built-in response = self.session.get(url, timeout=10) return results3. Caching
Section titled “3. Caching”Use the provider cache for expensive operations:
def search(self, query): cache_key = f"{self.name}:{query.file_hash}" cached = self.get_cache(cache_key) if cached: return cached
results = self._do_search(query) self.set_cache(cache_key, results, ttl=3600) return results4. Configuration
Section titled “4. Configuration”Make your plugin configurable:
def __init__(self): super().__init__() self.api_key = self.get_setting("api_key", "") self.timeout = self.get_setting("timeout", 10) self.max_results = self.get_setting("max_results", 50)5. Logging
Section titled “5. Logging”Use the logger for debugging:
import logging
logger = logging.getLogger(__name__)
def search(self, query): logger.debug(f"Searching {self.name} for {query.series_title}") # Your logic logger.info(f"Found {len(results)} results") return resultsTesting
Section titled “Testing”Unit Tests
Section titled “Unit Tests”Create tests for your plugin:
import pytestfrom my_plugin import MyProvider
def test_provider_search(): provider = MyProvider() query = SearchQuery( file_path="/test/video.mkv", series_title="Test Series", season=1, episode=1, ) results = provider.search(query) assert len(results) > 0 assert results[0].provider == "MyProvider"Integration Tests
Section titled “Integration Tests”Test with real API calls (use test credentials):
def test_provider_download(): provider = MyProvider() provider.api_key = "test_key"
result = provider.download("test_id", "/tmp/test.srt") assert os.path.exists(result)Publishing
Section titled “Publishing”1. Create GitHub Repository
Section titled “1. Create GitHub Repository”- Repository name:
sublarr-{plugin-name} - Include README with installation instructions
- Add license file
2. Submit to Marketplace
Section titled “2. Submit to Marketplace”Create a pull request to the Sublarr Community Plugins repository:
- Fork the repository
- Add your plugin to
registry.json - Create pull request
3. Registry Entry
Section titled “3. Registry Entry”{ "name": "my-plugin", "version": "1.0.0", "description": "My custom subtitle provider", "author": "Your Name", "category": "provider", "url": "https://github.com/username/my-plugin", "install_url": "https://github.com/username/my-plugin.git", "license": "MIT"}Examples
Section titled “Examples”See the built-in plugins in backend/providers/plugins/ for reference implementations:
template/- Template plugin with all featuresopensubtitles/- OpenSubtitles.com providerjimaku/- Jimaku anime provider
Support
Section titled “Support”- GitHub Issues: Sublarr Issues
- Discord: Sublarr Discord
- Reddit: r/Sublarr
- Documentation: sublarr.de/docs