Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 22 additions & 9 deletions MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public partial class MainWindow : Window
private readonly ConversionService _conversionService;
private readonly KindleWebService _kindleService;
private readonly UpdateService _updateService;
private readonly PluginService _pluginService;
private int _logoClickCount;
private readonly DispatcherTimer _easterEggResetTimer;
private readonly ObservableCollection<BookMetadata> _books = new();
Expand All @@ -38,6 +39,8 @@ public MainWindow()
_conversionService = new ConversionService();
_kindleService = new KindleWebService();
_updateService = new UpdateService();
_pluginService = new PluginService();
_pluginService.LoadPlugins();
Comment on lines +42 to +43

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin service is never unloaded when the application closes. This means plugin Shutdown methods are never called, potentially leaving resources unreleased. Consider adding a handler for window closing event in MainWindow that calls _pluginService.UnloadPlugins() to ensure proper cleanup.

Copilot uses AI. Check for mistakes.
FileListView.ItemsSource = _books;

// Easter egg timer - resets click count after 2 seconds of no clicks
Expand All @@ -63,6 +66,9 @@ public MainWindow()
UpdateDialog.ShowIfAvailable(this, updateInfo);
}
};

// Cleanup plugins on close
Closing += (s, e) => _pluginService.UnloadPlugins();
}

private void UpdateKindleUI()
Expand Down Expand Up @@ -441,7 +447,7 @@ private async void Convert_Click(object sender, RoutedEventArgs e)
OutputPath = dialog.FileName
};

await _conversionService.ConvertAsync(options);
await _conversionService.ConvertAsync(options, _pluginService);

SetStatus($"Saved: {Path.GetFileName(dialog.FileName)}", isSuccess: true);

Expand Down Expand Up @@ -528,7 +534,7 @@ private async void ConvertAll_Click(object sender, RoutedEventArgs e)
OutputPath = outputPath
};

await _conversionService.ConvertAsync(options);
await _conversionService.ConvertAsync(options, _pluginService);
book.Status = "Done";
_lastOutputPath = outputPath;
_lastBatchOutputPaths.Add(outputPath);
Expand Down Expand Up @@ -603,18 +609,25 @@ private void SetStatus(string message, bool isError = false, bool isSuccess = fa
StatusText.Foreground = (SolidColorBrush)FindResource("TextMutedBrush");
}

private static bool IsSupportedFile(string filePath)
private bool IsSupportedFile(string filePath)
{
var ext = Path.GetExtension(filePath).ToLowerInvariant();
return ext == ".mobi" || ext == ".epub";
return ext == ".mobi" || ext == ".epub" || _pluginService.IsPluginSupportedFile(filePath);
}

private static string GetFileTypeDescription(string extension) => extension switch
private string GetFileTypeDescription(string extension)
{
".mobi" => "Kindle MOBI File",
".epub" => "EPUB File",
_ => "Unknown File"
};
var pluginDescription = _pluginService.GetPluginFileTypeDescription(extension);
if (pluginDescription != null)
return pluginDescription;

return extension switch
{
".mobi" => "Kindle MOBI File",
".epub" => "EPUB File",
_ => "Unknown File"
};
}

private static string MakeSafeFilename(string name)
{
Expand Down
61 changes: 61 additions & 0 deletions Plugins/Examples/ExampleFormatPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Booky.Plugins;

namespace Booky.Plugins.Examples;

/// <summary>
/// Example format plugin template.
/// Shows how to add support for additional ebook formats.
///
/// To create a real plugin:
/// 1. Create a new .NET Class Library project
/// 2. Copy this file as a starting point
/// 3. Implement your own conversion logic
/// 4. Build and copy the DLL to Booky's Plugins folder
/// </summary>
public class ExampleFormatPlugin : IFormatPlugin
{
public string Id => "example-format";
public string Name => "Example Format Plugin";
public string Description => "Template for format plugins (adds new input format support)";
public string Version => "1.0.0";

public string[] SupportedExtensions => new[] { ".example" };

public void Initialize()
{
// Called when plugin is loaded
}

public void Shutdown()
{
// Called when plugin is unloaded
}

public string GetFileTypeDescription(string extension)
{
return extension.ToLowerInvariant() switch
{
".example" => "Example Ebook File",
_ => "Unknown File"
};
}

public async Task<PluginResult> ConvertToEpubAsync(string inputPath, string outputPath, string title, string author)
{
// This is where you would implement your conversion logic
//
// For example:
// 1. Read and parse the input file
// 2. Extract content, images, metadata
// 3. Create an EPUB file at outputPath
// 4. Return PluginResult.Ok(outputPath) on success
//
// You can use the same EPUB creation approach as Booky's built-in converter,
// or use a library like EpubSharp, VersOne.Epub, etc.

await Task.CompletedTask; // Placeholder for async operations

// Default: return failure (not implemented)
return PluginResult.Fail("This is an example plugin - conversion not implemented");
}
}
54 changes: 54 additions & 0 deletions Plugins/Examples/ExamplePreConversionPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Booky.Plugins;

namespace Booky.Plugins.Examples;

/// <summary>
/// Example pre-conversion plugin template.
/// This is a SKELETON - it does not contain any actual DRM removal code.
///
/// To create a real plugin:
/// 1. Create a new .NET Class Library project
/// 2. Copy this file as a starting point
/// 3. Implement your own processing logic
/// 4. Build and copy the DLL to Booky's Plugins folder
/// </summary>
public class ExamplePreConversionPlugin : IPreConversionPlugin
{
public string Id => "example-pre-conversion";
public string Name => "Example Pre-Conversion Plugin";
public string Description => "Template for pre-conversion plugins (e.g., file preprocessing)";
public string Version => "1.0.0";

public string[] SupportedExtensions => new[] { ".mobi", ".azw", ".azw3" };

public void Initialize()
{
// Called when plugin is loaded
// Initialize any resources here
}

public void Shutdown()
{
// Called when plugin is unloaded
// Clean up resources here
}

public async Task<PluginResult> ProcessAsync(string inputPath, string outputPath)
{
// This is where you would implement your processing logic
//
// For example:
// 1. Read the input file
// 2. Process it (decrypt, normalize, etc.)
// 3. Write the result to outputPath
// 4. Return PluginResult.Ok(outputPath) on success
//
// If you can't process this file, return PluginResult.Skip()
// If there's an error, return PluginResult.Fail("error message")

await Task.CompletedTask; // Placeholder for async operations

// Default: skip processing (pass through unchanged)
return PluginResult.Skip();
Comment on lines +36 to +52

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example shows returning PluginResult.Skip() as the default behavior, but this could be confusing for plugin developers. When a plugin returns Skip with no OutputPath, the pre-conversion logic in PluginService doesn't modify currentPath, effectively passing through the original file. However, the tempOutput file that was pre-created is never deleted, causing a resource leak. Consider updating the example to show proper cleanup or changing the PluginService to not create tempOutput until the plugin confirms it will process the file.

Copilot uses AI. Check for mistakes.
}
}
105 changes: 105 additions & 0 deletions Plugins/IBookyPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
namespace Booky.Plugins;

/// <summary>
/// Base interface for all Booky plugins.
/// Plugins are loaded from DLLs in the Plugins folder.
/// </summary>
public interface IBookyPlugin
{
/// <summary>
/// Unique identifier for the plugin
/// </summary>
string Id { get; }

/// <summary>
/// Display name shown in UI
/// </summary>
string Name { get; }

/// <summary>
/// Description of what the plugin does
/// </summary>
string Description { get; }

/// <summary>
/// Plugin version
/// </summary>
string Version { get; }

/// <summary>
/// Called when the plugin is loaded
/// </summary>
void Initialize();

/// <summary>
/// Called when the plugin is unloaded
/// </summary>
void Shutdown();
}

/// <summary>
/// Plugin that processes files before conversion.
/// Use case: DRM removal, format normalization, etc.
/// </summary>
public interface IPreConversionPlugin : IBookyPlugin
{
/// <summary>
/// File extensions this plugin can process (e.g., ".mobi", ".azw")
/// </summary>
string[] SupportedExtensions { get; }

/// <summary>
/// Process a file before conversion.
/// </summary>
/// <param name="inputPath">Path to the input file</param>
/// <param name="outputPath">Path where processed file should be written</param>
/// <returns>PluginResult with Success=true and OutputPath set if file was processed,
/// or PluginResult.Skip() if this plugin chose not to process the file</returns>
Task<PluginResult> ProcessAsync(string inputPath, string outputPath);
}

/// <summary>
/// Plugin that processes files after conversion.
/// Use case: Metadata enhancement, cover generation, etc.
/// </summary>
public interface IPostConversionPlugin : IBookyPlugin
{
/// <summary>
/// Process a file after conversion to EPUB.
/// </summary>
/// <param name="epubPath">Path to the converted EPUB file</param>
/// <returns>Result of the processing</returns>
Task<PluginResult> ProcessAsync(string epubPath);
}

/// <summary>
/// Plugin that adds support for additional input formats.
/// </summary>
public interface IFormatPlugin : IBookyPlugin
{
/// <summary>
/// File extensions this plugin handles (e.g., ".azw3", ".kfx")
/// </summary>
string[] SupportedExtensions { get; }

/// <summary>
/// Description for the file type (e.g., "Kindle AZW3 File")
/// </summary>
string GetFileTypeDescription(string extension);

/// <summary>
/// Convert the input file to EPUB.
/// </summary>
Task<PluginResult> ConvertToEpubAsync(string inputPath, string outputPath, string title, string author);
}

public class PluginResult
{
public bool Success { get; set; }
public string? ErrorMessage { get; set; }
public string? OutputPath { get; set; }

public static PluginResult Ok(string? outputPath = null) => new() { Success = true, OutputPath = outputPath };
public static PluginResult Fail(string error) => new() { Success = false, ErrorMessage = error };
public static PluginResult Skip() => new() { Success = true }; // Plugin chose not to process
}
83 changes: 83 additions & 0 deletions Plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Booky Plugins

Booky supports plugins to extend its functionality. Plugins are .NET DLLs placed in this folder.

## Plugin Types

| Interface | Purpose | Example Use Case |
|-----------|---------|------------------|
| `IPreConversionPlugin` | Process files before conversion | DRM removal, format normalization |
| `IPostConversionPlugin` | Process files after conversion | Metadata enhancement, cover generation |
| `IFormatPlugin` | Add support for new input formats | AZW3, KFX support |

## Creating a Plugin

1. Create a .NET Class Library project targeting `net8.0-windows`
2. Reference `Booky.exe` or copy `IBookyPlugin.cs` interfaces
3. Implement one or more plugin interfaces
4. Build and copy the DLL to the `Plugins` folder

### Example: Pre-Conversion Plugin

```csharp
using Booky.Plugins;

public class MyDrmPlugin : IPreConversionPlugin
{
public string Id => "my-drm-plugin";
public string Name => "My DRM Plugin";
public string Description => "Removes DRM from ebooks";
public string Version => "1.0.0";
public string[] SupportedExtensions => new[] { ".mobi", ".azw" };

public void Initialize() { }
public void Shutdown() { }

public async Task<PluginResult> ProcessAsync(string inputPath, string outputPath)
{
// Your DRM removal logic here
// Write the processed file to outputPath

return PluginResult.Ok(outputPath);
}
}
```

### Example: Format Plugin

```csharp
using Booky.Plugins;

public class Azw3Plugin : IFormatPlugin
{
public string Id => "azw3-plugin";
public string Name => "AZW3 Support";
public string Description => "Adds AZW3 format support";
public string Version => "1.0.0";
public string[] SupportedExtensions => new[] { ".azw3" };

public void Initialize() { }
public void Shutdown() { }

public string GetFileTypeDescription(string extension) => "Kindle AZW3 File";

public async Task<PluginResult> ConvertToEpubAsync(string inputPath, string outputPath, string title, string author)
{
// Your conversion logic here

return PluginResult.Ok(outputPath);
}
}
```

## Plugin Guidelines

- Plugins should fail gracefully and not crash Booky
- Return `PluginResult.Skip()` if the plugin can't process a file

Copilot AI Jan 16, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation states that plugins return PluginResult.Skip() if they can't process a file, but the actual behavior in RunPreConversionPluginsAsync treats Skip() the same as a failure - the plugin just doesn't modify the file. The documentation should clarify that Skip() means "plugin chose not to process this file, continue with the original file" rather than implying it's an error condition.

Suggested change
- Return `PluginResult.Skip()` if the plugin can't process a file
- Return `PluginResult.Skip()` when the plugin chooses not to process a file (Booky will continue using the original, unmodified file)

Copilot uses AI. Check for mistakes.
- Return `PluginResult.Fail("error message")` on errors
- Plugins are loaded on startup from all `.dll` files in this folder
- Use async/await for long-running operations

## Legal Notice

Booky does not include or endorse any DRM removal functionality. Any plugins that remove DRM are the responsibility of the plugin author and user. Users should only use such plugins for content they legally own and have the right to convert.
Loading