// ==UserScript==
// @name Nyaa.si Batch downloader
// @namespace Autodownload
// @author Victorique
// @description Batch download torrents from nyaa.si
// @include *://nyaa.si/user/*?q=*
// @include *://nyaa.si/user/*?f=*&c=*&q=*
// @version 6.1.2
// @icon https://i.imgur.com/nx5ejHb.png
// @license MIT
// @run-at document-idle
// @grant none
// @require https://gf.qytechs.cn/scripts/19117-jsutils/code/JsUtils.js
// @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/1.3.3/FileSaver.min.js
// ==/UserScript==
/*
var e = document.createElement("script");
e.src = 'http://localhost/userScripts/AutoDownloader.user.js';
e.type = "application/javascript;version=1.7";
document.getElementsByTagName("head")[0].appendChild(e);
*/
/* OBJECT CREATION START */
'use strict';
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
* @param {String} title The title of the episode
* @param {Number} size The size in MB of the episode
* @returns {Object} the proto of itself
*/
function Episode(res, downloadLink, seeds, leechers, uid, resides, title, size) {
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';
}
if (typeof size !== 'number') {
throw 'size must be a number';
}
var _res = res;
var _downloadLink = downloadLink;
var _seeds = seeds;
var _leechers = leechers;
var _uid = uid;
var _resides = resides;
var _title = title;
var _size = size;
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;
};
this.getSize = function () {
return _size;
};
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;
if (minSeeds > -1 && skipSeedLimit == false) {
var arrayOfEps = [
];
for (let i = 0, len = eps.length; i < len; i++) {
var currentEp = eps[i];
if (currentEp.getSeeds() < minSeeds) {
continue;
}
arrayOfEps.push(currentEp);
}
return arrayOfEps;
} else {
return eps;
}
};
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');
}
for (let i = 0, len = eps.length; i < len; i++) {
var epi = eps[i];
if (Utils.deepEquals(epi, ep)) {
console.warn('The episode supplied already exsists, this episode has been ignored');
return;
}
}
eps.push(ep);
};
var removeEpisodeFromAnime = function (obj) {
var arr = eps;
let i = arr.length;
while (i--) {
if (arr[i] === obj) {
arr.splice(i, 1);
}
}
};
return {
abstractGetEps: abstractGetEps,
addEp: addEp,
removeEpisodeFromAnime: removeEpisodeFromAnime
};
}());
/**
* 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': '640x360'
}
];
/**
* 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 (let 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) {
availableRes._remove_(currentRes);
}
}
}
}
};
var _resExists = function (_res) {
for (let 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 number';
}
if (typeof skipSeedLimit !== 'boolean') {
throw 'skipSeedLimit Must be an boolean';
}
var seedCount = 0;
if (getamountOfEpsFromRes(res, skipSeedLimit) === 0) {
return 0;
}
var eps = _AbstractEps.abstractGetEps(skipSeedLimit);
for (let i = 0, len = eps.length; i < len; 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 number';
}
if (typeof skipSeedLimit !== 'boolean') {
throw 'skipSeedLimit Must be an boolean';
}
var leechCount = 0;
if (getamountOfEpsFromRes(res, skipSeedLimit) === 0) {
return 0;
}
var eps = _AbstractEps.abstractGetEps(skipSeedLimit);
for (let i = 0, len = eps.length; i < len; i++) {
var currentEp = eps[i];
if (currentEp.getRes() === res) {
leechCount += currentEp.getLeechers();
}
}
return Math.round(leechCount = leechCount / getamountOfEpsFromRes(res, skipSeedLimit));
};
var getTotalSizeForRes = function (res, skipSeedLimit, decimals) {
if (typeof res !== 'number') {
throw 'res Must be an number';
}
if (typeof skipSeedLimit !== 'boolean') {
throw 'skipSeedLimit Must be an boolean';
}
var eps = getEpsForRes(res, skipSeedLimit);
return Utils.getHumanReadableSize(eps, decimals);
};
var getTotalSizeFromEps = function (eps, decimals) {
if (!Array.isArray(eps) && !(eps instanceof Episode)) {
throw 'eps Must be an array or a single Episode';
}
return Utils.getHumanReadableSize(eps, decimals);
};
/**
* 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;
};
/**
* 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 (let 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 arrayOfEps = [
];
var eps = _AbstractEps.abstractGetEps(skipSeedLimit);
for (let i = 0, len = eps.length; i < len; i++) {
var currentEp = eps[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 (!Utils.isjQueryObject(obj)) {
throw 'Object must be of type \'Jquery\'';
}
if (obj.is('tr')) {
var anchor = (function () {
let currectTd = getNameTr(obj);
var tableRows = currectTd.find('a:not(a.comments)');
if (tableRows.length > 1) {
throw 'Object must be unique';
}
return _getUidFromAnchor(tableRows.get(0));
}());
return anchor;
}
return null;
};
function getNameTr(obj) {
return obj.find('td:nth-child(2)');
}
/**
* 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 (Utils.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 eps = _AbstractEps.abstractGetEps(skipSeedLimit);
for (let i = 0, len = eps.length; i < len; i++) {
var currentEp = eps[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 eps = _AbstractEps.abstractGetEps(skipSeedLimit);
for (let i = 0, len = eps.length; i < len; i++) {
var currentEp = eps[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') {
if (anchor.indexOf(".torrent") > -1) {
return anchor.replace(".torrent", "").split("/").pop();
}
return anchor.split("/").pop();
}
return anchor.href.split("/").pop();
};
var getTdFromTable = function getTdFromTable(table, index) {
return table.find('td:nth-child(' + index + ')');
};
/**
* Get an array of all the pages avalible in the Anime (tables)
* @returns {Array} Array of URLS
*/
var getPageUrls = function () {
function range(start, end) {
return Array(end - start + 1).fill().map((_, idx) => start + idx)
}
let pages = $(".center > ul.pagination a");
if (pages.length === 0) {
return [];
}
let firstPage = ObjectUtil.getElementFromJqueryArray(pages, 1);
let lastPage = ObjectUtil.getElementFromJqueryArray(pages, pages.length - 2);
let firstPageNumber = Number.parseInt(firstPage.text());
let lastPageNumber = Number.parseInt(lastPage.text());
let rangeBetween = range(firstPageNumber, lastPageNumber);
let baseUrl = window.location.href;
let urls = [];
var currentPage = QueryString.p === undefined ? 1 : QueryString.p;
for (let i = 0; i < rangeBetween.length; i++) {
let num = rangeBetween[i];
if (num == currentPage) { // skip current page
continue;
}
let newUrl = UrlUtils.addParameter(baseUrl, "p", num.toString());
urls.push(newUrl);
}
return urls;
};
/**
* Remove an episode from the Episode array based on UI
* @param {String} uid the UID
*/
var removeEpisodesFromUid = function (uid) {
var episode = getEpisodeFromUid(uid, true);
_AbstractEps.removeEpisodeFromAnime(episode);
};
/**
* 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 eps = getEpisodesFromResidence(resides, exclude, true);
for (let i = 0, len = eps.length; i < len; i++) {
var currentEp = eps[i];
_AbstractEps.removeEpisodeFromAnime(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,
getTotalSizeForRes: getTotalSizeForRes,
getTotalSizeFromEps: getTotalSizeFromEps,
getTdFromTable: getTdFromTable
};
}());
/** Utility functions ***/
var Utils = (function () {
var sortSelect = function (selElem) {
var tmpAry = [];
for (let i = 0, length = selElem.options.length; i < 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());
});
selElem.innerHTML = "";
for (let i = 0, len = tmpAry.length; i < len; i++) {
var op = new Option(tmpAry[i][0]);
op.dataset.url = tmpAry[i][1];
selElem.options[i] = op;
}
};
let getTable = function getTable() {
return $('table');
};
let getQueryFromUrl = function getQueryFromUrl(url) {
var obj = url.split("&").reduce(function (prev, curr, i, arr) {
var p = curr.split("=");
prev[decodeURIComponent(p[0])] = decodeURIComponent(p[1]);
return prev;
}, {});
return obj;
};
let isjQueryObject = function (obj) {
return ObjectUtil.isjQuery(obj);
};
/**
* 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 urlsToDownload = [
];
$('#alertUser').html(html).slideDown("slow").show();
if (type === 'downloadSelected') {
$.each($('.checkboxes:checked').prev('a'), function (k, v) {
var ep = Anime.getEpisodeFromAnchor(this);
urlsToDownload.push(ep.getDownloadLink());
});
} else if (type === 'downloadSelects') {
$.each($('#animeSelection option:selected'), function (k, v) {
var url = this.dataset.url;
urlsToDownload.push(url);
});
} else {
var eps = Anime.getEpsForRes(parseInt($('#downloadRes').val()), false);
for (let i = 0, len = eps.length; i < len; i++) {
urlsToDownload.push(eps[i].getDownloadLink());
}
}
bindAlertControls();
function bindAlertControls() {
$('#alertButtonCancel').on('click', function () {
$('#alertUser').slideUp('slow');
$('#crossPage').prop('disabled', false);
});
$('#alertButton').on('click', function () {
doIt(urlsToDownload);
});
}
function doIt(urls) {
for (let i = 0; i < urls.length; i++) {
let currentUrl = urls[i];
AjaxUtils.downloadViaJavaScript(currentUrl, undefined, function (saveFunc) {
saveFunc();
}, undefined, undefined, "GET");
}
$('#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.p === 'undefined') ? 1 : QueryString.p);
};
/**
* 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 = ArrayUtils.arrayCopy(Anime.getAvailableResolutions(), true);
var resLength = avRes.length;
for (let i = 0, len = avRes.length; i < len; i++) {
var currentRes = avRes[i].res;
if (Anime.getamountOfEpsFromRes(currentRes, true) === 0) {
Anime.removeAvailableResolutions(currentRes);
}
}
};
var sortAllControls = function () {
sortSelect(document.getElementById('animeSelection'));
sortSelect(document.getElementById('downloadRes'));
$('#info').sortTable(0);
}
var reBindSelectFilters = function () {
$('input[name=\'filterSelect\']').offOn('change', handleSelect);
$('#clearResOptions').offOn('click', handleSelect);
$("#animeSelection").offOn("click", function () {
UI.autoEnableAcceptSelect();
});
$("#selectAllFromControl").offOn("click", function () {
let allSelected = $("#animeSelection option:selected").length === $("#animeSelection option").length;
if (allSelected) {
$(this).text("Select all");
$("#animeSelection option").prop("selected", false);
} else {
$(this).text("deselect all");
$("#animeSelection option").prop("selected", true);
}
UI.autoEnableAcceptSelect();
});
function handleSelect(event) {
var resTOFilter = $(event.target).data('set');
$('#selectAnime').html(UI.buildSelect(resTOFilter));
Utils.sortAllControls();
var searchApplied = UI.getAppliedSearch();
if (searchApplied !== '') {
UI.applySearch(searchApplied);
}
reBindSelectFilters();
}
};
var equals = function (episode, toEqual) {
if (!(episode instanceof Episode && toEqual instanceof Episode)) {
throw 'both objects must be episodes';
}
return episode.getUid() === toEqual.getUid();
};
var deepEquals = function (episode, toEqual) {
if (!(episode instanceof Episode && toEqual instanceof Episode)) {
throw 'both objects must be episodes';
}
for (var methods in episode) {
if (episode.hasOwnProperty(methods)) {
if (typeof episode[methods] === 'function') {
var method = episode[methods];
var method2 = toEqual[methods];
if (method.call(this) !== method2.call(this)) {
return false;
}
}
}
}
return true;
};
var getHumanReadableSize = function (from, decimals) {
var bits = 0;
if (Array.isArray(from)) {
for (let i = 0; i < from.length; i++) {
var ep = from[i];
bits += ep.getSize();
}
} else if (typeof from === 'number') {
bits = from;
} else {
bits += from.getSize();
}
function formatBytes(bytes, decimals) {
if (bytes == 0) {
return '0 Byte';
}
var k = 1024;
var dm = decimals + 1 || 3;
var sizes = [
'Bytes',
'KB',
'MB',
'GB',
'TB',
'PB',
'EB',
'ZB',
'YB'
];
let i = Math.floor(Math.log(bytes) / Math.log(k));
return (bytes / Math.pow(k, i)).toPrecision(dm) + ' ' + sizes[i];
}
return formatBytes(bits, decimals);
};
return {
disableButton: disableButton,
enableButton: enableButton,
doDownloads: doDownloads,
checkBoxValid: checkBoxValid,
getCurrentPageOffset: getCurrentPageOffset,
html5StoreSupport: html5StoreSupport,
cleanAvailableResolutions: cleanAvailableResolutions,
sortAllControls: sortAllControls,
reBindSelectFilters: reBindSelectFilters,
sortSelect: sortSelect,
isjQueryObject: isjQueryObject,
equals: equals,
deepEquals: deepEquals,
getHumanReadableSize: getHumanReadableSize,
getQueryFromUrl: getQueryFromUrl,
getTable: getTable
};
}());
function loading() {}
var UI = (function () {
var epsInSelect = [
];
var searchApplied = '';
/**
* Build the download infomation table
* @returns {String} The html of the built table
*/
var buildTable = function () {
var html = '';
html += '<table class="table table-responsive" 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 += '<th>Total size</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
var allRes = Anime.getAvailableResolutions();
for (let 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 += '<td>' + Anime.getTotalSizeForRes(localRes, true) + ' (aprox)</td>';
html += '</tr>';
}
html += '</tbody>';
html += '</table>';
return html;
};
var stateChangeAcceptSelect = function stateChangeAcceptSelect(state) {
$("#acceptSelect").enableButton(state);
};
var autoEnableAcceptSelect = function autoEnableAcceptSelect() {
let selection = $("#animeSelection option:selected");
if (selection.length > 0) {
UI.stateChangeAcceptSelect(true)
} else {
UI.stateChangeAcceptSelect(false);
}
};
var buildDropdownSelections = function () {
var html = '';
html += '<select class="form-control" style="margin-right:5px;display: inline;width: auto;" id="downloadRes">';
var allRes = Anime.getAvailableResolutions();
for (let 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;
var totalSize = null;
if (type === 'downloadSelected') {
amountOfAnime = $('.checkboxes:checked').length;
res = 'custom';
} else if (type === 'downloadSelects') {
amountOfAnime = $('#animeSelection option:selected').length;
totalSize = Utils.getHumanReadableSize((function () {
var localSize = 0;
$('#animeSelection option:selected').each(function (k, v) {
var url = this.dataset.url;
var epSize = Anime.getEpisodeFromAnchor(url).getSize();
localSize += epSize;
});
return localSize;
}()));
res = 'custom';
} else {
amountOfAnime = Anime.getamountOfEpsFromRes(selectedRes, false);
res = selectedRes === -1 ? 'Others' : selectedRes + 'p';
totalSize = Anime.getTotalSizeForRes(parseInt(res), false);
}
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>';
if (totalSize !== null) {
html += '<br /><div><strong>Total size: ' + totalSize + ' (aprox)</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 type="button" class="btn btn-success" id=\'alertButton\'>Okay</button>';
html += '<button type="button" class="btn btn-warning" 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 = '';
epsInSelect = [];
html += '<div id=\'selectWrapper\'>';
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 class="form-control" id=\'animeSelection\' multiple size=\'20\'>';
var allRes = Anime.getAvailableResolutions();
for (let i = 0; i < allRes.length; i++) {
var currRes = allRes[i];
var localRes = currRes.res;
var eps = Anime.getEpsForRes(localRes, false);
for (var j = 0, len = eps.length; j < len; j++) {
var currentEp = eps[j];
if (resTOFilter == 'none' || currentEp.getRes() == resTOFilter) {
html += '<option data-url=\'' + currentEp.getDownloadLink() + '\'>';
html += currentEp.getTitle() + ' - Seeders: ' + currentEp.getSeeds();
epsInSelect.push(currentEp);
html += '</option>';
} else {
break;
}
}
}
html += '</select>';
html += '<span>Filter select control: </span>';
var checked = false;
for (let 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 class="filterLabel">' + (allRes[i].res === -1 ? 'Others' : allRes[i].res + 'p') + '</label>';
checked = false;
}
html += '<a id=\'clearResOptions\' data-set=\'none\' >clear resolution filter</a>';
html += '<a id=\'selectAllFromControl\'>Select all</a>';
html += '</div>';
html += '</div>';
$("#acceptSelect").enableButton(false);
return html;
};
var applySearch = function (textToFilter) {
var opts = epsInSelect;
var rxp = new RegExp(textToFilter);
var optlist = $('#animeSelection').empty();
for (let i = 0, len = opts.length; i < len; i++) {
var ep = opts[i];
if (rxp.test(ep.getTitle())) {
optlist.append('<option data-url=\'' + ep.getDownloadLink() + '\'>' + ep.getTitle() + ' - Seeders: ' + ep.getSeeds() + '</option>');
}
}
searchApplied = textToFilter;
Utils.sortSelect(document.getElementById("animeSelection"));
UI.autoEnableAcceptSelect();
};
var getAppliedSearch = function () {
return searchApplied;
};
var getEpsInSelect = function () {
return epsInSelect;
};
return {
buildDropdownSelections: buildDropdownSelections,
buildTable: buildTable,
builDownloadAlert: builDownloadAlert,
showAjaxErrorAlert: showAjaxErrorAlert,
buildSelect: buildSelect,
getEpsInSelect: getEpsInSelect,
applySearch: applySearch,
getAppliedSearch: getAppliedSearch,
stateChangeAcceptSelect: stateChangeAcceptSelect,
autoEnableAcceptSelect: autoEnableAcceptSelect
};
}());
var DataParser = (function () {
var table = null;
let 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*=\'/static/img/icons/nyaa/1_2.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);
}
let 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, info.size));
});
return eps;
function parseRes(eventContent) {
var suppRes = Anime.getSupportedRes();
for (let 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) {
eventContent = $(eventContent);
let currentDownloadLink = Anime.getTdFromTable(eventContent, 3).find("a")[0].href;
let seeds = (isNaN(parseInt(Anime.getTdFromTable(eventContent, 6).text()))) ? 0 : parseInt(Anime.getTdFromTable(eventContent, 6).text());
let leech = (isNaN(parseInt(Anime.getTdFromTable(eventContent, 7).text()))) ? 0 : parseInt(Anime.getTdFromTable(eventContent, 7).text());
let title = Anime.getTdFromTable(eventContent, 2).text().trim().substring(1).trim();
let uid = Anime.getUidFromJqueryObject($(eventContent));
return {
'currentDownloadLink': currentDownloadLink,
'seeds': seeds,
'leech': leech,
'title': title,
'uid': uid,
'size': (function () {
var sizeValue = Anime.getTdFromTable(eventContent, 4).text();
var sizeText = $.trim(sizeValue.split(' ').pop());
let intValue = parseInt(sizeValue);
switch (sizeText) {
case 'MiB':
return ((Math.pow(2, 20)) / 1) * intValue;
break;
case 'GiB':
return intValue * 1073741824;
break;
default:
return 0;
break;
}
}())
};
}
};
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 (let 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 = {};
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;
}
});
return {
Seeds: Seeds
};
}());
//Local storeage object
var Localstore = {
getMinSeedsFromStore: function () {
return localStorage.getItem('minSeeds');
},
setMinSeedsFromStore: function (seeds) {
localStorage.setItem('minSeeds', seeds);
},
removeMinSeedsFromStore: function () {
localStorage.removeItem('minSeeds');
}
};
// 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 */
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.q !== '') {
Anime.setCurrentAnime(decodeURIComponent(QueryString.q).split('+').join(' '));
} else {
Anime.setCurrentAnime('Unknown');
}
// set subber
let paths = window.location.pathname.split("/");
var currentSubber = paths[2];
Anime.setCurrentSubber(currentSubber);
// Set eps
DataParser.setTable(Utils.getTable());
// 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 += '.collapsem{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 {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 th, #info td {padding: 5px;text-align: left;}';
styles += 'label[for=\'MinSeeds\']{ display: block; margin-top: 10px;}';
styles += '.filterLabel{margin-right: 10px;}'
// styles += '#SaveMinSeeds{margin-left:5px;}';
// Alerts
styles += '.alert {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; margin-right: 10px ;cursor: pointer;}';
styles += '#selectAllFromControl{cursor: pointer;}';
styles += '#downloadCustomButton{float:right;}';
styles += '#findEp{float: right; position: relative; bottom: 20px; width: 180px;}'
DomUtil.injectCss('https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css');
DomUtil.injectCss(styles);
}
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 collapsem" id="collapseToggle" title="Hide"></i>';
html += '</div>';
html += '<div class="panel-body" id="pannelContent"></div>';
html += '</div>';
$('.container > .row > h3').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 class="btn btn-default" type="button" data-type=\'downloadAll\' id="downloadAll">Download all</button>';
html += '<button class="btn btn-default" type=\'button\' id=\'downloadCustomButton\' data-type=\'downloadSelected\' >download your selected items</button>';
html += '</div>';
html += '<div id=\'options\'>';
html += '<div class="checkbox">';
html += "<label>";
html += '<input type=\'checkbox\' id=\'crossPage\' /> ';
html += "include Cross pages";
html += "</label>";
html += "</div>";
html += '<div class="input-group">';
html += '<input placeholder="Minimum seeders" class="form-control" type=\'number\' min=\'0\' id=\'MinSeeds\' title=\'Any episode that is below this limit will be excluded from the download.\'/>';
html += '<span class="input-group-btn">';
html += '<button class="btn btn-default" type=\'button\' id=\'SaveMinSeeds\'>Save</button>';
html += "</span>";
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 += '<input class="form-control" type=\'text\' id=\'findEp\' placeholder=\'Search Select (or use regex)\' />';
html += '<button class="btn btn-default" disabled 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.sortAllControls();
}
}
}
function bindListeners() {
Utils.reBindSelectFilters();
$('#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());
function preformParsing(urls) {
if (urls.length === 0) {
return;
}
if (Utils.checkBoxValid($('#crossPage'))) {
$('#tableInfo').html('<p>Please wait while we parse each page...</p>');
$('#selectAnime').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 = 0;
let queryObjet = Utils.getQueryFromUrl(cur);
if (queryObjet.p) {
currentPage = queryObjet.p;
} else {
currentPage = 1;
}
/*if (cur.indexOf('offset') > -1) {
currentPage = cur.substring(cur.indexOf('offset')).split('&')[0].split('=').pop();
} else {
currentPage = 1;
}*/
AjaxInfo.currentPage = currentPage;
var table = $(data).find("table");
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());
$('#selectAnime').html(UI.buildSelect());
Utils.sortAllControls();
$('#acceptSelect').show();
Utils.reBindSelectFilters();
$('#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());
$('#selectAnime').html(UI.buildSelect());
Utils.reBindSelectFilters();
Utils.sortAllControls();
}
}
});
$('#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);
}
$('#selectAnime').html(UI.buildSelect());
Utils.sortAllControls();
Utils.reBindSelectFilters();
});
$('#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');
}
});
});
$('#acceptSelect').on('click', function (e) {
Utils.doDownloads(e);
});
$('#findEp').on('keyup', function () {
UI.applySearch($(this).val());
});
}
}
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);
}
}
}