Initial init

This commit is contained in:
knight 2024-12-28 10:12:25 -05:00
commit d4d6fab753
7 changed files with 497 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
node_modules
dist
build
public/
images/
*.html
*.css
*.png
*.jpg
*.jpeg
*.gif
images.json

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
CMD ["node", "fetchImages.js"]

25
docker-compose.yml Normal file
View File

@ -0,0 +1,25 @@
version: '3'
services:
gallery:
build: .
volumes:
- ./public:/app/public
- ./images:/app/images
expose:
- "3000"
environment:
- NODE_ENV=production
restart: unless-stopped
networks:
- web
labels:
- "traefik.enable=true"
- "traefik.http.routers.gallery-secure.entrypoints=https"
- "traefik.http.routers.gallery-secure.rule=Host(`analog.uplink.tel`)"
- "traefik.http.routers.gallery-secure.tls.certresolver=http"
- "traefik.http.services.gallery.loadbalancer.server.port=3000"
networks:
web:
external: true

102
fetchImages.js Normal file
View File

@ -0,0 +1,102 @@
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { pipeline } from 'stream/promises';
const config = {
apiKey: 'w7bVb1xk3mTQg4RuAHR0000lWnv0iaJD2Rq4M0Y1SLU',
albumId: 'f6303d3d-5343-4579-a92e-c1eb37518ae7',
baseUrl: 'https://photos.ghost.tel',
outputDir: './images' // Directory to store downloaded images
};
async function downloadImage(imageUrl, fileName, apiKey) {
const response = await axios({
method: 'GET',
url: imageUrl,
responseType: 'stream',
headers: {
'x-api-key': apiKey
}
});
const outputPath = path.join(config.outputDir, fileName);
await pipeline(response.data, fs.createWriteStream(outputPath));
return outputPath;
}
async function getAlbumAssets() {
try {
// Create output directory if it doesn't exist
if (!fs.existsSync(config.outputDir)) {
fs.mkdirSync(config.outputDir, { recursive: true });
}
const url = `${config.baseUrl}/api/albums/${config.albumId}`;
console.log('Fetching album data from:', url);
const response = await axios.get(url, {
headers: {
'Accept': 'application/json',
'x-api-key': config.apiKey
}
});
if (!response.data) {
throw new Error('No data received from the API');
}
console.log('First asset metadata sample:', JSON.stringify(response.data.assets[0], null, 2));
console.log(`Found ${response.data.assets.length} assets in album`);
const images = [];
for (const asset of response.data.assets) {
const originalUrl = `${config.baseUrl}/api/assets/${asset.id}/original`;
const fileName = `${asset.id}_${asset.originalFileName || 'untitled'}`;
console.log(`Downloading: ${fileName}`);
try {
const localPath = await downloadImage(originalUrl, fileName, config.apiKey);
images.push({
id: asset.id,
originalUrl,
localPath,
name: asset.originalFileName || 'Untitled',
thumbnailUrl: `${config.baseUrl}/api/asset/thumbnail/${asset.id}`,
description: asset.exifInfo?.description || '',
metadata: {
createdAt: asset.createdAt,
fileCreatedAt: asset.fileCreatedAt,
deviceAssetId: asset.deviceAssetId,
type: asset.type
}
});
console.log(`Successfully downloaded: ${fileName}`);
} catch (downloadError) {
console.error(`Failed to download ${fileName}:`, downloadError.message);
}
}
// Save the metadata
fs.writeFileSync('./images.json', JSON.stringify(images, null, 2));
console.log(`Successfully processed ${images.length} images`);
return images;
} catch (error) {
console.error('Error fetching album assets:', error.message);
if (error.response) {
console.error('Response status:', error.response.status);
console.error('Response data:', error.response.data);
}
throw error;
}
}
// Execute the function
getAlbumAssets().catch(error => {
console.error('Failed to fetch images:', error);
process.exit(1);
});

131
generateSite.js Normal file
View File

