meta: drop plugin stubs for now
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Iedf1d829183c258d0f6ef8a313053d4d6a6a6964
This commit is contained in:
parent
e7e9ea6036
commit
d18317b49b
11 changed files with 0 additions and 1367 deletions
|
|
@ -1,518 +0,0 @@
|
||||||
# Pinakes Plugin Examples
|
|
||||||
|
|
||||||
This directory contains example plugins demonstrating the Pinakes plugin system.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Pinakes supports extensibility through a WASM-based plugin system. Plugins can
|
|
||||||
extend Pinakes functionality by:
|
|
||||||
|
|
||||||
- **Media Type Providers**: Add support for new file formats
|
|
||||||
- **Metadata Extractors**: Extract metadata from files
|
|
||||||
- **Thumbnail Generators**: Generate thumbnails for media types
|
|
||||||
- **Search Backends**: Implement custom search algorithms
|
|
||||||
- **Event Handlers**: React to system events
|
|
||||||
- **Theme Providers**: Provide custom UI themes
|
|
||||||
|
|
||||||
## Example Plugins
|
|
||||||
|
|
||||||
### 1. Markdown Metadata Extractor
|
|
||||||
|
|
||||||
**Directory**: `markdown-metadata/`
|
|
||||||
|
|
||||||
Enhances markdown file support with advanced frontmatter parsing.
|
|
||||||
|
|
||||||
**Demonstrates**:
|
|
||||||
|
|
||||||
- Metadata extraction from files
|
|
||||||
- Plugin configuration via `plugin.toml`
|
|
||||||
- Minimal capability requirements
|
|
||||||
|
|
||||||
**Plugin Kind**: `metadata_extractor`
|
|
||||||
|
|
||||||
### 2. HEIF/HEIC Support
|
|
||||||
|
|
||||||
**Directory**: `heif-support/`
|
|
||||||
|
|
||||||
Adds support for HEIF and HEIC image formats.
|
|
||||||
|
|
||||||
**Demonstrates**:
|
|
||||||
|
|
||||||
- Media type registration
|
|
||||||
- Metadata extraction from binary formats
|
|
||||||
- Thumbnail generation
|
|
||||||
- Resource limits (memory, CPU time)
|
|
||||||
|
|
||||||
**Plugin Kinds**: `media_type`, `metadata_extractor`, `thumbnail_generator`
|
|
||||||
|
|
||||||
## Plugin Architecture
|
|
||||||
|
|
||||||
### Plugin Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
my-plugin/
|
|
||||||
├── plugin.toml # Plugin manifest
|
|
||||||
├── Cargo.toml # Rust project configuration
|
|
||||||
├── src/
|
|
||||||
│ └── lib.rs # Plugin implementation
|
|
||||||
└── README.md # Plugin documentation
|
|
||||||
```
|
|
||||||
|
|
||||||
### Plugin Manifest (plugin.toml)
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[plugin]
|
|
||||||
name = "my-plugin"
|
|
||||||
version = "1.0.0"
|
|
||||||
api_version = "1.0"
|
|
||||||
author = "Your Name"
|
|
||||||
description = "Description of your plugin"
|
|
||||||
kind = ["metadata_extractor"]
|
|
||||||
|
|
||||||
[plugin.binary]
|
|
||||||
wasm = "my_plugin.wasm"
|
|
||||||
|
|
||||||
[capabilities]
|
|
||||||
network = false
|
|
||||||
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = ["/path/to/read"]
|
|
||||||
write = ["/path/to/write"]
|
|
||||||
|
|
||||||
[config]
|
|
||||||
# Plugin-specific configuration
|
|
||||||
option1 = "value1"
|
|
||||||
option2 = 42
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manifest Fields
|
|
||||||
|
|
||||||
#### [plugin] Section
|
|
||||||
|
|
||||||
- `name`: Plugin identifier (must be unique)
|
|
||||||
- `version`: Semantic version (e.g., "1.0.0")
|
|
||||||
- `api_version`: Pinakes Plugin API version (currently "1.0")
|
|
||||||
- `author`: Plugin author (optional)
|
|
||||||
- `description`: Short description (optional)
|
|
||||||
- `homepage`: Plugin homepage URL (optional)
|
|
||||||
- `license`: License identifier (optional)
|
|
||||||
- `kind`: Array of plugin kinds
|
|
||||||
- `dependencies`: Array of plugin names this plugin depends on (optional)
|
|
||||||
|
|
||||||
#### [plugin.binary] Section
|
|
||||||
|
|
||||||
- `wasm`: Path to WASM binary (relative to manifest)
|
|
||||||
- `entrypoint`: Custom entrypoint function name (optional, default: "_start")
|
|
||||||
|
|
||||||
#### [capabilities] Section
|
|
||||||
|
|
||||||
Capabilities define what the plugin can access:
|
|
||||||
|
|
||||||
**Filesystem**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = ["/tmp/cache", "/var/data"]
|
|
||||||
write = ["/tmp/output"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Network**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities]
|
|
||||||
network = true # or false
|
|
||||||
```
|
|
||||||
|
|
||||||
**Environment**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities]
|
|
||||||
environment = ["PATH", "HOME"] # or omit for no access
|
|
||||||
```
|
|
||||||
|
|
||||||
**Resource Limits**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities]
|
|
||||||
max_memory_mb = 128
|
|
||||||
max_cpu_time_secs = 10
|
|
||||||
```
|
|
||||||
|
|
||||||
### Plugin Kinds
|
|
||||||
|
|
||||||
#### media_type
|
|
||||||
|
|
||||||
Register new media types with file extensions and MIME types.
|
|
||||||
|
|
||||||
**Trait**: `MediaTypeProvider`
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
|
|
||||||
- `supported_media_types()`: Returns list of media type definitions
|
|
||||||
- `can_handle(path, mime_type)`: Check if plugin can handle a file
|
|
||||||
|
|
||||||
#### metadata_extractor
|
|
||||||
|
|
||||||
Extract metadata from files.
|
|
||||||
|
|
||||||
**Trait**: `MetadataExtractor`
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
|
|
||||||
- `extract_metadata(path)`: Extract metadata from file
|
|
||||||
- `supported_types()`: Returns list of supported media type IDs
|
|
||||||
|
|
||||||
#### thumbnail_generator
|
|
||||||
|
|
||||||
Generate thumbnails for media files.
|
|
||||||
|
|
||||||
**Trait**: `ThumbnailGenerator`
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
|
|
||||||
- `generate_thumbnail(path, output_path, options)`: Generate thumbnail
|
|
||||||
- `supported_types()`: Returns list of supported media type IDs
|
|
||||||
|
|
||||||
#### search_backend
|
|
||||||
|
|
||||||
Implement custom search algorithms.
|
|
||||||
|
|
||||||
**Trait**: `SearchBackend`
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
|
|
||||||
- `index_item(item)`: Index a media item
|
|
||||||
- `remove_item(item_id)`: Remove item from index
|
|
||||||
- `search(query)`: Perform search
|
|
||||||
- `get_stats()`: Get index statistics
|
|
||||||
|
|
||||||
#### event_handler
|
|
||||||
|
|
||||||
React to system events.
|
|
||||||
|
|
||||||
**Trait**: `EventHandler`
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
|
|
||||||
- `handle_event(event)`: Handle an event
|
|
||||||
- `interested_events()`: Returns list of event types to receive
|
|
||||||
|
|
||||||
#### theme_provider
|
|
||||||
|
|
||||||
Provide UI themes.
|
|
||||||
|
|
||||||
**Trait**: `ThemeProvider`
|
|
||||||
|
|
||||||
**Methods**:
|
|
||||||
|
|
||||||
- `get_themes()`: List available themes
|
|
||||||
- `load_theme(theme_id)`: Load theme data
|
|
||||||
|
|
||||||
## Creating a Plugin
|
|
||||||
|
|
||||||
### Step 1: Set Up Project
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create new Rust library project
|
|
||||||
cargo new --lib my-plugin
|
|
||||||
cd my-plugin
|
|
||||||
|
|
||||||
# Add dependencies
|
|
||||||
cat >> Cargo.toml <<EOF
|
|
||||||
[dependencies]
|
|
||||||
pinakes-plugin-api = { path = "../../crates/pinakes-plugin-api" }
|
|
||||||
async-trait = "0.1"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = "z"
|
|
||||||
lto = true
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Implement Plugin
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use pinakes_plugin_api::*;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub struct MyPlugin {
|
|
||||||
context: Option<PluginContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl Plugin for MyPlugin {
|
|
||||||
fn metadata(&self) -> &PluginMetadata {
|
|
||||||
// Return plugin metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn initialize(&mut self, context: PluginContext) -> PluginResult<()> {
|
|
||||||
self.context = Some(context);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn shutdown(&mut self) -> PluginResult<()> {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn health_check(&self) -> PluginResult<HealthStatus> {
|
|
||||||
Ok(HealthStatus {
|
|
||||||
healthy: true,
|
|
||||||
message: None,
|
|
||||||
metrics: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl MetadataExtractor for MyPlugin {
|
|
||||||
async fn extract_metadata(&self, path: &PathBuf) -> PluginResult<ExtractedMetadata> {
|
|
||||||
// Extract metadata from file
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supported_types(&self) -> Vec<String> {
|
|
||||||
vec!["my_type".to_string()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Build to WASM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install WASM target
|
|
||||||
rustup target add wasm32-wasi
|
|
||||||
|
|
||||||
# Build
|
|
||||||
cargo build --target wasm32-wasi --release
|
|
||||||
|
|
||||||
# Optimize (optional, wasm-tools provides wasm-strip functionality)
|
|
||||||
cargo install wasm-tools
|
|
||||||
wasm-tools strip target/wasm32-wasi/release/my_plugin.wasm -o target/wasm32-wasi/release/my_plugin.wasm
|
|
||||||
|
|
||||||
# Copy to plugin directory
|
|
||||||
cp target/wasm32-wasi/release/my_plugin.wasm .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Create Manifest
|
|
||||||
|
|
||||||
Create `plugin.toml` with appropriate configuration (see examples above).
|
|
||||||
|
|
||||||
### Step 5: Install Plugin
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy to plugins directory
|
|
||||||
cp -r my-plugin ~/.config/pinakes/plugins/
|
|
||||||
|
|
||||||
# Or use API
|
|
||||||
curl -X POST http://localhost:3000/api/v1/plugins/install \
|
|
||||||
-d '{"source": "/path/to/my-plugin"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Testing Plugins
|
|
||||||
|
|
||||||
### Unit Tests
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_metadata_extraction() {
|
|
||||||
let mut plugin = MyPlugin::default();
|
|
||||||
let context = PluginContext {
|
|
||||||
data_dir: PathBuf::from("/tmp/data"),
|
|
||||||
cache_dir: PathBuf::from("/tmp/cache"),
|
|
||||||
config: Default::default(),
|
|
||||||
capabilities: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
plugin.initialize(context).await.unwrap();
|
|
||||||
|
|
||||||
let metadata = plugin
|
|
||||||
.extract_metadata(&PathBuf::from("test.txt"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(metadata.title.is_some());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Integration Tests
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Load plugin in test Pinakes instance
|
|
||||||
pinakes --config test-config.toml plugin load /path/to/plugin
|
|
||||||
|
|
||||||
# Verify plugin is loaded
|
|
||||||
pinakes plugin list
|
|
||||||
|
|
||||||
# Test plugin functionality
|
|
||||||
pinakes scan /path/to/test/files
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Capability-Based Security
|
|
||||||
|
|
||||||
Plugins operate in a sandbox with explicit capabilities. Only request the
|
|
||||||
minimum capabilities needed:
|
|
||||||
|
|
||||||
**Good**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = ["/tmp/cache"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Bad**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = ["/", "/home", "/etc"] # Too broad!
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Limits
|
|
||||||
|
|
||||||
Always set appropriate resource limits:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[capabilities]
|
|
||||||
max_memory_mb = 128 # Reasonable for image processing
|
|
||||||
max_cpu_time_secs = 10 # Prevent runaway operations
|
|
||||||
```
|
|
||||||
|
|
||||||
### Input Validation
|
|
||||||
|
|
||||||
Always validate input in your plugin:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
async fn extract_metadata(&self, path: &PathBuf) -> PluginResult<ExtractedMetadata> {
|
|
||||||
// Check file exists
|
|
||||||
if !path.exists() {
|
|
||||||
return Err(PluginError::InvalidInput("File not found".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check file size
|
|
||||||
let metadata = std::fs::metadata(path)
|
|
||||||
.map_err(|e| PluginError::IoError(e.to_string()))?;
|
|
||||||
if metadata.len() > 10_000_000 { // 10MB limit
|
|
||||||
return Err(PluginError::InvalidInput("File too large".to_string()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process file...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
|
|
||||||
- Use descriptive error messages
|
|
||||||
- Return appropriate `PluginError` variants
|
|
||||||
- Don't panic - return errors instead
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
|
|
||||||
- Avoid blocking operations in async functions
|
|
||||||
- Use streaming for large files
|
|
||||||
- Implement timeouts for external operations
|
|
||||||
- Cache results when appropriate
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
- Provide sensible defaults
|
|
||||||
- Document all configuration options
|
|
||||||
- Validate configuration during initialization
|
|
||||||
|
|
||||||
### Documentation
|
|
||||||
|
|
||||||
- Write clear README with examples
|
|
||||||
- Document all configuration options
|
|
||||||
- Include troubleshooting section
|
|
||||||
- Provide integration examples
|
|
||||||
|
|
||||||
## API Reference
|
|
||||||
|
|
||||||
See the
|
|
||||||
[pinakes-plugin-api documentation](../../crates/pinakes-plugin-api/README.md)
|
|
||||||
for detailed API reference.
|
|
||||||
|
|
||||||
## Plugin Distribution
|
|
||||||
|
|
||||||
### Plugin Registry (Future)
|
|
||||||
|
|
||||||
A centralized plugin registry is planned for future releases:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install from registry
|
|
||||||
pinakes plugin install markdown-metadata
|
|
||||||
|
|
||||||
# Search plugins
|
|
||||||
pinakes plugin search heif
|
|
||||||
|
|
||||||
# Update all plugins
|
|
||||||
pinakes plugin update --all
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Distribution
|
|
||||||
|
|
||||||
Currently, plugins are distributed as directories containing:
|
|
||||||
|
|
||||||
- `plugin.toml` manifest
|
|
||||||
- WASM binary
|
|
||||||
- README and documentation
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Plugin Won't Load
|
|
||||||
|
|
||||||
**Check manifest syntax**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate TOML syntax
|
|
||||||
taplo check plugin.toml
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check API version**: Ensure `api_version = "1.0"` in manifest.
|
|
||||||
|
|
||||||
**Check binary path**: Verify WASM binary exists at path specified in
|
|
||||||
`plugin.binary.wasm`.
|
|
||||||
|
|
||||||
### Plugin Crashes
|
|
||||||
|
|
||||||
**Check resource limits**: Increase `max_memory_mb` or `max_cpu_time_secs` if
|
|
||||||
operations are timing out.
|
|
||||||
|
|
||||||
**Check capabilities**: Ensure plugin has necessary filesystem/network
|
|
||||||
capabilities.
|
|
||||||
|
|
||||||
**Check logs**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View plugin logs
|
|
||||||
tail -f ~/.local/share/pinakes/logs/plugin.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission Denied Errors
|
|
||||||
|
|
||||||
**Filesystem capabilities**: Add required paths to
|
|
||||||
`capabilities.filesystem.read` or `.write`.
|
|
||||||
|
|
||||||
**Network capabilities**: Set `capabilities.network = true` if plugin needs
|
|
||||||
network access.
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- **Issues**: https://github.com/notashelf/pinakes/issues
|
|
||||||
- **Discussions**: https://github.com/notashelf/pinakes/discussions
|
|
||||||
- **Documentation**: https://pinakes.readthedocs.io
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
All example plugins are licensed under MIT.
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
# HEIF/HEIC Support Plugin
|
|
||||||
|
|
||||||
This example plugin adds support for HEIF (High Efficiency Image Format) and
|
|
||||||
HEIC (HEIF Container) to Pinakes.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
HEIF is a modern image format that provides better compression than JPEG while
|
|
||||||
maintaining higher quality. This plugin enables Pinakes to:
|
|
||||||
|
|
||||||
- Recognize HEIF/HEIC files as a media type
|
|
||||||
- Extract metadata from HEIF images
|
|
||||||
- Generate thumbnails from HEIF images
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Media Type Registration**: Registers `.heif`, `.heic`, `.hif` extensions as
|
|
||||||
image media types
|
|
||||||
- **EXIF Extraction**: Extracts EXIF metadata including camera info, GPS
|
|
||||||
coordinates, timestamps
|
|
||||||
- **Thumbnail Generation**: Generates thumbnails in JPEG, PNG, or WebP format
|
|
||||||
- **Resource Limits**: Configurable memory and CPU limits for safe processing
|
|
||||||
- **Large Image Support**: Handles images up to 8192x8192 pixels
|
|
||||||
|
|
||||||
## Supported Formats
|
|
||||||
|
|
||||||
- **HEIF**: High Efficiency Image Format (`.heif`, `.hif`)
|
|
||||||
- **HEIC**: HEIF Container format used by Apple devices (`.heic`)
|
|
||||||
- **HEIF Sequences**: Multi-image HEIF files
|
|
||||||
- **HEIF with Alpha**: HEIF images with transparency
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
The plugin implements three traits:
|
|
||||||
|
|
||||||
### MediaTypeProvider
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
impl MediaTypeProvider for HeifPlugin {
|
|
||||||
fn supported_media_types(&self) -> Vec<MediaTypeDefinition> {
|
|
||||||
vec![MediaTypeDefinition {
|
|
||||||
id: "heif".to_string(),
|
|
||||||
name: "HEIF Image".to_string(),
|
|
||||||
category: "image".to_string(),
|
|
||||||
extensions: vec!["heif".to_string(), "heic".to_string(), "hif".to_string()],
|
|
||||||
mime_types: vec!["image/heif".to_string(), "image/heic".to_string()],
|
|
||||||
icon: Some("image".to_string()),
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn can_handle(&self, path: &PathBuf, mime_type: Option<&str>) -> PluginResult<bool> {
|
|
||||||
// Check file extension and/or MIME type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### MetadataExtractor
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
impl MetadataExtractor for HeifPlugin {
|
|
||||||
async fn extract_metadata(&self, path: &PathBuf) -> PluginResult<ExtractedMetadata> {
|
|
||||||
// 1. Parse HEIF file structure
|
|
||||||
// 2. Extract EXIF metadata
|
|
||||||
// 3. Get image dimensions
|
|
||||||
// 4. Return ExtractedMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supported_types(&self) -> Vec<String> {
|
|
||||||
vec!["heif".to_string()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### ThumbnailGenerator
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
impl ThumbnailGenerator for HeifPlugin {
|
|
||||||
async fn generate_thumbnail(
|
|
||||||
&self,
|
|
||||||
path: &PathBuf,
|
|
||||||
output_path: &PathBuf,
|
|
||||||
options: ThumbnailOptions,
|
|
||||||
) -> PluginResult<ThumbnailInfo> {
|
|
||||||
// 1. Decode HEIF image
|
|
||||||
// 2. Resize to thumbnail dimensions
|
|
||||||
// 3. Encode to output format
|
|
||||||
// 4. Save to output_path
|
|
||||||
// 5. Return ThumbnailInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supported_types(&self) -> Vec<String> {
|
|
||||||
vec!["heif".to_string()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
The plugin uses the following Rust crates (compiled to WASM):
|
|
||||||
|
|
||||||
- `libheif-rs`: HEIF decoding and encoding
|
|
||||||
- `image`: Image processing and thumbnail generation
|
|
||||||
- `kamadak-exif`: EXIF metadata parsing
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install WASM target
|
|
||||||
rustup target add wasm32-wasi
|
|
||||||
|
|
||||||
# Install wasm-tools for optimization (provides strip functionality)
|
|
||||||
cargo install wasm-tools
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Process
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build the plugin
|
|
||||||
cargo build --target wasm32-wasi --release
|
|
||||||
|
|
||||||
# Strip debug symbols to reduce size
|
|
||||||
wasm-tools strip target/wasm32-wasi/release/heif_support.wasm -o target/wasm32-wasi/release/heif_support.wasm
|
|
||||||
|
|
||||||
# Copy to plugin directory
|
|
||||||
cp target/wasm32-wasi/release/heif_support.wasm .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Size Optimization
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use wasm-opt for further optimization
|
|
||||||
wasm-opt -Oz heif_support.wasm -o heif_support_optimized.wasm
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy plugin directory to Pinakes plugins directory
|
|
||||||
cp -r examples/plugins/heif-support ~/.config/pinakes/plugins/
|
|
||||||
```
|
|
||||||
|
|
||||||
### Via API
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST http://localhost:3000/api/v1/plugins/install \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"source": "/path/to/heif-support"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Via Plugin Manager
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pinakes plugin install /path/to/heif-support
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The plugin can be configured through the `config` section in `plugin.toml`:
|
|
||||||
|
|
||||||
### EXIF Extraction
|
|
||||||
|
|
||||||
- `extract_exif`: Enable EXIF metadata extraction (default: true)
|
|
||||||
|
|
||||||
### Thumbnail Generation
|
|
||||||
|
|
||||||
- `generate_thumbnails`: Enable thumbnail generation (default: true)
|
|
||||||
- `thumbnail_quality`: JPEG quality for thumbnails, 1-100 (default: 85)
|
|
||||||
- `thumbnail_format`: Output format - "jpeg", "png", or "webp" (default: "jpeg")
|
|
||||||
|
|
||||||
### Resource Limits
|
|
||||||
|
|
||||||
- `max_memory_mb`: Maximum memory the plugin can use in megabytes (default: 256,
|
|
||||||
set in `[capabilities]`)
|
|
||||||
- `max_width`: Maximum image width to process (default: 8192)
|
|
||||||
- `max_height`: Maximum image height to process (default: 8192)
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
### Capabilities
|
|
||||||
|
|
||||||
- **Filesystem Read**: Only files being processed (via Pinakes)
|
|
||||||
- **Filesystem Write**: Thumbnail directory only
|
|
||||||
- **Network**: Disabled
|
|
||||||
- **Environment**: No access
|
|
||||||
|
|
||||||
### Resource Limits
|
|
||||||
|
|
||||||
- **Memory**: 256 MB maximum
|
|
||||||
- **CPU Time**: 30 seconds maximum per operation
|
|
||||||
|
|
||||||
### Sandboxing
|
|
||||||
|
|
||||||
The plugin runs in a WASM sandbox with:
|
|
||||||
|
|
||||||
- No access to host filesystem beyond granted paths
|
|
||||||
- No network access
|
|
||||||
- No arbitrary code execution
|
|
||||||
- Memory and CPU time limits enforced by runtime
|
|
||||||
|
|
||||||
## Performance
|
|
||||||
|
|
||||||
### Typical Performance
|
|
||||||
|
|
||||||
- **Metadata Extraction**: ~50-100ms for typical HEIF files
|
|
||||||
- **Thumbnail Generation**: ~200-500ms depending on source image size
|
|
||||||
- **Memory Usage**: 50-150 MB typical, 256 MB maximum
|
|
||||||
|
|
||||||
### Optimization Tips
|
|
||||||
|
|
||||||
1. Keep source images below 8192x8192 for best performance
|
|
||||||
2. Use JPEG thumbnail format for smaller file sizes
|
|
||||||
3. Adjust thumbnail quality vs file size tradeoff with `thumbnail_quality`
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
The plugin handles:
|
|
||||||
|
|
||||||
- **Corrupted Files**: Returns descriptive error
|
|
||||||
- **Unsupported Variants**: Gracefully skips unsupported HEIF features
|
|
||||||
- **Memory Limits**: Fails safely if image too large
|
|
||||||
- **Timeout**: Returns error if processing exceeds CPU time limit
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run unit tests
|
|
||||||
cargo test
|
|
||||||
|
|
||||||
# Test with sample HEIF files
|
|
||||||
cargo test --test integration -- --nocapture
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Plugin Fails to Load
|
|
||||||
|
|
||||||
- Check that `heif_support.wasm` exists in plugin directory
|
|
||||||
- Verify `plugin.toml` is valid TOML
|
|
||||||
- Check Pinakes logs for detailed error messages
|
|
||||||
|
|
||||||
### Thumbnails Not Generated
|
|
||||||
|
|
||||||
- Verify `generate_thumbnails = true` in config
|
|
||||||
- Check filesystem write permissions for thumbnail directory
|
|
||||||
- Ensure source image is below size limits
|
|
||||||
|
|
||||||
### Out of Memory Errors
|
|
||||||
|
|
||||||
- Reduce `max_width` and `max_height` in config
|
|
||||||
- Increase `max_memory_mb` if source images are large
|
|
||||||
- Check that source files aren't corrupted
|
|
||||||
|
|
||||||
## Future Enhancements
|
|
||||||
|
|
||||||
- Support for HEIF image sequences (burst photos)
|
|
||||||
- HDR metadata extraction
|
|
||||||
- Live Photo support
|
|
||||||
- AVIF format support (similar to HEIF)
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
[plugin]
|
|
||||||
name = "heif-support"
|
|
||||||
version = "1.0.0"
|
|
||||||
api_version = "1.0"
|
|
||||||
author = "Pinakes Contributors"
|
|
||||||
description = "HEIF/HEIC image format support for Pinakes"
|
|
||||||
homepage = "https://github.com/notashelf/pinakes"
|
|
||||||
license = "MIT"
|
|
||||||
kind = ["media_type", "metadata_extractor", "thumbnail_generator"]
|
|
||||||
|
|
||||||
[plugin.binary]
|
|
||||||
wasm = "heif_support.wasm"
|
|
||||||
|
|
||||||
[capabilities]
|
|
||||||
network = false
|
|
||||||
max_memory_mb = 256
|
|
||||||
max_cpu_time_secs = 30
|
|
||||||
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = ["/media"]
|
|
||||||
write = ["/tmp/pinakes"]
|
|
||||||
|
|
||||||
[config]
|
|
||||||
extract_exif = true
|
|
||||||
generate_thumbnails = true
|
|
||||||
thumbnail_quality = 85
|
|
||||||
thumbnail_format = "jpeg"
|
|
||||||
max_width = 8192
|
|
||||||
max_height = 8192
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
# Markdown Metadata Extractor Plugin
|
|
||||||
|
|
||||||
This example plugin demonstrates how to create a metadata extractor plugin for
|
|
||||||
Pinakes.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Markdown Metadata Extractor enhances Pinakes' built-in markdown support by:
|
|
||||||
|
|
||||||
- Parsing YAML and TOML frontmatter
|
|
||||||
- Extracting metadata from frontmatter fields
|
|
||||||
- Converting frontmatter tags to Pinakes media tags
|
|
||||||
- Extracting custom fields from frontmatter
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Frontmatter Parsing**: Supports both YAML (`---`) and TOML (`+++`)
|
|
||||||
frontmatter formats
|
|
||||||
- **Tag Extraction**: Automatically extracts tags from frontmatter and applies
|
|
||||||
them to media items
|
|
||||||
- **Custom Fields**: Preserves all frontmatter fields as custom metadata
|
|
||||||
- **Configuration**: Configurable via `plugin.toml` config section
|
|
||||||
|
|
||||||
## Frontmatter Example
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
title: "My Document"
|
|
||||||
author: "John Doe"
|
|
||||||
date: "2024-01-15"
|
|
||||||
tags: ["documentation", "example", "markdown"]
|
|
||||||
category: "tutorials"
|
|
||||||
draft: false
|
|
||||||
---
|
|
||||||
|
|
||||||
# My Document
|
|
||||||
|
|
||||||
Content goes here...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Implementation
|
|
||||||
|
|
||||||
The plugin implements the `MetadataExtractor` trait from `pinakes-plugin-api`:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
#[async_trait]
|
|
||||||
impl MetadataExtractor for MarkdownMetadataPlugin {
|
|
||||||
async fn extract_metadata(&self, path: &PathBuf) -> PluginResult<ExtractedMetadata> {
|
|
||||||
// 1. Read the file
|
|
||||||
// 2. Parse frontmatter
|
|
||||||
// 3. Extract metadata fields
|
|
||||||
// 4. Return ExtractedMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
fn supported_types(&self) -> Vec<String> {
|
|
||||||
vec!["markdown".to_string()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
The plugin is compiled to WebAssembly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo build --target wasm32-wasi --release
|
|
||||||
wasm-strip target/wasm32-wasi/release/markdown_metadata.wasm
|
|
||||||
cp target/wasm32-wasi/release/markdown_metadata.wasm .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Copy plugin directory to Pinakes plugins directory
|
|
||||||
cp -r examples/plugins/markdown-metadata ~/.config/pinakes/plugins/
|
|
||||||
|
|
||||||
# Or via API
|
|
||||||
curl -X POST http://localhost:3000/api/v1/plugins/install \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d '{"source": "/path/to/markdown-metadata"}'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
The plugin can be configured through the `config` section in `plugin.toml`:
|
|
||||||
|
|
||||||
- `extract_tags`: Extract tags from frontmatter (default: true)
|
|
||||||
- `parse_yaml`: Parse YAML frontmatter (default: true)
|
|
||||||
- `parse_toml`: Parse TOML frontmatter (default: true)
|
|
||||||
- `max_file_size`: Maximum file size to process in bytes (default: 10MB)
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
This plugin has minimal capabilities:
|
|
||||||
|
|
||||||
- **Filesystem**: No write access, read access only to files being processed
|
|
||||||
- **Network**: Disabled
|
|
||||||
- **Environment**: No access
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cargo test
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
[plugin]
|
|
||||||
name = "markdown-metadata"
|
|
||||||
version = "1.0.0"
|
|
||||||
api_version = "1.0"
|
|
||||||
author = "Pinakes Contributors"
|
|
||||||
description = "Extract metadata from Markdown files with YAML frontmatter"
|
|
||||||
homepage = "https://github.com/notashelf/pinakes"
|
|
||||||
license = "MIT"
|
|
||||||
kind = ["metadata_extractor"]
|
|
||||||
|
|
||||||
[plugin.binary]
|
|
||||||
wasm = "markdown_metadata.wasm"
|
|
||||||
|
|
||||||
[capabilities]
|
|
||||||
network = false
|
|
||||||
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = []
|
|
||||||
write = []
|
|
||||||
|
|
||||||
[config]
|
|
||||||
extract_tags = true
|
|
||||||
parse_yaml = true
|
|
||||||
parse_toml = true
|
|
||||||
max_file_size = 10485760
|
|
||||||
BIN
examples/plugins/media-stats-ui/Cargo.lock
generated
BIN
examples/plugins/media-stats-ui/Cargo.lock
generated
Binary file not shown.
|
|
@ -1,20 +0,0 @@
|
||||||
[workspace]
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "media-stats-ui"
|
|
||||||
version = "1.0.0"
|
|
||||||
edition = "2024"
|
|
||||||
description = "Library statistics dashboard and tag manager, a UI-only Pinakes plugin"
|
|
||||||
license = "EUPL-1.2"
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "media_stats_ui"
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
dlmalloc = { version = "0.2.12", features = ["global"] }
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
opt-level = "s"
|
|
||||||
lto = true
|
|
||||||
strip = true
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
{
|
|
||||||
"id": "stats",
|
|
||||||
"title": "Library Statistics",
|
|
||||||
"route": "/plugins/media-stats-ui/stats",
|
|
||||||
"icon": "chart-bar",
|
|
||||||
"layout": {
|
|
||||||
"type": "tabs",
|
|
||||||
"default_tab": 0,
|
|
||||||
"tabs": [
|
|
||||||
{
|
|
||||||
"label": "Overview",
|
|
||||||
"content": {
|
|
||||||
"type": "container",
|
|
||||||
"gap": 24,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "heading",
|
|
||||||
"level": 2,
|
|
||||||
"content": "Library Statistics"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"content": "Live summary of your media library. Refreshes every 30 seconds.",
|
|
||||||
"variant": "secondary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "card",
|
|
||||||
"title": "Summary",
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"type": "description_list",
|
|
||||||
"data": "stats",
|
|
||||||
"horizontal": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "chart",
|
|
||||||
"chart_type": "bar",
|
|
||||||
"data": "type-breakdown",
|
|
||||||
"title": "Files by Type",
|
|
||||||
"x_axis_label": "Media Type",
|
|
||||||
"y_axis_label": "Count",
|
|
||||||
"height": 280
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Recent Files",
|
|
||||||
"content": {
|
|
||||||
"type": "container",
|
|
||||||
"gap": 16,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "heading",
|
|
||||||
"level": 2,
|
|
||||||
"content": "Recently Added"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "data_table",
|
|
||||||
"data": "recent",
|
|
||||||
"sortable": true,
|
|
||||||
"filterable": true,
|
|
||||||
"page_size": 10,
|
|
||||||
"columns": [
|
|
||||||
{
|
|
||||||
"key": "file_name",
|
|
||||||
"header": "Filename"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "title",
|
|
||||||
"header": "Title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "media_type",
|
|
||||||
"header": "Type"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "file_size",
|
|
||||||
"header": "Size",
|
|
||||||
"data_type": "file_size"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key": "created_at",
|
|
||||||
"header": "Added",
|
|
||||||
"data_type": "date_time"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Media Grid",
|
|
||||||
"content": {
|
|
||||||
"type": "container",
|
|
||||||
"gap": 16,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "heading",
|
|
||||||
"level": 2,
|
|
||||||
"content": "Browse Media"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "media_grid",
|
|
||||||
"data": "recent",
|
|
||||||
"columns": 4,
|
|
||||||
"gap": 12
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data_sources": {
|
|
||||||
"stats": {
|
|
||||||
"type": "endpoint",
|
|
||||||
"path": "/api/v1/statistics",
|
|
||||||
"poll_interval": 30
|
|
||||||
},
|
|
||||||
"recent": {
|
|
||||||
"type": "endpoint",
|
|
||||||
"path": "/api/v1/media"
|
|
||||||
},
|
|
||||||
"type-breakdown": {
|
|
||||||
"type": "transform",
|
|
||||||
"source": "stats",
|
|
||||||
"expression": "stats.media_by_type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
{
|
|
||||||
"id": "tag-manager",
|
|
||||||
"title": "Tag Manager",
|
|
||||||
"route": "/plugins/media-stats-ui/tag-manager",
|
|
||||||
"icon": "tag",
|
|
||||||
"layout": {
|
|
||||||
"type": "tabs",
|
|
||||||
"default_tab": 0,
|
|
||||||
"tabs": [
|
|
||||||
{
|
|
||||||
"label": "All Tags",
|
|
||||||
"content": {
|
|
||||||
"type": "container",
|
|
||||||
"gap": 16,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "heading",
|
|
||||||
"level": 2,
|
|
||||||
"content": "Manage Tags"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "conditional",
|
|
||||||
"condition": {
|
|
||||||
"op": "eq",
|
|
||||||
"left": { "function": "len", "args": ["tags"] },
|
|
||||||
"right": 0
|
|
||||||
},
|
|
||||||
"then": {
|
|
||||||
"type": "text",
|
|
||||||
"content": "No tags yet. Use the 'Create Tag' tab to add one.",
|
|
||||||
"variant": "secondary"
|
|
||||||
},
|
|
||||||
"else": {
|
|
||||||
"type": "data_table",
|
|
||||||
"data": "tags",
|
|
||||||
"sortable": true,
|
|
||||||
"filterable": true,
|
|
||||||
"page_size": 20,
|
|
||||||
"columns": [
|
|
||||||
{ "key": "name", "header": "Tag Name" },
|
|
||||||
{ "key": "color", "header": "Color" },
|
|
||||||
{ "key": "item_count", "header": "Items", "data_type": "number" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Create Tag",
|
|
||||||
"content": {
|
|
||||||
"type": "container",
|
|
||||||
"gap": 24,
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"type": "heading",
|
|
||||||
"level": 2,
|
|
||||||
"content": "Create New Tag"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "text",
|
|
||||||
"content": "Tags are used to organise media items. Choose a name and an optional colour.",
|
|
||||||
"variant": "secondary"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "form",
|
|
||||||
"submit_label": "Create Tag",
|
|
||||||
"submit_action": "create-tag",
|
|
||||||
"cancel_label": "Reset",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"id": "name",
|
|
||||||
"label": "Tag Name",
|
|
||||||
"type": { "type": "text", "max_length": 64 },
|
|
||||||
"required": true,
|
|
||||||
"placeholder": "e.g. favourite, to-watch, archived",
|
|
||||||
"help_text": "Must be unique. Alphanumeric characters, spaces, and hyphens.",
|
|
||||||
"validation": [
|
|
||||||
{ "type": "min_length", "value": 1 },
|
|
||||||
{ "type": "max_length", "value": 64 },
|
|
||||||
{ "type": "pattern", "regex": "^[a-zA-Z0-9 \\-]+$" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "color",
|
|
||||||
"label": "Colour",
|
|
||||||
"type": {
|
|
||||||
"type": "select",
|
|
||||||
"options": [
|
|
||||||
{ "value": "#ef4444", "label": "Red" },
|
|
||||||
{ "value": "#f97316", "label": "Orange" },
|
|
||||||
{ "value": "#eab308", "label": "Yellow" },
|
|
||||||
{ "value": "#22c55e", "label": "Green" },
|
|
||||||
{ "value": "#3b82f6", "label": "Blue" },
|
|
||||||
{ "value": "#8b5cf6", "label": "Purple" },
|
|
||||||
{ "value": "#ec4899", "label": "Pink" },
|
|
||||||
{ "value": "#6b7280", "label": "Grey" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"required": false,
|
|
||||||
"default_value": "#3b82f6",
|
|
||||||
"help_text": "Optional accent colour shown beside the tag."
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"data_sources": {
|
|
||||||
"tags": {
|
|
||||||
"type": "endpoint",
|
|
||||||
"path": "/api/v1/tags",
|
|
||||||
"poll_interval": 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"actions": {
|
|
||||||
"create-tag": {
|
|
||||||
"method": "POST",
|
|
||||||
"path": "/api/v1/tags",
|
|
||||||
"success_message": "Tag created successfully!",
|
|
||||||
"error_message": "Failed to create tag: the name may already be in use."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
[plugin]
|
|
||||||
name = "media-stats-ui"
|
|
||||||
version = "1.0.0"
|
|
||||||
api_version = "1.0"
|
|
||||||
author = "Pinakes Contributors"
|
|
||||||
description = "Library statistics dashboard and tag manager UI plugin"
|
|
||||||
homepage = "https://github.com/notashelf/pinakes"
|
|
||||||
license = "EUPL-1.2"
|
|
||||||
kind = ["ui_page"]
|
|
||||||
|
|
||||||
[plugin.binary]
|
|
||||||
wasm = "target/wasm32-unknown-unknown/release/media_stats_ui.wasm"
|
|
||||||
|
|
||||||
[capabilities]
|
|
||||||
network = false
|
|
||||||
|
|
||||||
[capabilities.filesystem]
|
|
||||||
read = []
|
|
||||||
write = []
|
|
||||||
|
|
||||||
[ui]
|
|
||||||
required_endpoints = ["/api/v1/statistics", "/api/v1/media", "/api/v1/tags"]
|
|
||||||
|
|
||||||
# UI pages
|
|
||||||
[[ui.pages]]
|
|
||||||
file = "pages/stats.json"
|
|
||||||
|
|
||||||
[[ui.pages]]
|
|
||||||
file = "pages/tag-manager.json"
|
|
||||||
|
|
||||||
# Widgets injected into host views
|
|
||||||
[[ui.widgets]]
|
|
||||||
id = "stats-badge"
|
|
||||||
target = "library_header"
|
|
||||||
|
|
||||||
[ui.widgets.content]
|
|
||||||
type = "badge"
|
|
||||||
text = "Stats"
|
|
||||||
variant = "info"
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
//! Media Stats UI - Pinakes plugin
|
|
||||||
//!
|
|
||||||
//! A UI-only plugin that adds a library statistics dashboard and a tag manager
|
|
||||||
//! page. All UI definitions live in `pages/stats.json` and
|
|
||||||
//! `pages/tag-manager.json`; this WASM binary provides the minimum lifecycle
|
|
||||||
//! surface the host runtime requires.
|
|
||||||
//!
|
|
||||||
//! This plugin is kind = ["ui_page"]: no media-type, metadata, thumbnail, or
|
|
||||||
//! event-handler extension points are needed. The host will never call them,
|
|
||||||
//! but exporting them avoids linker warnings if the host performs capability
|
|
||||||
//! discovery via symbol inspection.
|
|
||||||
|
|
||||||
#![no_std]
|
|
||||||
|
|
||||||
extern crate alloc;
|
|
||||||
|
|
||||||
use core::alloc::Layout;
|
|
||||||
|
|
||||||
#[global_allocator]
|
|
||||||
static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;
|
|
||||||
|
|
||||||
#[panic_handler]
|
|
||||||
fn panic(_: &core::panic::PanicInfo) -> ! {
|
|
||||||
core::arch::wasm32::unreachable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Host functions provided by the Pinakes runtime.
|
|
||||||
unsafe extern "C" {
|
|
||||||
// Write a result value back to the host (ptr + byte length).
|
|
||||||
fn host_set_result(ptr: i32, len: i32);
|
|
||||||
|
|
||||||
// Emit a structured log message to the host logger.
|
|
||||||
// `level` mirrors tracing severity: 0=trace 1=debug 2=info 3=warn 4=error
|
|
||||||
fn host_log(level: i32, ptr: i32, len: i32);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// `json` is a valid slice; the host copies the bytes before
|
|
||||||
/// returning so there are no lifetime concerns.
|
|
||||||
fn set_response(json: &[u8]) {
|
|
||||||
unsafe { host_set_result(json.as_ptr() as i32, json.len() as i32) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// Same as [`set_response`]
|
|
||||||
fn log_info(msg: &[u8]) {
|
|
||||||
unsafe { host_log(2, msg.as_ptr() as i32, msg.len() as i32) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allocate a buffer for the host to write request data into.
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// The byte offset of the allocation, or -1 on failure.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// Size is positive; Layout construction cannot fail for align=1.
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub extern "C" fn alloc(size: i32) -> i32 {
|
|
||||||
if size <= 0 {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
unsafe {
|
|
||||||
let layout = Layout::from_size_align_unchecked(size as usize, 1);
|
|
||||||
let ptr = alloc::alloc::alloc(layout);
|
|
||||||
if ptr.is_null() { -1 } else { ptr as i32 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called once after the plugin is loaded. Returns 0 on success.
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub extern "C" fn initialize() -> i32 {
|
|
||||||
log_info(b"media-stats-ui: initialized");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Called before the plugin is unloaded. Returns 0 on success.
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub extern "C" fn shutdown() -> i32 {
|
|
||||||
log_info(b"media-stats-ui: shutdown");
|
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// an empty JSON array; this plugin adds no custom media types.
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub extern "C" fn supported_media_types(_ptr: i32, _len: i32) {
|
|
||||||
set_response(b"[]");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Returns
|
|
||||||
///
|
|
||||||
/// An empty JSON array; this plugin handles no event types.
|
|
||||||
#[unsafe(no_mangle)]
|
|
||||||
pub extern "C" fn interested_events(_ptr: i32, _len: i32) {
|
|
||||||
set_response(b"[]");
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue