diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 4d7fa5c..892dbb0 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -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 _books = new(); @@ -38,6 +39,8 @@ public MainWindow() _conversionService = new ConversionService(); _kindleService = new KindleWebService(); _updateService = new UpdateService(); + _pluginService = new PluginService(); + _pluginService.LoadPlugins(); FileListView.ItemsSource = _books; // Easter egg timer - resets click count after 2 seconds of no clicks @@ -63,6 +66,9 @@ public MainWindow() UpdateDialog.ShowIfAvailable(this, updateInfo); } }; + + // Cleanup plugins on close + Closing += (s, e) => _pluginService.UnloadPlugins(); } private void UpdateKindleUI() @@ -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); @@ -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); @@ -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) { diff --git a/Plugins/Examples/ExampleFormatPlugin.cs b/Plugins/Examples/ExampleFormatPlugin.cs new file mode 100644 index 0000000..8af7395 --- /dev/null +++ b/Plugins/Examples/ExampleFormatPlugin.cs @@ -0,0 +1,61 @@ +using Booky.Plugins; + +namespace Booky.Plugins.Examples; + +/// +/// 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 +/// +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 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"); + } +} diff --git a/Plugins/Examples/ExamplePreConversionPlugin.cs b/Plugins/Examples/ExamplePreConversionPlugin.cs new file mode 100644 index 0000000..6c20f85 --- /dev/null +++ b/Plugins/Examples/ExamplePreConversionPlugin.cs @@ -0,0 +1,54 @@ +using Booky.Plugins; + +namespace Booky.Plugins.Examples; + +/// +/// 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 +/// +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 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(); + } +} diff --git a/Plugins/IBookyPlugin.cs b/Plugins/IBookyPlugin.cs new file mode 100644 index 0000000..157bd2c --- /dev/null +++ b/Plugins/IBookyPlugin.cs @@ -0,0 +1,105 @@ +namespace Booky.Plugins; + +/// +/// Base interface for all Booky plugins. +/// Plugins are loaded from DLLs in the Plugins folder. +/// +public interface IBookyPlugin +{ + /// + /// Unique identifier for the plugin + /// + string Id { get; } + + /// + /// Display name shown in UI + /// + string Name { get; } + + /// + /// Description of what the plugin does + /// + string Description { get; } + + /// + /// Plugin version + /// + string Version { get; } + + /// + /// Called when the plugin is loaded + /// + void Initialize(); + + /// + /// Called when the plugin is unloaded + /// + void Shutdown(); +} + +/// +/// Plugin that processes files before conversion. +/// Use case: DRM removal, format normalization, etc. +/// +public interface IPreConversionPlugin : IBookyPlugin +{ + /// + /// File extensions this plugin can process (e.g., ".mobi", ".azw") + /// + string[] SupportedExtensions { get; } + + /// + /// Process a file before conversion. + /// + /// Path to the input file + /// Path where processed file should be written + /// PluginResult with Success=true and OutputPath set if file was processed, + /// or PluginResult.Skip() if this plugin chose not to process the file + Task ProcessAsync(string inputPath, string outputPath); +} + +/// +/// Plugin that processes files after conversion. +/// Use case: Metadata enhancement, cover generation, etc. +/// +public interface IPostConversionPlugin : IBookyPlugin +{ + /// + /// Process a file after conversion to EPUB. + /// + /// Path to the converted EPUB file + /// Result of the processing + Task ProcessAsync(string epubPath); +} + +/// +/// Plugin that adds support for additional input formats. +/// +public interface IFormatPlugin : IBookyPlugin +{ + /// + /// File extensions this plugin handles (e.g., ".azw3", ".kfx") + /// + string[] SupportedExtensions { get; } + + /// + /// Description for the file type (e.g., "Kindle AZW3 File") + /// + string GetFileTypeDescription(string extension); + + /// + /// Convert the input file to EPUB. + /// + Task 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 +} diff --git a/Plugins/README.md b/Plugins/README.md new file mode 100644 index 0000000..20fff07 --- /dev/null +++ b/Plugins/README.md @@ -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 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 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 +- 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. diff --git a/Services/ConversionService.cs b/Services/ConversionService.cs index d858fe4..3ed8640 100644 --- a/Services/ConversionService.cs +++ b/Services/ConversionService.cs @@ -102,14 +102,67 @@ private static BookMetadata ParseMobiToolOutput(string output) return metadata; } - public async Task ConvertAsync(ConversionOptions options) + public async Task ConvertAsync(ConversionOptions options, PluginService? pluginService = null) { - var ext = Path.GetExtension(options.InputPath).ToLowerInvariant(); + var inputPath = options.InputPath; + var ext = Path.GetExtension(inputPath).ToLowerInvariant(); + string? tempInputPath = null; - if (ext != ".mobi") - throw new NotSupportedException($"Unsupported format: {ext}. Only MOBI files are supported."); + try + { + // Run pre-conversion plugins (e.g., DRM removal) + if (pluginService != null) + { + inputPath = await pluginService.RunPreConversionPluginsAsync(inputPath); + + // Track if plugins created a temp file + if (inputPath != options.InputPath) + tempInputPath = inputPath; + } + + // Try format plugins first + if (pluginService != null) + { + var pluginResult = await pluginService.ConvertWithPluginAsync(inputPath, options.OutputPath, options.Title, options.Author); + if (pluginResult != null) + { + if (!pluginResult.Success) + throw new Exception(pluginResult.ErrorMessage ?? "Plugin conversion failed"); + + // Run post-conversion plugins + await pluginService.RunPostConversionPluginsAsync(options.OutputPath); + return; + } + } + + // Built-in conversion + if (ext != ".mobi") + throw new NotSupportedException($"Unsupported format: {ext}. Only MOBI files are supported."); - await ConvertMobiToEpubAsync(options); + var modifiedOptions = new ConversionOptions + { + InputPath = inputPath, + OutputPath = options.OutputPath, + Title = options.Title, + Author = options.Author + }; + + await ConvertMobiToEpubAsync(modifiedOptions); + + // Run post-conversion plugins + if (pluginService != null) + { + await pluginService.RunPostConversionPluginsAsync(options.OutputPath); + } + } + finally + { + // Clean up temp file created by pre-conversion plugins + if (tempInputPath != null && File.Exists(tempInputPath)) + { + try { File.Delete(tempInputPath); } catch { } + } + } } diff --git a/Services/PluginService.cs b/Services/PluginService.cs new file mode 100644 index 0000000..02600d1 --- /dev/null +++ b/Services/PluginService.cs @@ -0,0 +1,174 @@ +using System.IO; +using System.Reflection; +using Booky.Plugins; + +namespace Booky.Services; + +public class PluginService +{ + private readonly List _plugins = new(); + private readonly string _pluginsPath; + + public IReadOnlyList Plugins => _plugins.AsReadOnly(); + public IEnumerable PreConversionPlugins => _plugins.OfType(); + public IEnumerable PostConversionPlugins => _plugins.OfType(); + public IEnumerable FormatPlugins => _plugins.OfType(); + + public PluginService() + { + // Plugins folder next to the executable + var exeDir = AppContext.BaseDirectory; + _pluginsPath = Path.Combine(exeDir, "Plugins"); + } + + public void LoadPlugins() + { + if (!Directory.Exists(_pluginsPath)) + { + Directory.CreateDirectory(_pluginsPath); + return; + } + + var dllFiles = Directory.GetFiles(_pluginsPath, "*.dll"); + + foreach (var dllPath in dllFiles) + { + try + { + LoadPluginFromDll(dllPath); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to load plugin {dllPath}: {ex.Message}"); + } + } + } + + private void LoadPluginFromDll(string dllPath) + { + var assembly = Assembly.LoadFrom(dllPath); + + var pluginTypes = assembly.GetTypes() + .Where(t => typeof(IBookyPlugin).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract); + + foreach (var type in pluginTypes) + { + try + { + if (Activator.CreateInstance(type) is IBookyPlugin plugin) + { + plugin.Initialize(); + _plugins.Add(plugin); + System.Diagnostics.Debug.WriteLine($"Loaded plugin: {plugin.Name} v{plugin.Version}"); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to initialize plugin {type.Name}: {ex.Message}"); + } + } + } + + public void UnloadPlugins() + { + foreach (var plugin in _plugins) + { + try + { + plugin.Shutdown(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Failed to shut down plugin {plugin.Name}: {ex.Message}"); + } + } + _plugins.Clear(); + } + + /// + /// Check if any plugin supports the given file extension + /// + public bool IsPluginSupportedFile(string filePath) + { + var ext = Path.GetExtension(filePath).ToLowerInvariant(); + + return PreConversionPlugins.Any(p => p.SupportedExtensions.Contains(ext)) + || FormatPlugins.Any(p => p.SupportedExtensions.Contains(ext)); + } + + /// + /// Get file type description from plugins + /// + public string? GetPluginFileTypeDescription(string extension) + { + var normalizedExtension = extension.ToLowerInvariant(); + var formatPlugin = FormatPlugins.FirstOrDefault(p => p.SupportedExtensions.Contains(normalizedExtension)); + return formatPlugin?.GetFileTypeDescription(normalizedExtension); + } + + /// + /// Run pre-conversion plugins on a file + /// + public async Task RunPreConversionPluginsAsync(string inputPath) + { + var ext = Path.GetExtension(inputPath).ToLowerInvariant(); + var currentPath = inputPath; + + foreach (var plugin in PreConversionPlugins.Where(p => p.SupportedExtensions.Contains(ext))) + { + var tempOutput = Path.Combine(Path.GetTempPath(), $"booky_pre_{Guid.NewGuid()}{ext}"); + + try + { + var result = await plugin.ProcessAsync(currentPath, tempOutput); + + if (result.Success && !string.IsNullOrEmpty(result.OutputPath)) + { + // Clean up previous temp file if it's not the original + if (currentPath != inputPath && File.Exists(currentPath)) + File.Delete(currentPath); + + currentPath = result.OutputPath; + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Plugin {plugin.Name} failed: {ex.Message}"); + } + } + + return currentPath; + } + + /// + /// Run post-conversion plugins on an EPUB file + /// + public async Task RunPostConversionPluginsAsync(string epubPath) + { + foreach (var plugin in PostConversionPlugins) + { + try + { + await plugin.ProcessAsync(epubPath); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Plugin {plugin.Name} failed: {ex.Message}"); + } + } + } + + /// + /// Convert using a format plugin + /// + public async Task ConvertWithPluginAsync(string inputPath, string outputPath, string title, string author) + { + var ext = Path.GetExtension(inputPath).ToLowerInvariant(); + var plugin = FormatPlugins.FirstOrDefault(p => p.SupportedExtensions.Contains(ext)); + + if (plugin == null) + return null; + + return await plugin.ConvertToEpubAsync(inputPath, outputPath, title, author); + } +}