Skip to content

Instantly share code, notes, and snippets.

@stephenchew
Last active January 6, 2024 14:44
Show Gist options
  • Save stephenchew/b73ecc75b77a84a92fa350048d5ca84f to your computer and use it in GitHub Desktop.
Save stephenchew/b73ecc75b77a84a92fa350048d5ca84f to your computer and use it in GitHub Desktop.
Bulk edit photos in Photoprism

Bulk edit photos in Photoprism

Tested to work with builds: -

  • 231011-63f708417
  • 230923-e59851350
  • 230719-73fa7bbe8

Use this workaround until Photoprism implemented bulk-edit feature.

GitHub issue: photoprism/photoprism#271

Instructions

  1. Run the function content in declaration.js once, in Chrome DevTools.
  2. Execute the function with the content from run.js to bulk edit photos, as many times as required.

Rules

  1. ⚠️ Make sure there are MULTIPLE photos selected and "Edit" window is opened. ⚠️

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

  2. Comment out the content field of the attribute you do not want to update.
  3. Use prepend to add content to the front of the original field value. Typically used for "Title".
  4. Use append to add content to the back of the original value. Typically used in "Keywords".
  5. Use replace to set a new value.

Good to know

  1. Should you change your mind halfway the edit, just assign a truthy value to window.interrupt variable. E.g.
    window.interrupt = 1

    You should see an alert popup telling you the operation is halted. But what were changed prior to this, were unfortunately changed.

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 * 1000));
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;
};
await runBulk({
title: {
// content: 'The Awesome Café /',
type: 'prepend',
},
artist: {
// content: 'Stephen Chew',
type: 'replace',
},
keywords: {
// content: 'hygge',
type: 'append',
},
altitude: {
// content: 50,
type: 'replace',
},
latitude: {
// content: 55.69741126106553,
type: 'replace',
},
longitude: {
// content: 12.58521130458346,
type: 'replace',
},
day: {
// content: 1,
},
month: {
// content: 12,
},
year: {
// content: 2023,
},
});
@astromechza
Copy link

Worked for me! Thanks a lot.

@Blendan1
Copy link

Hey,

I saw what you did here and thougt it might need a GUI and some more securety checks so I used your code (I hope you dont mind) and made a Chrome plugin PhotoPrismBulkEditor.

I hope you like what I did and maybe find it usefull, comments, ideas and critique are welcome.

@boecko
Copy link

boecko commented Jan 2, 2024

Hi,

i've created a greasemonkey-script out of your gist.
see https://gist.github.com/boecko/e2d0effe7c61976c22e6bc0a8ee645c7

You can still run runBulk in the console, but it i've extended the edit-view of photoprismn.
bulkchange

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