Nyaa.se Batch downloader

Batch download torrents from nyaa.se

目前为 2015-09-24 提交的版本。查看 最新版本

// ==UserScript==
// @name        Nyaa.se Batch downloader
// @namespace   Autodownload
// @author      Victorique
// @description Batch download torrents from nyaa.se
// @include     http://www.nyaa.se/?page=search&cats=*&filter=*&term=*&user=*
// @include     http://www.nyaa.se/?page=search&filter=*&term=*&user=*
// @include     http://www.nyaa.se/?page=search&term=*&user=*
// @include     http://www.nyaa.eu/?page=search&cats=*&filter=*&term=*&user=*
// @include     http://www.nyaa.eu/?page=search&filter=*&term=*&user=*
// @include     http://www.nyaa.eu/?page=search&term=*&user=*
// @version     5.3.0
// @icon        https://i.imgur.com/nx5ejHb.png
// @license     MIT
// @run-at      document-start
// @grant       none
// @require     https://code.jquery.com/jquery-2.1.4.min.js
// ==/UserScript==

/*
var e = document.createElement("script");

e.src = 'http://localhost/userScripts/AutoDownloader.user.js';
e.type = "text/javascript";
document.getElementsByTagName("head")[0].appendChild(e);
*/

"use strict";

/* OBJECT CREATION START */
var Episode = (function () {
    /**
     * An Episode represents an table row in the current page
     * @param   {Number} res          The resolution used for this episode
     * @param   {String} downloadLink The download link for this episode
     * @param   {Number} seeds        The seed count for this episode
     * @param   {Number} leechers     The leech count for this episode
     * @param   {String} uid          The ID of this episode
     * @param   {Number} resides      The page that this Episode resides in
     * @returns {Object} the proto of itself
     */
    function Episode(res, downloadLink, seeds, leechers, uid, resides, title) {

        if (typeof res !== "number") {
            throw "res must be a number";
        }

        if (typeof downloadLink !== "string") {
            throw "downloadLink must be a string";
        }

        if (typeof seeds !== "number") {
            throw "seeds must be a number";
        }

        if (typeof leechers !== "number") {
            throw "leechers must be a number";
        }

        if (typeof uid !== "string") {
            throw "uid must be a string";
        }

        if (typeof resides !== "number") {
            throw "resides must be a number";
        }

        if (typeof title !== "string") {
            throw "Title must be a string";
        }
        var _res = res;

        var _downloadLink = downloadLink;

        var _seeds = seeds;

        var _leechers = leechers;

        var _uid = uid;

        var _resides = resides;

        var _title = title;

        this.getRes = function () {
            return _res;
        };

        this.getDownloadLink = function () {
            return _downloadLink;
        };

        this.getSeeds = function () {
            return _seeds;
        };

        this.getLeechers = function () {
            return _leechers;
        };

        this.getUid = function () {
            return _uid;
        };

        this.getResides = function () {
            return _resides;
        };

        this.getTitle = function () {
            return _title;
        };

        return this;
    }
    Episode.prototype = {
        constructor: Episode
    };
    return Episode;
}());

var Anime = (function () {

    var currentAnime = null;
    var currentSubber = null;

    var _AbstractEps = (function () {
        /**
         * Array of Episode Objects
         */
        var eps = [];

        var abstractGetEps = function (skipSeedLimit) {
            if (typeof skipSeedLimit !== "boolean") {
                throw "skipOptions must be true or false";
            }
            var minSeeds = Options.Seeds.minSeeds;
            var arrayOfEps = [];
            for (var i = 0; i < eps.length; i++) {
                var currentEp = eps[i];
                if (minSeeds > -1 && skipSeedLimit == false) {
                    if (currentEp.getSeeds() < minSeeds) {
                        continue;
                    }
                }
                arrayOfEps.push(currentEp);
            }
            return arrayOfEps;
        };

        var addEp = function (ep) {
            if (!ep instanceof Episode) {
                throw "addEp must take an Episode object";
            }
            if (_validRes(ep.getRes()) === false) {
                throw new TypeError("The Episode supplied does not have a valid resolution");
            }
            eps.push(ep);
        };

        /**
         * Remove an object from an array
         * @param {Array}  arra Array to search in
         * @param {Object} obj Object to remove
         */
        var removeObjectFromArray = function (obj, arra) { // TODO: move this into Utils
            var arr = eps;
            if (typeof arra !== "undefined") {
                arr = arra;
            }
            var i = arr.length;
            while (i--) {
                if (arr[i] === obj) {
                    arr.splice(i, 1);
                }
            }
        };

        return {
            abstractGetEps: abstractGetEps,
            addEp: addEp,
            removeObjectFromArray: removeObjectFromArray
        };
    }());

    /**
     * Array of available resolutions on the page
     */
    var availableRes = [];

    /**
     * Array of supported resolutions for this program
     */
    var supportedRes = [{
        "id": 1,
        "res": 1080,
        "fullRes": "1920x1080"
    }, {
        "id": 2,
        "res": 720,
        "fullRes": "1280x720"
    }, {
        "id": 3,
        "res": 480,
        "fullRes": "640x480"
    }, {
        "id": 4,
        "res": 360,
        "fullRes": "640×360"
    }];

    /**
     * Set the current Anime name
     * @param {String} anime The of the Anime
     */
    var setCurrentAnime = function (anime) {
        if (anime === "*") {
            anime = "Everything";
        }
        currentAnime = anime;
    };

    /**
     * Set the name of the current subber
     * @param {String} sub Name of the current subber
     */
    var setCurrentSubber = function (sub) {
        currentSubber = sub;
    };

    /**
     * Get the current Subber
     * @returns {String} The name of the Subber for this anime
     */
    var getCurrentSubber = function () {
        return currentSubber;
    };

    /**
     * Get the current anime name
     * @returns {String} Name of the anime
     */
    var getCurrentAnime = function () {
        return currentAnime;
    };

    var getSupportedRes = function () {
        return supportedRes;
    };

    var addSupportedRes = function (ResObj) {
        if (typeof ResObj !== "object") {
            throw "ResObj must be a object";
        }
        supportedRes.push(ResObj);
    };

    var getAvailableResolutions = function () {
        return availableRes;
    };

    var addAvailableResolutions = function (res, fullRes) {
        if (typeof res !== "number") {
            throw "res must be of type number";
        }

        if (typeof fullRes !== "string" && fullRes !== null) {
            throw "Full res must be a string or null";
        }

        if (_resExists(res)) {
            return;
        }
        availableRes.push({
            "res": res,
            "fullRes": fullRes
        });
    };

    var removeAvailableResolutions = function (resToRemove) {
        if (typeof resToRemove !== "number" && typeof resToRemove !== "string") {
            throw "the res to remove can only be a number or string";
        }

        for (var i = 0; i < availableRes.length; i++) {
            var currentRes = availableRes[i];
            for (var res in currentRes) {
                if (currentRes.hasOwnProperty(res)) {
                    var localRes = currentRes[res];
                    if (localRes === resToRemove) {
                        _AbstractEps.removeObjectFromArray(currentRes, availableRes);
                    }
                }
            }
        }
    };

    var _resExists = function (_res) {
        for (var i = 0; i < availableRes.length; i++) {
            var currentRes = availableRes[i];
            for (var res in currentRes) {
                if (currentRes.hasOwnProperty(res)) {
                    var localRes = currentRes[res];
                    if (localRes === _res) {
                        return true;
                    }
                }
            }
        }
        return false;
    };


    /**
     * Get the avrage seeds for a specified res
     * @param   {Number} res The res to get the avg seeds for
     * @returns {Number} The avg seeds
     */
    var avgSeedsForRes = function (res, skipSeedLimit) {
        if (typeof res !== "number") {
            throw "res Must be an int";
        }
        var seedCount = 0;
        if (getamountOfEpsFromRes(res, skipSeedLimit) === 0) {
            return 0;
        }
        var eps = _AbstractEps.abstractGetEps(skipSeedLimit);
        for (var i = 0; i < eps.length; i++) {
            var currentEp = eps[i];
            if (currentEp.getRes() === res) {
                seedCount += currentEp.getSeeds();
            }
        }
        return Math.round(seedCount = seedCount / getamountOfEpsFromRes(res, skipSeedLimit));
    };

    /**
     * Get the avrage leechers for a specified res
     * @param   {Number} res The res to get the avg seeds for
     * @returns {Number} The avg leechers
     */
    var avgPeersForRes = function (res, skipSeedLimit) {
        if (typeof res !== "number") {
            throw "res Must be an int";
        }
        var leechCount = 0;
        if (getamountOfEpsFromRes(res, skipSeedLimit) === 0) {
            return 0;
        }
        var eps = _AbstractEps.abstractGetEps(skipSeedLimit);
        for (var i = 0; i < eps.length; i++) {
            var currentEp = eps[i];
            if (currentEp.getRes() === res) {
                leechCount += currentEp.getLeechers();
            }
        }
        return Math.round(leechCount = leechCount / getamountOfEpsFromRes(res, skipSeedLimit));
    };



    /**
     * Get the total amount of eps for a res
     * @param   {Number} res res
     * @returns {Number} The amount of eps for the res
     */
    var getamountOfEpsFromRes = function (res, skipSeedLimit) {
        if (typeof res !== "number") {
            throw "res must be of type 'number'";
        }
        return getEpsForRes(res, skipSeedLimit).length;
    };



    var _isjQueryObject = function (obj) {
        if (obj instanceof jQuery || 'jquery' in Object(obj)) {
            return true;
        } else {
            return false;
        }
    };

    /**
     * Add Episodes to the array
     * @param {Episode} ep The Anime object to add
     */
    var addEps = function (ep) {
        _AbstractEps.addEp(ep);
    };


    /**
     * Add an array of Episode object to the Anime object
     * @param {Array} episode Array of Episode objects to add
     */
    var addAllEps = function (episode) {
        for (var i = 0; i < episode.length; i++) {
            _AbstractEps.addEp(episode[i]);
        }
    };

    var _validRes = function (res) {
        return _resExists(res); // if the res exists, then it's valid
    };

    /**
     * Get the Anime objects for a specified res
     * @param   {Number}  res res to use
     * @returns {Episode} Array of Episodes that match the specified res
     */
    var getEpsForRes = function (res, skipSeedLimit) {
        if (typeof res !== "number") {
            throw "res Must be an int";
        }
        var minSeeds = Options.Seeds.minSeeds;
        var arrayOfEps = [];
        var iterEps = _AbstractEps.abstractGetEps(skipSeedLimit);
        for (var i = 0; i < iterEps.length; i++) {
            var currentEp = iterEps[i];
            if (currentEp.getRes() === res) {
                arrayOfEps.push(currentEp);
            }
        }
        return arrayOfEps;
    };

    /**
     * Given a JQuery object that represents a "tr" of the table, this will return that Episode's UID
     * @param   {Object} obj The Jquery representation of a tr (table row)
     * @returns {String} The UID of that Episode
     */
    var getUidFromJqueryObject = function (obj) {
        if (!_isjQueryObject(obj)) {
            throw "Object must be of type 'Jquery'";
        }
        if (obj.is("tr")) {
            var anchor = (function () {
                var tableRows = obj.find("td.tlistdownload > a");
                if (tableRows.length > 1) {
                    throw "Object must be unique";
                }
                return _getUidFromAnchor(tableRows.get(0));
            }());
            return anchor;
        }
        return null;
    };

    /**
     * Get the Episode from a given anchor tag or url
     * @param   {Object}  anchor Jquery or pure JS anchor dom element or URL String
     * @returns {Episode} The eipside that matches the Anchor
     */
    var getEpisodeFromAnchor = function (anchor) {
        var link = (function () {
            if (_isjQueryObject(anchor)) {
                return anchor.get(0);
            }
            return anchor;
        }());
        var uid = _getUidFromAnchor(link);
        return getEpisodeFromUid(uid, true);
    };

    /**
     * Get the Episode object given a UID
     * @param   {String}  uid The Episode UID
     * @returns {Episode} The Episode that matches the UID
     */
    var getEpisodeFromUid = function (uid, skipSeedLimit) {
        if (typeof uid !== "string") {
            throw "uid must be of type String";
        }
        var iterEp = _AbstractEps.abstractGetEps(skipSeedLimit);
        for (var i = 0; i < iterEp.length; i++) {
            var currentEp = iterEp[i];
            if (currentEp.getUid() === uid) {
                return currentEp;
            }
        }
        return null;
    };

    /**
     * Get an array of Episodes that from the page that it came from
     * @param   {Number}  resides The page where the Episode originated from
     * @param   {Boolean} exclude Match this page only, or exculde this page and return all other objects. if true, will return all Episodes that are not of the passed in page
     * @returns {Episode} Array of Episodes
     */
    var getEpisodesFromResidence = function (resides, exclude, skipSeedLimit) {
        if (typeof resides !== "number") {
            throw "resides must be a number";
        }
        var arrayOfEps = [];
        var iterEp = _AbstractEps.abstractGetEps(skipSeedLimit);
        for (var i = 0; i < iterEp.length; i++) {
            var currentEp = iterEp[i];
            if (exclude === true) {
                if (currentEp.getResides() !== resides) {
                    arrayOfEps.push(currentEp);
                }
            } else {
                if (currentEp.getResides() === resides) {
                    arrayOfEps.push(currentEp);
                }
            }
        }
        return arrayOfEps;
    };

    /**
     * Get the UID from an anchor tag
     * @param   {Object} anchor Dom element of an Anchor
     * @returns {String} The UID
     */
    var _getUidFromAnchor = function (anchor) {
        if (typeof anchor === "string") {
            return anchor.split("=").pop();
        }
        return anchor.href.split("=").pop();
    };

    /**
     * Get an array of all the pages avalible in the Anime (tables)
     * @returns {Array} Array of URLS
     */
    var getPageUrls = function (asHref, page) {
        if (typeof page === "undefined") {
            page = $('div.pages');
        }
        if (typeof asHref !== "boolean") {
            throw "asHref must be a boolean";
        }
        if (asHref === false) {
            return $(page).filter(function (i) {
                return i === 0;
            });
        }
        var urls = [];
        $.each(page.filter(function (i) {
            return i === 0;
        }).find('a'), function (k, v) {
            urls.push(this.href);
        });
        return urls;
    };

    /**
     * Remove an episode from the Episode array based on UI
     * @param {String} uid the UID
     */
    var removeEpisodesFromUid = function (uid) {
        var episodes = getEpisodeFromUid(uid, true);
        for (var i = 0; i < episodes.length; i++) {
            var currentEp = episodes[i];
            _AbstractEps.removeObjectFromArray(currentEp);
        }
    };

    /**
     * Remove all episodes that match a page number
     * @param {Number}  resides The page number
     * @param {Boolean} exclude if true, this will remove all Episode objects that do not match the passed in page number. if false, removes all episodes that match the passed in page number
     */
    var removeEpisodesFromResidence = function (resides, exclude) {
        if (typeof exclude !== "boolean") {
            throw "excluse must be true or false";
        }
        var episodes = getEpisodesFromResidence(resides, exclude, true);
        for (var i = 0; i < episodes.length; i++) {
            var currentEp = episodes[i];
            _AbstractEps.removeObjectFromArray(currentEp);
        }
    };

    var getAmountOfEps = function () {
        return _AbstractEps.abstractGetEps(true).length;
    };


    return {
        setCurrentAnime: setCurrentAnime,
        getCurrentAnime: getCurrentAnime,
        addEps: addEps,
        getEpsForRes: getEpsForRes,
        getamountOfEpsFromRes: getamountOfEpsFromRes,
        setCurrentSubber: setCurrentSubber,
        getCurrentSubber: getCurrentSubber,
        avgSeedsForRes: avgSeedsForRes,
        getUidFromJqueryObject: getUidFromJqueryObject,
        getEpisodeFromUid: getEpisodeFromUid,
        getEpisodeFromAnchor: getEpisodeFromAnchor,
        getPageUrls: getPageUrls,
        addAllEps: addAllEps,
        getEpisodesFromResidence: getEpisodesFromResidence,
        removeEpisodesFromUid: removeEpisodesFromUid,
        removeEpisodesFromResidence: removeEpisodesFromResidence,
        avgPeersForRes: avgPeersForRes,
        getAmountOfEps: getAmountOfEps,
        addAvailableResolutions: addAvailableResolutions,
        getAvailableResolutions: getAvailableResolutions,
        removeAvailableResolutions: removeAvailableResolutions,
        getSupportedRes: getSupportedRes,
        addSupportedRes: addSupportedRes
    };
}());


/** Utility functions ***/

var Utils = (function () {
    var sortSelect = function (selElem) {
        var tmpAry = [];
        for (var i = 0; i < selElem.options.length; i++) {
            tmpAry[i] = [];
            tmpAry[i][0] = selElem.options[i].text;
            tmpAry[i][1] = selElem.options[i].dataset.url;
        }
        tmpAry.sort(function (a, b) {
            return a[0].toUpperCase().localeCompare(b[0].toUpperCase());
        });
        while (selElem.options.length > 0) {
            selElem.options[0] = null;
        }
        for (var i = 0; i < tmpAry.length; i++) {
            var op = new Option(tmpAry[i][0]);
            op.dataset.url = tmpAry[i][1]
            selElem.options[i] = op;
        }
    };

    /**
     * Disable the given button
     * @param {Object} button Jquery object of the button
     */
    var disableButton = function (button) {
        button.prop('disabled', true);
    };

    /**
     * Enable the given button
     * @param {Object} button Jquery object of the button
     */
    var enableButton = function (button) {
        button.prop('disabled', false);
    };

    /**
     * Do the downloads
     * @param {Object} event The event to decide if it is a download all or a downlaod selection (to make this method more abstract)
     */
    var doDownloads = function (event) {
        $("#crossPage").prop("disabled", true);
        var type = $(event.target).data("type");
        var amountOfAnime;
        var collectionOfAnime;
        var download = false;
        var html = UI.builDownloadAlert(type);
        var epsToDownload = [];
        $("#alertUser").html(html).slideDown("slow");

        if (type === "downloadSelected") {
            $.each($('.checkboxes:checked').prev('a'), function (k, v) {
                epsToDownload.push(Anime.getEpisodeFromAnchor(this));
            });
        } else if (type === "downloadSelects") {
            $.each($('#animeSelection option:selected'), function (k, v) {
                epsToDownload.push(Anime.getEpisodeFromAnchor($(this).data("url")));
            });
        } else {
            epsToDownload = Anime.getEpsForRes(parseInt($("#downloadRes").val()), false);
        }

        bindAlertControls();

        function bindAlertControls() {
            $("#alertButtonCancel").on("click", function () {
                $("#alertUser").slideUp("slow");
                $("#crossPage").prop("disabled", false);
            });

            $("#alertButton").on("click", function () {
                doIt(epsToDownload);
            });
        }

        function doIt(eps) {
            for (var i = 0; i < eps.length; i++) {
                var currentEp = eps[i];
                var currentDownloadLink = currentEp.getDownloadLink();
                var link = document.createElement("a");
                link.download = currentEp.getTitle();
                link.href = currentDownloadLink;
                link.click();
                link.remove();
            }
            $("#alertUser").slideUp("slow");
            $("#crossPage").prop("disabled", false);
        }
    };

    /**
     * Returns if the checkbox is checked
     * @param   {Object}  checkbox The checkbox
     * @returns {Boolean} If ehcked or not
     */
    var checkBoxValid = function (checkbox) {
        return checkbox.is(":checked");
    };

    var _minSeedsSet = function () {
        return Options.Seeds.minSeeds !== -1;
    };

    /**
     * Return the current page offset (what table page you are on)
     * @returns {Number} The offset
     */
    var getCurrentPageOffset = function () {
        return parseInt((typeof QueryString.offset === "undefined") ? 1 : QueryString.offset);
    };

    /**
     * Returns true of false if you can support HTML5 storeag
     * @returns {Boolean}
     */
    var html5StoreSupport = function () {
        try {
            return 'localStorage' in window && window['localStorage'] !== null;
        } catch (e) {
            return false;
        }
    };

    var cleanAvailableResolutions = function () {
        var avRes = Anime.getAvailableResolutions();
        for (var i = 0; i < Anime.getAvailableResolutions().length; i++) {
            var currentRes = avRes[i].res;
            if (Anime.getamountOfEpsFromRes(currentRes, true) === 0) {
                Anime.removeAvailableResolutions(currentRes);
            }
        }
    };

    var injectCss = function (css) {
        if (_isUrl(css)) {
            $("<link>").prop({
                "type": "text/css",
                "rel": "stylesheet"
            }).attr("href", css).appendTo("head");
        } else {
            $("<style>").prop("type", "text/css").html(css).appendTo("head");
        }
    };

    var _isUrl = function (url) {
        var matcher = new RegExp(/^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/);
        return matcher.test(url);
    };

    var sortAllSelects = function () {
        console.time("time");
        sortSelect(document.getElementById("animeSelection"));
        sortSelect(document.getElementById("downloadRes"));
        console.timeEnd("time");
    }

    var reBind = function () {
        $("input[name='filterSelect']").off("change").on("change", handleSelect);

        $("#clearResOptions").off("click").on("click", handleSelect);

        function handleSelect(event) {
            var resTOFilter = $(event.target).data("set")
            $("#selectContainer").html(UI.buildSelect(resTOFilter));
            Utils.sortAllSelects();
            reBind();
        }
    };

    return {
        disableButton: disableButton,
        enableButton: enableButton,
        doDownloads: doDownloads,
        checkBoxValid: checkBoxValid,
        getCurrentPageOffset: getCurrentPageOffset,
        html5StoreSupport: html5StoreSupport,
        injectCss: injectCss,
        cleanAvailableResolutions: cleanAvailableResolutions,
        sortAllSelects: sortAllSelects,
        reBind: reBind,
        sortSelect: sortSelect
    };
}());

