coursewatcher 1.0.1 → 1.2.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 +9 -0
- package/package.json +6 -6
- package/public/css/styles.css +186 -81
- package/public/js/player.js +55 -137
- package/{bin/coursewatcher.js → src/cli.js} +4 -4
- package/src/server.js +1 -0
- package/views/pages/player.ejs +16 -32
package/README.md
CHANGED
|
@@ -20,6 +20,15 @@ npx coursewatcher
|
|
|
20
20
|
npx coursewatcher --port 8080 --no-browser
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
## CLI Options
|
|
24
|
+
|
|
25
|
+
| Argument | Description | Default |
|
|
26
|
+
|----------|-------------|---------|
|
|
27
|
+
| `[path]` | Path to course directory | `.` (Current directory) |
|
|
28
|
+
| `-p, --port` | Port to run the server on | `3000` |
|
|
29
|
+
| `--no-browser` | Prevent browser from opening automatically | `false` |
|
|
30
|
+
|
|
31
|
+
|
|
23
32
|
## Installation
|
|
24
33
|
|
|
25
34
|
```bash
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coursewatcher",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A CLI tool and web interface for tracking progress in downloaded video courses",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"coursewatcher": "./
|
|
7
|
+
"coursewatcher": "./src/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"start": "node
|
|
11
|
-
"dev": "node
|
|
10
|
+
"start": "node src/cli.js",
|
|
11
|
+
"dev": "node src/cli.js",
|
|
12
12
|
"test": "jest",
|
|
13
13
|
"test:coverage": "jest --coverage",
|
|
14
14
|
"test:watch": "jest --watch"
|
|
@@ -34,7 +34,6 @@
|
|
|
34
34
|
"url": "https://github.com/serhiishtokal/CourseWatcher/issues"
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
|
-
"bin/",
|
|
38
37
|
"src/",
|
|
39
38
|
"views/",
|
|
40
39
|
"public/"
|
|
@@ -45,7 +44,8 @@
|
|
|
45
44
|
"commander": "^12.1.0",
|
|
46
45
|
"ejs": "^3.1.10",
|
|
47
46
|
"express": "^4.21.1",
|
|
48
|
-
"open": "^8.4.2"
|
|
47
|
+
"open": "^8.4.2",
|
|
48
|
+
"plyr": "^3.8.4"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"jest": "^29.7.0",
|
package/public/css/styles.css
CHANGED
|
@@ -614,91 +614,23 @@ a:hover {
|
|
|
614
614
|
color: white;
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
-
.playback-controls {
|
|
618
|
-
display: flex;
|
|
619
|
-
gap: var(--space-xl);
|
|
620
|
-
flex-wrap: wrap;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
.control-group {
|
|
624
|
-
display: flex;
|
|
625
|
-
align-items: center;
|
|
626
|
-
gap: var(--space-sm);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
.control-label {
|
|
630
|
-
color: var(--color-text-muted);
|
|
631
|
-
font-size: 0.85rem;
|
|
632
|
-
margin-right: var(--space-sm);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
.control-btn {
|
|
636
|
-
padding: var(--space-xs) var(--space-md);
|
|
637
|
-
background: var(--color-bg-secondary);
|
|
638
|
-
border: 1px solid var(--color-border);
|
|
639
|
-
border-radius: var(--radius-md);
|
|
640
|
-
color: var(--color-text);
|
|
641
|
-
cursor: pointer;
|
|
642
|
-
font-size: 0.8rem;
|
|
643
|
-
transition: all var(--transition-fast);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
.control-btn:hover {
|
|
647
|
-
background: var(--color-primary);
|
|
648
|
-
border-color: var(--color-primary);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
.control-value {
|
|
652
|
-
min-width: 50px;
|
|
653
|
-
text-align: center;
|
|
654
|
-
font-weight: 600;
|
|
655
|
-
color: var(--color-primary);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
617
|
/* ==========================================
|
|
659
|
-
|
|
618
|
+
Plyr Customization
|
|
660
619
|
========================================== */
|
|
661
620
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
cursor: pointer;
|
|
670
|
-
color: var(--color-text-secondary);
|
|
671
|
-
font-size: 0.9rem;
|
|
672
|
-
user-select: none;
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
.shortcuts-grid {
|
|
676
|
-
display: grid;
|
|
677
|
-
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
678
|
-
gap: var(--space-sm);
|
|
679
|
-
margin-top: var(--space-md);
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
.shortcut {
|
|
683
|
-
display: flex;
|
|
684
|
-
align-items: center;
|
|
685
|
-
gap: var(--space-sm);
|
|
686
|
-
font-size: 0.85rem;
|
|
687
|
-
color: var(--color-text-secondary);
|
|
621
|
+
:root {
|
|
622
|
+
--plyr-color-main: var(--color-primary);
|
|
623
|
+
--plyr-video-background: #000;
|
|
624
|
+
--plyr-menu-background: var(--color-surface);
|
|
625
|
+
--plyr-menu-color: var(--color-text);
|
|
626
|
+
--plyr-menu-shadow: 0 4px 15px rgba(0, 0, 0, 0.5);
|
|
627
|
+
--plyr-font-family: var(--font-sans);
|
|
688
628
|
}
|
|
689
629
|
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
min-width: 28px;
|
|
695
|
-
padding: var(--space-xs) var(--space-sm);
|
|
696
|
-
background: var(--color-bg-secondary);
|
|
697
|
-
border: 1px solid var(--color-border);
|
|
698
|
-
border-radius: var(--radius-sm);
|
|
699
|
-
font-family: var(--font-mono);
|
|
700
|
-
font-size: 0.75rem;
|
|
701
|
-
color: var(--color-text);
|
|
630
|
+
.plyr--video {
|
|
631
|
+
border-radius: var(--radius-lg);
|
|
632
|
+
overflow: hidden;
|
|
633
|
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
|
702
634
|
}
|
|
703
635
|
|
|
704
636
|
/* ==========================================
|
|
@@ -932,4 +864,177 @@ kbd {
|
|
|
932
864
|
justify-content: center;
|
|
933
865
|
min-width: 120px;
|
|
934
866
|
}
|
|
935
|
-
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/* ==========================================
|
|
870
|
+
Video Player Overlay Controls
|
|
871
|
+
========================================== */
|
|
872
|
+
|
|
873
|
+
.video-wrapper {
|
|
874
|
+
position: relative;
|
|
875
|
+
overflow: hidden;
|
|
876
|
+
/* Ensure controls don't spill out */
|
|
877
|
+
group: video-group;
|
|
878
|
+
/* For potential future container queries */
|
|
879
|
+
background: black;
|
|
880
|
+
/* Prevent white flashes */
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.video-wrapper:hover .overlay-controls,
|
|
884
|
+
.video-wrapper:focus-within .overlay-controls,
|
|
885
|
+
.video-player.paused+.overlay-controls,
|
|
886
|
+
.overlay-controls:hover {
|
|
887
|
+
opacity: 1;
|
|
888
|
+
pointer-events: auto;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
.overlay-controls {
|
|
892
|
+
position: absolute;
|
|
893
|
+
bottom: 0;
|
|
894
|
+
left: 0;
|
|
895
|
+
right: 0;
|
|
896
|
+
background: linear-gradient(to top, rgba(0, 0, 0, 0.9) 0%, rgba(0, 0, 0, 0.6) 60%, transparent 100%);
|
|
897
|
+
padding: var(--space-md);
|
|
898
|
+
opacity: 0;
|
|
899
|
+
transition: opacity var(--transition-normal);
|
|
900
|
+
pointer-events: none;
|
|
901
|
+
/* Let clicks pass through when hidden */
|
|
902
|
+
display: flex;
|
|
903
|
+
flex-direction: column;
|
|
904
|
+
gap: var(--space-sm);
|
|
905
|
+
z-index: 10;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
.controls-row {
|
|
909
|
+
display: flex;
|
|
910
|
+
align-items: center;
|
|
911
|
+
justify-content: space-between;
|
|
912
|
+
gap: var(--space-md);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
.control-group {
|
|
916
|
+
display: flex;
|
|
917
|
+
align-items: center;
|
|
918
|
+
gap: var(--space-md);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.control-group.right {
|
|
922
|
+
justify-content: flex-end;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/* Icon Buttons */
|
|
926
|
+
.control-btn-icon {
|
|
927
|
+
background: transparent;
|
|
928
|
+
border: none;
|
|
929
|
+
color: white;
|
|
930
|
+
font-size: 1.2rem;
|
|
931
|
+
cursor: pointer;
|
|
932
|
+
padding: var(--space-xs);
|
|
933
|
+
border-radius: var(--radius-sm);
|
|
934
|
+
transition: all var(--transition-fast);
|
|
935
|
+
display: flex;
|
|
936
|
+
align-items: center;
|
|
937
|
+
justify-content: center;
|
|
938
|
+
width: 32px;
|
|
939
|
+
height: 32px;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
.control-btn-icon:hover {
|
|
943
|
+
background: rgba(255, 255, 255, 0.2);
|
|
944
|
+
transform: scale(1.1);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/* Text Buttons (Speed) */
|
|
948
|
+
.control-btn-text {
|
|
949
|
+
background: rgba(255, 255, 255, 0.1);
|
|
950
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
951
|
+
color: white;
|
|
952
|
+
font-size: 1rem;
|
|
953
|
+
cursor: pointer;
|
|
954
|
+
padding: 0 var(--space-sm);
|
|
955
|
+
border-radius: var(--radius-sm);
|
|
956
|
+
height: 24px;
|
|
957
|
+
transition: all var(--transition-fast);
|
|
958
|
+
display: flex;
|
|
959
|
+
align-items: center;
|
|
960
|
+
justify-content: center;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
.control-btn-text:hover {
|
|
964
|
+
background: var(--color-primary);
|
|
965
|
+
border-color: var(--color-primary);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
/* Speed Control */
|
|
969
|
+
.speed-control {
|
|
970
|
+
display: flex;
|
|
971
|
+
align-items: center;
|
|
972
|
+
gap: var(--space-sm);
|
|
973
|
+
background: rgba(0, 0, 0, 0.5);
|
|
974
|
+
padding: var(--space-xs) var(--space-sm);
|
|
975
|
+
border-radius: var(--radius-md);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
.speed-value {
|
|
979
|
+
color: white;
|
|
980
|
+
font-weight: 600;
|
|
981
|
+
font-size: 0.9rem;
|
|
982
|
+
min-width: 40px;
|
|
983
|
+
text-align: center;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/* Time Display */
|
|
987
|
+
.time-display {
|
|
988
|
+
color: white;
|
|
989
|
+
font-size: 0.9rem;
|
|
990
|
+
font-feature-settings: "tnum";
|
|
991
|
+
font-variant-numeric: tabular-nums;
|
|
992
|
+
margin-left: var(--space-sm);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
/* Seek Bar */
|
|
996
|
+
.seek-bar-container {
|
|
997
|
+
position: relative;
|
|
998
|
+
height: 6px;
|
|
999
|
+
width: 100%;
|
|
1000
|
+
cursor: pointer;
|
|
1001
|
+
display: flex;
|
|
1002
|
+
align-items: center;
|
|
1003
|
+
margin-bottom: var(--space-xs);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
.seek-bar-container:hover .seek-bar-bg {
|
|
1007
|
+
height: 8px;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
.seek-bar-bg {
|
|
1011
|
+
background: rgba(255, 255, 255, 0.3);
|
|
1012
|
+
height: 4px;
|
|
1013
|
+
width: 100%;
|
|
1014
|
+
border-radius: 2px;
|
|
1015
|
+
transition: height var(--transition-fast);
|
|
1016
|
+
position: absolute;
|
|
1017
|
+
top: 50%;
|
|
1018
|
+
transform: translateY(-50%);
|
|
1019
|
+
pointer-events: none;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.seek-bar-fill {
|
|
1023
|
+
background: var(--color-primary);
|
|
1024
|
+
height: 100%;
|
|
1025
|
+
width: 0%;
|
|
1026
|
+
border-radius: 2px;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
.seek-slider {
|
|
1030
|
+
position: absolute;
|
|
1031
|
+
width: 100%;
|
|
1032
|
+
height: 100%;
|
|
1033
|
+
opacity: 0;
|
|
1034
|
+
cursor: pointer;
|
|
1035
|
+
margin: 0;
|
|
1036
|
+
z-index: 2;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/* Hide default controls on fullscreen if needed (we rely on custom ones now) */
|
|
1040
|
+
/* .video-player::-webkit-media-controls { display:none !important; } */
|
package/public/js/player.js
CHANGED
|
@@ -1,38 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Video Player Controls
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
4
|
+
* Handles Plyr initialization, keyboard shortcuts, and progress auto-save.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
(function () {
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
// Constants
|
|
11
|
-
const SPEED_STEP = 0.25;
|
|
12
|
-
const MIN_SPEED = 0.25;
|
|
13
|
-
const MAX_SPEED = 4.0;
|
|
14
|
-
const SEEK_SHORT = 5;
|
|
15
|
-
const SEEK_MEDIUM = 10;
|
|
16
|
-
const SEEK_LONG = 30;
|
|
17
11
|
const SAVE_INTERVAL = 5000; // Save progress every 5 seconds
|
|
18
12
|
|
|
19
13
|
// Elements
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const speedUp = document.getElementById('speedUp');
|
|
24
|
-
const seekBack5 = document.getElementById('seekBack5');
|
|
25
|
-
const seekForward5 = document.getElementById('seekForward5');
|
|
26
|
-
const seekForward10 = document.getElementById('seekForward10');
|
|
27
|
-
const statusButtons = document.querySelectorAll('.status-btn');
|
|
14
|
+
const videoElement = document.getElementById('videoPlayer');
|
|
15
|
+
|
|
16
|
+
// Notes & Status
|
|
28
17
|
const notesEditor = document.getElementById('notesEditor');
|
|
29
18
|
const saveNotesBtn = document.getElementById('saveNotes');
|
|
30
19
|
const notesSaveStatus = document.getElementById('notesSaveStatus');
|
|
20
|
+
const statusButtons = document.querySelectorAll('.status-btn');
|
|
31
21
|
|
|
32
|
-
if (!
|
|
22
|
+
if (!videoElement) return;
|
|
33
23
|
|
|
34
|
-
const videoId =
|
|
35
|
-
const savedPosition = parseFloat(
|
|
24
|
+
const videoId = videoElement.dataset.videoId;
|
|
25
|
+
const savedPosition = parseFloat(videoElement.dataset.savedPosition) || 0;
|
|
36
26
|
|
|
37
27
|
let saveTimeout = null;
|
|
38
28
|
let lastSavedPosition = savedPosition;
|
|
@@ -41,54 +31,45 @@
|
|
|
41
31
|
// Initialization
|
|
42
32
|
// ==========================================
|
|
43
33
|
|
|
44
|
-
/**
|
|
45
|
-
* Initialize player
|
|
46
|
-
*/
|
|
47
34
|
function init() {
|
|
35
|
+
// Initialize Plyr
|
|
36
|
+
const player = new Plyr('#videoPlayer', {
|
|
37
|
+
keyboard: { focused: true, global: true },
|
|
38
|
+
seekTime: 5,
|
|
39
|
+
controls: [
|
|
40
|
+
'play-large', // The large play button in the center
|
|
41
|
+
'restart', // Restart playback
|
|
42
|
+
'rewind', // Rewind by the seek time (default 10 seconds)
|
|
43
|
+
'play', // Play/pause playback
|
|
44
|
+
'fast-forward', // Fast forward by the seek time (default 10 seconds)
|
|
45
|
+
'progress', // The progress bar and scrubber for playback and buffering
|
|
46
|
+
'current-time', // The current time of playback
|
|
47
|
+
'duration', // The full duration of the media
|
|
48
|
+
'mute', // Toggle mute
|
|
49
|
+
'volume', // Volume control
|
|
50
|
+
'captions', // Toggle captions
|
|
51
|
+
'settings', // Settings menu
|
|
52
|
+
'pip', // Picture-in-picture (currently Safari only)
|
|
53
|
+
'airplay', // Airplay (currently Safari only)
|
|
54
|
+
'fullscreen', // Toggle fullscreen
|
|
55
|
+
]
|
|
56
|
+
});
|
|
57
|
+
|
|
48
58
|
// Restore saved position
|
|
49
|
-
|
|
50
|
-
if (savedPosition > 0
|
|
51
|
-
|
|
59
|
+
player.on('ready', () => {
|
|
60
|
+
if (savedPosition > 0) {
|
|
61
|
+
player.currentTime = savedPosition;
|
|
52
62
|
}
|
|
53
|
-
updateSpeedDisplay();
|
|
54
63
|
});
|
|
55
64
|
|
|
56
|
-
//
|
|
57
|
-
setupPlaybackControls();
|
|
65
|
+
// Setup Logic
|
|
58
66
|
setupStatusControls();
|
|
59
|
-
setupKeyboardShortcuts();
|
|
60
67
|
setupNotesControls();
|
|
61
|
-
setupProgressAutoSave();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ==========================================
|
|
65
|
-
// Playback Controls
|
|
66
|
-
// ==========================================
|
|
67
|
-
|
|
68
|
-
function setupPlaybackControls() {
|
|
69
|
-
speedDown?.addEventListener('click', () => changeSpeed(-SPEED_STEP));
|
|
70
|
-
speedUp?.addEventListener('click', () => changeSpeed(SPEED_STEP));
|
|
71
|
-
seekBack5?.addEventListener('click', () => seek(-SEEK_SHORT));
|
|
72
|
-
seekForward5?.addEventListener('click', () => seek(SEEK_SHORT));
|
|
73
|
-
seekForward10?.addEventListener('click', () => seek(SEEK_MEDIUM));
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function changeSpeed(delta) {
|
|
77
|
-
let newSpeed = video.playbackRate + delta;
|
|
78
|
-
newSpeed = Math.max(MIN_SPEED, Math.min(MAX_SPEED, newSpeed));
|
|
79
|
-
newSpeed = Math.round(newSpeed * 100) / 100; // Fix floating point
|
|
80
|
-
video.playbackRate = newSpeed;
|
|
81
|
-
updateSpeedDisplay();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function updateSpeedDisplay() {
|
|
85
|
-
if (speedDisplay) {
|
|
86
|
-
speedDisplay.textContent = video.playbackRate.toFixed(2) + 'x';
|
|
87
|
-
}
|
|
88
|
-
}
|
|
68
|
+
setupProgressAutoSave(player);
|
|
89
69
|
|
|
90
|
-
|
|
91
|
-
|
|
70
|
+
// Custom shortcuts not covered by Plyr (if any)
|
|
71
|
+
// Plyr covers Space, K, F, M, Arrow keys.
|
|
72
|
+
// We can add custom ones if needed, but standard ones are usually enough.
|
|
92
73
|
}
|
|
93
74
|
|
|
94
75
|
// ==========================================
|
|
@@ -123,111 +104,48 @@
|
|
|
123
104
|
}
|
|
124
105
|
}
|
|
125
106
|
|
|
126
|
-
// ==========================================
|
|
127
|
-
// Keyboard Shortcuts
|
|
128
|
-
// ==========================================
|
|
129
|
-
|
|
130
|
-
function setupKeyboardShortcuts() {
|
|
131
|
-
document.addEventListener('keydown', (e) => {
|
|
132
|
-
// Ignore if typing in notes
|
|
133
|
-
if (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT') {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
switch (e.key.toLowerCase()) {
|
|
138
|
-
case ' ':
|
|
139
|
-
e.preventDefault();
|
|
140
|
-
video.paused ? video.play() : video.pause();
|
|
141
|
-
break;
|
|
142
|
-
case 'w':
|
|
143
|
-
changeSpeed(-SPEED_STEP);
|
|
144
|
-
break;
|
|
145
|
-
case 'e':
|
|
146
|
-
changeSpeed(SPEED_STEP);
|
|
147
|
-
break;
|
|
148
|
-
case 'j':
|
|
149
|
-
seek(e.shiftKey ? -SEEK_LONG : -SEEK_SHORT);
|
|
150
|
-
break;
|
|
151
|
-
case 'k':
|
|
152
|
-
seek(SEEK_SHORT);
|
|
153
|
-
break;
|
|
154
|
-
case 'l':
|
|
155
|
-
seek(e.shiftKey ? SEEK_LONG : SEEK_MEDIUM);
|
|
156
|
-
break;
|
|
157
|
-
case 'arrowleft':
|
|
158
|
-
seek(-SEEK_SHORT);
|
|
159
|
-
break;
|
|
160
|
-
case 'arrowright':
|
|
161
|
-
seek(SEEK_SHORT);
|
|
162
|
-
break;
|
|
163
|
-
case 'arrowup':
|
|
164
|
-
e.preventDefault();
|
|
165
|
-
video.volume = Math.min(1, video.volume + 0.1);
|
|
166
|
-
break;
|
|
167
|
-
case 'arrowdown':
|
|
168
|
-
e.preventDefault();
|
|
169
|
-
video.volume = Math.max(0, video.volume - 0.1);
|
|
170
|
-
break;
|
|
171
|
-
case 'm':
|
|
172
|
-
video.muted = !video.muted;
|
|
173
|
-
break;
|
|
174
|
-
case 'f':
|
|
175
|
-
toggleFullscreen();
|
|
176
|
-
break;
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function toggleFullscreen() {
|
|
182
|
-
if (document.fullscreenElement) {
|
|
183
|
-
document.exitFullscreen();
|
|
184
|
-
} else {
|
|
185
|
-
video.requestFullscreen?.() || video.webkitRequestFullscreen?.();
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
107
|
// ==========================================
|
|
190
108
|
// Progress Auto-Save
|
|
191
109
|
// ==========================================
|
|
192
110
|
|
|
193
|
-
function setupProgressAutoSave() {
|
|
111
|
+
function setupProgressAutoSave(player) {
|
|
194
112
|
// Save on pause
|
|
195
|
-
|
|
113
|
+
player.on('pause', () => saveProgress(player));
|
|
196
114
|
|
|
197
115
|
// Save on video end
|
|
198
|
-
|
|
199
|
-
saveProgress();
|
|
116
|
+
player.on('ended', () => {
|
|
117
|
+
saveProgress(player);
|
|
200
118
|
updateStatus('completed');
|
|
201
119
|
});
|
|
202
120
|
|
|
203
121
|
// Periodic save during playback
|
|
204
|
-
|
|
205
|
-
const currentPos = Math.floor(
|
|
122
|
+
player.on('timeupdate', () => {
|
|
123
|
+
const currentPos = Math.floor(player.currentTime);
|
|
206
124
|
|
|
207
125
|
// Only save every 5 seconds of playback
|
|
208
126
|
if (Math.abs(currentPos - lastSavedPosition) >= 5) {
|
|
209
|
-
scheduleProgressSave();
|
|
127
|
+
scheduleProgressSave(player);
|
|
210
128
|
}
|
|
211
129
|
});
|
|
212
130
|
|
|
213
131
|
// Save before page unload
|
|
214
132
|
window.addEventListener('beforeunload', () => {
|
|
215
|
-
saveProgressSync();
|
|
133
|
+
saveProgressSync(player);
|
|
216
134
|
});
|
|
217
135
|
}
|
|
218
136
|
|
|
219
|
-
function scheduleProgressSave() {
|
|
137
|
+
function scheduleProgressSave(player) {
|
|
220
138
|
if (saveTimeout) return;
|
|
221
139
|
|
|
222
140
|
saveTimeout = setTimeout(() => {
|
|
223
|
-
saveProgress();
|
|
141
|
+
saveProgress(player);
|
|
224
142
|
saveTimeout = null;
|
|
225
143
|
}, 1000);
|
|
226
144
|
}
|
|
227
145
|
|
|
228
|
-
async function saveProgress() {
|
|
229
|
-
const position =
|
|
230
|
-
const duration =
|
|
146
|
+
async function saveProgress(player) {
|
|
147
|
+
const position = player.currentTime;
|
|
148
|
+
const duration = player.duration;
|
|
231
149
|
|
|
232
150
|
if (isNaN(position) || isNaN(duration)) return;
|
|
233
151
|
|
|
@@ -243,9 +161,9 @@
|
|
|
243
161
|
}
|
|
244
162
|
}
|
|
245
163
|
|
|
246
|
-
function saveProgressSync() {
|
|
247
|
-
const position =
|
|
248
|
-
const duration =
|
|
164
|
+
function saveProgressSync(player) {
|
|
165
|
+
const position = player.currentTime;
|
|
166
|
+
const duration = player.duration;
|
|
249
167
|
|
|
250
168
|
if (isNaN(position) || isNaN(duration)) return;
|
|
251
169
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* CourseWatcher CLI Entry Point
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* Main executable for the coursewatcher command.
|
|
7
7
|
* Parses arguments and starts the web server.
|
|
8
8
|
*
|
|
9
|
-
* @module
|
|
9
|
+
* @module cli
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const { program } = require('commander');
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { version, description } = require('../package.json');
|
|
15
|
-
const { startServer } = require('
|
|
16
|
-
const { log, success, error } = require('
|
|
15
|
+
const { startServer } = require('./server');
|
|
16
|
+
const { log, success, error } = require('./utils/logger');
|
|
17
17
|
|
|
18
18
|
program
|
|
19
19
|
.name('coursewatcher')
|
package/src/server.js
CHANGED
|
@@ -53,6 +53,7 @@ async function startServer(options) {
|
|
|
53
53
|
|
|
54
54
|
// Static files
|
|
55
55
|
app.use('/static', express.static(path.join(__dirname, '..', 'public')));
|
|
56
|
+
app.use('/lib/plyr', express.static(path.join(__dirname, '..', 'node_modules', 'plyr', 'dist')));
|
|
56
57
|
|
|
57
58
|
// Routes
|
|
58
59
|
app.use('/', createVideoRoutes({
|
package/views/pages/player.ejs
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
<%- include('../partials/header') %>
|
|
7
7
|
|
|
8
8
|
<main class="container player-container">
|
|
9
|
+
<!-- Video Player Section -->
|
|
9
10
|
<!-- Video Player Section -->
|
|
10
11
|
<section class="player-section">
|
|
12
|
+
<h1 class="video-title">
|
|
13
|
+
<%= video.title %>
|
|
14
|
+
</h1>
|
|
11
15
|
<div class="video-wrapper">
|
|
12
|
-
<video id="videoPlayer" class="
|
|
13
|
-
data-
|
|
16
|
+
<video id="videoPlayer" class="plyr" playsinline controls data-video-id="<%= video.id %>"
|
|
17
|
+
data-saved-position="<%= video.position %>">
|
|
14
18
|
<source src="/api/videos/<%= video.id %>/stream" type="video/mp4">
|
|
15
19
|
Your browser does not support the video tag.
|
|
16
20
|
</video>
|
|
@@ -40,9 +44,6 @@
|
|
|
40
44
|
|
|
41
45
|
<!-- Video Info Section -->
|
|
42
46
|
<section class="video-info-section">
|
|
43
|
-
<h1 class="video-title">
|
|
44
|
-
<%= video.title %>
|
|
45
|
-
</h1>
|
|
46
47
|
|
|
47
48
|
<!-- Status Controls -->
|
|
48
49
|
<div class="status-controls">
|
|
@@ -63,37 +64,18 @@
|
|
|
63
64
|
</div>
|
|
64
65
|
</div>
|
|
65
66
|
|
|
66
|
-
<!-- Playback Controls -->
|
|
67
|
-
<div class="playback-controls">
|
|
68
|
-
<div class="control-group">
|
|
69
|
-
<span class="control-label">Speed</span>
|
|
70
|
-
<button class="control-btn" id="speedDown" title="Decrease speed (W)">W ◀</button>
|
|
71
|
-
<span class="control-value" id="speedDisplay">1.0x</span>
|
|
72
|
-
<button class="control-btn" id="speedUp" title="Increase speed (E)">▶ E</button>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div class="control-group">
|
|
76
|
-
<span class="control-label">Seek</span>
|
|
77
|
-
<button class="control-btn" id="seekBack5" title="Back 5s (J)">◀◀ 5s</button>
|
|
78
|
-
<button class="control-btn" id="seekForward5" title="Forward 5s (K)">5s ▶▶</button>
|
|
79
|
-
<button class="control-btn" id="seekForward10" title="Forward 10s (L)">10s ▶▶</button>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
67
|
<!-- Keyboard Shortcuts Help -->
|
|
84
68
|
<details class="shortcuts-help">
|
|
85
69
|
<summary>⌨️ Keyboard Shortcuts</summary>
|
|
86
70
|
<div class="shortcuts-grid">
|
|
87
|
-
<div class="shortcut"><kbd>Space</kbd> Play/Pause</div>
|
|
88
|
-
<div class="shortcut"><kbd>
|
|
89
|
-
<div class="shortcut"><kbd>
|
|
90
|
-
<div class="shortcut"><kbd
|
|
91
|
-
<div class="shortcut"><kbd
|
|
92
|
-
<div class="shortcut"><kbd
|
|
93
|
-
<div class="shortcut"><kbd
|
|
94
|
-
<div class="shortcut"><kbd>
|
|
95
|
-
<div class="shortcut"><kbd>←</kbd> Back 5s</div>
|
|
96
|
-
<div class="shortcut"><kbd>→</kbd> Forward 5s</div>
|
|
71
|
+
<div class="shortcut"><kbd>Space</kbd> / <kbd>K</kbd> Play/Pause</div>
|
|
72
|
+
<div class="shortcut"><kbd>M</kbd> Mute/Unmute</div>
|
|
73
|
+
<div class="shortcut"><kbd>F</kbd> Fullscreen</div>
|
|
74
|
+
<div class="shortcut"><kbd>←</kbd> Back 10s</div>
|
|
75
|
+
<div class="shortcut"><kbd>→</kbd> Forward 10s</div>
|
|
76
|
+
<div class="shortcut"><kbd>↑</kbd> Volume Up</div>
|
|
77
|
+
<div class="shortcut"><kbd>↓</kbd> Volume Down</div>
|
|
78
|
+
<div class="shortcut"><kbd>C</kbd> Captions</div>
|
|
97
79
|
</div>
|
|
98
80
|
</details>
|
|
99
81
|
</section>
|
|
@@ -112,6 +94,8 @@
|
|
|
112
94
|
|
|
113
95
|
<%- include('../partials/footer') %>
|
|
114
96
|
|
|
97
|
+
<link rel="stylesheet" href="/lib/plyr/plyr.css" />
|
|
98
|
+
<script src="/lib/plyr/plyr.js"></script>
|
|
115
99
|
<script src="/static/js/player.js"></script>
|
|
116
100
|
</body>
|
|
117
101
|
|