Skip to content

Commit 9c13f9a

Browse files
authored
Merge pull request #82 from EdinUser/fix/issues-with-playlist-and-stats
chore(release): prepare version 4.0.1 with analytics fixes, playlist updates, and storage optimizations
2 parents 9c20873 + 41f1bc1 commit 9c13f9a

11 files changed

Lines changed: 732 additions & 150 deletions

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22

33
All notable changes to YT re:Watch will be documented in this file.
44

5+
## [4.0.1] - 2025-11-26
6+
7+
### 🐛 Stats & Analytics Fixes
8+
- Fixed inconsistent Analytics summary cards by rebuilding the persistent stats snapshot from the full hybrid history (IndexedDB + localStorage) and using it as the single source of truth.
9+
- Summary cards (Total Watch Time, Videos Watched, Shorts Watched, Average Duration, Completion Rate, Playlists Saved) now read from the stored stats counters instead of only the currently loaded page.
10+
- Activity (Last 7 Days) and Watch Time by Hour charts now use the same persisted `daily`/`hourly` stats snapshot (with local‑day keys and 24 hourly buckets) for stable, full‑history graphs.
11+
12+
### 🎬 Playlist Handling Improvements
13+
- Improved playlist detection and title extraction across YouTube layout variants using a broader selector set and a retry‑based saver.
14+
- Playlist “pause history” toggles are now more reliable and consistently respected when saving progress, preventing unwanted history entries for ignored playlists.
15+
16+
### 🧹 Data Hygiene
17+
- Hybrid stats rebuilds now prune the `daily` stats map to the last 7 local days, keeping the snapshot compact and aligned with what the UI displays.
18+
519
## [4.0.0] - 2025-11-25
620

721
### ✨ Major New Features

docs/detailed_guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ The export file contains:
319319
- **Video History**: All your watched videos with timestamps
320320
- **Playlists**: All saved playlists
321321
- **Settings**: Your customization preferences
322-
- **Stats**: Aggregated watch‑time snapshot powering Analytics (from dataVersion 1.1)
322+
- **Stats**: Aggregated watch‑time snapshot powering Analytics (from dataVersion 1.1) with last‑7‑days `daily` buckets and a 24‑slot `hourly` distribution to keep the snapshot compact.
323323

324324
---
325325

docs/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Your history list shows the channel name under each video title to help you scan
120120
![Analytics detailed view](./images/ytrw_stats4.jpg)
121121
*Additional analytics showing watch time patterns and channel statistics*
122122

123-
These charts now prefer locally persisted, privacy‑preserving statistics for better accuracy and responsiveness. Keys are local‑day `YYYY-MM-DD` and 24 hourly buckets.
123+
These charts now prefer a locally persisted, privacy‑preserving stats snapshot (rebuilt from your full hybrid history) for better accuracy and responsiveness. Keys are local‑day `YYYY-MM-DD` and 24 hourly buckets, and the activity view focuses on the last 7 local days only.
124124

125125
- **Longest Unfinished Videos**: Resume long videos you haven't finished (shows channel, time left, and link)
126126
- **Top Watched Channels**: Your top 5 channels by videos watched (with links)

docs/technical.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,12 @@ await ytStorage.updateStats(deltaSeconds, Date.now(), {
339339
shorts: 80,
340340
totalDurationSeconds: 540000,
341341
completed: 120
342-
}
342+
},
343+
// Internal metadata:
344+
// - stats_synced: true once a full hybrid rebuild has run
345+
// - lastFullRebuild: timestamp (ms) of the last full rebuild from merged storage
346+
stats_synced: true,
347+
lastFullRebuild: 1695520000000
343348
}
344349
```
345350

src/content.js

Lines changed: 250 additions & 13 deletions
Large diffs are not rendered by default.

src/manifest.chrome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "YT re:Watch",
4-
"version": "4.0.0",
4+
"version": "4.0.1",
55
"description": "YouTube history extension for multiple accounts. Track progress with privacy: visual overlays, account switching, no login required",
66
"author": "Edin User",
77
"homepage_url": "https://github.com/EdinUser/YouTubeLocalHistory",

src/manifest.firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "YT re:Watch",
4-
"version": "4.0.0",
4+
"version": "4.0.1",
55
"description": "YouTube history extension for multiple accounts. Track progress with privacy: visual overlays, account switching, no login required",
66
"author": "Edin User",
77
"homepage_url": "https://github.com/EdinUser/YouTubeLocalHistory",

src/popup.js

Lines changed: 113 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ let totalPlaylistRecords = 0;
131131
// Stored aggregated stats cache (from persistent storage)
132132
let storedStats = null;
133133

134+
// Full merged video list for analytics (hybrid: IndexedDB + storage.local).
135+
// Populated by updateAnalytics() when the Analytics tab is shown.
136+
let analyticsAllVideos = null;
137+
134138
// Default settings
135139
const DEFAULT_SETTINGS = {
136140
autoCleanPeriod: 90, // days
@@ -1027,21 +1031,57 @@ function formatAnalyticsDuration(seconds) {
10271031

10281032
// Calculate analytics data
10291033
function calculateAnalytics(records) {
1030-
const totalSeconds = (storedStats && typeof storedStats.totalWatchSeconds === 'number')
1031-
? storedStats.totalWatchSeconds
1034+
const hasStoredStats = !!(storedStats && typeof storedStats.totalWatchSeconds === 'number' && storedStats.counters);
1035+
1036+
// Total watch time: prefer persisted snapshot, fall back to current-page data
1037+
const totalSeconds = hasStoredStats
1038+
? Math.max(0, Math.floor(storedStats.totalWatchSeconds || 0))
10321039
: records.reduce((sum, record) => sum + (record.time || 0), 0);
1033-
const totalDuration = records.reduce((sum, record) => sum + (record.duration || 0), 0);
1034-
const completedVideos = records.filter(record =>
1035-
record.time && record.duration && (record.time / record.duration) >= 0.9
1036-
).length;
1040+
1041+
let videosWatched = 0;
1042+
let shortsWatched = 0;
1043+
let avgDurationSeconds = 0;
1044+
let completionRate = 0;
1045+
1046+
if (hasStoredStats) {
1047+
const counters = storedStats.counters || {};
1048+
const videosCount = Math.max(0, Math.floor(Number(counters.videos || 0)));
1049+
const shortsCount = Math.max(0, Math.floor(Number(counters.shorts || 0)));
1050+
const totalItems = videosCount + shortsCount;
1051+
const totalDurationSeconds = Math.max(0, Math.floor(Number(counters.totalDurationSeconds || 0)));
1052+
const completedCount = Math.max(0, Math.floor(Number(counters.completed || 0)));
1053+
1054+
videosWatched = totalItems;
1055+
shortsWatched = shortsCount;
1056+
avgDurationSeconds = totalItems > 0 ? (totalDurationSeconds / totalItems) : 0;
1057+
completionRate = totalItems > 0 ? Math.round((completedCount / totalItems) * 100) : 0;
1058+
} else {
1059+
// Fallback: current-page-only behavior (what you previously had)
1060+
const totalDuration = records.reduce((sum, record) => sum + (record.duration || 0), 0);
1061+
const completedVideos = records.filter(record =>
1062+
record.time && record.duration && (record.time / record.duration) >= 0.9
1063+
).length;
1064+
1065+
videosWatched = records.length;
1066+
shortsWatched = allShortsRecords.length;
1067+
avgDurationSeconds = videosWatched > 0 ? (totalDuration / videosWatched) : 0;
1068+
completionRate = videosWatched > 0 ? Math.round((completedVideos / videosWatched) * 100) : 0;
1069+
}
1070+
1071+
// Playlists are already loaded from getAllPlaylists() in updateAnalytics()
1072+
const playlistsSaved = Array.isArray(allPlaylists) ? allPlaylists.length : 0;
10371073

10381074
return {
1039-
totalWatchTime: formatAnalyticsDuration(totalSeconds),
1040-
videosWatched: records.length,
1041-
shortsWatched: allShortsRecords.length,
1042-
avgDuration: formatAnalyticsDuration(totalDuration / records.length || 0),
1043-
completionRate: Math.round((completedVideos / records.length) * 100) || 0,
1044-
playlistsSaved: allPlaylists.length
1075+
totalWatchTime: formatAnalyticsDuration(Math.floor(totalSeconds)),
1076+
videosWatched,
1077+
shortsWatched,
1078+
avgDuration: formatAnalyticsDuration(
1079+
Number.isFinite(avgDurationSeconds) && avgDurationSeconds > 0
1080+
? Math.floor(avgDurationSeconds)
1081+
: 0
1082+
),
1083+
completionRate,
1084+
playlistsSaved
10451085
};
10461086
}
10471087

@@ -1108,6 +1148,21 @@ async function updateAnalytics() {
11081148
allPlaylists = [];
11091149
}
11101150

1151+
// Load full merged video history for analytics (hybrid: IndexedDB + storage.local)
1152+
// so that channel/skip/completion distributions are based on the complete dataset,
1153+
// not just the current page.
1154+
try {
1155+
const videosObj = await ytStorage.getAllVideos();
1156+
if (videosObj && typeof videosObj === 'object') {
1157+
analyticsAllVideos = Object.values(videosObj);
1158+
} else {
1159+
analyticsAllVideos = [];
1160+
}
1161+
} catch (e) {
1162+
console.warn('[Analytics] Failed to load full video history for analytics, falling back to current page data:', e);
1163+
analyticsAllVideos = [...allHistoryRecords, ...allShortsRecords];
1164+
}
1165+
11111166
// Migrate stats if daily/hourly appear empty due to previous key format
11121167
try {
11131168
const haveHistory = (allHistoryRecords && allHistoryRecords.length) || (allShortsRecords && allShortsRecords.length);
@@ -1286,26 +1341,36 @@ function updateActivityChart() {
12861341
// Clear previous chart
12871342
ctx.clearRect(0, 0, canvas.width, canvas.height);
12881343

1289-
// Get last 7 days of activity (prefer stored daily totals if present)
1344+
// Get last 7 days of activity
12901345
const now = new Date();
12911346
const days = Array.from({length: 7}, (_, i) => {
12921347
const date = new Date(now);
12931348
date.setDate(date.getDate() - i);
12941349
return `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;
12951350
}).reverse();
12961351

