coursewatcher 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +26 -23
  2. package/dist/app/cli/main.js +36 -0
  3. package/dist/app/server/create-app.js +151 -0
  4. package/dist/app/server/start-server.js +72 -0
  5. package/dist/modules/catalog/catalog-mappers.js +49 -0
  6. package/dist/modules/catalog/catalog-repository.js +168 -0
  7. package/dist/modules/catalog/catalog-service.js +76 -0
  8. package/dist/modules/notes/notes-repository.js +25 -0
  9. package/dist/modules/notes/notes-service.js +28 -0
  10. package/dist/modules/playback/playback-repository.js +32 -0
  11. package/dist/modules/playback/playback-service.js +89 -0
  12. package/dist/platform/config/app-config.js +24 -0
  13. package/dist/platform/config/package-info.js +21 -0
  14. package/dist/platform/database/database-manager.js +101 -0
  15. package/dist/platform/errors/app-error.js +30 -0
  16. package/dist/platform/logging/logger.js +21 -0
  17. package/dist/shared/contracts/api.js +2 -0
  18. package/dist/web/assets/api-client-hFlLSS3K.js +1 -0
  19. package/dist/web/assets/catalog-route-pOIhR3yd.js +1 -0
  20. package/dist/web/assets/index-CwspbIw1.js +10 -0
  21. package/dist/web/assets/index-VjwsJnuQ.css +1 -0
  22. package/dist/web/assets/jsx-runtime-C2ZT__TU.js +4 -0
  23. package/dist/web/assets/playback-route-Bmy_Z7k7.js +2 -0
  24. package/dist/web/assets/search-route-CeGVOVPT.js +1 -0
  25. package/dist/web/index.html +14 -0
  26. package/package.json +75 -57
  27. package/public/css/styles.css +0 -1375
  28. package/public/js/player.js +0 -359
  29. package/src/cli.js +0 -45
  30. package/src/controllers/video-controller.js +0 -175
  31. package/src/models/database.js +0 -169
  32. package/src/server.js +0 -179
  33. package/src/services/notes-service.js +0 -97
  34. package/src/services/progress-service.js +0 -148
  35. package/src/services/video-service.js +0 -354
  36. package/src/utils/config.js +0 -56
  37. package/src/utils/errors.js +0 -57
  38. package/src/utils/logger.js +0 -48
  39. package/views/layouts/main.ejs +0 -13
  40. package/views/pages/error.ejs +0 -25
  41. package/views/pages/index.ejs +0 -101
  42. package/views/pages/player.ejs +0 -161
  43. package/views/pages/search.ejs +0 -63
  44. package/views/partials/footer.ejs +0 -3
  45. package/views/partials/head.ejs +0 -8
  46. package/views/partials/header.ejs +0 -14
  47. package/views/partials/video-card.ejs +0 -36
