// ==UserScript==
// @name MTurk Wage Reporter
// @namespace localhost
// @description Tracks a best-estimate hourly wage on active HITs being worked.
// @include https://www.mturk.com/mturk/accept?*
// @include https://www.mturk.com/mturk/continue?*
// @include https://www.mturk.com/mturk/previewandaccept?*
// @include https://www.mturk.com/mturk/preview?*
// @include https://www.mturk.com/mturk/dashboard*
// @include https://www.mturk.com/mturk/submit*
// @version 0.5.1b
// @grant GM_setValue
// @grant GM_getValue
// @require http://code.jquery.com/jquery-2.1.1.js
// @author DeliriumTremens 2014
// @downloadURL none
// ==/UserScript==
//
// 2014-07-10 0.1b Beginning development. Creating timer and tab tracker, as well as initial IndexedDB for storage of data.
//
//
// 2014-07-15 0.4.2b Continued development. Not ready for live usage yet. Expanded math for wage calculation, added safeguards
// to prevent adding expired or missing HITs. Added formatting for dashboard. Set up groundwork for additional
// math to be included in the next update. Added a buttload of comments to the code.
//
//
// 2014-07-16 0.5b More work! Added function to remove a HIT that expires from the database under the assumption that expired hits
// were forgotten about rather than actively worked on until expiration. Updated wage calculation because I had a major
// brainfart about DIVIDING DOLLARS BY HOURS -- don't ask... Yet more safeguards and checks put in place...
//
// 2014-07-16 0.5.1b Added a dropdown to select hourly wage by requester. Minor bug fixes.
//
// ------------------------------------------------------------------------------------------------------------------------------------------------
// First, create indexedDB variables.
// This sets up the various indexedDB functions and processes for interacting with WageDB.
// Parts borrowed from HITdb
var indexedDB = window.indexedDB || window.webkitIndexedDB || window.mozIndexedDB;
// Variables used for updating the wage per requester chosen
var reqList = [];
var wageReturn = 0;
var WageStorage = {};
window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.mozIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.mozIDBKeyRange;
var idbKeyRange = window.IDBKeyRange;
WageStorage.indexedDB = {};
WageStorage.indexedDB.db = null;
// Global catch for indexedDB errors
WageStorage.indexedDB.onerror = function (e) {
console.log(e);
}
// Check if database exists. If no database, create one.
// Parts borrowed from HITdb.
var inProgress = false; // Boolean for when a HIT is continued from queue
var dbExists = true; // Boolean for when the database is already created
var v = 5; // Version of the database
WageStorage.indexedDB.create = function () { // Function that creates the database if it hasn't been created already.
var request = indexedDB.open("WageDB", v);
request.onupgradeneeded = function (e) {
WageStorage.indexedDB.db = e.target.result;
var db = WageStorage.indexedDB.db;
var newDB = false;
if (!db.objectStoreNames.contains("Wage")) {
var store = db.createObjectStore("Wage", {
keyPath: "hitId"
}); // Primary key of the database (not an index)
store.createIndex("date", "date", {
unique: false
}); // These are the other fields (or Indexes) of the database.
store.createIndex("reqName", "reqName", {
unique: false
});
store.createIndex("reqId", "reqId", {
unique: false
});
store.createIndex("reward", "reward", {
unique: false
});
store.createIndex("start", "start", {
unique: false
});
store.createIndex("stop", "stop", {
unique: false
});
store.createIndex("unworked", "unworked", {
unique: false
});
newDB = true;
}
db.close();
}
request.onsuccess = function (e) {
WageStorage.indexedDB.db = e.target.result;
var db = WageStorage.indexedDB.db;
db.close();
}
request.onerror = WageStorage.indexedDB.onerror;
}
// Function for adding HIT data into database.
// Includes logic for updating HITs which were worked on in several sittings (or from queue)
WageStorage.indexedDB.addhit = function () {
var request = indexedDB.open("WageDB", v);
request.onsuccess = function (e) {
WageStorage.indexedDB.db = e.target.result;
var db = WageStorage.indexedDB.db;
var newDB = false;
if (!db.objectStoreNames.contains("Wage")) { // Make sure database is there... This should never fire.
db.close();
} else {
var trans = db.transaction(["Wage"], 'readwrite');
var store = trans.objectStore("Wage");
var request;
request = store.put({
hitId: hitId,
date: date,
reqName: reqName,
reqId: reqId[1],
reward: reward,
start: wageStart,
stop: wageEnd,
unworked: wageExtra
}); // Insert fresh hit into database
request.onsuccess = function (e) {
console.log(e.target.result);
console.log("Inserted HIT into database...")
}
}
db.close();
}
request.onerror = WageStorage.indexedDB.onerror;
}
// Function for getting todays data.
// Gets reward amount, start time, stop time, and queue time (unworked time)
WageStorage.indexedDB.getWage = function () {
console.log("Retrieving wage for dashboard...");
var request = indexedDB.open("WageDB", v);
request.onsuccess = function (e) {
WageStorage.indexedDB.db = e.target.result;
var db = WageStorage.indexedDB.db
var transaction = db.transaction('Wage', 'readonly');
var store = transaction.objectStore('Wage');
var index = store.index('date');
var range = IDBKeyRange.only(date);
var results = [];
var tmp_results = {};
index.openCursor(range).onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
var hit = cursor.value;
if (tmp_results[hit.hitId] === undefined) {
tmp_results[hit.hitId] = [];
tmp_results[hit.hitId][0] = hit.reward;
tmp_results[hit.hitId][1] = hit.start;
tmp_results[hit.hitId][2] = hit.stop;
tmp_results[hit.hitId][3] = hit.unworked;
if ($.inArray(hit.reqName, reqList) === -1) {
reqList.push(hit.reqName);
}
}
cursor.continue ();
} else {
for (var key in tmp_results) {
results.push(tmp_results[key]);
}
console.log("Calculating wage...");
console.log(reqList);
var wage = calculateWage(results); // Calls function to calculate wage
console.log("Wage calculated, showing results..." + wage);
wageReturn = wage;
addTableElement(wage); // Calls function to add wage to dashboard after all data has been pulled
}
}
}
}
// Function to return a wage for a specific requester
WageStorage.indexedDB.getReq = function (requesterName) {
var request = indexedDB.open("WageDB", v);
var requester = requesterName;
request.onsuccess = function (e) {
WageStorage.indexedDB.db = e.target.result;
var db = WageStorage.indexedDB.db
var transaction = db.transaction('Wage', 'readonly');
var store = transaction.objectStore('Wage');
var index = store.index('date');
var range = IDBKeyRange.only(date);
var results = [];
var tmp_results = {};
index.openCursor(range).onsuccess = function (event) {
var cursor = event.target.result;
if (cursor) {
var hit = cursor.value;
if (tmp_results[hit.hitId] === undefined) {
tmp_results[hit.hitId] = [];
tmp_results[hit.hitId][0] = hit.reward;
tmp_results[hit.hitId][1] = hit.start;
tmp_results[hit.hitId][2] = hit.stop;
tmp_results[hit.hitId][3] = hit.unworked;
tmp_results[hit.hitId][4] = hit.reqName;
}
cursor.continue ();
} else {
for (var key in tmp_results) {
if (requester === 'All') {
tmp_results[key].pop();
results.push(tmp_results[key]);
} else {
if (tmp_results[key][4] === requester) {
tmp_results[key].pop();
results.push(tmp_results[key]);
}
}
}
console.log("Calculating specific Requester wage...");
var wage = calculateWage(results); // Calls function to calculate wage
console.log("Requester Wage calculated, showing results..." + wage);
$('td[name="wageCell"]').text("$" + wage + "/hr");
//wageReturn = wage;
return;
}
}
return;
}
return;
}
// Function to check if HIT has already been started, if so then store the start and end time so it can be updated and have unworked time added to database
WageStorage.indexedDB.getHit = function () {
console.log("Checking if HIT exists...");
var request = indexedDB.open("WageDB", v);
request.onsuccess = function (e) {
WageStorage.indexedDB.db = e.target.result;
var hitId2 = $('input[name="hitId"]').val();
var db = WageStorage.indexedDB.db
var transaction = db.transaction('Wage', 'readonly');
var store = transaction.objectStore('Wage');
var hitCheck = store.get(hitId2);
hitCheck.onsuccess = function (event) {
if (event.target.result === undefined) {
inProgress = false;
console.log("New HIT...");
} else {
inProgress = true;
wageExtra = (event.target.result.unworked + (wageStart - event.target.result.stop));
wageStart = event.target.result.start;
console.log("HIT exists, continuing...");
}
}
}
}
// Function to remove bad or unused HIT from database
WageStorage.indexedDB.delHit = function (remHit) {
console.log("Expired HIT... Removing from DB...");
var request = indexedDB.open("WageDB", v);
var remHit = remHit;
request.onsuccess = function (e) {
WageStorage.indexedDB.db = e.target.result;
var db = WageStorage.indexedDB.db
var transaction = db.transaction('Wage', 'readwrite');
var store = transaction.objectStore('Wage');
var hitDel = store.delete(remHit);
hitDel.onsuccess = function (event) {
console.log("HIT successfully deleted from database...");
}
}
}
// Script Variables
var wageLost = false; // Boolean for a returned hit, and a wage that is abandoned. Will record the time with zero payment.
var wageExtra = 0; // Placeholder for extra time that was not worked.
var wageStart = null; // Placeholder for start time of HIT
var wageEnd = null; // Placeholder for end time of HIT
var wageNix = false; // Placeholder for determining if HIT was expired or unavailable or captcha'd
var expiredHit = false; // Placeholder for expired HIT
var reqId = document.URL.match(/requesterId=(.*?)&/i); // Parse requester ID out of URL
if (reqId) {
GM_setValue("reqId", reqId); // If no requester ID is available, it's because you accepted next hit in batch. Retrieves ID.
} else {
reqId = GM_getValue("reqId"); // Stores requester ID in a global script var to be pulled in case of batch work.
}
// HIT Data
var hitId = $('input[name="hitId"]').val(); // Parses hit id from html
var reward = parseFloat($('span[class="reward"]:eq(1)').text().replace('$', '')); // Parses reward from html
var reqName = $('input[name="prevRequester"]').val(); // Parses requester name from html
var date = new Date(); // Get todays date
date = date.toLocaleDateString().replace(/\//g, '-'); // Convert date to usable string
// Create table element for showing hourly wage. Parts borrowed from Today's Projected Earnings script
var allTds, thisTd;
var allTdhtml = [];
calculateWage = function (wage) { // Function that does the meat of the wage calculation. Meaty.
var currStart = 0;
var currEnd = 0;
var currWages = [];
var currHit = 0;
var batch = [];
var batchWage = 0;
var currWage = 0;
for (var i = 0; i < wage.length; i++) {
if (currStart < wage[i][1] && wage[i][1] < currEnd) {
// If current HIT started before previous HIT was submitted, start batch processing
//console.log(" Batch starting...");
currEnd = wage[i][2];
currHit = (wage[i][0]) / ((((currEnd - currStart) - wage[i][3]) / 1000) / 3600);
batch.push(currHit);
} else if ((currStart >= wage[i][1] && wage[i][1] >= currEnd) || (currStart < wage[i][1] && wage[i][1] > currEnd)) {
// Before starting a hit outside of a confirmed batch, add contents of batch to wages
//console.log(" single hit starting...");
if (batch.length >= 1) {
for (var i = 0; i < batch.length; i++) {
batchWage += batch[i];
}
batchWage = wageRound((batchWage / batch.length), 2);
currWages.push(batchWage);
currStart = wage[i][1];
currEnd = wage[i][2];
batch.push((wage[i][0]) / ((((currEnd - currStart) - wage[i][3]) / 1000) / 3600));
} else {
currStart = wage[i][1];
currEnd = wage[i][2];
batch.push((wage[i][0]) / ((((currEnd - currStart) - wage[i][3]) / 1000) / 3600));
}
}
}
if (batch.length >= 1) {
for (var i = 0; i < batch.length; i++) {
batchWage += batch[i];
}
batchWage = wageRound((batchWage / batch.length), 2);
currWages.push(batchWage);
batchWage = 0;
batch = [];
}
for (var i = 0; i < currWages.length; i++) {
// Add up all of the calculated wages in the current wages array
currWage += currWages[i];
}
// Find the rounded average of all wages in the array
currWage = wageRound((currWage / currWages.length), 2);
return currWage;
}
addTableElement = function (wage) { // Function to add the table cells to dashboard and displaying the wage
var belowThisTD = (($.inArray('Today\'s Projected Earnings', allTdhtml) > -1) ? /Today's Projected Earnings/ : /Total Earnings/); // If Projected Earnings script is installed, this will be sure to place is in the correct spot
var rowColor = (($.inArray('Today\'s Projected Earnings', allTdhtml) > -1) ? "#f1f3eb" : "FFFFFF"); // If Projected Earnings script is installed, this will ensure the correct color is used for the new row
for (var i = 0; i < allTds.length; i++) {
thisTd = allTds[i];
if (thisTd.innerHTML.match(belowThisTD) && thisTd.className.match(/metrics\-table\-first\-value/)) {
var row = document.createElement('tr');
row.className = "even";
row.setAttribute("style", ("background-color:" + rowColor));
var hourlyWageTitle = document.createElement('p');
hourlyWageTitle.setAttribute("name", "wageTitle");
hourlyWageTitle.innerHTML = "Today's Hourly Wage ";
hourlyWageTitle.setAttribute("style", ("background-color:" + rowColor));
var cellLeft = document.createElement('td');
cellLeft.className = "metrics-table-first-value";
cellLeft.appendChild(hourlyWageTitle);
row.appendChild(cellLeft);
var cellRight = document.createElement('td');
cellRight.setAttribute("name", "wageCell");
cellRight.innerHTML = "$" + wage + "/hr";
row.appendChild(cellRight);
thisTd.parentNode.parentNode.insertBefore(row, thisTd.parentNode.nextSibling);
}
}
$('p[name="wageTitle"]').append('');
}
// Dropdown specific functions and settings for the requester selector
$(document).on('change', '.wageDrop', function () {
console.log("changing changing");
var optionSelected = $('.wageDrop option:selected').text();
WageStorage.indexedDB.getReq(optionSelected);
});
// Function for legitimate rounding because javascript sucks at floating point
function wageRound(num, decimals) {
return Math.round(num * Math.pow(10, decimals)) / Math.pow(10, decimals);
}
// Timestamp when the HIT is accepted
$(window).ready(function () {
if (document.URL === "https://www.mturk.com/mturk/dashboard" || /preview\?/i.test(document.URL)) {
console.log("Nothing to do here...");
} // Don't record anything on dashboard, instead add appropriate element to tables
else {
wageStart = new Date().getTime(); // As soon as the window can run something, set the start time.
console.log("Starting HIT");
}
});
$(window).on('load', function () {
allTds = document.getElementsByTagName('td'); // As soon as the page is fully loaded, grab all of the td elements to an array
for (var i = 0; i < allTds.length; i++) {
allTdhtml.push(allTds[i].innerHTML);
}
if (/preview\?/i.test(document.URL)) {
if (/expired/i.test($('#alertboxHeader').text())) { // Remove previous HIT from database -- this one expired.
console.log(wageNix + " checker for expired hit");
WageStorage.indexedDB.delHit(GM_getValue("hitId"));
}
} else if (/accept\?/i.test(document.URL) || /previewandaccept\?/i.test(document.URL) || /continue\?/i.test(document.URL)) {
wageNix = (/could not/i.test($('#alertboxHeader').text())); // Set wageNix true if hit is gone
wageNix = (/expired/i.test($('#alertboxHeader').text())); // Set wageNix true if hit is expired
wageNix = (($('input[name="userCaptchaResponse"]').length > 0) ? true : false); // Set wageNix true if captcha
wageNix = (/There are no more available HITs in this group/i.test($('#alertboxMessage').text()));
console.log(wageNix + " checker for missing hit");
WageStorage.indexedDB.getHit(); // As soon as the apge is fully loaded, call function to check if HIT is already in DB
} else if (/dashboard/i.test(document.URL)) {
console.log("On the Dashboard, get wage...");
WageStorage.indexedDB.getWage();
}
});
// Detect if 'Return HIT' button has been clicked to record timer as zero-earnings.
// Timestamp when button was clicked to end time spent.
$('a[href*="/mturk/return?"]').on('click', function () {
wageLost = true;
wageEnd = new Date().getTime();
});
$('img[name="/submit"]').on('click', function () {
wageLost = false;
});
// Create database if not created yet
WageStorage.indexedDB.create();
// Detect the page unloading and react accordingly.
// If return button was clicked, record zero wage earnings, otherwise record earnings.
// Timestamp if submitted to end time spent.
$(window).on('beforeunload', function () {
if (wageLost) {
// ***** DO STUFF WHEN RETURN CLICKED *****
reward = 0;
WageStorage.indexedDB.addhit();
console.log("Wage lost due to return...");
} else if (wageStart === null || wageNix === true || /preview\?/i.test(document.URL) || /There are no more available HITs in this group/i.test($('#alertboxMessage').text())) {
// ***** THIS HIT IS GONE OR PREVIEWED *****
console.log("Either HIT expired or we were on a preview page");
} else {
// ***** DO STUFF WHEN SUBMITTED *****
console.log("This should fire no matter what, unless returned...");
wageEnd = new Date().getTime();
GM_setValue("hitId", hitId);
WageStorage.indexedDB.addhit();
}
});