Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@BrokenEagle
Last active February 3, 2022 18:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrokenEagle/caba9ebf286f70f26b5eae6e7bde7d84 to your computer and use it in GitHub Desktop.
Save BrokenEagle/caba9ebf286f70f26b5eae6e7bde7d84 to your computer and use it in GitHub Desktop.
Shows all of the tag, rating, parent and source changes made to a post since it was uploaded.
// ==UserScript==
// @name UploadsReport
// @namespace https://gist.github.com/BrokenEagle
// @version 4.0
// @description Shows the tag changes made since the original upload.
// @source https://danbooru.donmai.us/users/23799
// @author BrokenEagle
// @match *://*.donmai.us/profile
// @match *://*.donmai.us/users/*
// @match *://*.donmai.us/post_versions?*
// @exclude /^https?://\w+\.donmai\.us/.*\.(xml|json|atom)(\?|$)/
// @grant none
// @run-at document-end
// @downloadURL https://gist.github.com/BrokenEagle/caba9ebf286f70f26b5eae6e7bde7d84/raw/UploadsReport.user.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20201230-module/lib/module.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20201215/lib/load.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20201215/lib/utility.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20201215/lib/network.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20201215/lib/danbooru.js
// ==/UserScript==
/* global $ JSPLib */
/****Global variables****/
//Variables for load.js
const PROGRAM_LOAD_REQUIRED_VARIABLES = ['window.jQuery'];
const PROGRAM_LOAD_OPTIONAL_SELECTORS = ['#post-versions-table', '.user-statistics'];
//Program name constants
const PROGRAM_NAME = 'UploadsReport';
//CSS constants
const PROGRAM_CSS = `
.edits-column {
vertical-align: top;
}
.diff-list ins,
.diff-list ins a,
.diff-list del,
.diff-list del a {
margin-right: 0;
}
.ur-heading {
font-size: 125%;
font-weight: bold;
text-decoration: underline;
margin-bottom: 0.2em;
}
.ur-subheading {
font-weight: bold;
margin-bottom: 0.1em;
}
.ur-section {
margin-bottom: 1em;
}
.ur-explanation {
margin-top: 1em;
color: grey;
font-size: 80%;
}
.ur-all-changes {
border: 1px dashed #DDD;
padding: 5px;
}
.ur-last-change {
margin-top: 1em;
border: 1px solid #CCC;
padding: 10px;
font-size: 80%;
color: #666;
background-color: #F8F8F8;
}
#ur-order-options {
font-size: 80% !important;
}
#ur-order-options a {
color: red;
}
#ur-order-options a.active {
color: green;
}`;
//HTML constants
const REPORT_HEADER ='<h1>Tag Changes Report <span id="ur-order-options">(&thinsp;order by: <a id="ur-upload" href="#">upload</a> | <a id="ur-changes" href="#">changes</a>&thinsp;)</span></h1>';
const PROFILE_LINKS = '%s&emsp;-&emsp;<span style="font-size: 105%; font-weight: bold;">Uploads report</span> (<span style="font-size: 80%"><b>order by:</b> <a href="%s">upload</a> | <a id="ur-changes" href="#">changes</a> </span>)';
const INACTIVE_PREV = '<span class="paginator-prev" rel="prev" data-shortcut="a left" title="Shortcut is a or left"><i class="icon fas fa-chevron-left"></i></span>';
const INACTIVE_NEXT = '<span class="paginator-next" rel="next" data-shortcut="d right" title="Shortcut is d or right"><i class="icon fas fa-chevron-right"></i></span>';
const INACTIVE_PAGE = '<span class="paginator-current font-bold">%s</span>';
const ACTIVE_PREV = '<a class="paginator-prev" rel="prev" data-shortcut="a left" href="#" title="Shortcut is a or left" data-page="%s"><i class="icon fas fa-chevron-left "></i></a>';
const ACTIVE_NEXT = '<a class="paginator-next" rel="next" data-shortcut="d right" href="#" title="Shortcut is d or right" data-page="%s"><i class="icon fas fa-chevron-right"></i></a>';
const ACTIVE_PAGE = '<a class="paginator-page desktop-only" href="#" data-page="%s">%s</a>';
const ELLIPSIS = '<i class="icon fas fa-ellipsis-h paginator-ellipsis text-muted desktop-only"></i>';
//Other constants
const RATINGS_KEY = {
s: 'safe',
q: 'questionable',
e: 'explicit',
};
/****Functions****/
// Helper functions
function FilterMetatags(taglist) {
return taglist.filter((tag) => !(tag.startsWith('rating:') || tag.startsWith('parent:') || tag.startsWith('source:')));
}
function GetUrlSearch(params) {
return Object.keys(params).map((key) => (encodeURIComponent(key) + '=' + encodeURIComponent(params[key]))).join('&');
}
function TimeAgo(timestamp) {
let time_interval = Date.now() - timestamp;
if (time_interval < JSPLib.utility.one_hour) {
return JSPLib.utility.setPrecision(time_interval / JSPLib.utility.one_minute, 2) + " minutes ago";
} else if (time_interval < JSPLib.utility.one_day) {
return JSPLib.utility.setPrecision(time_interval / JSPLib.utility.one_hour, 2) + " hours ago";
} else if (time_interval < JSPLib.utility.one_month) {
return JSPLib.utility.setPrecision(time_interval / JSPLib.utility.one_day, 2) + " days ago";
} else {
return "> 1 month ago";
}
}
// Render functions
function RenderDiffItem(tag,is_add) {
let link = JSPLib.danbooru.postSearchLink(tag, tag);
let html_tag = (is_add ? 'ins' : 'del');
return `<${html_tag}>${link}</${html_tag}>`;
}
function RenderDiffList(title, type, items, joiner) {
let title_html = (title ? `<div class="ur-subheading">${title}</div>` : '');
return `
<div class="ur-section">
${title_html}
<span class="${type}-diff-list diff-list">
${items.join(joiner)}
</span>
</div>`;
}
// Auxiliary functions
function GetPostIDs() {
return JSPLib.utility.arrayUnique($('#post-versions-table tbody tr').map((i,entry)=>Number(entry.dataset.postId)).toArray());
}
function GetTags(row) {
return $('.tags-column .inline-tag-list [data-tag-name]', row).map((i,entry)=>entry.dataset.tagName).toArray();
}
function SortTableRows(post_ids) {
let $tbody = $('#post-versions-table tbody');
let $rows = $tbody.find('tr').detach();
let rows = Array(...$rows);
let sorted_rows = post_ids.map((post_id) => {
let row = rows.find((row) => {
return $(row).data('post-id') === post_id;
});
return row;
});
$('#post-versions-table th.post-version-select-column').after('<th width="1%" class="post-column">Post</th>');
$('#p-revert-listing > .post-preview').remove();
sorted_rows.forEach((row)=>{
$('td.post-version-select-column', row).after('<td class="post-column"></td>')
$tbody.append(row);
});
}
// Network functions
async function GetVersions(post_ids) {
let version_url_addons = {search: { post_id: post_ids.join(',')}, expires_in: '3600s'};
let versions = await JSPLib.danbooru.getAllItems('post_versions', 200, null, {addons: version_url_addons, long_format: false});
let version_dict = versions.reduce((acc,version)=>{
acc[version.post_id] = (version.post_id in acc ? JSPLib.utility.concat(acc[version.post_id], [version]) : [version]);
return acc;
}, {});
return version_dict;
}
async function GetPaginatorTokens(username, current_page, limit) {
let count_data = await JSPLib.danbooru.submitRequest('counts/posts', {tags: 'user:' + username, expires_in: '3600s'}, {counts: {posts: 0}});
let upload_count = count_data.counts.posts;
let previous_page = Math.max(1, current_page - 1);
let last_page = Math.ceil(upload_count / limit) || Infinity;
let next_page = Math.min(last_page, current_page + 1);
let left_page = Math.max(current_page - 4, 2);
let penultimate_page = last_page - 1;
let right_page = Math.min(current_page + 4, penultimate_page);
let pages = [1];
if (left_page !== 2) {
pages.push('...');
}
pages = JSPLib.utility.concat(pages, Array.from({length: right_page - left_page}, (_, i) => i + left_page));
if (right_page !== penultimate_page) {
pages.push('...');
}
if (last_page > 1 && last_page !== Infinity) {
pages.push(last_page);
}
return [pages, previous_page, last_page];
}
function PageNavigation(params, page_num, updater_name) {
delete params.page;
delete params.version;
params.is_new = 'true';
params.changes_page = String(page_num);
params.mode = 'changes';
let limit = Math.min(Number(params.limit) || 20, 100);
let post_url_addons = {tags: 'status:any order:change user:' + updater_name, page: page_num, limit: limit, only: 'id', expires_in: '3600s'};
JSPLib.danbooru.submitRequest('posts', post_url_addons).then((posts)=>{
params['search[post_id]'] = posts.map((post) => post.id).join(',');
location = '/post_versions?' + GetUrlSearch(params);
});
}
// Main execution functions
function GetChanges(user_id, version_dict) {
let change_dict = {};
for (let key in version_dict) {
version_dict[key].sort((a,b) => (a.version - b.version));
let changes = change_dict[key] = {last_edited: null, added_tags: [], removed_tags: [], last_changes: null, total_edits: 0};
for (let i = 0; i < version_dict[key].length; i++) {
let version = version_dict[key][i];
let added_tags = FilterMetatags(version.added_tags);
let removed_tags = FilterMetatags(version.removed_tags);
if (version.updater_id === user_id) {
changes.added_tags = JSPLib.utility.arrayDifference(changes.added_tags, removed_tags);
changes.removed_tags = JSPLib.utility.arrayDifference(changes.removed_tags, added_tags);
if (version.rating_changed) {
changes.rating_diff = null;
changes.rating_orig = RATINGS_KEY[version.rating];
}
if (version.parent_changed) {
changes.parent_diff = null;
changes.parent_orig = version.parent_id;
}
if (version.source_changed) {
changes.source_diff = null;
changes.source_orig = version.source;
}
} else {
changes.added_tags = JSPLib.utility.arrayUnion(changes.added_tags, added_tags);
changes.removed_tags = JSPLib.utility.arrayUnion(changes.removed_tags, removed_tags);
changes.last_changes = {added_tags, removed_tags};
if (version.rating_changed) {
changes.last_changes.rating_diff = changes.rating_diff = RATINGS_KEY[version.rating];
}
if (version.parent_changed) {
changes.last_changes.parent_diff = changes.parent_diff = version.parent_id;
}
if (version.source_changed) {
changes.last_changes.source_diff = changes.source_diff = version.source;
}
changes.last_edited = version.updated_at;
changes.total_edits += 1;
}
}
let added_tags = JSPLib.utility.dataCopy(changes.added_tags);
changes.added_tags = JSPLib.utility.arrayDifference(added_tags, changes.removed_tags);
changes.removed_tags = JSPLib.utility.arrayDifference(changes.removed_tags, added_tags);
}
return change_dict;
}
function ProcessRow(row, changes) {
let $edits_col = $('.edits-column', row).html("");
let $changes_col = $('.changes-column', row).html("");
if (changes.last_edited === null) {
return;
}
let $all_edits = $('<div class="ur-all-changes"><div class="ur-heading">All edits</div></div>');
if (changes.added_tags.length || changes.removed_tags.length) {
let add_items = changes.added_tags.map((tag) => RenderDiffItem(tag, true));
let rem_items = changes.removed_tags.map((tag) => RenderDiffItem(tag, false));
$all_edits.append(RenderDiffList('Tag changes', 'tag', add_items.concat(rem_items), '&emsp;'));
$changes_col.append('<div class="version-statuses" data-altered="true">Tags</div>');
}
if (changes.rating_diff !== null) {
let rem_item = RenderDiffItem('rating:' + changes.rating_orig, false);
let add_item = RenderDiffItem('rating:' + changes.rating_diff, true);
$all_edits.append(RenderDiffList('Rating change', 'rating', [rem_item, add_item], '&nbsp;->&nbsp;'));
$changes_col.append('<div class="version-statuses" data-altered="true">Rating</div>');
}
if (changes.parent_diff !== null) {
let old_parent = (Number.isInteger(changes.parent_orig) ? changes.parent_orig : 'none');
let new_parent = (Number.isInteger(changes.parent_diff) ? changes.parent_diff : 'none');
let rem_item = RenderDiffItem('parent:' + old_parent, false);
let add_item = RenderDiffItem('parent:' + new_parent, true);
$all_edits.append(RenderDiffList('Parent change', 'parent', [rem_item, add_item], '&nbsp;->&nbsp;'));
$changes_col.append('<div class="version-statuses" data-altered="true">Parent</div>');
}
if (changes.source_diff !== null) {
let old_source = (changes.source_orig !== "" ? changes.source_orig : 'none');
let new_source = (changes.source_diff !== "" ? changes.source_diff : 'none');
let rem_item = RenderDiffItem('source:' + old_source, false);
let add_item = RenderDiffItem('source:' + new_source, true);
$all_edits.append(RenderDiffList('Source change', 'source', [rem_item, add_item], '<br>'));
$changes_col.append('<div class="version-statuses" data-altered="true">Source</div>');
}
if (changes.last_edited !== null) {
let time_string = TimeAgo(new Date(changes.last_edited).getTime());
$all_edits.append(`<div class="ur-explanation">(Last edited: ${time_string})</div>`);
}
$edits_col.append($all_edits);
if (false || changes.total_edits > 1) {
let $last_change = $('<div class="ur-last-change"><span class="ur-heading">Final edit</span></div>');
let add_headers = ((changes.last_changes.added_tags.length || changes.last_changes.removed_tags.length) + Boolean(changes.last_changes.rating_diff) + Boolean(changes.last_changes.parent_diff) + Boolean(changes.last_changes.source_diff)) > 1;
if (changes.last_changes.added_tags.length || changes.last_changes.removed_tags.length) {
let add_items = changes.last_changes.added_tags.map((tag) => RenderDiffItem(tag, true));
let rem_items = changes.last_changes.removed_tags.map((tag) => RenderDiffItem(tag, false));
let header = (add_headers ? 'Tags' : null);
$last_change.append(RenderDiffList(header, 'tag', add_items.concat(rem_items), '&emsp;'));
}
if (changes.last_changes.rating_diff) {
let rem_item = RenderDiffItem('rating:' + changes.rating_orig, false);
let add_item = RenderDiffItem('rating:' + changes.last_changes.rating_diff, true);
let header = (add_headers ? 'Rating' : null);
$last_change.append(RenderDiffList(header, 'rating', [rem_item, add_item], '&nbsp;->&nbsp;'));
}
if (changes.last_changes.parent_diff) {
let old_parent = (Number.isInteger(changes.parent_orig) ? changes.parent_orig : 'none');
let new_parent = (Number.isInteger(changes.last_changes.parent_diff) ? changes.last_changes.parent_diff : 'none');
let rem_item = RenderDiffItem('parent:' + old_parent, false);
let add_item = RenderDiffItem('parent:' + new_parent, true);
let header = (add_headers ? 'Parent' : null);
$last_change.append(RenderDiffList(header, 'parent', [rem_item, add_item], '&nbsp;->&nbsp;'));
}
if (changes.last_changes.source_diff) {
let old_source = (changes.source_orig !== "" ? changes.source_orig : 'none');
let new_source = (changes.last_changes.source_diff !== "" ? changes.last_changes.source_diff : 'none');
let rem_item = RenderDiffItem('source:' + old_source, false);
let add_item = RenderDiffItem('source:' + new_source, true);
let header = (add_headers ? 'Source' : null);
$last_change.append(RenderDiffList(header, 'source', [rem_item, add_item], '<br>'));
}
$edits_col.append($last_change);
}
}
async function ProcessChanges() {
let version_dict = await GetVersions(GetPostIDs());
let changes_dict = GetChanges($('.user').data('user-id'), version_dict);
$('#c-post-versions tbody tr').each((i,row)=>{
let post_id = Number(row.dataset.postId);
let changes = changes_dict[post_id];
if (!changes) {
console.warn(`versions not found for post #${post_id} with version #${row.dataset.id}`);
return;
}
ProcessRow(row, changes);
});
}
// Initialization functions
function InitializeHeaders(params) {
$('#a-index > h1:first-of-type').replaceWith(REPORT_HEADER);
$('#post-versions-table th.updated-column').text('Uploaded');
if (params.mode === 'changes' && params['search[post_id]'] !== undefined) {
$('#ur-changes').addClass('active');
} else {
$('#ur-upload').addClass('active');
}
$('#a-index > form, #version-comparisons').hide();
SetModeClicks(params);
}
async function InitializePaginator(username, current_page, limit) {
let [pages, previous_page, last_page] = await GetPaginatorTokens(username, current_page, limit);
let $paginator = $('#a-index > div.paginator').html("");
if (previous_page === current_page) {
$paginator.append(INACTIVE_PREV);
} else {
$paginator.append(JSPLib.utility.sprintf(ACTIVE_PREV, previous_page));
}
for (let i = 0; i < pages.length; i++) {
if (pages[i] === current_page) {
$paginator.append(JSPLib.utility.sprintf(INACTIVE_PAGE, pages[i]));
} else if (pages[i] === '...') {
$paginator.append(ELLIPSIS);
} else if (Number.isInteger(pages[i])) {
$paginator.append(JSPLib.utility.sprintf(ACTIVE_PAGE, pages[i], pages[i]));
}
}
if (last_page === current_page || last_page === Infinity) {
$paginator.append(INACTIVE_NEXT);
} else {
$paginator.append(JSPLib.utility.sprintf(ACTIVE_NEXT, previous_page));
}
}
function InitializeThumbnails(post_ids) {
let $tbody = $('#post-versions-table tbody');
JSPLib.network.getNotify('/posts', {tags: 'status:any id:' + post_ids.join(','), limit: post_ids.length, size: '180'}).then((page_html)=>{
let $page = $(page_html);
post_ids.forEach((post_id)=>{
let $thumb = $page.find(`.post-preview[data-id=${post_id}]`);
$thumb.find('.post-preview-score').remove();
$thumb.removeClass('post-preview-show-votes post-preview-fit-compact');
$thumb.addClass('post-preview-fit-fixed blacklisted');
let $row = $tbody.find(`tr[data-post-id=${post_id}]`);
$row.find('td.post-column').append($thumb);
});
});
}
function InitializeChangesMode(params) {
let username = $('#post-versions-table .updated-column .user').data('userName');
let current_page = Number(params.changes_page) || 1;
let limit = Math.min(Number(params.limit) || 20, 100);
InitializePaginator(username, current_page, limit).then((pages)=>{
$('a.paginator-prev, a.paginator-next, a.paginator-page').on('click.ur', (event)=>{
let paginator_classes = event.currentTarget.classList;
let page_num = $(event.currentTarget).data('page');
PageNavigation(params, page_num, username);
event.preventDefault();
});
});
let post_ids_ordered = params['search[post_id]'].split(',').map(Number);
SortTableRows(post_ids_ordered);
InitializeThumbnails(post_ids_ordered);
}
function InitializeProfilePage() {
$('table.user-statistics tr').each((_i, row)=>{
if ($(row).find('th').text() === 'Uploads') {
let $entry = $('td', row);
let links = $entry.find('a');
$entry.html(JSPLib.utility.sprintf(PROFILE_LINKS, links[0].outerHTML, links[1].href));
$('#ur-changes').on('click.ur', (event)=>{
let user_name = $('#a-show > h1 > .user').data('user-name');
let user_id = $(document.body).data('user-id');
PageNavigation({'search[updater_id]': user_id, 'search[is_new]': true, type: 'current'}, 1, user_name);
event.preventDefault();
});
return false;
}
});
}
// Event handlers
function SetModeClicks(params) {
let mode = (params.mode === "changes" ? 'changes' : 'upload');
let username = $('#post-versions-table .updated-column .user').data('userName');
$('#ur-upload').on('click.ur', (event)=>{
if (mode !== 'upload' || confirm('Reload page?')) {
params.page = '1';
params.mode = 'upload';
delete params['search[post_id]'];
location.search = '?' + GetUrlSearch(params);
}
event.preventDefault();
});
$('#ur-changes').on('click.ur', (event)=>{
if (mode !== 'changes' || confirm('Reload page?')) {
PageNavigation(params, 1, username);
}
event.preventDefault();
});
}
//Main program
function Main() {
if (document.body.dataset.controller === 'post-versions') {
let params = JSPLib.utility.parseParams(location.search.slice(1));
if ((params['search[updater_id]'] !== undefined || params['search[updater_name]'] !== undefined) && (params['search[version]'] === "1" || params['search[is_new]'] === "true") && (params.type === "current" || params.type === undefined)) {
InitializeHeaders(params);
if (params.mode === 'changes' && params['search[post_id]'] !== undefined) {
InitializeChangesMode(params);
}
ProcessChanges();
JSPLib.utility.setCSSStyle(PROGRAM_CSS, 'program');
}
} else if (document.body.dataset.controller === 'users') {
InitializeProfilePage();
}
}
/****Execution start****/
JSPLib.load.programInitialize(Main, PROGRAM_NAME, PROGRAM_LOAD_REQUIRED_VARIABLES, [], PROGRAM_LOAD_OPTIONAL_SELECTORS);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment