Compare commits
	
		
			2 Commits
		
	
	
		
			78d4618dfa
			...
			740ab0f2ad
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						740ab0f2ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						1c0b5771e3
	
				 | 
					
					
						
@@ -0,0 +1,266 @@
 | 
			
		||||
// ==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
 | 
			
		||||
	addCheckBoxIfMissing('day')
 | 
			
		||||
	addCheckBoxIfMissing('month')
 | 
			
		||||
	addCheckBoxIfMissing('year')
 | 
			
		||||
        addCheckBoxIfMissing('latitude')
 | 
			
		||||
        addCheckBoxIfMissing('longitude')
 | 
			
		||||
        addCheckBoxIfMissing('altitude')
 | 
			
		||||
        addSubmitIfMissing()
 | 
			
		||||
    },1000);
 | 
			
		||||
 | 
			
		||||
    window.runBulk = runBulk
 | 
			
		||||
    window.runGpsBulkUpdate = runGpsBulkUpdate
 | 
			
		||||
    window.runKeywordBulkUpdate = runKeywordBulkUpdate
 | 
			
		||||
 | 
			
		||||
})();
 | 
			
		||||
		Reference in New Issue
	
	Block a user