Last active
February 3, 2022 18:36
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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">( order by: <a id="ur-upload" href="#">upload</a> | <a id="ur-changes" href="#">changes</a> )</span></h1>'; | |
const PROFILE_LINKS = '%s - <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), ' ')); | |
$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], ' -> ')); | |
$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], ' -> ')); | |
$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), ' ')); | |
} | |
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], ' -> ')); | |
} | |
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], ' -> ')); | |
} | |
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