@@ -1,359 +0,0 @@
1
- /**
2
- * Video Player Controls
3
- *
4
- * Handles Plyr initialization, keyboard shortcuts, progress auto-save,
5
- * and autoplay countdown functionality.
6
- */
7
-
8
- (function () {
9
- 'use strict';
10
-
11
- // Constants
12
- const SAVE_INTERVAL = 5000; // Save progress every 5 seconds
13
- const AUTOPLAY_COUNTDOWN = 5; // Seconds before auto-navigating to next video
14
-
15
- // Elements
16
- const videoElement = document.getElementById('videoPlayer');
17
-
18
- // Notes & Status
19
- const notesEditor = document.getElementById('notesEditor');
20
- const saveNotesBtn = document.getElementById('saveNotes');
21
- const notesSaveStatus = document.getElementById('notesSaveStatus');
22
- const statusButtons = document.querySelectorAll('.status-btn');
23
-
24
- // Autoplay elements
25
- const autoplayOverlay = document.getElementById('autoplayOverlay');
26
- const countdownNumber = document.getElementById('countdownNumber');
27
- const countdownProgress = document.getElementById('countdownProgress');
28
- const nextVideoTitle = document.getElementById('nextVideoTitle');
29
- const cancelAutoplayBtn = document.getElementById('cancelAutoplay');
30
-
31
- if (!videoElement) return;
32
-
33
- const videoId = videoElement.dataset.videoId;
34
- const savedPosition = parseFloat(videoElement.dataset.savedPosition) || 0;
35
- const nextVideoUrl = videoElement.dataset.nextVideoUrl;
36
-
37
- let saveTimeout = null;
38
- let lastSavedPosition = savedPosition;
39
- let autoplayInterval = null;
40
- let autoplayCountdown = AUTOPLAY_COUNTDOWN;
41
-
42
- // ==========================================
43
- // Initialization
44
- // ==========================================
45
-
46
- function init() {
47
- // Initialize Plyr
48
- const player = new Plyr('#videoPlayer', {
49
- keyboard: { focused: true, global: true },
50
- seekTime: 5,
51
- controls: [
52
- 'play-large', // The large play button in the center
53
- 'restart', // Restart playback
54
- 'rewind', // Rewind by the seek time (default 10 seconds)
55
- 'play', // Play/pause playback
56
- 'fast-forward', // Fast forward by the seek time (default 10 seconds)
57
- 'progress', // The progress bar and scrubber for playback and buffering
58
- 'current-time', // The current time of playback
59
- 'duration', // The full duration of the media
60
- 'mute', // Toggle mute
61
- 'volume', // Volume control
62
- 'captions', // Toggle captions
63
- 'settings', // Settings menu
64
- 'pip', // Picture-in-picture (currently Safari only)
65
- 'airplay', // Airplay (currently Safari only)
66
- 'fullscreen', // Toggle fullscreen
67
- ]
68
- });
69
-
70
- // Restore saved position and autoplay
71
- player.on('ready', () => {
72
- if (savedPosition > 0) {
73
- player.currentTime = savedPosition;
74
- }
75
-
76
- // Always try to autoplay
77
- player.play().catch(() => {
78
- // Autoplay might be blocked by browser on first visit, ignore
79
- console.log('Autoplay was prevented by browser');
80
- });
81
- });
82
-
83
- // Setup Logic
84
- setupStatusControls();
85
- setupNotesControls();
86
- setupProgressAutoSave(player);
87
- setupAutoplay(player);
88
-
89
- // Custom shortcuts not covered by Plyr (if any)
90
- // Plyr covers Space, K, F, M, Arrow keys.
91
- // We can add custom ones if needed, but standard ones are usually enough.
92
- }
93
-
94
- // ==========================================
95
- // Status Controls
96
- // ==========================================
97
-
98
- function setupStatusControls() {
99
- statusButtons.forEach(btn => {
100
- btn.addEventListener('click', () => {
101
- const status = btn.dataset.status;
102
- updateStatus(status);
103
- });
104
- });
105
- }
106
-
107
- async function updateStatus(status) {
108
- try {
109
- const response = await fetch(`/api/videos/${videoId}/status`, {
110
- method: 'POST',
111
- headers: { 'Content-Type': 'application/json' },
112
- body: JSON.stringify({ status }),
113
- });
114
-
115
- if (response.ok) {
116
- // Update UI
117
- statusButtons.forEach(btn => {
118
- btn.classList.toggle('active', btn.dataset.status === status);
119
- });
120
- }
121
- } catch (err) {
122
- console.error('Failed to update status:', err);
123
- }
124
- }
125
-
126
- // ==========================================
127
- // Progress Auto-Save
128
- // ==========================================
129
-
130
- function setupProgressAutoSave(player) {
131
- // Save on pause
132
- player.on('pause', () => saveProgress(player));
133
-
134
- // Save on video end (but don't update status here, autoplay handles it)
135
- player.on('ended', () => {
136
- saveProgress(player);
137
- updateStatus('completed');
138
- });
139
-
140
- // Periodic save during playback
141
- player.on('timeupdate', () => {
142
- const currentPos = Math.floor(player.currentTime);
143
-
144
- // Only save every 5 seconds of playback
145
- if (Math.abs(currentPos - lastSavedPosition) >= 5) {
146
- scheduleProgressSave(player);
147
- }
148
- });
149
-
150
- // Save before page unload
151
- window.addEventListener('beforeunload', () => {
152
- saveProgressSync(player);
153
- });
154
- }
155
-
156
- function scheduleProgressSave(player) {
157
- if (saveTimeout) return;
158
-
159
- saveTimeout = setTimeout(() => {
160
- saveProgress(player);
161
- saveTimeout = null;
162
- }, 1000);
163
- }
164
-
165
- async function saveProgress(player) {
166
- const position = player.currentTime;
167
- const duration = player.duration;
168
-
169
- if (isNaN(position) || isNaN(duration)) return;
170
-
171
- try {
172
- await fetch(`/api/videos/${videoId}/progress`, {
173
- method: 'POST',
174
- headers: { 'Content-Type': 'application/json' },
175
- body: JSON.stringify({ position, duration }),
176
- });
177
- lastSavedPosition = position;
178
- } catch (err) {
179
- console.error('Failed to save progress:', err);
180
- }
181
- }
182
-
183
- function saveProgressSync(player) {
184
- const position = player.currentTime;
185
- const duration = player.duration;
186
-
187
- if (isNaN(position) || isNaN(duration)) return;
188
-
189
- // Use sendBeacon for reliable delivery on page unload
190
- navigator.sendBeacon(
191
- `/api/videos/${videoId}/progress`,
192
- new Blob([JSON.stringify({ position, duration })], { type: 'application/json' })
193
- );
194
- }
195
-
196
- // ==========================================
197
- // Autoplay Countdown
198
- // ==========================================
199
-
200
- function setupAutoplay(player) {
201
- if (!nextVideoUrl || !autoplayOverlay) return;
202
-
203
- // Get skip button
204
- const skipToNextBtn = document.getElementById('skipToNext');
205
-
206
- // Get next video title from queue
207
- const nextVideoId = nextVideoUrl.split('/').pop();
208
- const nextQueueItem = document.querySelector(`.queue-item[data-video-id="${nextVideoId}"]`);
209
- const nextTitle = nextQueueItem ? nextQueueItem.dataset.videoTitle : 'Next Video';
210
-
211
- // Set the title in the overlay
212
- if (nextVideoTitle) {
213
- nextVideoTitle.textContent = nextTitle;
214
- }
215
-
216
- // When video ends, start countdown
217
- player.on('ended', () => {
218
- startAutoplayCountdown();
219
- });
220
-
221
- // Skip button - navigate immediately
222
- if (skipToNextBtn) {
223
- skipToNextBtn.addEventListener('click', () => {
224
- if (autoplayInterval) {
225
- clearInterval(autoplayInterval);
226
- }
227
- window.location.href = nextVideoUrl;
228
- });
229
- }
230
-
231
- // Cancel button
232
- if (cancelAutoplayBtn) {
233
- cancelAutoplayBtn.addEventListener('click', () => {
234
- cancelAutoplayCountdown();
235
- });
236
- }
237
-
238
- // ESC key to cancel
239
- document.addEventListener('keydown', (e) => {
240
- if (e.key === 'Escape' && !autoplayOverlay.classList.contains('hidden')) {
241
- cancelAutoplayCountdown();
242
- }
243
- });
244
- }
245
-
246
- function startAutoplayCountdown() {
247
- if (!nextVideoUrl) return;
248
-
249
- autoplayCountdown = AUTOPLAY_COUNTDOWN;
250
-
251
- // Show overlay
252
- autoplayOverlay.classList.remove('hidden');
253
-
254
- // Update countdown display
255
- updateCountdownDisplay();
256
-
257
- // Start countdown interval
258
- autoplayInterval = setInterval(() => {
259
- autoplayCountdown--;
260
- updateCountdownDisplay();
261
-
262
- if (autoplayCountdown <= 0) {
263
- clearInterval(autoplayInterval);
264
- // Navigate to next video with autoplay
265
- window.location.href = nextVideoUrl;
266
- }
267
- }, 1000);
268
- }
269
-
270
- function updateCountdownDisplay() {
271
- if (countdownNumber) {
272
- countdownNumber.textContent = autoplayCountdown;
273
- }
274
-
275
- if (countdownProgress) {
276
- // Calculate progress (circle circumference is 2 * PI * r = 2 * PI * 45 ≈ 283)
277
- const circumference = 283;
278
- const progress = (AUTOPLAY_COUNTDOWN - autoplayCountdown) / AUTOPLAY_COUNTDOWN;
279
- const offset = circumference * progress;
280
- countdownProgress.style.strokeDashoffset = offset;
281
- }
282
- }
283
-
284
- function cancelAutoplayCountdown() {
285
- if (autoplayInterval) {
286
- clearInterval(autoplayInterval);
287
- autoplayInterval = null;
288
- }
289
-
290
- // Hide overlay
291
- if (autoplayOverlay) {
292
- autoplayOverlay.classList.add('hidden');
293
- }
294
-
295
- // Reset progress ring
296
- if (countdownProgress) {
297
- countdownProgress.style.strokeDashoffset = 0;
298
- }
299
- }
300
-
301
- // ==========================================
302
- // Notes Controls
303
- // ==========================================
304
-
305
- function setupNotesControls() {
306
- if (!saveNotesBtn || !notesEditor) return;
307
-
308
- saveNotesBtn.addEventListener('click', saveNotes);
309
-
310
- // Auto-save notes on blur
311
- notesEditor.addEventListener('blur', saveNotes);
312
-
313
- // Ctrl+S to save notes
314
- notesEditor.addEventListener('keydown', (e) => {
315
- if ((e.ctrlKey || e.metaKey) && e.key === 's') {
316
- e.preventDefault();
317
- saveNotes();
318
- }
319
- });
320
- }
321
-
322
- async function saveNotes() {
323
- const content = notesEditor.value;
324
-
325
- try {
326
- const response = await fetch(`/api/videos/${videoId}/notes`, {
327
- method: 'POST',
328
- headers: { 'Content-Type': 'application/json' },
329
- body: JSON.stringify({ content }),
330
- });
331
-
332
- if (response.ok) {
333
- showSaveStatus('✓ Saved');
334
- } else {
335
- showSaveStatus('✗ Error saving');
336
- }
337
- } catch (err) {
338
- console.error('Failed to save notes:', err);
339
- showSaveStatus('✗ Error saving');
340
- }
341
- }
342
-
343
- function showSaveStatus(message) {
344
- if (!notesSaveStatus) return;
345
-
346
- notesSaveStatus.textContent = message;
347
- setTimeout(() => {
348
- notesSaveStatus.textContent = '';
349
- }, 2000);
350
- }
351
-
352
- // Initialize when DOM is ready
353
- if (document.readyState === 'loading') {
354
- document.addEventListener('DOMContentLoaded', init);
355
- } else {
356
- init();
357
- }
358
- })();
359
-
package/src/cli.js DELETED
@@ -1,45 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * CourseWatcher CLI Entry Point
5
- *
6
- * Main executable for the coursewatcher command.
7
- * Parses arguments and starts the web server.
8
- *
9
- * @module cli
10
- */
11
-
12
- const { program } = require('commander');
13
- const path = require('path');
14
- const { version, description } = require('../package.json');
15
- const { startServer } = require('./server');
16
- const { log, success, error } = require('./utils/logger');
17
-
18
- program
19
- .name('coursewatcher')
20
- .description(description)
21
- .version(version)
22
- .argument('[path]', 'path to course directory', '.')
23
- .option('-p, --port <number>', 'server port')
24
- .option('--no-browser', 'do not open browser automatically')
25
- .action(async (coursePath, options) => {
26
- try {
27
- const absolutePath = path.resolve(coursePath);
28
- const isPortSpecified = !!options.port;
29
- const port = isPortSpecified ? parseInt(options.port, 10) : 3000;
30
-
31
- log(`Starting CourseWatcher in: ${absolutePath}`);
32
-
33
- await startServer({
34
- coursePath: absolutePath,
35
- port,
36
- allowFallback: !isPortSpecified,
37
- openBrowser: options.browser,
38
- });
39
- } catch (err) {
40
- error(`Failed to start CourseWatcher: ${err.message}`);
41
- process.exit(1);
42
- }
43
- });
44
-
45
- program.parse();
@@ -1,175 +0,0 @@
1
- /**
2
- * Video Controller
3
- *
4
- * Route handlers for video-related pages and API endpoints.
5
- *
6
- * @module controllers/video-controller
7
- */
8
-
9
- const path = require('path');
10
- const express = require('express');
11
- const { NotFoundError } = require('../utils/errors');
12
-
13
- /**
14
- * Create video routes
15
- * @param {Object} services - Service instances
16
- * @returns {express.Router} Express router
17
- */
18
- function createVideoRoutes(services) {
19
- const router = express.Router();
20
- const { videoService, progressService, notesService } = services;
21
-
22
- // ============================================
23
- // Page Routes
24
- // ============================================
25
-
26
- /**
27
- * Home page - List all modules and videos
28
- */
29
- router.get('/', (req, res) => {
30
- const sortBy = req.query.sort || 'name';
31
- const modules = videoService.getAllModulesWithVideos(sortBy);
32
- const stats = videoService.getStats();
33
-
34
- res.render('pages/index', {
35
- title: 'CourseWatcher',
36
- modules,
37
- stats,
38
- currentSort: sortBy,
39
- });
40
- });
41
-
42
- /**
43
- * Video player page
44
- */
45
- router.get('/video/:id', (req, res, next) => {
46
- try {
47
- const videoId = parseInt(req.params.id, 10);
48
- const video = videoService.getVideoById(videoId);
49
- const adjacent = videoService.getAdjacentVideos(videoId);
50
- const notes = notesService.getNotes(videoId);
51
- const queue = videoService.getQueueVideos(videoId);
52
-
53
- res.render('pages/player', {
54
- title: video.title,
55
- video,
56
- adjacent,
57
- notes,
58
- queue,
59
- });
60
- } catch (err) {
61
- next(err);
62
- }
63
- });
64
-
65
- /**
66
- * Search page
67
- */
68
- router.get('/search', (req, res) => {
69
- const query = req.query.q || '';
70
- const results = query ? videoService.searchVideos(query) : [];
71
-
72
- res.render('pages/search', {
73
- title: 'Search',
74
- query,
75
- results,
76
- });
77
- });
78
-
79
- // ============================================
80
- // API Routes
81
- // ============================================
82
-
83
- /**
84
- * Stream video file
85
- */
86
- router.get('/api/videos/:id/stream', (req, res, next) => {
87
- try {
88
- const videoId = parseInt(req.params.id, 10);
89
- const video = videoService.getVideoById(videoId);
90
-
91
- res.sendFile(video.path);
92
- } catch (err) {
93
- next(err);
94
- }
95
- });
96
-
97
- /**
98
- * Update video progress (position)
99
- */
100
- router.post('/api/videos/:id/progress', express.json(), (req, res, next) => {
101
- try {
102
- const videoId = parseInt(req.params.id, 10);
103
- const { position, duration } = req.body;
104
-
105
- const updated = progressService.updatePosition(videoId, position, duration);
106
- res.json({ success: true, video: updated });
107
- } catch (err) {
108
- next(err);
109
- }
110
- });
111
-
112
- /**
113
- * Update video status
114
- */
115
- router.post('/api/videos/:id/status', express.json(), (req, res, next) => {
116
- try {
117
- const videoId = parseInt(req.params.id, 10);
118
- const { status } = req.body;
119
-
120
- const updated = progressService.updateStatus(videoId, status);
121
- res.json({ success: true, video: updated });
122
- } catch (err) {
123
- next(err);
124
- }
125
- });
126
-
127
- /**
128
- * Get video notes
129
- */
130
- router.get('/api/videos/:id/notes', (req, res, next) => {
131
- try {
132
- const videoId = parseInt(req.params.id, 10);
133
- const notes = notesService.getNotes(videoId);
134
- res.json(notes);
135
- } catch (err) {
136
- next(err);
137
- }
138
- });
139
-
140
- /**
141
- * Save video notes
142
- */
143
- router.post('/api/videos/:id/notes', express.json(), (req, res, next) => {
144
- try {
145
- const videoId = parseInt(req.params.id, 10);
146
- const { content } = req.body;
147
-
148
- const notes = notesService.saveNotes(videoId, content);
149
- res.json({ success: true, notes });
150
- } catch (err) {
151
- next(err);
152
- }
153
- });
154
-
155
- /**
156
- * Search API endpoint
157
- */
158
- router.get('/api/search', (req, res) => {
159
- const query = req.query.q || '';
160
- const results = query ? videoService.searchVideos(query) : [];
161
- res.json(results);
162
- });
163
-
164
- /**
165
- * Get course stats
166
- */
167
- router.get('/api/stats', (req, res) => {
168
- const stats = videoService.getStats();
169
- res.json(stats);
170
- });
171
-
172
- return router;
173
- }
174
-
175
- module.exports = { createVideoRoutes };