2024-04-05 14:01:20 +02:00
|
|
|
// ==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
|
2024-04-05 14:06:56 +02:00
|
|
|
addCheckBoxIfMissing('day')
|
|
|
|
addCheckBoxIfMissing('month')
|
|
|
|
addCheckBoxIfMissing('year')
|
2024-04-05 14:01:20 +02:00
|
|
|
addCheckBoxIfMissing('latitude')
|
|
|
|
addCheckBoxIfMissing('longitude')
|
|
|
|
addCheckBoxIfMissing('altitude')
|
|
|
|
addSubmitIfMissing()
|
|
|
|
},1000);
|
|
|
|
|
|
|
|
window.runBulk = runBulk
|
|
|
|
window.runGpsBulkUpdate = runGpsBulkUpdate
|
|
|
|
window.runKeywordBulkUpdate = runKeywordBulkUpdate
|
|
|
|
|
|
|
|
})();
|