Skip to content

Instantly share code, notes, and snippets.

@boecko
Last active April 17, 2024 19:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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
})();
@sedlund
Copy link

sedlund commented Jan 3, 2024

gist has some unfortunate features for updateURL and downloadURL.

The acaa4c8b7548ede02262bcd50af0212c26d1ef01 is the revision hash of this particular version. So you really want the URLs to be only https://gist.github.com/boecko/e2d0effe7c61976c22e6bc0a8ee645c7/raw/photoprismbulkeditor.user.js

The other issue is github does not send no-cache header with the raw URL. Github cache, your browser, http proxy, etc will cache it. This will prevent quick updates. For something like this unlikely to change very often, it's probably not a huge deal, but something to keep in mind when working with it.

To manually work around that (in your browser, curl, etc, not in the updateURL's) you can use the above URL and add a ?someRandomString eg https://gist.github.com/boecko/e2d0effe7c61976c22e6bc0a8ee645c7/raw/photoprismbulkeditor.user.js?afdshakhfdka to the end which will force a fresh GET of the URL.

@boecko
Copy link
Author

boecko commented Jan 3, 2024

@sedlund thanks .. updated it

@sedlund
Copy link

sedlund commented Jan 3, 2024

I would not recommend using ?uf1Ahngo at the end of the updateURL or downloadURL in the gist file. It is only for cache busting, as it must be changed each time to force a new GET. It should only be used on the command line to manually force a GET from the server.

@Moosbueffel
Copy link

How does it work? Do I something wrong, or doesn't it work?

I changed the tittle and pressed the button bulkchange. Nothing happended!

I checked the box at height and did an input of 100. Than I pressed bulkchange. The button bulkchange than changed to inactive(?). If I than go to next picture, nothing changed there. If I go back, there is a hook instead of the button bulkchange. And the previous edited height was deleted.

Before:
grafik

After:
grafik

And where do I input the keywords: prepend, append and replace?

@boecko
Copy link
Author

boecko commented Jan 11, 2024

@Moosbueffel It's just for gps coordinates at the moment, as noted in the first line of the readme.

You should have checkboxes on "Höhe", "Breitengrad" and "Längengrad". Which version of photoprismn do you run ?

@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