Greasy Fork

Greasy Fork is available in English.

Apple Trailer Download HD+

Download movie trailers from the Apple iTunes Movie Trailers site

当前为 2016-01-20 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            Apple Trailer Download HD+
// @namespace       http://www.digitaledgestudios.nl/
// @author          mhu
// @description     Download movie trailers from the Apple iTunes Movie Trailers site
// @include         http://trailers.apple.com/trailers/*/*
// @exclude         http://trailers.apple.com/trailers/*/*/gallery/*
// @version         2.0.23
// @require         http://greasyfork.icu/libraries/GM_config/20131122/GM_config.js
// @grant           GM_addStyle
// @grant           GM_xmlhttpRequest
// @grant           GM_registerMenuCommand
// @grant           GM_setClipboard
// @noframes
// ==/UserScript==


// based on jQuery.browser
browserInfo = function () {
    var browser = {},
        uaMatch = function (ua) {
            ua = ua.toLowerCase();

            var match = /(chrome)[ \/]([\w.]+)/.exec(ua) ||
                /(webkit)[ \/]([\w.]+)/.exec(ua) ||
                /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) ||
                /(msie) ([\w.]+)/.exec(ua) ||
                /(trident)[ \/](?:.*? rv:([\w.]+))/.exec(ua) ||
                ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) ||
                [];

            return {
                browser: match[1] || "",
                version: match[2] || "0"
            };
        },
        matched = uaMatch(navigator.userAgent);

    browser.version = 0;
    if (matched.browser) {
        browser[matched.browser] = true;
        browser.version = parseFloat(matched.version);

        if (browser.trident) {
            // IE11+
            browser.msie = true;
        }

        // Chrome is Webkit, but Webkit is also Safari.
        if (browser.chrome) {
            browser.webkit = true;
        } else if (browser.webkit) {
            browser.safari = true;
        }
    }

    return browser;
}();

//
// Workaround for GM_xmlhttpRequest with GreaseMonkey 1.x on Firefox 32
// Cheating GM_xmlhttpRequest leaking checking use arguments which could be converted to false.
// https://gist.github.com/tiansh/bbe60ec5c9c0531643db
//
var GM_xmlhttpRequest = (function () {
    var old = GM_xmlhttpRequest;

    // only install patch on Firefox 32
    if (!browserInfo.mozilla || browserInfo.version != 32) {
        return old;
    }

    return function (details) {
        var i, x = new Number(0);
        for (i in details) {
            if (details.hasOwnProperty(i)) {
                x[i] = details[i];
            }
        }
        return old(x);
    };
}());

(function() {
    "use strict";

    // constants
    var TITLE = 'Apple Trailer Download HD+',
        ERROR_MSG = 'No downloadable trailers found',
        CLICK_MSG = 'Please click the Watch the trailer link',
        APPLEERRORINDICATORS = /(class="page-errors"|pageType="errorPage")/i,
        APPLEERRORINDICATOR_OLD = /pageType="errorPage"/i,
        SIZES = [ 'Small (480p)', 'Normal (720p)', 'Large (1080p)' ],

    // globals
        g_allLinks = {"480p": [], "720p": [], "1080p": []},
        g_atdcontainer,
        g_baseUrl;

    /**
    * This is where we start. Inject styles.
    * Will run automatically after the load event has fired.
    */
    function initScript() {
        var css = '#atdContainer {background-color: rgba(0,0,0, 0.9); background-image:-moz-linear-gradient(right, #B5B5B5, #7D7D7D); background-image:-webkit-linear-gradient(right, #B5B5B5, #7D7D7D); background-image:linear-gradient(right, #B5B5B5, #7D7D7D); border:2px solid white; border-radius:5px; bottom:10px; box-shadow:0 -1px 5px rgba(0, 0, 0, 0.75); color:#333; display:table; font-family:Arial,sans-serif; height:35px; max-width:200px; min-width:120px; opacity:.9; padding:0 5px; position:fixed; right:10px; text-align:left; z-index:900;} ' +
            '#atdHeader { background-color: rgba(0,0,0, 1); background-image: -moz-linear-gradient(right, rgba(120, 120, 120, 0.55), rgba(75, 75, 75, 0.75)); background-image: -webkit-linear-gradient(right, rgba(120, 120, 120, 0.55), rgba(75, 75, 75, 0.75)); background-image: linear-gradient(right, rgba(120, 120, 120, 0.55), rgba(75, 75, 75, 0.75)); border-top-left-radius: 5px; border-top-right-radius: 5px; box-shadow:-1px -1px 2px black inset; margin: 0 -5px; padding: 2px 3px 5px; text-align: center; } ' +
            '#atdHeaderLink { color: #FFFFFF; font: small-caps bold 0.9em/1.1em helvetica,arial,sans-serif; text-shadow: 1px 0 rgba(0, 0, 0, 0.9); } ' +
            '#atdContainer li {display: list-item; font-size:1em; line-height:1.4em; padding-left:17px; background:url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAABl0RVh0U29mdHdhcmUAUGFpbnQuTkVUIHYzLjUuODc7gF0AAAGgSURBVDhPhZM7SwNBEMddc/EJooVgJzYWEcFKRFCwEJtgIX6Ha+y0tbaSkMs7IYQQkiYISaGSQjvFBzZ+ABFBfFRpBOWQ+JuQC3e58/Jrlpnd+c/s7Kwa6CGTyShd11uWG3vWNM1JpVRT07QQe+e2vfHeeIedTqeHU6nUxiWcQiKRmEdQk0OsAUnmCBCH5ZQDBITuoQXXEIvFJsTfEZjuCtiVksmkMgxjPRKJLBaLxf1feAUqWUZASVWemUWVjKpSqeg3Hc7gER7gDhqNxgnBq4is2bOPYAQluFarHX6BlCzQPNNui+8WOLvgqAJFuXvgDaxgr1XEqtXqLteRpEPdBuJQpVJpSjL6CUhD6U/76Vx9KJfLoX4CVxCNRtvP5xCQCuh4+Bv8KviAbDa75Tk89OHgBfwEZO8ZEJlzicTj8fAF9BN4B2sauyI4BqlghvVIhuY/ERmqer1+zDO6+yBqTGGQQTI+4QeeQAapCWLLOBM8Zb2C6yU6ImO5XG67UCjs0Fydq+3l8/kV7CXsTZkZm0BA3nNUJtH3W3p8eetH/gFubA10E1qWsgAAAABJRU5ErkJggg==") no-repeat scroll -2px 1px transparent;} ' +
            '#atdContainer li > a {color:#0F0F1F; font-weight:normal; font-size:0.9em; text-decoration:none;vertical-align: middle;cursor:pointer; } ' +
            '#atdContainer li.error {color:#D90000; font-weight:normal; font-size:0.9em; text-decoration:none;} ' +
            '#atd480p, #atd720p, #atd1080p, #atdLoader {box-shadow:-1px -1px 3px #000000 inset;} ' +
            '#atd1080p {padding-bottom:4px; } ' +
            '#atdContainer > div:not(.toggle) {margin:0 -5px; padding:3px 5px;} ' +
            '#atdContainer .toggle {cursor:pointer;border: thin solid black; box-shadow: 0px -1px 3px rgba(255, 255, 255, 0.25) inset; color: white; font-size: 1em; font-weight: bold; margin: 0 -5px; padding: 0 5px 0 0; text-align: right;} ' +
            '#atdContainer .toggle:hover {margin: 0 -6px; box-shadow:0 2px 5px rgba(0, 0, 0, 0.75);} ' +
            '#atdContainer .toggle:last-of-type {margin-bottom:1px;} ' +
            '#atdLoader {background:url("data:image/gif;base64,R0lGODlhKwALAPEAAP///wAAAIKCggAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAKwALAAACMoSOCMuW2diD88UKG95W88uF4DaGWFmhZid93pq+pwxnLUnXh8ou+sSz+T64oCAyTBUAACH5BAkKAAAALAAAAAArAAsAAAI9xI4IyyAPYWOxmoTHrHzzmGHe94xkmJifyqFKQ0pwLLgHa82xrekkDrIBZRQab1jyfY7KTtPimixiUsevAAAh+QQJCgAAACwAAAAAKwALAAACPYSOCMswD2FjqZpqW9xv4g8KE7d54XmMpNSgqLoOpgvC60xjNonnyc7p+VKamKw1zDCMR8rp8pksYlKorgAAIfkECQoAAAAsAAAAACsACwAAAkCEjgjLltnYmJS6Bxt+sfq5ZUyoNJ9HHlEqdCfFrqn7DrE2m7Wdj/2y45FkQ13t5itKdshFExC8YCLOEBX6AhQAADsAAAAAAAAAAAA=") center 15px no-repeat;} ' +
            '#atdLoaderText {font-size:0.9em; margin:0; padding:30px 0 5px; text-align:center;} ' +
            '#atdError {background-image:-moz-linear-gradient(top, rgba(255, 0, 0, 0.2), rgba(255, 0, 0, 0.6));-webkit-linear-gradient(top, rgba(255, 0, 0, 0.2), rgba(255, 0, 0, 0.6));linear-gradient(top, rgba(255, 0, 0, 0.2), rgba(255, 0, 0, 0.6));color:black;} ' +
            '.atdListing {margin-bottom:0}' +
            '.errorMsg {font-weight:bold; font-size:0.8em;}' +
            '.roundBottomCorners {border-bottom-left-radius:5px; border-bottom-right-radius:5px;} ' +
            '.hidden {display:none;}';

        writeLog(TITLE + " script started");

        addNewStyle(css);

        // add header
        g_atdcontainer.appendChild(createAtdHeader());

        // Show loading animation until fetching necessary data has finished.
        g_atdcontainer.appendChild(createLoader());
        document.body.insertBefore(g_atdcontainer, document.body.firstChild);
    }

    /**
    * Loads the backbone collection containing the download links (async)
    */
    function getBackboneTrailerPage() {
        writeLog(TITLE + ": Downloading '" + g_baseUrl + "data/page.json'...");
        GM_xmlhttpRequest({
            "url": g_baseUrl + "data/page.json",
            "method": "GET",
            "synchronous": false,
            "timeout": 60*1000,
            "onload": function(xhr) {
                // analyse the contents
                if (!xhr.responseText || APPLEERRORINDICATORS.test(xhr.responseText)) {
                    if (APPLEERRORINDICATOR_OLD.test(xhr.responseText))
                    {
                        parseOldTrailerPage(); // try old page type
                    }
                    else
                    {
                        getTrailerPage(); // try trailer page
                    }
                }
                else {
                    parseBackboneTrailerPage(xhr.responseText);
                }
            },
            "ontimeout": function() {
                showError("Timeout occurred");
            },
            "onerror": function() {
                showError("Could not download data/page.json");
            }
        });
    }

    function parseBackboneTrailerPage(page) {
        var collection, i, clip, src_title, sizes;

        writeLog(TITLE + ": Analyzing downloaded page...");

        try {
            // convert to json
            collection = JSON.parse(page);

            // get all the HD links from the json collection
            if (collection.clips && collection.clips.length) {
                getTrailerListingColor(addNewStyle);

                for(i = 0; i < collection.clips.length; i++) {
                    clip = collection.clips[i];
                    src_title = clip.title || "Trailer";

                    if (clip.versions && clip.versions.enus && clip.versions.enus.sizes) {
                        sizes = clip.versions.enus.sizes;
                        if (sizes.sd) {
                            addDownloadLink(sizes.sd.src, src_title, "480p");
                        }
                        if (sizes.hd720) {
                            addDownloadLink(sizes.hd720.src, src_title, "720p");
                        }
                        if (sizes.hd1080) {
                            addDownloadLink(sizes.hd1080.src, src_title, "1080p");
                        }
                    }
                }

                showAdtContainer();
            } else {
                showError('');
            }
        }
        catch (err) {
            showError("Could not parse page.json: " + (err.description || err.message));
        }
    }

    // start getting the links
    function getTrailerPage() {
        writeLog(TITLE + ": Downloading '" + g_baseUrl + "includes/large.html'...");
        GM_xmlhttpRequest({
            "method": "GET",
            "synchronous": false,
            "url": g_baseUrl + "includes/large.html",
            "timeout": 60*1000,
            "onload": function(xhr, status) {
                // analyse the contents
                if (!xhr.responseText || APPLEERRORINDICATORS.test(xhr.responseText)) {
                    showError("Could not find a valid trailer page");
                }
                else {
                    getTrailerSubPages(xhr.responseText);
                }
            },
            "ontimeout": function() {
                showError("Timeout occurred");
            },
            "onerror": function() {
                showError("Could not download includes/large.html");
            }
        });
    }

    /**
    * Loads the trailer subpage containing the download links (async)
    */
    function getDownloadlinks(url, title, cb) {
        GM_xmlhttpRequest({
            "url": g_baseUrl + url,
            "method": "GET",
            "synchronous": false,
            "timeout": 60*1000,
            "onload": function(xhr) {
                // analyse the contents
                if (!xhr.responseText || APPLEERRORINDICATORS.test(xhr.responseText)) {
                    showError("Could not download includes/large.html");
                }
                else {
                    insertDownloadLink(xhr.responseText, title);
                }
                if (cb) { cb(); }
            },
            "ontimeout": function() {
                showError("Timeout occurred");
                if (cb) { cb(); }
            },
            "onerror": function() {
                showError("Could not download trailer subpage");
                if (cb) { cb(); }
            }
        });
    }

    /**
    * Adds the trailer links to the respective arrays
    */
    function insertDownloadLink(page, title) {
        var reLink = new RegExp('<a class="movieLink" href="([^\\?"]+).mov', 'img'),
            result;

        // get all the HD links from the trailer page
        reLink.lastIndex = 0;
        if ((result = reLink.exec(page))) {
            if (result.length >= 2) {
                addDownloadLink(result[1], title);
            }
        }
    }

    function addDownloadLink(url, title, curSize)
    {
        var size, i, size_url, size_links, duplicate;

        try
        {
            for(size in g_allLinks) {
                if (!g_allLinks.hasOwnProperty(size)) {
                    continue;
                }
                if (curSize && curSize != size) {
                    continue;
                }

                size_url = normalizeTrailerLink(url, size);
                size_links = g_allLinks[size];

                // avoid duplicates
                duplicate = false;
                for (i = 0; i < size_links.length; i++) {
                    if (size_links[i] && size_links[i].href == url) {
                        duplicate = true;
                        break;
                    }
                }

                if (!duplicate) {
                    writeLog(TITLE + ": - Trailer link found: '" + size_url + "' (" + url + ")");
                    size_links.push(createElem('a', { 'textContent': title, 'href': size_url, 'title': 'Download ' + size }));
                }
            }
        }
        catch (err)
        {
            writeLog(TITLE + ": " + (err.message || err.description));
        }
    }

    function normalizeTrailerLink(url, size) {
        // url = url.toLowerCase();  don't change the case!!!

        // remove size
        url = url.replace(/_h?([0-9]+p?)(.mov)?$/, '');

        // workaround some incorrect links
        url = url.replace("http://movietrailers.apple.com", "http://trailers.apple.com");
        url = url.replace("http://trailers.apple.com/movies//trailers/independent", "http://trailers.apple.com/movies/independent");

        // add requested size
        url = url + "_h" + size + ".mov";

        return url;
    }

    /**
    * Scan the page for HD quicktime movies. Get all <a> tags with specific href's
    */
    function getTrailerSubPages(page) {
        var src_url, src_title,
        reLink = new RegExp('<a href="includes/([^#"]+)#?.[^"]*"', 'img'),
        reTitle_old = new RegExp('<h4>([^<]+)</h4>', 'img'),
        reTitle_new = new RegExp('<h3 title="[^"]+">([^<]+)</h3>', 'img'),
        reBackToTrailers = new RegExp('<div class="[^"]*back-to-trailers[^"]*">[^<]*<a\b', 'img'),
        result = null, requests, j,
        links = [],
        titles = [];

        writeLog(TITLE + ": Analyzing downloaded page...");

        // get all the HD links from the trailer page
        reLink.lastIndex = 0;
        while ((result = reLink.exec(page))) {
            if (result.length >= 2 && links.indexOfCI(result[1]) < 0) {
                writeLog(TITLE + ": - Trailer page found: '" + result[1] + "'");
                links.push(result[1]);
            }
        }

        // get all the titles from the trailer page
        reTitle_old.lastIndex = 0;
        while ((result = reTitle_old.exec(page))) {
            if (result.length >= 2) {
                writeLog(TITLE + ": - Trailer title found: '" + result[1] + "'");
                titles.push(result[1]);
            }
        }

        if (titles.length === 0) {
            reTitle_new.lastIndex = 0;
            while ((result = reTitle_new.exec(page))) {
                if (result.length >= 2) {
                    writeLog(TITLE + ": - Trailer title found: '" + result[1] + "'");
                    titles.push(result[1]);
                }
            }
        }

        if (links.length > 0) {
            getTrailerListingColor(addNewStyle);

            requests = links.length;
            for (j = 0; j < links.length; j++) {
                src_url = "includes/" + links[j];
                src_title = (links.length == titles.length ? titles[j] : ("Trailer " + (j + 1)));

                getDownloadlinks(src_url, src_title, function() { requests--; });
            }

            showAdtContainer(function() {return requests});
        } else {
            // check if a "watch the trailer" link exists
            if (reBackToTrailers.test(page)) {
                showMessage(CLICK_MSG);
            }
            else {
                showError('');
            }
        }
    }

    /**
    * Scan the old style page for HD quicktime movies. Get all <a> tags with specific href's
    * (eg: http://trailers.apple.com/trailers/disney/ponyo/)
    */
    function parseOldTrailerPage() {
        var src_url, src_title, links, titles, j;

        writeLog(TITLE + ": Analyzing current page...");

        try {
            links = Array.filter($('#content').getElementsByClassName('hd'), function(elem) {
                // we're only interested in links with a href pointing to a .mov
                if (elem.nodeName !== 'A' || !elem.hasAttribute('href')) {
                    return false;
                }
                return (elem.getAttribute('href').endsWith('1080p.mov'));
            });

            titles = Array.filter($('.trailer-nav').getElementsByClassName('text'), function(elem) {
                // we're only interested in spans with class text
                return (elem.nodeName === 'SPAN');
            });
        }
        catch (err) {
            links = [];
        }

        if (links.length > 0) {
            getTrailerListingColor(addNewStyle);

            for (j = 0; j < links.length; j++) {
                src_url = links[j].getAttribute('href').replace(".mov", "");
                src_title = (links.length == titles.length ? titles[j].textContent || titles[j].innerText : ("Trailer " + (j + 1)));

                addDownloadLink(src_url, src_title);
            }

            showAdtContainer();
        } else {
            // check if a "watch the trailer" link exists
            if ($('.back-to-trailers')) {
                showMessage(CLICK_MSG);
            }
            else {
                showError('');
            }
        }
    }

    /**
    * Try to fill the container
    */
    function showAdtContainer(checkPendingRequests)
    {
        var retryCount = 0;
        var si = setInterval(function() {
            if (!checkPendingRequests || checkPendingRequests() === 0) {
                clearInterval(si);
                fillAdtContainer();
                return;
            }

            retryCount++;
            if (retryCount > 10) {
                clearInterval(si);
                showError("Timeout occurred");
            }
        }, 500);
    }

    /**
    * Fill the already prepared container with all the trailer listings and
    * the respective toggles.
    */
    function fillAdtContainer() {
        var cont = null, i, size, size_links,
            ul = null, len, li;

        for(size in g_allLinks) {
            if (!g_allLinks.hasOwnProperty(size)) {
                continue;
            }
            size_links = g_allLinks[size];

            cont = prepContainer('atd' + size);
            ul = createElem('ul', { 'className': 'atdListing' });
            g_atdcontainer.appendChild(createToggle(size));
            li = null;
            for (i = 0; i < size_links.length; i++) {
                if (!size_links[i]) {
                    continue;
                }

                if (typeof (size_links[i]) == "string") {
                    li = createElem('li', { 'textContent': size_links[i], 'className': 'error' });
                }
                else {
                    li = createElem('li');
                    li.appendChild(size_links[i]);
                }
                ul.appendChild(li);
            }
            cont.appendChild(ul);
            g_atdcontainer.appendChild(cont);
            if (g_atdcontainer.style.display != 'table') { g_atdcontainer.style.display = 'table'; }
        }
        removeNode($('#atdLoader'));
    }

    function copyToClipboard(e) {
        if (typeof GM_setClipboard === 'undefined') {
            return;
        }

        var s = "", size, size_links,
            count = 0, i, len,
            elem = e.target,
            caption = elem.id;

        if (caption.endsWith('480p')) {
            size = "480p";
        } else if (caption.endsWith('720p')) {
            size = "720p";
        } else {
            size = "1080p";
        }

        size_links = g_allLinks[size];
        for (i = 0; i < size_links.length; i++) {
            if (typeof (size_links[i]) != "string" && size_links[i].href) {
                s += size_links[i].href + "\n";
                count++;
            }
        }

        if (s !== "") {
            GM_setClipboard(s);
            writeLog(TITLE + ": " + count + " movie link(s) copied to clipboard");
        }

        if (e.stopPropagation) {
            e.stopPropagation();
        }
        if (e.preventDefault) {
            e.preventDefault();
        }
        e.cancelBubble = true;
        e.returnValue = false;
        return false;
    }

    function showError(msg) {
        var omg = createElem('div', { 'id': 'atdError', 'textContent': ERROR_MSG, 'className': 'roundBottomCorners' });
        if (msg !== '') {
            omg.appendChild(createElem('br'));
            omg.appendChild(createElem('span', { 'textContent': msg, 'className': 'errorMsg' }));
        }
        g_atdcontainer.appendChild(omg);
        removeNode($('#atdLoader'));
    }

    function showMessage(msg) {
        var omg = createElem('div', { 'id': 'atdError', 'textContent': msg, 'className': 'roundBottomCorners' });
        g_atdcontainer.appendChild(omg);
        removeNode($('#atdLoader'));
    }

    // Prototypes ---------------------------------
    /**
    * Determine whether a string starts with a certain string.
    */
    String.prototype.startsWith = function(str) { return (this.indexOf(str) === 0); };

    /**
    * Determine whether a string ends with a certain string.
    */
    String.prototype.endsWith = function(str) { return this.indexOf(str, this.length - str.length) !== -1; };

    // case-insensitive indexOf function for arrays
    if (typeof Array.prototype.indexOfCI == 'undefined') {
        Array.prototype.indexOfCI = function(s) {
            if (s === null || (typeof s == "undefined")) {
                return -1;
            }
            for (var i = 0; i < this.length; i++) {
                if (this[i].toLowerCase() == s.toLowerCase()) {
                    return i;
                }
            }
            return -1;
        };
    }

    // Helper functions ---------------------------
    /**
    * Creates a new element.
    * @param {String} elem The element to create
    * @param {Object} attrs The new element's attributes
    * @returns {HtmlElement} The created element
    */
    function createElem(elem, attrs) {
        var newElem = document.createElement(elem), a;
        for (a in attrs) {
            if (attrs.hasOwnProperty(a)) {
                if (a === 'textContent') {
                    newElem.appendChild(document.createTextNode(attrs[a]));
                } else {
                    newElem[a] = attrs[a];
                }
            }
        }
        return newElem;
    }

    /**
    * Create the side panel header. Create a P element and add an A element to
    * quickly access this script's preferences.
    * @returns {HtmlElement}
    */
    function createAtdHeader() {
        var atdHeader = createElem('p', { 'id': 'atdHeader' }),
            atdHeaderLink = createElem('a', { 'id': 'atdHeaderLink', 'textContent': 'Apple Trailers' }),
            canvas = createElem('canvas', { 'id': 'tempCanvas', 'width': '10', height: '10', 'className': 'hidden' });

        atdHeaderLink.addEventListener('click', function() { GM_config.open(); }, false);
        atdHeader.appendChild(atdHeaderLink);
        atdHeader.appendChild(canvas);
        return atdHeader;
    }

    /**
    * Create and return the busy animation container. Processing all the
    * links can take a while if there are a lot of trailers.
    * @returns {HtmlElement}
    */
    function createLoader() {
        var ldr = createElem('div', { 'id': 'atdLoader', 'className': 'roundBottomCorners' });
        ldr.appendChild(createElem('p', { 'id': 'atdLoaderText', 'textContent': 'Gathering ...' }));
        return ldr;
    }

    /**
    * Create a DIV element which will serve as a toggle as well as
    * an indicator for the three sections (480p, 720p, 1080p).
    * Also adds an event listener (click) to the DIV element to control
    * the toggling.
    */
    function createToggle(caption) {
        var div = createElem('div', { 'id': 'atdTgl' + caption, 'textContent': caption, 'className': 'toggle', 'title': caption + ' trailers' });
        if (caption == '1080p' && GM_config.get('defaultSize') != 'Large (1080p)') { addClass(div, 'roundBottomCorners'); }
        div.addEventListener('click', toggleVisibility, false);
        div.addEventListener('contextmenu', copyToClipboard, false);
        return div;
    }

    /**
    * Prepare a container (DIV element) for the trailer listings (UL).
    * @param {String} newId The id of the container element
    * @returns {HtmlElement} The created DIV element
    */
    function prepContainer(newId) {
        var elem = createElem('div', { 'id': newId });
        if (GM_config.get('defaultSize').indexOf(newId.substr(3)) < 0) { elem.className = 'hidden'; }
        if (newId.endsWith('1080p')) { addClass(elem, 'roundBottomCorners'); }
        return elem;
    }

    /**
    * Return the final computed value of an element's CSS property.
    * @param {HtmlElement} elem The element
    * @param {String} prop The property
    * @returns {String} The final computed value
    */
    function getCssProp(elem, prop) {
        var cssProp;

        try {
            cssProp = window.getComputedStyle(elem, null).getPropertyValue(prop);
            if (cssProp) {
                cssProp.replace(/\\s+!important/gi, '');
            }
        } catch (err) {
            cssProp = null;
        }

        return cssProp;
    }

    /**
    * Return an element matching the specified selector.
    * @param {String} selector The selector
    * @param {Node} root Start looking here
    * @returns {HtmlElement|null} Search result
    */
    function $(selector, root) {
        var e = null;
        root = root || document;
        if (/^#(?!(?:[\w]+)?[ \.,\+\[~>#])/.test(selector)) {
            e = root.getElementById(selector.substring(1));
        } else {
            e = root.querySelector(selector);
        }
        return e;
    }

    /**
    * Removes a node from the DOM.
    * @param {HTMLElement} nod The node to remove
    */
    function removeNode(nod) { if (nod) { nod.parentNode.removeChild(nod); } }

    /**
    * Add a class to the className attribute.
    * @param {HtmlElement} elem The element to check
    * @param {String} cls The class to add
    */
    function addClass(elem, cls) {
        if (elem.nodeType === 1) {
            if (!elem.className) {
                elem.className = cls;
            } else {
                if (!hasClass(elem, cls)) { elem.className += " " + cls; }
            }
        }
    }

    /**
    * Remove a class from the className attribute. If it is the last attribute or
    * if cls isn't specified, the class attribute will be removed.
    * @param {HtmlElement} elem The element to change
    * @param {String} cls The class to remove
    */
    function remClass(elem, cls) {
        if (elem.nodeType === 1) {
            if (cls) {
                if (elem.className === cls) {
                    elem.removeAttribute('class');
                } else {
                    if (hasClass(elem, cls)) {
                        var cn = ' ' + elem.className + ' ';
                        cn = cn.replace(' ' + cls + ' ', '');
                        elem.className = cn.trim();
                    }
                }
            } else {
                elem.removeAttribute('class');
            }
        }
    }

    /**
    * Determine whether a className attribute has a specific class attached.
    * @param {HtmlElement} elem The element to check
    * @param {String} cls The class to look for
    * @returns {Boolean} Does the element have the class?
    */
    function hasClass(elem, cls) {
        if (!elem.className) {
            return false;
        }

        var cn = ' ' + elem.className + ' ';
        return cn.indexOf(' ' + cls + ' ') > -1;
    }

    /**
    * Event listener for showing/hiding the respective trailer listings.
    * In case of the 1080p section, create rounded bottom borders if closed.
    * @param {Event} e The event
    */
    function toggleVisibility(e) {
        var elem = e.target,
        toggleElem = $('#' + elem.id + ' + div');

        if (!hasClass(toggleElem, 'hidden')) {
            addClass(toggleElem, 'hidden');
            if (elem.id.endsWith('1080p')) { addClass(elem, 'roundBottomCorners'); }
        } else {
            remClass(toggleElem, 'hidden');
            if (elem.id.endsWith('1080p')) { remClass(elem, 'roundBottomCorners'); }
        }
    }

    /**
    * Adds a new CSS ruleset to the page. Uses GM_addStyle API; fallback in place.
    * @param {String} style Contains the CSS rules to add to the page
    */
    function addNewStyle(newStyle) {
        var node, heads;

        if (typeof GM_addStyle !== 'undefined') {
            GM_addStyle(newStyle);
        } else {
            heads = document.getElementsByTagName('head');
            if (heads.length > 0) {
                node = document.createElement('style');
                node.type = 'text/css';
                node.appendChild(document.createTextNode(newStyle));
                heads[0].appendChild(node);
            }
        }
    }

    /**
    * Travel up the DOM until the parent has the specified class and return the node.
    * @param {HtmlElement} elm The starting element
    * @param {String} cls The targeted parent has this class
    * @returns {HtmlElement} The targeted parent element
    */
    function parentUntilClassIs(elm, cls) {
        var p = elm;
        while (p.parentNode) {
            p = p.parentNode;
            if (hasClass(p, cls)) {
                break;
            }
        }
        return p;
    }

    /**
    * Determine whether n is a number or not.
    * @param {String|Number} n The string/number to check
    * @returns {Boolean} n can be interpreted as number
    */
    function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); }

    function writeLog(s) {
        if (window.console) {
            console.log(s);
        }
    }

    /**
    * Return the color used in Apple's trailer listing. If that's not
    * possible, assume Apple's default blue color scheme.
    * @returns {String} A CSS rule containing the trailer listing color
    */
    function getTrailerListingColor(cb) {
        var heading = $('#trailers > h2'), cssProp, css, m, url, img,
            validColor = function(css) {
                var valid = false, r, g, b;
                if (css && css.indexOf("rgb(") === 0) {
                    css = css.substr(4, css.length - 2);
                    css = css.split(",");
                    if (css.length == 3) {
                        r = parseInt(css[0], 10);
                        g = parseInt(css[1], 10);
                        b = parseInt(css[2], 10);
                        return !((r < 30 && g < 30 && b < 30) || (r > 230 && g > 230 && b > 230));
                    }
                }
                return false;
            };

        if (!cb) { return; }

        if (heading) {
            cssProp = getCssProp(heading, 'background-color');
        }
        if (!validColor(cssProp)) {
            heading = $('.top-wrapper');
            cssProp = getCssProp(heading, 'background-color');
        }
        if (!validColor(cssProp)) {
            heading = $('.top-wrapper');
            if (heading) {
                try {
                    m = getCssProp(heading, 'background-image').match(/url\(([^)]+)\)/i);
                    url = m[1].replace(/"/g, "");
                    img = new Image();
                    img.onload = function(e) {
                        var rgb = new ColorFinder(function favorHue(r, g, b) {
                            return ((r < 30 && g < 30 && b < 30) || (r > 230 && g > 230 && b > 230)) ? 0 : ((Math.abs(r - g) * Math.abs(r - g) + Math.abs(r - b) * Math.abs(r - b) + Math.abs(g - b) * Math.abs(g - b)) / 65535 * 50 + 1);
                        }).getMostProminentColor(img);
                        cssProp = 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')';
                        css = '#atdContainer > div.toggle {background:-moz-linear-gradient(left, ' + cssProp + ' 5%, rgba(0,0,0,.75) 85%) repeat scroll 0 0 transparent;background:-webkit-linear-gradient(left, ' + cssProp + ' 5%, rgba(0,0,0,.75) 85%) repeat scroll 0 0 transparent;background:linear-gradient(left, ' + cssProp + ' 5%, rgba(0,0,0,.75) 85%) repeat scroll 0 0 transparent;';
                        cb(css);
                    };
                    img.crossOrigin = '';
                    img.src = url;

                    return;
                }
                catch (err) {
                    cssProp = getCssProp(heading, 'background-color');
                }
            }
        }
        if (!cssProp) {
            cssProp = 'rgb(2, 131, 224)';
        }

        css = '#atdContainer > div.toggle {background:-moz-linear-gradient(left, ' + cssProp + ' 5%, rgba(0,0,0,.75) 85%) repeat scroll 0 0 transparent;background:-webkit-linear-gradient(left, ' + cssProp + ' 5%, rgba(0,0,0,.75) 85%) repeat scroll 0 0 transparent;background:linear-gradient(left, ' + cssProp + ' 5%, rgba(0,0,0,.75) 85%) repeat scroll 0 0 transparent;';
        cb(css);
    }

    function getAverageRGB(imgEl) {
        var blockSize = 5, // only visit every 5 pixels
            defaultRGB = 'rgb(2, 131, 224)', // for non-supporting envs
            canvas = document.getElementById("tempCanvas"),
            context = canvas.getContext && canvas.getContext('2d'),
            data, width, height,
            i = -4,
            length,
            rgb = { r: 0, g: 0, b: 0 },
            count = 0;

        if (!context) {
            return defaultRGB;
        }

        height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
        width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
        context.drawImage(imgEl, 0, 0);

        try {
            data = context.getImageData(0, 0, width, height);
        }
        catch (e) {
            return defaultRGB;
        }

        length = data.data.length;
        while ((i += blockSize * 4) < length) {
            ++count;
            rgb.r += data.data[i];
            rgb.g += data.data[i + 1];
            rgb.b += data.data[i + 2];
        }

        // ~~ used to floor values
        rgb.r = ~~(rgb.r / count);
        rgb.g = ~~(rgb.g / count);
        rgb.b = ~~(rgb.b / count);

        return 'rgb(' + rgb.r + ', ' + rgb.g + ', ' + rgb.b + ')';
    }

    function removeHash(s) {
        if (!s) { return s; }
        var i = s.lastIndexOf("#");
        return (i >= 0 ? s.substr(0, i) : s);
    }

    // Scriptish users don't need this line because of @noframes
    if (window.unsafeWindow && window.unsafeWindow.top !== window.unsafeWindow.self) { return; }

    // Init
    g_atdcontainer = createElem('div', { 'id': 'atdContainer' });
    g_baseUrl = removeHash(window.location.href);

    // Config
    GM_config.init({
        'id': 'GM_config',
        'title': TITLE,
        'fields': {
            'defaultSize': {
                'section': 'Created by JC2k8, Zatic, Mirzmaster and MHU',
                'type': 'select',
                'label': 'Default trailer size (open panel)',
                'default': 'Large (1080p)',
                'options': SIZES
            }
        },
        css: "#GM_config * { font-family: helvetica,arial,tahoma,myriad pro,sans-serif;}" +
             "#GM_config { background-color: rgba(0,0,0, 0.9); margin: 10% }" +
             "#GM_config_wrapper { background: #eee; padding: 1em;}" +
             "#GM_config .indent40 { margin-left: 40%;}" +
             "#GM_config .field_label { font-weight: bold; font-size: 12px; margin-right: 6px; float: left;}" +
             "#GM_config .block { display: block;}" +
             "#GM_config .saveclose_buttons { margin: 16px 10px 10px; padding: 2px 12px;}" +
             "#GM_config .reset, #GM_config .reset a, #GM_config_buttons_holder { text-align: right; color: #000;}" +
             "#GM_config .config_header { font-size: 20pt; margin: 0; padding: 0; font-weight: bold;}" +
             "#GM_config .config_desc, #GM_config .section_desc, #GM_config .reset { font-size: 9pt;}" +
             "#GM_config .center { text-align: center;}" +
             "#GM_config .section_header_holder { margin-top: 4px;}" +
             "#GM_config .config_var { margin: 0 0 4px; line-height: 22px; }" +
             "#GM_config .section_header { font-size: 13pt; background: #414141; color: #FFF; border: 1px solid #000; margin: 0 0 16px; }" +
             "#GM_config .section_desc { font-size: 9pt; background: #EFEFEF; color: #575757; border: 1px solid #CCC; margin: 0 0 6px; }"
    });

    // register menu command to access preferences
    GM_registerMenuCommand(TITLE +' Preferences...', function() { GM_config.open(); });

    // wait for the document to be fully loaded
    window.addEventListener("load", function() {
        initScript();

        setTimeout(function() {
            try {
                getBackboneTrailerPage();
            }
            catch(err) {
                writeLog(TITLE + ": ERROR: " + (err.description || err.message));
            }
        }, 250);
    }, false);
}());