var UI = (function () {
    /**
     * Build the download infomation table
     * @returns {String} The html of the built table
     */
    var buildTable = function () {
        var html = "";
        html += "<table style='width: 100%' id='info'>";

        html += "<caption>Download infomation</caption>";

        html += "<thead>";
        html += "<tr>";
        html += "<th>resolution</th>";
        html += "<th>Episode count</th>";
        html += "<th>Average seeds</th>";
        html += "<th>Average leechers</th>";
        html += "</tr>";
        html += "</thead>";

        html += "<tbody>";
        var allRes = Anime.getAvailableResolutions();
        for (var i = 0; i < allRes.length; i++) {
            var currRes = allRes[i];
            var localRes = currRes.res;
            html += "<tr>";
            html += "<td>" + (localRes === -1 ? "Others" : localRes + "p") + "</td>";
            html += "<td>" + Anime.getamountOfEpsFromRes(localRes, true) + "</td>";
            html += "<td>" + Anime.avgSeedsForRes(localRes, true) + "</td>";
            html += "<td>" + Anime.avgPeersForRes(localRes, true) + "</td>";
            html += "</tr>";
        }

        html += "</tbody>";
        html += "</table>";


        return html;
    };

    var buildDropdownSelections = function () {
        var html = "";
        html += "<select style=\"margin-right:5px;\" id=\"downloadRes\">";
        var allRes = Anime.getAvailableResolutions();
        for (var i = 0; i < allRes.length; i++) {
            var currRes = allRes[i];
            var localRes = currRes.res;
            html += "<option value=" + localRes + ">" + (localRes === -1 ? "Others" : localRes + "p") + "</option>";
        }

        html += "</select>";

        return html;
    };

    var builDownloadAlert = function (type) {
        if (typeof type !== "string") {
            throw "type must a string";
        }
        var amountOfAnime = 0;
        var selectedRes = parseInt($("#downloadRes").val());
        var res = null;
        if (type === "downloadSelected") {
            amountOfAnime = $(".checkboxes:checked").length;
            res = "custom";
        } else if (type === "downloadSelects") {
            amountOfAnime = $("#animeSelection option:selected").length;
            res = "custom";
        } else {
            amountOfAnime = Anime.getamountOfEpsFromRes(selectedRes, false);
            res = selectedRes === -1 ? "Others" : selectedRes + "p";
        }
        var seedLimit = Options.Seeds.minSeeds === -1 ? "None" : Options.Seeds.minSeeds;


        var html = "";
        html += "<div class='alert alert-success'>";
        html += "<div><strong>Download: " + res + "</strong></div> <br />";
        html += "<div><strong>Seed Limit: " + seedLimit + "</strong></div>";
        html += "<p>You are about to download " + amountOfAnime + " ep(s)</p>";
        html += "<p>This will cause " + amountOfAnime + " download pop-up(s) Are you sure you want to continue?</p>";
        html += "<p>If there are a lot of eps, your browser might stop responding for a while. This is normal. If you are on Google Chrome, it will ask you to allow multiple-downloads</p>";
        html += "<button id='alertButton'>Okay</button>";
        html += "<button id='alertButtonCancel'>Cancel</button>";
        html += "</div>";
        return html;
    };

    var showAjaxErrorAlert = function (AjaxInfo) {
        if (!$("#parseErrors").is(":hidden")) {
            return;
        }
        $("#parseErrors").html("");
        var html = "";
        html += "<div class='alert alert-danger'>";
        html += "<p>There was an error in getting the infomation from page: '" + AjaxInfo.error.pageAtError + "'</p>";
        html += "<p>The last successful page parsed was page number " + (AjaxInfo.currentPage === null ? 1 : AjaxInfo.currentPage) + " </p>";
        html += "<button id='errorClose'> close </button>";
        html += "</div>";
        $("#parseErrors").html(html);
        $("#parseErrors").slideDown("slow");
        $("#errorClose").off("click").on("click", function () {
            $("#parseErrors").slideUp("slow", function () {
                $(this).html("");
            });
        });
    };


    var buildSelect = function (resTOFilter) {
        resTOFilter = typeof resTOFilter === "undefined" ? "none" : resTOFilter;
        var html = "";
        html += "<div id='selectContainer'>";
        html += "<p>Or you can select episodes here:</p>";
        html += "<p>Seed limit: " + (Options.Seeds.minSeeds === -1 ? "None" : Options.Seeds.minSeeds) + "</p>"
        html += "<select id='animeSelection' multiple size='20'>";
        var allRes = Anime.getAvailableResolutions();
        for (var i = 0; i < allRes.length; i++) {
            var currRes = allRes[i];
            var localRes = currRes.res;
            var eps = Anime.getEpsForRes(localRes, false);
            for (var j = 0; j < eps.length; j++) {
                var currentEp = eps[j];
                if (resTOFilter == "none") {
                    html += "<option data-url=" + currentEp.getDownloadLink() + ">";
                    html += currentEp.getTitle();
                    html += "</option>";
                } else {
                    if (currentEp.getRes() == resTOFilter) {
                        html += "<option data-url=" + currentEp.getDownloadLink() + ">";
                        html += currentEp.getTitle();
                        html += "</option>";
                    }
                }
            }
        }
        html += "</select>";
        html += "<span>Filter select control: </span>";
        var checked = false;
        for (var i = 0; i < allRes.length; i++) {
            if (resTOFilter == allRes[i].res) {
                checked = true;
            }
            html += "<input type='radio' " + (checked ? "checked" : "") + " data-set= '" + allRes[i].res + "' name='filterSelect'/>" + "<label>" + (allRes[i].res === -1 ? "Others" : allRes[i].res + "p") + "</label>";
            checked = false;
        }
        html += "<a href='#' id='clearResOptions' data-set='none' >clear</a>";
        html += "</div>";
        return html;
    };


    var buildCustomResOptons = function () {
        $("input[name='filterSelect']").on("change", function (e) {
            var resTOFilter = this.id;
            $("#selectContainer").html(UI.buildSelect(resTOFilter));
            Utils.reBind();
        });
    };

    return {
        buildDropdownSelections: buildDropdownSelections,
        buildTable: buildTable,
        builDownloadAlert: builDownloadAlert,
        showAjaxErrorAlert: showAjaxErrorAlert,
        buildSelect: buildSelect
    };
}());

