The DRM Pentesting Toolkit uses a sophisticated, modular plugin-based architecture for maximum flexibility and extensibility. Each plugin is a Python script that performs a specific task in the DRM workflow—such as device loading, content discovery, license extraction, or output generation. Plugins are auto-discovered from the plugins/ directory and can be executed individually or chained together in a stack for complex workflows.
- Automatic Function Introspection: The plugin manager automatically detects plugin arguments through Python's
inspectmodule - Dynamic Context Passing: Plugin outputs are merged into a shared context and passed to subsequent plugins
- Credential Integration: Automatic mapping of credentials based on
environment.iniconfiguration - Search Query Support: Built-in search functionality for content discovery and custom filename support
- Separated Data Structure: Distinct handling of MPD and License data with HTTP method support
- Proxy Support: Full proxy support with authenticated and non-authenticated configurations
- Error Handling: Comprehensive error management with detailed logging
- Hot Reloading: Plugin rescanning without application restart
- Subfolder Organization: Support for organizing plugins in subdirectories
- Zero Configuration: New plugins work immediately without modifying core code
- Type Safety: Automatic argument validation and type checking
- State Management: Automatic state passing between plugin executions
- Error Isolation: Plugin failures don't crash the entire application
- Extensibility: Easy to add new DRM systems, platforms, and workflows
Create a new Python file in the plugins/ directory. Use descriptive names and optional numeric prefixes for execution order:
# Simple naming
# plugins/my_content_search.py
# Ordered naming (recommended for workflows)
# plugins/9_my_license_plugin.py
# Subfolder organization
# plugins/custom/my_platform_plugin.pyYour plugin must define a run() function as the entry point. The plugin manager uses function introspection to automatically determine and provide the required arguments:
def run(search_query=None, USER=None, PASS=None, mpd_url=None, license_url=None, **kwargs):
"""
Plugin documentation here.
Args:
search_query: Search term from UI (optional) - also used for custom filenames
USER: Username from credentials (optional)
PASS: Password from credentials (optional)
mpd_url: MPD URL from MPD Data tab (optional)
license_url: License URL from License Data tab (optional)
**kwargs: Additional context variables from previous plugins
Returns:
Dictionary with results and preview
"""
# Your plugin logic here
passdef run(search_query=None, USER=None, PASS=None, mpd_url=None, license_url=None, **kwargs):
"""Example content search plugin with custom filename support."""
if not search_query:
return {
"error": "No search query provided",
"preview": "Search query required"
}
try:
# Use search_query for both content discovery and custom filename
sanitized_filename = search_query.replace(" ", "_").replace(":", "")
# Simulate content search
results = [
{"title": f"{search_query} - Episode 1", "id": "12345"},
{"title": f"{search_query} - Episode 2", "id": "12346"}
]
return {
"search_results": results,
"search_query": search_query,
"preview": f"Found {len(results)} results for '{search_query}'"
}
except Exception as e:
return {
"error": str(e),
"preview": f"Search failed: {e}"
}Device file loading and CDM initialization
- Loads Widevine (
.wvd) and PlayReady (.prd) device files - Creates CDM instances and opens sessions
- Parameters:
device_file - Returns:
device,cdm,session_id,file_path - Dependencies:
pywidevine,pyplayready
MPD manifest parsing and PSSH extraction
- Fetches MPD files from URLs with configurable HTTP methods
- Extracts Widevine PSSH data using regex patterns
- Supports custom headers, cookies, and POST data from MPD Data tab
- Parameters:
mpd_url,mpd_headers,mpd_cookies,mpd_post_data,mpd_method - Returns:
pssh,mpd_content - Dependencies:
requests
License server communication and key extraction
- Sends license challenges to license servers
- Extracts decryption keys from license responses
- Supports custom headers, cookies, and POST data from License Data tab
- Parameters:
license_url,license_headers,license_cookies,license_post_data,license_method,pssh,cdm,session_id - Returns:
keys,license_response - Dependencies:
requests
M3U playlist generation
- Creates M3U playlists with extracted keys and stream URLs
- Handles file I/O with proper directory creation
- Parameters:
keys,mpd_url - Returns:
m3u_content,m3u_file,output_path
Advanced content download using N_m3u8DL-RE
- Downloads encrypted content using the N_m3u8DL-RE tool with decryption keys
- Custom Filename Support: Uses
search_queryparameter for automatic filename generation - Supports multiple key formats and header configurations
- Automatically uses Shaka Packager for optimal output quality
- Parameters:
keys,mpd_url,search_query(for custom filename),headers,cookies - Returns:
download_command,output_path,custom_filename - Dependencies: N_m3u8DL-RE executable, Shaka Packager
- Parameters:
mpd_url,keys,headers,output_format - Returns:
command,downloaded_files,output_directory,return_code - Features:
- Multiple header format support (JSON, dictionary, multi-line)
- Comprehensive error handling and logging
- Automatic working directory management
- Support for multiple decryption keys
- File size reporting and download verification
- Background process management
- Progress monitoring and ETA calculation
- Dependencies:
subprocess,psutil,json
Widevine device validation
- Validates Widevine device file integrity
- Tests CDM functionality and key extraction capability
- Parameters:
device_file,cdm,session_id - Returns:
validation_result,tests_passed,device_info - Dependencies:
pywidevine,requests
PlayReady device validation
- Validates PlayReady device file integrity
- Tests device functionality and license handling
- Parameters:
device_file,cdm,session_id - Returns:
validation_result,tests_passed,device_info - Dependencies:
pyplayready,requests
Shahid platform license generation
- Generates signed license URLs for Shahid VIP platform
- Implements HMAC-SHA256 authentication
- Parameters:
assetId,country_code - Returns:
license_url,signature,timestamp - Dependencies:
requests,hmac,hashlib
def run(search_query=None, USER=None, PASS=None, **kwargs):
"""
Your plugin documentation here.
Args:
search_query: Search term from UI (optional)
USER: Username from credentials (optional)
PASS: Password from credentials (optional)
**kwargs: Additional context variables
Returns:
Dictionary with results and preview
"""
# Your plugin logic here
passdef run(search_query=None, USER=None, PASS=None, **kwargs):
"""Example content search plugin."""
if not search_query:
return {
"error": "No search query provided",
"preview": "Search query required"
}
try:
# Simulate content search
results = [
{"title": f"{search_query} - Episode 1", "id": "12345"},
{"title": f"{search_query} - Episode 2", "id": "12346"}
]
return {
"search_results": results,
"search_query": search_query,
"preview": f"Found {len(results)} results for '{search_query}'"
}
except Exception as e:
return {
"error": str(e),
"preview": f"Search failed: {e}"
}The following variables are commonly available in the plugin context and can be used as parameters in your run() function:
search_query: Search term entered in the UI (also used for custom filenames)device_file: Selected device file path (.wvdor.prd)USER,PASS: Credentials from selected profileproxy_host,proxy_port,proxy_user,proxy_pass: Proxy configuration
mpd_url: MPD manifest URLmpd_headers: Headers for MPD requestsmpd_cookies: Cookies for MPD requestsmpd_post_data: POST data for MPD requestsmpd_method: HTTP method for MPD requests (GET, POST, PUT, etc.)
license_url: License server URLlicense_headers: Headers for license requestslicense_cookies: Cookies for license requestslicense_post_data: POST data for license requestslicense_method: HTTP method for license requests (GET, POST, PUT, etc.)
device: Loaded device object from device loading pluginscdm: CDM instance from device loading pluginssession_id: CDM session ID from device loading pluginspssh: PSSH data extracted from MPD parsing pluginskeys: Decryption keys from license pluginsmpd_content: Raw MPD content from parsing pluginslicense_response: License server response datam3u_content: Generated M3U playlist contentm3u_file: Path to generated M3U fileoutput_path: Download output directory pathcustom_filename: Sanitized filename from search_query
headers: Fallback tompd_headersif not specifiedcookies: Fallback tompd_cookiesif not specifiedpostdata: Fallback tompd_post_dataif not specifiedmethod: Fallback tompd_methodif not specified
headers: HTTP headers as string (multi-line) or dictionarypostdata: POST data for license requests (JSON or raw)cookies: Cookie data for session management
USER/PASS: Username/password credentialsEMAIL/PASSWORD: Email/password credentialsMAC: MAC address for device identificationAPIKEY: API key for service authentication- Any other fields defined in
SlicesAliasconfiguration
- Any keys returned by previous plugins are automatically added to the context
- Context accumulates as plugins execute in sequence
- Downstream plugins can access all previous plugin outputs
The toolkit includes built-in search query functionality that allows users to enter search terms in the main UI for content discovery, filtering, and custom filename generation.
def run(search_query=None, **kwargs):
"""Plugin that uses search query from UI for both search and custom filename."""
if not search_query:
return {
"preview": "No search query provided",
"search_performed": False
}
# Use search query for content discovery
print(f"Searching for: {search_query}")
# Use search query for custom filename generation
import re
sanitized_filename = re.sub(r'[<>:"/\\|?*]', '', search_query.replace(' ', '_'))
# Example: Search multiple platforms
results = []
for platform in ["Platform A", "Platform B", "Platform C"]:
# Simulate platform-specific search
platform_results = search_platform(platform, search_query)
results.extend(platform_results)
return {
"search_query": search_query,
"custom_filename": sanitized_filename,
"search_results": results,
"total_results": len(results),
"preview": f"Found {len(results)} results for '{search_query}'"
}The search_query parameter can be used to generate custom filenames for downloaded content:
def run(search_query=None, keys=None, mpd_url=None, **kwargs):
"""Download plugin with custom filename support."""
import re
import os
# Generate custom filename from search query
if search_query:
# Sanitize the search query for filesystem compatibility
custom_filename = re.sub(r'[<>:"/\\|?*]', '', search_query)
custom_filename = custom_filename.replace(' ', '_').replace(':', '')
# Use in download command
output_file = f"OUTPUT/{custom_filename}"
else:
# Use default filename
output_file = "OUTPUT/downloaded_content"
# Example download command with custom filename
download_command = [
"N_m3u8DL-RE/N_m3u8DL-RE.exe",
mpd_url,
"--save-name", custom_filename,
"--save-dir", "OUTPUT"
]
return {
"custom_filename": custom_filename,
"output_path": output_file,
"download_command": " ".join(download_command),
"preview": f"Download configured with filename: {custom_filename}"
}- Dual Purpose: Use search_query for both content discovery and custom filename generation
- Validation: Always check if
search_queryis provided before using it - Sanitization: Sanitize search queries for safe use in HTTP requests and file paths
- Context Passing: Include the search query and custom filename in your return dictionary
- Error Handling: Provide meaningful error messages when search queries are required but missing
- Matching: Consider case-insensitive matching and partial string matching
The toolkit provides comprehensive proxy support for enhanced privacy and access to geo-restricted content.
Proxy files are stored in the proxies/ directory with .txt extension:
# Non-authenticated proxies (host:port)
128.199.202.122:3128
198.199.86.11:8080
185.125.204.174:80
# Authenticated proxies (host:port:username:password)
proxy.example.com:8080:username:password
premium.proxy.com:3128:user123:pass456
rp.proxyscrape.com:6060:51qmsxyvtf8cmxb:jtx76zocdfgyzwq
# Mixed configurations are supporteddef run(proxies=None, use_proxies=False, **kwargs):
"""Plugin with proxy support."""
proxy_config = None
if use_proxies and proxies:
# Select first available proxy (or implement selection logic)
proxy = proxies[0]
if len(proxy.split(':')) == 4:
# Authenticated proxy
host, port, username, password = proxy.split(':')
proxy_config = {
'http': f'http://{username}:{password}@{host}:{port}',
'https': f'https://{username}:{password}@{host}:{port}'
}
else:
# Non-authenticated proxy
host, port = proxy.split(':')
proxy_config = {
'http': f'http://{host}:{port}',
'https': f'https://{host}:{port}'
}
# Use proxy in requests
response = requests.get(url, proxies=proxy_config)
return {
"proxy_used": proxy_config is not None,
"response_status": response.status_code,
"preview": f"Request completed via {'proxy' if proxy_config else 'direct connection'}"
}- Error Handling: Always handle proxy connection failures gracefully
- Fallback: Implement fallback to direct connection if proxy fails
- Testing: Test proxy connectivity before making important requests
- Security: Don't log proxy credentials in plaintext
"preview": f"Found {len(results)} results for '{search_query}'"- Always check if
search_queryis provided before using it - Include the search query in your return dictionary for downstream plugins
- Provide meaningful error messages when search queries are required but missing
- Consider case-insensitive matching and partial string matching
The toolkit now supports a separated tab structure with distinct configuration for MPD and License requests, each with customizable HTTP methods.
def run(mpd_url=None, mpd_headers=None, mpd_cookies=None, mpd_post_data=None, mpd_method='GET', **kwargs):
"""Plugin that uses MPD data with custom HTTP method."""
import requests
# Prepare headers
headers = {}
if mpd_headers:
for line in mpd_headers.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
headers[key.strip()] = value.strip()
# Prepare cookies
cookies = {}
if mpd_cookies:
for line in mpd_cookies.split('\n'):
if '=' in line:
key, value = line.split('=', 1)
cookies[key.strip()] = value.strip()
# Make request with specified method
if mpd_method.upper() == 'POST':
response = requests.post(mpd_url, headers=headers, cookies=cookies, data=mpd_post_data)
elif mpd_method.upper() == 'PUT':
response = requests.put(mpd_url, headers=headers, cookies=cookies, data=mpd_post_data)
else: # Default to GET
response = requests.get(mpd_url, headers=headers, cookies=cookies)
return {
"mpd_content": response.text,
"status_code": response.status_code,
"preview": f"MPD fetched via {mpd_method.upper()} - {response.status_code}"
}def run(license_url=None, license_headers=None, license_cookies=None,
license_post_data=None, license_method='POST', pssh=None, cdm=None, session_id=None, **kwargs):
"""Plugin that handles license requests with custom HTTP method."""
import requests
if not all([pssh, cdm, session_id]):
return {"error": "Missing CDM data", "preview": "CDM session required"}
# Generate license challenge
challenge = cdm.get_license_challenge(session_id, pssh)
# Prepare headers
headers = {'Content-Type': 'application/octet-stream'}
if license_headers:
for line in license_headers.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
headers[key.strip()] = value.strip()
# Prepare cookies
cookies = {}
if license_cookies:
for line in license_cookies.split('\n'):
if '=' in line:
key, value = line.split('=', 1)
cookies[key.strip()] = value.strip()
# Make license request with specified method
data = license_post_data if license_post_data else challenge
if license_method.upper() == 'GET':
response = requests.get(license_url, headers=headers, cookies=cookies)
elif license_method.upper() == 'PUT':
response = requests.put(license_url, headers=headers, cookies=cookies, data=data)
else: # Default to POST
response = requests.post(license_url, headers=headers, cookies=cookies, data=data)
# Parse license and extract keys
cdm.parse_license(session_id, response.content)
keys = cdm.get_keys(session_id)
return {
"keys": keys,
"license_response": response.content,
"status_code": response.status_code,
"preview": f"License via {license_method.upper()} - {len(keys)} keys extracted"
}The plugin manager maintains backward compatibility with legacy plugins using the old parameter names:
def run(headers=None, cookies=None, postdata=None, method='GET', **kwargs):
"""Legacy plugin - automatically mapped to new structure."""
# This plugin will receive:
# - headers -> mpd_headers (if mpd context) or license_headers (if license context)
# - cookies -> mpd_cookies or license_cookies
# - postdata -> mpd_post_data or license_post_data
# - method -> mpd_method or license_methodThe toolkit provides comprehensive proxy support for enhanced privacy and access to geo-restricted content.
Proxy files are stored in the proxies/ directory with .txt extension:
# Non-authenticated proxies (host:port)
128.199.202.122:3128
198.199.86.11:8080
185.125.204.174:80
# Authenticated proxies (host:port:username:password)
proxy.example.com:8080:username:password
premium.proxy.com:3128:user123:pass456
rp.proxyscrape.com:6060:51qmsxyvtf8cmxb-country-vg:jtx76zocdfgyzwq
# Mixed configurations
free.proxy.com:8080
authenticated.proxy.net:3128:myuser:mypassdef run(use_proxies=False, proxies=None, search_query=None, **kwargs):
"""Plugin demonstrating proxy usage."""
import requests
import random
if not search_query:
return {"error": "Search query required", "preview": "No search query"}
session = requests.Session()
proxy_info = "Direct connection"
# Configure proxy if enabled
if use_proxies and proxies:
# Select a random proxy for load balancing
proxy = random.choice(proxies)
# Format proxy URL
if proxy.get("username") and proxy.get("password"):
proxy_url = f"http://{proxy['username']}:{proxy['password']}@{proxy['host']}:{proxy['port']}"
else:
proxy_url = f"http://{proxy['host']}:{proxy['port']}"
# Configure session with proxy
session.proxies = {
'http': proxy_url,
'https': proxy_url
}
proxy_info = f"Proxy: {proxy['host']}:{proxy['port']}"
if proxy.get("username"):
proxy_info += " (authenticated)"
try:
# Make request with or without proxy
response = session.get(f"https://api.example.com/search?q={search_query}")
return {
"search_query": search_query,
"proxy_used": proxy_info,
"status_code": response.status_code,
"preview": f"Search completed via {proxy_info}"
}
except Exception as e:
return {
"error": str(e),
"preview": f"Request failed: {e}"
}
finally:
session.close()Each proxy in the proxies list is a dictionary:
{
"host": "proxy.example.com",
"port": "8080",
"username": "user123", # None for non-authenticated proxies
"password": "pass456", # None for non-authenticated proxies
"type": "http" # Currently only HTTP proxies supported
}- Always check if
use_proxiesis enabled before using proxy configuration - Handle proxy connection failures gracefully with fallbacks
- Use
session.close()in afinallyblock to clean up connections - Consider proxy rotation for load balancing and redundancy
- Test proxy functionality with connection timeouts
The toolkit features a sophisticated credential management system that automatically maps credentials to plugin parameters based on configuration patterns.
Configure credential types in settings/environment.ini:
[WORDLIST TYPE]
Name=Credentials
Regex=^.*:.*$
Verify=True
Separator=:
Slices=USERNAME,PASSWORD
SlicesAlias=USER,PASS
[EMAIL TYPE]
Name=Email
Regex=^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}:.*$
Verify=True
Separator=:
Slices=EMAIL,PASSWORD
SlicesAlias=EMAIL,PASS
[API TYPE]
Name=API
Regex=^[A-Za-z0-9_-]{20,}$
Verify=True
Separator=
Slices=APIKEY
SlicesAlias=APIKEY
[MAC TYPE]
Name=MAC
Regex=^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$
Verify=True
Separator=
Slices=MAC
SlicesAlias=MACdef run(USER=None, PASS=None, EMAIL=None, APIKEY=None, **kwargs):
"""Plugin with credential support."""
# Check for different credential types
if USER and PASS:
# Username/password authentication
auth_data = {"username": USER, "password": PASS}
return authenticate_with_credentials(auth_data)
elif EMAIL and PASS:
# Email/password authentication
auth_data = {"email": EMAIL, "password": PASS}
return authenticate_with_email(auth_data)
elif APIKEY:
# API key authentication
headers = {"Authorization": f"Bearer {APIKEY}"}
return authenticate_with_api_key(headers)
else:
return {
"error": "No valid credentials provided",
"preview": "Authentication failed - no credentials"
}Create credential files in the credentials/ directory:
# credentials/username_passwords.txt
user1:password123
admin:secretpass
testuser:mypassword
# credentials/email_accounts.txt
user@example.com:password123
admin@company.com:adminpass
# credentials/api_keys.txt
AIzaSyBf8v9k2Xp7Q3m4R5t6Y7u8I9o0P1a2S3d4F5g6
sk-abcd1234efgh5678ijkl9012mnop3456qrstdef run(**kwargs):
"""Plugin with comprehensive error handling."""
try:
# Main plugin logic
result = perform_operation()
# Validate result
if not result:
return {
"error": "Operation returned no results",
"preview": "No data available"
}
return {
"data": result,
"preview": f"Successfully processed {len(result)} items"
}
except requests.RequestException as e:
return {
"error": f"Network error: {str(e)}",
"preview": "Network request failed"
}
except ValueError as e:
return {
"error": f"Data validation error: {str(e)}",
"preview": "Invalid data format"
}
except Exception as e:
return {
"error": f"Unexpected error: {str(e)}",
"preview": "Plugin execution failed"
}import logging
def run(**kwargs):
"""Plugin with logging support."""
# Set up logging (optional - for development)
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
logger.debug(f"Plugin started with args: {kwargs}")
try:
# Your plugin logic
result = process_data()
logger.info(f"Processing completed: {len(result)} items")
return {
"data": result,
"preview": f"Processed {len(result)} items successfully"
}
except Exception as e:
logger.error(f"Plugin failed: {e}")
return {
"error": str(e),
"preview": "Processing failed"
}def run(cdm=None, session_id=None, **kwargs):
"""Plugin with proper resource cleanup."""
temp_files = []
try:
# Create temporary files
temp_file = create_temp_file()
temp_files.append(temp_file)
# Process data
result = process_with_temp_file(temp_file)
return {
"result": result,
"preview": "Processing completed successfully"
}
except Exception as e:
return {
"error": str(e),
"preview": "Processing failed"
}
finally:
# Cleanup resources
for temp_file in temp_files:
try:
os.remove(temp_file)
except:
pass
# Close CDM session if created
if cdm and session_id:
try:
cdm.close(session_id)
except:
passdef run(**kwargs):
"""Plugin with built-in testing capabilities."""
# Test mode detection
test_mode = kwargs.get('_test_mode', False)
if test_mode:
# Return test data
return {
"test_data": generate_test_data(),
"preview": "Test mode - generated sample data"
}
# Normal execution
try:
result = normal_operation(**kwargs)
return {
"data": result,
"preview": "Operation completed successfully"
}
except Exception as e:
return {
"error": str(e),
"preview": "Operation failed"
}
def generate_test_data():
"""Generate test data for plugin validation."""
return {
"sample_keys": ["key1", "key2", "key3"],
"sample_urls": ["http://example.com/content1.mpd"],
"timestamp": datetime.now().isoformat()
}def run(large_dataset=None, **kwargs):
"""Plugin optimized for large datasets."""
if not large_dataset:
return {
"error": "No dataset provided",
"preview": "Missing input data"
}
# Process data in chunks for memory efficiency
chunk_size = 1000
results = []
for i in range(0, len(large_dataset), chunk_size):
chunk = large_dataset[i:i + chunk_size]
processed_chunk = process_chunk(chunk)
results.extend(processed_chunk)
# Optional progress reporting
progress = (i + chunk_size) / len(large_dataset) * 100
print(f"Progress: {progress:.1f}%")
return {
"processed_data": results,
"total_processed": len(results),
"preview": f"Processed {len(results)} items in {len(large_dataset)//chunk_size + 1} chunks"
}import asyncio
import aiohttp
def run(**kwargs):
"""Plugin with async HTTP requests."""
urls = kwargs.get('urls', [])
if not urls:
return {
"error": "No URLs provided",
"preview": "Missing URL list"
}
# Run async operations
results = asyncio.run(fetch_multiple_urls(urls))
return {
"responses": results,
"total_requests": len(urls),
"preview": f"Completed {len(results)} async requests"
}
async def fetch_multiple_urls(urls):
"""Fetch multiple URLs concurrently."""
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
return await asyncio.gather(*tasks)
async def fetch_url(session, url):
"""Fetch single URL."""
try:
async with session.get(url) as response:
return {
"url": url,
"status": response.status,
"content": await response.text()
}
except Exception as e:
return {
"url": url,
"error": str(e)
}def run(search_query=None, USER=None, PASS=None, **kwargs):
"""Discover content across multiple platforms."""
if not search_query:
return {
"error": "Search query required",
"preview": "No search term provided"
}
platforms = {
"netflix": search_netflix,
"amazon": search_amazon,
"hulu": search_hulu
}
all_results = []
for platform_name, search_func in platforms.items():
try:
results = search_func(search_query, USER, PASS)
for result in results:
result['platform'] = platform_name
all_results.extend(results)
except Exception as e:
print(f"Failed to search {platform_name}: {e}")
return {
"search_results": all_results,
"total_found": len(all_results),
"platforms_searched": list(platforms.keys()),
"preview": f"Found {len(all_results)} results across {len(platforms)} platforms"
}def run(license_url=None, pssh=None, headers=None, **kwargs):
"""Custom license server interaction."""
if not license_url or not pssh:
return {
"error": "License URL and PSSH required",
"preview": "Missing required parameters"
}
# Parse headers
headers_dict = {}
if headers:
if isinstance(headers, str):
for line in headers.split('\n'):
if ':' in line:
key, value = line.split(':', 1)
headers_dict[key.strip()] = value.strip()
elif isinstance(headers, dict):
headers_dict = headers
# Prepare license request
license_data = {
"pssh": pssh,
"challenge": generate_challenge(pssh)
}
try:
response = requests.post(
license_url,
json=license_data,
headers=headers_dict,
timeout=30
)
if response.status_code == 200:
keys = extract_keys_from_response(response.content)
return {
"keys": keys,
"license_response": response.content,
"preview": f"Extracted {len(keys)} keys successfully"
}
else:
return {
"error": f"License server returned {response.status_code}",
"preview": "License request failed"
}
except Exception as e:
return {
"error": str(e),
"preview": "License request failed"
}def run(keys=None, mpd_url=None, **kwargs):
"""Validate extracted keys and content data."""
validation_results = {
"keys_valid": False,
"mpd_accessible": False,
"content_playable": False
}
# Validate keys
if keys:
validation_results["keys_valid"] = validate_keys(keys)
validation_results["key_count"] = len(keys)
# Validate MPD
if mpd_url:
validation_results["mpd_accessible"] = test_mpd_access(mpd_url)
# Test content playability (if both keys and MPD available)
if keys and mpd_url and validation_results["keys_valid"] and validation_results["mpd_accessible"]:
validation_results["content_playable"] = test_content_decryption(keys, mpd_url)
# Generate summary
passed_tests = sum(1 for result in validation_results.values() if result is True)
total_tests = sum(1 for result in validation_results.values() if isinstance(result, bool))
return {
"validation_results": validation_results,
"tests_passed": passed_tests,
"total_tests": total_tests,
"preview": f"Validation: {passed_tests}/{total_tests} tests passed"
}
def validate_keys(keys):
"""Validate key format and content."""
if not keys or not isinstance(keys, list):
return False
for key in keys:
if not isinstance(key, str) or len(key) < 32:
return False
return True
def test_mpd_access(mpd_url):
"""Test if MPD is accessible."""
try:
response = requests.head(mpd_url, timeout=10)
return response.status_code == 200
except:
return False- Unit Testing: Test individual plugin functions with mock data
- Integration Testing: Test plugins within the full application context
- Error Case Testing: Verify proper error handling and recovery
- Performance Testing: Monitor execution time and resource usage
Use the included debug_params.py plugin to inspect available context:
# Add debug_params.py to your plugin stack to see all available variables
def run(**kwargs):
"""Debug plugin that displays all available context variables."""
preview_lines = ["Available Context Variables:"]
for key, value in sorted(kwargs.items()):
value_preview = str(value)[:50] + "..." if len(str(value)) > 50 else str(value)
preview_lines.append(f" {key}: {value_preview}")
return {
"all_context": kwargs,
"context_keys": list(kwargs.keys()),
"preview": "\n".join(preview_lines)
}def run(debug_mode=False, **kwargs):
"""Plugin with comprehensive logging."""
if debug_mode:
print(f"Plugin started with {len(kwargs)} context variables")
for key in kwargs.keys():
print(f" Available: {key}")
try:
# Plugin logic here
result = perform_operation()
if debug_mode:
print(f"Operation completed successfully: {result}")
return {
"result": result,
"debug_info": kwargs if debug_mode else None,
"preview": f"Operation completed {'(debug mode)' if debug_mode else ''}"
}
except Exception as e:
error_msg = f"Error in plugin: {e}"
print(error_msg) # Always log errors
return {
"error": error_msg,
"debug_context": kwargs if debug_mode else None,
"preview": "Plugin execution failed"
}- Documentation: Include comprehensive docstrings with parameter descriptions
- Error Handling: Always handle exceptions gracefully with meaningful error messages
- Resource Cleanup: Use
try/finallyblocks or context managers for resource management - Parameter Validation: Check required parameters and provide clear error messages
- Return Standards: Always include a
previewkey in return dictionaries
- Input Validation: Sanitize and validate all input parameters
- Credential Handling: Never log or expose credentials in plain text
- Network Security: Use HTTPS where possible and validate SSL certificates
- File Access: Restrict file operations to intended directories
- Injection Prevention: Avoid using user input directly in system commands
- Lazy Loading: Load heavy resources only when needed
- Connection Reuse: Use session objects for multiple HTTP requests
- Parallel Processing: Use threading for I/O-bound operations
- Memory Management: Clean up large objects and close file handles
- Timeout Settings: Always set reasonable timeouts for network operations
Problem: Plugin doesn't appear in the GUI plugin list Solutions:
- Ensure the file is in the
plugins/directory - Check that the filename has a
.pyextension - Verify the plugin has a
run()function - Use the "Rescan" button to refresh the plugin list
- Check for Python syntax errors in the plugin file
Problem: Plugin receives None values for expected parameters
Solutions:
- Check parameter names match available context variables
- Verify credentials are properly configured in
environment.ini - Ensure previous plugins in the chain provide required data
- Use the
debug_params.pyplugin to inspect available context
Problem: Plugins fail to authenticate with services Solutions:
- Verify credential format matches regex patterns in
environment.ini - Check that "Expose Credentials to Plugins" is enabled in the GUI
- Test credentials manually before using in plugins
- Check for API key/token expiration
Problem: Network requests fail or proxy connections timeout Solutions:
- Verify proxy configuration and format
- Test proxy connectivity with
check_proxies.pyplugin - Implement proper timeout settings in requests
- Use fallback mechanisms for proxy failures
- Check firewall and network security settings
Problem: Plugins consume excessive memory or run slowly Solutions:
- Implement proper resource cleanup with
finallyblocks - Use streaming for large file operations
- Limit concurrent operations and use appropriate thread pools
- Implement caching for repeated operations
- Monitor and profile plugin execution times
- Import only what you need to minimize startup time and memory usage
- Handle errors gracefully and always return a
previewstring for the UI - Close sessions/clean up in a
finallyorexceptblock if needed - Do not use global state—all state should be passed via arguments/context
- Test your plugin by adding it to the stack and running it from the GUI
- Use meaningful parameter names that match the context variables
- Provide default values for optional parameters
- Document your plugins with clear docstrings and parameter descriptions
- Validate inputs and provide helpful error messages
- Follow naming conventions and use numeric prefixes for execution order
- Project Repository: Check the main repository for updates and examples
- Plugin Examples: Examine existing plugins in the
plugins/directory - Debug Tools: Use
debug_params.pyto understand available context - Community Plugins: Look for community-contributed plugins and examples
- Issue Tracker: Report bugs and request features through the issue tracker
This documentation is continuously updated to reflect the latest features and best practices. For the most current information, always refer to the latest version of this file and examine the actual plugin implementations in the codebase.