For daily, weekly, monthly and previous, the data represents actual post values.
The data has been compressed into an array to save space, but the following is what each index represents:
0. Post ID
1. Score
2. Upscore
3. Downscore
4. Favcount
5. Tagcount
6. Gentags
7. Copyrights string
8. Created timestamp
For yearly and alltime, the data represents the finalized statistics and chart data.
Reverse tag implications (rti):The number of tags that a tag implicates. Used to determine the base copyright tag.
`;
const PROGRAM_DATA_DETAILS = `
All timestamps are in milliseconds since the epoch (Epoch converter).
General data
prune-expires: When the program will next check for cache data that has expired.
user-settings: All configurable settings.
Status data
current-metric: Which metric to show upon first opening the table.
hide-current-uploads: Should the table be opened on page load?
stash-current-uploads: Should the table be hidden and the restore link shown? Takes precedence over hide-current-uploads.
`;
//Time periods
const timevalues = ['d','w','mo','y','at'];
const manual_periods = ['w','mo'];
const limited_periods = ['y','at'];
const copyright_periods = ['d','w','mo'];
//Period constants
const period_info = {
countexpires: {
d: 5 * JSPLib.utility.one_minute,
w: JSPLib.utility.one_hour,
mo: JSPLib.utility.one_day,
y: JSPLib.utility.one_week,
at: JSPLib.utility.one_month
},
uploadexpires: {
d: 5 * JSPLib.utility.one_minute,
w: JSPLib.utility.one_day,
mo: JSPLib.utility.one_week,
y: JSPLib.utility.one_month,
at: JSPLib.utility.one_year
},
longname: {
d: 'daily',
w: 'weekly',
mo: 'monthly',
y: 'yearly',
at: 'alltime'
},
header: {
d: 'Day',
w: 'Week',
mo: 'Month',
y: 'Year',
at: 'All-time'
},
points: {
w: 7,
mo: 30,
y: 12,
at: 0
},
xlabel: {
w: "Days ago",
mo: "Days ago",
y: "Months ago",
at: "Months ago"
},
divisor: {
w: JSPLib.utility.one_day,
mo: JSPLib.utility.one_day,
y: JSPLib.utility.one_month,
at: JSPLib.utility.one_month,
}
};
const longname_key = {
daily: 'd',
weekly: 'w',
monthly: 'mo',
yearly: 'y',
alltime: 'at'
};
//Time constants
const prune_expires = JSPLib.utility.one_day;
const noncritical_recheck = JSPLib.utility.one_minute;
const rti_expiration = JSPLib.utility.one_month;
const JQUERY_DELAY = 1; //For jQuery updates that should not be done synchronously
//Network call configuration
const max_post_limit_query = 100;
//Metrics used by statistics functions
const tooltip_metrics = ['score','upscore','downscore','favcount','tagcount','gentags','week','day'];
const chart_metrics = ['score','upscore','downscore','favcount','tagcount','gentags'];
//Feedback messages
const empty_uploads_message_owner = 'Feed me more uploads!';
const empty_uploads_message_other = 'No uploads for this user.';
const empty_approvals_message_other = 'No approvals for this user.';
const empty_uploads_message_anonymous = 'User is Anonymous, so no uploads.';
const copyright_no_uploads = 'No uploads, so no copyrights available for this period.';
const copyright_no_statistics = 'No statistics available for this period (click the table header).';
//Other constants
const name_field = "name";
const id_field = "id";
const user_fields = "name,level_string";
const post_fields = "id,score,up_score,down_score,fav_count,tag_count,tag_count_general,tag_string_copyright,created_at";
//Validation values
const validation_constraints = {
countentry: JSPLib.validate.counting_constraints,
implicationentry: JSPLib.validate.counting_constraints,
postentries: JSPLib.validate.array_constraints,
statentries: JSPLib.validate.hash_constraints,
postentry: [
JSPLib.validate.integer_constraints, //ID
JSPLib.validate.integer_constraints, //SCORE
JSPLib.validate.integer_constraints, //UPSCORE
JSPLib.validate.integer_constraints, //DOWNSCORE
JSPLib.validate.integer_constraints, //FAVCOUNT
JSPLib.validate.integer_constraints, //TAGCOUNT
JSPLib.validate.integer_constraints, //GENTAGS
JSPLib.validate.stringonly_constraints, //COPYRIGHTS
JSPLib.validate.integer_constraints //CREATED
],
postmetric: {
chart_data: JSPLib.validate.hash_constraints,
score: JSPLib.validate.hash_constraints,
upscore: JSPLib.validate.hash_constraints,
downscore: JSPLib.validate.hash_constraints,
favcount: JSPLib.validate.hash_constraints,
tagcount: JSPLib.validate.hash_constraints,
gentags: JSPLib.validate.hash_constraints,
week: JSPLib.validate.array_constraints,
day: JSPLib.validate.array_constraints
},
timestat: JSPLib.validate.basic_number_validator,
poststat: {
max: JSPLib.validate.integer_constraints,
average: JSPLib.validate.number_constraints,
stddev: JSPLib.validate.number_constraints,
outlier: JSPLib.validate.integer_constraints,
adjusted: JSPLib.validate.number_constraints
},
chartentry: {
score: JSPLib.validate.array_constraints,
upscore: JSPLib.validate.array_constraints,
downscore: JSPLib.validate.array_constraints,
favcount: JSPLib.validate.array_constraints,
tagcount: JSPLib.validate.array_constraints,
gentags: JSPLib.validate.array_constraints,
uploads: JSPLib.validate.array_constraints
},
chartdata: {
x: JSPLib.validate.integer_constraints,
y: JSPLib.validate.number_constraints
}
};
/**FUNCTIONS**/
//Validation functions
function BuildValidator(validation_key) {
return {
expires: JSPLib.validate.expires_constraints,
value: validation_constraints[validation_key]
};
}
function ValidateEntry(key,entry) {
if (!JSPLib.validate.validateIsHash(key,entry)) {
return false;
}
if (key.match(/^ct(d|w|mo|y|at)?-/)) {
return JSPLib.validate.validateHashEntries(key, entry, BuildValidator('countentry'));
} else if (key.match(/^rti-/)) {
return JSPLib.validate.validateHashEntries(key, entry, BuildValidator('implicationentry'));
} else if (key.match(/^(daily|weekly|monthly|previous)-(uploads|approvals)-/)) {
if (!JSPLib.validate.validateHashEntries(key, entry, BuildValidator('postentries'))) {
return false;
}
return ValidatePostentries(key + '.value', entry.value);
} else if (key.match(/^(yearly|alltime)-(uploads|approvals)-/)) {
if (!JSPLib.validate.validateHashEntries(key, entry, BuildValidator('statentries'))) {
return false;
}
return ValidateStatEntries(key + '.value',entry.value);
}
this.debug('log',"Bad key!");
return false;
}
function ValidatePostentries(key,postentries) {
for (let i = 0;i < postentries.length;i++){
let value_key = key + `[${i}]`;
if (!JSPLib.validate.validateIsArray(value_key, postentries[i], {is: validation_constraints.postentry.length})) {
return false;
}
//It's technically not a hash, although it works since arrays can be treated like one
if (!JSPLib.validate.validateHashEntries(value_key, postentries[i], validation_constraints.postentry)) {
return false;
}
}
return true;
}
function ValidateStatEntries(key,statentries) {
if (!JSPLib.validate.validateHashEntries(key, statentries, validation_constraints.postmetric)) {
return false;
}
for (let i = 0; i < tooltip_metrics.length; i++) {
let metric = tooltip_metrics[i];
let metric_key = key + '.' + metric;
if (metric === 'week' || metric === 'day') {
if (!JSPLib.validate.validateArrayValues(metric_key, statentries[metric], validation_constraints.timestat)) {
return false;
}
} else if (!JSPLib.validate.validateHashEntries(metric_key, statentries[metric], validation_constraints.poststat)) {
return false;
}
}
return ValidateChartEntries(key + '.chart_data', statentries.chart_data);
}
function ValidateChartEntries(key,chartentries) {
if (!JSPLib.validate.validateHashEntries(key, chartentries, validation_constraints.chartentry)) {
return false;
}
for (let chart_key in chartentries) {
for (let i = 0; i < chartentries[chart_key].length; i ++) {
if (!JSPLib.validate.validateHashEntries(`${key}.${chart_key}[${i}]`, chartentries[chart_key][i], validation_constraints.chartdata)) {
return false;
}
}
}
return true;
}
function ValidateProgramData(key,entry) {
var checkerror = [];
switch (key) {
case 'cu-user-settings':
checkerror = JSPLib.menu.validateUserSettings(entry, SETTINGS_CONFIG);
break;
case 'cu-prune-expires':
if (!Number.isInteger(entry)) {
checkerror = ["Value is not an integer."];
}
break;
case 'cu-current-metric':
if (!tooltip_metrics.includes(entry)) {
checkerror = [`Value not in list: ${tooltip_metrics}`];
}
break;
case 'cu-hide-current-uploads':
case 'cu-stash-current-uploads':
if (!JSPLib.validate.isBoolean(entry)) {
checkerror = ['Value is not a boolean.'];
}
break;
default:
checkerror = ["Not a valid program data key."];
}
if (checkerror.length) {
JSPLib.validate.outputValidateError(key,checkerror);
return false;
}
return true;
}
//Library functions
////NONE
//Table functions
function AddTable(input,inner_args="") {
return `
\r\n` + input + '
\r\n';
}
function AddTableHead(input,inner_args="") {
return `\r\n` + input + '\r\n';
}
function AddTableBody(input,inner_args="") {
return `\r\n` + input + '\r\n';
}
function AddTableRow(input,inner_args="") {
return `
\r\n` + input + '
\r\n';
}
function AddTableHeader(input,inner_args="") {
return `
` + input + '
\r\n';
}
function AddTableData(input,inner_args="") {
return `
` + input + '
\r\n';
}
//Render functions
//Render table
function RenderHeader() {
var tabletext = AddTableHeader('Name');
let click_periods = manual_periods.concat(limited_periods);
let times_shown = GetShownPeriodKeys();
times_shown.forEach((period)=>{
let header = period_info.header[period];
if (click_periods.includes(period)) {
let is_available = CU.period_available[CU.usertag][CU.current_username][period];
let link_class = (manual_periods.includes(period) ? 'cu-manual' : 'cu-limited');
let header_class = (!is_available ? 'cu-process' : '');
let counter_html = (!is_available ? ' (...)' : '');
tabletext += AddTableHeader(`${header}${counter_html}`,`class="cu-period-header ${header_class}" data-period="${period}"`);
} else {
tabletext += AddTableHeader(header,`class="cu-period-header" data-period="${period}"`);
}
});
return AddTableHead(AddTableRow(tabletext));
}
function RenderBody() {
if (CU.active_copytags.length > 5) {
$("#count-table").addClass("overflowed");
} else {
$("#count-table").removeClass("overflowed");
}
var tabletext = RenderRow('');
for (let i = 0;i < CU.active_copytags.length; i++) {
tabletext += RenderRow(CU.active_copytags[i]);
}
return AddTableBody(tabletext);
}
function RenderRow(key) {
var rowtag = (key === ''? `${CU.usertag}:` + CU.display_username : key);
var rowtext = (key === ''? CU.display_username : key).replace(/_/g,' ');
rowtext = JSPLib.utility.maxLengthString(rowtext);
var rowaddon = (key === '' ? `class="user-${CU.level_string.toLowerCase()} with-style"` : 'class="tag-type-3"');
var tabletext = AddTableData(JSPLib.danbooru.postSearchLink(rowtag, rowtext, rowaddon));
let times_shown = GetShownPeriodKeys();
let click_periods = manual_periods.concat(limited_periods);
for (let i = 0;i < times_shown.length; i++) {
let period = times_shown[i];
let data_text = GetTableValue(key,period);
let is_limited = limited_periods.includes(period);
let class_name = (!is_limited ? 'cu-hover' : '');
if (click_periods.includes(period) && key === '') {
class_name += (manual_periods.includes(period) ? ' cu-manual' : ' cu-limited');
}
let rowdata = `class="${class_name}" data-period="${period}"`;
let is_available = CU.period_available[CU.usertag][CU.current_username][period];
if (is_available && is_limited && key == '') {
tabletext += AddTableData(RenderTooltipData(data_text,times_shown[i],true),rowdata);
} else if (is_available && !is_limited) {
tabletext += AddTableData(RenderTooltipData(data_text,times_shown[i]),rowdata);
} else {
tabletext += AddTableData(`${data_text}`,rowdata);
}
}
return AddTableRow(tabletext,`data-key="${key}"`);
}
function RenderOrderMessage(period,sorttype) {
let header = period_info.header[period];
switch (sorttype) {
case 0:
return `Copyrights ordered by user postcount; ${header} period; H -> L`;
case 1:
return `Copyrights ordered by user postcount; ${header} period; L -> H`;
case 2:
return `Copyrights ordered by site postcount; ${header} period; H -> L`;
case 3:
return `Copyrights ordered by site postcount; ${header} period; L -> H`;
}
}
//Get the data and validate it without checking the expires
function GetCountData(key,default_val=null) {
let count_data = JSPLib.storage.getIndexedSessionData(key);
if (!ValidateEntry(key,count_data)) {
return default_val;
}
return count_data.value;
}
function GetTableValue(key,type) {
if (key == '') {
return GetCountData('ct' + type + `-${CU.usertag}:` + CU.current_username,"N/A");
}
var useruploads = GetCountData('ct' + type + `-${CU.usertag}:` + CU.current_username + ' ' + key,"N/A");
var alluploads = GetCountData('ct' + type + '-' + key,"N/A");
return `(${useruploads}/${alluploads})`;
}
//Render copyrights
function RenderCopyrights(period) {
let copytags = CU.user_copytags[CU.usertag][CU.current_username][period].sort();
return copytags.map((copyright)=>{
let tag_text = JSPLib.utility.maxLengthString(copyright);
let taglink = JSPLib.danbooru.postSearchLink(copyright, tag_text, 'class="tag-type-3"');
let active = CU.active_copytags.includes(copyright) ? ' class="cu-active-copyright"' : '';
return `${taglink}`;
}).join(' ');
}
function RenderCopyrightControls() {
return copyright_periods.map((period)=>{
let period_name = period_info.longname[period];
return `${JSPLib.utility.titleizeString(period_name)}`;
}).join(' ') + 'Manual';
}
//Render Tooltips
function RenderTooltipData(text,period,limited=false) {
let tooltip_html = RenderAllToolPopups(period,limited);
return `
${text}${tooltip_html}
`;
}
function RenderAllToolPopups(period,limited) {
return tooltip_metrics.map((metric) => RenderToolpopup(metric,period,limited)).join('');
}
function RenderToolpopup(metric,period,limited) {
let inner_text = (limited ? RenderStatistics('',metric,period,true) : '');
return `
${inner_text}`;
}
function RenderAllTooltipControls() {
return tooltip_metrics.map((metric) => RenderToolcontrol(metric)).join('');
}
function RenderToolcontrol(metric) {
return `
${JSPLib.utility.titleizeString(metric)}`;
}
function RenderStatistics(key,attribute,period,limited=false) {
let period_key = GetPeriodKey(period_info.longname[period]);
let data = JSPLib.storage.getIndexedSessionData(period_key);
if (!data) {
return "No data!";
}
let stat = data.value;
if (!limited) {
let uploads = PostDecompressData(stat);
if (key !== '') {
uploads = uploads.filter((val) => val.copyrights.split(' ').includes(key));
}
//It's possible with their longer expirations for daily copyrights that don't exist in other periods
if (uploads.length === 0) {
return "No data!";
}
stat = GetAllStatistics(uploads,attribute);
} else {
stat = stat[attribute];
}
return RenderAllStats(stat,attribute);
}
function RenderAllStats(stat,attribute) {
if (attribute === 'week') {
return RenderWeeklist(stat);
} else if (attribute === 'day') {
return RenderDaylist(stat);
} else {
return RenderStatlist(stat);
}
}
function RenderWeeklist(stat) {
return `
Sun: ${stat[0]}
Mon: ${stat[1]}
Tue: ${stat[2]}
Wed: ${stat[3]}
Thu: ${stat[4]}
Fri: ${stat[5]}
Sat: ${stat[6]}
`;
}
function RenderDaylist(stat) {
return `
00-04: ${stat[0]}
04-08: ${stat[1]}
08-12: ${stat[2]}
12-16: ${stat[3]}
16-20: ${stat[4]}
20-24: ${stat[5]}
`;
}
function RenderStatlist(stat) {
return `
Max: ${stat.max}
Avg: ${stat.average}
StD: ${stat.stddev}
Out: ${stat.outlier}
Adj: ${stat.adjusted}
`;
}
function GetAllStatistics(posts,attribute) {
if (attribute === 'week') {
return GetWeekStatistics(posts);
} else if (attribute === 'day') {
return GetDayStatistics(posts);
} else {
return GetPostStatistics(posts,attribute);
}
}
function GetWeekStatistics(posts) {
let week_days = new Array(7).fill(0);
posts.forEach((upload)=>{
let timeindex = new Date(upload.created).getUTCDay();
week_days[timeindex] += 1;
});
let week_stats = week_days.map((day)=>{
let percent = (100 * day / posts.length);
return (percent === 0 || percent === 100 ? percent : JSPLib.utility.setPrecision(percent,1));
});
return week_stats;
}
function GetDayStatistics(posts) {
let day_hours = new Array(6).fill(0);
posts.forEach((upload)=>{
let timeindex = Math.floor(new Date(upload.created).getUTCHours() / 4);
day_hours[timeindex] += 1;
});
let day_stats = day_hours.map((day)=>{
let percent = (100 * day / posts.length);
return (percent === 0 || percent === 100 ? percent : JSPLib.utility.setPrecision(percent,1));
});
return day_stats;
}
function GetPostStatistics(posts,attribute) {
let data = JSPLib.utility.getObjectAttributes(posts,attribute);
let data_max = Math.max(...data);
let data_average = JSPLib.statistics.average(data);
let data_stddev = JSPLib.statistics.standardDeviation(data);
let data_outliers = JSPLib.statistics.removeOutliers(data);
let data_removed = data.length - data_outliers.length;
let data_adjusted = JSPLib.statistics.average(data_outliers);
return {
max: data_max,
average: JSPLib.utility.setPrecision(data_average,2),
stddev: JSPLib.utility.setPrecision(data_stddev,2),
outlier: data_removed,
adjusted: JSPLib.utility.setPrecision(data_adjusted,2)
};
}
function AssignPostIndexes(period,posts,time_offset) {
let points = period_info.points[period];
//Have to do it this way to avoid getting the same object
let periods = JSPLib.utility.arrayFill(points, "[]");
posts.forEach((post)=>{
let index = Math.floor((Date.now() - post.created - time_offset)/(period_info.divisor[period]));
index = (points ? Math.min(points-1,index) : index);
index = Math.max(0,index);
if (index >= periods.length) {
periods = periods.concat(JSPLib.utility.arrayFill(index + 1 - periods.length, "[]"));
}
periods[index].push(post);
});
return periods;
}
function GetPeriodAverages(indexed_posts,metric) {
let period_averages = [];
for (let index in indexed_posts) {
if (!indexed_posts[index].length) continue;
let data_point = {
x: parseInt(index),
y: JSPLib.utility.setPrecision(JSPLib.statistics.average(JSPLib.utility.getObjectAttributes(indexed_posts[index],metric)),2)
};
period_averages.push(data_point);
}
return period_averages;
}
function GetPeriodPosts(indexed_posts) {
let period_uploads = [];
for (let index in indexed_posts) {
if (!indexed_posts[index].length) continue;
let data_point = {
x: parseInt(index),
y: indexed_posts[index].length
};
period_uploads.push(data_point);
}
return period_uploads;
}
//Helper functions
//Returns a sorted key array from highest to lowest using the length of the array in each value
function SortDict(dict) {
var items = Object.keys(dict).map((key) => [key, dict[key].length]);
items.sort((first, second)=>{
if (first[1] !== second[1]) {
return second[1] - first[1];
} else {
return first[0].localeCompare(second[0]);
}
});
return items.map((entry) => entry[0]);
}
function BuildTagParams(type,tag) {
return (type === 'at' ? '' : ('age:..1' + type + ' ')) + tag;
}
function GetCopyrightCount(posts) {
let copyright_count = {};
posts.forEach((post)=>{
post.copyrights.split(' ').forEach((tag)=>{
copyright_count[tag] = copyright_count[tag] || [];
copyright_count[tag] = copyright_count[tag].concat([post.id]);
});
});
if (CU.user_settings.postcount_threshold) {
for (let copyright in copyright_count) {
if (copyright_count[copyright].length < CU.user_settings.postcount_threshold) {
delete copyright_count[copyright];
}
}
}
return copyright_count;
}
function CompareCopyrightCounts(dict1,dict2) {
let difference = [];
JSPLib.utility.arrayUnion(Object.keys(dict1), Object.keys(dict2)).forEach((key)=>{
if (!JSPLib.utility.arrayEquals(dict1[key], dict2[key])) {
difference.push(key);
}
});
return difference;
}
function CheckCopyrightVelocity(tag) {
var dayuploads = JSPLib.storage.getIndexedSessionData('ctd-' + tag);
var weekuploads = JSPLib.storage.getIndexedSessionData('ctw-' + tag);
if (dayuploads === null || weekuploads === null) {
return true;
}
var day_gettime = dayuploads.expires - period_info.countexpires.d; //Time data was originally retrieved
var week_velocity = (JSPLib.utility.one_week) / (weekuploads.value | 1); //Milliseconds per upload
var adjusted_poll_interval = Math.min(week_velocity, JSPLib.utility.one_day); //Max wait time is 1 day
return Date.now() > day_gettime + adjusted_poll_interval;
}
async function MergeCopyrightTags(user_copytags) {
let query_implications = JSPLib.utility.arrayDifference(user_copytags,Object.keys(CU.reverse_implications));
let promise_array = query_implications.map((key) => GetReverseTagImplication(key));
let reverse_implications = await Promise.all(promise_array);
query_implications.forEach((key,i)=>{
CU.reverse_implications[key] = reverse_implications[i];
});
return user_copytags.filter((value) => (CU.reverse_implications[value] === 0));
}
function IsMissingTag(tag) {
return GetShownPeriodKeys().reduce((total,period) => (total || !GetCountData(`ct${period}-${tag}`)), false);
}
function MapPostData(posts) {
return posts.map((entry) => (
{
id: entry.id,
score: entry.score,
upscore: entry.up_score,
downscore: -entry.down_score,
favcount: entry.fav_count,
tagcount: entry.tag_count,
gentags: entry.tag_count_general,
copyrights: entry.tag_string_copyright,
created: new Date(entry.created_at).getTime()
}
));
}
function PreCompressData(posts) {
return posts.map((entry) => [entry.id, entry.score, entry.upscore, entry.downscore, entry.favcount, entry.tagcount, entry.gentags, entry.copyrights, entry.created]);
}
function PostDecompressData(posts) {
return posts.map((entry) => (
{
id: entry[0],
score: entry[1],
upscore: entry[2],
downscore: entry[3],
favcount: entry[4],
tagcount: entry[5],
gentags: entry[6],
copyrights: entry[7],
created: entry[8]
}
));
}
function GetTagData(tag) {
return Promise.all(CU.user_settings.periods_shown.map((period) => GetCount(longname_key[period],tag)));
}
function GetPeriodKey(period_name) {
return `${period_name}-${CU.counttype}-${CU.current_username}`;
}
async function CheckPeriodUploads() {
let promise_array = [];
const checkPeriod = (key,period,check)=>{
CU.period_available[CU.usertag][CU.current_username][period] = Boolean(check);
if (!check) {
JSPLib.storage.removeIndexedSessionData(key);
}
};
CU.period_available[CU.usertag][CU.current_username] = CU.period_available[CU.usertag][CU.current_username] || {};
let times_shown = GetShownPeriodKeys();
for (let i = 0; i < times_shown.length; i++) {
let period = times_shown[i];
if (period in CU.period_available[CU.usertag][CU.current_username]) {
continue;
}
let data_key = GetPeriodKey(period_info.longname[period]);
let max_expires = period_info.uploadexpires[period];
let check_promise = JSPLib.storage.checkLocalDB(data_key,ValidateEntry,max_expires).then((check)=>{checkPeriod(data_key,period,check);});
promise_array.push(check_promise);
}
return Promise.all(promise_array);
}
async function PopulateTable() {
//Prevent function from being reentrant while processing uploads
PopulateTable.is_started = true;
var post_data = [];
InitializeControls();
if (CU.checked_users[CU.usertag][CU.current_username] === undefined) {
TableMessage(`