Skip to content

Commit

Permalink
feat: Artist Squares
Browse files Browse the repository at this point in the history
  • Loading branch information
cryptofyre committed Sep 14, 2024
1 parent 50008a3 commit 2e1a60b
Show file tree
Hide file tree
Showing 3 changed files with 442 additions and 31 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"ffmpeg": "^0.0.4",
"fluent-ffmpeg": "^2.1.3",
"node-fetch": "^3.3.2",
"sharp": "^0.33.5",
"winston": "^3.14.0"
},
"packageManager": "[email protected]"
Expand Down
167 changes: 167 additions & 0 deletions server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const require = createRequire(import.meta.url);
import crypto from 'crypto';
import Queue from 'bull';
import path from 'path';
import sharp from 'sharp';
import fsSync from 'fs/promises'; // Use the promises API for async operations
import fs from 'fs';
import { fileURLToPath } from 'url';
Expand All @@ -19,10 +20,15 @@ const __dirname = path.dirname(__filename);
const app = express();
const port = 3000;
const cacheDir = path.join(__dirname, 'cache');
const artistSquaresDir = path.join(__dirname, 'cache', 'artist-squares');

// Ensure cache directory exists
fsSync.mkdir(cacheDir, { recursive: true }).catch(err => logger.error(`Error creating cache directory: ${err.message}`));

// Ensure artist squares directory exists
fsSync.mkdir(artistSquaresDir, { recursive: true }).catch(err => logger.error(`Error creating artist squares directory: ${err.message}`));