// Copyright (c) 2013 Pieroxy <[email protected]>
// This work is free. You can redistribute it and/or modify it
// under the terms of the WTFPL, Version 2
// For more information see LICENSE.txt or http://www.wtfpl.net/
//
// For more information, the home page:
// http://pieroxy.net/blog/pages/color-finder/index.html
//
// Detection of the most prominent color in an image
// version 1.1.1

function ColorFinder(colorFactorCallback) {
    "use strict";

    this.callback = colorFactorCallback;
    this.getMostProminentColor = function(imgEl) {
        var rgb = null, data;
        if (!this.callback) {
            this.callback = function() { return 1; };
        }
        data = this.getImageData(imgEl);
        rgb = this.getMostProminentRGBImpl(data, 6, rgb, this.callback);
        rgb = this.getMostProminentRGBImpl(data, 4, rgb, this.callback);
        rgb = this.getMostProminentRGBImpl(data, 2, rgb, this.callback);
        rgb = this.getMostProminentRGBImpl(data, 0, rgb, this.callback);
        return rgb;
    };

    this.getImageData = function(imgEl, degrade, rgbMatch, colorFactorCallback) {

        var rgb,
            canvas = document.createElement('canvas'),
            defaultRGB = 'rgb(2, 131, 224)', // for non-supporting envs
            context = canvas.getContext && canvas.getContext('2d'),
            data, width, height, key, factor, result,
            i = -4,
            db = {},
            length, r, g, b,
            count = 0;

        if (!context) {
            return defaultRGB;
        }

        height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
        width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;

        context.drawImage(imgEl, 0, 0);

        try {
            data = context.getImageData(0, 0, width, height);
        } catch (e) {
            /* security error, img on diff domain */
            return null;
        }

        length = data.data.length;
        factor = Math.max(1, Math.round(length / 5000));
        result = {};
        while ((i += 4 * factor) < length) {
            if (data.data[i + 3] > 32) {
                key = (data.data[i] >> degrade) + "," + (data.data[i + 1] >> degrade) + "," + (data.data[i + 2] >> degrade);
                if (!result.hasOwnProperty(key)) {
                    rgb = { r: data.data[i], g: data.data[i + 1], b: data.data[i + 2], count: 1 };
                    rgb.weight = this.callback(rgb.r, rgb.g, rgb.b);
                    if (rgb.weight <= 0) {
                        rgb.weight = 1e-10;
                    }
                    result[key] = rgb;
                } else {
                    rgb = result[key];
                    rgb.count++;
                }
            }
        }

        return result;

    };

    this.getMostProminentRGBImpl = function(pixels, degrade, rgbMatch, colorFactorCallback) {

        var rgb = { r: 0, g: 0, b: 0, count: 0, d: degrade },
            db = {}, data, i,
            pixel, pixelKey, pixelGroupKey,
            length, r, g, b, totalWeight,
            count = 0;


        for (pixelKey in pixels) {
            if (pixels.hasOwnProperty(pixelKey)) {
                pixel = pixels[pixelKey];
                totalWeight = pixel.weight * pixel.count;
                ++count;
                if (this.doesRgbMatch(rgbMatch, pixel.r, pixel.g, pixel.b)) {
                    pixelGroupKey = (pixel.r >> degrade) + "," + (pixel.g >> degrade) + "," + (pixel.b >> degrade);
                    if (db.hasOwnProperty(pixelGroupKey)) {
                        db[pixelGroupKey] += totalWeight;
                    }
                    else {
                        db[pixelGroupKey] = totalWeight;
                    }
                }
            }
        }

        for (i in db) {
            if (db.hasOwnProperty(i)) {
                data = i.split(",");
                r = data[0];
                g = data[1];
                b = data[2];
                count = db[i];

                if (count > rgb.count) {
                    rgb.count = count;
                    data = i.split(",");
                    rgb.r = r;
                    rgb.g = g;
                    rgb.b = b;
                }
            }
        }

        return rgb;

    };

    this.doesRgbMatch = function(rgb, r, g, b) {
        if (rgb === null) { return true; }
        r = r >> rgb.d;
        g = g >> rgb.d;
        b = b >> rgb.d;
        return rgb.r == r && rgb.g == g && rgb.b == b;
    };
}