init f0ckm
This commit is contained in:
60
debug/autotagger.mjs
Normal file
60
debug/autotagger.mjs
Normal file
@@ -0,0 +1,60 @@
|
||||
import db from "../src/inc/sql.mjs";
|
||||
import lib from "../src/inc/lib.mjs";
|
||||
|
||||
(async () => {
|
||||
const _args = process.argv.slice(2);
|
||||
const _from = +_args[0];
|
||||
const _to = _from + 500;
|
||||
|
||||
const f0cks = await db`
|
||||
select *
|
||||
from items
|
||||
where
|
||||
id not in (select item_id from tags_assign group by item_id) and
|
||||
mime like 'image/%' and
|
||||
id between ${_from} and ${_to}
|
||||
`;
|
||||
|
||||
console.time('blah');
|
||||
for(let f of f0cks) {
|
||||
const tmp = await lib.detectNSFW(f.dest);
|
||||
|
||||
console.log(
|
||||
'https://f0ck.me/' + f.id,
|
||||
tmp.isNSFW,
|
||||
tmp.score.toFixed(2),
|
||||
{
|
||||
sexy: tmp.scores.sexy.toFixed(2),
|
||||
porn: tmp.scores.porn.toFixed(2),
|
||||
hentai: tmp.scores.hentai.toFixed(2),
|
||||
neutral: tmp.scores.neutral.toFixed(2)
|
||||
}
|
||||
);
|
||||
|
||||
await db`
|
||||
insert into "tags_assign" ${
|
||||
db({
|
||||
item_id: f.id,
|
||||
tag_id: tmp.nsfw ? 2 : 1,
|
||||
user_id: 1
|
||||
})
|
||||
}
|
||||
`;
|
||||
|
||||
if(tmp.hentai >= .7) {
|
||||
await db`
|
||||
insert into "tags_assign" ${
|
||||
db({
|
||||
item_id: f.id,
|
||||
tag_id: 4, // hentai
|
||||
user_id: 1 // autotagger
|
||||
})
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
console.timeEnd('blah');
|
||||
process.exit();
|
||||
})();
|
||||
78
debug/backfill_phash.mjs
Normal file
78
debug/backfill_phash.mjs
Normal file
@@ -0,0 +1,78 @@
|
||||
import db from "../src/inc/sql.mjs";
|
||||
import queue from "../src/inc/queue.mjs";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
const __dirname = path.dirname(new URL(import.meta.url).pathname);
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '..');
|
||||
const BATCH_SIZE = 50;
|
||||
|
||||
async function backfill() {
|
||||
console.log("Starting PHash backfill...");
|
||||
|
||||
try {
|
||||
// Count total items needing backfill
|
||||
const countResult = await db`SELECT count(*) as count FROM items WHERE (phash IS NULL OR phash = '') AND active = true`;
|
||||
const total = countResult[0].count;
|
||||
console.log(`Found ${total} items to process.`);
|
||||
|
||||
let processed = 0;
|
||||
let errors = 0;
|
||||
|
||||
while (true) {
|
||||
const items = await db`
|
||||
SELECT id, dest
|
||||
FROM items
|
||||
WHERE (phash IS NULL OR phash = '')
|
||||
AND active = true
|
||||
ORDER BY id DESC
|
||||
LIMIT ${BATCH_SIZE}
|
||||
`;
|
||||
|
||||
if (items.length === 0) break;
|
||||
|
||||
for (const item of items) {
|
||||
// Correctly resolve path relative to project root
|
||||
const filePath = path.join(PROJECT_ROOT, 'public', 'b', item.dest);
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(`[SKIP] File not found: ${item.dest} (ID: ${item.id})`);
|
||||
// Mark as MISSING so we don't pick it up again
|
||||
await db`UPDATE items SET phash = 'MISSING' WHERE id = ${item.id}`;
|
||||
processed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[PROCESSING] ID ${item.id}: ${item.dest}`);
|
||||
const phash = await queue.generatePHash(filePath);
|
||||
|
||||
if (phash) {
|
||||
await db`UPDATE items SET phash = ${phash} WHERE id = ${item.id}`;
|
||||
console.log(`[SUCCESS] ID ${item.id}: Updated PHash.`);
|
||||
} else {
|
||||
console.log(`[FAILED] ID ${item.id}: Could not generate PHash.`);
|
||||
// Mark as ERROR so we don't pick it up again
|
||||
await db`UPDATE items SET phash = 'ERROR' WHERE id = ${item.id}`;
|
||||
errors++;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] ID ${item.id}:`, err);
|
||||
await db`UPDATE items SET phash = 'ERROR' WHERE id = ${item.id}`;
|
||||
errors++;
|
||||
}
|
||||
processed++;
|
||||
}
|
||||
console.log(`Progress: ${processed} completed (Remaining in batch loop...)`);
|
||||
}
|
||||
|
||||
console.log("Backfill complete!");
|
||||
process.exit(0);
|
||||
|
||||
} catch (err) {
|
||||
console.error("Fatal error:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
backfill();
|
||||
102
debug/blur_existing_thumbnails.mjs
Normal file
102
debug/blur_existing_thumbnails.mjs
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Backfill script to generate blurred thumbnails for existing NSFW items
|
||||
*
|
||||
* Run with: node debug/blur_existing_thumbnails.mjs
|
||||
*/
|
||||
|
||||
import { exec as _exec } from "child_process";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import cfg from "../src/inc/config.mjs";
|
||||
import sql from "../src/inc/sql.mjs";
|
||||
|
||||
const db = sql;
|
||||
|
||||
const exec = (cmd) => new Promise((resolve, reject) => {
|
||||
_exec(cmd, { maxBuffer: 5e3 * 1024 }, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
err.stderr = stderr;
|
||||
return reject(err);
|
||||
}
|
||||
resolve({ stdout });
|
||||
});
|
||||
});
|
||||
|
||||
async function main() {
|
||||
console.log('[BACKFILL] Starting blurred thumbnail generation for existing NSFW items...');
|
||||
|
||||
// Find all items with NSFW tag (tag_id = 2) that are active
|
||||
const nsfwItems = await db`
|
||||
SELECT DISTINCT items.id
|
||||
FROM items
|
||||
JOIN tags_assign ON tags_assign.item_id = items.id
|
||||
WHERE tags_assign.tag_id = 2
|
||||
AND items.active = true
|
||||
`;
|
||||
|
||||
console.log(`[BACKFILL] Found ${nsfwItems.length} NSFW items`);
|
||||
|
||||
const tDir = cfg.paths.t;
|
||||
console.log(`[BACKFILL] Thumbnail directory: ${tDir}`);
|
||||
|
||||
// Debug: check if directory exists and list some files
|
||||
try {
|
||||
const files = await fs.promises.readdir(tDir);
|
||||
console.log(`[BACKFILL] Directory exists, contains ${files.length} files`);
|
||||
console.log(`[BACKFILL] Sample files: ${files.slice(0, 5).join(', ')}`);
|
||||
} catch (e) {
|
||||
console.error(`[BACKFILL] ERROR: Cannot access thumbnail directory: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let processed = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
|
||||
for (const item of nsfwItems) {
|
||||
const src = path.join(tDir, `${item.id}.webp`);
|
||||
const dst = path.join(tDir, `${item.id}_blur.webp`);
|
||||
|
||||
// Check if blur already exists
|
||||
try {
|
||||
await fs.promises.access(dst);
|
||||
skipped++;
|
||||
continue; // Already exists
|
||||
} catch {
|
||||
// Doesn't exist, proceed
|
||||
}
|
||||
|
||||
// Check if source exists
|
||||
try {
|
||||
await fs.promises.access(src);
|
||||
} catch {
|
||||
console.log(`[BACKFILL] Source thumbnail missing for item ${item.id}`);
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Generate blurred thumbnail
|
||||
try {
|
||||
await exec(`magick "${src}" -blur 0x20 "${dst}"`);
|
||||
processed++;
|
||||
if (processed % 100 === 0) {
|
||||
console.log(`[BACKFILL] Progress: ${processed} processed, ${skipped} skipped, ${errors} errors`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[BACKFILL] Failed to blur item ${item.id}:`, err.message);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[BACKFILL] Complete!`);
|
||||
console.log(` Processed: ${processed}`);
|
||||
console.log(` Skipped (already exist): ${skipped}`);
|
||||
console.log(` Errors: ${errors}`);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('[BACKFILL] Fatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
95
debug/clean.mjs
Normal file
95
debug/clean.mjs
Normal file
@@ -0,0 +1,95 @@
|
||||
import cfg from "../src/inc/config.mjs";
|
||||
import db from "../src/inc/sql.mjs";
|
||||
import fs from "node:fs";
|
||||
import readline from 'node:readline/promises';
|
||||
|
||||
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
||||
|
||||
// npm run clean -- --dry-run
|
||||
const dry = !!process.argv.filter(a => a == '--dry-run').length;
|
||||
console.log(`dry run? ${dry}`);
|
||||
|
||||
const dirs = {
|
||||
b: "./public/b",
|
||||
t: "./public/t",
|
||||
tmp: "./tmp"
|
||||
};
|
||||
const files = {
|
||||
b: (await fs.promises.readdir(dirs.b)) .filter(f => f !== '.empty'),
|
||||
t: (await fs.promises.readdir(dirs.t)) .filter(f => f !== '.empty'),
|
||||
tmp: (await fs.promises.readdir(dirs.tmp)).filter(f => f !== '.empty')
|
||||
};
|
||||
const extensions = [ ...Object.values(cfg.mimes), 'mov' ];
|
||||
const count = {
|
||||
missing: { b: [], t: [] },
|
||||
invalid: { b: [], t: [] },
|
||||
spare: { b: [], t: [] },
|
||||
tmp: files.tmp
|
||||
};
|
||||
const rows = await db`select id, dest from items where active = true`;
|
||||
const f0cks = {
|
||||
b: rows.flatMap(f => f.dest),
|
||||
t: rows.flatMap(f => `${f.id}.webp`)
|
||||
};
|
||||
|
||||
// missing
|
||||
for(const row of rows) {
|
||||
if(!fs.existsSync(`${dirs.b}/${row.dest}`))
|
||||
count.missing.b.push(row.id);
|
||||
if(!fs.existsSync(`${dirs.t}/${row.id}.webp`))
|
||||
count.missing.t.push(row.id);
|
||||
}
|
||||
|
||||
// invalid
|
||||
count.invalid.b = files.b.filter(f => !extensions.includes(f.toLowerCase().split('.')[1]));
|
||||
count.invalid.t = files.t.filter(f => !f.endsWith('.webp'));
|
||||
|
||||
// spare
|
||||
for(const file of files.b)
|
||||
if(!f0cks.b.includes(file))
|
||||
count.spare.b.push(`${dirs.b}/${file}`);
|
||||
|
||||
for(const file of files.t)
|
||||
if(!f0cks.t.includes(file))
|
||||
count.spare.t.push(`${dirs.t}/${file}`);
|
||||
|
||||
// show confusing summary
|
||||
console.log(count);
|
||||
|
||||
// delete spare if --dry-run
|
||||
if(!dry) {
|
||||
let q;
|
||||
if(count.spare.b.length > 0) {
|
||||
q = (await rl.question(`delete ${count.spare.b.length} unnecessary files in ${dirs.b}? [y/N] `)) == 'y';
|
||||
if(q) {
|
||||
await Promise.all(count.spare.b.map(f => fs.promises.unlink(f)));
|
||||
console.log(`deleted ${count.spare.b.length} files`);
|
||||
}
|
||||
else
|
||||
console.log('abort...');
|
||||
}
|
||||
|
||||
if(count.spare.t.length > 0) {
|
||||
q = (await rl.question(`delete ${count.spare.t.length} unnecessary files in ${dirs.t}? [y/N] `)) == 'y';
|
||||
if(q) {
|
||||
await Promise.all(count.spare.t.map(f => fs.promises.unlink(f)));
|
||||
console.log(`deleted ${count.spare.t.length} files`);
|
||||
}
|
||||
else
|
||||
console.log('abort...');
|
||||
}
|
||||
|
||||
if(files.tmp.length > 0) {
|
||||
q = (await rl.question(`delete ${files.tmp.length} files in ${dirs.tmp}? [y/N] `)) == 'y';
|
||||
if(q) {
|
||||
await Promise.all(files.tmp.map(f => fs.promises.unlink(`${dirs.tmp}/${f}`)));
|
||||
console.log(`deleted ${files.tmp.length} files`);
|
||||
}
|
||||
else
|
||||
console.log('abort...');
|
||||
}
|
||||
}
|
||||
|
||||
// close connection
|
||||
await db.end();
|
||||
process.exit();
|
||||
104
debug/find_duplicates.mjs
Normal file
104
debug/find_duplicates.mjs
Normal file
@@ -0,0 +1,104 @@
|
||||
|
||||
import db from "../src/inc/sql.mjs";
|
||||
|
||||
const THRESHOLD = 15;
|
||||
const REQUIRED_MATCHES = 2;
|
||||
|
||||
// Hamming distance helper — operates on a single hex-encoded hash segment
|
||||
const getHammingDistance = (h1, h2) => {
|
||||
if (!h1 || !h2 || h1.length !== h2.length) return 9999;
|
||||
let distance = 0;
|
||||
for (let i = 0; i < h1.length; i += 2) {
|
||||
const v1 = parseInt(h1.substr(i, 2), 16);
|
||||
const v2 = parseInt(h2.substr(i, 2), 16);
|
||||
let xor = v1 ^ v2;
|
||||
while (xor) {
|
||||
distance += xor & 1;
|
||||
xor >>= 1;
|
||||
}
|
||||
}
|
||||
return distance;
|
||||
};
|
||||
|
||||
async function findDuplicates() {
|
||||
console.log("Fetching items...");
|
||||
|
||||
// Fetch all valid phashes
|
||||
const items = await db`
|
||||
SELECT id, phash
|
||||
FROM items
|
||||
WHERE phash IS NOT NULL
|
||||
AND phash != ''
|
||||
AND phash != 'MISSING'
|
||||
AND phash != 'ERROR'
|
||||
AND phash NOT LIKE '00000000%'
|
||||
ORDER BY id ASC
|
||||
`;
|
||||
|
||||
console.log(`Checking ${items.length} items for duplicates (Threshold: ${THRESHOLD}, Required frame matches: ${REQUIRED_MATCHES})...`);
|
||||
|
||||
const duplicates = new Map(); // Map<OriginalID, List<{id, dist}>>
|
||||
const processed = new Set();
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const current = items[i];
|
||||
|
||||
if (processed.has(current.id)) continue;
|
||||
|
||||
const matchList = [];
|
||||
|
||||
for (let j = i + 1; j < items.length; j++) {
|
||||
const compare = items[j];
|
||||
if (processed.has(compare.id)) continue;
|
||||
|
||||
// Split multi-frame hashes properly — do NOT compare the whole string
|
||||
const aHashes = current.phash.split('_');
|
||||
const bHashes = compare.phash.split('_');
|
||||
const framesToCompare = Math.min(aHashes.length, bHashes.length);
|
||||
|
||||
let matchCount = 0;
|
||||
for (let f = 0; f < framesToCompare; f++) {
|
||||
const dist = getHammingDistance(aHashes[f], bHashes[f]);
|
||||
if (dist <= THRESHOLD) matchCount++;
|
||||
}
|
||||
|
||||
const isMatch = (framesToCompare >= 3 && matchCount >= REQUIRED_MATCHES)
|
||||
|| (framesToCompare === 2 && matchCount >= 2)
|
||||
|| (framesToCompare === 1 && matchCount === 1);
|
||||
|
||||
if (isMatch) {
|
||||
const avgDist = Math.round(
|
||||
aHashes.slice(0, framesToCompare)
|
||||
.reduce((sum, h, idx) => sum + getHammingDistance(h, bHashes[idx]), 0)
|
||||
/ framesToCompare
|
||||
);
|
||||
matchList.push({ id: compare.id, dist: avgDist });
|
||||
processed.add(compare.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (matchList.length > 0) {
|
||||
duplicates.set(current.id, matchList);
|
||||
processed.add(current.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (duplicates.size === 0) {
|
||||
console.log("No duplicates found.");
|
||||
} else {
|
||||
console.log(`Found ${duplicates.size} duplicate sets:`);
|
||||
console.log("---------------------------------------------------");
|
||||
}
|
||||
|
||||
for (const [originalId, matchList] of duplicates.entries()) {
|
||||
const matchStr = matchList.map(m => `ID:${m.id} (avg-dist:${m.dist})`).join(", ");
|
||||
console.log(`Original ID: ${originalId} matches with: ${matchStr}`);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
findDuplicates().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
72
debug/fix_deleted.mjs
Normal file
72
debug/fix_deleted.mjs
Normal file
@@ -0,0 +1,72 @@
|
||||
|
||||
import db from "../src/inc/sql.mjs";
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
(async () => {
|
||||
console.log("Starting migration...");
|
||||
|
||||
// 1. Ensure column exists
|
||||
try {
|
||||
await db`select is_deleted from items limit 1`;
|
||||
console.log("Column 'is_deleted' already exists.");
|
||||
} catch (err) {
|
||||
if (err.message.includes('column "is_deleted" does not exist')) {
|
||||
console.log("Column 'is_deleted' missing. Adding it now...");
|
||||
await db`ALTER TABLE items ADD COLUMN is_deleted BOOLEAN DEFAULT FALSE`;
|
||||
console.log("Column added successfully.");
|
||||
} else {
|
||||
console.error("Unexpected error checking column:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const items = await db`select id, dest from items where active = false`;
|
||||
console.log(`Found ${items.length} inactive items.`);
|
||||
|
||||
let trashCount = 0;
|
||||
let pendingCount = 0;
|
||||
let brokenCount = 0;
|
||||
|
||||
for (const item of items) {
|
||||
try {
|
||||
await fs.access(`./deleted/b/${item.dest}`);
|
||||
// File exists in deleted, mark as is_deleted = true
|
||||
await db`update items set is_deleted = true where id = ${item.id}`;
|
||||
trashCount++;
|
||||
} catch {
|
||||
// Not in deleted, check public
|
||||
try {
|
||||
await fs.access(`./public/b/${item.dest}`);
|
||||
// In public, is_deleted = false (default)
|
||||
pendingCount++;
|
||||
} catch {
|
||||
// Not in either? Broken.
|
||||
console.log(`Item ${item.id} (${item.dest}) missing from both locations. Cleaning up...`);
|
||||
|
||||
// 2. Fix FK constraint: Check if this item is used as an avatar
|
||||
try {
|
||||
// Find a safe fallback avatar (active item)
|
||||
const fallback = await db`select id from items where active = true limit 1`;
|
||||
if (fallback.length > 0) {
|
||||
const safeId = fallback[0].id;
|
||||
const users = await db`update "user_options" set avatar = ${safeId} where avatar = ${item.id} returning user_id`;
|
||||
if (users.length > 0) {
|
||||
console.log(` > Reassigned avatar for ${users.length} users (from ${item.id} to ${safeId})`);
|
||||
}
|
||||
}
|
||||
} catch (fkErr) {
|
||||
console.error(` ! Error fixing avatar FK for ${item.id}:`, fkErr.message);
|
||||
}
|
||||
|
||||
await db`delete from items where id = ${item.id}`;
|
||||
brokenCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Migration complete.`);
|
||||
console.log(`Trash (soft-deleted): ${trashCount}`);
|
||||
console.log(`Pending: ${pendingCount}`);
|
||||
console.log(`Broken: ${brokenCount}`);
|
||||
process.exit(0);
|
||||
})();
|
||||
84
debug/recreate_hashes.mjs
Normal file
84
debug/recreate_hashes.mjs
Normal file
@@ -0,0 +1,84 @@
|
||||
import fs from 'fs';
|
||||
import crypto from 'crypto';
|
||||
import db from '../src/inc/sql.mjs';
|
||||
import path from 'path';
|
||||
|
||||
const run = async () => {
|
||||
console.log('Starting hash recreation (Production Mode - Streams)...');
|
||||
|
||||
try {
|
||||
// Fetch only necessary columns
|
||||
const items = await db`SELECT id, dest, checksum, size FROM items ORDER BY id ASC`;
|
||||
console.log(`Found ${items.length} items. Processing...`);
|
||||
|
||||
let updated = 0;
|
||||
let errors = 0;
|
||||
let skipped = 0;
|
||||
|
||||
for (const [index, item] of items.entries()) {
|
||||
const filePath = path.join('./public/b', item.dest);
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
// Silent error in logs for missing files to avoid spamming "thousands" of lines if many are missing
|
||||
// Use verbose logging if needed, but here we'll just count them.
|
||||
// Actually, precise logs are better for "production" to know what's wrong.
|
||||
console.error(`[MISSING] File not found for item ${item.id}: ${filePath}`);
|
||||
errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get file size without reading content
|
||||
const stats = await fs.promises.stat(filePath);
|
||||
const size = stats.size;
|
||||
|
||||
// Calculate hash using stream to ensure low memory usage
|
||||
const hash = await new Promise((resolve, reject) => {
|
||||
const hashStream = crypto.createHash('sha256');
|
||||
const rs = fs.createReadStream(filePath);
|
||||
|
||||
rs.on('error', reject);
|
||||
rs.on('data', chunk => hashStream.update(chunk));
|
||||
rs.on('end', () => resolve(hashStream.digest('hex')));
|
||||
});
|
||||
|
||||
if (hash !== item.checksum || size !== item.size) {
|
||||
console.log(`[UPDATE] Item ${item.id} (${index + 1}/${items.length})`);
|
||||
if (hash !== item.checksum) console.log(` - Hash: ${item.checksum} -> ${hash}`);
|
||||
if (size !== item.size) console.log(` - Size: ${item.size} -> ${size}`);
|
||||
|
||||
await db`
|
||||
UPDATE items
|
||||
SET checksum = ${hash}, size = ${size}
|
||||
WHERE id = ${item.id}
|
||||
`;
|
||||
updated++;
|
||||
} else {
|
||||
skipped++;
|
||||
}
|
||||
|
||||
// Log progress every 100 items
|
||||
if ((index + 1) % 100 === 0) {
|
||||
console.log(`Progress: ${index + 1}/${items.length} (Updated: ${updated}, Errors: ${errors})`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Processing item ${item.id}:`, err);
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Done.');
|
||||
console.log(`Total: ${items.length}`);
|
||||
console.log(`Updated: ${updated}`);
|
||||
console.log(`Skipped (No changes): ${skipped}`);
|
||||
console.log(`Errors (Missing files): ${errors}`);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Fatal error:', err);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
|
||||
run();
|
||||
87
debug/thumbnailer.mjs
Normal file
87
debug/thumbnailer.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
import sql from "../src/inc/sql.mjs";
|
||||
import fs from "fs";
|
||||
import { exec as _exec } from "child_process";
|
||||
|
||||
const exec = cmd => new Promise((resolve, reject) => {
|
||||
_exec(cmd, { maxBuffer: 5e3 * 1024 }, (err, stdout, stderr) => {
|
||||
if(err)
|
||||
return reject(err);
|
||||
if(stderr)
|
||||
console.error(stderr);
|
||||
resolve({ stdout: stdout });
|
||||
});
|
||||
});
|
||||
|
||||
const _args = process.argv.slice(2);
|
||||
const _itemid = +_args[0] || 0;
|
||||
|
||||
// Ensure temp and output directories exist
|
||||
if (!fs.existsSync('./tmp')) fs.mkdirSync('./tmp', { recursive: true });
|
||||
if (!fs.existsSync('./public/t')) fs.mkdirSync('./public/t', { recursive: true });
|
||||
|
||||
let items;
|
||||
if(_itemid > 0)
|
||||
items = await sql`select id, dest, mime, src from "items" where id = ${_itemid}`;
|
||||
else
|
||||
items = await sql`select id, dest, mime, src from "items"`;
|
||||
let count = 1;
|
||||
let total = items.length;
|
||||
|
||||
for(let item of items) {
|
||||
const itemid = item.id;
|
||||
const filename = item.dest;
|
||||
const mime = item.mime;
|
||||
const link = item.src;
|
||||
try {
|
||||
if(mime.startsWith('video/') || mime == 'image/gif') {
|
||||
const seeks = ['20%', '40%', '60%', '80%'];
|
||||
for (const seek of seeks) {
|
||||
await exec(`ffmpegthumbnailer -i./public/b/${filename} -s1024 -t ${seek} -o./tmp/${itemid}.png`);
|
||||
try {
|
||||
const { stdout } = await exec(`magick "./tmp/${itemid}.png" -colorspace Gray -format "%[fx:mean]" info:`);
|
||||
if (parseFloat(stdout.trim()) > 0.05) break;
|
||||
} catch (e) { break; }
|
||||
}
|
||||
}
|
||||
else if(mime.startsWith('image/') && mime != 'image/gif')
|
||||
await exec(`magick ./public/b/${filename}[0] ./tmp/${itemid}.png`);
|
||||
else if(mime.startsWith('audio/')) {
|
||||
if(link.match(/soundcloud/)) {
|
||||
let cover = (await exec(`yt-dlp --get-thumbnail "${link}"`)).stdout.trim();
|
||||
if(!cover.match(/default_avatar/)) {
|
||||
cover = cover.replace(/-(large|original)\./, '-t500x500.');
|
||||
try {
|
||||
await exec(`wget "${cover}" -O ./tmp/${itemid}.jpg`);
|
||||
const size = (await fs.promises.stat(`./tmp/${itemid}.jpg`)).size;
|
||||
if(size >= 0) {
|
||||
await exec(`magick ./tmp/${itemid}.jpg ./tmp/${itemid}.png`);
|
||||
await exec(`magick ./tmp/${itemid}.jpg ./public/ca/${itemid}.webp`);
|
||||
}
|
||||
} catch(err) {
|
||||
//console.log(err);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await exec(`ffmpeg -i ./public/b/${filename} -update 1 -map 0:v -map 0:1 -c copy ./tmp/${itemid}.png`);
|
||||
await exec(`magick ./tmp/${itemid}.png ./public/ca/${itemid}.webp`);
|
||||
}
|
||||
}
|
||||
else {
|
||||
await exec(`ffmpeg -i ./public/b/${filename} -update 1 -map 0:v -map 0:1 -c copy ./tmp/${itemid}.png`);
|
||||
await exec(`magick ./tmp/${itemid}.png ./public/ca/${itemid}.webp`);
|
||||
}
|
||||
}
|
||||
|
||||
await exec(`magick "./tmp/${itemid}.png" -resize "128x128^" -gravity center -crop 128x128+0+0 +repage ./public/t/${itemid}.webp`);
|
||||
await fs.promises.unlink(`./tmp/${itemid}.png`).catch(err => {});
|
||||
await fs.promises.unlink(`./tmp/${itemid}.jpg`).catch(err => {});
|
||||
} catch(err) {
|
||||
console.error(`Failed to generate thumbnail for ${itemid}:`, err.message);
|
||||
// await exec(`magick ./mugge.png ./public/t/${itemid}.webp`);
|
||||
}
|
||||
console.log(`current: ${itemid} (${count} / ${total})`);
|
||||
count++;
|
||||
}
|
||||
|
||||
console.log('Thumbnail generation complete.');
|
||||
process.exit(0);
|
||||
34
debug/trigger.mjs
Normal file
34
debug/trigger.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import { promises as fs } from "fs";
|
||||
|
||||
(async () => {
|
||||
const _args = process.argv.slice(2);
|
||||
const _e = {
|
||||
network: "console",
|
||||
message: _args.join(" "),
|
||||
args: _args.slice(1),
|
||||
channel: "console",
|
||||
user: {
|
||||
prefix: "console!console@console",
|
||||
nick: "console",
|
||||
username: "console",
|
||||
account: "console"
|
||||
},
|
||||
reply: (...args) => console.log(args),
|
||||
replyAction: (...args) => console.log(args),
|
||||
replyNotice: (...args) => console.log(args)
|
||||
};
|
||||
|
||||
const trigger = (await Promise.all((await fs.readdir("./src/inc/trigger"))
|
||||
.filter(f => f.endsWith(".mjs"))
|
||||
.map(async t => await (await import(`../src/inc/trigger/${t}`)).default())
|
||||
)).filter(t => t[0].call.test(_e.message)).map(t => ({ name: t[0].name, f: t[0].f }));
|
||||
|
||||
try {
|
||||
if(trigger.length === 0)
|
||||
return console.error("no matches");
|
||||
console.log(`triggered > ${trigger[0].name} (${_e.message})`);
|
||||
await trigger[0].f(_e);
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
})();
|
||||
110
debug/user-admin.mjs
Normal file
110
debug/user-admin.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
import db from "../src/inc/sql.mjs";
|
||||
import lib from "../src/inc/lib.mjs";
|
||||
|
||||
const usage = () => {
|
||||
console.log(`
|
||||
Usage: node user-admin.mjs <command> [args]
|
||||
|
||||
Commands:
|
||||
hash <password> - Generate a password hash
|
||||
create <user> <pass> [--admin] [--mod] - Create a new user
|
||||
delete <user> - Delete a user
|
||||
passwd <user> <new_pass> - Change a user's password
|
||||
list - List all users
|
||||
`);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length === 0) usage();
|
||||
|
||||
const cmd = args[0];
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
switch (cmd) {
|
||||
case "hash": {
|
||||
if (args.length < 2) usage();
|
||||
const hash = await lib.hash(args[1]);
|
||||
console.log(`Hash: ${hash}`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "create": {
|
||||
if (args.length < 3) usage();
|
||||
const username = args[1];
|
||||
const password = args[2];
|
||||
const isAdmin = args.includes("--admin");
|
||||
const isMod = args.includes("--mod");
|
||||
|
||||
const hash = await lib.hash(password);
|
||||
const ts = ~~(Date.now() / 1e3);
|
||||
|
||||
const existing = await db`select id from "user" where login = ${username.toLowerCase()}`;
|
||||
if (existing.length > 0) {
|
||||
console.error(`Error: User '${username}' already exists.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const newUser = await db`
|
||||
insert into "user" ("login", "user", "password", "admin", "is_moderator", "created_at")
|
||||
values (${username.toLowerCase()}, ${username}, ${hash}, ${isAdmin}, ${isMod}, to_timestamp(${ts}))
|
||||
returning id
|
||||
`;
|
||||
|
||||
const userId = newUser[0].id;
|
||||
|
||||
// Add default options
|
||||
await db`
|
||||
insert into user_options (user_id, mode, theme, avatar)
|
||||
values (${userId}, 3, 'amoled', 0)
|
||||
`;
|
||||
|
||||
console.log(`Successfully created user '${username}' (ID: ${userId})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "delete": {
|
||||
if (args.length < 2) usage();
|
||||
const username = args[1].toLowerCase();
|
||||
|
||||
const result = await db`delete from "user" where login = ${username} returning id`;
|
||||
if (result.length === 0) {
|
||||
console.error(`Error: User '${username}' not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Successfully deleted user '${username}' (ID: ${result[0].id})`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "passwd": {
|
||||
if (args.length < 3) usage();
|
||||
const username = args[1].toLowerCase();
|
||||
const newPass = args[2];
|
||||
|
||||
const hash = await lib.hash(newPass);
|
||||
const result = await db`update "user" set password = ${hash} where login = ${username} returning id`;
|
||||
|
||||
if (result.length === 0) {
|
||||
console.error(`Error: User '${username}' not found.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`Successfully updated password for user '${username}'`);
|
||||
break;
|
||||
}
|
||||
|
||||
case "list": {
|
||||
const users = await db`select id, login, "user", admin, is_moderator, created_at from "user" order by id asc`;
|
||||
console.table(users);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
usage();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Fatal error:", err);
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user