var DataParser = (function () {
    var table = null;

    var isParsing = false;

    var setTable = function (_table) {
        table = _table;
    };
    /**
     * Parses a table and returns an array of Episodes from it
     * @param   {Object}  table Jquery representation of the anime table
     * @returns {Episode} Array of Episodes
     */
    var parseTable = function (currentPage) {
        if (table === null) {
            throw "no table to parse on, table is null";
        }
        var trRow = table.find("img[src='http://files.nyaa.se/www-37.png']").closest("tr");

        var eps = [];

        $.each($(trRow), function (k, v) {
            var Dres = parseRes(this);
            if (Dres === -1) {
                Anime.addAvailableResolutions(-1, null);
            } else {
                Anime.addAvailableResolutions(Dres.res, Dres.fullRes);
            }
            var info = getEpisodeInfo(this);
            eps.push(new Episode(typeof Dres.res === ("undefined") ? -1 : Dres.res, info.currentDownloadLink, info.seeds, info.leech, info.uid, currentPage, info.title));

        });
        return eps;

        function parseRes(eventContent) {
            var suppRes = Anime.getSupportedRes();
            for (var i = 0; i < Anime.getSupportedRes().length; i++) {
                var currRes = suppRes[i].res;
                var currFullRes = suppRes[i].fullRes;
                if ($(eventContent).children("td:nth-child(2)").text().indexOf(currRes + "p") > -1 || $(eventContent).children("td:nth-child(2)").text().indexOf(currFullRes) > -1) {
                    return suppRes[i];
                }
            }
            return -1;
        }

        function getEpisodeInfo(eventContent) {
            return {
                "currentDownloadLink": $(eventContent).find('td:nth-child(3) >a').attr('href'),
                "seeds": (isNaN(parseInt($(eventContent).find("td.tlistsn").text()))) ? 0 : parseInt($(eventContent).find("td.tlistsn").text()),
                "leech": (isNaN(parseInt($(eventContent).find("td.tlistln").text()))) ? 0 : parseInt($(eventContent).find("td.tlistln").text()),
                "title": $(eventContent).children(".tlistname").text(),
                "uid": Anime.getUidFromJqueryObject($(eventContent))
            };
        }
    };

    return {
        parseTable: parseTable,
        setTable: setTable,
        isParsing: isParsing
    };
}());

var QueryString = function () {
    var query_string = {};
    var query = window.location.search.substring(1);
    var vars = query.split('&');
    for (var i = 0; i < vars.length; i++) {
        var pair = vars[i].split('=');
        if (typeof query_string[pair[0]] === 'undefined') {
            query_string[pair[0]] = pair[1];
        } else if (typeof query_string[pair[0]] === 'string') {
            var arr = [
        query_string[pair[0]],
        pair[1]
      ];
            query_string[pair[0]] = arr;
        } else {
            query_string[pair[0]].push(pair[1]);
        }
    }
    return query_string;
}();

var Options = (function () {
    var Seeds = {};
    var Res = {};
    Object.defineProperty(Seeds, "minSeeds", {
        enumerable: true,
        set: function (seeds) {
            if (typeof seeds !== "number") {
                throw "seeds must be a number";
            }
            this._minSeeds = seeds;
            if (Utils.html5StoreSupport() === true) { // also set it on the local DB
                if (this._minSeeds === -1) {
                    Localstore.removeMinSeedsFromStore();
                } else {
                    Localstore.setMinSeedsFromStore(this._minSeeds);
                }
            }
        },
        get: function () {
            return typeof this._minSeeds === "undefined" ? -1 : this._minSeeds;
        }

    });

    Object.defineProperties(Res, {
        "getAll": {
            get: function () {
                return Anime.getSupportedRes();
            }
        },
        "add": {
            value: function (customRes) {
                if (typeof customRes !== "object") {
                    throw "res must be an object";
                }
                if (typeof customRes.fullRes !== "string") {
                    throw new TypeError("Full res must be a string");
                }

                if (typeof customRes.res !== "number") {
                    throw new TypeError("res must be a number");
                }

                if (typeof customRes.id !== "number") {
                    throw new TypeError("id must be a number");
                }
                try {
                    Localstore.addCustomResToStore(customRes);
                } catch (e) {
                    throw (e);
                }
            }
        },

        "remove": {
            value: function (resToRemove) {
                if (resToRemove !== null && typeof resToRemove !== "number") {
                    throw "resToRemove must be null or an number";
                }
                Localstore.removeCustomResFromStore(resToRemove)
            }
        }
    });

    return {
        Seeds: Seeds,
        Res: Res
    };
}());

//Local storeage object
var Localstore = {
    getMinSeedsFromStore: function () {
        return localStorage.getItem("minSeeds");
    },
    setMinSeedsFromStore: function (seeds) {
        localStorage.setItem('minSeeds', seeds);
    },
    removeMinSeedsFromStore: function () {
        localStorage.removeItem("minSeeds");
    },

    addCustomResToStore: function (resObject) {
        // get all current custom res
        var currentReses = JSON.parse(localStorage.getItem("customRes"));
        if (currentReses === null) {
            localStorage.setItem("customRes", JSON.stringify([]));
            return;
        }
        for (var i = 0; i < currentReses.length; i++) {
            var currResObj = currentReses[i];
            if (resObject.res === currResObj.res || resObject.fullRes === currResObj.fullRes) {
                throw TypeError("ERROR: The supplied resolution already exists");
            }
        }
        // add new res
        currentReses.push(resObject);

        // convert back into string
        var parsedRes = JSON.stringify(currentReses);

        localStorage.setItem("customRes", parsedRes);

    },

    getCustomResFromStore: function () {
        return JSON.parse(localStorage.getItem("customRes"));
    },

    removeCustomResFromStore: function (id) {
        if (id === null) {
            localStorage.removeItem("customRes");
            return;
        }
        var allObj = this.getCustomResFromStore();
        for (var i = 0; i < allObj.length; i++) {
            var currObjId = allObj[i].id;
            if (currObjId === id) {
                _removeObjectFromArray(allObj, allObj[i]);
            }
        }
        var parsedRes = JSON.stringify(allObj);

        localStorage.setItem("customRes", parsedRes);

        function _removeObjectFromArray(arr, obj) {
            var i = arr.length;
            while (i--) {
                if (arr[i] === obj) {
                    arr.splice(i, 1);
                }
            }
        }
    }
};

