burn / oujs.org scripts stats

// ==UserScript==
// @name        oujs.org scripts stats
// @namespace   https://openuserjs.org/users/burn
// @author      Burn
// @copyright   2014, burn (https://openuserjs.org//users/burn)
// @description Shows how many installs and ratings your scripts have got since last visit. Greasemonkey v4+ compatible
// @license     MIT
// @include     https://openuserjs.org/users/*
// @version     3.0.3
// @updateURL   https://openuserjs.org/meta/burn/oujs.org_scripts_stats.meta.js
// @downloadURL https://openuserjs.org/install/burn/oujs.org_scripts_stats.user.js
// @require  	https://greasemonkey.github.io/gm4-polyfill/gm4-polyfill.js
// @grant    	GM_getValue
// @grant    	GM_setValue
// @grant    	GM_deleteValue
// @grant    	GM.getValue
// @grant    	GM.setValue
// @grant       GM.deleteValue
// ==/UserScript==

/*jshint esversion: 8 */ 
(async function() {
	'use strict';

	var DBG = false,
		storeName = "OUJS.org_scripts_stats",
		storeStatsName 	= "OUJS.org_user_stats";

	// helpers functions
	var log = function(s){
		if (DBG) console.log(s);
	}
	, qSel = function(q, c){
		var d = (typeof c == 'object') ? c : document;
		return d.querySelector(q) || false;
	}
	, qSelAll = function(q, c){
		var d = (typeof c == 'object') ? c : document;
		return d.querySelectorAll(q) || false;
	}
	, serialize = function(name, val){
		GM.setValue(name, JSON.stringify(val));
	}
	, deserialize = async function(name, def){
		log('in serialize');
		def = def || '{}';
        var tmpOut = await GM.getValue(name, def);
        log("deserialize: " + tmpOut);
		return JSON.parse(tmpOut);
	}
	, getKey = function(prop, val, obj){
        var j = 0, jL = obj.length;
		for (; j<jL; j++) {
            if (obj[j][prop] == val){return j;}
		}
		return false;
	}
	, timeDifference = async function(laterDate, earlierDate){

		var difference = parseInt((laterDate - earlierDate), 10)
		,   strOut = ""
		,   humanDate = (new Date(earlierDate)).toLocaleString()
        ,   arrDiff = []
        ,   daysDifference
        ,   hoursDifference
        ,   minutesDifference
        ,   secondsDifference;
        log("diff = " + difference + " - laterD: " + laterDate + " - earlierD: " + earlierDate);
		if (difference <= 1) return strOut + "now (" + humanDate + ")";
		daysDifference = Math.floor(difference/1000/60/60/24);
        if (daysDifference > 0) arrDiff.push( daysDifference + " days");
		difference -= daysDifference*1000*60*60*24;
		hoursDifference = Math.floor(difference/1000/60/60);
        if (hoursDifference > 0) arrDiff.push(hoursDifference + " hours");
		difference -= hoursDifference*1000*60*60;
		minutesDifference = Math.floor(difference/1000/60);
        if (minutesDifference > 0) arrDiff.push(minutesDifference + " min.");
		difference -= minutesDifference*1000*60;
		secondsDifference = Math.floor(difference/1000);
		// only care of seconds within a day ago
        if (secondsDifference > 0 && daysDifference < 2)
        	arrDiff.push(secondsDifference + " sec.");
		strOut += arrDiff.join(" ") + " ago (" + humanDate + ")";
		return strOut;
	}
	, setStatsCSS = function() {
        var cssOut = "margin:0;\
			padding:0;\
			text-align:center;\
			display:inline;\
			color:green;\
			line-height:6px;\
			font-size:11px;";
		return cssOut;
	}
	, setNegativeStatsCSS = function() {
		    var cssOut = "margin:0;\
			padding:0;\
			text-align:center;\
			display:inline;\
			color:darkred;\
			line-height:6px;\
			font-size:11px;";
		return cssOut;
	}
	, isUserLogged = async function() {
		var menu = qSelAll("nav.navbar .navbar-collapse-top a")
		,   userN = false
        ,   k = menu.length - 1
        // catch username by its index, for both users and admins
		,   userIdx = menu.length - 2;

		for (k; k>=0; k--) {
			if (menu[k].innerHTML.indexOf("Login / ") > -1)
				return false;
			if (k === userIdx) {
				userN = menu[k].innerHTML;
				log("username: " + userN);
				break;
			}
		}
		return userN;
	}
	, isScriptsPage = async function() {
        var loc, usrN = await isUserLogged();
		if ( false !== usrN ) {
			var RE_loc = new RegExp('https:\/\/openuserjs.org\/users\/' + usrN + '\/scripts\/?', 'i');
			loc = window.location.href.toString();
			var matches = loc.match(RE_loc);
			if (null != matches && matches.length > 0) {
				log("We are in Scripts Page");
				return true;
			}
		}
		return false;
	}

	, isUserPage = async function () {
		var loc, usrN = await isUserLogged();
		if ( false !== usrN ) {
			var RE_loc = new RegExp('https:\/\/openuserjs.org\/users\/'+ usrN + '\/?$', 'i');
			loc = window.location.href.toString();
			var matches = loc.match(RE_loc);
			if (null != matches && matches.length > 0) {
				log("We are in User Page");
				return true;
			}
		}
		return false;
	}

	, checkFirstExecution = async function() {
		// check if we're running the script for the first time (e.g. after upgrading it)
		// horrible workaround made for compatibility reasons
		var isFirstTime = await GM.getValue("OUJS.org_scripts_stats.spy", 1);
		if (1 === isFirstTime) {
			GM.deleteValue(storeName);
			GM.deleteValue(storeStatsName);
			log("first userscript execution");
			GM.setValue("OUJS.org_scripts_stats.spy", 0);
		}
	}
	, collectScriptsStats = async function() {
		log("Found " + defs.scriptsList.length + " scripts");
		for (defs.k=0, defs.kL=defs.scriptsList.length; defs.k<defs.kL; defs.k++) {
			defs.tit = qSel( "a.tr-link-a b", defs.scriptsList[defs.k] ).innerHTML;
			log("Title found: " + defs.tit);
			defs.inst = qSel( "td.text-center.td-fit p", defs.scriptsList[defs.k] ).innerHTML;
			log("Installs found: " + defs.inst);
			defs.rating = qSel( "td.rating p", defs.scriptsList[defs.k] ).innerHTML;
			log("Rating found: " + defs.rating);
            defs.url = qSel("a.tr-link-a", defs.scriptsList[defs.k]).getAttribute("href");
            log("Url found: " + defs.url);
			defs.child = Object.create(defs.objScript);
			defs.child.title = defs.tit;
            defs.child.url = window.location.protocol + "//"
                + window.location.hostname + defs.url;
			defs.child.installs = parseInt(defs.inst, 10);
			defs.child.rating = parseInt(defs.rating, 10);

			if (false !== (defs.kk = getKey("title", defs.child.title, defs.arrStored))) {
				log(defs.child.title + " stats found in archive, calculating deltas");
				defs.dInst = ( defs.child.installs - defs.arrStored[defs.kk].installs );
				defs.dRating = ( defs.child.rating - defs.arrStored[defs.kk].rating ) ;
				defs.gTotInstalls += defs.dInst;
				defs.gTotRatings += defs.dRating;

				// updating values into archive
				defs.arrStored[defs.kk].installs = defs.child.installs;
				defs.arrStored[defs.kk].rating = defs.child.rating;
				log("Archive entry for " + defs.arrStored[defs.kk].title + " updated: " + defs.arrStored[defs.kk].installs
					+ " installs - " + defs.arrStored[defs.kk].rating + " rating");
			} else {
				log(defs.child.title + " not found in archive, will add it.");
				defs.arrScripts.push(defs.child);
			}

			if (defs.dInst > 0) {
				defs.elDelta = document.createElement("sup");
				defs.elDelta.style.cssText = setStatsCSS();
				defs.elDelta.innerHTML =  "+" + defs.dInst;
				qSel( "td.text-center.td-fit p", defs.scriptsList[defs.k] )
					.appendChild(defs.elDelta);
			}
			if (defs.dRating > 0) {
				defs.elDelta = document.createElement("sup");
				defs.elDelta.style.cssText = setStatsCSS();
				defs.elDelta.innerHTML =  "+" + defs.dRating;
				qSel( "td.rating p", defs.scriptsList[defs.k] )
					.appendChild(defs.elDelta);
			} else if (defs.dRating < 0) {
				defs.elDelta = document.createElement("sub");
				defs.elDelta.style.cssText = setNegativeStatsCSS();
				defs.elDelta.innerHTML =  defs.dRating;
				qSel( "td.rating p", defs.scriptsList[defs.k] )
					.appendChild(defs.elDelta);
			}
			defs.child = null;
		} // end for

		// total installs and ratings since last page visit
		if (defs.gTotInstalls > 0) {
			defs.elTotInst = document.createElement("sup");
			defs.elTotInst.style.cssText = setStatsCSS();
			defs.elTotInst.innerHTML =  "+" + defs.gTotInstalls;
			qSel(".container-fluid.col-sm-8 th:nth-of-type(2) a span:first-of-type")
				.appendChild(defs.elTotInst);
		}
		if (defs.gTotRatings > 0) {
			defs.elTotRat= document.createElement("sup");
			defs.elTotRat.style.cssText = setStatsCSS();
			defs.elTotRat.innerHTML =  "+" + defs.gTotRatings;
			qSel(".container-fluid.col-sm-8 th:nth-of-type(3) a span:first-of-type")
				.appendChild(defs.elTotRat);
		} else if (defs.gTotRatings < 0) {
			defs.elTotRat= document.createElement("sub");
			defs.elTotRat.style.cssText = setNegativeStatsCSS();
			defs.elTotRat.innerHTML = defs.gTotRatings;
			qSel(".container-fluid.col-sm-8 th:nth-of-type(3) a span:first-of-type")
				.appendChild(defs.elTotRat);
		}
        var merged = defs.arrScripts.concat(defs.arrStored);
        log("faccio il merge di " + defs.arrScripts.length + " e " + defs.arrStored.length)
		log("End. After merging: " + merged.length);
		serialize(storeName, merged);
	}

	, collectUserStats = async function() {
		defs.oldStatsDate = (Object.keys(defs.arrStatsStored).length>0)
				? defs.arrStatsStored.pop().dateTime : (new Date()).getTime();
		log("Found date in arrStatsStored: " + defs.oldStatsDate);
		defs.dtList = qSelAll("dt", defs.globalStatsList);
		defs.ddList = qSelAll("dd", defs.globalStatsList);
		log("Found " + defs.dtList.length + " dt");
		log("Found " + defs.ddList.length + " dd");

		// user global stats: start from 1 because we do not track the joining date
		for (defs.k=1, defs.kL = defs.dtList.length; defs.k < defs.kL; defs.k++) {
			defs.lbl = defs.dtList[defs.k].innerHTML;
			defs.val = defs.ddList[defs.k].innerHTML;
			//log(" ==== " + defs.ddList[defs.k].innerHTML.indexOf("."));
			if (defs.val.indexOf(".") > -1) {
				log(defs.ddList[defs.k].innerHTML +
                    " Found decimal separator at index " +
                    defs.ddList[defs.k].innerHTML.indexOf("."));
				defs.val = parseFloat(defs.val).toFixed(1);
			}
			log(defs.lbl + " : " + defs.val);
			defs.child = Object.create(defs.objGlobalStat);
			defs.child.label = defs.lbl;
			defs.child.value = defs.val;
			defs.dValue = 0;
			if (false !== (defs.kk = getKey("label", defs.child.label, defs.arrStatsStored))) {
				log(defs.child.label + " stats found in archive with value "
					+ defs.arrStatsStored[defs.kk].value);
				defs.dValue = ( defs.child.value - defs.arrStatsStored[defs.kk].value );
				log( defs.child.label + ": " + defs.child.value + "-"
						+ defs.arrStatsStored[defs.kk].value + "=" + defs.dValue);
			} else {
				log("Key for " + defs.child.label + " not found in " +
					defs.arrStatsStored);
			}
			if (defs.dValue > 0) {
				if (defs.dValue.toString().indexOf(".") > -1) {
					log(defs.dValue +
							" Found decimal separator at index " +
							defs.dValue.toString().indexOf("."));
					defs.dValue = parseFloat(defs.dValue).toFixed(1);
				}
				defs.elTotInst = document.createElement("sup");
				defs.elTotInst.style.cssText = setStatsCSS();
				defs.elTotInst.innerHTML =  "	+" + defs.dValue;
				defs.ddList[defs.k].appendChild(defs.elTotInst);
			}
			defs.arrStats.push(defs.child);
			defs.child = null;
		}
		setDateTime(defs.arrStats, defs.oldStatsDate);
		log("End. Storing " + (defs.arrStats.length - 1) + " global stats");
		if (defs.arrStats.length > 0) serialize(storeStatsName, defs.arrStats);
	}

	, setDateTime = async function(arrTarget, oldD) {
		defs.nowDate = (new Date()).getTime();
		log("Pushing " + defs.nowDate + " into " + arrTarget);
		arrTarget.push( {dateTime: defs.nowDate} );
		if (false === qSel("#oujs-stats-datetime")) {
			log("Creating element to show date difference");
			defs.elDiffTimeLabel = document.createElement("dt");
			defs.elDiffTimeLabel.setAttribute("id", "oujs-stats-datetime");
            defs.elDiffTimeLabel.innerHTML = "Last check";
			defs.globalStatsList.appendChild(defs.elDiffTimeLabel);
            defs.elDiffTime = document.createElement("dd");
            defs.elDiffTime.innerHTML = await timeDifference(defs.nowDate, oldD);
            defs.globalStatsList.appendChild(defs.elDiffTime);
		}
	};

	// end helpers

	//GM.deleteValue(storeName); // will reset your archive! uncomment when debugging only
	//GM.deleteValue(storeStatsName); // will reset your archive! uncomment when debugging only

	var defs = {
		tit 			   : "",
		dInst 			   : 0,
		kk 				   : 0,
		inst 			   : 0,
		child			   : null,
		elDelta 		   : 0,
		k 				   : 0,
		kL 				   : 1,
		rating 			   : 0,
		dRating 		   : 0,
        url                : "",
		oldDate 		   : 0,
		oldStatsDate       : 0,
		nowDate 	       : null,
		gTotInstalls 	   : 0,
		gTotRatings 	   : 0,
		elTotInst 		   : 0,
		elTotRat 		   : 0,
		elDiffTime 		   : null,
        elDiffTimeLabel    : null,
		lbl 			   : "",
		val 			   : 0,
		dtList 			   : false,
		ddList 			   : false,
		dValue 			   : 0,
		gTotValue 		   : 0,
		arrScripts 		   : [], // scripts found on page
		arrStats		   : [], // stats found on page
		arrStored 		   : await deserialize(storeName),// scripts previously stored by this userscript
		arrStatsStored     : await deserialize(storeStatsName),
		objScript 		   : { // script object
			title    : "",
			installs : 0,
			rating   : 0
		},
		objGlobalStat 	   : { // global stat object
			label   : "",
			value   : 0
		},
		scriptsList 	   : qSelAll(".tr-link"),
		globalStatsList    : qSel
			(".container-fluid.col-sm-4 .panel-default .panel-body .dl-horizontal")
	};


	// (sort of) main
    if (true === await isScriptsPage()) {
    	await checkFirstExecution();
    	await collectScriptsStats();
    	await collectUserStats();
    }
    if (true === await isUserPage()) {
    	await checkFirstExecution();
    	await collectUserStats();
    }

})();