coursewatcher 1.3.0 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coursewatcher",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
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": {
@@ -27,17 +27,17 @@
27
27
  const countdownProgress = document.getElementById('countdownProgress');
28
28
  const nextVideoTitle = document.getElementById('nextVideoTitle');
29
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;
30
+
31
+ if (!videoElement) return;
32
+
33
+ const videoId = videoElement.dataset.videoId;
34
+ const startPosition = parseFloat(videoElement.dataset.startPosition) || 0;
35
+ const nextVideoUrl = videoElement.dataset.nextVideoUrl;
36
+
37
+ let saveTimeout = null;
38
+ let lastSavedPosition = startPosition;
39
+ let autoplayInterval = null;
40
+ let autoplayCountdown = AUTOPLAY_COUNTDOWN;
41
41
 
42
42
  // ==========================================
43
43
  // Initialization
@@ -67,14 +67,14 @@
67
67
  ]
68
68
  });
69
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(() => {
70
+ // Restore saved position and autoplay
71
+ player.on('ready', () => {
72
+ if (startPosition > 0) {
73
+ player.currentTime = startPosition;
74
+ }
75
+
76
+ // Always try to autoplay
77
+ player.play().catch(() => {
78
78
  // Autoplay might be blocked by browser on first visit, ignore
79
79
  console.log('Autoplay was prevented by browser');
80
80
  });
@@ -6,9 +6,21 @@
6
6
  * @module controllers/video-controller
7
7
  */
8
8
 
9
- const path = require('path');
10
- const express = require('express');
11
- const { NotFoundError } = require('../utils/errors');
9
+ const path = require('path');
10
+ const express = require('express');
11
+ const { config } = require('../utils/config');
12
+ const { NotFoundError } = require('../utils/errors');
13
+
14
+ function getPlaybackStartPosition(video) {
15
+ const isShortVideo = video.duration > 0
16
+ && video.duration < config.shortVideoResumeCutoffSeconds;
17
+
18
+ if (video.status === 'completed' || isShortVideo) {
19
+ return 0;
20
+ }
21
+
22
+ return video.status === 'in-progress' ? video.position : 0;
23
+ }
12
24
 
13
25
  /**
14
26
  * Create video routes
@@ -46,20 +58,22 @@ function createVideoRoutes(services) {
46
58
  try {
47
59
  const videoId = parseInt(req.params.id, 10);
48
60
  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
- }
61
+ const adjacent = videoService.getAdjacentVideos(videoId);
62
+ const notes = notesService.getNotes(videoId);
63
+ const queue = videoService.getQueueVideos(videoId);
64
+ const startPosition = getPlaybackStartPosition(video);
65
+
66
+ res.render('pages/player', {
67
+ title: video.title,
68
+ video,
69
+ adjacent,
70
+ notes,
71
+ queue,
72
+ startPosition,
73
+ });
74
+ } catch (err) {
75
+ next(err);
76
+ }
63
77
  });
64
78
 
65
79
  /**
@@ -28,47 +28,68 @@ class ProgressService {
28
28
  * @param {number} [duration] - Video duration in seconds
29
29
  * @returns {Object} Updated video
30
30
  */
31
- updatePosition(videoId, position, duration = null) {
32
- // Validate inputs
33
- if (typeof position !== 'number' || position < 0) {
34
- throw new ValidationError('Position must be a non-negative number');
35
- }
31
+ updatePosition(videoId, position, duration = null) {
32
+ // Validate inputs
33
+ if (typeof position !== 'number' || position < 0) {
34
+ throw new ValidationError('Position must be a non-negative number');
35
+ }
36
36
 
37
37
  // Get current video
38
38
  const video = this._db.get('SELECT * FROM videos WHERE id = ?', [videoId]);
39
39
  if (!video) {
40
40
  throw new NotFoundError(`Video with id ${videoId}`);
41
41
  }
42
-
43
- // Determine new status based on position
44
- let newStatus = video.status;
45
- const videoDuration = duration || video.duration;
46
-
47
- if (videoDuration > 0) {
48
- const watchPercent = position / videoDuration;
49
-
50
- if (watchPercent >= config.completionThreshold) {
51
- newStatus = 'completed';
52
- } else if (position > 0) {
53
- newStatus = 'in-progress';
54
- }
55
- } else if (position > 0 && video.status === 'unwatched') {
56
- newStatus = 'in-progress';
57
- }
58
-
59
- // Update database
60
- this._db.run(
61
- `UPDATE videos
62
- SET position = ?,
63
- duration = COALESCE(?, duration),
64
- status = ?,
65
- updated_at = CURRENT_TIMESTAMP
66
- WHERE id = ?`,
67
- [position, duration, newStatus, videoId]
68
- );
69
-
70
- return this._db.get('SELECT * FROM videos WHERE id = ?', [videoId]);
71
- }
42
+
43
+ // Determine new status based on position
44
+ let newStatus = video.status;
45
+ const videoDuration = typeof duration === 'number' && !Number.isNaN(duration)
46
+ ? duration
47
+ : video.duration;
48
+ const isShortVideo = videoDuration > 0
49
+ && videoDuration < config.shortVideoResumeCutoffSeconds;
50
+ const completionPosition = videoDuration * config.completionThreshold;
51
+
52
+ if (videoDuration > 0) {
53
+ const watchPercent = position / videoDuration;
54
+
55
+ if (watchPercent >= config.completionThreshold) {
56
+ newStatus = 'completed';
57
+ } else if (position > 0) {
58
+ newStatus = 'in-progress';
59
+ }
60
+ } else if (position > 0 && video.status === 'unwatched') {
61
+ newStatus = 'in-progress';
62
+ }
63
+
64
+ const shouldPreserveCompletedProgress = video.status === 'completed'
65
+ && videoDuration > 0
66
+ && position < completionPosition;
67
+
68
+ if (shouldPreserveCompletedProgress) {
69
+ newStatus = 'completed';
70
+ }
71
+
72
+ let newPosition = position;
73
+
74
+ if (shouldPreserveCompletedProgress) {
75
+ newPosition = video.position;
76
+ } else if (isShortVideo && newStatus !== 'completed') {
77
+ newPosition = 0;
78
+ }
79
+
80
+ // Update database
81
+ this._db.run(
82
+ `UPDATE videos
83
+ SET position = ?,
84
+ duration = COALESCE(?, duration),
85
+ status = ?,
86
+ updated_at = CURRENT_TIMESTAMP
87
+ WHERE id = ?`,
88
+ [newPosition, duration, newStatus, videoId]
89
+ );
90
+
91
+ return this._db.get('SELECT * FROM videos WHERE id = ?', [videoId]);
92
+ }
72
93
 
73
94
  /**
74
95
  * Update video status manually
@@ -16,12 +16,13 @@ const config = {
16
16
  dbFolder: '.coursewatcher',
17
17
  dbFilename: 'database.sqlite',
18
18
 
19
- // Video settings
20
- videoExtensions: ['.mp4', '.webm', '.ogv', '.ogg'],
21
- completionThreshold: 0.9, // 90% watched = completed
22
-
23
- // Playback settings
24
- defaultPlaybackSpeed: 1.0,
19
+ // Video settings
20
+ videoExtensions: ['.mp4', '.webm', '.ogv', '.ogg'],
21
+ completionThreshold: 0.9, // 90% watched = completed
22
+ shortVideoResumeCutoffSeconds: 300,
23
+
24
+ // Playback settings
25
+ defaultPlaybackSpeed: 1.0,
25
26
  speedStep: 0.25,
26
27
  seekShort: 5,
27
28
  seekMedium: 10,
@@ -13,14 +13,14 @@
13
13
  <section class="player-section">
14
14
  <h1 class="video-title">
15
15
  <%= video.title %>
16
- </h1>
17
- <div class="video-wrapper">
18
- <video id="videoPlayer" class="plyr" playsinline controls
19
- data-video-id="<%= video.id %>" data-saved-position="<%= video.position %>"
20
- data-next-video-url="<%= adjacent.next ? '/video/' + adjacent.next : '' %>">
21
- <source src="/api/videos/<%= video.id %>/stream" type="video/mp4">
22
- Your browser does not support the video tag.
23
- </video>
16
+ </h1>
17
+ <div class="video-wrapper">
18
+ <video id="videoPlayer" class="plyr" playsinline controls
19
+ data-video-id="<%= video.id %>" data-start-position="<%= startPosition %>"
20
+ data-next-video-url="<%= adjacent.next ? '/video/' + adjacent.next : '' %>">
21
+ <source src="/api/videos/<%= video.id %>/stream" type="video/mp4">
22
+ Your browser does not support the video tag.
23
+ </video>
24
24
 
25
25
  <!-- Auto-play Countdown Overlay -->
26
26
  <div id="autoplayOverlay" class="autoplay-overlay hidden">
@@ -158,4 +158,4 @@
158
158
  <script src="/static/js/player.js"></script>
159
159
  </body>
160
160
 
161
- </html>
161
+ </html>