From 1c0b5771e31817ad3340219996aacb40420cf4b3 Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Fri, 5 Apr 2024 14:01:20 +0200 Subject: [PATCH] qutebrowser: inital version of photoprism bulk edit userscript --- .../greasemonkey/photoprismbulkeditor.user.js | 263 ++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 .local/share/qutebrowser/greasemonkey/photoprismbulkeditor.user.js diff --git a/.local/share/qutebrowser/greasemonkey/photoprismbulkeditor.user.js b/.local/share/qutebrowser/greasemonkey/photoprismbulkeditor.user.js new file mode 100644 index 0000000..71384ee --- /dev/null +++ b/.local/share/qutebrowser/greasemonkey/photoprismbulkeditor.user.js @@ -0,0 +1,263 @@ +// ==UserScript== +// @name photoprism bulkeditor +// @version 0.1 +// @description bulkeditor +// @author andy@boeckler.org +// @match https://*/library/browse* +// @match https://*/library/all* +// @match https://*/library/albums/*/view* +// @match https://*/library/favorites* +// @namespace https://bilder.bocken.org/ +// @updateURL https://gist.github.com/boecko/e2d0effe7c61976c22e6bc0a8ee645c7/raw/photoprismbulkeditor.user.js +// @downloadURL https://gist.github.com/boecko/e2d0effe7c61976c22e6bc0a8ee645c7/raw/photoprismbulkeditor.user.js +// @grant none +// ==/UserScript== + +(function() { + 'use strict'; + // from https://gist.github.com/stephenchew/b73ecc75b77a84a92fa350048d5ca84f + //------- START + const isDefined = (value) => typeof value !== 'undefined' && value !== null; + + /** + * + * @returns `true` if the field is set, `false` otherwise + */ + const updateField = (data, field) => { + const type = data[field].type; + const value = data[field].content; + + if (!value) { + return false; + } + + const element = document.forms[0].__vue__._data.inputs.find((element) => + element.$el.className.includes(`input-${field}`) + ); + + switch (type.toLowerCase()) { + case 'prepend': + element.internalValue = value + ' ' + element.internalValue; + break; + case 'append': + element.internalValue += ' ' + value; + break; + case 'replace': + element.internalValue = value; + break; + default: + console.error(`'${type}' is not a valid way of updating a field.`); + return false; + } + + return true; + }; + + const runBulk = async (data) => { + const validation = validateData(data); + + if (validation) { + console.error('There is an error in the data:\n\n' + validation); + return; + } + + console.time('bulk-edit'); + + try { + const pause = async (seconds) => new Promise((r) => setTimeout(r, seconds * 500)); + + let count = 0; + + do { + let dirty = false; + + if (window.interrupt) { + alert('Execution interrupted by user.'); + delete window.interrupt; + return; + } + + for (let field of Object.keys(data)) { + dirty |= updateField(data, field); + } + + if (!dirty) { + console.warn('No field was set. Nothing has changed.'); + return; + } + + const applyButton = document.querySelector('button.action-apply'); + applyButton.click(); + count++; + + const rightButton = document.querySelector('.v-toolbar__items .action-next'); + if (rightButton.disabled) { + break; + } + + await pause(1); + rightButton.click(); + await pause(1); + } while (true); + + const doneButton = document.querySelector('button.action-done'); + doneButton.click(); + + console.info(`Bulk edited ${count} photos.`); + } finally { + console.timeEnd('bulk-edit'); + } + }; + + /** + * Return LF delimited error message, or `null` if all is good. + */ + const validateData = (data) => { + if (!data) { + return 'No data provided.'; + } + + const error = []; + + if (isDefined(data.day?.content)) { + const day = parseInt(data.day.content, 10); + if (isNaN(day) || day < -1 || day > 31 || day === 0) { + error.push('Day must be between 1 and 31. Set to -1 for "Unknown".'); + } + data.day.type = 'replace'; + } + + if (isDefined(data.month?.content)) { + const month = parseInt(data.month.content, 10); + if (isNaN(month) || month < -1 || month > 12 || month === 0) { + error.push('Month must be between 1 and 12. Set to -1 for "Unknown".'); + } + data.month.type = 'replace'; + } + + if (isDefined(data.year?.content)) { + const year = parseInt(data.year.content, 10); + const currentYear = new Date().getFullYear(); + if ((isNaN(year) || year < 1750 || year > currentYear) && year !== -1) { + // 1750 is Photoprism defined year + error.push('Year must be between 1750 and ' + currentYear + '. Set to -1 for "Unknown".'); + } + data.year.type = 'replace'; + } + + return error.length > 0 ? error.join('\n') : null; + }; +//------- END + + const runGpsBulkUpdate = async (url) => { + if(!url) return + let m = url.match(/@(.*)z/) + if( !m[1] ) { + console.warn("URL ist falsch", url) + return + } + let gpsCoords = m[1].split(',') + let data = { + latitude: { + content: gpsCoords[0], + type: 'replace' + }, + longitude: { + content: gpsCoords[1], + type: 'replace' + }, + altitude: { + content: 0, + type: 'replace' + } + } + if(gpsCoords[2] && gpsCoords[2].match(/^\d+$/)) { + data.altitude = { + content: gpsCoords[2], + type: 'replace' + } + } + console.log('runBulk', data); + return await runBulk(data); + // return true; + } + + const runKeywordBulkUpdate = async () => { + let keywords = prompt("Keywords?") + if(!keywords) return + let data = { + keywords: { + content: keywords, + type: 'append', + } + } + return await runBulk(data); + // return true; + } + +//------- greasmonkey code + const BTN_STYLE_1 = 'cursor: pointer; border: solid white' + const BTN_STYLE_2 = 'cursor: pointer; border: solid white; opacity:0.5' + const checkBoxes = {} + const inputs = {} + let submitNode = null + let bulkRunning = false + async function submitHandler(e) { + e.preventDefault() + let bulkData = {} + for(let name in checkBoxes) { + if(!checkBoxes[name].checked) continue + bulkData[name] = { + content: inputs[name].value, + type: 'replace' + } + } + if(Object.keys(bulkData).length == 0) return; + submitNode.disabled = true + submitNode.setAttribute('style', BTN_STYLE_2); + bulkRunning = true + await runBulk(bulkData); + bulkRunning = false + } + + function addSubmitIfMissing() { + const selector = '.input-title input[type=text]' + let inputNode = document.querySelector(selector) + if( inputNode==null || inputNode.offsetParent == null) return + if(inputNode.nextSibling) return + + let newSubmit = document.createElement('input'); + newSubmit.setAttribute('type', 'submit'); + newSubmit.setAttribute('value', 'Bulkchange'); + newSubmit.setAttribute('style', BTN_STYLE_1); + inputNode.parentNode.append(newSubmit); + newSubmit.onclick = submitHandler + submitNode = newSubmit + } + function addCheckBoxIfMissing(name) { + const selector = '.input-' + name + ' input[type=text]' + let inputNode = document.querySelector(selector) + // wenn da und nicht unsichtbar + if( inputNode==null || inputNode.offsetParent == null) return + if(inputNode.nextSibling) return + + let newCheckBox = document.createElement('input'); + newCheckBox.setAttribute('type', 'checkbox'); + inputNode.parentNode.append(newCheckBox); + checkBoxes[name] = newCheckBox + inputs[name] = inputNode + } + + var checkExistTimer = setInterval(function () { + if(bulkRunning) return + addCheckBoxIfMissing('latitude') + addCheckBoxIfMissing('longitude') + addCheckBoxIfMissing('altitude') + addSubmitIfMissing() + },1000); + + window.runBulk = runBulk + window.runGpsBulkUpdate = runGpsBulkUpdate + window.runKeywordBulkUpdate = runKeywordBulkUpdate + +})();