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 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.1",
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": "./bin/coursewatcher.js"
7
+ "coursewatcher": "./src/cli.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "node bin/coursewatcher.js",
11
- "dev": "node bin/coursewatcher.js",
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",
@@ -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
- Keyboard Shortcuts
618
+ Plyr Customization
660
619
  ========================================== */
661
620
 
662
- .shortcuts-help {
663
- margin-top: var(--space-lg);
664
- padding-top: var(--space-lg);
665
- border-top: 1px solid var(--color-border);
666
- }
667
-
668
- .shortcuts-help summary {
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
- kbd {
691
- display: inline-flex;
692
- align-items: center;
693
- justify-content: center;
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; } */
@@ -1,38 +1,28 @@
1
1
  /**
2
2
  * Video Player Controls
3
3
  *
4
- * Handles keyboard shortcuts, playback controls, and progress auto-save.
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 video = document.getElementById('videoPlayer');
21
- const speedDisplay = document.getElementById('speedDisplay');
22
- const speedDown = document.getElementById('speedDown');
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 (!video) return;
22
+ if (!videoElement) return;
33
23
 
34
- const videoId = video.dataset.videoId;
35
- const savedPosition = parseFloat(video.dataset.savedPosition) || 0;
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
- video.addEventListener('loadedmetadata', () => {
50
- if (savedPosition > 0 && savedPosition < video.duration) {
51
- video.currentTime = savedPosition;
59
+ player.on('ready', () => {
60
+ if (savedPosition > 0) {
61
+ player.currentTime = savedPosition;
52
62
  }
53
- updateSpeedDisplay();
54
63
  });
55
64
 
56
- // Set up event listeners
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
- function seek(seconds) {
91
- video.currentTime = Math.max(0, Math.min(video.duration, video.currentTime + seconds));
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
- video.addEventListener('pause', saveProgress);
113
+ player.on('pause', () => saveProgress(player));
196
114
 
197
115
  // Save on video end
198
- video.addEventListener('ended', () => {
199
- saveProgress();
116
+ player.on('ended', () => {
117
+ saveProgress(player);
200
118
  updateStatus('completed');
201
119
  });
202
120
 
203
121
  // Periodic save during playback
204
- video.addEventListener('timeupdate', () => {
205
- const currentPos = Math.floor(video.currentTime);
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 = video.currentTime;
230
- const duration = video.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 = video.currentTime;
248
- const duration = video.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 bin/coursewatcher
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('../src/server');
16
- const { log, success, error } = require('../src/utils/logger');
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({
@@ -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="video-player" controls preload="metadata"
13
- data-video-id="<%= video.id %>" data-saved-position="<%= video.position %>">
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>W</kbd> Slower</div>
89
- <div class="shortcut"><kbd>E</kbd> Faster</div>
90
- <div class="shortcut"><kbd>J</kbd> Back 5s</div>
91
- <div class="shortcut"><kbd>K</kbd> Forward 5s</div>
92
- <div class="shortcut"><kbd>L</kbd> Forward 10s</div>
93
- <div class="shortcut"><kbd>Shift+J</kbd> Back 30s</div>
94
- <div class="shortcut"><kbd>Shift+L</kbd> Forward 30s</div>
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