|
// ==UserScript== |
|
// @name photoprism bulkeditor |
|
// @version 0.1 |
|
// @description bulkeditor |
|
// @author [email protected] |
|
// @match https://*/library/browse* |
|
// @match https://*/library/all* |
|
// @match https://*/library/albums/*/view* |
|
// @match https://*/library/favorites* |
|
// @namespace https://boeckler.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 |
|
|
|
})(); |
Update: I didn't realize this is only for gps coordinates, so I'll need to look for something that can bulk update other metadata.
I've been trying to get this to work with no luck. I'm running PhotoPrism® CE
Build 221118-e58fee0 .
I get the bulkchange button, but nothing happens nor updates. If I try to manually "runBulk" in the console I get the following error...
async (data) => {
const validation = validateData(data);