// ==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);
}
*/
}
}