1297-
const allVideosForDaily = [...allHistoryRecords, ...allShortsRecords];
1352+
const videoSource = Array.isArray(analyticsAllVideos) && analyticsAllVideos.length
1353+
? analyticsAllVideos
1354+
: [...allHistoryRecords, ...allShortsRecords];
1355+
1356+
// Number of videos per day (use full merged list when available)
12981357
const activity = days.map(day => {
1299-
return allVideosForDaily.filter(record => {
1358+
return videoSource.filter(record => {
1359+
if (!record.timestamp) return false;
13001360
const d = new Date(record.timestamp);
13011361
const recordDate = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
13021362
return recordDate === day;
13031363
}).length;
13041364
});
13051365

1306-
// Also compute total minutes per day
1366+
// Total minutes per day: prefer storedStats.daily (rebuilt from hybrid stats)
13071367
const minutesPerDay = days.map(day => {
1308-
const seconds = allVideosForDaily.reduce((sum, record) => {
1368+
if (storedStats && storedStats.daily && typeof storedStats.daily === 'object') {
1369+
const seconds = Number(storedStats.daily[day] || 0);
1370+
return Math.round(seconds / 60);
1371+
}
1372+
1373+
const secondsFromVideos = videoSource.reduce((sum, record) => {
13091374
if (!record.timestamp) return sum;
13101375
const d = new Date(record.timestamp);
13111376
const recordDate = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
@@ -1314,7 +1379,7 @@ function updateActivityChart() {
13141379
}
13151380
return sum;
13161381
}, 0);
1317-
return Math.round(seconds / 60);
1382+
return Math.round(secondsFromVideos / 60);
13181383
});
13191384

13201385
// Draw chart
@@ -1380,18 +1445,24 @@ function updateWatchTimeByHourChart() {
13801445
// Clear previous chart
13811446
ctx.clearRect(0, 0, canvas.width, canvas.height);
13821447

1383-
// Calculate watch time by hour on the fly from current records
1384-
const hourlyData = new Array(24).fill(0);
1385-
const allVideos = [...allHistoryRecords, ...allShortsRecords];
1386-
allVideos.forEach(record => {
1387-
if (record.timestamp) {
1388-
const hour = new Date(record.timestamp).getHours();
1389-
hourlyData[hour] += record.time || 0;
1390-
}
1391-
});
1448+
// Calculate watch time by hour.
1449+
// Prefer persisted stats.hourly (rebuilt from hybrid storage), fall back to current records.
1450+
let hourlySeconds = null;
1451+
if (storedStats && Array.isArray(storedStats.hourly) && storedStats.hourly.length === 24) {
1452+
hourlySeconds = storedStats.hourly.map(v => Number(v || 0));
1453+
} else {
1454+
hourlySeconds = new Array(24).fill(0);
1455+
const allVideos = [...allHistoryRecords, ...allShortsRecords];
1456+
allVideos.forEach(record => {
1457+
if (record.timestamp) {
1458+
const hour = new Date(record.timestamp).getHours();
1459+
hourlySeconds[hour] += record.time || 0;
1460+
}
1461+
});
1462+
}
13921463

13931464
// Convert seconds to minutes for better readability
1394-
const hourlyMinutes = hourlyData.map(seconds => Math.round(seconds / 60));
1465+
const hourlyMinutes = hourlySeconds.map(seconds => Math.round(seconds / 60));
13951466

13961467
// Draw chart
13971468
const maxMinutes = Math.max(...hourlyMinutes, 1);
@@ -3372,7 +3443,12 @@ function renderTopChannels() {
33723443

33733444
// Aggregate by channel
33743445
const channelMap = {};
3375-
allHistoryRecords.forEach(record => {
3446+
// Prefer full merged video list (non-Shorts) when available
3447+
const source = Array.isArray(analyticsAllVideos) && analyticsAllVideos.length
3448+
? analyticsAllVideos.filter(r => !r.isShorts)
3449+
: allHistoryRecords;
3450+
3451+
source.forEach(record => {
33763452
const channel = record.channelName || 'Unknown Channel';
33773453
const channelId = record.channelId || '';
33783454
if (channel === 'Unknown Channel') return; // skip unknown
@@ -3444,7 +3520,10 @@ function renderSkippedChannels() {
34443520
if (!container) return;
34453521

34463522
// Only consider long videos
3447-
const longVideos = allHistoryRecords.filter(r => r.duration >= 600);
3523+
const source = Array.isArray(analyticsAllVideos) && analyticsAllVideos.length
3524+
? analyticsAllVideos.filter(r => !r.isShorts)
3525+
: allHistoryRecords;
3526+
const longVideos = source.filter(r => r.duration >= 600);
34483527
const skipped = longVideos.filter(r => (r.time / r.duration) < 0.1);
34493528

34503529
// Aggregate by channel
@@ -3528,7 +3607,10 @@ function renderCompletionBarChart() {
35283607
ctx.clearRect(0, 0, canvas.width, canvas.height);
35293608

35303609
// Only consider long videos
3531-
const longVideos = allHistoryRecords.filter(r => r.duration >= 600);
3610+
const source = Array.isArray(analyticsAllVideos) && analyticsAllVideos.length
3611+
? analyticsAllVideos.filter(r => !r.isShorts)
3612+
: allHistoryRecords;
3613+
const longVideos = source.filter(r => r.duration >= 600);
35323614
const skipped = longVideos.filter(r => (r.time / r.duration) < 0.1);
35333615
const partial = longVideos.filter(r => (r.time / r.duration) >= 0.1 && (r.time / r.duration) < 0.9);
35343616
const completed = longVideos.filter(r => (r.time / r.duration) >= 0.9);

0 commit comments

Comments
 (0)