Letterboxd External Ratings

Adds ratings of film from external sites to film pages

// ==UserScript==
// @name        Letterboxd External Ratings
// @namespace   https://github.com/soyguijarro/userscripts
// @description Adds ratings of film from external sites to film pages
// @copyright   2015, Ramón Guijarro (http://soyguijarro.com)
// @homepageURL https://github.com/soyguijarro/userscripts
// @supportURL  https://github.com/soyguijarro/userscripts/issues
// @icon        https://raw.githubusercontent.com/soyguijarro/userscripts/master/img/letterboxd_icon.png
// @license     GPLv3; http://www.gnu.org/licenses/gpl.html
// @version     1.8
// @include     *://letterboxd.com/film/*
// @include     *://letterboxd.com/film/*/crew/*
// @include     *://letterboxd.com/film/*/studios/*
// @include     *://letterboxd.com/film/*/genres/*
// @exclude     *://letterboxd.com/film/*/views/*
// @exclude     *://letterboxd.com/film/*/lists/*
// @exclude     *://letterboxd.com/film/*/likes/*
// @exclude     *://letterboxd.com/film/*/fans/*
// @exclude     *://letterboxd.com/film/*/ratings/*
// @exclude     *://letterboxd.com/film/*/reviews/*
// @grant       GM_addStyle
// @grant       GM_xmlhttpRequest
// @grant       unsafeWindow
// ==/UserScript==

var ratingsData = { "IMDb": {origRatingMax: 10, isLoaded: false},
                    "Metascore": {origRatingMax: 100, isLoaded: false},
                    "Tomatometer": {isLoaded: false} };

function updateRatingElt(site) {
    var ratingElts = document.querySelectorAll("section.ratings-external a"),
        ratingElt = ratingElts[Object.keys(ratingsData).indexOf(site)],
        ratingInnerElt = ratingElt.firstElementChild,
        ratingData = ratingsData[site];

    if (ratingData.isLoaded) {
        ratingInnerElt.classList.remove("spinner");

        if (ratingData.origRating && ratingData.origRating !== "" &&
            ratingData.origRating !== 0 && !isNaN(ratingData.origRating)) {
            if (localStorage.origRatingsMode === "true") {
                ratingInnerElt.removeAttribute("class");
                ratingInnerElt.textContent = ratingData.origRating +
                    ((ratingData.origRatingMax) ? ("/" + ratingData.origRatingMax) : "%");
            } else {
                ratingInnerElt.className = "rating rated-" +
                    Math.round(ratingData.oneToTenRating);
            }
            ratingElt.href = ratingData.url;
            ratingElt.style.cursor = "pointer";
        } else {
            ratingInnerElt.removeAttribute("class");
            ratingInnerElt.textContent = "N/A";
        }
    }
}

function createRatingsSection(callback) {
    var sidebarElt = document.getElementsByClassName("sidebar")[0],
        ratingsSectionElt = document.createElement("section"),
        modeToggleElt = document.createElement("ul"),
        modeToggleInnerElt = document.createElement("li"),
        modeToggleInnerInnerElt = document.createElement("a"),
        ratingElt,
        ratingInnerElt,
        cssRules = "section.ratings-external {\
                        margin-top: 20px;\
                    }\
                    section.ratings-external a {\
                        display: block;\
                        font-size: 12px;\
                        line-height: 1.5;\
                        margin-bottom: 0.5em;\
                    }\
                    section.ratings-external span {\
                        text-align: right;\
                        position: absolute;\
                        right: 0;\
                        color: #6C3;\
                    }\
                    section.ratings-external span.spinner {\
                        background: url('" + getSpinnerImageUrl() + "');\
                        height: 12px;\
                        width: 12px;\
                        margin: 3px 0;\
                    }";

    function getSpinnerImageUrl() {
        var spinnersObj = unsafeWindow.globals.spinners;

        for (var prop in spinnersObj) {
            if (/spinner_12/.test(prop)) {
                return spinnersObj[prop];
            }
        }
        return null;
    }

    function getModeToggleButtonText() {
        var ratingsModeName =
            (localStorage.origRatingsMode === "true") ? "five-star" : "original";

        return "Show " + ratingsModeName + " ratings";
    }

    function toggleRatingsMode(evt) {
        evt.preventDefault();

        localStorage.origRatingsMode = !(localStorage.origRatingsMode === "true");
        modeToggleInnerInnerElt.textContent = getModeToggleButtonText();

        for (var i = 0; i < Object.keys(ratingsData).length; i++) {
            updateRatingElt(Object.keys(ratingsData)[i]);
        }
    }

    // Set up section to be inserted in page
    ratingsSectionElt.className = "section ratings-external";

    // Set up section elements that will contain ratings
    for (var i = 0; i < Object.keys(ratingsData).length; i++) {
        ratingElt = document.createElement("a");
        ratingInnerElt = document.createElement("span");

        ratingElt.textContent = Object.keys(ratingsData)[i];
        ratingElt.className = "rating-green";
        ratingInnerElt.className = "spinner";
        ratingElt.style.cursor = "default";

        ratingElt.appendChild(ratingInnerElt);
        ratingsSectionElt.appendChild(ratingElt);
    }

    // Set up ratings mode toggle button
    modeToggleElt.className = "box-link-list box-links";
    modeToggleInnerInnerElt.className = "box-link";
    modeToggleInnerInnerElt.href = "#";
    modeToggleInnerInnerElt.textContent = getModeToggleButtonText();
    modeToggleInnerInnerElt.addEventListener("click", toggleRatingsMode, false);
    modeToggleInnerElt.appendChild(modeToggleInnerInnerElt);

    modeToggleElt.appendChild(modeToggleInnerElt);
    ratingsSectionElt.appendChild(modeToggleElt);

    // Insert section in page
    sidebarElt.insertBefore(ratingsSectionElt, sidebarElt.lastElementChild);
    GM_addStyle(cssRules);

    callback();
}

