Initial commit: susan automation scripts
Overnight transcoding, music discovery/import, system health reports, stats page generator, and bookmark management. Secrets stored in /etc/automation/ — not in repo.
This commit is contained in:
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
// Add a bookmark to Music/Wishlist folder in Floccus bookmarks
|
||||
// Usage: node add_bookmark_to_wishlist.js <url> <title>
|
||||
|
||||
const crypto = require('crypto');
|
||||
const fs = require('fs');
|
||||
|
||||
const BOOKMARKS_PATH = '/var/www/webdav/bookmarks.xbel';
|
||||
const PASSWORD = process.env.BOOKMARKS_PASSWORD || JSON.parse(require('fs').readFileSync('/etc/automation/bookmarks.json', 'utf8')).password;
|
||||
const WISHLIST_FOLDER_TITLE = 'Wishlist';
|
||||
|
||||
const url = process.argv[2];
|
||||
const title = process.argv[3];
|
||||
|
||||
if (!url || !title) {
|
||||
console.error('Usage: node add_bookmark_to_wishlist.js <url> <title>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function decrypt(data, password) {
|
||||
const ciphertext = Buffer.from(data.ciphertext, 'base64');
|
||||
const salt = data.salt;
|
||||
const key = crypto.pbkdf2Sync(password, salt, 250000, 32, 'sha256');
|
||||
const iv = ciphertext.slice(0, 16);
|
||||
const encrypted = ciphertext.slice(16, -16);
|
||||
const tag = ciphertext.slice(-16);
|
||||
|
||||
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
||||
}
|
||||
|
||||
function encrypt(plaintext, password, salt) {
|
||||
const key = crypto.pbkdf2Sync(password, salt, 250000, 32, 'sha256');
|
||||
const iv = crypto.randomBytes(16);
|
||||
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
|
||||
return Buffer.concat([iv, encrypted, tag]).toString('base64');
|
||||
}
|
||||
|
||||
const escapeXml = (str) => str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(BOOKMARKS_PATH, 'utf8'));
|
||||
let xml = decrypt(data, PASSWORD);
|
||||
|
||||
// Check if bookmark already exists
|
||||
if (xml.includes(escapeXml(url)) || xml.includes(url)) {
|
||||
console.log('Bookmark already exists');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Find highest ID
|
||||
const idMatch = xml.match(/highestId :(\d+):/);
|
||||
let highestId = idMatch ? parseInt(idMatch[1]) : 100;
|
||||
const newId = highestId + 1;
|
||||
|
||||
// Find the Wishlist folder and add bookmark inside it
|
||||
const wishlistPattern = /<folder id="\d+">\s*<title>Wishlist<\/title>/;
|
||||
|
||||
if (wishlistPattern.test(xml)) {
|
||||
// Add bookmark inside existing Wishlist folder
|
||||
const newBookmark = `<bookmark href="${escapeXml(url)}" id="${newId}">
|
||||
<title>${escapeXml(title)}</title>
|
||||
</bookmark>
|
||||
</folder>`;
|
||||
|
||||
// Find Wishlist folder's closing tag and insert before it
|
||||
xml = xml.replace(
|
||||
/(<folder id="\d+">\s*<title>Wishlist<\/title>[\s\S]*?)(<\/folder>)/,
|
||||
(match, folderContent, closingTag) => folderContent + newBookmark
|
||||
);
|
||||
} else {
|
||||
console.error('Wishlist folder not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Update highest ID
|
||||
xml = xml.replace(/highestId :(\d+):/, `highestId :${newId}:`);
|
||||
|
||||
// Save
|
||||
const newCiphertext = encrypt(xml, PASSWORD, data.salt);
|
||||
fs.writeFileSync(BOOKMARKS_PATH, JSON.stringify({ ciphertext: newCiphertext, salt: data.salt }));
|
||||
|
||||
console.log(`Added: ${title}`);
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user