burn / oujs.org scripts stats

// ==UserScript==
// @name        oujs.org scripts stats 
// @namespace   https://openuserjs.org/users/burn
// @author      Burn
// @description Shows how many installs and ratings your scripts have got since last visit 
// @include     https://openuserjs.org/users/*
// @version     2.7.9
// @grant       GM_log
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// ==/UserScript==

(function(){
	'use strict';
	
	var DBG = false,
		storeName = "OUJS.org_scripts_stats",
		storeStatsName 	= "OUJS.org_user_stats";
		
	// helpers functions
	var 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 = function(name, def){
		def = def || '{}';
        var tmpOut = GM_getValue(name, def);if (DBG) GM_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 = function(laterDate, earlierDate){
		var difference = parseInt((laterDate - earlierDate), 10)
		,   strOut = "Last check: "
		,   humanDate = (new Date(earlierDate)).toLocaleString()
        ,   arrDiff = []
        ,   daysDifference
        ,   hoursDifference
        ,   minutesDifference
        ,   secondsDifference;
        if (DBG) GM_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 = 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 (DBG) GM_log(k + " - " + menu[k].innerHTML);
			if (menu[k].innerHTML.indexOf("Login / ") > -1) {
				return false;
			}	
			if (k === userIdx) {
				userN = menu[k].innerHTML;if (DBG) GM_log("username: " + userN);
				break;
			}
		}
		return userN;
	}
	, isScriptsPage = function() {
        var loc, usrN = 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) {
				if (DBG) GM_log("We are in Scripts Page");
				return true;
			}
		}
		return false;
	}
	
	, isUserPage = function () {
		var loc, usrN = 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) {
				if (DBG) GM_log("We are in User Page");
				return true;
			}
		}
		return false;
	}
	
	, checkFirstExecution = 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 = parseInt(GM_getValue("OUJS.org_scripts_stats.spy", 1), 10);
		if (1 === isFirstTime) {
			GM_deleteValue(storeName);
			GM_deleteValue(storeStatsName);
			if (DBG) GM_log("first userscript execution");
			GM_setValue("OUJS.org_scripts_stats.spy", 0);
		}
	}
	
	, collectScriptsStats = function() {
		if (DBG) GM_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;
			if (DBG) GM_log("Title found: " + defs.tit);
			defs.inst = qSel( "td.text-center.td-fit p", defs.scriptsList[defs.k] ).innerHTML;
			if (DBG) GM_log("Installs found: " + defs.inst);
			defs.rating = qSel( "td.rating p", defs.scriptsList[defs.k] ).innerHTML;
			if (DBG) GM_log("Rating found: " + defs.rating);
			defs.child = Object.create(defs.objScript);
			defs.child.title = defs.tit;
			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))) {
				if (DBG) GM_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;
				if (DBG) GM_log("Archive entry for " + defs.arrStored[defs.kk].title + " updated: " + defs.arrStored[defs.kk].installs
					+ " installs - " + defs.arrStored[defs.kk].rating + " rating");
			} else {
				if (DBG) GM_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")
				.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")
				.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")
				.appendChild(defs.elTotRat);
		}
		
		var merged = defs.arrScripts.concat(defs.arrStored);
		if (DBG) GM_log("End. After merging: " + merged.toSource());
		serialize(storeName, merged);
	}
	
	, collectUserStats = function() {
		defs.oldStatsDate = (Object.keys(defs.arrStatsStored).length>0) 
				? defs.arrStatsStored.pop().dateTime : (new Date()).getTime();
		if (DBG) GM_log("Found date in arrStatsStored: " + defs.oldStatsDate);
		defs.dtList = qSelAll("dt", defs.globalStatsList);
		defs.ddList = qSelAll("dd", defs.globalStatsList);
		if (DBG) GM_log("Found " + defs.dtList.length + " dt");
		if (DBG) GM_log("Found " + defs.ddList.length + " dd");
	
		// user's global stats: start from 1 because we do not keep track of 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;
			//if (DBG) GM_log(" ==== " + defs.ddList[defs.k].innerHTML.indexOf("."));
			if (defs.val.indexOf(".") > -1) {
				if (DBG) GM_log(defs.ddList[defs.k].innerHTML + 
							" Found decimal separator at index " + 
							defs.ddList[defs.k].innerHTML.indexOf("."));
				defs.val = parseFloat(defs.val).toFixed(1);
			}
			if (DBG) GM_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))) {
				if (DBG) GM_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 );
				if (DBG) GM_log( defs.child.label + ": " + defs.child.value + "-" 
						+ defs.arrStatsStored[defs.kk].value + "=" + defs.dValue);
			} else {
				if (DBG) GM_log("Key for " + defs.child.label + " not found in " +
					defs.arrStatsStored.toSource());
			}
			if (defs.dValue > 0) {
				if (defs.dValue.toString().indexOf(".") > -1) {
					if (DBG) GM_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);
		if (DBG) GM_log("End. Storing " + (defs.arrStats.length - 1) + " global stats");
		if (defs.arrStats.length>0 ) {
			serialize(storeStatsName, defs.arrStats);
		}
	}
	
	, setDateTime = function(arrTarget, oldD) {
		defs.nowDate = (new Date()).getTime();
		if (DBG) GM_log("Pushing " + defs.nowDate + " into " + arrTarget.toSource());
		arrTarget.push( {dateTime: defs.nowDate} );
		if (false === qSel("#oujs-stats-datetime")) {
			if (DBG) GM_log("Creating element to show date difference");
			defs.elDiffTime = document.createElement("p");
			defs.elDiffTime.setAttribute("id", "oujs-stats-datetime");
			defs.elDiffTime.style.cssText = "display:block;text-align:left;padding:2px 0;font-size:11px;color:#555555";
			defs.elDiffTime.innerHTML = timeDifference(defs.nowDate, oldD);
			defs.globalStatsList.parentNode.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,
		oldDate 		: 0,
		oldStatsDate    : 0,
		nowDate 		: null,
		gTotInstalls 	: 0,
		gTotRatings 	: 0,
		elTotInst 		: 0,
		elTotRat 		: 0,
		elDiffTime 		: 0,
		lbl 			: "",
		val 			: 0,
		dtList 			: false,
		ddList 			: false,
		dValue 			: 0,
		gTotValue 		: 0,
		arrScripts 		: [], // scripts found on page
		arrStats		: [], // stats found on page
		arrStored 		: deserialize(storeName), // scripts previously stored by this userscript
		arrStatsStored  : 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 === isScriptsPage()) {
    	checkFirstExecution();
    	collectScriptsStats();
    	collectUserStats();
    }
    if (true === isUserPage()) {
    	checkFirstExecution();
    	collectUserStats();
    }
	
})();