]> git.rmz.io Git - dotfiles.git/commitdiff
add DeviantArt Gallery Ripper userscript
authorSamir Benmendil <samir.benmendil@gmail.com>
Sun, 1 Sep 2013 17:26:57 +0000 (19:26 +0200)
committerSamir Benmendil <samir.benmendil@gmail.com>
Sun, 1 Sep 2013 17:26:57 +0000 (19:26 +0200)
dwb/greasemonkey/98706.user.js [new file with mode: 0644]

diff --git a/dwb/greasemonkey/98706.user.js b/dwb/greasemonkey/98706.user.js
new file mode 100644 (file)
index 0000000..a48a2a3
--- /dev/null
@@ -0,0 +1,871 @@
+// ==UserScript==
+// @name           (dm) Deviant Art Gallery Ripper
+// @namespace      DeviantRipper
+// @description    Click button and generate a list of direct image link urls for all images for a users gallery.
+// @version        1.0.15
+// @lastupdated    2013-08-29
+// @include        http://*.deviantart.com/favourites/*
+// @match          http://*.deviantart.com/favourites/*
+// @include        http://*.deviantart.com/gallery/*
+// @match          http://*.deviantart.com/gallery/*
+// @!nclude        http://*.deviantart.com/art/*
+// @!atch          http://*.deviantart.com/art/*
+// @include        http://browse.deviantart.com/*
+// @match          http://browse.deviantart.com/*
+// @include        http://backend.deviantart.com/rss.xml*
+// @match          http://backend.deviantart.com/rss.xml*
+// ==/UserScript==
+
+/*
+    Known issue: ?
+
+    Documentation needed. 
+
+ */
+
+var GM_log;
+if (typeof GM_log  === 'undefined') { 
+    GM_log = function (str) { console.log(str); };
+}
+
+var debug = false;
+//var debug = true;
+
+/*
+ * xHttp object
+ *     variables:
+ *        maxreq        - maximum number of async requests to do
+ *        runcon        - number of running connections
+ *        interval    - interval holder
+ *        links        - array() object used to help with triggering
+ *                        new connections for asyncLoad
+ *    functions:
+ *
+ *        startInterval - starts the interval loop
+ *            accepts args: (heartbeat_function, interval)
+ *            heartbeat_function = function called on each trigger pulse
+ *            interval = integer of ms between interval triggers
+ *
+ *        stopInterval() - stops the interval loop
+ *            no args
+ *
+ *        load - synchronous xmlHttpRequest
+ *            only handles simple "GET"
+ *            accepts args: (string url)
+ *
+ *        asyncLoad    - asynchronous xmlHttpRequest
+ *            accepts args: (string url or associative array of options,
+ *                    function callback, optional args)
+ *                url = url string to load
+ *                if object can have params like GM_xmlHttpRequest ex:
+ *                {url:String, method:String, headers:{associative array},
+ *                    data:String, onload:Function, onerror:Function,
+ *                    onreadystatechange:Function}
+ *                onload, onerror, onreadystatechange are called with
+ *                    function(xmlHttpRequest event object, objparams, 
+ *                        callback, extra_args);
+ *
+ *                See below in asyncLoad definition comment for more
+ *                    information.
+ *
+ *                function = callback function called as
+ *                    callback(xhtr, string url, optional args)
+ *                    do not specify a callback if using an onload above
+ *
+ *                optional args = single variable passed verbatim to
+ *                    the callback function
+ *
+ *        default_heartbeat
+ *            default heartbeat function
+ *
+ *        default_next_url
+ *            default routine to handle next url fetch
+ *
+ *         default_callback_xhtr
+ *             a default callback to use when retrieving a request
+ */
+function XHttp() {
+    var xHttpInstance = this;
+    this.maxreq = 4;
+    this.runcon = 0;
+    this.interval = null;
+    this.links = [];
+    
+    this.default_heartbeat = function () {
+        if ((xHttpInstance.runcon < xHttpInstance.maxreq) && 
+                (xHttpInstance.links.length > 0)) {
+            // do something here when you have an opening to get more stuff
+            xHttpInstance.default_next_url();
+        }
+        if ((xHttpInstance.links.length === 0) && 
+                (xHttpInstance.runcon === 0)) {
+            // do something here when out of things to 
+            xHttpInstance.stopInterval();
+        }
+    };
+    this.default_next_url = function () {
+        if (xHttpInstance.links.length > 0) {
+            var link_data = xHttpInstance.links.shift().toString();
+            xHttpInstance.asyncLoad(link_data,
+                    xHttpInstance.default_callback_xhtr);
+        }
+    };
+    this.default_callback_xhtr = function (xhtr, strURL, args) {
+        // do something with the result
+        // xhtr is the xmlHttpRequest object
+        // common values are:
+        // xhtr.responseText
+        // xhtr.responseXml
+        // xhtr.readyState
+        // xhtr.status
+
+        if (xhtr.status === 404) {
+            // do something when 404 not found
+            // like:
+            alert("Page wasn't found at: " + strURL + "\n" +
+                    xhtr.status + " " + xhtr.statusText);
+        } else if (xhtr.status === 200) {
+            // do something when 200 ok
+            // like:
+            alert(xhtr.responseText);
+        } else {
+            // do other stuff with other codes
+            alert("Site returned: " + xhtr.status + " '" +
+                    xhtr.statusText + "' for: \n" + strURL);
+        }
+    };
+    /*
+     * startInterval (heartbeat_function, heartbeat_pulse)
+     *    accepts:
+     *        heartbeat_function: function reference to call on each heartbeat
+     *            pulse
+     *        heartbeat_pulse: integer 
+     * 
+     */
+    this.startInterval = function (heartbeat_function, heartbeat_pulse) {
+        var pulse_rate;
+        var heartbeat_func;
+        // check for and stop existing interval
+        if (xHttpInstance.interval !== null) { xHttpInstance.stopInterval(); }
+        
+        if (typeof heartbeat_pulse === 'undefined') {
+            pulse_rate = 100;
+        } else {
+            pulse_rate = heartbeat_pulse;
+            if (isNaN(pulse_rate)) { // validate its an actual number
+                throw "startInterval given invalid pulse rate :" +
+                    heartbeat_pulse;
+            }
+        }
+        if (typeof heartbeat_function === 'undefined') {
+            heartbeat_func = xHttpInstance.default_heartbeat;
+        } else {
+            heartbeat_func = heartbeat_function;
+        }
+        
+        if (!heartbeat_func instanceof Function) {
+            throw "startInterval given incorrect heartbeat function argument.";
+        }
+        /* end error checking */
+        xHttpInstance.interval = setInterval(heartbeat_func, pulse_rate);
+    };
+    
+    /*
+     * stopInterval ()
+     * 
+     * stops the xHttp interval loop.
+     */
+    this.stopInterval = function () {
+        clearInterval(xHttpInstance.interval);
+        xHttpInstance.interval = null;
+    };
+    
+    /*
+     * load (strURL)
+     * 
+     * synchronus XMLHttpRequest load with simple parameter request.
+     * Returns text value of get request or false.
+     */
+    this.load = function (strURL) {
+        //if (debug) { GM_log("Getting url: " + strURL); }
+        var xhtr = new XMLHttpRequest();
+        xhtr.open("GET", strURL, false);
+        xhtr.send();
+        if (xhtr.readyState === 4 && xhtr.status === 200) { 
+            return xhtr.responseText; 
+        } else {
+            return false;
+        }
+    };
+    /*
+     * asyncLoad(objparams, callback, extra_args)
+     * 
+     * multithreaded url fetching routine
+     * gets url contents and sends to callback function
+     * 
+     * if objparams is passed as a string function assumes
+     * simple get request with objparams being a url string.
+     * 
+     * otherwise:
+     * objparams object properties imitates grease monkey
+     * GM_xmlHttpRequest function.
+     * 
+     * method -    a string, the HTTP method to use on this request.
+     *         Generally GET, but can be any HTTP verb, including POST,
+     *         PUT, and DELETE.
+     * 
+     * url - a string, the URL to use on this request. Required.
+     * 
+     * headers - an associative array of HTTP headers to include on
+     *         this request. Optional, defaults to an empty array. Example:
+     *         headers: {'User-Agent': 'Mozilla/4.0 (compatible) Greasemonkey',
+       *            'Accept': 'application/atom+xml,application/xml,text/xml'}
+       *
+       * data - a string, the body of the HTTP request. Optional, defaults
+       *         to an empty string. If you are simulating posting a form
+       *         (method == 'POST'), you must include a Content-type of
+       *         'application/x-www-form-urlencoded' in the headers field,
+       *         and include the URL-encoded form data in the data field.
+       * 
+       * onreadystatechange - a function object, the callback function to be
+       *         called repeatedly while the request is in progress.
+       * 
+       * onerror - a function object, the callback function to be called
+       *         if an error occurs while processing the request.
+       * 
+       * onload - a function object, the callback function to be called when
+       *         the request has finished successfully.
+       *         ** DO NOT ** specify a callback function if using onload.
+       *         onload will take precedence and fire instead of callback.
+       *         onload will pass the callback value to its called function
+       *         if you want to use the values in some way. See definition
+       *         below for the default_onload.
+     */
+
+    this.asyncLoad = function (objparams, callback, extra_args) {
+        //if (debug) GM_log("Async Getting url : " + url);
+
+        // local function Variables
+        var url = "";
+        var method;
+        var default_method = "GET";
+        var send_data = "";
+        var http_req = new XMLHttpRequest();
+        var xHttpPtr = xHttpInstance;
+        var headkey;
+        var useGMxml = false; 
+
+        var onerror_wrapper = null;
+        var onload_wrapper = null;
+        var onreadystatechange_wrapper = null;
+        // end local function variables
+
+        var default_onerror = function (args) {
+            /*
+             * do something here when there's errors.
+             */
+            var target;
+            if (args.target) { 
+                target = args.target;
+            } else {
+                target = args;
+            }
+            xHttpInstance.runcon -= 1;
+            if (onerror_wrapper !== null) {
+                onerror_wrapper(target, objparams, callback, extra_args);
+            }
+        };
+
+        var default_onreadystatechange = function (args) {
+            var target;
+            if (args.target) { 
+                target = args.target;
+            } else {
+                target = args;
+            }
+            if (onreadystatechange_wrapper !== null) {
+                onreadystatechange_wrapper(target, objparams, 
+                        callback, extra_args);
+            }
+        };
+
+        var default_onload = function (args) {
+//            if (debug) { 
+//    GM_log("xmlHttpRequest response: " + 
+//            args.readyState + " " + args.status + " " + 
+//            url); 
+//            }
+            var target;
+            if (args.target) { 
+                target = args.target;
+            } else {
+                target = args;
+            }
+            xHttpPtr.runcon -= 1;
+            if (onload_wrapper !== null) { 
+                onload_wrapper(target, objparams, callback, extra_args); 
+            } else {
+                callback(target, url, extra_args);
+            }
+        };
+
+        if (typeof objparams !== 'object') {
+            if (typeof objparams === 'string') {
+                url = objparams;
+                method = default_method;
+                http_req.open(method, url, true);
+            } else {
+                throw "asyncLoad error: parameters not object or string";
+            }
+        } else {
+
+            // check url parameter value
+            if (typeof objparams['url'] !== 'string') {
+                throw "asyncLoad error: missing url parameter.";
+            } else {
+                // make sure its not blank
+                url = objparams['url'];
+                if (url === '') {
+                    throw "asyncLoad error: url parameter is empty string.";
+                }
+            }
+
+            // check if we specified method
+            if (typeof objparams['method'] === 'string') {
+                method = objparams['method'];
+            } else {
+                method = default_method;
+            }
+
+            // open xmlHttpRequest so we can properly set headers
+            http_req.open(method, url, true);
+
+            // check if we specified any custom headers and have some sort
+            // of validation of the data. Just ignores non strings.
+            if (typeof objparams['headers'] === 'object') {
+                for (headkey in objparams['headers']) {
+                    if (objparams['headers'].hasOwnProperty(headkey)) {
+                        if (typeof headkey === 'string') {
+                            if (typeof objparams['headers'][headkey]
+                                    === 'string') {
+                                http_req.setRequestHeader(headkey, 
+                                        objparams['headers'][headkey]);
+                            }
+                        }
+                    }
+                }
+            }
+
+            if (typeof objparams['data'] === 'string') {
+                send_data = objparams['data'];
+            }
+
+            if (typeof objparams['onreadystatechange'] === 'function') {
+                onreadystatechange_wrapper = objparams['onreadystatechange'];
+            }
+
+            if (typeof objparams['onerror'] === 'function') {
+                onerror_wrapper = objparams['onerror'];
+            }
+
+            if (typeof objparams['onload'] === 'function') {
+                onload_wrapper = objparams['onload'];
+            }
+            
+            if (objparams['useGMxml']) {
+                useGMxml = true;
+            }
+
+        }
+
+        if (typeof callback !== 'function' && 
+                typeof onload_wrapper !== 'function') {
+            throw "asyncLoad error: no callback or onload function passed.";
+        }
+
+        xHttpPtr.runcon += 1;
+
+        if (useGMxml) {
+            GM_xmlhttpRequest({
+                method: method,
+                url: url,
+                headers: objparams['headers'],
+                onload: default_onload,
+                onerror: default_onerror,
+                onreadystatechange: default_onreadystatechange
+            });
+
+        } else {
+            http_req.onerror = default_onerror;
+            http_req.onreadystatechange = default_onreadystatechange;
+            http_req.onload = default_onload;
+            
+            http_req.send(send_data);
+        }
+    };
+}
+
+var deviantRipper = {
+        isChrome : /chrome/i.test(navigator.userAgent),
+        isFireFox : /firefox/i.test(navigator.userAgent),
+        abort_links : false,
+        useGMxml : false,      // flag to use GM_xmlHttpRequest instead of XMLHttpRequst
+        xml_link_data : [],    // array holder for xlm page links
+        pages : {
+            //recurse var used for thumbnail pages mainly. if set to 0 and button
+            //clicked on single page it doesn't really do anything useful.
+            recurse: true,     // recuse into lower gallery pages
+            current: 0,        // current counter reused for image and gallery parsing
+            total: 0,          // total counter used for image parsing
+            urls: [],          // holder for url html list
+            toparse: [],       // list of urls of single image pages that need to be parsed for DDL
+            textbox: null,     // textbox holder
+            fetchStatus: 0     // status id for script checking status:
+            // 0 = not started, 1 = getting indexes
+            // 2 = getting image DDL, 3 = finished everything
+            // 4 = displayed urls (finished or aborted)
+        },
+
+        /*
+         * display_url_list()
+         * 
+         * function called when we're all done and we want to
+         * display the list of url's we got.
+         */
+        display_url_list : function () {
+            var docNamespace = 'http://www.w3.org/1999/xhtml';
+                       var counter;
+                       var tmpStr;
+            if (debug) { GM_log("Call: display_url_list()"); }
+            if (debug) { GM_log(deviantRipper); }
+            if (deviantRipper.pages.fetchStatus > 3) { return; }
+            deviantRipper.pages.textbox = 
+                document.createElementNS(docNamespace, "textarea");
+            deviantRipper.pages.textbox.style.width = '100%';
+                       for (counter = 0; counter < deviantRipper.pages.urls.length; counter += 1) {
+                   if (debug) { GM_log("Fixing " + deviantRipper.pages.urls[counter]); }
+                               if (deviantRipper.pages.urls[counter].indexOf('http://th') > -1) {
+                                       tmpStr = deviantRipper.pages.urls[counter].replace('http://th', 'http://fc').replace('/PRE/', '/');
+                                       deviantRipper.pages.urls[counter] = tmpStr;
+                               }
+                       }
+                       deviantRipper.pages.textbox.innerHTML = 
+                deviantRipper.pages.urls.join('\r\n');
+            document.body.insertBefore(deviantRipper.pages.textbox,
+                    document.body.firstChild);
+            deviantRipper.pages.fetchStatus = 4;
+        },
+
+        /*
+         * init()
+         * 
+         * Called as first function execution upon script load.
+         * Sets up the xmlHttpRequest helpers and generates click button.
+         */
+        init : function () {
+            // Check whether we're on backend
+            deviantRipper.xml_xHttp = new XHttp();
+            
+            if (debug) { GM_log("init() isChrome: " + deviantRipper.isChrome + " isFireFox: " + deviantRipper.isFireFox); }
+            if (deviantRipper.isFireFox === true) {
+                deviantRipper.useGMxml = true;
+            }
+            
+            if (/backend/i.test(location.hostname) === true) { 
+                if (/rss\.xml/i.test(location.href) === true) {
+                    // test if we're in iframe if not then get out
+                    if (window === parent) { return; }
+                    
+                    deviantRipper.pages.btnID = deviantRipper.btn.generateXMLButton();
+                    deviantRipper.btn.startXML(document.location.href);
+                }
+            } else {
+                deviantRipper.pages.btnID = deviantRipper.btn.generateButton();
+            }
+        },
+
+        checker: {
+            /*
+             * isThumbnailGallery (doc)
+             * 
+             * return true if page seems to be a gallery index
+             * or false if it looks like its a single image page
+             * detection is looking for the comments by the artist
+             * usually found on the single image page
+             */ 
+            isThumbnailGallery : function (doc) {
+                if (debug) { GM_log("Call: isThumbnailGallery()"); }
+                return (doc.getElementById("artist-comments")) ? false : true;
+            },
+
+            /*
+             * isAborted ()
+             * 
+             * check if we clicked the button to abort script
+             * if we did it requires a page reload to start again
+             *  
+             */
+            isAborted : function () {
+                if (debug) { GM_log("isAborted(): " + deviantRipper.abort_links); }
+                if (deviantRipper.abort_links === true) {
+                    deviantRipper.pages.btnID.value = 'Aborted: ' + deviantRipper.pages.btnID.value;
+                    if (debug) { GM_log("FetchStatus: " + deviantRipper.pages.fetchStatus); }
+                    if (deviantRipper.pages.fetchStatus > 1) { deviantRipper.display_url_list(); }
+                    deviantRipper.xml_link_data = [];
+                    deviantRipper.pages.toparse = [];
+                    return true;
+                } else {
+                    return false;
+                }
+            },
+
+            /*
+             * next_xml ()
+             * 
+             * get our next gallery page from our stack,
+             * increment our fetching counter, and fetch page
+             */
+            next_xml : function () {
+                var link_uri;
+                
+                if (debug) { GM_log("Call: next_xml()"); }
+                if (deviantRipper.checker.isAborted()) {
+                    return false;
+                }
+                if (deviantRipper.xml_link_data.length > 0) {
+                    link_uri = deviantRipper.xml_link_data.shift().toString();
+                    if (debug) { GM_log("Shifted: " + link_uri + "\ntypeof: " + typeof link_uri); }
+                    
+                    if (deviantRipper.useGMxml) {
+                        if (debug) {
+                            GM_log("Using GreaseMonkey GM_xmlHttpRequest.");
+                        }
+                        deviantRipper.xml_xHttp.asyncLoad({
+                            url: link_uri,
+                            useGMxml: true,
+                            onload: deviantRipper.callback.scan_xml_dom
+                        });
+                    } else {
+                        deviantRipper.xml_xHttp.asyncLoad(link_uri, deviantRipper.callback.scan_xml_dom);
+                    }
+
+                }
+            }
+        }, // end checker
+        
+        parser: {
+            /*
+             * image_links_xml (docbase)
+             * 
+             * function called after we load a gallery index page,
+             * "docbase" references the document of the index page
+             * so we can start looking for thumbnails in order to
+             * get the single image page links.
+             */
+            image_links_xml : function (docbase) {
+                if (debug) { GM_log("Call: image_links_xml()"); }
+                var items;
+                var hifi = null;
+                var lofi = null;
+                               var thumbnail;
+                               var thumbnails;
+                var content;
+                var counter;
+                var locounter;
+
+                items = docbase.getElementsByTagNameNS('*', 'item');
+                if (items.length < 1) {
+                    deviantRipper.pages.recurse = false;
+                    return;
+                }
+
+                for (counter = 0; counter < items.length; counter += 1) {
+                    content = items[counter].getElementsByTagNameNS('*', 'content');
+                                       thumbnails = items[counter].getElementsByTagNameNS('*', 'thumbnail');
+                                       thumbnail = null;
+
+                    if (thumbnails.length > 0) {
+                                               // grab last thumbnail item and use it incase we don't find any content lines
+                                               thumbnail = thumbnails[thumbnails.length - 1].getAttribute('url');
+                    }
+                                       
+                    for (locounter = 0; locounter < content.length; locounter += 1) {
+                        if (content[locounter].getAttribute('medium') === 'image') { lofi = content[locounter].getAttribute('url'); }
+                        if (content[locounter].getAttribute('medium') === 'document') { hifi = content[locounter].getAttribute('url'); }
+                    }
+                    if (hifi !== null) {
+                        if (debug) { GM_log("Hifi: " + hifi); }
+                        deviantRipper.pages.urls.push(hifi);
+                    } else if (lofi !== null) {
+                        if (debug) { GM_log("Lofi: " + lofi); }
+                        deviantRipper.pages.urls.push(lofi);
+                                       } else {
+                                               if (debug) { GM_log("thumbnail: " + thumbnail); }
+                                               deviantRipper.pages.urls.push(thumbnail);
+                    }
+                }
+                if (debug) { GM_log([counter, length, deviantRipper.pages.urls.length]); }
+            },
+            
+
+            /*
+             * next_xml_page_link (docbase)
+             * 
+             * Function called after loading xml page looking for next
+             */
+            next_xml_page_link : function (docbase) {
+                if (debug) { GM_log("Call: next_xml_page_link()"); }
+                if (debug) { GM_log(docbase); }
+                var rtn_val;
+//                var links;
+//                var counter, length;
+//                links = docbase.getElementsByTagNameNS('http://www.w3.org/2005/Atom', 'link');
+//                for (counter = 0, length = links.length;
+//                        counter < length; 
+//                        counter += 1) {
+//                    if (links[counter].getAttribute('rel').toString() === "next") {
+//                        rtn_val = links[counter];
+//                        break;
+//                    }
+//                }
+                rtn_val = docbase.querySelector('link[rel="next"]');
+                if (rtn_val) {
+                    rtn_val = rtn_val.getAttribute('href');
+                    if (debug) { GM_log("NextXML page: " + rtn_val); }
+                    return rtn_val;
+                } else { 
+                    return false; 
+                }
+            }
+            
+
+        }, // end parser
+        
+        callback: {
+
+            /*
+             * scan_xml_dom (HTML_Data, url, args)
+             * 
+             * called when gallery page html is loaded
+             * so we can parse images out and set next page
+             */
+            scan_xml_dom : function (HTML_Data, url, args) {
+                if (debug) { GM_log("Call: scan_xml_dom()"); }
+                var html_dom;
+                var nextPage;
+                var parser;
+                
+                html_dom = HTML_Data.responseXML;
+                if (!html_dom) {
+                    if (HTML_Data.responseText !== "") {
+                        parser = new DOMParser();
+                        html_dom = parser.parseFromString(HTML_Data.responseText, "text/xml");
+                    } else {
+                        throw "There was an error parsing XML from: " + url;
+                    }
+                }
+                
+                // parse and add images on page to fetch stack
+                deviantRipper.parser.image_links_xml(html_dom);
+
+                deviantRipper.pages.current += 1;
+                deviantRipper.pages.btnID.value = "Loading xml page " + 
+                    deviantRipper.pages.current + 
+                    "(" + deviantRipper.pages.urls.length + ")";
+                
+                if (deviantRipper.pages.recurse) {
+                    nextPage = deviantRipper.parser.next_xml_page_link(html_dom);
+                    if (nextPage) { deviantRipper.xml_link_data.push(nextPage.toString()); }
+                }
+                
+            }
+            
+        }, // end callback
+        
+        btn: {
+            /*
+             * getLinks ()
+             * 
+             * onclick function triggered when the
+             * button we injected is clicked to get
+             * our direct links.
+             */
+            getLinks : function () {
+                if (debug) { GM_log("Call: getLinks()"); }
+                var iframeLoader;
+                var feedbutton;
+                var docNamespace = 'http://www.w3.org/1999/xhtml';
+             
+                deviantRipper.pages.btnID.removeEventListener("click", deviantRipper.btn.getLinks, false);
+                feedbutton = document.querySelector('link[type="application/rss+xml"]');
+                if (!feedbutton) {
+                    throw "No feed button on this page.";
+                }
+                
+                if (deviantRipper.isChrome === true) {
+                    deviantRipper.pages.btnID.parentNode.removeChild(
+                        deviantRipper.pages.btnID
+                    );
+                  
+                    iframeLoader = document.createElementNS(
+                        docNamespace,
+                        'iframe'
+                    );
+                    iframeLoader.src = feedbutton.href;
+                    iframeLoader.style.width = '100%';
+                    iframeLoader.style.height = '100px';
+                    document.body.insertBefore(
+                        iframeLoader, 
+                        document.body.firstChild
+                    );
+                } else {
+                    deviantRipper.btn.startXML(feedbutton.href);
+                }
+                
+            },
+
+            /*
+             * startXML ()
+             * 
+             * started from init() to start grabbing XML pages
+             * starting with current loaded one. Script assumes
+             * we loaded from an iframe.
+             */
+            startXML : function (galleryLink) {
+                if (debug) { GM_log("Call: startXML(" + arguments[0] + ")"); }
+                deviantRipper.pages.btnID.addEventListener('click', deviantRipper.btn.abortLinkChecking, false);
+                deviantRipper.xml_link_data.push(galleryLink.toString());
+                deviantRipper.pages.fetchStatus = 1;
+                deviantRipper.xml_xHttp.startInterval(deviantRipper.heartbeat.load_xml, 50);
+            },
+
+            /*
+             * abortLinkChecking ()
+             * 
+             * onclick triggered when button is clicked
+             * while we're getting links.
+             */
+            abortLinkChecking : function () {
+                deviantRipper.abort_links = true;
+                GM_log("abortLinkChecking()");
+                deviantRipper.pages.btnID.removeEventListener('click', deviantRipper.abortLinkChecking, false);
+            },
+
+            /*
+             * generateButton()
+             * 
+             * creates the click button for our page
+             */
+            generateButton : function () {
+                if (debug) { GM_log("Call: generateButton()"); }
+                var new_button;
+                var btnLoc;
+
+                new_button = document.createElement("input");
+                new_button.type = "button";
+                new_button.value = "Get URLs for Gallery";
+                new_button.setAttribute("onsubmit", "return false;");
+
+                // var btnLoc = document.getElementById("gmi-GalleryEditor");
+                btnLoc = document.getElementById("output");
+                if (btnLoc) {
+                    btnLoc.insertBefore(new_button, btnLoc.firstChild);
+                    new_button.addEventListener("click", deviantRipper.btn.getLinks, false);
+                } else {
+                    new_button.value = "Root Thumbnail Page?";
+                    document.body.insertBefore(new_button, document.body.firstChild);    
+                }
+                return new_button;
+            },
+            
+            /*
+             * generateXMLButton()
+             * 
+             * creates the click button for our page
+             */
+            generateXMLButton : function () {
+                if (debug) { GM_log("Call: generateXMLButton()"); }
+                var new_button;
+                var docNamespace = 'http://www.w3.org/1999/xhtml';
+                var replacedRootNode = document.createElement('clearinghouse');
+                
+                // empty out the current document view.
+                if (deviantRipper.isChrome === true) {
+                    while (document.documentElement.firstChild) {
+                        replacedRootNode.appendChild(
+                            document.documentElement.firstChild
+                        );
+                    }
+                } else if (deviantRipper.isFireFox === true) {
+                    while (document.body.firstChild) {
+                        replacedRootNode.appendChild(
+                            document.body.firstChild
+                        );
+                    }
+                }
+
+                if (document.body === null) {
+                    document.body = document.createElementNS('http://www.w3.org/1999/xhtml', 'body'); 
+                    document.documentElement.appendChild(document.body);
+                }
+
+                new_button = document.createElementNS(docNamespace, 'input');
+                new_button.type = "button";
+                new_button.value = "Loading...";
+                new_button.setAttribute("onsubmit", "return false;");
+                document.body.appendChild(new_button);
+                new_button.addEventListener('click', deviantRipper.btn.abortLinkChecking, false);
+                
+                return new_button;
+            }
+        }, // end btn
+        
+        heartbeat: {
+            
+            /*
+             * load_xml ()
+             * 
+             * heartbeat loop while loading gallerie indices
+             */
+            load_xml : function () {
+                var runcon = deviantRipper.xml_xHttp.runcon;
+                var maxreq = deviantRipper.xml_xHttp.maxreq;
+                var length = deviantRipper.xml_link_data.length;
+                if ((runcon < maxreq) && (length > 0)) {
+                    if (debug) { GM_log("heartbeat load_xml()\nrunning connections: (" + runcon + ') max running (' + maxreq + ')'); }
+                    deviantRipper.checker.next_xml();
+                }
+                if ((length === 0) && (runcon === 0)) {
+                    if (debug) { GM_log("Stopping heartbeat out of xml pages to pull."); }
+                    deviantRipper.xml_xHttp.stopInterval();
+                    
+                    deviantRipper.pages.total = deviantRipper.pages.toparse.length;
+                    deviantRipper.pages.fetchStatus = 3;
+                    deviantRipper.xml_xHttp.startInterval(deviantRipper.heartbeat.xml_finisher, 50);
+                }
+            },
+            
+            /*
+             * xml_finisher ()
+             * 
+             * watches for xml to finish loading then displays the urls.
+             */
+            xml_finisher : function () {
+                var runcon = deviantRipper.xml_xHttp.runcon;
+                var length = deviantRipper.xml_link_data.length;
+                if ((length === 0) && (runcon === 0)) {
+                    if (debug) { GM_log("Stopping heartbeat xml_finisher."); }
+                    deviantRipper.xml_xHttp.stopInterval();
+                    
+                    deviantRipper.display_url_list();
+                }
+            }
+        } // end heartbeat
+
+    };
+
+
+
+if (debug) { GM_log("Current URL loaded from: " + document.location.href); }
+//start the dirty stuff
+deviantRipper.init();