function fillRatingsSection() {
    var moreDetailsElt = document.querySelector("section.col-main p.text-link"),
        imdbIdMatch = moreDetailsElt.innerHTML.
            match(/http:\/\/www\.imdb.com\/title\/tt(\d+)\//),
        rottenApiReqBaseUrl = "http://api.rottentomatoes.com/api/public/v1.0/",
        rottenApiReqParams = "movie_alias.json?type=imdb&id=",
        rottenApiReqUrl,
        imdbUrl,
        imdbId;

    function updateRatingData(site, origRating, oneToTenRating, url) {
        ratingsData[site].origRating = origRating;
        ratingsData[site].oneToTenRating = oneToTenRating;
        ratingsData[site].url = url;
        ratingsData[site].isLoaded = true;

        updateRatingElt(site);
    }

    function getIMDbAndMetaRatings(res) {
        var parser = new DOMParser(),
            dom = parser.parseFromString(res.responseText, "text/html"),
            ratingsElt = dom.getElementById("title-overview-widget");

        function getIMDbRating() {
            var imdbRating,
                imdbRatingElt = ratingsElt.querySelector("span[itemprop=ratingValue]");

            if (imdbRatingElt) {
                imdbRating = parseFloat(imdbRatingElt.textContent);
                updateRatingData("IMDb", imdbRating, imdbRating, imdbUrl);
            } else {
                updateRatingData("IMDb", null);
            }
        }

        function getMetaRating() {
            var metaRating,
                metaRatingElt = ratingsElt.querySelector(".metacriticScore span");

            if (metaRatingElt) {
                metaRating = parseFloat(metaRatingElt.textContent);

                GM_xmlhttpRequest({
                    method: "GET",
                    url: imdbUrl + "criticreviews", // Metacritic reviews page on IMDb
                    onload: function (res) {
                        var pageContent,
                            metaUrl;

                        dom = parser.parseFromString(res.responseText, "text/html");
                        pageContent = dom.getElementById("main").innerHTML;
                        metaUrl = pageContent.
                            match(/<a.*href="(.*?)".*>See all \d+ reviews/)[1];

                        updateRatingData("Metascore", metaRating,
                            metaRating / 10, metaUrl);
                    }
                });
            } else {
                updateRatingData("Metascore", null);
            }
        }

        if (ratingsElt) {
            getIMDbRating();
            getMetaRating();
        } else {
            updateRatingData("IMDb", null);
            updateRatingData("Metascore", null);
        }
    }

    function getRottenRating(res) {
        var json = JSON.parse(res.responseText),
            rottenId,
            rottenUrl,
            rottenRating;

        if (json) {
            if (json.id && json.ratings && !json.error) {
                rottenUrl = "http://www.rottentomatoes.com/m/" + json.id;
                rottenRating = json.ratings.critics_score;

                if (rottenRating > 0) {
                    updateRatingData("Tomatometer", rottenRating,
                        rottenRating / 10, rottenUrl);
                } else {
                    updateRatingData("Tomatometer", null);
                }
            } else {
                updateRatingData("Tomatometer", null);
            }
        }
    }

    if (imdbIdMatch) {
        imdbUrl = imdbIdMatch[0];
        imdbId = imdbIdMatch[1];
        rottenApiReqUrl = rottenApiReqBaseUrl + rottenApiReqParams + imdbId;

        GM_xmlhttpRequest({
            method: "GET",
            url: imdbUrl,
            onload: getIMDbAndMetaRatings
        });

        GM_xmlhttpRequest({
            method: "GET",
            url: rottenApiReqUrl,
            onload: getRottenRating
        });
    } else {
        updateRatingData("IMDb", null);
        updateRatingData("Metascore", null);
        updateRatingData("Tomatometer", null);
    }
}

localStorage.origRatingsMode = (localStorage.origRatingsMode || true);
createRatingsSection(fillRatingsSection);