/** * BreakPilot H5P Service * Self-hosted H5P Interactive Content Server using @lumieducation/h5p-express */ import express from 'express'; import cors from 'cors'; import path from 'path'; import { fileURLToPath } from 'url'; import bodyParser from 'body-parser'; import { H5PEditor, H5PPlayer, fsImplementations, H5PConfig } from '@lumieducation/h5p-server'; import { h5pAjaxExpressRouter, libraryAdministrationExpressRouter, contentTypeCacheExpressRouter } from '@lumieducation/h5p-express'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 8080; // Middleware app.use(cors()); app.use(bodyParser.json({ limit: '500mb' })); app.use(bodyParser.urlencoded({ extended: true, limit: '500mb' })); // H5P Configuration const config = new H5PConfig( new fsImplementations.InMemoryStorage(), { baseUrl: 'http://localhost:8003', contentFilesUrl: '/h5p/content', downloadUrl: '/h5p/download', coreUrl: '/h5p/core', librariesUrl: '/h5p/libraries', playUrl: '/h5p/play', ajaxUrl: '/h5p/ajax' } ); // Storage implementations const contentStorage = new fsImplementations.FileContentStorage( process.env.H5P_STORAGE_PATH || path.join(__dirname, 'h5p-content') ); const libraryStorage = new fsImplementations.FileLibraryStorage( path.join(__dirname, 'h5p-libraries') ); const temporaryStorage = new fsImplementations.DirectoryTemporaryFileStorage( path.join(__dirname, 'h5p-temp') ); // Initialize H5P Editor and Player let h5pEditor; let h5pPlayer; async function initH5P() { try { h5pEditor = new H5PEditor( contentStorage, config, libraryStorage, undefined, undefined, temporaryStorage ); h5pPlayer = new H5PPlayer( libraryStorage, contentStorage, config ); // Install H5P core files await h5pEditor.installLibraryFromHub('H5P.Column'); console.log('✅ H5P Editor and Player initialized'); return true; } catch (error) { console.error('⚠️ H5P initialization:', error.message); // Continue even if library install fails - editor will show library list return true; } } // User object for H5P (simplified) const getUser = (req) => ({ id: req.headers['x-user-id'] || 'anonymous', name: req.headers['x-user-name'] || 'Anonymous', email: req.headers['x-user-email'] || 'anonymous@breakpilot.app', canInstallRecommended: true, canUpdateAndInstallLibraries: true, canCreateRestricted: true, type: 'local' }); // Function to register H5P routes (called after init) function registerH5PRoutes() { // Serve H5P core static files (JS/CSS) app.use('/h5p/core', express.static(path.join(__dirname, 'h5p-core'))); app.use('/h5p/editor', express.static(path.join(__dirname, 'h5p-core'))); // Serve H5P libraries app.use('/h5p/libraries', express.static(path.join(__dirname, 'h5p-libraries'))); // Serve H5P content files app.use('/h5p/content', express.static(path.join(__dirname, 'h5p-content'))); // ============= H5P AJAX ROUTES (from h5p-express) ============= app.use( '/h5p/ajax', h5pAjaxExpressRouter( h5pEditor, path.resolve('h5p-core'), path.resolve('h5p-libraries'), (req) => getUser(req) ) ); // ============= LIBRARY ADMINISTRATION ============= app.use( '/h5p/libraries-admin', libraryAdministrationExpressRouter( h5pEditor, (req) => getUser(req) ) ); // ============= CONTENT TYPE CACHE ============= app.use( '/h5p/content-type-cache', contentTypeCacheExpressRouter( h5pEditor, (req) => getUser(req) ) ); } // ============= EDITOR & PLAYER HTML PAGES ============= // Create new H5P content (Editor UI) app.get('/h5p/editor/new', async (req, res) => { try { const editorModel = await h5pEditor.render(undefined, 'en', getUser(req)); const html = ` H5P Editor - BreakPilot ${editorModel.styles.map(style => ``).join('\n ')}

🎓 H5P Content Creator

${editorModel.html}
${editorModel.scripts.map(script => ``).join('\n ')} `; res.send(html); } catch (error) { console.error('Editor render error:', error); res.status(500).send(`

Error loading H5P Editor

Error: ${error.message}

Please check that H5P libraries are installed.

`); } }); // Edit existing H5P content app.get('/h5p/editor/:contentId', async (req, res) => { try { const editorModel = await h5pEditor.render(req.params.contentId, 'en', getUser(req)); const html = ` H5P Editor - ${req.params.contentId} ${editorModel.styles.map(style => ``).join('\n ')}

Edit H5P Content

${editorModel.html} ${editorModel.scripts.map(script => ``).join('\n ')} `; res.send(html); } catch (error) { res.status(500).send(`Error: ${error.message}`); } }); // Play H5P content app.get('/h5p/play/:contentId', async (req, res) => { try { const playerModel = await h5pPlayer.render(req.params.contentId, getUser(req)); res.send(` H5P Player ${playerModel.styles.map(s => ``).join('\n ')}
${playerModel.html}
${playerModel.scripts.map(s => ``).join('\n ')} `); } catch (error) { res.status(500).send(`Error: ${error.message}`); } }); // ============= CONTENT MANAGEMENT ============= // Save/Update content app.post('/h5p/content/:contentId?', async (req, res) => { try { const contentId = req.params.contentId; const { library, params } = req.body; const savedId = await h5pEditor.saveOrUpdateContent( contentId || undefined, params, library, getUser(req) ); res.json({ success: true, contentId: savedId, message: 'Content saved successfully' }); } catch (error) { console.error('Save content error:', error); res.status(500).json({ success: false, error: error.message }); } }); // List all content app.get('/h5p/content', async (req, res) => { try { const contentIds = await contentStorage.listContent(); res.json({ contentIds }); } catch (error) { res.status(500).json({ error: error.message }); } }); // Delete content app.delete('/h5p/content/:contentId', async (req, res) => { try { await contentStorage.deleteContent(req.params.contentId, getUser(req)); res.json({ success: true, message: 'Content deleted' }); } catch (error) { res.status(500).json({ error: error.message }); } }); // ============= INFO & HEALTH ENDPOINTS ============= app.get('/', (req, res) => { res.send(` BreakPilot H5P Service

🎓 H5P Service

✅ Running
Create New H5P Content Health Check
`); }); app.get('/health', (req, res) => { res.json({ status: 'healthy', service: 'h5p-service', version: '1.0.0' }); }); app.get('/info', (req, res) => { res.json({ service: 'BreakPilot H5P Service', version: '1.0.0', endpoints: { root: '/', health: '/health', editorNew: '/h5p/editor/new', editorEdit: '/h5p/editor/:contentId', player: '/h5p/play/:contentId', contentList: '/h5p/content' } }); }); // Debug endpoint to see what render() returns app.get('/debug/editor-model', async (req, res) => { try { const model = await h5pEditor.render(undefined, 'en', getUser(req)); res.json({ styles: model.styles, scripts: model.scripts, integration: model.integration }); } catch (error) { res.status(500).json({ error: error.message, stack: error.stack }); } }); // Start server async function start() { try { await initH5P(); // Register H5P routes after initialization registerH5PRoutes(); app.listen(PORT, () => { console.log(` ╔════════════════════════════════════════════════════╗ ║ 🎓 BreakPilot H5P Service ║ ║ 📍 http://localhost:${PORT} ║ ║ ✅ Ready to create interactive content! ║ ╚════════════════════════════════════════════════════╝ `); }); } catch (error) { console.error('Failed to start H5P service:', error); process.exit(1); } } start();