// Download fix for firefox
HTMLElement.prototype.click = function () {
    var evt = this.ownerDocument.createEvent('MouseEvents');
    evt.initMouseEvent('click', true, true, this.ownerDocument.defaultView, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
    this.dispatchEvent(evt);
};

/* OBJECT CREATION END */

$(document).ready(function () {
    init(); // init the pannel and set up objects and listeners
    AfterInit(); // set page laod items and settings after the object and ui is built
});

function init() {
    setAnimeObj();
    buildUi();
    bindListeners();


    function setAnimeObj() {
        // Set currentAnime
        if (QueryString.term !== "") {
            Anime.setCurrentAnime(decodeURIComponent(QueryString.term).split("+").join(" "));
        } else {
            Anime.setCurrentAnime("Unknown");
        }

        // set subber

        Anime.setCurrentSubber($(".notice > a > *").html());

        // Set eps

        DataParser.setTable($("table.tlist"));

        // set the seed limit
        var eps = DataParser.parseTable(Utils.getCurrentPageOffset());
        if (Localstore.getMinSeedsFromStore() !== null) {
            var minSeeds = parseInt(Localstore.getMinSeedsFromStore());
            Options.Seeds.minSeeds = minSeeds;
        }

        Anime.addAllEps(eps);

    }

    function buildUi() {
        makeStyles();
        buildPanel();
        afterBuild();

        function makeStyles() {
            var styles = "";

            // Panel
            styles += ".panel{background-color: #fff; border: 1px solid transparent; border-radius: 4px; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); margin-bottom: 20px; margin-left: 8px; margin-right: 8px; width: auto; margin-top: 19px;}";
            styles += ".panel-success {border-color: #d6e9c6;}";
            styles += ".collapse{cursor: pointer; position: absolute; right: 4px; top: 2px;}";
            styles += ".panel-heading{   border-bottom: 1px solid transparent; border-top-left-radius: 3px; border-top-right-radius: 3px; padding: 4px 15px; text-align:center}";
            styles += ".panel-success > .panel-heading {background-color: #dff0d8; border-color: #d6e9c6; color: #3c763d; position: relative;}";
            styles += ".panel-title {color: inherit; margin-bottom: 0; margin-top: 0; padding: 6px; display: inline-block;}";
            styles += ".panel-body {padding: 15px;}";
            styles += ".avgSeeds{floar:left; padding-right:10px; color:#3c763d;}";
            styles += ".checkboxes{left:1px; margin:0; padding:0; position: relative; top: 1px; z-index: 1;}";
            styles += "#topbar{z-index: 2 !important;}";

            // Infomation
            styles += "#info, #info th, #info td {border: 1px solid black;border-collapse: collapse;}";
            styles += "#info th, #info td {padding: 5px;text-align: left;}";
            styles += "label[for='MinSeeds']{ display: block; margin-top: 10px;}";
            styles += "#SaveMinSeeds{margin-left:5px;}";

            // Alerts
            styles += ".alert {border: 1px solid transparent;border-radius: 4px;margin-bottom: 20px;padding: 15px; position:relative;}";
            styles += ".alert-success {background-color: #dff0d8;border-color: #d6e9c6;color: #3c763d;}";
            styles += ".alert-danger {color: #A94442;background-color: #F2DEDE;border-color: #EBCCD1;}";
            styles += "#alertUser, #parseErrors{margin-top: 15px;}";
            styles += "#alertButton{position:absolute; bottom:5px; right:5px;}";
            styles += "#alertButtonCancel{position:absolute; bottom:5px; right: 66px;}";
            styles += "#errorClose{position:absolute; bottom:5px; right: 11px;}";

            // Anime Selects
            styles += "#animeSelection{width: 100%;}";
            styles += "#clearResOptions{margin-left: 10px;}";

            // Custom Resolutions

            styles += "#customResButtonDiv {margin-top:10px;}";

            styles += "#downloadCustomButton{float:right;}";
            Utils.injectCss(styles);
            Utils.injectCss("https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css");

        }

        function buildPanel() {
            var html = "";
            html += '<div class="panel panel-success">';

            html += '<div class="panel-heading">';
            html += '<h3 id="panel-title" class="panel-title"></h3>';
            html += '<i class="fa fa-minus collapse" id="collapseToggle" title="Hide"></i>';
            html += "</div>";

            html += '<div class="panel-body" id="pannelContent"></div>';

            html += '</div>';
            $(".content > .notice").after(html);
            buildPanelContent();

            function buildPanelContent() {
                var html = "";
                html += "<div>";
                $("#panel-title").html("<span> Download \"" + Anime.getCurrentAnime() + " (" + Anime.getCurrentSubber() + ")\"</span>");
                if (Anime.getAmountOfEps() === 0) {
                    html += "<span> No translated anime found or error occured</span>";
                    html += "</div>";
                    $("#pannelContent").html(html);
                    return;
                }
                html += "<span>Pick a resolution: </span>";

                html += "<span id='selectDownload'>";

                html += UI.buildDropdownSelections();

                html += "</span>";

                html += "<button type=\"button\" data-type='downloadAll' id=\"downloadAll\">Download all</button>";

                html += "<button type='button' id='downloadCustomButton' data-type='downloadSelected' >download your selected items</button>";

                html += "</div>";

                html += "<div id='options'>";


                html += "<label for='crossPage'> include Cross pages</label>";
                html += "<input type='checkbox' id='crossPage' /> ";

                html += "<label for='MinSeeds'>Minimum seeders:</label>";
                html += "<input type='number' min='0' id='MinSeeds' title='Any episode that is below this limit will be excluded from the download.'/>";
                html += "<button type='button' id='SaveMinSeeds'>Save</button>";

                /*  html += "<div id='customResButtonDiv'>";
                html += "<button id='customRes'> Manage resolutions </button>";
                html += "</div>";
                */
                html += "</div>";

                html += "<div id='tableInfo'>";
                html += UI.buildTable();
                html += "</div>";

                html += "<div id='alertUser' class='hide'></div>";

                html += "<div class='selectAnime' id='selectAnime'>";
                html += UI.buildSelect();
                html += "</div>";
                html += "<button id='acceptSelect' data-type='downloadSelects'>Select for download</button>";

                html += "<div id='parseErrors' class ='hide'></div>";
                $("#pannelContent").html(html);
            }
        }

        function afterBuild() {
            makeCheckBoxes();
            sortLists();

            function makeCheckBoxes() {
                $(".tlistdownload > a").after("<input class='checkboxes' type='checkbox'/>");
            }

            function sortLists() {
                Utils.sortAllSelects();
            }
        }

    }

    function bindListeners() {
        $("#downloadAll").on("click", function (e) {
            Utils.doDownloads(e);
        });

        $("#downloadCustomButton").on("click", function (e) {
            Utils.doDownloads(e);
        });

        $(".checkboxes").on("click", function (e) {
            if (Utils.checkBoxValid($(".checkboxes"))) {
                Utils.enableButton($("#downloadCustomButton"));
            } else {
                Utils.disableButton($("#downloadCustomButton"));
            }
        });

        $("#crossPage").on("click", function (e) {
            preformParsing(Anime.getPageUrls(true));

            function preformParsing(urls) {
                if (Utils.checkBoxValid($("#crossPage"))) {
                    $("#tableInfo").html("<p>Please wait while we parse each page...</p>");
                    $("#selectContainer").html("");
                    $("#acceptSelect").hide();
                    $("#crossPage, #downloadAll").prop("disabled", true);
                    $("#parseErrors").slideUp("fast", function () {
                        $(this).html("");
                    });
                    var AjaxInfo = {
                        error: {
                            pageAtError: null
                        },
                        currentPage: null
                    };
                    urls.reduce(function (prev, cur, index) {
                        return prev.then(function (data) {
                            return $.ajax(cur).then(function (data) {
                                DataParser.isParsing = true;
                                var currentPage = cur.split("=").pop();
                                if (cur.indexOf("offset") === -1) {
                                    currentPage = 1;
                                }
                                AjaxInfo.currentPage = currentPage;
                                var table = $(data).find("table.tlist");
                                DataParser.setTable(table);
                                Anime.addAllEps(DataParser.parseTable(parseInt(currentPage)));
                                $("#tableInfo").append("<div>Page " + currentPage + " Done </div>");
                            }, function () {
                                AjaxInfo.error.pageAtError = cur.split("=").pop();
                            });
                        });
                    }, $().promise()).always(function () {
                        if (AjaxInfo.error.pageAtError !== null) {
                            UI.showAjaxErrorAlert(AjaxInfo);
                        }
                        $("#tableInfo").html(UI.buildTable());
                        $("#downloadRes").html(UI.buildDropdownSelections());
                        $("#selectContainer").html(UI.buildSelect());
                        Utils.sortAllSelects();
                        $("#acceptSelect").show();
                        Utils.reBind();
                        $("#crossPage, #downloadAll").prop("disabled", false);
                        DataParser.isParsing = false;
                    });
                } else { // when un-chekced, clear the Episodes of all eps that are not of the current page
                    $("#tableInfo").html("<p>Please wait while we re-calculate the Episodes</p>");
                    var currentPage = Utils.getCurrentPageOffset();
                    Anime.removeEpisodesFromResidence(currentPage, true);
                    Utils.cleanAvailableResolutions();
                    $("#downloadRes").html(UI.buildDropdownSelections());
                    $("#tableInfo").html(UI.buildTable());
                    $("#selectContainer").html(UI.buildSelect());
                    Utils.reBind();
                    Utils.sortAllSelects();
                }

            }
        });

        $("#SaveMinSeeds").on("click", function (e) {
            if (parseInt($("#MinSeeds").val()) < 0) {
                alert("number cannot be negative");
                return;
            }
            var value = parseInt($("#MinSeeds").val() === "" ? -1 : $("#MinSeeds").val());
            Options.Seeds.minSeeds = value;
            if (value === -1) {
                alert("Minimum seeds have been cleared");
            } else {
                alert("Minimum seeds now set to: " + value);
            }
            $("#selectContainer").html(UI.buildSelect());
            Utils.sortAllSelects();
            Utils.reBind();
        });

        $("#collapseToggle").on("click", function () {
            $("#pannelContent").stop(true, true).slideToggle("slow", function () {
                if ($(this).is(":hidden")) {
                    $("#collapseToggle").removeClass("fa-minus").addClass("fa-plus");
                    $("#collapseToggle").attr("title", "Show");
                } else {
                    $("#collapseToggle").addClass("fa-minus").removeClass("fa-plus");
                    $("#collapseToggle").attr("title", "Hide");
                }
            });
        });

        Utils.reBind();

        $("#acceptSelect").on("click", function (e) {
            Utils.doDownloads(e);
        });
    }
}

function AfterInit() {
    initButtonStates();
    if (Utils.html5StoreSupport()) {
        setOptionsFromLocalStore();
    }

    function initButtonStates() {
        if (Utils.checkBoxValid($(".checkboxes"))) {
            Utils.enableButton($("#downloadCustomButton"));
        } else {
            Utils.disableButton($("#downloadCustomButton"));
        }
    }

    function setOptionsFromLocalStore() {
        // Min seeds
        if (Localstore.getMinSeedsFromStore() !== null) {
            $("#MinSeeds").val(Options.Seeds.minSeeds);
        }

        //customRes 
        /* In devlopment
        if (Options.Res.getAll === null) {
            Options.Res.add({});
        } else {
            Anime.addSupportedRes(Options.Res.getAll);
        }
        */
    }
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址