Frederick888 / U2 Sticky Manager (Forum)

// ==UserScript==
// @name				U2 Sticky Manager (Forum)
// @namespace           https://onee3.org/
// @version             0.94
// @description         Easier to manage the sticky torrents. Only for moderators.
// @include				*://u2.dmhy.org/forums.php?action=viewtopic&*
// @copyright			2014+, Frederick888
// @require				https://onee3.org/libs/jquery/3.2.1/jquery.min.js
// @grant				none
// @license             GPL-3.0-or-later
// ==/UserScript==

var D = 0;
var A = 60;
var duration = 3.5;
var processed = false;
console.log('Using jQuery v' + $.fn.jquery);

// these obey the order shown in page (deduplicated based on post id + torrent id)
var $apps, app_ids, app_applicants, app_pids, app_ranks;
// items to be added (html, random order)
var items = [];
// sorted items with styles (jQuery elements)
var sortedItems = [];
// torrent id => torrent data
var torrentData = {};

function nextSame() {
    var $currTr = $(this).closest('tr.manage-item');
    var seq = $currTr.data('origin-seq');
    var next = -1, pos = seq + 1;
    while (pos != seq) {
        if (app_ids[pos] == app_ids[seq]) {
            next = pos;
            break;
        } else {
            pos++;
            if (pos > app_ids.length) pos = 0;
        }
    }
    if (next != -1) {
        $currTr.data('origin-seq', next);
        $currTr.find('td.applicant').html(app_applicants[next]);
        $currTr.find('button.pid').html(app_pids[next]);
    }
}

function modified_time(pid) {
    $postHeader = $('table#pid' + pid);
    postCreatedTime = Date.parse($postHeader.find('time').attr('title') === undefined ?
        $postHeader.find('time').html() :
        $postHeader.find('time').attr('title'));
    $postBody = $postHeader.parent().next('table.main');
    if ($postBody.find('time').length > 0) {
        return Date.parse($postBody.find('time').attr('title') === undefined ?
            $postBody.find('time').html() :
            $postBody.find('time').attr('title'));
    } else {
        return postCreatedTime;
    }
}

function sortManger() {
    sortedItems = [];
    // sort the items, order: rank, modified date, post id, torrent id
    var j, k, currTid, currPid;
    for (j = 0; j < items.length; j++) {
        sortedItems.push($(items[j]));
        // since item is not ordered, we need to find the origin order
        currTid = parseInt(sortedItems[j].find('.torrent-id').html());
        currPid = parseInt(sortedItems[j].find('.pid').html());
        for (k = 0; k < app_ids.length; k++) {
            if (app_ids[k] == currTid && app_pids[k] == currPid) {
                sortedItems[j].data('origin-seq', k);
            }
        }
    }
    sortedItems.sort(function ($a, $b) {
        aSeq = $a.data('origin-seq');
        aRank = app_ranks[aSeq];
        aTid = parseInt($a.find('.torrent-id').html());
        aPid = parseInt($a.find('.pid').html());
        aTime = modified_time(aPid);

        bSeq = $b.data('origin-seq');
        bRank = app_ranks[bSeq];
        bTid = parseInt($b.find('.torrent-id').html());
        bPid = parseInt($b.find('.pid').html());
        bTime = modified_time(bPid);

        if (aRank != bRank) {
            return aRank - bRank;
        } else if (aTime != bTime) {
            return aTime - bTime;
        } else if (aPid != bPid) {
            return aPid - bPid;
        } else if (aTid != bTid) {
            return aTid - bTid;
        }
        return 0;
    });

    // Set the styles
    for (j = 0; j < sortedItems.length; j++) {
        currTid = parseInt(sortedItems[j].find('.torrent-id').html());
        // If the torrent is not deleted or recycled or ...
        if (torrentData[currTid].data !== undefined && torrentData[currTid].data.status == 'p') {
            // Currently not sticky
            if (torrentData[currTid].data.pos_state == 'normal') {
                sortedItems[j].find('.to-stick').removeAttr('disabled');
            } else {
                // Currently sticky
                sortedItems[j].find('.to-stick').removeAttr('disabled');
                sortedItems[j].find('.to-stick').prop('checked', true);
                sortedItems[j].find('.to-stick').attr('previous', 't');
                sortedItems[j].find('.to-remove').removeAttr('disabled');
                sortedItems[j].css('background-color', 'skyblue');
            }
        }
        // Enable PID button if the torrent is repeatedly applied
        if (app_ids.indexOf(currTid) != app_ids.lastIndexOf(currTid)) {
            sortedItems[j].find('.pid').removeAttr('disabled');
            sortedItems[j].find('.pid').click(nextSame);
        }
    }

    var rank = 0;
    for (j = 0; j < sortedItems.length; j++) {
        currTid = parseInt(sortedItems[j].find('.torrent-id').html());
        var toAdd = true;
        for (k = j + 1; k < sortedItems.length; k++) {
            if (parseInt(sortedItems[k].find('.torrent-id').html()) == currTid) {
                toAdd = false;
                break;
            }
        }
        if (toAdd) {
            if (app_ranks[sortedItems[j].data('origin-seq')] > rank) {
                // add delimiters based on ranks
                $('#manager>tbody>tr:last-child').before('<tr style="height:10px"></tr>');
                rank = app_ranks[sortedItems[j].data('origin-seq')];
            }
            $('#manager>tbody>tr:last-child').before(sortedItems[j]);
        }
    }

    // set sequence number column
    var $seqTd = $('.seq');
    for (var seq = 1; seq <= $seqTd.length; seq++) {
        $($seqTd[seq - 1]).html(seq);
    }

    $('input.to-stick, input.to-remove').click(function () {
        if ($(this).prop('disabled') === false) {
            if ($(this).attr('previous') == 't') {
                $(this).prop('checked', false);
                $(this).attr('previous', 'f');
            } else {
                $(this).parent().find('input').each(function () {
                    $(this).attr('previous', 'f');
                });
                $(this).attr('previous', 't');
            }
        }
    });
}

function recalculate() {
    A = parseFloat($('#value-a').val());
    duration = parseFloat($('#value-duration').val());
    $('.manage-item').each(function () {
        var size = parseFloat($(this).find('.torrent-size').html());
        $(this).find('.torrent-cost').html((A * D * size / 7 * duration).toFixed(3));
    });
    $('#process-sticky').removeAttr('disabled');
}

function addItem(data, applicant, pid) {
    var name, size, finished, seeder, leecher, cost;
    var torrentId = data.data.id;
    if (data.code === 0) {
        size = parseInt(data.data.size) / 1024 / 1024 / 1024;
        size = (size).toFixed(2);
        name = data.data.name;

        finished = parseInt(data.data.times_completed);
        seeder = parseInt(data.data.seeders);
        leecher = parseInt(data.data.leechers);
        cost = (A * D * size / 7 * duration).toFixed(3);
    } else {
        name = data.err_msg;
        size = 'x';
        finished = 'x';
        seeder = 'x';
        leecher = 'x';
        cost = 'x';
    }

    var addRow = '<tr class="manage-item" id="manage' + torrentId +
        '"><td class="torrent-checked"><input type="radio" class="to-stick" name="choice-' + torrentId + '" value="' + torrentId +
        '" disabled/><input type="radio" class="to-remove" name="choice-' + torrentId + '" value="' + torrentId +
        '" disabled/></td><td class="seq"></td><td class="applicant" style="min-width:100px;max-width:100px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;">' +
        applicant + '</td><td><button type="button" class="pid" disabled>' + pid +
        '</button></td><td class="torrent-name" style="max-width:900px"><a target="_blank" href="details.php?id=' + torrentId + '">' + name +
        '</a></td><td style="min-width:50px;text-align:right;" title="Check Log">' +
        '<a target="_blank" class="torrent-id" href="log.php?search=all&action=dailylog&query=details.php%3Fid%3D' + torrentId + '">' + torrentId +
        '</a></td><td class="torrent-size" style="min-width:50px;text-align:right;">' + size +
        '</td><td class="torrent-cost" style="min-width:70px;text-align:right;">' + cost +
        '</td><td class="torrent-seeder" style="min-width:20px;text-align:right;">' + seeder +
        '</td><td class="torrent-leecher" style="min-width:20px;text-align:right;">' + leecher +
        '</td><td class="torrent-finished" style="min-width:20px;text-align:right;">' + finished + '</td></tr>';
    items.push(addRow);
}

function process() {
    var toStick = [], toRemove = [], i;
    $('#manager .to-stick').each(function () {
        if (this.checked) {
            toStick.push($(this).val());
        }
    });
    $('#manager .to-remove').each(function () {
        if (this.checked) {
            toRemove.push($(this).val());
        }
    });

    var pid, cost, size, message;
    // UC       POST to forumpostadmin.php
    // markdes={add|sub}&mark={amount}&reason={URLEncodedMessage}&admintype=mark&pid={PostID}
    // Posting  POST to forums.php?action=post
    // id={ArticleID}&type=reply&body={URLEncodedMessage}
    // Sticky GET httpapi_torrentstick.php?sticky={1|0}&id={torrentid}
    // Comment  POST to forumpostadmin.php
    // admintype=comment&pid={PostId}&comment={Comment}
    for (i = 0; i < toStick.length; i++) {
        pid = parseInt($('.manage-item#manage' + toStick[i]).find('.pid').html());
        cost = parseFloat($('tr#manage' + toStick[i] + '>.torrent-cost').html());
        size = parseFloat($('tr#manage' + toStick[i] + '>.torrent-size').html());
        message = encodeURIComponent('Torrent #' + toStick[i] + ': CHARGE, A × D × Size × Duration = ' + A + ' × ' + D + ' × ' + size + ' × (' + duration + ' ÷ 7) = ' + cost);
        $.ajax({
            url: 'forumpostadmin.php',
            method: 'post',
            aPid: pid,
            aTid: toStick[i],
            data: 'markdes=sub&mark=' + cost + '&reason=' + message + '&admintype=mark&pid=' + pid,
            error: function (xhr, stat, err) {
                console.log('Failed to manage PID#' + this.aPid + ' with Torrent#' + this.aTid);
            },
            success: function (data) {
                console.log('Succeeded in managing PID#' + this.aPid + ' with Torrent#' + this.aTid);
                $.get('httpapi_torrentstick.php', { sticky: 1, id: this.aTid });
            }
        });
    }
    for (i = 0; i < toRemove.length; i++) {
        pid = parseInt($('.manage-item#manage' + toRemove[i]).find('.pid').html());
        message = encodeURIComponent('Torrent #' + toRemove[i] + ': REMOVE');
        $.ajax({
            url: 'forumpostadmin.php',
            method: 'post',
            aPid: pid,
            aTid: toRemove[i],
            data: 'admintype=comment&comment=' + message + '&pid=' + pid,
            error: function (xhr, stat, err) {
                console.log('Failed to manage PID#' + this.aPid + ' with Torrent#' + this.aTid);
            },
            success: function (data) {
                console.log('Succeeded in managing PID#' + this.aPid + ' with Torrent#' + this.aTid);
                $.get('httpapi_torrentstick.php', { sticky: 0, id: this.aTid });
            }
        });
    }

    if (toStick.length == 0 && toRemove.length == 0) {
        if (confirm('Reply with marker only?')) {
            reply('');
        }
    }

    processed = true;
}

// Checkbox Name ID Size Cost ↑ ↓ √
function addManager() {
    console.log('Starting manager');

    processed = false;

    var $enableBtn = $('.enable-manager');
    $enableBtn.attr('disabled', 'disabled');
    $enableBtn.html('Just a moment...<span class="spinner"><img src="https://onee3.org/libs/loading_spinner_wedges.gif" width="15" height="15" style="vertical-align:middle;"/></spin>');

    $.ajax({
        url: 'httpapi_globalconstant.php?constant=D',
        dataType: 'json',
        error: function (xhr, stat, err) {
            alert('Failed to get D');
        },
        success: function (data) {
            if (data.code !== 0) {
                alert('Failed to get D');
                return;
            }
            D = parseFloat(data.data);
            console.log('D=' + D);
        }
    }).done(function () {
        $apps = $('div.post-body a.faqlink');
        for (var h = $apps.length - 1; h >= 0; h--) {
            if (!(/\bdetails\.php\b/.test($($apps[h]).attr('href')))) {
                $apps.splice(h, 1);
            }
        }
        app_ids = [];
        app_applicants = [];
        app_pids = [];
        app_ranks = [];
        var rank = 0;
        $apps.each(function () {
            // Ignore posts with manager tag "ignore"
            if (/MANAGER_TAG_IGNORE/.test($(this).parent().html())) {
                rank++;
                return;
            }
            if (/details\.php\?.*id=(\d+)/.test($(this).attr('href'))) {
                var currId = parseInt(RegExp.$1);
                var currPid = /pid\d+/i.exec($(this).closest('.post-body').attr('id'))[0];
                currPid = parseInt(/\d+/.exec(currPid)[0]);
                // Add all applications but only one for same applications in one post
                var toAdd = true;
                for (var z = 0; z < app_ids.length; z++) {
                    if (app_ids[z] == currId && app_pids[z] == currPid) {
                        toAdd = false;
                        break;
                    }
                }
                if (toAdd) {
                    app_ids.push(currId);
                    app_pids.push(currPid);
                    app_ranks.push(rank);
                    var applicant = $('#pid' + currPid + ' a[href^="userdetails.php"]')[0].outerHTML;
                    applicant = applicant.substring(0, 3) + 'target="_blank"' + applicant.substring(2);
                    app_applicants.push(applicant);
                }
            }
        });

        $('td.outer').append('<table id="manager"><tbody><tr><td colspan="20" style="text-align:right"><label for="value-a">A: </label>' +
            '<input type="number" id="value-a" value="' + A +
            '" style="margin-right:15px;width:100px;"><label for="value-d">D: </label><input type="number" id="value-d" disabled value="' + D.toFixed(4) +
            '" style="margin-right:15px;width:100px;"><label for="value-duration">Duration: </label><input type="number" id="value-duration" value="' + duration +
            '" style="margin-right:15px;width:100px;"><button type="button" id="re-cal">Re-calculate</button></td></tr><tr><td>Ch/Rm</td><td>#</td>' +
            '<td>Applicant</td><td>Post</td><td>Name</td><td style="text-align:right">ID</td><td style="text-align:right">Size</td>' +
            '<td style="text-align:right">Cost</td><td style="text-align:right">↑</td><td style="text-align:right">↓</td>' +
            '<td style="text-align:right">√</td></tr><tr><td colspan="11" style="text-align:right">' +
            '<button type="button" id="process-sticky">Process!</button></td></tr></tbody></table>');
        $('#process-sticky').on('click', process);

        $('#value-a, #value-duration').change(function () {
            $('#process-sticky').attr('disabled', 'disabled');
        });
        $('#re-cal').click(recalculate);

        for (var i = 0; i < app_ids.length; i++) {
            if (isNaN(torrentData[app_ids[i]])) {
                // httpapi_torrentinfo.php?tid=xxx
                // status: p=normal; d=recycled; w=offer
                // pos_state: normal/sticky
                $.ajax({
                    url: 'httpapi_torrentinfo.php?tid=' + app_ids[i],
                    applicant: app_applicants[i],
                    pid: app_pids[i],
                    tid: app_ids[i],
                    dataType: 'json',
                    error: function (xhr, stat, err) {
                        alert('Failed to get info of #' + this.tid);
                    },
                    success: function (data, status, xhr) {
                        torrentData[this.tid] = data;
                        addItem(data, this.applicant, this.pid);
                    }
                });
            } else {
                addItem(torrentData[app_ids[i]], app_applicants[i], app_pids[i]);
            }
        }
    });
}

function reply(message) {
    message = "MANAGER_TAG_IGNORE\n" + message;
    $('#compose>textarea').text(message);
    $('#qr').trigger('click');
}

$(document).ajaxStop(function () {
    console.log('stopped');
    if (!processed) {
        $('.enable-manager').html('Enabled');
        sortManger();
        location.hash = '#manager';
    } else {
        console.log('all done');

        var replyMsg = "D=" + D + "\n\n";
        var toStick = [], toRemove = [], i;
        $('#manager .to-stick').each(function () {
            if (this.checked) {
                toStick.push($(this).val());
            }
        });
        $('#manager .to-remove').each(function () {
            if (this.checked) {
                toRemove.push($(this).val());
            }
        });

        var dataRow, nameColumn, nameColor, nameText;
        for (i = 0; i < toStick.length; i++) {
            dataRow = $('tr#manage' + toStick[i]);
            nameColumn = $(dataRow.find('a')[0]);
            if (/color:(#[a-z0-9]+)/i.test(nameColumn.attr('style'))) {
                nameColor = RegExp.$1;
            } else {
                nameColor = 'black';
            }
            (/<b><bdo .*?>(.*)<\/bdo><\/b>/).test(nameColumn.html());
            nameText = RegExp.$1.replace(/^<i>/, "[i]").replace(/<\/i>$/, "[/i]");
            replyMsg += '[url=' + nameColumn.attr('href') + '][color=' + nameColor +
                '][b]' + nameText + '[/b][/color][/url]' + "\n";
            replyMsg += $(dataRow.find('a')[1]).html() + "\n";
            replyMsg += 'https://u2.dmhy.org/' + $(dataRow.find('a')[1]).attr('href') + "\n";
            replyMsg += 'Operation: Stick  Size: ' + dataRow.children('.torrent-size').html() + 'GB  Cost: ' + dataRow.children('.torrent-cost').html();

            if (i < toStick.length - 1 || toRemove.length > 0) {
                replyMsg += "\n\n";
            }
        }
        for (i = 0; i < toRemove.length; i++) {
            dataRow = $('tr#manage' + toRemove[i]);
            nameColumn = $(dataRow.find('a')[0]);
            if (/color:(#[a-z0-9]+)/i.test(nameColumn.attr('style'))) {
                nameColor = RegExp.$1;
            } else {
                nameColor = 'black';
            }
            (/<b><bdo .*?>(.*)<\/bdo><\/b>/).test(nameColumn.html());
            nameText = RegExp.$1.replace(/^<i>/, "[i]").replace(/<\/i>$/, "[/i]");
            replyMsg += '[url=' + nameColumn.attr('href') + '][color=' + nameColor +
                '][b]' + nameText + '[/b][/color][/url]' + "\n";
            replyMsg += $(dataRow.find('a')[1]).html() + "\n";
            replyMsg += 'https://u2.dmhy.org/' + $(dataRow.find('a')[1]).attr('href') + "\n";
            replyMsg += 'Operation: Remove';

            if (i < toRemove.length - 1) {
                replyMsg += "\n\n";
            }
        }
        reply(replyMsg);
    }
});

$('td.outer>table.main>tbody').append('<tr><td style="text-align:center"><button type="button" class="enable-manager">Enable Sticky Manager<div></div></button></td></tr>');

$('.enable-manager').on('click', addManager);
if (window.location.hash.indexOf('manager') >= 0) addManager();