@ -0,0 +1,131 @@
const fs = require('fs').promises;
const path = require('path');
const http = require('http');
async function generateSite() {
// Read the images data
const imagesData = JSON.parse(
await fs.readFile('images.json', 'utf-8')
);
console.log(imagesData);
// Create output directory if it doesn't exist
await fs.mkdir('public', { recursive: true });
// Copy the CSS file to public directory
await fs.copyFile('public/styles.css', 'public/styles.css').catch(err => {
console.error('Error copying CSS file:', err);
});
// Copy the favicon
await fs.copyFile('public/AnalogCameraS.png', 'public/favicon.png').catch(err => {
console.error('Error copying favicon:', err);
});
// Generate individual pages for each image
for (let i = 0; i < imagesData.length; i++) {
const image = imagesData[i];
const prevImage = imagesData[i > 0 ? i - 1 : imagesData.length - 1];
const nextImage = imagesData[(i + 1) % imagesData.length];
// Use image ID for filename
const fileName = `${image.id}.html`;
const prevFileName = `${prevImage.id}.html`;
const nextFileName = `${nextImage.id}.html`;
const html = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${image.description || `Image ${i + 1}`}</title>
<link rel="stylesheet" href="/styles.css">
<link rel="icon" type="image/png" href="/favicon.png">
</head>
<body>
<div class="image-container">
${image?.localPath || 'No image URL available'
? `<img src="${image?.localPath || 'No image URL available'}" alt="${image.metadata.description || ''}">`
: '<p>Image not available</p>'
}
${image.description ? `<p class="description">${image.description}</p>` : ''}
<div class="navigation">
<a href="${prevFileName}"></a>
<a href="${nextFileName}"></a>
</div>
</div>
</body>
</html>`;
await fs.writeFile(`public/${fileName}`, html);
}
// Update index.html to redirect to first image's ID
const indexHtml = `
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=${imagesData[0].id}.html">
</head>
<body>
Redirecting...
</body>
</html>`;
await fs.writeFile('public/index.html', indexHtml);
console.log('Site generated successfully!');
}
async function serveStaticSite(port = 3000) {
const server = http.createServer(async (req, res) => {
try {
// Convert URL to filesystem path
let filePath;
if (req.url.startsWith('/images/')) {
// Serve directly from images folder
filePath = req.url.slice(1); // Remove leading slash
} else {
// Serve from public folder
filePath = path.join('public', req.url === '/' ? 'index.html' : req.url);
// Add .html extension if no extension exists
if (!path.extname(filePath)) {
filePath += '.html';
}
}
const content = await fs.readFile(filePath);
// Set content type based on file extension
const ext = path.extname(filePath).toLowerCase();
const contentType = {
'.html': 'text/html',
'.css': 'text/css',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
}[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
} catch (error) {
console.error('Error serving file:', error);
res.writeHead(404);
res.end('Not found');
}
});
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
}
// Generate the site and then serve it
generateSite()
.then(() => serveStaticSite())
.catch(console.error);

205
package-lock.json generated Normal file
View File

@ -0,0 +1,205 @@
{
"name": "static-gallery",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "static-gallery",
"version": "1.0.0",
"dependencies": {
"axios": "^1.7.9",
"node-fetch": "^3.3.2"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.7.9",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
"integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/data-uri-to-buffer": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/fetch-blob": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "paypal",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"dependencies": {
"node-domexception": "^1.0.0",
"web-streams-polyfill": "^3.0.3"
},
"engines": {
"node": "^12.20 || >= 14.13"
}
},
"node_modules/follow-redirects": {
"version": "1.15.9",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/formdata-polyfill": {
"version": "4.0.10",
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
"license": "MIT",
"dependencies": {
"fetch-blob": "^3.1.2"
},
"engines": {
"node": ">=12.20.0"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/jimmywarting"
},
{
"type": "github",
"url": "https://paypal.me/jimmywarting"
}
],
"license": "MIT",
"engines": {
"node": ">=10.5.0"
}
},
"node_modules/node-fetch": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
"license": "MIT",
"dependencies": {
"data-uri-to-buffer": "^4.0.0",
"fetch-blob": "^3.1.4",
"formdata-polyfill": "^4.0.10"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/node-fetch"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/web-streams-polyfill": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
"license": "MIT",
"engines": {
"node": ">= 8"
}
}
}
}

13
package.json Normal file
View File

@ -0,0 +1,13 @@
{
"name": "static-gallery",
"version": "1.0.0",
"main": "fetchImages.js",
"scripts": {
"fetch": "node fetchImages.js",
"generate": "node generateSite.js"
},
"dependencies": {
"axios": "^1.7.9",
"node-fetch": "^3.3.2"
}
}