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.
- package/README.md +26 -23
- package/dist/app/cli/main.js +36 -0
- package/dist/app/server/create-app.js +151 -0
- package/dist/app/server/start-server.js +72 -0
- package/dist/modules/catalog/catalog-mappers.js +49 -0
- package/dist/modules/catalog/catalog-repository.js +168 -0
- package/dist/modules/catalog/catalog-service.js +76 -0
- package/dist/modules/notes/notes-repository.js +25 -0
- package/dist/modules/notes/notes-service.js +28 -0
- package/dist/modules/playback/playback-repository.js +32 -0
- package/dist/modules/playback/playback-service.js +89 -0
- package/dist/platform/config/app-config.js +24 -0
- package/dist/platform/config/package-info.js +21 -0
- package/dist/platform/database/database-manager.js +101 -0
- package/dist/platform/errors/app-error.js +30 -0
- package/dist/platform/logging/logger.js +21 -0
- package/dist/shared/contracts/api.js +2 -0
- package/dist/web/assets/api-client-hFlLSS3K.js +1 -0
- package/dist/web/assets/catalog-route-pOIhR3yd.js +1 -0
- package/dist/web/assets/index-CwspbIw1.js +10 -0
- package/dist/web/assets/index-VjwsJnuQ.css +1 -0
- package/dist/web/assets/jsx-runtime-C2ZT__TU.js +4 -0
- package/dist/web/assets/playback-route-Bmy_Z7k7.js +2 -0
- package/dist/web/assets/search-route-CeGVOVPT.js +1 -0
- package/dist/web/index.html +14 -0
- package/package.json +75 -57
- package/public/css/styles.css +0 -1375
- package/public/js/player.js +0 -359
- package/src/cli.js +0 -45
- package/src/controllers/video-controller.js +0 -175
- package/src/models/database.js +0 -169
- package/src/server.js +0 -179
- package/src/services/notes-service.js +0 -97
- package/src/services/progress-service.js +0 -148
- package/src/services/video-service.js +0 -354
- package/src/utils/config.js +0 -56
- package/src/utils/errors.js +0 -57
- package/src/utils/logger.js +0 -48
- package/views/layouts/main.ejs +0 -13
- package/views/pages/error.ejs +0 -25
- package/views/pages/index.ejs +0 -101
- package/views/pages/player.ejs +0 -161
- package/views/pages/search.ejs +0 -63
- package/views/partials/footer.ejs +0 -3
- package/views/partials/head.ejs +0 -8
- package/views/partials/header.ejs +0 -14
- package/views/partials/video-card.ejs +0 -36
package/public/js/player.js
DELETED
|
@@ -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 };
|