// Configure logging
const logger = winston.createLogger({
level: 'info',
Expand Down Expand Up @@ -236,6 +242,167 @@ app.get('/artwork/:key.gif', (req, res) => {
}
});

const artistSquareQueue = new Queue('artistSquareQueue', {
redis: {
host: '10.10.79.15',
port: 6379
},
limiter: {
max: 5,
duration: 1000
}
});

artistSquareQueue.on('error', (error) => {
logger.error(`Artist Square Queue error: ${error.message}`);
});

artistSquareQueue.on('failed', (job, err) => {
logger.error(`Artist Square Job ${job.id} failed: ${err.message}`);
});

// Function to generate a unique key for artist square
const generateArtistSquareKey = (imageUrls) => {
const combinedUrls = imageUrls.sort().join('');
return crypto.createHash('md5').update(combinedUrls).digest('hex');
};

// Function to create artist square
const createArtistSquare = async (imageUrls) => {
const size = 500; // Size of the final square image
const images = await Promise.all(imageUrls.map(async url => {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
return sharp(Buffer.from(arrayBuffer))
.resize(size, size, { fit: 'cover' })
.toBuffer();
}));

let composite;
const background = { r: 0, g: 0, b: 0, alpha: 1 };

if (images.length === 2) {
composite = sharp({
create: { width: size, height: size, channels: 4, background }
})
.composite([
{ input: await sharp(images[0]).resize(size / 2, size).toBuffer(), top: 0, left: 0 },
{ input: await sharp(images[1]).resize(size / 2, size).toBuffer(), top: 0, left: size / 2 }
]);
} else if (images.length === 3) {
composite = sharp({
create: { width: size, height: size, channels: 4, background }
})
.composite([
// Primary artist at the top
{ input: await sharp(images[0]).resize(size, size / 2).toBuffer(), top: 0, left: 0 },
// Secondary artists split at the bottom
{ input: await sharp(images[1]).resize(size / 2, size / 2).toBuffer(), top: size / 2, left: 0 },
{ input: await sharp(images[2]).resize(size / 2, size / 2).toBuffer(), top: size / 2, left: size / 2 }
]);
} else if (images.length === 4) {
composite = sharp({
create: { width: size, height: size, channels: 4, background }
})
.composite([
{ input: await sharp(images[0]).resize(size / 2, size / 2).toBuffer(), top: 0, left: 0 },
{ input: await sharp(images[1]).resize(size / 2, size / 2).toBuffer(), top: 0, left: size / 2 },
{ input: await sharp(images[2]).resize(size / 2, size / 2).toBuffer(), top: size / 2, left: 0 },
{ input: await sharp(images[3]).resize(size / 2, size / 2).toBuffer(), top: size / 2, left: size / 2 }
]);
} else {
throw new Error('Invalid number of images. Must be 2, 3, or 4.');
}

return composite.jpeg().toBuffer();
};

// Process job queue for artist squares
artistSquareQueue.process(3, async (job) => {
const { imageUrls, key, jobId } = job.data;
const squarePath = path.join(artistSquaresDir, `${key}.jpg`);

logger.info(`Job ${jobId}: Starting artist square job for ${imageUrls.length} images`);

if (fs.existsSync(squarePath)) {
logger.info(`Job ${jobId}: Artist square already exists for key ${key}`);
return squarePath;
}

try {
const squareBuffer = await createArtistSquare(imageUrls);
await fsSync.writeFile(squarePath, squareBuffer);
logger.info(`Job ${jobId}: Artist square created for key ${key}`);
return squarePath;
} catch (error) {
logger.error(`Job ${jobId}: Error creating artist square - ${error.message}`);
throw new Error('Error creating artist square');
}
});


// Artist Square Post
app.post('/artwork/artist-square', express.json(), async (req, res) => {
const imageUrls = req.body.imageUrls;

if (!Array.isArray(imageUrls) || imageUrls.length < 2 || imageUrls.length > 4) {
return res.status(400).send('Invalid input. Provide 2-4 image URLs.');
}

const key = generateArtistSquareKey(imageUrls);
const squarePath = path.join(artistSquaresDir, `${key}.jpg`);
const jobId = crypto.randomBytes(4).toString('hex');

if (fs.existsSync(squarePath)) {
logger.info(`Job ${jobId}: Artist square already exists for key ${key}`);
return res.status(200).json({ key, message: 'Artist square already exists', url: `https://art.cider.sh/artwork/artist-square/${key}.jpg` });
}

try {
const job = await artistSquareQueue.add({ imageUrls, key, jobId });
logger.info(`Job ${jobId}: Added to the artist square queue`);

job.finished().then(() => {
// Set cache headers for Cloudflare
const sevenDaysInSeconds = 7 * 24 * 60 * 60;
const expiresDate = new Date(Date.now() + sevenDaysInSeconds * 1000).toUTCString();

res.setHeader('Cache-Control', `public, max-age=${sevenDaysInSeconds}`);
res.setHeader('Expires', expiresDate);

logger.info(`Job ${jobId}: Artist square processing completed`);
res.status(202).json({ key, message: 'Artist square is being processed', url: `https://art.cider.sh/artwork/artist-square/${key}.jpg` });
}).catch((err) => {
logger.error(`Job ${jobId}: Error finishing processing - ${err.message}`);
res.status(500).send('Error processing the artist square');
});
} catch (error) {
logger.error(`Job ${jobId}: Error adding to the queue - ${error.message}`);
res.status(500).send('Error adding to the queue');
}
});

// New route to retrieve artist square
app.get('/artwork/artist-square/:key.jpg', (req, res) => {
const key = req.params.key;
const squarePath = path.join(artistSquaresDir, `${key}.jpg`);

if (fs.existsSync(squarePath)) {
// Set cache headers for Cloudflare
const sevenDaysInSeconds = 7 * 24 * 60 * 60;
const expiresDate = new Date(Date.now() + sevenDaysInSeconds * 1000).toUTCString();

res.setHeader('Cache-Control', `public, max-age=${sevenDaysInSeconds}`);
res.setHeader('Expires', expiresDate);

logger.info(`Retrieving artist square for key ${key}`);
return res.sendFile(squarePath);
} else {
logger.warn(`Artist square not found for key ${key}`);
return res.status(404).send('Artist square not found');
}
});

app.listen(port, () => {
logger.info(`Server is running on http://art.cider.sh/`);
});
Loading

0 comments on commit 2e1a60b

Please sign in to comment.