Skip to content

Instantly share code, notes, and snippets.

@boecko
Last active April 17, 2024 19:35
Show Gist options
  • Save boecko/e2d0effe7c61976c22e6bc0a8ee645c7 to your computer and use it in GitHub Desktop.
Save boecko/e2d0effe7c61976c22e6bc0a8ee645c7 to your computer and use it in GitHub Desktop.
Greasemonkey script to bulk edit gps in photoprismn

This is a greasemonkey/tampermonkey script to bulk edit gps in photoprismn

⚠️ MAKE SURE YOU HAVE GREASEMONKEY or TAMPERMONKEY INSTALLED ⚠️

Install it via https://gist.github.com/boecko/e2d0effe7c61976c22e6bc0a8ee645c7/raw/photoprismbulkeditor.user.js

Rules adapted from stephenchews gist

  1. ⚠️ Make sure there are MULTIPLE photos selected before pressing bulkchange ⚠️

    Failing to do so will cause the operation to loop through ALL of your photos.

  2. Check the the checkbox of the attribute you do want to update.

Screenshot

bulkchange

Related Links

// ==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://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
})();
@boecko
Copy link
Author

boecko commented Jan 11, 2024

@Moosbueffel
you can still use
runBulk..
like in the original script of @stephenchew

@Moosbueffel
Copy link

I use: Build 231128-f48ff16ef

I took a short view to script of @stephenchew, but it seems to difficult for me, so I was happt to find your solution. Also it seems to need a google browser. (I normally use firefox and sometimes opera, which has the chromium basis. perhaps it will also work there). I can give him a try next week.

@boecko
Copy link
Author

boecko commented Jan 11, 2024

I use: Build 231128-f48ff16ef

I took a short view to script of @stephenchew, but it seems to difficult for me, so I was happt to find your solution. Also it seems to need a google browser. (I normally use firefox and sometimes opera, which has the chromium basis. perhaps it will also work there). I can give him a try next week.

Es sollte auch im Firefox funktionieren.

  • F12 drücken
  • runBulk... in der console ausführen wie bei @stephenchew beschrieben und darauf achten, dass Fotos selektiert sind.

Der Code von stephenchew ist schon automatisch geladen.

@wsteelenyc
Copy link

wsteelenyc commented Apr 16, 2024

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);

    if (validation) {
        console.error('There is an error in the data:\n\n' + validation);
        return;
    }…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment