144 lines
5.1 KiB
JavaScript
144 lines
5.1 KiB
JavaScript
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import sharp from 'sharp';
|
|
import { createConnection } from './database.js';
|
|
|
|
class PictureSyncer {
|
|
constructor() {
|
|
if (PictureSyncer.instance) {
|
|
return PictureSyncer.instance;
|
|
}
|
|
this.cacheBaseDir = process.env.CACHE_LOCATION || '.';
|
|
|
|
// Track syncing state per group
|
|
this.isSyncing = new Map(); // groupName -> boolean
|
|
this.queuedSyncs = new Map(); // groupName -> { imageIds, groupName }
|
|
|
|
PictureSyncer.instance = this;
|
|
}
|
|
|
|
async syncImages(imageIds, groupName) {
|
|
// Check if already syncing this group
|
|
if (this.isSyncing.get(groupName)) {
|
|
if (this.queuedSyncs.has(groupName)) {
|
|
console.log(`🚫 Image sync for '${groupName}' already in progress and queued. Ignoring.`);
|
|
return;
|
|
}
|
|
console.log(`⏳ Image sync for '${groupName}' already in progress. Queuing.`);
|
|
this.queuedSyncs.set(groupName, { imageIds, groupName });
|
|
return;
|
|
}
|
|
|
|
await this._doSync(imageIds, groupName);
|
|
}
|
|
|
|
async _doSync(imageIds, groupName) {
|
|
this.isSyncing.set(groupName, true);
|
|
|
|
try {
|
|
await this._performSync(imageIds, groupName);
|
|
} finally {
|
|
this.isSyncing.set(groupName, false);
|
|
|
|
// Process queued sync for this group if any
|
|
if (this.queuedSyncs.has(groupName)) {
|
|
console.log(`🔄 Processing queued image sync for '${groupName}'...`);
|
|
const queued = this.queuedSyncs.get(groupName);
|
|
this.queuedSyncs.delete(groupName);
|
|
setImmediate(() => this.syncImages(queued.imageIds, queued.groupName));
|
|
}
|
|
}
|
|
}
|
|
|
|
async _performSync(imageIds, groupName) {
|
|
const groupDir = path.join(this.cacheBaseDir, 'img', groupName);
|
|
|
|
// Ensure directory exists
|
|
await fs.mkdir(groupDir, { recursive: true });
|
|
|
|
// Get existing files
|
|
let existingFiles = [];
|
|
try {
|
|
existingFiles = await fs.readdir(groupDir);
|
|
} catch (err) {
|
|
// Directory might be empty or new
|
|
}
|
|
|
|
// Filter for image files (assuming we save as {id}.avif)
|
|
const existingIds = existingFiles
|
|
.filter(f => f.endsWith('.avif'))
|
|
.map(f => parseInt(f.replace('.avif', '')));
|
|
|
|
const validIds = new Set(imageIds.filter(id => id !== null && id !== undefined));
|
|
|
|
// 1. Delete obsolete images
|
|
const toDelete = existingIds.filter(id => !validIds.has(id));
|
|
for (const id of toDelete) {
|
|
const filePath = path.join(groupDir, `${id}.avif`);
|
|
await fs.unlink(filePath);
|
|
}
|
|
if (toDelete.length > 0) {
|
|
console.log(`🗑️ Deleted ${toDelete.length} obsolete images.`);
|
|
}
|
|
|
|
// 2. Download missing images
|
|
const toDownload = imageIds.filter(id => id !== null && id !== undefined && !existingIds.includes(id));
|
|
|
|
if (toDownload.length > 0) {
|
|
console.log(`📥 Downloading ${toDownload.length} new images for group '${groupName}'...`);
|
|
await this._downloadImages(toDownload, groupDir);
|
|
} else {
|
|
console.log(`✅ No new images to download for group '${groupName}'.`);
|
|
}
|
|
}
|
|
|
|
async _downloadImages(ids, dir) {
|
|
let pool;
|
|
try {
|
|
pool = await createConnection();
|
|
|
|
// Process in chunks to avoid huge queries
|
|
const chunkSize = 50;
|
|
for (let i = 0; i < ids.length; i += chunkSize) {
|
|
const chunk = ids.slice(i, i + chunkSize);
|
|
const list = chunk.join(',');
|
|
|
|
const result = await pool.request().query(`
|
|
SELECT kBild, bBild
|
|
FROM tBild
|
|
WHERE kBild IN (${list})
|
|
`);
|
|
|
|
for (const record of result.recordset) {
|
|
if (record.bBild) {
|
|
const filePath = path.join(dir, `${record.kBild}.avif`);
|
|
// Resize to 130x130 and convert to AVIF using sharp
|
|
await sharp(record.bBild)
|
|
.resize(130, 130, {
|
|
fit: 'cover',
|
|
position: 'center'
|
|
})
|
|
.avif({ quality: 80 })
|
|
.toFile(filePath);
|
|
}
|
|
}
|
|
const processed = Math.min(i + chunkSize, ids.length);
|
|
if (processed === ids.length) {
|
|
console.log(`✅ Processed ${processed}/${ids.length} images.`);
|
|
} else {
|
|
console.log(`⏳ Processed ${processed}/${ids.length} images...`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('❌ Error downloading images:', err);
|
|
} finally {
|
|
if (pool) {
|
|
await pool.close();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const instance = new PictureSyncer();
|
|
export default instance;
|