From d4d6fab753e6694fc7ccfcc9740612050b012b14 Mon Sep 17 00:00:00 2001 From: knight Date: Sat, 28 Dec 2024 10:12:25 -0500 Subject: [PATCH] Initial init --- .gitignore | 12 +++ Dockerfile | 9 ++ docker-compose.yml | 25 ++++++ fetchImages.js | 102 ++++++++++++++++++++++ generateSite.js | 131 +++++++++++++++++++++++++++++ package-lock.json | 205 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 +++ 7 files changed, 497 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 fetchImages.js create mode 100644 generateSite.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..39e8b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +node_modules +dist +build +public/ +images/ +*.html +*.css +*.png +*.jpg +*.jpeg +*.gif +images.json \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..1b18911 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM node:18-alpine + +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm install + +COPY . . + +CMD ["node", "fetchImages.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..94de1c3 --- /dev/null +++ b/docker-compose.yml @@ -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 \ No newline at end of file diff --git a/fetchImages.js b/fetchImages.js new file mode 100644 index 0000000..b7f367b --- /dev/null +++ b/fetchImages.js @@ -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); +}); diff --git a/generateSite.js b/generateSite.js new file mode 100644 index 0000000..2e3a236 --- /dev/null +++ b/generateSite.js @@ -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 = ` + + + + + + ${image.description || `Image ${i + 1}`} + + + + +
+ ${image?.localPath || 'No image URL available' + ? `${image.metadata.description || ''}` + : '

Image not available

' + } + ${image.description ? `

${image.description}

` : ''} + +
+ +`; + + await fs.writeFile(`public/${fileName}`, html); + } + + // Update index.html to redirect to first image's ID + const indexHtml = ` + + + + + + + Redirecting... + +`; + + 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); + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7766928 --- /dev/null +++ b/package-lock.json @@ -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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..668d298 --- /dev/null +++ b/package.json @@ -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" + } +}