Skip to content

Instantly share code, notes, and snippets.

@JimChengLin
Last active October 3, 2024 14:36
TimeJ
// ==UserScript==
// @name TimeJ
// @namespace TimeJ
// @version 1.0
// @description TimeJ
// @author Jim
// @match *://*/*
// @grant GM.setValue
// @grant GM.getValue
// @grant GM.deleteValue
// @grant GM.listValues
// @grant GM.setClipboard
// ==/UserScript==
(function () {
'use strict';
if (window.self !== window.top) return;
let recordStartTimeMs = null;
const tabID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString();
function startRecord() {
if (recordStartTimeMs === null) recordStartTimeMs = Date.now();
}
async function stopRecord() {
if (recordStartTimeMs === null) return;
const recordStopTimeMs = Date.now();
const recordStopTimeMin = Math.floor(recordStopTimeMs / 60000); // in minutes
// Extract domain name
const domain = window.location.host.split('.').slice(-2).join('.');
const tabStoreKey = `${recordStopTimeMin}-${domain}-${tabID}`;
let records = JSON.parse(await GM.getValue(tabStoreKey, '[]'));
records.push([recordStartTimeMs, recordStopTimeMs]);
recordStartTimeMs = null;
await GM.setValue(tabStoreKey, JSON.stringify(records));
}
if (document.visibilityState === 'visible')
startRecord();
// Trigger recording on page visibility change
document.addEventListener('visibilitychange', () => {
document.visibilityState === 'visible' ? startRecord() : stopRecord();
});
// Stop recording before the page unloads
window.addEventListener('beforeunload', stopRecord);
// Epoch function for periodic tasks
async function epoch() {
if (!document.hidden) {
await stopRecord();
await startRecord();
}
const currTimeMin = Math.floor(Date.now() / 60000);
const r = currTimeMin % 4;
if (r === 0 || r === 2) {
return;
}
if (r === 1) {
await GM.setValue('leaderID', tabID);
return;
}
const leaderID = await GM.getValue('leaderID');
if (leaderID !== tabID) return;
const keys = await GM.listValues();
for (const k of keys) {
if (k !== 'leaderID') {
const recordStopTimeMin = parseInt(k.split('-')[0]);
if (currTimeMin - recordStopTimeMin > 4320) { // 3 days in minutes
await GM.deleteValue(k);
}
}
}
}
// Set epoch to run every minute
setInterval(epoch, 60000);
// Merge overlapping time ranges
function mergeTimeRanges(timeRanges) {
// Sort time ranges by start time
timeRanges.sort((a, b) => a[0] - b[0]);
const mergedRanges = [timeRanges[0]];
for (let i = 1; i < timeRanges.length; i++) {
const lastMerged = mergedRanges[mergedRanges.length - 1];
const current = timeRanges[i];
// If current range overlaps or touches the last merged range, merge them
if (current[0] <= lastMerged[1]) {
lastMerged[1] = Math.max(lastMerged[1], current[1]);
} else {
mergedRanges.push(current);
}
}
return mergedRanges;
}
// Convert milliseconds to a more human-readable format
function formatDuration(ms) {
if (ms < 60000) {
const seconds = ms / 1000;
return `${seconds.toFixed(2)} seconds`;
} else {
const seconds = Math.floor(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes} minutes ${remainingSeconds} seconds`;
}
}
// Calculate time spent per hour
function calculateTimeSpentPerHour(data) {
const timeSpentPerHour = {};
const domainTimeRanges = {};
// Aggregate time ranges per domain
for (const shards of Object.values(data)) {
for (const [domain, timeRanges] of shards) {
domainTimeRanges[domain] = (domainTimeRanges[domain] || []).concat(timeRanges);
}
}
// Merge and calculate time per hour
for (const domain in domainTimeRanges) {
const mergedRanges = mergeTimeRanges(domainTimeRanges[domain]);
for (const [startTime, endTime] of mergedRanges) {
let start = new Date(startTime);
let end = new Date(endTime);
while (start < end) {
let hourStart = new Date(start);
hourStart.setMinutes(0, 0, 0); // Start of hour
let hourEnd = new Date(hourStart);
hourEnd.setHours(hourEnd.getHours() + 1); // End of hour
const actualEnd = (end < hourEnd) ? end : hourEnd;
const timeSpent = actualEnd - start;
const hourKey = hourStart.toLocaleString([], {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
hour12: false
});
timeSpentPerHour[hourKey] = timeSpentPerHour[hourKey] || {};
timeSpentPerHour[hourKey][domain] = (timeSpentPerHour[hourKey][domain] || 0) + timeSpent;
start = hourEnd;
}
}
}
// Format time durations, sort domains by time spent
for (const hour in timeSpentPerHour) {
const sortedDomains = Object.entries(timeSpentPerHour[hour])
.sort(([, timeA], [, timeB]) => timeB - timeA)
.map(([domain, time]) => [domain, formatDuration(time)]);
// Convert sorted domains back to object
timeSpentPerHour[hour] = Object.fromEntries(sortedDomains);
}
return Object.fromEntries(Object.entries(timeSpentPerHour).sort());
}
// Print stats on key combination (Ctrl + 1)
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === '1') {
(async function () {
const keys = await GM.listValues();
let rawData = {};
for (const k of keys) {
if (k !== 'leaderID') {
const parts = k.split('-');
const recordStopTimeMinStr = parts[0];
const domain = parts.slice(1, -1).join('-');
const records = JSON.parse(await GM.getValue(k));
rawData[recordStopTimeMinStr] = rawData[recordStopTimeMinStr] || [];
rawData[recordStopTimeMinStr].push([domain, records]);
}
}
const res = JSON.stringify(calculateTimeSpentPerHour(rawData), null, 2);
await GM.setClipboard(res, 'text');
})();
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment