/*===========================================================================*\
| The Amazon Review Tabulator - TART |
| (c) 2016 by Another Floyd |
| From your "Public Reviews Written by You" page on Amazon, this script |
| collects and tabulates vote tallies and related information, from all of |
| your Amazon reviews. Click the "Tabulate" link in the "Your Profile" |
| panel. Click the heart icon, for options. |
\*===========================================================================*/
// ==UserScript==
// @name The Amazon Review Tabulator - TART
// @namespace floyd.scripts
// @version 1.3.6
// @author Another Floyd at Amazon.com
// @description Lists all of your reviews with vote and comment tallies, with updates highlighted
// @include https://*amazon.com/gp/cdp/member-reviews*
// @include https://*amazon.co.uk/gp/cdp/member-reviews*
// @include https://*amazon.ca/gp/cdp/member-reviews*
// @include https://*amazon.com.au/gp/cdp/member-reviews*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_log
// @grant GM_openInTab
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js
// @require https://gf.qytechs.cn/scripts/20744-sortable/code/sortable.js?version=132520
// @require https://openuserjs.org/src/libs/sizzle/GM_config.js
// ==/UserScript==
// Start
(function() {
var showUpdatesOnly = false;
var primaryDisplayBuffer = "";
var updateDisplayBuffer = "";
var userID = "";
var reviewCount = 0;
var reviewerRanking = "";
var helpfulVotes = 0;
var oldStoreItemIDs = [];
var oldStoreUpvotes = [];
var oldStoreDownvotes = [];
var oldStoreComments = [];
var newStoreItemIDs = "";
var newStoreUpvotes = "";
var newStoreDownvotes = "";
var newStoreComments = "";
var tallyUpvotes = 0;
var tallyDownvotes = 0;
var tallyStars = 0;
var tallyComments = 0;
// use this reference for progress indicator
var profileDiv = "";
var profileDivOriginalHTML = "";
var profileDivTabulateHTML = "<br></br><a href='javascript:tabulate();'>Tabulate</a> <a href='javascript:options();' title='Click for TART options' style='text-decoration:none;font-size:135%;font-weight:bold'>" + String.fromCharCode(9829) + "</a>";
function assembleDisplayBuffers (completeSetOfTableRows, reviewsProcessed) {
var today = new Date();
var formattedToday = today.toLocaleDateString('en-US',{month:'long',day:'numeric',year:'numeric'});
var toggleLink = (GM_config.get('DisplayMode')) ? "<p><a href='javascript:toggleView();'>Toggle View: All Reviews | Updates Only</a>" : "";
var bMargin = (GM_config.get('FixedFooter')) ? "36" : "0";
// set up top of display page
primaryDisplayBuffer = "<!DOCTYPE html><html lang='en'>" +
"<head><meta charset='utf-8'/><title>TART Amazon Review Details</title>" +
"<style type='text/css'>" +
"body {font-family:Arial,sans-serif;font-size:" + GM_config.get('FontSize') + "px; margin:0; padding:0px 5px}" +
".tg {border-collapse:collapse;border-spacing:0;width:100%}" +
".tg td{padding:" + GM_config.get('RowPadding') + "px 4px; border-style:solid; border-width:1px; overflow:hidden; word-break:normal; font-size:" + GM_config.get('FontSize') + "px}" +
".tg th{padding:" + GM_config.get('RowPadding') + "px 4px; border-style:solid; border-width:1px; overflow:hidden; word-break:normal; font-size:" + GM_config.get('FontSize') + "px}" +
".tg .header-left{font-weight:bold;background-color:#010066;color:#ffffff;text-align:left}" +
".tg .header-right{font-weight:bold;background-color:#010066;color:#ffffff;text-align:right}" +
".tg .cell-left{text-align:left}" +
".tg .cell-right{text-align:right}" +
".tg .hilite-left{text-align:left;background-color:#FFFF12}" +
".tg .hilite-right{text-align:right;background-color:#FFFF12}" +
".txtLarge {font-size:18px;font-weight:bold}" +
".tableLink, .tableLink:link, .tableLink:visited {text-decoration:none; font-weight:bold; font-size:110%; color:#000000}" +
".footerLink, .footerLink:link, .footerLink:visited {text-decoration:none; font-weight:bold; color:#ffffff}" +
"table.sortable th.sorted {background-color:#000000}" +
"#footer {position: fixed;bottom: 0}" +
"</style></head><body>" +
"<span class='txtLarge'>Amazon Review Details</span><br>" +
"Prepared with <a href='https://gf.qytechs.cn/en/scripts/24434-the-amazon-review-tabulator-tart' target='_new'>TART</a> - " + formattedToday +
"<p>Reviewer Ranking: " + reviewerRanking + "<br>" +
"Review Count: " + reviewCount + "<br>" +
"Helpful Votes: " + helpfulVotes + "<br>" +
"Upvote/Review Ratio: " + (helpfulVotes/reviewCount).toFixed(2) + toggleLink +
"</p><table class='tg sortable' style='margin-bottom:" + bMargin + "px'>" +
"<thead><tr>" +
"<th class='header-left sort-number sort-default' style='width:5%'>#</th>" +
"<th class='header-left sort-text' style='width:42%'>Item</th>" +
"<th class='header-left sort-date' style='width:12%'>Date</th>" +
"<th class='header-right sort-number' style='width:6%'>Stars</th>" +
"<th class='header-right sort-number' style='width:8%'>Upvotes</th>" +
"<th class='header-right sort-number' style='width:10%'>Downvotes</th>" +
"<th class='header-right sort-number' style='width:8%'>% Helpful</th>" +
"<th class='header-right sort-number' style='width:9%'>Comments</th></tr></thead><tbody>";
// had to go with fixed % widths on the columns, in order to have second table
// with fixed footers... that would have matching column widths
updateDisplayBuffer = primaryDisplayBuffer; // both displays have same top section
primaryDisplayBuffer += completeSetOfTableRows;
// info needed in footer
var calcStars = (tallyStars/reviewsProcessed).toFixed(1);
var calcHelpfulPct = helpfulPercent(tallyUpvotes,tallyDownvotes);
var newStoreFooter = calcStars + " " + tallyUpvotes + " " + tallyDownvotes + " " + calcHelpfulPct + " " + tallyComments;
var oldStoreFooter = GM_getValue("recentFooterValues", " ").split(" "); // read 'em
GM_setValue("recentFooterValues", newStoreFooter.trim()); // write 'em with new values
// prep footer; cell widths are superfluous if footer will not be fixed
var tableFooter = "<tfoot><tr><td class='header-left' style='width:5%'> </td>" +
"<td class='header-left' style='width:42%'> </td>" +
"<td class='header-left' style='width:12%'> </td>" +
"<td class='header-right' style='width:6%'><a href='javascript: void(0)' class='footerLink' title='Previous: " + oldStoreFooter[0] + "'>" + calcStars + "</a></td>" +
"<td class='header-right' style='width:8%'><a href='javascript: void(0)' class='footerLink' title='Previous: " + oldStoreFooter[1] + "'>" + tallyUpvotes + "</a></td>" +
"<td class='header-right' style='width:10%'><a href='javascript: void(0)' class='footerLink' title='Previous: " + oldStoreFooter[2] + "'>" + tallyDownvotes + "</a></td>" +
"<td class='header-right' style='width:8%'><a href='javascript: void(0)' class='footerLink' title='Previous: " + oldStoreFooter[3] + "'>" + calcHelpfulPct + "</a></td>" +
"<td class='header-right' style='width:9%'><a href='javascript: void(0)' class='footerLink' title='Previous: " + oldStoreFooter[4] + "'>" + tallyComments + "</a></td>" + "</tr></tfoot>";
// add footer either to be fixed at bottom of screen, or normal
if(GM_config.get('FixedFooter')) {
primaryDisplayBuffer += "</tbody></table><table class='tg' id='footer' style='width:calc(100% - 10px)'>" + tableFooter + "</table></body></html>"; // fixed, by virtue of the styled 'footer' id
}
else {
primaryDisplayBuffer += "</tbody>" + tableFooter + "</table></body></html>"; // normal
}
// get rows containing updated reviews, only
var tempDiv = document.createElement('div');
tempDiv.innerHTML = primaryDisplayBuffer;
var findUpdateRows = document.evaluate("//td[@class='hilite-left']/..", tempDiv, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for(var d = 0; d < findUpdateRows.snapshotLength; d++) {
updateDisplayBuffer += findUpdateRows.snapshotItem(d).outerHTML;
}
updateDisplayBuffer += "</tbody></table></body></html>";
}
function tabulate() {
// reset global accumulators to ensure that repeated script runs remain clean
newStoreItemIDs = "";
newStoreUpvotes = "";
newStoreDownvotes = "";
newStoreComments = "";
tallyUpvotes = 0;
tallyDownvotes = 0;
tallyStars = 0;
tallyComments = 0;
// read in stored info from past run, for use in change detection
oldStoreItemIDs = GM_getValue("recentItemIDs", "").split(" ");
oldStoreUpvotes = GM_getValue("recentUpvotes", "").split(" ");
oldStoreDownvotes = GM_getValue("recentDownvotes", "").split(" ");
oldStoreComments = GM_getValue("recentComments", "").split(" ");
// prepare url with country domain and user ID, ready for review page number
var tld = "com";
var url = window.location.href;
if(url.indexOf('amazon.co.uk/') > -1) tld = "co.uk";
if(url.indexOf('amazon.ca/') > -1) tld = "ca";
if(url.indexOf('amazon.com.au/') > -1) tld = "com.au";
var urlStart = "https://www.amazon." + tld + "/gp/cdp/member-reviews/" + userID + "?ie=UTF8&display=public&page=";
var urlEnd = "&sort_by=MostRecentReview";
// space and counters for incoming data
var perPageResponseDiv = [];
var pageSetOfTableRows = [];
var pageResponseCount = 0;
var reviewsProcessed = 0;
var pageCount = Math.floor(reviewCount / 10) + ((reviewCount % 10 > 0) ? 1 : 0);
//var pageCount = 2; // for testing
// initialize the progress indicator
// sort of pre-redundant to do this here AND in the loop, but,
// looks better, if there is a lag before the first response
var progressHTML = "<br></br><b>" + pageCount + "</b>";
profileDiv.innerHTML = profileDivOriginalHTML + progressHTML;
// download and process Amazon pages
var receivedPageWithNoReviews = false;
var x = 1;
while (x <= pageCount) {
(function(x){
var urlComplete = urlStart + x + urlEnd;
perPageResponseDiv[x] = document.createElement('div');
GM_xmlhttpRequest({
method: "GET",
url: urlComplete,
onload: function(response) {
// capture incoming data
perPageResponseDiv[x].innerHTML = response.responseText;
pageResponseCount++;
// update the progress indicator
var progressHTML = "<br></br><b>" + (pageCount - pageResponseCount) + "</b>";
profileDiv.innerHTML = profileDivOriginalHTML + progressHTML;
// get parent of any reviewText DIV
var findReviews = document.evaluate("//div[@class='reviewText']/..", perPageResponseDiv[x], null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); // evaluating the doc DIV made above
var reviewsOnPage = findReviews.snapshotLength;
if(reviewsOnPage == 0) receivedPageWithNoReviews = true;
// process each review found on current page
pageSetOfTableRows[x] = ""; // initialize each member prior to concatenating
for (var j = 0; j < reviewsOnPage; j++) {
var oneReview = findReviews.snapshotItem(j);
var reviewChildren = oneReview.children;
var childCount = reviewChildren.length;
var commentCount = 0;
var itemTitle = "No Title Available";
var itemLink = "";
var permaLink = "";
var starRating = 0;
var reviewDate = "";
var upVotes = 0;
var downVotes = 0;
var itemID = "";
// get number of comments, and permalink
var tempText = reviewChildren[childCount-2].textContent;
if(tempText.indexOf('Comment (') > -1 || tempText.indexOf('Comments (') > -1) {
var paren1 = tempText.indexOf('(');
var paren2 = tempText.indexOf(')');
commentCount = tempText.substring(paren1+1,paren2);
commentCount = parseInt(commentCount.replace(/,/g, '')); // remove commas
}
var lst = reviewChildren[childCount-2].getElementsByTagName('a');
permaLink = lst[2].getAttribute("href");
// the data items below do not have reliable positions, due to presence
// or not, of vine voice tags, verified purchase, votes, etc.
// so, are done in a loop with IF checks. Must start loop just above review
// text, in case the reviewer has used any of the phrases I am searching for
for (var i = childCount - 4; i > -1; i--) {
var childHTML = reviewChildren[i].innerHTML; // used 2x, below
// get item title and item link
var titleClue = childHTML.indexOf('This review is from');
if(titleClue > -1) {
var lst = reviewChildren[i].getElementsByTagName('a');
itemLink = lst[0].getAttribute("href");
itemTitle = lst[0].textContent;
}
// get star rating AND review date
var ratingClue = childHTML.indexOf('out of 5 stars');
if(ratingClue > -1) {
starRating = childHTML.substring(ratingClue-4,ratingClue-1);
reviewDate = reviewChildren[i].lastElementChild.textContent;
var lst = reviewDate.split(" ");
reviewDate = lst[0].substring(0,3) + " " + lst[1] + " " + lst[2];
}
// get vote counts
var childText = reviewChildren[i].textContent;
var voteClue = childText.indexOf('people found the following review helpful');
if(voteClue > -1) {
var list = childText.trim().split(" "); // there were extra, invisible spaces!
upVotes = parseInt(list[0].replace(/,/g, '')); // remove commas
var totalVotes = parseInt(list[2].replace(/,/g, ''));
downVotes = totalVotes - upVotes;
}
}
// get item ID
var lst = oneReview.parentNode.getElementsByTagName('a');
itemID = lst[0].getAttribute("name");
// get HTML formatted table row
pageSetOfTableRows[x] += prepOneTableRow((j+1+(x-1)*10),itemID,itemTitle,permaLink,reviewDate,starRating,upVotes,downVotes,commentCount);
// clear the response, to save memory --
// could be critical when there are many review pages
perPageResponseDiv[x].innerHTML = "";
reviewsProcessed++; // more reliable than reviewCount, for calculating avg. rating
}
// see if all data from multiple page loads has arrived
if(pageResponseCount==pageCount) {
// assemble the sets of table rows
var completeSetOfTableRows = "";
for(var y=1; y <= pageCount; y++) {
completeSetOfTableRows += pageSetOfTableRows[y];
}
assembleDisplayBuffers(completeSetOfTableRows, reviewsProcessed);
// store info to be used in subsequent run, for change detection
GM_setValue("recentItemIDs", newStoreItemIDs.trim());
GM_setValue("recentUpvotes", newStoreUpvotes.trim());
GM_setValue("recentDownvotes", newStoreDownvotes.trim());
GM_setValue("recentComments", newStoreComments.trim());
// replace progress indicator with Tabulate link
profileDiv.innerHTML = profileDivOriginalHTML + profileDivTabulateHTML;
// show message if any of the received pages contained NO reviews...
// SOMETHING was received -- an empty, error, or 'please try again' type page
if(receivedPageWithNoReviews) {
alert("A page or more of reviews was not received. \n\nReview the results, anyway, because any highlighted updates will not be highlighted in the next run. \n\nAlso, any reviews missing from the results will be highlighted in the next run, as 'new' reviews.");
}
// --- a display alternative - put results IN the Amazon page
//document.body.innerHTML = primaryDisplayBuffer;
// --- to open new window, user must allow popups for https://www.amazon.com
//var resultsWindow = window.open("data:text/html," + encodeURIComponent(primaryDisplayBuffer), "_blank", "scrollbars=yes");
// --- display using GM_openInTab; does not require exception to be set by user
if(!GM_config.get('DisplayMode')) GM_openInTab("data:text/html," + encodeURIComponent(primaryDisplayBuffer));
else document.body.innerHTML = primaryDisplayBuffer;
}
}
});
})(x);
x++;
}
}
function toggleView() {
showUpdatesOnly = !showUpdatesOnly;
if(showUpdatesOnly) document.body.innerHTML = updateDisplayBuffer;
else document.body.innerHTML = primaryDisplayBuffer;
}
function helpfulPercent(upVotes,downVotes) {
var helpfulPercent = "";
upVotes = upVotes;
downVotes = downVotes;
if(upVotes + downVotes > 0) helpfulPercent = (upVotes/(upVotes+downVotes)*100).toFixed(1);
return helpfulPercent;
}
function prepOneTableRow (row,itemID,itemTitle,permaLink,reviewDate,starRating,upVotes,downVotes,commentCount) {
// do these before mangling the values with <b> tags </b>
var helpfulPct = helpfulPercent(upVotes,downVotes);
itemTitle = "<a href='" + permaLink + "' target='_new'>" + itemTitle.substring(0,55) + "</a>";
// keep tallies to use in table footer
tallyUpvotes += upVotes;
tallyDownvotes += downVotes;
tallyStars += parseInt(starRating);
tallyComments += commentCount;
// assemble storage info, to use in subsequent run, for change detection
newStoreItemIDs += itemID + " ";
newStoreUpvotes += upVotes + " ";
newStoreDownvotes += downVotes + " ";
newStoreComments += commentCount + " ";
// see if review for this item has previously been examined
var matchIdx = -1;
for(var i=0; i<oldStoreItemIDs.length; i++) {
if(oldStoreItemIDs[i] == itemID) {
// we have a match, an item that has previously been seen
matchIdx = i;
break;
}
}
var hiliteRow = false;
if(matchIdx > -1) {
// entry exists; see if any of the numbers have changed
if(oldStoreUpvotes[matchIdx] != upVotes) {
// for changed number, make it bold, and hilite row
// and store previous value for display as tooltip, for mouse hover
upVotes = "<a href='javascript: void(0)' class='tableLink' title='Previous: " + oldStoreUpvotes[matchIdx] + "'>" + upVotes + "</a>";
hiliteRow = true;
}
if(oldStoreDownvotes[matchIdx] != downVotes) {
downVotes = "<a href='javascript: void(0)' class='tableLink' title='Previous: " + oldStoreDownvotes[matchIdx] + "'>" + downVotes + "</a>";
hiliteRow = true;
}
if(oldStoreComments[matchIdx] != commentCount) {
commentCount = "<a href='javascript: void(0)' class='tableLink' title='Previous: " + oldStoreComments[matchIdx] + "'>" + commentCount + "</a>";
hiliteRow = true;
}
}
else {
// no match, so, it's a new review; bold the title and hilite the row
itemTitle = "<b>" + itemTitle + "</b>";
hiliteRow = true;
}
var tdLeft = "<td class='cell-left'>";
var tdRight = "<td class='cell-right'>";
if(hiliteRow===true && oldStoreItemIDs[0].length > 0) {
tdLeft = "<td class='hilite-left'>";
tdRight = "<td class='hilite-right'>";
}
var tableRow = "<tr>" + tdLeft + row + "</td>" + tdLeft + itemTitle + "</td>" + tdLeft + reviewDate + "</td>" + tdRight + starRating + "</td>" + tdRight + upVotes + "</td>" + tdRight + downVotes + "</td>" + tdRight + helpfulPct + "</td>" + tdRight + commentCount + "</td></tr>";
return tableRow;
}
// create Options menu with GM_config
var frame = document.createElement('div');
document.body.appendChild(frame);
GM_config.init(
{
'id': 'MyConfig', // The id used for this instance of GM_config
'title': 'TART Options', // Panel Title
'fields': // Fields object
{
'DisplayMode': // Line item
{
'type': 'checkbox',
'label': 'Enhanced display (uncheck for new tab with fewer features)',
'default': true
},
'FixedFooter':
{
'type': 'checkbox',
'label': 'Show fixed footer at bottom of screen',
'default': true
},
'FontSize':
{
'label': 'Text size',
'type': 'unsigned int',
'size': 2,
'default': 12
},
'RowPadding':
{
'label': 'Row padding',
'type': 'unsigned int',
'size': 2,
'default': 10
}
},
'events': // Callback functions object
{
'open': function() {
// style the panel as it's being displayed
frame.style.position = "auto";
frame.style.width = "auto";
frame.style.height = "auto";
frame.style.backgroundColor = "#F3F3F3";
frame.style.padding = "10px";
frame.style.borderWidth = "5px";
frame.style.borderStyle = "ridge";
frame.style.borderColor = "gray";
var x = (document.documentElement.clientWidth - frame.offsetWidth) / 2;
frame.style.left = x + 'px';
}
},
'frame': frame, // specify the DIV element used for the panel
'css': '#MyConfig .config_header { font-size: 12pt; font-weight:bold; margin-bottom:12px }' +
'#MyConfig .field_label { font-size: 12px; font-weight:normal; margin: 0 3px }'
});
// event listener to pick up mouse clicks, to run script functions
document.addEventListener('click', function(event) {
var tempstr = new String(event.target);
var quash = false;
if(tempstr.indexOf('tabulate') > -1) {
tabulate();
quash = true;
}
if(tempstr.indexOf('options') > -1) {
GM_config.open();
quash = true;
}
if(tempstr.indexOf('toggleView') > -1) {
toggleView();
quash = true;
}
if(quash) {
event.stopPropagation();
event.preventDefault();
}
}, true);
function main() {
var findProfileLink = "";
var url = window.location.href;
try {
// find reviewer ID
if(url.indexOf('amazon.com/') > -1) {
// for Amazon US
findProfileLink = document.evaluate("//b[contains(.,'Your Profile')]/a", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
else {
// Amazon UK, CA, AU
findProfileLink = document.evaluate("//a[contains(.,'Customer Reviews')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
}
// and, the following lines are ok for US and UK, but, maybe not for others
var profileLink = findProfileLink.snapshotItem(0).getAttribute("href");
var lst = profileLink.split("/");
userID = lst[4];
// find profile info panel
var findDiv = document.evaluate("//div[contains(.,'Helpful Votes')]", document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
profileDiv = findDiv.snapshotItem(0);
// get reviewer ranking and helpful votes
var lst = profileDiv.textContent.split(" ");
reviewerRanking = lst[4].substring(9);
helpfulVotes = lst[9].substring(7);
// get review count
var prevSibDiv = profileDiv.previousElementSibling;
charIdx = prevSibDiv.textContent.lastIndexOf(':');
reviewCount = prevSibDiv.textContent.substring(charIdx+2);
// remove any commas, though has not been necessary w/thousands of reviews
reviewCount = parseInt(reviewCount.replace(/,/g, ''));
// add Tabulate link; also, save content for use with progress indicator
profileDivOriginalHTML = profileDiv.innerHTML;
profileDiv.innerHTML += profileDivTabulateHTML;
}
catch(err) {
alert("TART: Profile information was not found in the structure expected");
}
// for trouble-shooting; these display in the user's Browser Console,
// typically on the menu under Tools/Web Developer, or some such
GM_log("User ID: " + userID);
GM_log("Reviewer Ranking: " + reviewerRanking );
GM_log("Helpful Votes: " + helpfulVotes);
GM_log("Review Count: " + reviewCount);
}
main();
})();
// End